Skip to main content

nodejs/
host.rs

1//! The JavaScript object heap and runtime, reached from fusevm through
2//! registered builtins (`register_builtin`) and the strict numeric hook.
3//!
4//! node-js owns no VM and no JIT: the compiler lowers JS to `fusevm::Chunk`, and
5//! every JS-specific operation the VM can't do natively is a builtin call that
6//! lands here. Local variables live in `Rc<RefCell>` environments chained
7//! parent-to-child, so a nested function/closure captures its enclosing scope by
8//! reference.
9//!
10//! Value representation:
11//!   - immediate: `Value::Float` (every JS number — one IEEE-754 f64 type),
12//!     `Value::Bool` (true/false), `Value::Undef` (undefined);
13//!   - heap `Value::Obj(u32)` handles: string, array, object, function,
14//!     builtin-namespace, and the canonical `null` — the reference types.
15
16use fusevm::{Chunk, NumOp, VMResult, Value, VM};
17use indexmap::IndexMap;
18use std::cell::RefCell;
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::rc::Rc;
22use std::sync::mpsc::{Receiver, Sender};
23use std::time::{Duration, Instant};
24
25/// A unit of I/O work handed from a background I/O thread to the main-thread
26/// event loop. It is a boxed closure so `host.rs` stays agnostic of `net`/`http`:
27/// the I/O thread captures only plain `Send` data (bytes, ids, `TcpStream`s) and
28/// the closure runs the JS-touching dispatch on the main thread (where the
29/// thread-local host lives). I/O threads NEVER touch the host directly.
30pub type IoTask = Box<dyn FnOnce() -> Result<(), String> + Send>;
31
32/// Builtin ids emitted by the compiler and registered on every VM. The compiler
33/// (`compiler.rs`) and the handler table (`builtins.rs::install`) must agree on
34/// these exactly.
35pub mod ops {
36    pub const GETLOCAL: u16 = 1; // [name] -> value (scope-chain read)
37    pub const SETLOCAL: u16 = 2; // [name, value] -> value (assignment)
38    pub const DECLARE: u16 = 3; // [name, value] -> value (let/const/var into current scope)
39    pub const DELNAME: u16 = 4; // [name]
40    pub const GETATTR: u16 = 5; // [recv, name] -> value (member .x)
41    pub const SETATTR: u16 = 6; // [recv, name, value]
42    pub const GETITEM: u16 = 7; // [recv, idx] -> value (computed [k])
43    pub const SETITEM: u16 = 8; // [recv, idx, value]
44    pub const DELITEM: u16 = 9; // [recv, idx] -> Bool (delete obj[k])
45    pub const MKSTR: u16 = 10; // [parts...] -> str (concat)
46    pub const MKARR: u16 = 11; // [items...] -> array
47    pub const MKOBJ: u16 = 12; // [tag,k,v,...] -> object (tag 1 = ...spread of k)
48    pub const CALL: u16 = 13; // [name, args...] -> resolve name & call
49    pub const CALL_METHOD: u16 = 14; // [recv, name, args...]
50    pub const CALL_VALUE: u16 = 15; // [callable, args...]
51    pub const NEW: u16 = 16; // [ctor, args...] -> instance
52    pub const TRUTHY: u16 = 17; // [v] -> Bool (JS truthiness)
53    pub const TOSTR: u16 = 18; // [v] -> str via String(v)
54    pub const MKFUNC: u16 = 19; // [func_id, defaults...] -> closure
55    pub const GETITER: u16 = 20; // [iterable] -> iterator (left on stack)
56    pub const FORITER: u16 = 21; // peek iterator -> pushes value + Bool(has_next)
57    pub const FORIN_KEYS: u16 = 22; // [obj] -> array of enumerable keys
58    pub const CONTAINS: u16 = 23; // [key, obj] -> Bool (`in`)
59    pub const SIG_RETURN: u16 = 24; // [v] -> return v from the function
60    pub const BINOP: u16 = 25; // [tag, a, b] -> bitwise/shift result (JS int32 semantics)
61    pub const UNARY: u16 = 26; // [tag, v] -> unary +/~ result
62    pub const STRICT_EQ: u16 = 27; // [a, b] -> Bool (===)
63    pub const LOOSE_EQ: u16 = 28; // [a, b] -> Bool (==)
64    pub const TYPEOF: u16 = 29; // [v] -> str
65    pub const LOAD_NULL: u16 = 30; // [] -> the canonical null
66    pub const THROW: u16 = 31; // [v] -> throw
67    pub const TRY: u16 = 32; // [try_id] -> run a try/catch/finally block
68    pub const NULLISH: u16 = 33; // [v] -> Bool (v is null or undefined)
69    pub const UNPACK: u16 = 34; // [iterable, count, star] -> pushes count values
70    pub const BUILD_ARGS: u16 = 35; // [tag,val,...] -> flat array (tag 1 = ...spread)
71    pub const THIS: u16 = 36; // [] -> current `this`
72    pub const INSTANCEOF: u16 = 37; // [a, b] -> Bool
73    pub const DELPROP_NAME: u16 = 38; // [recv, name] -> Bool (delete obj.name)
74    pub const APPLY: u16 = 39; // [callable, argsArray] -> call with spread args
75    pub const APPLY_METHOD: u16 = 40; // [recv, name, argsArray] -> method call with spread
76    pub const OBJ_REST: u16 = 41; // [obj, excludedKeys] -> object of remaining keys
77    pub const DIV: u16 = 42; // [a, b] -> IEEE `a / b` (JS: x/0 = ±Infinity, 0/0 = NaN)
78    pub const MKCLASS: u16 = 43; // [parent_or_undef, ctor_fn] -> class constructor value
79    pub const DEF_MEMBER: u16 = 44; // [class, name, kind, is_static, fn] -> define method/get/set
80    pub const SUPER_CALL: u16 = 45; // [args...] -> invoke parent ctor on `this`, then init fields
81    pub const SUPER_GET: u16 = 46; // [name] -> resolve `super.name` (method up the parent chain)
82    pub const YIELD: u16 = 47; // [v] -> suspend the running generator, yield v
83    pub const PROPKEY: u16 = 48; // [v] -> property-key string (Symbol -> internal key, else String())
84    pub const NEW_TARGET: u16 = 49; // [] -> the current frame's new.target (undefined if not `new`)
85    pub const DEF_FIELD: u16 = 50; // [class, name, thunk] -> register an instance field initializer
86    pub const AWAIT: u16 = 51; // [v] -> await v (suspend the async coroutine until v settles)
87    pub const DEF_ACCESSOR: u16 = 52; // [obj, name, kind, fn] -> install a getter/setter on obj
88    pub const DBG_LINE: u16 = 53; // [line] -> DAP statement marker (debug only)
89    pub const MKBIGINT: u16 = 54; // [decimal_str] -> heap BigInt value
90    pub const MKREGEX: u16 = 55; // [pattern, flags] -> heap RegExp value
91    pub const TAG_TMPL: u16 = 56; // [tag, cooked..., raw..., n, values...] -> tagged-template call
92    pub const GET_ASYNC_ITER: u16 = 57; // [iterable] -> async iterator (for-await-of)
93    pub const ASYNC_STEP: u16 = 58; // [asyncIterator] -> Promise of {value, done}
94    pub const NUM_STEP: u16 = 59; // [tag(±1), old] -> pushes ToNumeric(old), returns old±1 (type-preserving; BigInt-aware ++/--)
95    pub const ITER_CLOSE: u16 = 60; // [iterator] -> close it (for-of break: run a generator's finally / call .return())
96    pub const TYPEOF_NAME: u16 = 61; // [name] -> str; `typeof <ident>` reads the name WITHOUT throwing (unbound -> "undefined")
97    pub const SIG_BREAK: u16 = 62; // [label|""] -> raise a Break signal and halt the chunk (break out of a `try`)
98    pub const SIG_CONTINUE: u16 = 63; // [label|""] -> raise a Continue signal and halt the chunk
99    pub const SIG_UNWIND: u16 = 64; // [tag] -> 0 none / 1 break here / 2 continue here; halts the chunk to propagate
100    pub const PUSH_SCOPE: u16 = 65; // [] -> enter a fresh block scope (`let`/`const` live here)
101    pub const POP_SCOPE: u16 = 66; // [] -> leave the innermost block scope
102    pub const COPY_SCOPE: u16 = 67; // [] -> replace the innermost block scope with a COPY (per-iteration `let`)
103    pub const DECLARE_VAR: u16 = 68; // [name, value] -> declare at FUNCTION scope, ignoring block scopes (`var`)
104    pub const NAMED_EVAL: u16 = 69; // [key, kind, fn] -> fn; SetFunctionName for a COMPUTED key (kind picks the `get `/`set ` prefix)
105    pub const POW: u16 = 70; // [a, b] -> JS `a ** b` (NOT native `Op::Pow`: IEEE pow answers 1 for `(-1) ** Infinity` and `1 ** NaN`)
106    pub const DECLARE_CONST: u16 = 71; // [name, value] -> value; like DECLARE but the binding is IMMUTABLE (`const`)
107    pub const MARK_HOLE: u16 = 72; // [arr, index] -> arr; record an ELIDED array-literal element
108    pub const SETLOCAL_STRICT: u16 = 73; // [name, value] -> value; like SETLOCAL but an UNRESOLVABLE name throws ReferenceError instead of creating a global (strict-mode PutValue)
109    pub const HOIST_VAR: u16 = 74; // [name] -> create the `var` binding as undefined IF ABSENT (hoisting)
110    pub const FORIN_ALIVE: u16 = 75; // [obj, key] -> Bool; is `key` STILL an enumerable property of `obj`? (a `for-in` body may have deleted it)
111    pub const HOIST_TDZ: u16 = 76; // [name] -> declare `name` in the CURRENT scope as UNINITIALIZED (the `let`/`const`/`class` temporal dead zone)
112    pub const NEW_SPREAD: u16 = 77; // [ctor, argsArray] -> instance; `new C(...xs)`, where the argument list is built at run time
113    pub const SUPER_CALL_SPREAD: u16 = 78; // [argsArray] -> invoke the parent ctor with a run-time argument list (`super(...xs)`)
114}
115
116/// Per-call-site callee SOURCE TEXT, for the `TypeError` a failed call raises.
117///
118/// V8 reports the callee the way the source wrote it — `z.f is not a function`,
119/// not `f is not a function` — by re-printing the AST of the call it was
120/// evaluating. The text is therefore a static property of the SITE, so the
121/// compiler records it once per call op and nothing is carried at run time: the
122/// table is consulted only on the error path.
123///
124/// Keyed by the chunk's `op_hash` (which `ChunkBuilder::build` computes anyway)
125/// paired with the op index. `op_hash` covers the op vector but not the name
126/// pool, so two chunks that compile to the same ops with different names share a
127/// key; the consequence is confined to which receiver text an error message
128/// names, never to what a program does.
129mod call_sites {
130    use std::cell::RefCell;
131
132    thread_local! {
133        pub(super) static SITES: RefCell<rustc_hash::FxHashMap<(u64, usize), String>> =
134            RefCell::new(rustc_hash::FxHashMap::default());
135    }
136
137    /// Record every call site of a freshly built chunk.
138    pub fn register(op_hash: u64, sites: Vec<(usize, String)>) {
139        if sites.is_empty() {
140            return;
141        }
142        SITES.with(|m| {
143            let mut m = m.borrow_mut();
144            for (ip, text) in sites {
145                m.insert((op_hash, ip), text);
146            }
147        });
148    }
149
150    /// The callee text recorded for the op at `ip` of the chunk `op_hash`.
151    pub fn text(op_hash: u64, ip: usize) -> Option<String> {
152        SITES.with(|m| m.borrow().get(&(op_hash, ip)).cloned())
153    }
154
155    pub fn clear() {
156        SITES.with(|m| m.borrow_mut().clear());
157    }
158}
159
160pub use call_sites::{clear as clear_call_sites, register as register_call_sites};
161
162/// How many `for…of` / `yield*` iterators are parked on the VM stack at each
163/// `yield` op, recorded by the compiler the same way callee text is.
164///
165/// A `.return()`/`.throw()` injected at a suspension point halts the generator's
166/// chunk outright, which jumps past the loop exits that would have closed those
167/// iterators — so the halt path has to close them itself, and this is how it
168/// knows how many are there and that they are the top of the stack.
169mod yield_sites {
170    use std::cell::RefCell;
171
172    thread_local! {
173        pub(super) static DEPTHS: RefCell<rustc_hash::FxHashMap<(u64, usize), usize>> =
174            RefCell::new(rustc_hash::FxHashMap::default());
175    }
176
177    pub fn register(op_hash: u64, sites: Vec<(usize, usize)>) {
178        if sites.is_empty() {
179            return;
180        }
181        DEPTHS.with(|m| {
182            let mut m = m.borrow_mut();
183            for (ip, depth) in sites {
184                m.insert((op_hash, ip), depth);
185            }
186        });
187    }
188
189    pub fn depth(op_hash: u64, ip: usize) -> usize {
190        DEPTHS.with(|m| m.borrow().get(&(op_hash, ip)).copied().unwrap_or(0))
191    }
192
193    pub fn clear() {
194        DEPTHS.with(|m| m.borrow_mut().clear());
195    }
196}
197
198pub use yield_sites::{clear as clear_yield_sites, register as register_yield_sites};
199
200/// Every call site and yield site registered so far, as the cache stores them:
201/// `(op_hash, ip)` keys with their recorded value.
202///
203/// The tables are built by the COMPILER (`finish_chunk`), so a run that loads a
204/// program from the bytecode cache never fills them — and everything that reads
205/// them silently degrades: a generator's parked `for…of`/`yield*` iterators are
206/// not closed on an injected `.return()`, so their `finally` never runs, and a
207/// `TypeError` loses the callee's source text. Storing them alongside the
208/// program is what makes a cache hit behave like a compile.
209pub type SiteTables = (Vec<((u64, usize), String)>, Vec<((u64, usize), usize)>);
210
211/// Snapshot both registries.
212pub fn site_tables() -> SiteTables {
213    let calls = call_sites::SITES.with(|m| {
214        m.borrow()
215            .iter()
216            .map(|(k, v)| (*k, v.clone()))
217            .collect::<Vec<_>>()
218    });
219    let yields =
220        yield_sites::DEPTHS.with(|m| m.borrow().iter().map(|(k, v)| (*k, *v)).collect::<Vec<_>>());
221    (calls, yields)
222}
223
224/// Put a snapshot back — what a cache hit does in place of compiling.
225pub fn restore_site_tables(t: &SiteTables) {
226    call_sites::SITES.with(|m| {
227        let mut m = m.borrow_mut();
228        for (k, v) in &t.0 {
229            m.insert(*k, v.clone());
230        }
231    });
232    yield_sites::DEPTHS.with(|m| {
233        let mut m = m.borrow_mut();
234        for (k, v) in &t.1 {
235            m.insert(*k, *v);
236        }
237    });
238}
239
240/// The number of loop iterators parked on the stack at the op currently
241/// executing, for the abrupt-completion close in `b_yield`.
242pub fn parked_iters(vm: &fusevm::VM) -> usize {
243    yield_sites::depth(vm.chunk.op_hash, vm.ip.saturating_sub(1))
244}
245
246/// Rewrite a `<subject> is not a function` / `is not a constructor` message with
247/// the SOURCE TEXT of the callee at the currently executing op, as V8 does.
248///
249/// `subject` is what the raising code named — the method name, or the callee's
250/// rendered value. The message's own subject must END WITH it, which is the
251/// guard that keeps an unrelated error raised deeper inside a native method from
252/// being relabelled with this call's text. (A native dispatcher may prefix its
253/// own receiver word, e.g. `map.get is not a function`, so the whole subject is
254/// replaced rather than trimmed by length.)
255///
256/// Returns the message unchanged when no site was recorded, so a shape the
257/// printer declines to print keeps the old wording rather than an invented one.
258/// The source text recorded for the op currently executing, if any. `vm.ip` has
259/// already advanced past it.
260pub fn call_site_text(vm: &fusevm::VM) -> Option<String> {
261    call_sites::text(vm.chunk.op_hash, vm.ip.saturating_sub(1))
262}
263
264pub fn name_call_site(vm: &fusevm::VM, subject: &str, msg: String) -> String {
265    for tail in [
266        " is not a function",
267        " is not a constructor",
268        " is not iterable",
269    ] {
270        let Some(head) = msg.strip_suffix(tail) else {
271            continue;
272        };
273        // The prefix is the error class (`TypeError: `); the rest is the subject.
274        // The FIRST separator, not the last: a rendered VALUE can contain one —
275        // `{ a: 1 } is not iterable` split at the last `": "` left the subject
276        // as `1 }`, which matched nothing and silently skipped the rename.
277        let (prefix, found) = match head.find(": ") {
278            Some(i) => (&head[..i + 2], &head[i + 2..]),
279            None => ("", head),
280        };
281        if !found.ends_with(subject) {
282            return msg;
283        }
284        // `vm.ip` has already advanced past the op being executed.
285        let Some(text) = call_sites::text(vm.chunk.op_hash, vm.ip.saturating_sub(1)) else {
286            return msg;
287        };
288        return format!("{prefix}{text}{tail}");
289    }
290    msg
291}
292
293/// `SIG_UNWIND` scope tags: what the emitting site is nested in.
294pub mod unwind {
295    /// No enclosing loop in this chunk — any pending signal propagates outward.
296    pub const NO_LOOP: &str = "";
297    /// An enclosing UNLABELED loop in this chunk.
298    pub const PLAIN_LOOP: &str = "\u{0}";
299    /// `SIG_UNWIND` result codes.
300    pub const NONE: i64 = 0;
301    pub const BREAK: i64 = 1;
302    pub const CONTINUE: i64 = 2;
303}
304
305/// `DEF_MEMBER` member-kind tags.
306pub mod member {
307    pub const METHOD: i64 = 0;
308    pub const GET: i64 = 1;
309    pub const SET: i64 = 2;
310    /// A static FIELD (`static x = 1`), which is a data property of the
311    /// constructor rather than a method. Only distinguished from `METHOD` for a
312    /// PRIVATE name, where the declaration must install the private element
313    /// without tripping the brand check an ordinary write to `#x` gets — and
314    /// where node's brand-check message words a field differently from a method.
315    pub const STATIC_FIELD: i64 = 3;
316}
317
318/// Bitwise/shift op tags carried by `ops::BINOP` (JS ToInt32/ToUint32 rules).
319pub mod binop {
320    pub const BITAND: i64 = 0;
321    pub const BITOR: i64 = 1;
322    pub const BITXOR: i64 = 2;
323    pub const SHL: i64 = 3;
324    pub const SHR: i64 = 4;
325    pub const USHR: i64 = 5;
326}
327
328/// Unary op tags carried by `ops::UNARY`.
329pub mod unop {
330    pub const POS: i64 = 0; // unary +
331    pub const BITNOT: i64 = 1; // ~
332}
333
334// ── heap objects ───────────────────────────────────────────────────────────
335
336/// A compiled function template: parameter shape + body chunk. Shared by every
337/// closure created from the same function/arrow.
338#[derive(Clone, serde::Serialize, serde::Deserialize)]
339pub struct FuncDef {
340    pub name: String,
341    /// Parameter binding templates (destructuring lowered by the compiler into
342    /// the body prologue; here we only track the simple arg slots).
343    pub params: Vec<ParamSlot>,
344    pub chunk: Chunk,
345    pub is_arrow: bool,
346    /// True for a `function*`/`*method`/generator arrow: calling it builds a
347    /// suspended generator instead of running the body.
348    pub is_generator: bool,
349    /// True for an `async` function/method/arrow: calling it drives a coroutine
350    /// and returns a Promise; `await` inside suspends via the same yielder.
351    pub is_async: bool,
352    /// True when the function body (or the enclosing program) is strict. A
353    /// SLOPPY function called with no receiver gets the GLOBAL object as
354    /// `this`; a strict one keeps `undefined` (10.2.1.2 OrdinaryCallBindThis).
355    #[serde(default)]
356    pub strict: bool,
357    /// True for a MethodDefinition (`{ m(){} }`, a class method/accessor). A
358    /// non-generator method is not a constructor, so it owns no `prototype`.
359    #[serde(default)]
360    pub is_method: bool,
361    /// True for a NAMED function *expression* (`const f = function fact(n) {…}`):
362    /// the closure gets an extra environment binding its own name to itself, so
363    /// the body can recurse through that name even when the outer binding differs.
364    #[serde(default)]
365    pub self_name: bool,
366}
367
368/// One parameter slot. `name` is the simple bound name; a destructuring pattern
369/// is lowered to a synthetic `.arg{i}` name plus body prologue code.
370#[derive(Clone, serde::Serialize, serde::Deserialize)]
371pub struct ParamSlot {
372    pub name: String,
373    /// True for the `...rest` collector.
374    pub rest: bool,
375    /// True if this slot has a default expression (applied in the body prologue).
376    pub has_default: bool,
377}
378
379/// A compiled `try`/`catch`/`finally` block. Bodies are bare chunks run in the
380/// current scope.
381#[derive(Clone, serde::Serialize, serde::Deserialize)]
382pub struct TryDef {
383    pub block: Chunk,
384    /// `(catch_param_name, catch_body)`.
385    pub handler: Option<(Option<String>, Chunk)>,
386    pub finalizer: Option<Chunk>,
387}
388
389/// A live closure value.
390#[derive(Clone)]
391pub struct FuncVal {
392    pub def_id: usize,
393    /// Captured lexical environment (enclosing scope chain), for free vars.
394    pub env: Option<Env>,
395    /// `this` captured at definition time (arrow functions).
396    pub this: Option<Value>,
397    pub is_arrow: bool,
398    /// The owning class name for a method (drives `super` resolution). `None` for
399    /// plain functions/arrows.
400    pub home_class: Option<String>,
401    /// Whether that method is a STATIC one. `super.x` resolves against a
402    /// different object in each case — the parent constructor for a static
403    /// method, the parent's prototype for an instance method — and the class
404    /// name alone cannot tell them apart, since both carry the same one.
405    pub home_static: bool,
406    /// The object literal a shorthand method was defined in, for `super` inside
407    /// it. A class method resolves `super` through `home_class` instead; this
408    /// is the `[[HomeObject]]` an ordinary `{ m() { super.x } }` needs, and
409    /// without it there was nothing to resolve against.
410    pub home_object: Option<Value>,
411}
412
413/// A heap object.
414#[derive(Clone)]
415pub enum JsObj {
416    Str(String),
417    Array(Vec<Value>),
418    Object(IndexMap<String, Value>),
419    Func(FuncVal),
420    /// A first-class reference to a builtin function or namespace
421    /// (`console.log`, `Math`, `parseInt`).
422    Builtin(String),
423    /// A bound method value (`obj.method` captured then called): dispatches
424    /// through `call_method(recv, name, args)` when invoked.
425    BoundMethod {
426        recv: Value,
427        name: String,
428    },
429    /// The single canonical `null`.
430    Null,
431    /// A live iterator over a sequence, with a cursor.
432    Iter {
433        items: Vec<Value>,
434        idx: usize,
435    },
436    /// A bound function (`fn.bind(thisArg, ...preargs)`).
437    BoundFunc {
438        target: Value,
439        this: Value,
440        args: Vec<Value>,
441    },
442    /// A class constructor value: the runtime object produced by a `class`.
443    Class(ClassVal),
444    /// A `Symbol` — a unique property key. `registered` marks a `Symbol.for`
445    /// key (shared) vs a fresh `Symbol()`.
446    Symbol {
447        desc: Option<String>,
448        id: u64,
449    },
450    /// A `Map` (or `WeakMap` when `weak`): insertion-ordered key→value entries.
451    Map {
452        entries: IndexMap<MapKey, (Value, Value)>,
453        weak: bool,
454    },
455    /// A `Set` (or `WeakSet` when `weak`): insertion-ordered unique values.
456    Set {
457        entries: IndexMap<MapKey, Value>,
458        weak: bool,
459    },
460    /// A live generator, backed by a stackful `corosensei` coroutine in
461    /// `JsHost.generators`.
462    Generator {
463        id: u32,
464    },
465    /// A Promise, backed by a `PromiseCell` in `JsHost.promises`.
466    Promise {
467        id: u32,
468    },
469    /// An arbitrary-precision `BigInt` (`typeof === "bigint"`).
470    BigInt(num_bigint::BigInt),
471    /// A compiled regular expression (`/pat/flags` or `new RegExp(...)`).
472    RegExp(Box<RegExpObj>),
473    /// A `Proxy`: every essential internal method is diverted to `handler`'s
474    /// traps (see `crate::proxy`). `revoked` is set by the thunk
475    /// `Proxy.revocable` hands back, after which every operation throws.
476    Proxy {
477        target: Value,
478        handler: Value,
479        revoked: bool,
480    },
481}
482
483/// Which variant a heap object is, carrying none of its contents.
484///
485/// Property access has to pick a branch by variant, but the code inside a branch
486/// re-enters the host (`bound_method`, `lookup_chain`, `invoke`), so it cannot
487/// hold a `&JsObj` borrow across the match. The way out used to be
488/// `h.get(v).cloned()` — which deep-copies the entire backing store (a whole
489/// `Vec<Value>`, `IndexMap`, or `String`) just to read its tag. That made one
490/// property read O(len) and any loop over a collection O(n^2). This type is the
491/// same discriminant with nothing attached, so the probe is O(1) and each branch
492/// re-borrows for only the one field it actually needs.
493/// The well-known symbols node-js actually honors. `Symbol.<name>` is the
494/// interned symbol `@@Symbol.<name>`, and using it as a property key stores
495/// under the sentinel string `@@<name>` (`property_key`) so the internal
496/// lookups (`@@iterator`, `@@toPrimitive`, …) can find it without a symbol
497/// table walk. Symbols V8 defines but node-js does not act on are deliberately
498/// absent: a symbol that reads back while the operator it names ignores it would
499/// be a silent fake. `hasInstance` is listed because `instance_of` consults it.
500pub const WELL_KNOWN_SYMBOLS: &[&str] = &[
501    "iterator",
502    "asyncIterator",
503    "toPrimitive",
504    "toStringTag",
505    "hasInstance",
506    // Nine more the table was missing entirely, so `Symbol.species` and friends
507    // read `undefined` and no protocol keyed on them could be expressed.
508    "species",
509    "isConcatSpreadable",
510    "match",
511    "matchAll",
512    "replace",
513    "search",
514    "split",
515    "unscopables",
516    "dispose",
517    "asyncDispose",
518];
519
520/// Whether the internal key `k` came from a SYMBOL used as a property key
521/// (`@@sym:<id>`, or a well-known `@@iterator`), as opposed to one of node-js's
522/// hidden slots (`@@native`, `@@bytes`, `@@ms`, `@@kind`, …). Only the former
523/// is an observable JavaScript property.
524pub fn is_symbol_key(k: &str) -> bool {
525    match k.strip_prefix("@@") {
526        Some(rest) => rest
527            .strip_prefix("sym:")
528            .map(|i| i.parse::<u64>().is_ok())
529            .unwrap_or_else(|| WELL_KNOWN_SYMBOLS.contains(&rest)),
530        None => false,
531    }
532}
533
534#[derive(Clone, Copy, PartialEq, Eq, Debug)]
535pub enum ObjKind {
536    Str,
537    Array,
538    Object,
539    Func,
540    Builtin,
541    BoundMethod,
542    Null,
543    Iter,
544    BoundFunc,
545    Class,
546    Symbol,
547    Map,
548    Set,
549    Generator,
550    Promise,
551    BigInt,
552    RegExp,
553    Proxy,
554}
555
556impl JsObj {
557    /// This object's variant, without touching its contents.
558    pub fn kind(&self) -> ObjKind {
559        match self {
560            JsObj::Str(_) => ObjKind::Str,
561            JsObj::Array(_) => ObjKind::Array,
562            JsObj::Object(_) => ObjKind::Object,
563            JsObj::Func(_) => ObjKind::Func,
564            JsObj::Builtin(_) => ObjKind::Builtin,
565            JsObj::BoundMethod { .. } => ObjKind::BoundMethod,
566            JsObj::Null => ObjKind::Null,
567            JsObj::Iter { .. } => ObjKind::Iter,
568            JsObj::BoundFunc { .. } => ObjKind::BoundFunc,
569            JsObj::Class(_) => ObjKind::Class,
570            JsObj::Symbol { .. } => ObjKind::Symbol,
571            JsObj::Map { .. } => ObjKind::Map,
572            JsObj::Set { .. } => ObjKind::Set,
573            JsObj::Generator { .. } => ObjKind::Generator,
574            JsObj::Promise { .. } => ObjKind::Promise,
575            JsObj::BigInt(_) => ObjKind::BigInt,
576            JsObj::RegExp(_) => ObjKind::RegExp,
577            JsObj::Proxy { .. } => ObjKind::Proxy,
578        }
579    }
580}
581
582/// A `RegExp` object: the compiled `fancy_regex::Regex` plus the JS-visible
583/// source, flag booleans, and the mutable `lastIndex` cursor (used by `g`/`y`
584/// matching). fancy-regex adds lookaround + backreferences on top of the Rust
585/// `regex` fast path, so the JS grammar node-js can accept is a near-superset.
586#[derive(Clone)]
587pub struct RegExpObj {
588    /// The translated regex. Construction of a pattern fancy-regex still cannot
589    /// express (documented in BUGS.md) throws at `RegExp` build time, so a live
590    /// `RegExpObj` always holds a compiled engine.
591    ///
592    /// Shared (`Rc`) rather than owned, because a regex LITERAL builds a fresh
593    /// `RegExpObj` on every evaluation — it has to, since `lastIndex` is
594    /// per-object mutable state — while the compiled engine behind it is
595    /// immutable and identical every time. See `regexp::compiled`.
596    pub re: std::rc::Rc<fancy_regex::Regex>,
597    pub source: String,
598    pub flags: String,
599    pub global: bool,
600    pub ignore_case: bool,
601    pub multiline: bool,
602    pub dot_all: bool,
603    pub sticky: bool,
604    pub unicode: bool,
605    /// `lastIndex`, in UTF-16 code units; advanced by `exec`/`test` under the
606    /// `g`/`y` flags. The newtype keeps it from being confused with the regex
607    /// engine's byte offsets, which are the same shape and differ off the BMP.
608    pub last_index: crate::utf16::U16Index,
609}
610
611/// A Promise's settled state and pending reactions.
612pub struct PromiseCell {
613    pub state: PromiseState,
614    pub value: Value,
615    /// Reactions registered while still pending; drained (as microtasks) on
616    /// settle.
617    pub reactions: Vec<PromiseReaction>,
618    /// True once a rejection has been observed by a handler (`.then`/`.catch`),
619    /// so the loop doesn't report it as unhandled.
620    pub handled: bool,
621}
622
623/// A pending Promise reaction: a user `.then` (JS handlers + a result promise) or
624/// a native continuation (Promise chaining / async `await` resumption).
625pub enum PromiseReaction {
626    Js {
627        on_ful: Value,
628        on_rej: Value,
629        result: Value,
630    },
631    Native(Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>),
632}
633
634#[derive(Default, Clone, Copy, PartialEq, Eq)]
635pub enum PromiseState {
636    #[default]
637    Pending,
638    Fulfilled,
639    Rejected,
640}
641
642/// A live class constructor. The prototype object (holding instance methods) and
643/// the static-side own properties live on the heap; `parent` is the superclass
644/// constructor value (`None` for a base class).
645#[derive(Clone)]
646pub struct ClassVal {
647    pub name: String,
648    /// The constructor function value (a `JsObj::Func`), or `None` for a class
649    /// with only a synthesized default constructor.
650    pub ctor: Option<Value>,
651    pub parent: Option<Value>,
652    /// `C.prototype` — the object instances delegate to.
653    pub proto: Value,
654    /// Static own properties (static methods/fields), plus `name`/`prototype`.
655    pub statics: IndexMap<String, Value>,
656    /// Instance field initializers: `(name, thunk_fn, name_anon_init)`, run
657    /// per-instance after `super()` (or at construction start for a base class).
658    /// `name_anon_init` records the SYNTACTIC fact that the initializer was an
659    /// anonymous function definition, so 15.7.10 NamedEvaluation applies to its
660    /// result — it cannot be re-derived at run time (a field initialised from an
661    /// already-anonymous function held elsewhere must not be renamed).
662    pub fields: Vec<(String, Value, bool)>,
663}
664
665/// The result of resolving `super.name`: a getter to invoke (accessor property)
666/// or a directly-usable value (method / data property).
667pub enum SuperRef {
668    Getter(Value),
669    Data(Value),
670}
671
672/// A `Map`/`Set` key under SameValueZero: `NaN` collapses to one key, `-0` and
673/// `+0` are the same key, primitives compare by value, objects by heap identity.
674#[derive(Clone, PartialEq, Eq, Hash)]
675pub enum MapKey {
676    Undef,
677    Null,
678    Bool(bool),
679    /// f64 bit pattern with `NaN` canonicalized and `-0` normalized to `+0`.
680    Num(u64),
681    /// A `BigInt` key, by its decimal string (SameValueZero: `1n` is one key).
682    Big(String),
683    Str(String),
684    /// Heap identity (objects, arrays, functions, symbols).
685    Ref(u32),
686}
687
688// ── environments ─────────────────────────────────────────────────────────────
689
690/// The map behind a scope. Hashing these with `FxHash` instead of the default
691/// was measured SLOWER, not faster — fib went 652ms to 1086ms and a 5M-iteration
692/// counting loop 1894ms to 2381ms on the same machine — so the default stands.
693pub type VarMap = IndexMap<String, Value>;
694
695/// A local-variable environment, shared (by `Rc`) between a frame and any nested
696/// function that captures it.
697pub struct EnvData {
698    pub vars: VarMap,
699    /// The names in `vars` that were declared `const`, so an assignment to one
700    /// throws (16.1.3 / 8.5.2 — an immutable binding rejects SetMutableBinding).
701    ///
702    /// A separate set rather than a flag inside `VarMap`'s value, because
703    /// `set_name` is a hot path — the common case is an env with NO consts,
704    /// where `is_empty()` settles it without hashing the name a second time.
705    pub consts: rustc_hash::FxHashSet<String>,
706    pub parent: Option<Env>,
707}
708pub type Env = Rc<RefCell<EnvData>>;
709
710/// An accessor property: `(getter, setter)`, either optional.
711pub type Accessor = (Option<Value>, Option<Value>);
712
713/// Prefix of the hidden property-map entry that reserves an accessor's slot in
714/// own-key insertion order (see `set_accessor`).
715pub const ORD_MARKER: &str = "@@ord:";
716
717/// The three ECMAScript own-property attributes. `PropAttrs::default()` is the
718/// all-true shape a plain `o.k = v` assignment produces, which is why only
719/// deviations need storing.
720#[derive(Clone, Copy, Debug, PartialEq, Eq)]
721pub struct PropAttrs {
722    pub writable: bool,
723    pub enumerable: bool,
724    pub configurable: bool,
725}
726
727impl Default for PropAttrs {
728    fn default() -> Self {
729        PropAttrs {
730            writable: true,
731            enumerable: true,
732            configurable: true,
733        }
734    }
735}
736
737impl PropAttrs {
738    /// The attribute shape V8 gives an internal-but-inspectable slot such as
739    /// `Error.prototype.message`, `err.stack` or a `Buffer`'s view metadata:
740    /// readable and replaceable, but never enumerated.
741    pub const HIDDEN: PropAttrs = PropAttrs {
742        writable: true,
743        enumerable: false,
744        configurable: true,
745    };
746}
747
748fn new_env(parent: Option<Env>) -> Env {
749    Rc::new(RefCell::new(EnvData {
750        vars: VarMap::default(),
751        consts: rustc_hash::FxHashSet::default(),
752        parent,
753    }))
754}
755
756/// A fresh empty scope chained under `parent`.
757pub fn child_env(parent: Env) -> Env {
758    new_env(Some(parent))
759}
760
761/// One function activation.
762pub struct Frame {
763    pub env: Env,
764    /// The env this activation started in — the FUNCTION scope. `var` and hoisted
765    /// function declarations bind here no matter how many block scopes are open.
766    pub base_env: Env,
767    pub this_obj: Option<Value>,
768    /// `new.target` for this activation (the constructor when invoked via `new`).
769    pub new_target: Option<Value>,
770    /// The class value owning the running method (drives `super`); `None` outside
771    /// a class method/constructor.
772    pub home_class: Option<Value>,
773    /// Whether the running method is a static one — see `FuncVal::home_static`.
774    pub home_static: bool,
775    /// The object literal owning the running method — see
776    /// `FuncVal::home_object`.
777    pub home_object: Option<Value>,
778    /// Whether the code in this activation is strict. A write the object
779    /// refuses is a silent no-op in sloppy mode and a `TypeError` here, so the
780    /// ASSIGNMENT SITE decides — not the object being written to.
781    pub strict: bool,
782    /// Source line the frame is currently executing (updated by the DAP line hook
783    /// under `--dap`; stays 0 on ordinary runs).
784    pub line: u32,
785    /// The function name that owns this frame, for the DAP `stackTrace`; `None`
786    /// for the module frame and anonymous activations.
787    pub owner: Option<String>,
788    /// True ONLY for the program's module frame. A generator/async body runs on a
789    /// coroutine whose swapped-in context holds just ITS OWN frame, so the frame
790    /// COUNT cannot tell "module scope" from "coroutine body scope" — without this
791    /// flag every top-level `let`/`var` in such a body declared a GLOBAL, shared
792    /// across concurrent activations of the same function.
793    pub is_module: bool,
794    /// Whether this activation's `this` is bound yet — see [`ThisState`].
795    pub this_state: ThisState,
796}
797
798/// The `[[ThisBindingStatus]]` of a function environment (9.1.1.3), as far as it
799/// is observable: only a DERIVED class constructor starts with `this`
800/// uninitialized, and only `super()` binds it.
801///
802/// The instance is still allocated up front (`construct_class`), so the
803/// state is what makes it unreachable until then: `this` before `super()`, a
804/// second `super()`, and returning without one are each the error node raises
805/// rather than a silent write to the pre-allocated object.
806#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
807pub enum ThisState {
808    /// Every other activation: `this` is whatever was passed in.
809    #[default]
810    Plain,
811    /// A derived constructor before `super()` has returned.
812    Pending,
813    /// A derived constructor after `super()`.
814    Bound,
815}
816
817/// A non-local control signal. `Break`/`Continue` carry the optional loop label
818/// and are only raised when the target loop lives in an ENCLOSING chunk (a
819/// `break` inside a `try` block, which the host runs as its own chunk); a
820/// same-chunk `break` is a plain compiler-resolved jump.
821#[derive(Clone)]
822pub enum Signal {
823    Return(Value),
824    Break(Option<String>),
825    Continue(Option<String>),
826}
827
828/// The JavaScript runtime.
829pub struct JsHost {
830    heap: Vec<JsObj>,
831    /// Function templates, indexed by def id.
832    pub funcs: Vec<FuncDef>,
833    /// try/catch/finally block templates, indexed by try id.
834    pub tries: Vec<TryDef>,
835    /// Module-level (global) names.
836    globals: VarMap,
837    /// The one uninitialized-binding marker, allocated on first use. See
838    /// [`JsHost::tdz_marker`].
839    tdz: Option<Value>,
840    /// Module-top-level names still in their temporal dead zone. Kept out of
841    /// `globals` so the marker is never reachable as `globalThis.<name>`.
842    tdz_globals: rustc_hash::FxHashSet<String>,
843    /// Top-level `const` names (a module frame declares into `globals`), so an
844    /// assignment to one throws the same way a block-scoped `const` does.
845    global_consts: rustc_hash::FxHashSet<String>,
846    /// The frame stack (bottom = module).
847    frames: Vec<Frame>,
848    /// The program's top-level scope — the scope runtime-compiled source runs in
849    /// (`new Function`, indirect `eval`, `vm.runInThisContext`; see
850    /// `run_chunk_in_global_scope`), as opposed to whatever function frame
851    /// happens to be executing when that source is compiled.
852    ///
853    /// Held as its own field rather than read off `frames[0]` because a coroutine
854    /// body runs with `frames` SWAPPED for its own one-frame context
855    /// (`install_gen_ctx`), so the bottom frame is not the top-level frame there.
856    ///
857    /// Note this is node-js's ONE top-level scope. Node distinguishes the global
858    /// scope from a CommonJS module's scope (a module body is a wrapper
859    /// function), so in Node a file's top-level `var` is invisible to dynamic
860    /// code; here the entry file is evaluated with Script semantics, so it stays
861    /// visible. That is the same entry-file-is-a-Script divergence `BUGS.md`
862    /// records for top-level `return`, not a separate one — and `node -e`, which
863    /// really is a Script, matches Node exactly.
864    global_env: Env,
865    pub error: Option<String>,
866    /// The in-flight thrown value, if any (JS `throw`).
867    pub exc: Option<Value>,
868    pub signal: Option<Signal>,
869    /// Promises that settled REJECTED this tick. Drained at each microtask
870    /// checkpoint: any still without a handler is an unhandled rejection.
871    pub pending_rejections: Vec<u32>,
872    /// `process.on(event, fn)` listeners, by event name.
873    pub process_listeners: IndexMap<String, Vec<ProcListener>>,
874    /// The canonical `null` handle (allocated once).
875    null_val: Value,
876    /// `[[Prototype]]` link per heap object, by heap index. Absent = default
877    /// (`Object.prototype` for objects, `null` for the root).
878    protos: HashMap<u32, Value>,
879    /// Heap objects whose `[[Prototype]]` is *explicitly* null — via
880    /// `Object.create(null)` or `Object.setPrototypeOf(o, null)`. Distinct from a
881    /// bare `{}` (absent from `protos` but conceptually `Object.prototype`), which
882    /// is why `Object.create(null) instanceof Object` can read `false`.
883    null_proto_objs: HashSet<u32>,
884    /// Own properties of function objects (functions are objects in JS): a live
885    /// closure's `name`/`prototype`/static-ish members. Keyed by heap index.
886    fn_props: HashMap<u32, IndexMap<String, Value>>,
887    /// Accessor (getter/setter) properties per owning object, by heap index then
888    /// key: `(get, set)`. Class `get x()`/`set x()` install here on the prototype.
889    accessors: HashMap<u32, IndexMap<String, Accessor>>,
890    /// Own-property attributes that deviate from the plain-assignment default
891    /// (`{writable, enumerable, configurable}` all true), by heap index then key.
892    /// Only non-default entries are stored, so an ordinary object costs nothing;
893    /// `prop_attrs` returns the default for any key absent here. This is what
894    /// makes `Object.defineProperty(o, k, {enumerable: false})` invisible to
895    /// `Object.keys`/`for-in`/`JSON.stringify` while `getOwnPropertyNames` still
896    /// reports it, and what hides `Error`'s `message`/`stack` the way V8 does.
897    prop_attrs: HashMap<u32, IndexMap<String, PropAttrs>>,
898    /// Heap objects sealed against new properties by `Object.preventExtensions`,
899    /// `Object.seal` or `Object.freeze`.
900    non_extensible: HashSet<u32>,
901    /// Private names (`#m`) declared as a METHOD or accessor rather than as a
902    /// field, for the brand-check error text: node distinguishes `Receiver must
903    /// be an instance of class C` (a private method or accessor) from `Cannot
904    /// read private member #x …` (a private field). Which class is answered by
905    /// the running method's home class, not by this set, so two classes
906    /// declaring the same private method name stay exact.
907    private_methods: HashSet<String>,
908    /// The ELIDED element positions of each array, by heap index. Absent (the
909    /// overwhelmingly common case) means the array is dense.
910    ///
911    /// A hole is deliberately NOT a `Value` variant. A sentinel value would have
912    /// to be mapped back to `undefined` at every element read in the runtime, and
913    /// a single missed read would leak an un-nameable value into user code — a
914    /// worse failure than storing `undefined` and losing the distinction. Keeping
915    /// the marker OUTSIDE the value domain makes that leak structurally
916    /// impossible: the element vector still holds a perfectly ordinary
917    /// `Value::Undef` at a hole, so any code path that has not been taught about
918    /// holes degrades to exactly the pre-existing behaviour (a visible
919    /// `undefined`) instead of producing something unrepresentable.
920    ///
921    /// Sized like the array it describes in the worst case (`new Array(n)` marks
922    /// every index), which is the same order as the `Vec<Value>` already paid for
923    /// that array — so it cannot turn a working allocation into an OOM.
924    array_holes: HashMap<u32, rustc_hash::FxHashSet<usize>>,
925    /// See `take_super_replacement`.
926    super_replacement: Option<Value>,
927    /// Set by `run_class_ctor` for the one call that follows: the next user
928    /// function activation is a derived constructor and starts `Pending`.
929    derived_ctor_next: bool,
930    /// Whether the entry script's top-level `var`s bind to its own scope rather
931    /// than to the globals map — the CommonJS wrapper Node puts every file in.
932    module_scope: bool,
933    /// User-assigned static properties on a builtin namespace/constructor, keyed
934    /// by namespace name then property (`Error` → `prepareStackTrace`,
935    /// `stackTraceLimit`). Each bare `Error` reference allocates a fresh
936    /// `Builtin` handle, so these cannot live in `fn_props` (which is per-heap-
937    /// index); this stable side table lets `Error.prepareStackTrace = fn` persist.
938    builtin_statics: HashMap<String, IndexMap<String, Value>>,
939    /// The shared well-known `Object.prototype` object (chain root for objects).
940    object_proto: Value,
941    /// Class name of each class `prototype` object, by heap index — lets an
942    /// instance recover its constructor name (for `util.inspect` prefix and
943    /// `obj.constructor.name`).
944    proto_class: HashMap<u32, Value>,
945    /// Class constructor values by name, so a running method's `home_class` name
946    /// resolves to its class value (for `super`).
947    class_registry: HashMap<String, Value>,
948    /// Well-known prototype objects for the builtin error constructors, by name.
949    error_protos: HashMap<String, Value>,
950    /// The template object of each tagged-template SITE, keyed by the chunk that
951    /// holds the site and the site's ordinal within its compilation.
952    ///
953    /// GetTemplateObject (13.2.8.4) caches by Parse Node, so a site evaluated
954    /// twice hands back the SAME object: ``const t = () => tag`x`;`` makes
955    /// `t() === t()` true, and a tag that memoizes on the strings array — the
956    /// documented reason the object is cached, and how `lit-html` and `graphql`
957    /// avoid re-parsing — saw a fresh array every call here. Two sites with
958    /// identical text are still distinct objects, which the chunk hash plus the
959    /// ordinal keep apart.
960    template_objects: HashMap<(u64, u64), Value>,
961    /// Real prototype *objects* for the builtin exotics whose instances need a
962    /// genuine `[[Prototype]]` link (`Buffer`, `Uint8Array`). Most builtin
963    /// prototypes are `Builtin("<Ctor>.prototype")` thunk namespaces, which
964    /// cannot appear on a prototype chain and report `typeof "function"`.
965    native_protos: HashMap<String, Value>,
966    /// `Symbol.for` registry: description → symbol value.
967    symbol_registry: HashMap<String, Value>,
968    /// Monotonic id source for fresh `Symbol()` values.
969    next_symbol: u64,
970    /// Every live symbol by its id, so a `@@sym:<id>` property key can be
971    /// turned back into the symbol VALUE for `Object.getOwnPropertySymbols`.
972    symbols_by_id: HashMap<u64, Value>,
973    /// Well-known symbol ids (`Symbol.iterator` …) to their ECMAScript name.
974    /// Identity is by id, not description, so a user `Symbol("Symbol.iterator")`
975    /// is a distinct key.
976    well_known_ids: HashMap<u64, String>,
977    /// Suspended generator coroutines, indexed by `JsObj::Generator.id`.
978    generators: Vec<GenCell>,
979    /// Promise cells, indexed by `JsObj::Promise.id`.
980    promises: Vec<PromiseCell>,
981    /// Whether the loop is part-way through draining the microtask queue, so a
982    /// `nextTick` queued by one of them waits for the round to finish. See
983    /// `next_microtask`.
984    draining_micro: bool,
985    /// `process.nextTick` callbacks (drained before promise microtasks).
986    pub nextticks: std::collections::VecDeque<Task>,
987    /// Promise-reaction / `queueMicrotask` microtasks.
988    pub microtasks: std::collections::VecDeque<Task>,
989    /// `setTimeout`/`setInterval`/`setImmediate` macrotasks.
990    pub macrotasks: Vec<Timer>,
991    /// Monotonic timer-id source.
992    next_timer: u64,
993    /// Cloned by I/O worker threads to post `IoTask`s back to the main-thread
994    /// event loop. Kept alive for the host's lifetime so the loop's `recv` never
995    /// sees a spurious `Disconnected` while a server is running.
996    io_tx: Sender<IoTask>,
997    /// Owned by the event loop (taken out for the blocking `recv`). Receives the
998    /// `IoTask`s posted by I/O threads.
999    io_rx: Option<Receiver<IoTask>>,
1000    /// Ref-count of "things keeping the process alive": open listeners, live
1001    /// sockets, ref'd handles. The loop exits only when this is `0` AND both task
1002    /// queues are empty. A pure script never touches it, so it exits exactly as
1003    /// before.
1004    open_handles: usize,
1005    /// In-process output sink. When `Some`, everything the program writes to
1006    /// stdout/stderr is appended here instead of reaching the process streams —
1007    /// what an embedder (a TUI that owns the terminal) needs so a `console.log`
1008    /// cannot corrupt its display. `None` (the default) is the ordinary
1009    /// standalone `node` behaviour: writes go straight to the real streams.
1010    ///
1011    /// Bytes, not `String`: a program may legitimately write output that is not
1012    /// valid UTF-8 (`process.stdout.write(Buffer.from([0xff]))`), and a `String`
1013    /// buffer can only hold the lossy `U+FFFD` transcription of it.
1014    capture: Option<Vec<u8>>,
1015    /// `process.exitCode`: the code the process exits with when the event loop
1016    /// drains, or `None` while unset. Separate from an explicit
1017    /// `process.exit(n)`, which exits immediately with `n`.
1018    pub exit_code: Option<i32>,
1019    /// Whether the `exit` event has already been emitted, so the `process.exit`
1020    /// path and the end-of-loop path cannot both fire it (Node's `_exiting`).
1021    pub exiting: bool,
1022    /// The one `globalThis` object. It has to be a singleton: `globalThis` is an
1023    /// identity in JS, so `globalThis === globalThis` is `true` and a property
1024    /// written through one read is visible through the next. Minting a fresh
1025    /// object per read made both false.
1026    global_obj: Value,
1027}
1028
1029/// One `process.on`/`process.once` registration. `once` is not decoration: a
1030/// `once` listener must be UNREGISTERED before it runs, so a second `emit` of
1031/// the same event does not reach it. Treating `once` as an alias of `on` made
1032/// `process.once('e', f); process.emit('e'); process.emit('e')` call `f` twice
1033/// and leave it in `process.listeners('e')` — node v26.7.0 calls it once and
1034/// reports zero listeners afterwards.
1035#[derive(Clone)]
1036pub struct ProcListener {
1037    pub f: Value,
1038    pub once: bool,
1039}
1040
1041/// A queued unit of work: either a JS callback invocation (`queueMicrotask`,
1042/// `nextTick`, timer body) or a native step (Promise reaction / async resume).
1043pub enum Task {
1044    Js { cb: Value, args: Vec<Value> },
1045    Native(Box<dyn FnOnce() -> Result<(), String>>),
1046}
1047
1048impl Task {
1049    fn run(self) -> Result<(), String> {
1050        match self {
1051            Task::Js { cb, args } => invoke(&cb, args, None).map(|_| ()),
1052            Task::Native(f) => f(),
1053        }
1054    }
1055}
1056
1057/// A scheduled macrotask (`setTimeout`/`setInterval`/`setImmediate`). Ordering
1058/// is by `(delay, seq)` — a deterministic virtual clock, never wall time.
1059pub struct Timer {
1060    pub id: u64,
1061    pub delay: f64,
1062    pub seq: u64,
1063    pub callback: Value,
1064    pub args: Vec<Value>,
1065    pub cancelled: bool,
1066    /// Repeat period in ms for a `setInterval` timer; `None` for the one-shot
1067    /// `setTimeout`/`setImmediate`. A repeating timer is re-armed with a fresh
1068    /// deadline each time it fires, so it keeps the loop alive indefinitely —
1069    /// exactly like Node, where `setInterval` runs until cleared.
1070    pub interval: Option<f64>,
1071    /// Node's `ref`/`unref` handle bit. Only a *referenced* pending timer keeps
1072    /// the event loop alive; an unref'd one still fires while the loop happens
1073    /// to be alive for another reason, but never holds it open by itself.
1074    pub refed: bool,
1075    /// Real wall-clock deadline (`now + delay`), used only on the real-clock
1076    /// path (an open handle or a pending interval). On the pure virtual clock
1077    /// this is ignored.
1078    pub deadline: Instant,
1079}
1080
1081/// One suspended generator. `coro` is `None` only while actively running (taken
1082/// out across `Coroutine::resume`); `ctx` holds its volatile execution context
1083/// (frames/signal/error/exc) while suspended.
1084struct GenCell {
1085    coro: Option<corosensei::Coroutine<Value, Value, Result<Value, String>>>,
1086    /// Raw pointer to the coroutine body's `Yielder`, published on entry (same
1087    /// thread → valid for the body's life). Read by `yield` to suspend.
1088    yielder: *const (),
1089    ctx: GenContext,
1090    done: bool,
1091    /// True once the body has been resumed at least once (so it is suspended at a
1092    /// `yield`). `.return()`/`.throw()` only unwind a *started* generator.
1093    started: bool,
1094    /// A completion injected by `.return(v)` / `.throw(e)`: consumed by the next
1095    /// `yield` resume so the body unwinds (running any pending `finally`).
1096    inject: Option<GenInject>,
1097    /// True for an `async function*` body, where `await` AND `yield` share one
1098    /// coroutine yielder: `await` wraps its operand in an await marker so the
1099    /// driver can tell an internal suspension from a real yield.
1100    async_gen: bool,
1101    /// `[[AsyncGeneratorQueue]]` — pending requests as
1102    /// `(completion, step promise id)`. ECMA-262 27.6.3.6 keeps this queue so
1103    /// overlapping requests resume the body ONE AT A TIME and settle in request
1104    /// order; without it a second request issued before the first settles races
1105    /// past it and the results arrive swapped. `.next`, `.return` AND `.throw`
1106    /// all enqueue — a `.return()` that skipped the queue would terminate the
1107    /// body while an earlier `.next()` was still suspended on an `await`, and
1108    /// that `.next()` would then wrongly report `{done: true}`.
1109    queue: std::collections::VecDeque<(GenReq, u32)>,
1110    /// True while a queued request is being driven.
1111    running: bool,
1112    /// The [`stack_floor`] that applies while this generator's body is running.
1113    ///
1114    /// A corosensei coroutine executes on its OWN mmap'd stack, so the address
1115    /// range the thread's pthread record describes says nothing about how much
1116    /// room the body has left. Recorded from the coroutine's `Stack::limit()` at
1117    /// construction and swapped in around every resume; without it the guard
1118    /// compared a coroutine stack pointer against the main stack's floor and
1119    /// (depending on where mmap landed) either fired immediately or never.
1120    stack_floor: usize,
1121}
1122
1123/// A forced completion pushed into a suspended generator by `.return()`/`.throw()`.
1124enum GenInject {
1125    Return(Value),
1126    Throw(Value),
1127}
1128
1129/// One queued `[[AsyncGeneratorQueue]]` request. ECMA-262 27.6.3.6
1130/// `AsyncGeneratorEnqueue` records a *completion*, not just a sent value, which
1131/// is why `.return()` and `.throw()` queue behind pending `.next()` calls
1132/// instead of unwinding the body on the spot.
1133#[derive(Clone)]
1134pub enum GenReq {
1135    /// `.next(v)` — resume normally with `v`.
1136    Next(Value),
1137    /// `.return(v)` — resume with a forced return completion.
1138    Return(Value),
1139    /// `.throw(e)` — resume with a forced throw completion.
1140    Throw(Value),
1141}
1142
1143/// The mutable "execution registers" swapped at every generator resume/suspend
1144/// boundary so a suspended generator's half-finished frame/signal state never
1145/// leaks into the resuming caller. The heap, function/class tables and globals
1146/// are shared and never swapped.
1147#[derive(Default)]
1148struct GenContext {
1149    frames: Vec<Frame>,
1150    error: Option<String>,
1151    exc: Option<Value>,
1152    signal: Option<Signal>,
1153}
1154
1155thread_local! {
1156    /// Id of the generator whose body is currently executing, or `None` at the
1157    /// root. `yield` suspends this generator.
1158    static CUR_GEN: std::cell::Cell<Option<u32>> = const { std::cell::Cell::new(None) };
1159}
1160
1161thread_local! {
1162    static HOST: RefCell<JsHost> = RefCell::new(JsHost::new());
1163}
1164
1165/// Run `f` with mutable access to the thread-local host.
1166pub fn with_host<R>(f: impl FnOnce(&mut JsHost) -> R) -> R {
1167    HOST.with(|h| f(&mut h.borrow_mut()))
1168}
1169
1170/// Reset the host to a clean slate (fresh module frame).
1171pub fn reset_host() {
1172    with_host(|h| *h = JsHost::new());
1173    // Drop any cached module handles / factory closure — they index the old heap.
1174    crate::module::reset();
1175}
1176
1177impl Default for JsHost {
1178    fn default() -> Self {
1179        Self::new()
1180    }
1181}
1182
1183impl JsHost {
1184    pub fn new() -> JsHost {
1185        let global_env = new_env(None);
1186        let (io_tx, io_rx) = std::sync::mpsc::channel();
1187        let mut h = JsHost {
1188            tdz: None,
1189            tdz_globals: Default::default(),
1190            heap: Vec::new(),
1191            funcs: Vec::new(),
1192            tries: Vec::new(),
1193            globals: VarMap::default(),
1194            global_consts: rustc_hash::FxHashSet::default(),
1195            frames: vec![Frame {
1196                env: global_env.clone(),
1197                base_env: global_env.clone(),
1198                this_obj: None,
1199                new_target: None,
1200                home_class: None,
1201                home_static: false,
1202                home_object: None,
1203                strict: false,
1204                line: 0,
1205                owner: None,
1206                is_module: true,
1207                this_state: ThisState::Plain,
1208            }],
1209            global_env,
1210            error: None,
1211            exc: None,
1212            signal: None,
1213            pending_rejections: Vec::new(),
1214            process_listeners: IndexMap::new(),
1215            null_val: Value::Undef,
1216            protos: HashMap::new(),
1217            null_proto_objs: HashSet::new(),
1218            fn_props: HashMap::new(),
1219            accessors: HashMap::new(),
1220            prop_attrs: HashMap::new(),
1221            non_extensible: HashSet::new(),
1222            private_methods: HashSet::new(),
1223            array_holes: HashMap::new(),
1224            super_replacement: None,
1225            derived_ctor_next: false,
1226            module_scope: false,
1227            builtin_statics: HashMap::new(),
1228            object_proto: Value::Undef,
1229            proto_class: HashMap::new(),
1230            class_registry: HashMap::new(),
1231            error_protos: HashMap::new(),
1232            template_objects: HashMap::new(),
1233            native_protos: HashMap::new(),
1234            symbol_registry: HashMap::new(),
1235            next_symbol: 1,
1236            symbols_by_id: HashMap::new(),
1237            well_known_ids: HashMap::new(),
1238            generators: Vec::new(),
1239            promises: Vec::new(),
1240            microtasks: std::collections::VecDeque::new(),
1241            draining_micro: false,
1242            nextticks: std::collections::VecDeque::new(),
1243            macrotasks: Vec::new(),
1244            next_timer: 1,
1245            io_tx,
1246            io_rx: Some(io_rx),
1247            open_handles: 0,
1248            capture: None,
1249            exit_code: None,
1250            exiting: false,
1251            global_obj: Value::Undef,
1252        };
1253        h.null_val = h.alloc(JsObj::Null);
1254        // `Object.prototype`: the chain root, its own `[[Prototype]]` is null.
1255        h.object_proto = h.new_object(IndexMap::new());
1256        h.global_obj = h.new_object(IndexMap::new());
1257        h
1258    }
1259
1260    /// Whether `v` IS the one `globalThis` object (not merely an object).
1261    pub fn is_global_object(&self, v: &Value) -> bool {
1262        !matches!(self.global_obj, Value::Undef) && self.global_obj == *v
1263    }
1264
1265    /// The `globalThis` object — one per host, so its identity and its
1266    /// properties both survive across reads.
1267    pub fn global_object(&mut self) -> Value {
1268        if matches!(self.global_obj, Value::Undef) {
1269            self.global_obj = self.new_object(IndexMap::new());
1270        }
1271        self.global_obj.clone()
1272    }
1273
1274    // ── prototype chain ──────────────────────────────────────────────────
1275    /// The `[[Prototype]]` of a heap value, if explicitly linked.
1276    pub fn proto_of(&self, v: &Value) -> Option<Value> {
1277        if let Value::Obj(i) = v {
1278            self.protos.get(i).cloned()
1279        } else {
1280            None
1281        }
1282    }
1283    /// Set `v`'s `[[Prototype]]` to `proto`. Null links the object as an explicit
1284    /// null-prototype object (recorded so `instanceof Object` reads false);
1285    /// undefined just clears any link without the null marker.
1286    pub fn set_proto(&mut self, v: &Value, proto: Value) {
1287        if let Value::Obj(i) = v {
1288            if self.is_null(&proto) {
1289                self.protos.remove(i);
1290                self.null_proto_objs.insert(*i);
1291            } else if matches!(proto, Value::Undef) {
1292                self.protos.remove(i);
1293            } else {
1294                self.protos.insert(*i, proto);
1295                self.null_proto_objs.remove(i);
1296            }
1297        }
1298    }
1299    /// Whether `v`'s `[[Prototype]]` was explicitly set to null.
1300    pub fn has_null_proto(&self, v: &Value) -> bool {
1301        matches!(v, Value::Obj(i) if self.null_proto_objs.contains(i))
1302    }
1303    /// Whether `util.inspect` renders `v` with the `[Object: null prototype]`
1304    /// tag. That is a question about the object's ACTUAL `[[Prototype]]`, which
1305    /// for `Object.prototype` is null even though nothing ever set it so: it is
1306    /// the chain root and was never passed through `set_proto`, so the
1307    /// explicitly-nulled registry does not hold it and `console.log(Object
1308    /// .prototype)` printed a bare `{}` where node prints the tag.
1309    ///
1310    /// Kept apart from [`Self::has_null_proto`], which nine other call sites ask
1311    /// about whether Object.prototype's own methods and `__proto__` accessor are
1312    /// INHERITED. `Object.prototype` inherits nothing and still owns all of them.
1313    pub fn inspects_null_proto(&self, v: &Value) -> bool {
1314        self.has_null_proto(v) || *v == self.object_proto
1315    }
1316    pub fn object_proto(&self) -> Value {
1317        self.object_proto.clone()
1318    }
1319    /// Record that the prototype object `proto` belongs to the class constructor
1320    /// `class_val` (so instances can recover their constructor).
1321    pub fn tag_proto_class(&mut self, proto: &Value, class_val: Value) {
1322        if let Value::Obj(i) = proto {
1323            self.proto_class.insert(*i, class_val);
1324        }
1325    }
1326    /// The class whose `prototype` object IS `v`, if `v` is one.
1327    pub fn class_owning_proto(&self, v: &Value) -> Option<Value> {
1328        match v {
1329            Value::Obj(i) => self.proto_class.get(i).cloned(),
1330            _ => None,
1331        }
1332    }
1333    /// The class constructor value nearest in `obj`'s prototype chain, if any.
1334    pub fn class_of(&self, obj: &Value) -> Option<Value> {
1335        let mut cur = self.proto_of(obj);
1336        while let Some(p) = cur {
1337            if let Value::Obj(i) = &p {
1338                if let Some(c) = self.proto_class.get(i) {
1339                    return Some(c.clone());
1340                }
1341            }
1342            cur = self.proto_of(&p);
1343        }
1344        None
1345    }
1346    /// The constructor display name of `obj` for `util.inspect` (empty ⇒ plain
1347    /// object, no prefix).
1348    pub fn ctor_name(&self, obj: &Value) -> String {
1349        if let Some(c) = self.class_of(obj) {
1350            if let Some(JsObj::Class(cv)) = self.get(&c) {
1351                return cv.name.clone();
1352            }
1353        }
1354        // A `function F(){}` constructor is not a `class`, so it has no
1355        // `proto_class` entry. V8's `getConstructorName` walks the prototype
1356        // chain for an own `constructor` that is a named function — which is
1357        // what makes `console.log(new F())` print `F { y: 2 }`.
1358        let mut cur = self.proto_of(obj);
1359        while let Some(p) = cur {
1360            let ctor = match self.get(&p) {
1361                Some(JsObj::Object(props)) => props.get("constructor").cloned(),
1362                Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => self.fn_prop(&p, "constructor"),
1363                _ => None,
1364            };
1365            if let Some(f) = ctor {
1366                let n = self.callable_name(&f);
1367                if !n.is_empty() {
1368                    return n;
1369                }
1370            }
1371            cur = self.proto_of(&p);
1372        }
1373        String::new()
1374    }
1375
1376    /// Whether a callable owns a `prototype` property. `MakeConstructor`
1377    /// (10.2.5) runs for an ordinary function definition and for every
1378    /// generator; an arrow, a `MethodDefinition`, an async function and a bound
1379    /// function are not constructors and own none.
1380    pub fn owns_prototype(&self, v: &Value) -> bool {
1381        match self.get(v) {
1382            Some(JsObj::Class(_)) => true,
1383            Some(JsObj::Func(f)) => match self.funcs.get(f.def_id) {
1384                Some(d) => d.is_generator || !(d.is_arrow || d.is_async || d.is_method),
1385                None => false,
1386            },
1387            _ => false,
1388        }
1389    }
1390
1391    /// A function's own-property table (created on demand).
1392    pub fn fn_prop(&self, v: &Value, name: &str) -> Option<Value> {
1393        if let Value::Obj(i) = v {
1394            self.fn_props.get(i).and_then(|m| m.get(name).cloned())
1395        } else {
1396            None
1397        }
1398    }
1399
1400    /// A class static member, inherited down the constructor chain: a subclass
1401    /// sees its superclass's `static` methods/fields (`Sub.create` → `Base.create`).
1402    pub fn class_static(&self, class_val: &Value, name: &str) -> Option<Value> {
1403        let mut cur = class_val.clone();
1404        loop {
1405            if let Some(v) = self.fn_prop(&cur, name) {
1406                return Some(v);
1407            }
1408            match self.get(&cur) {
1409                Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1410                _ => return None,
1411            }
1412        }
1413    }
1414
1415    /// The first `extends` ancestor that is NOT a user class — the builtin
1416    /// constructor a class chain bottoms out in (`class D extends Array {}` →
1417    /// the `Array` builtin), or `None` for a chain of user classes only.
1418    ///
1419    /// `class_static` walks `ClassVal.parent` and gives up the moment the parent
1420    /// stops being a `Class`, so a static declared by the BUILTIN half of the
1421    /// chain was unreachable: `D.from` read `undefined` where node inherits
1422    /// `Array.from`. Returning the ancestor lets the caller finish the lookup
1423    /// with an ordinary property read, which is what reaches a builtin's
1424    /// statics.
1425    pub fn class_builtin_ancestor(&self, class_val: &Value) -> Option<Value> {
1426        let mut cur = class_val.clone();
1427        loop {
1428            match self.get(&cur) {
1429                Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1430                _ => return Some(cur),
1431            }
1432        }
1433    }
1434    pub fn set_fn_prop(&mut self, v: &Value, name: &str, val: Value) {
1435        if let Value::Obj(i) = v {
1436            self.fn_props
1437                .entry(*i)
1438                .or_default()
1439                .insert(name.to_string(), val);
1440        }
1441        // `name` and `prototype` are own properties of every function/class, but
1442        // never enumerable ones (SetFunctionName 10.2.9, MakeConstructor
1443        // 10.2.5), so `Object.keys(fn)` and `for (k in fn)` report only what a
1444        // script assigned. An ARRAY receiver reaching the same side table has no
1445        // such exotic keys — `arr.name = 'x'` is an ordinary enumerable property.
1446        if !matches!(self.kind_of(v), Some(ObjKind::Func) | Some(ObjKind::Class)) {
1447            return;
1448        }
1449        let attrs = match name {
1450            "name" => PropAttrs {
1451                writable: false,
1452                enumerable: false,
1453                configurable: true,
1454            },
1455            "prototype" => PropAttrs {
1456                writable: true,
1457                enumerable: false,
1458                configurable: false,
1459            },
1460            _ => return,
1461        };
1462        self.set_prop_attrs(v, name, attrs);
1463    }
1464    /// A user-assigned static on a builtin namespace (`Error.prepareStackTrace`).
1465    pub fn builtin_static(&self, ns: &str, name: &str) -> Option<Value> {
1466        self.builtin_statics
1467            .get(ns)
1468            .and_then(|m| m.get(name).cloned())
1469    }
1470    /// Assign a static on a builtin namespace (persists across fresh `Builtin`
1471    /// handles for the same namespace).
1472    pub fn set_builtin_static(&mut self, ns: &str, name: &str, val: Value) {
1473        self.builtin_statics
1474            .entry(ns.to_string())
1475            .or_default()
1476            .insert(name.to_string(), val);
1477    }
1478    /// `delete <ns>.<name>` for a script-assigned static. Reports whether the
1479    /// key was there — without this, `delete Array.prototype.patch` answered
1480    /// true and left the entry in place, so the patch outlived its own removal.
1481    pub fn remove_builtin_static(&mut self, ns: &str, name: &str) -> bool {
1482        self.builtin_statics
1483            .get_mut(ns)
1484            .is_some_and(|m| m.shift_remove(name).is_some())
1485    }
1486    /// Every namespace a script has assigned a static onto, with that
1487    /// namespace's assigned keys — the source of the user-added half of
1488    /// `Object.getOwnPropertyNames(Array.prototype)`.
1489    pub fn builtin_static_keys(&self, ns: &str) -> Vec<String> {
1490        self.builtin_statics
1491            .get(ns)
1492            .map(|m| m.keys().cloned().collect())
1493            .unwrap_or_default()
1494    }
1495    /// Drop an own property from the side table (`delete arr.foo`,
1496    /// `delete fn.tag`). Reports whether the key was there.
1497    pub fn remove_fn_prop(&mut self, v: &Value, name: &str) -> bool {
1498        match v {
1499            Value::Obj(i) => self
1500                .fn_props
1501                .get_mut(i)
1502                .map(|m| m.shift_remove(name).is_some())
1503                .unwrap_or(false),
1504            _ => false,
1505        }
1506    }
1507    pub fn fn_prop_keys(&self, v: &Value) -> Vec<String> {
1508        if let Value::Obj(i) = v {
1509            self.fn_props
1510                .get(i)
1511                .map(|m| m.keys().cloned().collect())
1512                .unwrap_or_default()
1513        } else {
1514            Vec::new()
1515        }
1516    }
1517
1518    /// Install an accessor `(get, set)` for `key` on the object `owner`.
1519    pub fn set_accessor(
1520        &mut self,
1521        owner: &Value,
1522        key: &str,
1523        get: Option<Value>,
1524        set: Option<Value>,
1525    ) {
1526        if let Value::Obj(i) = owner {
1527            // Accessors live in their own table, but JS reports own keys in a
1528            // single insertion order across data AND accessor properties. Drop an
1529            // ordering marker into the property map so
1530            // `{ a: 1, get b() {}, c: 3 }` enumerates a, b, c — not a, c, b.
1531            // The marker is `@@`-prefixed, so it is invisible to every reader.
1532            let marker = format!("{ORD_MARKER}{key}");
1533            match self.get_mut(owner) {
1534                Some(JsObj::Object(props)) => {
1535                    if !props.contains_key(key) && !props.contains_key(&marker) {
1536                        props.insert(marker, Value::Undef);
1537                    }
1538                }
1539                // A function or class keeps its own properties in the fn-prop
1540                // side table, so its ordering marker belongs there. Without it a
1541                // static accessor enumerated AFTER every static field and method
1542                // regardless of where the class body declared it: node reports
1543                // `class A { static s = 2; static get sv(){} static m(){} }` as
1544                // `['sv', 'm', 's']` — the methods and accessors in source order
1545                // first, then the fields — and this reported `['m', 's', 'sv']`.
1546                Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1547                    let table = self.fn_props.entry(*i).or_default();
1548                    if !table.contains_key(key) && !table.contains_key(&marker) {
1549                        table.insert(marker, Value::Undef);
1550                    }
1551                }
1552                _ => {}
1553            }
1554            let slot = self
1555                .accessors
1556                .entry(*i)
1557                .or_default()
1558                .entry(key.to_string())
1559                .or_insert((None, None));
1560            if get.is_some() {
1561                slot.0 = get;
1562            }
1563            if set.is_some() {
1564                slot.1 = set;
1565            }
1566        }
1567    }
1568    /// The accessor `(get, set)` for `key` directly on `owner` (no chain walk).
1569    /// Drop an own accessor property entirely, marker and all.
1570    ///
1571    /// `delete obj.accessorProp` used to clear only the property map, and an
1572    /// accessor does not live there — so the delete reported success while the
1573    /// getter kept answering and `in` kept reporting the key.
1574    pub fn remove_accessor(&mut self, owner: &Value, key: &str) {
1575        if let Value::Obj(i) = owner {
1576            if let Some(m) = self.accessors.get_mut(i) {
1577                m.shift_remove(key);
1578            }
1579        }
1580        let marker = format!("{ORD_MARKER}{key}");
1581        match self.get_mut(owner) {
1582            Some(JsObj::Object(props)) => {
1583                props.shift_remove(&marker);
1584            }
1585            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1586                if let Value::Obj(i) = owner {
1587                    if let Some(t) = self.fn_props.get_mut(i) {
1588                        t.shift_remove(&marker);
1589                    }
1590                }
1591            }
1592            _ => {}
1593        }
1594    }
1595
1596    /// Turn an own accessor property into a data property carrying `value`,
1597    /// keeping its place in the own-key order.
1598    ///
1599    /// `set_accessor` records that order with an `@@ord:` marker in the
1600    /// property map rather than a real key, so deleting the accessor and
1601    /// inserting the value would append the key at the end instead. Node
1602    /// reports `{ a: 1, get b() {}, c: 3 }` redefined through
1603    /// `Object.defineProperty(o, 'b', { value })` as `a, b, c`.
1604    pub fn accessor_to_data(&mut self, owner: &Value, key: &str, value: Value) {
1605        if let Value::Obj(i) = owner {
1606            if let Some(m) = self.accessors.get_mut(i) {
1607                m.shift_remove(key);
1608            }
1609        }
1610        let marker = format!("{ORD_MARKER}{key}");
1611        let swap = |map: &mut IndexMap<String, Value>| match map.get_index_of(&marker) {
1612            Some(pos) => {
1613                *map = map
1614                    .iter()
1615                    .enumerate()
1616                    .map(|(n, (k, v))| {
1617                        if n == pos {
1618                            (key.to_string(), value.clone())
1619                        } else {
1620                            (k.clone(), v.clone())
1621                        }
1622                    })
1623                    .collect();
1624            }
1625            None => {
1626                map.insert(key.to_string(), value.clone());
1627            }
1628        };
1629        let fn_table = matches!(
1630            self.get(owner),
1631            Some(JsObj::Func(_)) | Some(JsObj::Class(_))
1632        );
1633        if fn_table {
1634            if let Value::Obj(i) = owner {
1635                swap(self.fn_props.entry(*i).or_default());
1636            }
1637        } else if let Some(JsObj::Object(props)) = self.get_mut(owner) {
1638            swap(props);
1639        }
1640    }
1641
1642    /// Move the per-heap-index bookkeeping of `src` onto `dst`.
1643    ///
1644    /// Used when one object becomes another in place (a class extending a
1645    /// builtin exotic). The prototype link is deliberately NOT moved: `dst`
1646    /// already points at the leaf class's prototype, which is the one its
1647    /// methods must resolve through.
1648    pub fn move_index_state(&mut self, src: u32, dst: u32) {
1649        if let Some(holes) = self.array_holes.remove(&src) {
1650            self.array_holes.insert(dst, holes);
1651        }
1652        if let Some(attrs) = self.prop_attrs.remove(&src) {
1653            self.prop_attrs.entry(dst).or_default().extend(attrs);
1654        }
1655        if let Some(props) = self.fn_props.remove(&src) {
1656            self.fn_props.entry(dst).or_default().extend(props);
1657        }
1658        if let Some(acc) = self.accessors.remove(&src) {
1659            self.accessors.entry(dst).or_default().extend(acc);
1660        }
1661    }
1662
1663    pub fn own_accessor(&self, owner: &Value, key: &str) -> Option<(Option<Value>, Option<Value>)> {
1664        if let Value::Obj(i) = owner {
1665            self.accessors.get(i).and_then(|m| m.get(key).cloned())
1666        } else {
1667            None
1668        }
1669    }
1670
1671    /// The own accessor-property keys of `owner`, in installation order.
1672    pub fn own_accessor_keys(&self, owner: &Value) -> Vec<String> {
1673        match owner {
1674            Value::Obj(i) => self
1675                .accessors
1676                .get(i)
1677                .map(|m| m.keys().cloned().collect())
1678                .unwrap_or_default(),
1679            _ => Vec::new(),
1680        }
1681    }
1682
1683    // ── own-property attributes ──────────────────────────────────────────
1684
1685    /// Record non-default attributes for `owner[key]`. Storing the default shape
1686    /// clears the entry so the table only ever holds deviations.
1687    pub fn set_prop_attrs(&mut self, owner: &Value, key: &str, attrs: PropAttrs) {
1688        if let Value::Obj(i) = owner {
1689            if attrs == PropAttrs::default() {
1690                if let Some(m) = self.prop_attrs.get_mut(i) {
1691                    m.shift_remove(key);
1692                }
1693            } else {
1694                self.prop_attrs
1695                    .entry(*i)
1696                    .or_default()
1697                    .insert(key.to_string(), attrs);
1698            }
1699        }
1700    }
1701
1702    /// Copy every recorded property attribute from `from` to `to`. A pass that
1703    /// rebuilds an object (`JSON.stringify`'s `toJSON` walk) must carry them
1704    /// across or the copy silently re-exposes non-enumerable slots.
1705    pub fn copy_prop_attrs(&mut self, from: &Value, to: &Value) {
1706        if let (Value::Obj(f), Value::Obj(_)) = (from, to) {
1707            if let Some(m) = self.prop_attrs.get(f).cloned() {
1708                for (k, a) in m {
1709                    self.set_prop_attrs(to, &k, a);
1710                }
1711            }
1712        }
1713    }
1714
1715    /// The attributes of own property `owner[key]` (all-true when unrecorded).
1716    pub fn prop_attrs(&self, owner: &Value, key: &str) -> PropAttrs {
1717        // An array's `length` is the array exotic's own property (10.4.2):
1718        // never enumerated and never configurable, and writable until
1719        // `Object.freeze` clears that — which is what stops a `push` from
1720        // extending a frozen array. Reporting it unconditionally writable made
1721        // `Object.isFrozen(Object.freeze([]))` false once the elements started
1722        // being sealed, because `length` was then the one key that never
1723        // followed.
1724        if key == "length" && matches!(self.get(owner), Some(JsObj::Array(_))) {
1725            let writable = match owner {
1726                Value::Obj(i) => self
1727                    .prop_attrs
1728                    .get(i)
1729                    .and_then(|m| m.get(key))
1730                    .map(|a| a.writable)
1731                    .unwrap_or(true),
1732                _ => true,
1733            };
1734            return PropAttrs {
1735                writable,
1736                enumerable: false,
1737                // An ARGUMENTS object's `length` is an ordinary data property
1738                // (10.4.4.6), so it is configurable where a real array's is
1739                // not. The two share a backing representation here, so the
1740                // exotic's attributes have to be told apart explicitly.
1741                configurable: crate::builtins::is_arguments_h(self, owner),
1742            };
1743        }
1744        match owner {
1745            Value::Obj(i) => self
1746                .prop_attrs
1747                .get(i)
1748                .and_then(|m| m.get(key))
1749                .copied()
1750                .unwrap_or_default(),
1751            _ => PropAttrs::default(),
1752        }
1753    }
1754
1755    /// Whether own property `owner[key]` shows up in `for-in`/`Object.keys`.
1756    /// Internal slots (`@@…`) and private class fields (`#…`) never do.
1757    pub fn is_enumerable(&self, owner: &Value, key: &str) -> bool {
1758        !key.starts_with("@@") && !key.starts_with('#') && self.prop_attrs(owner, key).enumerable
1759    }
1760
1761    /// Mark `owner[key]` non-enumerable, leaving it writable/configurable — the
1762    /// shape of every V8 "hidden but real" own property.
1763    pub fn hide_prop(&mut self, owner: &Value, key: &str) {
1764        self.set_prop_attrs(owner, key, PropAttrs::HIDDEN);
1765    }
1766
1767    /// Whether a plain `owner[key] = v` assignment is allowed to land. A
1768    /// non-writable data property silently ignores the write in sloppy mode,
1769    /// which is the mode every script here runs in; so does adding a *new* key to
1770    /// a non-extensible object.
1771    pub fn can_write_prop(&self, owner: &Value, key: &str) -> bool {
1772        if !self.prop_attrs(owner, key).writable {
1773            return false;
1774        }
1775        // An intrinsic prototype on the chain may define the name NON-WRITABLE,
1776        // and those members own no map entry for the walk below to find:
1777        // `o[Symbol.toStringTag] = 'x'` where `o` inherits from `Map.prototype`
1778        // is refused in node and was creating an own property here, which then
1779        // changed the object's brand.
1780        // The receiver's OWN kind counts too, not only the prototypes an
1781        // explicit link reaches: a plain array inherits `Array.prototype`
1782        // implicitly, with no link for the walk to follow, and
1783        // `a[Symbol.unscopables] = 'x'` is refused there just the same.
1784        //
1785        // Restricted to SYMBOL-keyed members. The string-keyed non-writable
1786        // ones — `Function.prototype.length`/`name`, `String.prototype.length`
1787        // — are also OWN properties of every instance, so the inherited rule
1788        // never decides them; applying it anyway blocked `SetFunctionName`
1789        // itself, and naming the setter in `Object.defineProperty(o, 'v', {set
1790        // (x) {…}})` then threw.
1791        if key.starts_with("@@")
1792            && crate::builtins::own_ctor_name(self, owner)
1793                .into_iter()
1794                .chain(crate::builtins::chain_intrinsic_ctors_h(self, owner))
1795                .any(|c| crate::builtins::is_proto_readonly(c, key))
1796        {
1797            return false;
1798        }
1799        // 10.1.9.2: with no OWN property, the inherited one decides. A
1800        // non-writable data property up the chain blocks the write rather than
1801        // being shadowed — including one on a frozen prototype. Only own
1802        // attributes were consulted, so `Object.create(frozenBase).f = 2`
1803        // quietly created an own property node refuses to create.
1804        //
1805        // An inherited ACCESSOR does not block: its setter runs, and the write
1806        // path checks for one before reaching here.
1807        let has_own = match self.get(owner) {
1808            Some(JsObj::Object(p)) => p.contains_key(key),
1809            _ => true,
1810        };
1811        if !has_own {
1812            let mut cur = self.proto_of(owner);
1813            while let Some(proto) = cur {
1814                if self.own_accessor(&proto, key).is_some() {
1815                    break;
1816                }
1817                let present =
1818                    matches!(self.get(&proto), Some(JsObj::Object(p)) if p.contains_key(key));
1819                if present {
1820                    if !self.prop_attrs(&proto, key).writable {
1821                        return false;
1822                    }
1823                    break;
1824                }
1825                cur = self.proto_of(&proto);
1826            }
1827        }
1828        if self.is_extensible(owner) {
1829            return true;
1830        }
1831        // A non-extensible object refuses a NEW key. Only the plain-object arm
1832        // could name its own keys, so every other shape answered "own" for any
1833        // key at all: `Object.freeze(arr).extra = 1` landed, and so did a write
1834        // to the frozen template object a tagged template hands its tag.
1835        match self.get(owner) {
1836            Some(JsObj::Object(p)) => p.contains_key(key),
1837            Some(JsObj::Array(items)) => {
1838                key == "length"
1839                    || key
1840                        .parse::<usize>()
1841                        .map(|i| i < items.len())
1842                        .unwrap_or(false)
1843                    || self.fn_prop(owner, key).is_some()
1844            }
1845            // A RegExp's `lastIndex` is an own property, so a merely
1846            // NON-EXTENSIBLE regexp still accepts a write to it.
1847            Some(JsObj::RegExp(_)) => key == "lastIndex" || self.fn_prop(owner, key).is_some(),
1848            // Every other shape keeps its own properties in the fn-prop side
1849            // table (a function's statics, a Map's assigned properties), so
1850            // "does it already own this key" is that table's question. Answering
1851            // a blanket `true` let a NEW key land on a frozen function and a
1852            // frozen Map.
1853            _ => self.fn_prop(owner, key).is_some(),
1854        }
1855    }
1856
1857    /// Mark `v` closed to new properties (`Object.preventExtensions`).
1858    pub fn prevent_extensions(&mut self, v: &Value) {
1859        if let Value::Obj(i) = v {
1860            self.non_extensible.insert(*i);
1861        }
1862    }
1863
1864    pub fn is_extensible(&self, v: &Value) -> bool {
1865        !matches!(v, Value::Obj(i) if self.non_extensible.contains(i))
1866    }
1867
1868    /// Apply `Object.seal` (`freeze == false`) or `Object.freeze` (`true`): close
1869    /// the object and strip `configurable` — and, when freezing, `writable` —
1870    /// from every own property, data and accessor alike.
1871    pub fn seal_object(&mut self, v: &Value, freeze: bool) {
1872        self.prevent_extensions(v);
1873        let mut keys = self.integrity_keys(v);
1874        keys.extend(self.own_accessor_keys(v));
1875        for k in keys {
1876            let mut a = self.prop_attrs(v, &k);
1877            a.configurable = false;
1878            if freeze {
1879                a.writable = false;
1880            }
1881            self.set_prop_attrs(v, &k, a);
1882        }
1883    }
1884
1885    /// The own DATA-property keys SetIntegrityLevel (7.3.15) walks.
1886    ///
1887    /// An array's elements are own properties too, and only the `Object` arm was
1888    /// walked — so `Object.freeze([1, 2])` sealed nothing: `a[0] = 9` wrote
1889    /// through, and the elements still reported `writable: true,
1890    /// configurable: true` while `Object.isFrozen` answered true over an empty
1891    /// key list. `length` is an own property as well, and freezing it is what
1892    /// stops a `push` from extending a frozen array.
1893    fn integrity_keys(&self, v: &Value) -> Vec<String> {
1894        let side_table_keys = |v: &Value| -> Vec<String> {
1895            match v {
1896                Value::Obj(i) => self
1897                    .fn_props
1898                    .get(i)
1899                    .map(|m| m.keys().cloned().collect())
1900                    .unwrap_or_default(),
1901                _ => Vec::new(),
1902            }
1903        };
1904        match self.get(v) {
1905            Some(JsObj::Object(p)) => p.keys().cloned().collect(),
1906            // A RegExp's only own property is its `lastIndex` cursor, which
1907            // lives in the `RegExpObj` struct. Without it here `Object.freeze`
1908            // sealed nothing and a frozen regexp's cursor still moved.
1909            Some(JsObj::RegExp(_)) => vec!["lastIndex".to_string()],
1910            // A function's statics and a Map's assigned properties live in the
1911            // fn-prop side table, and freezing has to reach them too.
1912            Some(JsObj::Func(_))
1913            | Some(JsObj::Class(_))
1914            | Some(JsObj::Map { .. })
1915            | Some(JsObj::Set { .. })
1916            | Some(JsObj::Promise { .. }) => side_table_keys(v),
1917            Some(JsObj::Array(items)) => (0..items.len())
1918                .map(|i| i.to_string())
1919                .chain(std::iter::once("length".to_string()))
1920                // A named property stuck on an array (`a.tag = 't'`, a match
1921                // array's `.index`/`.groups`) is an own property too, and
1922                // freezing has to reach it.
1923                .chain(match v {
1924                    Value::Obj(i) => self
1925                        .fn_props
1926                        .get(i)
1927                        .map(|m| m.keys().cloned().collect::<Vec<_>>())
1928                        .unwrap_or_default(),
1929                    _ => Vec::new(),
1930                })
1931                .collect(),
1932            _ => Vec::new(),
1933        }
1934    }
1935
1936    /// `Object.isSealed` (`freeze == false`) / `Object.isFrozen` (`true`).
1937    pub fn is_sealed(&self, v: &Value, freeze: bool) -> bool {
1938        if self.is_extensible(v) {
1939            return false;
1940        }
1941        let mut keys = self.integrity_keys(v);
1942        keys.extend(self.own_accessor_keys(v));
1943        keys.iter().all(|k| {
1944            let a = self.prop_attrs(v, k);
1945            !a.configurable && (!freeze || !a.writable)
1946        })
1947    }
1948
1949    /// A fresh unique `Symbol(desc)` value.
1950    pub fn new_symbol(&mut self, desc: Option<String>) -> Value {
1951        let id = self.next_symbol;
1952        self.next_symbol += 1;
1953        let v = self.alloc(JsObj::Symbol { desc, id });
1954        self.symbols_by_id.insert(id, v.clone());
1955        v
1956    }
1957
1958    /// The symbol VALUE an internal symbol property key (`@@sym:<id>` or a
1959    /// well-known `@@iterator`) came from.
1960    pub fn symbol_of_key(&self, k: &str) -> Option<Value> {
1961        if let Some(id) = k.strip_prefix("@@sym:").and_then(|i| i.parse::<u64>().ok()) {
1962            return self.symbols_by_id.get(&id).cloned();
1963        }
1964        let name = k.strip_prefix("@@")?;
1965        WELL_KNOWN_SYMBOLS
1966            .contains(&name)
1967            .then(|| {
1968                self.symbol_registry
1969                    .get(&format!("@@Symbol.{name}"))
1970                    .cloned()
1971            })
1972            .flatten()
1973    }
1974
1975    /// The own symbol-keyed property keys of `v` as SYMBOL values —
1976    /// `Object.getOwnPropertySymbols` / the symbol half of `Reflect.ownKeys`.
1977    pub fn own_symbol_keys(&self, v: &Value) -> Vec<Value> {
1978        let keys: Vec<String> = match self.get(v) {
1979            Some(JsObj::Object(p)) => p.keys().cloned().collect(),
1980            // An Array/Function receiver has no property map: its non-index own
1981            // properties — symbol-keyed ones included — live in the fn-prop side
1982            // table, and are just as much own properties as an object's.
1983            Some(_) => self.fn_prop_keys(v),
1984            None => return Vec::new(),
1985        };
1986        keys.iter().filter_map(|k| self.symbol_of_key(k)).collect()
1987    }
1988
1989    /// The own SYMBOL-keyed enumerable `(internal key, value)` pairs of `v` —
1990    /// what `CopyDataProperties` (object spread, `Object.assign`) copies
1991    /// alongside the string keys, and what `Object.keys` / `for-in` /
1992    /// `JSON.stringify` deliberately skip.
1993    pub fn own_symbol_entries(&self, v: &Value) -> Vec<(String, Value)> {
1994        match self.get(v) {
1995            Some(JsObj::Object(p)) => p
1996                .iter()
1997                .filter(|(k, _)| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
1998                .map(|(k, val)| (k.clone(), val.clone()))
1999                .collect(),
2000            // Array/Function: the side table (see `own_symbol_keys`).
2001            Some(_) => self
2002                .fn_prop_keys(v)
2003                .into_iter()
2004                .filter(|k| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2005                .map(|k| {
2006                    let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
2007                    (k, val)
2008                })
2009                .collect(),
2010            None => Vec::new(),
2011        }
2012    }
2013    /// The shared `Symbol.for(key)` value (interned by description).
2014    pub fn symbol_for(&mut self, key: &str) -> Value {
2015        if let Some(v) = self.symbol_registry.get(key) {
2016            return v.clone();
2017        }
2018        let s = self.new_symbol(Some(key.to_string()));
2019        self.symbol_registry.insert(key.to_string(), s.clone());
2020        s
2021    }
2022    /// `Symbol.keyFor(sym)`: the registry key `Symbol.for` interned `sym` under,
2023    /// or `undefined` for a symbol that is not in the registry at all.
2024    ///
2025    /// Matched by symbol IDENTITY, not by description — `Symbol.for('k')` and
2026    /// `Symbol('k')` share a description and only the first is registered. The
2027    /// `@@Symbol.*` well-known entries are registry-internal and never a
2028    /// `keyFor` answer, matching node: `Symbol.keyFor(Symbol.iterator)` is
2029    /// `undefined` there.
2030    pub fn symbol_registry_key(&mut self, sym: &Value) -> Value {
2031        let Some(key) = self
2032            .symbol_registry
2033            .iter()
2034            .find(|(k, v)| self.strict_eq(v, sym) && !k.starts_with("@@Symbol."))
2035            .map(|(k, _)| k.clone())
2036        else {
2037            return Value::Undef;
2038        };
2039        self.new_str(key)
2040    }
2041    /// The well-known `Symbol.iterator` (a fixed shared symbol whose internal
2042    /// property key is `@@iterator`).
2043    pub fn well_known_iterator(&mut self) -> Value {
2044        self.symbol_for("@@Symbol.iterator")
2045    }
2046    /// The well-known `Symbol.asyncIterator` (internal key `@@asyncIterator`).
2047    pub fn well_known_async_iterator(&mut self) -> Value {
2048        self.symbol_for("@@Symbol.asyncIterator")
2049    }
2050    /// A well-known symbol by its ECMAScript name (`toPrimitive`,
2051    /// `toStringTag`, …). Its internal property key is `@@<name>` — see
2052    /// [`WELL_KNOWN_SYMBOLS`] and `property_key`.
2053    ///
2054    /// Its DESCRIPTION is `Symbol.<name>`, so `String(Symbol.iterator)` prints
2055    /// `Symbol(Symbol.iterator)` as V8 does, while the registry key keeps the
2056    /// `@@` prefix — `Symbol.for('Symbol.iterator')` therefore stays a
2057    /// different symbol, and identification is by id, so a user-made
2058    /// `Symbol('Symbol.iterator')` is not mistaken for the well-known one.
2059    pub fn well_known_symbol(&mut self, name: &str) -> Value {
2060        let key = format!("@@Symbol.{name}");
2061        if let Some(v) = self.symbol_registry.get(&key) {
2062            return v.clone();
2063        }
2064        let s = self.new_symbol(Some(format!("Symbol.{name}")));
2065        if let Some(JsObj::Symbol { id, .. }) = self.get(&s) {
2066            self.well_known_ids.insert(*id, name.to_string());
2067        }
2068        self.symbol_registry.insert(key, s.clone());
2069        s
2070    }
2071    /// The internal property-key string for a value used as a key. A `Symbol`
2072    /// maps to a stable per-symbol string so symbol-keyed props round-trip;
2073    /// `Symbol.iterator` maps to the sentinel `@@iterator`.
2074    pub fn property_key(&self, v: &Value) -> String {
2075        if let Some(JsObj::Symbol { id, .. }) = self.get(v) {
2076            if let Some(n) = self.well_known_ids.get(id) {
2077                return format!("@@{n}");
2078            }
2079            return format!("@@sym:{id}");
2080        }
2081        self.str_of(v)
2082    }
2083
2084    pub fn null(&self) -> Value {
2085        self.null_val.clone()
2086    }
2087    pub fn is_null(&self, v: &Value) -> bool {
2088        matches!(self.get(v), Some(JsObj::Null))
2089    }
2090
2091    // ── program loading ──────────────────────────────────────────────────
2092    pub fn program_offsets(&self) -> (usize, usize) {
2093        (self.funcs.len(), self.tries.len())
2094    }
2095    pub fn load_program(&mut self, funcs: Vec<FuncDef>, tries: Vec<TryDef>) {
2096        self.funcs.extend(funcs);
2097        self.tries.extend(tries);
2098    }
2099    pub fn try_def(&self, id: usize) -> Option<TryDef> {
2100        self.tries.get(id).cloned()
2101    }
2102
2103    /// What `try` statement `id` HAS — `(has handler, catch parameter name, has
2104    /// finalizer)` — without copying its chunks. Running a `try` used to clone
2105    /// the whole `TryDef`, so a `try` inside a loop deep-copied its block, its
2106    /// handler and its finalizer on every iteration just to learn its shape.
2107    pub fn try_shape(&self, id: usize) -> Option<(bool, Option<String>, bool)> {
2108        let t = self.tries.get(id)?;
2109        Some((
2110            t.handler.is_some(),
2111            t.handler.as_ref().and_then(|(bind, _)| bind.clone()),
2112            t.finalizer.is_some(),
2113        ))
2114    }
2115
2116    /// One `try` part's bytecode: 0 = block, 1 = handler body, 2 = finalizer.
2117    /// Reached only when no pooled VM already holds that chunk.
2118    pub fn try_chunk(&self, id: usize, part: u64) -> Option<Chunk> {
2119        let t = self.tries.get(id)?;
2120        match part {
2121            0 => Some(t.block.clone()),
2122            1 => t.handler.as_ref().map(|(_, body)| body.clone()),
2123            _ => t.finalizer.clone(),
2124        }
2125    }
2126
2127    // ── heap allocation / accessors ──────────────────────────────────────
2128    pub fn alloc(&mut self, obj: JsObj) -> Value {
2129        self.heap.push(obj);
2130        Value::Obj((self.heap.len() - 1) as u32)
2131    }
2132    pub fn get(&self, v: &Value) -> Option<&JsObj> {
2133        if let Value::Obj(i) = v {
2134            self.heap.get(*i as usize)
2135        } else {
2136            None
2137        }
2138    }
2139    pub fn get_mut(&mut self, v: &Value) -> Option<&mut JsObj> {
2140        if let Value::Obj(i) = v {
2141            self.heap.get_mut(*i as usize)
2142        } else {
2143            None
2144        }
2145    }
2146    /// Which variant `v` points at, without copying its contents. Use this in
2147    /// place of `get(v).cloned()` whenever only the tag is needed — see
2148    /// [`ObjKind`].
2149    pub fn kind_of(&self, v: &Value) -> Option<ObjKind> {
2150        self.get(v).map(JsObj::kind)
2151    }
2152    pub fn new_str(&mut self, s: impl Into<String>) -> Value {
2153        self.alloc(JsObj::Str(s.into()))
2154    }
2155    pub fn new_array(&mut self, items: Vec<Value>) -> Value {
2156        self.alloc(JsObj::Array(items))
2157    }
2158
2159    /// Record that `name` was declared as a private method or accessor.
2160    pub fn note_private_method(&mut self, name: &str) {
2161        self.private_methods.insert(name.to_string());
2162    }
2163
2164    /// Whether `name` was declared as a private method/accessor by some class,
2165    /// as opposed to a private field.
2166    pub fn is_private_method(&self, name: &str) -> bool {
2167        self.private_methods.contains(name)
2168    }
2169
2170    /// The name of the class whose body the running function belongs to. Only a
2171    /// method of that class can even mention its private names, so this is the
2172    /// class a failed brand check must name.
2173    /// The `super` binding of the frame now running: the owning class name,
2174    /// whether the method is static, and the home object of an object-literal
2175    /// method. An ARROW captures all three at creation, the way it captures
2176    /// `this` — `super` inside an arrow means the enclosing METHOD's `super`.
2177    /// Whether the activation now running is strict code.
2178    /// Whether `v` is a function whose own body is SLOPPY — not an arrow, and
2179    /// with no `'use strict'` of its own or inherited from its script. This is
2180    /// the receiver test the `arguments`/`caller` poison pill keys on: node
2181    /// decides by the FUNCTION, never by the code doing the reading.
2182    pub fn fn_is_sloppy(&self, v: &Value) -> bool {
2183        match self.get(v) {
2184            Some(JsObj::Func(fv)) => {
2185                !fv.is_arrow && !self.funcs.get(fv.def_id).is_some_and(|d| d.strict)
2186            }
2187            _ => false,
2188        }
2189    }
2190    pub fn current_strict(&self) -> bool {
2191        self.frame().strict
2192    }
2193
2194    /// Mark the frame about to run as STRICT — used for a program whose own top
2195    /// level says `'use strict'`, which has no `FuncDef` to carry the flag.
2196    pub fn set_current_strict(&mut self) {
2197        if let Some(f) = self.frames.last_mut() {
2198            f.strict = true;
2199        }
2200    }
2201
2202    pub fn current_home(&self) -> (Option<String>, bool, Option<Value>) {
2203        (
2204            self.current_home_class_name(),
2205            self.frame().home_static,
2206            self.frame().home_object.clone(),
2207        )
2208    }
2209
2210    pub fn current_home_class_name(&self) -> Option<String> {
2211        match self.get(&self.current_home_class()?) {
2212            Some(JsObj::Class(c)) => Some(c.name.clone()),
2213            _ => None,
2214        }
2215    }
2216
2217    /// Whether `recv` — or anything on its prototype chain — carries the private
2218    /// name `key`. A private FIELD is an own property of the instance; a private
2219    /// METHOD lives on the class prototype, one link up.
2220    pub fn has_private(&self, recv: &Value, key: &str) -> bool {
2221        let mut cur = Some(recv.clone());
2222        while let Some(v) = cur {
2223            let owns = match self.get(&v) {
2224                Some(JsObj::Object(p)) => p.contains_key(key),
2225                Some(JsObj::Class(c)) => c.statics.contains_key(key),
2226                _ => false,
2227            };
2228            if owns || self.own_accessor(&v, key).is_some() || self.fn_prop(&v, key).is_some() {
2229                return true;
2230            }
2231            cur = self.proto_of(&v);
2232        }
2233        false
2234    }
2235
2236    // ── array holes ──────────────────────────────────────────────────────
2237    //
2238    // Every read/write of an array's elision set goes through this block. See
2239    // the `array_holes` field for why the marker lives here rather than in
2240    // `Value`.
2241
2242    /// Whether element `i` of array `arr` is an elided element (a "hole"), as
2243    /// opposed to a stored `undefined`. `false` for anything that is not an
2244    /// array, and for every index of a dense one.
2245    pub fn is_hole(&self, arr: &Value, i: usize) -> bool {
2246        match (arr, ()) {
2247            (Value::Obj(idx), ()) => self.array_holes.get(idx).is_some_and(|hs| hs.contains(&i)),
2248            _ => false,
2249        }
2250    }
2251
2252    /// Whether `arr` has any elided element at all — one hash probe, and the
2253    /// guard every hole-aware code path takes before doing anything slower.
2254    pub fn has_holes(&self, arr: &Value) -> bool {
2255        matches!(arr, Value::Obj(i) if self.array_holes.contains_key(i))
2256    }
2257
2258    /// `arr`'s hole positions in ASCENDING order, or an empty vec if dense.
2259    /// Sorted because every consumer (own-key enumeration, `util.inspect`
2260    /// run-grouping) needs index order, and the backing set has none.
2261    pub fn hole_indices(&self, arr: &Value) -> Vec<usize> {
2262        let Value::Obj(i) = arr else {
2263            return Vec::new();
2264        };
2265        let Some(hs) = self.array_holes.get(i) else {
2266            return Vec::new();
2267        };
2268        let mut v: Vec<usize> = hs.iter().copied().collect();
2269        v.sort_unstable();
2270        v
2271    }
2272
2273    /// Record element `i` of `arr` as elided.
2274    pub fn mark_hole(&mut self, arr: &Value, i: usize) {
2275        if let Value::Obj(idx) = arr {
2276            self.array_holes.entry(*idx).or_default().insert(i);
2277        }
2278    }
2279
2280    /// Record `range` of `arr` as elided (a `new Array(n)`, a `length` grow, or
2281    /// the gap a write past the end opens).
2282    pub fn mark_hole_range(&mut self, arr: &Value, range: std::ops::Range<usize>) {
2283        if range.is_empty() {
2284            return;
2285        }
2286        if let Value::Obj(idx) = arr {
2287            self.array_holes.entry(*idx).or_default().extend(range);
2288        }
2289    }
2290
2291    /// Element `i` now holds a real value: it is no longer a hole. Every write
2292    /// to an array index calls this, which is what keeps a stale hole record
2293    /// from outliving the elision it described.
2294    pub fn clear_hole(&mut self, arr: &Value, i: usize) {
2295        let Value::Obj(idx) = arr else { return };
2296        let Some(hs) = self.array_holes.get_mut(idx) else {
2297            return;
2298        };
2299        hs.remove(&i);
2300        if hs.is_empty() {
2301            self.array_holes.remove(idx);
2302        }
2303    }
2304
2305    /// `arr` is dense from here on (`fill` over the whole array, a fresh
2306    /// dense assignment into an existing handle).
2307    pub fn clear_holes(&mut self, arr: &Value) {
2308        if let Value::Obj(idx) = arr {
2309            self.array_holes.remove(idx);
2310        }
2311    }
2312
2313    /// Copy `src`'s elision set onto `dst`, optionally shifting each position by
2314    /// `f`. Used by every method that derives a new array whose holes track the
2315    /// source's (`slice`, `concat`, `map`).
2316    pub fn copy_holes(&mut self, src: &Value, dst: &Value, f: impl Fn(usize) -> Option<usize>) {
2317        if !self.has_holes(src) {
2318            return;
2319        }
2320        let moved: rustc_hash::FxHashSet<usize> =
2321            self.hole_indices(src).into_iter().filter_map(f).collect();
2322        self.install_holes(dst, moved);
2323    }
2324
2325    /// Rewrite `arr`'s own elision set in place: `f(i)` gives the position each
2326    /// existing hole moves to, or `None` if the mutation removed it. This is the
2327    /// one primitive behind every structural array mutation — `shift` is
2328    /// `i.checked_sub(1)`, `unshift(k)` is `i + k`, `reverse` is `len-1-i`, and
2329    /// `splice` is the general case.
2330    pub fn remap_holes(&mut self, arr: &Value, f: impl Fn(usize) -> Option<usize>) {
2331        if !self.has_holes(arr) {
2332            return;
2333        }
2334        let moved: rustc_hash::FxHashSet<usize> =
2335            self.hole_indices(arr).into_iter().filter_map(f).collect();
2336        self.install_holes(arr, moved);
2337    }
2338
2339    /// Replace `arr`'s elision set outright, dropping the record entirely when
2340    /// the new set is empty so `has_holes` stays a single negative probe for the
2341    /// dense case.
2342    pub fn install_holes(&mut self, arr: &Value, holes: rustc_hash::FxHashSet<usize>) {
2343        let Value::Obj(idx) = arr else { return };
2344        if holes.is_empty() {
2345            self.array_holes.remove(idx);
2346        } else {
2347            self.array_holes.insert(*idx, holes);
2348        }
2349    }
2350
2351    /// Forget any hole at or past `len` — what a `pop`, a `length` shrink or a
2352    /// truncating `splice` leaves behind.
2353    pub fn truncate_holes(&mut self, arr: &Value, len: usize) {
2354        self.remap_holes(arr, |i| (i < len).then_some(i));
2355    }
2356
2357    /// `util.inspect`'s `formatSpecialArray`: the element strings of a SPARSE
2358    /// array, where each maximal run of elided positions collapses to a single
2359    /// `<N empty items>` entry. Returns the entries and whether the last of them
2360    /// is the `... N more items` tail (which the grid layout must not size a
2361    /// column to).
2362    ///
2363    /// The `maxArrayLength` cap counts ENTRIES, not indices, so a run costs one
2364    /// slot however long it is — matching node, where `[ ...Array(200) ]`-style
2365    /// sparse arrays print a single `<200 empty items>`.
2366    fn inspect_sparse(
2367        &self,
2368        v: &Value,
2369        items: &[Value],
2370        indent: usize,
2371        st: &mut InspectCycles,
2372    ) -> (Vec<String>, bool) {
2373        let holes: rustc_hash::FxHashSet<usize> = self.hole_indices(v).into_iter().collect();
2374        let empties = |n: usize| {
2375            let unit = if n == 1 { "item" } else { "items" };
2376            format!("<{n} empty {unit}>")
2377        };
2378        let mut out: Vec<String> = Vec::new();
2379        // The first index not yet accounted for by an entry.
2380        let mut index = 0usize;
2381        for (i, it) in items.iter().enumerate() {
2382            if out.len() >= inspect_max_array_length() {
2383                break;
2384            }
2385            if holes.contains(&i) {
2386                continue;
2387            }
2388            if i > index {
2389                out.push(empties(i - index));
2390                index = i;
2391                if out.len() >= inspect_max_array_length() {
2392                    break;
2393                }
2394            }
2395            out.push(self.inspect_lvl(it, indent + 2, st));
2396            index = i + 1;
2397        }
2398        let remaining = items.len() - index;
2399        if remaining == 0 {
2400            return (out, false);
2401        }
2402        if out.len() < inspect_max_array_length() {
2403            // Trailing holes are still `<N empty items>`, not a truncation.
2404            out.push(empties(remaining));
2405            (out, false)
2406        } else {
2407            let unit = if remaining == 1 { "item" } else { "items" };
2408            out.push(format!("... {remaining} more {unit}"));
2409            (out, true)
2410        }
2411    }
2412    pub fn new_object(&mut self, mut props: IndexMap<String, Value>) -> Value {
2413        // Integer-index keys enumerate ascending-first regardless of the order
2414        // they were supplied in (object literal, spread, Object.assign result).
2415        canonicalize_own_keys(&mut props);
2416        // A map carrying the hidden `@@native` tag IS an instance of that native
2417        // class, so it hangs off the class prototype rather than
2418        // `Object.prototype`. Eleven classes — `Hash`, `Cipheriv`,
2419        // `StringDecoder`, `Script`, `URLSearchParams`, `Console`,
2420        // `AbortController` among them — built plain objects instead, so
2421        // `x.constructor.name` read `"Object"` and a chain walk found none of
2422        // the class's methods. Linking HERE means a construction site cannot
2423        // forget it; the tag is already in the map at every one of them.
2424        let tag = props.get("@@native").and_then(|v| self.as_str(v));
2425        let obj = self.alloc(JsObj::Object(props));
2426        if let Some(proto) = tag.and_then(|t| self.ensure_ctor_proto(&t)) {
2427            self.set_proto(&obj, proto);
2428        }
2429        obj
2430    }
2431    pub fn as_str(&self, v: &Value) -> Option<String> {
2432        match v {
2433            Value::Str(s) => Some((**s).clone()),
2434            Value::Obj(_) => match self.get(v) {
2435                Some(JsObj::Str(s)) => Some(s.clone()),
2436                _ => None,
2437            },
2438            _ => None,
2439        }
2440    }
2441
2442    // ── scope / names ────────────────────────────────────────────────────
2443    fn frame(&self) -> &Frame {
2444        self.frames.last().unwrap()
2445    }
2446    fn cur_env(&self) -> Env {
2447        self.frame().env.clone()
2448    }
2449
2450    // ── DAP debug introspection (used only under `--dap`) ────────────────────
2451    /// Number of active call frames (the debugger's step-depth reference).
2452    pub fn frame_depth(&self) -> usize {
2453        self.frames.len()
2454    }
2455    /// Record the source line the innermost frame is executing (DAP line hook).
2456    pub fn set_cur_line(&mut self, line: u32) {
2457        if let Some(f) = self.frames.last_mut() {
2458            f.line = line;
2459        }
2460    }
2461    /// The `.stack` tail for an error created right now: one `    at <name>`
2462    /// line per live frame, innermost first, ending at the module frame.
2463    ///
2464    /// These are the REAL user frames — node-js has no `file:line:column` (the
2465    /// per-frame line is only tracked under `--dap`) and no Node-internal
2466    /// module-loader frames, so `.stack` names the call chain but can never be
2467    /// byte-identical to V8's. The names are what makes a thrown error
2468    /// diagnosable; the missing positions are documented in BUGS.md.
2469    /// V8's `Error.stackTraceLimit` — how many frames a captured stack keeps.
2470    ///
2471    /// The default is 10, it is settable, and setting it to 0 is the documented
2472    /// way to make error construction cheap. It did not exist, so the read was
2473    /// `undefined` and every stack carried every frame regardless.
2474    pub fn stack_trace_limit(&self) -> usize {
2475        match self.builtin_static("Error", "stackTraceLimit") {
2476            Some(v) => {
2477                let n = self.to_number(&v);
2478                if n.is_finite() && n > 0.0 {
2479                    n as usize
2480                } else if n.is_nan() || n <= 0.0 {
2481                    0
2482                } else {
2483                    usize::MAX
2484                }
2485            }
2486            None => 10,
2487        }
2488    }
2489
2490    pub fn stack_frames(&self) -> String {
2491        let limit = self.stack_trace_limit();
2492        if limit == 0 {
2493            return String::new();
2494        }
2495        let mut out = String::new();
2496        for (i, f) in self.frames.iter().enumerate().rev().take(limit) {
2497            let name = match (&f.owner, i) {
2498                (Some(n), _) => n.clone(),
2499                (None, 0) => "Object.<anonymous>".to_string(),
2500                (None, _) => "<anonymous>".to_string(),
2501            };
2502            out.push_str("\n    at ");
2503            out.push_str(&name);
2504        }
2505        if out.is_empty() && limit > 0 {
2506            out.push_str("\n    at <anonymous>");
2507        }
2508        out
2509    }
2510
2511    /// The call stack as (frame name, line) pairs, innermost first — for the DAP
2512    /// `stackTrace`. `owner` carries the function name where known.
2513    pub fn dbg_stack(&self) -> Vec<(String, u32)> {
2514        self.frames
2515            .iter()
2516            .rev()
2517            .map(|f| {
2518                let name = f.owner.clone().unwrap_or_else(|| "<module>".to_string());
2519                (name, f.line)
2520            })
2521            .collect()
2522    }
2523    /// The innermost frame's locals as (name, inspect) pairs — for DAP `variables`.
2524    pub fn dbg_locals(&self) -> Vec<(String, String)> {
2525        let env = self.cur_env();
2526        let names: Vec<String> = env.borrow().vars.keys().cloned().collect();
2527        names
2528            .into_iter()
2529            .map(|n| {
2530                let v = self.read_name(&n).unwrap_or(Value::Undef);
2531                (n, self.inspect(&v))
2532            })
2533            .collect()
2534    }
2535
2536    /// Scope-chain read: local + enclosing chain, then globals.
2537    /// Whether `name` is a module-top-level binding that has not reached its
2538    /// declaration yet. Separate from [`JsHost::is_tdz`], which answers for a
2539    /// block-scoped one by inspecting the value it holds.
2540    pub fn is_tdz_global(&self, name: &str) -> bool {
2541        self.tdz_globals.contains(name)
2542    }
2543
2544    pub fn read_name(&self, name: &str) -> Option<Value> {
2545        let mut env = Some(self.cur_env());
2546        while let Some(e) = env {
2547            if let Some(v) = e.borrow().vars.get(name) {
2548                return Some(v.clone());
2549            }
2550            env = e.borrow().parent.clone();
2551        }
2552        self.globals.get(name).cloned()
2553    }
2554    pub fn read_global(&self, name: &str) -> Option<Value> {
2555        self.globals.get(name).cloned()
2556    }
2557
2558    /// Whether `name` is bound anywhere on the scope chain or in the globals —
2559    /// `read_name(..).is_some()` without cloning the value it finds. The
2560    /// strict-mode assignment path asks this and nothing else.
2561    pub fn has_name(&self, name: &str) -> bool {
2562        let mut env = Some(self.cur_env());
2563        while let Some(e) = env {
2564            if e.borrow().vars.contains_key(name) {
2565                return true;
2566            }
2567            env = e.borrow().parent.clone();
2568        }
2569        self.globals.contains_key(name)
2570    }
2571
2572    /// Assign to an existing binding up the scope chain, else create a global
2573    /// (JS assignment to an undeclared name targets the global object).
2574    /// Assign to an existing binding, or create a global. Returns `false` when
2575    /// the nearest binding is an immutable (`const`) one, which the caller turns
2576    /// into `TypeError: Assignment to constant variable.` — assigning to a
2577    /// `const` used to succeed SILENTLY, so code that node rejects ran on with
2578    /// a mutated constant.
2579    #[must_use]
2580    pub fn set_name(&mut self, name: &str, val: Value) -> bool {
2581        let mut env = Some(self.cur_env());
2582        while let Some(e) = env {
2583            // `get_mut`, not `contains_key` + `insert`: overwriting an existing
2584            // binding hashed the name twice and allocated a fresh `String` key
2585            // for a key that was already there — once per assignment, so once
2586            // per loop iteration in any counting loop.
2587            //
2588            // The const check runs only at the env that OWNS the name, and the
2589            // `is_empty` guard settles the common (no consts here) case without
2590            // hashing the name again.
2591            let mut b = e.borrow_mut();
2592            if b.vars.contains_key(name) {
2593                if !b.consts.is_empty() && b.consts.contains(name) {
2594                    return false;
2595                }
2596                if let Some(slot) = b.vars.get_mut(name) {
2597                    *slot = val;
2598                }
2599                return true;
2600            }
2601            drop(b);
2602            env = e.borrow().parent.clone();
2603        }
2604        if self.global_consts.contains(name) {
2605            return false;
2606        }
2607        match self.globals.get_mut(name) {
2608            Some(slot) => *slot = val,
2609            None => {
2610                self.globals.insert(name.to_string(), val);
2611            }
2612        }
2613        true
2614    }
2615
2616    /// Declare a `const` binding: the same placement as [`Self::declare_name`],
2617    /// plus recording the name as immutable in whichever scope received it.
2618    pub fn declare_const_name(&mut self, name: &str, val: Value) {
2619        let f = self.frame();
2620        let to_globals = f.is_module && Rc::ptr_eq(&f.env, &f.base_env);
2621        self.declare_name(name, val);
2622        if to_globals {
2623            self.global_consts.insert(name.to_string());
2624        } else {
2625            self.cur_env().borrow_mut().consts.insert(name.to_string());
2626        }
2627    }
2628
2629    /// The value a lexical binding holds between entering its scope and reaching
2630    /// its declaration — its TEMPORAL DEAD ZONE. One heap object for the whole
2631    /// process, so the check is a heap-index comparison and the marker cannot be
2632    /// produced by any JavaScript expression. It never escapes: every path that
2633    /// could read it throws first.
2634    pub fn tdz_marker(&mut self) -> Value {
2635        if let Some(v) = &self.tdz {
2636            return v.clone();
2637        }
2638        let v = self.alloc(JsObj::Builtin("@@tdz".into()));
2639        self.tdz = Some(v.clone());
2640        v
2641    }
2642
2643    /// Whether `v` is the uninitialized-binding marker.
2644    pub fn is_tdz(&self, v: &Value) -> bool {
2645        matches!((&self.tdz, v), (Some(Value::Obj(a)), Value::Obj(b)) if a == b)
2646    }
2647
2648    /// Declare `name` in the CURRENT scope as uninitialized, unless that scope
2649    /// already binds it. Emitted at the top of every scope for each `let`,
2650    /// `const` and `class` declared directly in it, so a read before the
2651    /// declaration throws instead of finding an OUTER binding of the same name —
2652    /// `let x = 1; { x; let x = 2 }` used to read the outer `1`.
2653    pub fn hoist_tdz(&mut self, name: &str) {
2654        let marker = self.tdz_marker();
2655        let f = self.frame();
2656        // At module top level a lexical binding lives in `globals`, which is ALSO
2657        // what backs `globalThis.<name>` — so parking the marker there exposes it
2658        // to JavaScript, and `const crypto = …` made `globalThis.crypto` read
2659        // back as the marker. Top-level dead zones are tracked in a separate set
2660        // that only the name-read path consults.
2661        if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2662            if !self.globals.contains_key(name) {
2663                self.tdz_globals.insert(name.to_string());
2664            }
2665            return;
2666        }
2667        let env = self.cur_env();
2668        let mut e = env.borrow_mut();
2669        if !e.vars.contains_key(name) {
2670            e.vars.insert(name.to_string(), marker);
2671        }
2672    }
2673
2674    /// Declare a new binding in the current scope (`let`/`const`). At the top of
2675    /// the module frame there is no local env, so those names become globals; once
2676    /// a block scope is open the binding belongs to that block.
2677    pub fn declare_name(&mut self, name: &str, val: Value) {
2678        let f = self.frame();
2679        if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2680            self.tdz_globals.remove(name);
2681            self.globals.insert(name.to_string(), val);
2682        } else {
2683            self.cur_env()
2684                .borrow_mut()
2685                .vars
2686                .insert(name.to_string(), val);
2687        }
2688    }
2689
2690    /// Declare a `var` (or a hoisted function declaration): FUNCTION-scoped, so it
2691    /// skips every open block scope and lands in the activation's base env.
2692    /// Create a hoisted `var` binding, initialised to `undefined`, only when the
2693    /// name is not already bound in this activation.
2694    ///
2695    /// `var` bindings come into existence when the scope is entered, not where
2696    /// the declaration is written — `f(){ x; var x = 1 }` reads `undefined`
2697    /// rather than throwing. "If absent" is what keeps a parameter intact: in
2698    /// `function f(a) { var a; }` the `var` names a binding that already exists
2699    /// and must not be reset, which is also why a bare `var x;` emits nothing at
2700    /// its own position.
2701    pub fn hoist_var_name(&mut self, name: &str) {
2702        // The ENTRY script's top level is a CommonJS module body, not global
2703        // scope: node wraps every file in a function, so a top-level `var` is a
2704        // local of that wrapper. Binding it into the globals map made
2705        // `var x = 3` at the top of the entry readable as `globalThis.x`, where
2706        // node says `undefined` — a REQUIRED module already ran inside a real
2707        // frame and behaved correctly, so only the entry file differed.
2708        if self.frame().is_module && !self.module_scope {
2709            self.globals.entry(name.to_string()).or_insert(Value::Undef);
2710            return;
2711        }
2712        let base = self.frame().base_env.clone();
2713        let mut env = base.borrow_mut();
2714        if !env.vars.contains_key(name) {
2715            env.vars.insert(name.to_string(), Value::Undef);
2716        }
2717    }
2718
2719    pub fn declare_var_name(&mut self, name: &str, val: Value) {
2720        if self.frame().is_module && !self.module_scope {
2721            self.globals.insert(name.to_string(), val);
2722            return;
2723        }
2724        let base = self.frame().base_env.clone();
2725        base.borrow_mut().vars.insert(name.to_string(), val);
2726    }
2727
2728    /// Enter a fresh block scope.
2729    pub fn push_scope(&mut self) {
2730        let env = self.cur_env();
2731        self.frames.last_mut().unwrap().env = child_env(env);
2732    }
2733
2734    /// Open a scope that is also the activation's VARIABLE environment, and
2735    /// return the previous one so the caller can restore it.
2736    ///
2737    /// A block scope is not enough for a strict direct `eval`: `var` and a
2738    /// hoisted function declaration bind to `base_env`, so they walked straight
2739    /// past a plain `push_scope` and still landed in the caller's function
2740    /// scope. Only `let`/`const` were contained.
2741    pub fn push_var_scope(&mut self) -> Env {
2742        let env = child_env(self.cur_env());
2743        let f = self.frames.last_mut().unwrap();
2744        let prev = std::mem::replace(&mut f.base_env, env.clone());
2745        f.env = env;
2746        prev
2747    }
2748
2749    /// Restore the variable environment a `push_var_scope` replaced.
2750    pub fn pop_var_scope(&mut self, prev: Env) {
2751        let f = self.frames.last_mut().unwrap();
2752        f.env = prev.clone();
2753        f.base_env = prev;
2754    }
2755
2756    /// Leave the innermost block scope (never pops past the activation's base).
2757    pub fn pop_scope(&mut self) {
2758        let cur = self.cur_env();
2759        if Rc::ptr_eq(&cur, &self.frame().base_env) {
2760            return;
2761        }
2762        let parent = cur.borrow().parent.clone();
2763        if let Some(p) = parent {
2764            self.frames.last_mut().unwrap().env = p;
2765        }
2766    }
2767
2768    /// Replace the innermost block scope with a fresh copy of its bindings — the
2769    /// per-iteration environment a `for (let i …)` loop creates, so a closure made
2770    /// in one iteration keeps that iteration's value.
2771    pub fn copy_scope(&mut self) {
2772        let cur = self.cur_env();
2773        if Rc::ptr_eq(&cur, &self.frame().base_env) {
2774            return;
2775        }
2776        let parent = cur.borrow().parent.clone();
2777        let fresh = new_env(parent);
2778        fresh.borrow_mut().vars = cur.borrow().vars.clone();
2779        self.frames.last_mut().unwrap().env = fresh;
2780    }
2781
2782    /// The current block-scope env, for save/restore across a nested chunk.
2783    pub fn scope_snapshot(&self) -> Env {
2784        self.cur_env()
2785    }
2786    pub fn restore_scope(&mut self, env: Env) {
2787        self.frames.last_mut().unwrap().env = env;
2788    }
2789    pub fn set_global(&mut self, name: &str, val: Value) {
2790        self.globals.insert(name.to_string(), val);
2791    }
2792
2793    // ── output capture ───────────────────────────────────────────────────
2794    //
2795    // Every write a *program* makes — `console.log`, `process.stdout.write`,
2796    // `print` — funnels through `write_out`, so turning capture on redirects all
2797    // of them at once. Diagnostics the runtime itself emits (the REPL banner, a
2798    // crash traceback from `main`) deliberately do not: they belong to the
2799    // process, not to the program.
2800
2801    /// Start capturing program output in-process. Any text already captured is
2802    /// discarded, so each run starts clean.
2803    pub fn begin_capture(&mut self) {
2804        self.capture = Some(Vec::new());
2805    }
2806
2807    /// Stop capturing and take everything written since [`begin_capture`],
2808    /// returning the empty string when capture was not on. The captured bytes
2809    /// are rendered lossily: this API hands back a `String`, so a program that
2810    /// wrote non-UTF-8 gets `U+FFFD` here even though the same write reaches a
2811    /// real stdout byte-exact. Use [`end_capture_bytes`] to keep those bytes.
2812    ///
2813    /// [`begin_capture`]: JsHost::begin_capture
2814    /// [`end_capture_bytes`]: JsHost::end_capture_bytes
2815    pub fn end_capture(&mut self) -> String {
2816        String::from_utf8_lossy(&self.capture.take().unwrap_or_default()).into_owned()
2817    }
2818
2819    /// Stop capturing and take the raw bytes, without the lossy transcription
2820    /// [`end_capture`] applies.
2821    ///
2822    /// [`end_capture`]: JsHost::end_capture
2823    pub fn end_capture_bytes(&mut self) -> Vec<u8> {
2824        self.capture.take().unwrap_or_default()
2825    }
2826
2827    /// Whether output is being captured — the one thing a caller needs to know
2828    /// before asking the real stream a question (`isTTY`, cursor position).
2829    pub fn capturing(&self) -> bool {
2830        self.capture.is_some()
2831    }
2832
2833    /// Write program output: into the capture buffer when capturing, else to the
2834    /// process stream `stderr` selects. `s` is written verbatim — callers add
2835    /// their own line ending, as `console.log` does and `process.stdout.write`
2836    /// does not.
2837    pub fn write_out(&mut self, s: &str, stderr: bool) {
2838        self.write_out_bytes(s.as_bytes(), stderr);
2839    }
2840
2841    /// Write program output as raw BYTES. `process.stdout.write(buf)` hands Node
2842    /// a byte string and Node writes it through untouched, so a `Buffer` holding
2843    /// `ff fe 41` reaches stdout as those three bytes. Routing it through a Rust
2844    /// `String` first replaced every non-UTF-8 byte with `U+FFFD` — three bytes
2845    /// became seven — so the byte path exists separately from [`write_out`].
2846    ///
2847    /// [`write_out`]: JsHost::write_out
2848    pub fn write_out_bytes(&mut self, bytes: &[u8], stderr: bool) {
2849        if let Some(buf) = &mut self.capture {
2850            buf.extend_from_slice(bytes);
2851            return;
2852        }
2853        use std::io::Write as _;
2854        if stderr {
2855            let mut e = std::io::stderr();
2856            let _ = e.write_all(bytes);
2857            let _ = e.flush();
2858        } else {
2859            let mut o = std::io::stdout();
2860            let _ = o.write_all(bytes);
2861            let _ = o.flush();
2862        }
2863    }
2864    pub fn del_name(&mut self, name: &str) {
2865        if self
2866            .cur_env()
2867            .borrow_mut()
2868            .vars
2869            .shift_remove(name)
2870            .is_some()
2871        {
2872            return;
2873        }
2874        self.globals.shift_remove(name);
2875    }
2876
2877    pub fn current_this(&self) -> Option<Value> {
2878        self.frame().this_obj.clone()
2879    }
2880
2881    /// The running activation's [`ThisState`].
2882    pub fn this_state(&self) -> ThisState {
2883        self.frame().this_state
2884    }
2885
2886    /// Mark the next user-function activation as a derived constructor.
2887    pub fn mark_next_call_derived_ctor(&mut self) {
2888        self.derived_ctor_next = true;
2889    }
2890
2891    /// BindThisValue (9.1.1.3.1) for a `super()` that has just returned: the
2892    /// nearest derived-constructor activation becomes `Bound`. That is the top
2893    /// frame, or — for `super()` inside an arrow — the constructor below the
2894    /// arrow's own frame. `false` when it was already bound: the second call.
2895    pub fn bind_super_this(&mut self) -> bool {
2896        let Some(f) = self
2897            .frames
2898            .iter_mut()
2899            .rev()
2900            .find(|f| f.this_state != ThisState::Plain)
2901        else {
2902            return true;
2903        };
2904        if f.this_state == ThisState::Bound {
2905            return false;
2906        }
2907        f.this_state = ThisState::Bound;
2908        true
2909    }
2910
2911    /// The object a `super()` call substituted for the instance, if any.
2912    ///
2913    /// `construct_class` allocates the instance up front, so when a base
2914    /// constructor RETURNS an object the substitution happens deep inside the
2915    /// VM, after that allocation. This carries it back out. Each
2916    /// `construct_class` saves and restores the previous value around its own
2917    /// run, so a `new` inside a constructor body cannot steal it.
2918    pub fn take_super_replacement(&mut self) -> Option<Value> {
2919        self.super_replacement.take()
2920    }
2921
2922    pub fn swap_super_replacement(&mut self, v: Option<Value>) -> Option<Value> {
2923        std::mem::replace(&mut self.super_replacement, v)
2924    }
2925
2926    /// Rebind the running activation's `this`.
2927    ///
2928    /// Only `super()` does this: when the parent constructor RETURNS an object,
2929    /// 15.7.15 makes that object the derived instance, so the rest of the
2930    /// derived constructor has to write to it rather than to the one allocated
2931    /// before the call.
2932    pub fn set_current_this(&mut self, v: Value) {
2933        if let Some(f) = self.frames.last_mut() {
2934            f.this_obj = Some(v.clone());
2935        }
2936        self.super_replacement = Some(v);
2937    }
2938    /// The callbacks to run for `event`, consuming any `once` registration in
2939    /// the same step — so a listener that re-emits the event cannot re-enter a
2940    /// one-shot handler.
2941    pub fn take_process_listeners(&mut self, event: &str) -> Vec<Value> {
2942        let Some(list) = self.process_listeners.get_mut(event) else {
2943            return Vec::new();
2944        };
2945        let fired: Vec<Value> = list.iter().map(|l| l.f.clone()).collect();
2946        list.retain(|l| !l.once);
2947        fired
2948    }
2949
2950    /// Bind the TOP-LEVEL `this` — the value a `this` outside any function sees.
2951    ///
2952    /// Node answers differently per entry point and both answers are objects:
2953    /// `node f.js` runs a CommonJS module, so top-level `this` is
2954    /// `module.exports`; `node -e` and `node -` run a Script, so it is
2955    /// `globalThis`. Verified on node v26.7.0 —
2956    /// `console.log(this === globalThis, this === module.exports)` is
2957    /// `false true` from a file and `true false` from `-e` and from stdin. It
2958    /// was `undefined` at every entry point here, so `this.x = 1` at module
2959    /// scope threw instead of populating the exports object.
2960    ///
2961    /// Only the base frame is touched: a plain function call still gets its own
2962    /// (`undefined`) binding rather than inheriting this one.
2963    pub fn set_top_this(&mut self, v: Value) {
2964        if let Some(f) = self.frames.first_mut() {
2965            f.this_obj = Some(v);
2966        }
2967    }
2968    pub fn current_env_capture(&self) -> Env {
2969        self.frame().env.clone()
2970    }
2971    pub fn current_new_target(&self) -> Option<Value> {
2972        self.frame().new_target.clone()
2973    }
2974    fn current_home_class(&self) -> Option<Value> {
2975        self.frame().home_class.clone()
2976    }
2977
2978    /// The `(parent_ctor, this_class_fields)` for a running constructor's
2979    /// `super(...)`, derived from the frame's home class.
2980    pub fn super_context(&self) -> (Option<Value>, Vec<(String, Value, bool)>) {
2981        match self.current_home_class() {
2982            Some(cv) => match self.get(&cv) {
2983                Some(JsObj::Class(c)) => (c.parent.clone(), c.fields.clone()),
2984                _ => (None, Vec::new()),
2985            },
2986            None => (None, Vec::new()),
2987        }
2988    }
2989
2990    /// Resolve `super.name` to either the parent-prototype getter (to be invoked
2991    /// by the caller, outside any host borrow) or a directly-usable value.
2992    pub fn super_resolve(&self, name: &str) -> SuperRef {
2993        // A shorthand method in an OBJECT LITERAL resolves `super` through its
2994        // home object's prototype; only a class method has a home CLASS. With
2995        // nothing tracked for the literal case, `{ m() { super.x() } }` had no
2996        // parent to look in and reported the method missing.
2997        if let Some(home) = self.frame().home_object.clone() {
2998            let target = self.proto_of(&home).unwrap_or(Value::Undef);
2999            if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3000                return SuperRef::Getter(getter);
3001            }
3002            return SuperRef::Data(lookup_chain(self, &target, name).unwrap_or(Value::Undef));
3003        }
3004        let parent = match self
3005            .current_home_class()
3006            .and_then(|cv| match self.get(&cv) {
3007                Some(JsObj::Class(c)) => c.parent.clone(),
3008                _ => None,
3009            }) {
3010            Some(p) => p,
3011            None => return SuperRef::Data(Value::Undef),
3012        };
3013        // A STATIC method's home object is the constructor, so `super.x` reads
3014        // off the parent CONSTRUCTOR; an instance method's is the prototype
3015        // object, so it reads off the parent's prototype. Always taking the
3016        // prototype meant `static s() { return super.s(); }` found nothing and
3017        // then tried to call it.
3018        let target = if self.frame().home_static {
3019            parent.clone()
3020        } else {
3021            match self.get(&parent) {
3022                Some(JsObj::Class(pc)) => pc.proto.clone(),
3023                _ => self.fn_prop(&parent, "prototype").unwrap_or(Value::Undef),
3024            }
3025        };
3026        if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3027            return SuperRef::Getter(getter);
3028        }
3029        if let Some(v) = lookup_chain(self, &target, name) {
3030            return SuperRef::Data(v);
3031        }
3032        // A static method lives in the fn-prop side table, not the property map.
3033        SuperRef::Data(self.fn_prop(&target, name).unwrap_or(Value::Undef))
3034    }
3035
3036    // ── signals / errors ─────────────────────────────────────────────────
3037    pub fn take_error(&mut self) -> Option<String> {
3038        self.error.take()
3039    }
3040    pub fn raise_str(&mut self, class: &str, msg: &str) -> String {
3041        let s = if msg.is_empty() {
3042            class.to_string()
3043        } else {
3044            format!("{class}: {msg}")
3045        };
3046        self.error = Some(s.clone());
3047        s
3048    }
3049}
3050
3051// ── error constructors ───────────────────────────────────────────────────────
3052
3053pub fn type_error(msg: &str) -> String {
3054    format!("TypeError: {msg}")
3055}
3056pub fn ref_error(name: &str) -> String {
3057    format!("ReferenceError: {name} is not defined")
3058}
3059
3060/// The error a read of a lexical binding still in its TEMPORAL DEAD ZONE
3061/// raises. Distinct from [`ref_error`] on purpose: node says which of the two
3062/// happened, and the difference is how a reader tells a misspelled name from a
3063/// `let` used above its declaration.
3064pub fn tdz_error(name: &str) -> String {
3065    format!("ReferenceError: Cannot access '{name}' before initialization")
3066}
3067pub fn range_error(msg: &str) -> String {
3068    format!("RangeError: {msg}")
3069}
3070
3071/// V8's `String::kMaxLength` on a 64-bit build, in UTF-16 code units — the
3072/// largest string the engine will materialize.
3073///
3074/// Measured on node v26.7.0 (darwin arm64):
3075/// `require('buffer').constants.MAX_STRING_LENGTH` is `536870888`,
3076/// `'a'.repeat(536870888)` succeeds with that length, and
3077/// `'a'.repeat(536870889)` is `RangeError: Invalid string length`.
3078pub const MAX_STRING_LENGTH: usize = 536_870_888;
3079
3080/// The error V8 raises for a string operation whose RESULT would exceed
3081/// [`MAX_STRING_LENGTH`]. It is raised from the length arithmetic, before any
3082/// allocation: `'a'.repeat(2**40)` throws promptly on node where node-js used to
3083/// sit building a 1 TiB `String` until it was killed.
3084pub fn invalid_string_length() -> String {
3085    range_error("Invalid string length")
3086}
3087
3088/// `ToUint32`-validated array length — ECMA-262 10.4.2.2 `ArrayCreate` step 1
3089/// and 10.4.2.4 `ArraySetLength` step 3.
3090///
3091/// A length is legal only if `ToUint32(v)` equals `ToNumber(v)` exactly, so
3092/// `-1`, `1.5`, `NaN`, `Infinity`, `'x'` and `2**32` are all
3093/// `RangeError: Invalid array length` while `'3'` is `3` and `-0` is `0`
3094/// (measured on node v26.7.0: `new Array(-0).length` is `0`, `a.length = '3'`
3095/// leaves `3`, `a.length = 'x'` throws). node-js validated none of them — it
3096/// built `[-1]` from `new Array(-1)`, silently ignored `a.length = -1`, and sat
3097/// materializing four billion elements for `a.length = 2**32`.
3098pub fn to_array_length(v: &Value) -> Result<usize, String> {
3099    // 10.4.2.4 steps 2-3 run TWO conversions: `ToUint32(value)` and then
3100    // `ToNumber(value)`, compared against each other. Both are observable — a
3101    // counting `valueOf` sees two calls in node and saw one here — and the
3102    // second is what makes `arr.length = 1.5` a RangeError rather than 1.
3103    let u32_pass = to_number_value(v)?;
3104    let n = to_number_value(v)?;
3105    let _ = u32_pass;
3106    // `ToUint32`: truncate toward zero, then modulo 2^32.
3107    let u = if n.is_finite() {
3108        (n.trunc() as i64).rem_euclid(1i64 << 32) as u32
3109    } else {
3110        0
3111    };
3112    // `-0` compares equal to `0` here, which is what makes `new Array(-0)` legal.
3113    if (u as f64) != n {
3114        return Err(range_error("Invalid array length"));
3115    }
3116    Ok(u as usize)
3117}
3118
3119/// A Node *coded* error raised from the JS layer: `Name [ERR_CODE]: message`.
3120///
3121/// `builtins::synth_error` parses that head back apart, so the bracketed code
3122/// becomes the enumerable `err.code` that `err.code === 'ERR_INVALID_URL'`-style
3123/// handling reads. Writing the head by hand at each throw site is what left a
3124/// dozen of them with `err.code === undefined` while the message matched.
3125///
3126/// Use this for errors Node raises from `lib/internal/errors.js`, whose `.name`
3127/// is left bracketed while the stack is captured and therefore shows up in both
3128/// `String(err)` and `err.stack` — measured on v26.7.0:
3129///
3130/// ```text
3131/// process.exit(1.5) -> RangeError [ERR_OUT_OF_RANGE]: The value of "code" …
3132/// ```
3133pub fn coded_error(class: &str, code: &str, msg: &str) -> String {
3134    format!("{class} [{code}]: {msg}")
3135}
3136
3137/// The marker `plain_coded_error` hides a code behind, and `synth_error` strips.
3138pub const CODE_MARK: &str = "\u{1}code:";
3139
3140/// Marks an error string as a `DOMException` carrying a WHATWG error NAME
3141/// rather than one of the ECMAScript error classes. WebCrypto and the abort
3142/// APIs reject with these, and the name (`NotSupportedError`) is not a class
3143/// `synth_error` could otherwise recognise.
3144pub const DOM_MARK: &str = "\u{1}dom:";
3145
3146/// A `DOMException` error string: `name` is the WHATWG error name.
3147pub fn dom_error(name: &str, msg: &str) -> String {
3148    format!("{DOM_MARK}{name}\u{1}{msg}")
3149}
3150
3151/// A Node coded error raised from the *native* layer: `.code` is set, but the
3152/// name is never bracketed, so `String(err)` is the plain `Name: message`.
3153///
3154/// The distinction is observable and is not a stylistic choice — on v26.7.0,
3155/// `String(new URL("/x") error)` is `TypeError: Invalid URL` with
3156/// `.code === 'ERR_INVALID_URL'`, while the JS-layer `process.exit(1.5)` error
3157/// brackets its code into the very same two reads. Encoding both through one
3158/// `Name [CODE]:` head would have to pick one and be wrong about the other.
3159///
3160/// The code rides in a marker at the head of the message rather than in the
3161/// error class, because the class text is exactly what must NOT carry it. The
3162/// marker is an internal wire format between a throw site and `synth_error`; it
3163/// never survives into a `.message`.
3164pub fn plain_coded_error(class: &str, code: &str, msg: &str) -> String {
3165    format!("{class}: {CODE_MARK}{code}\u{1}{msg}")
3166}
3167
3168/// Marks the start of the extra string own properties a
3169/// [`plain_coded_error_with`] error carries after its message.
3170pub const FIELDS_MARK: char = '\u{2}';
3171
3172/// [`plain_coded_error`] plus extra enumerable string own properties, set after
3173/// `code` in the order given — `new URL('x', 'nope')` throws with
3174/// `Object.keys(e)` reading `["code","input","base"]`.
3175///
3176/// Each field is `key\u{3}<byte length>\u{3}value`, so a value (a URL input is
3177/// arbitrary user text) may carry any character, the separators included.
3178pub fn plain_coded_error_with(class: &str, code: &str, msg: &str, fields: &[(&str, &str)]) -> String {
3179    let mut s = plain_coded_error(class, code, msg);
3180    s.push(FIELDS_MARK);
3181    for (k, v) in fields {
3182        s.push_str(&format!("{k}\u{3}{}\u{3}{v}", v.len()));
3183    }
3184    s
3185}
3186
3187/// An error string as a person reads it: `TypeError: Invalid URL`, with the
3188/// internal code and field markers of [`plain_coded_error`] /
3189/// [`plain_coded_error_with`] removed. An uncaught native error is printed
3190/// from its string, and printed the wire format (`\u{1}code:ERR_INVALID_URL…`).
3191pub fn plain_error_text(e: &str) -> String {
3192    let Some(i) = e.find(CODE_MARK) else {
3193        return e.to_string();
3194    };
3195    let (head, rest) = e.split_at(i);
3196    match rest[CODE_MARK.len()..].split_once('\u{1}') {
3197        Some((_, m)) => format!("{head}{}", split_error_fields(m).0),
3198        None => e.to_string(),
3199    }
3200}
3201
3202/// Split a [`plain_coded_error_with`] message back into the message and its
3203/// fields. A message with no field mark comes back whole with no fields.
3204pub fn split_error_fields(msg: &str) -> (&str, Vec<(&str, &str)>) {
3205    let Some((head, mut rest)) = msg.split_once(FIELDS_MARK) else {
3206        return (msg, Vec::new());
3207    };
3208    let mut fields = Vec::new();
3209    while let Some((k, tail)) = rest.split_once('\u{3}') {
3210        let Some((len, tail)) = tail.split_once('\u{3}') else { break };
3211        let Ok(len) = len.parse::<usize>() else { break };
3212        let Some(v) = tail.get(..len) else { break };
3213        fields.push((k, v));
3214        rest = &tail[len..];
3215    }
3216    (head, fields)
3217}
3218
3219/// `TypeError [ERR_INVALID_ARG_TYPE]: The "<name>" <kind> must be of type
3220/// <expected>. Received …` — Node's single most common argument rejection.
3221pub fn invalid_arg_type(name: &str, kind: &str, expected: &str, v: &Value) -> String {
3222    coded_error(
3223        "TypeError",
3224        "ERR_INVALID_ARG_TYPE",
3225        &format!(
3226            "The \"{name}\" {kind} must be of type {expected}. Received {}",
3227            crate::stdlib::received_desc(v)
3228        ),
3229    )
3230}
3231
3232// ── the fusevm run plumbing ──────────────────────────────────────────────────
3233
3234thread_local! {
3235    static DEBUG_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3236}
3237
3238/// Enable/disable DAP debug execution (`node --dap`).
3239pub fn set_debug_mode(on: bool) {
3240    DEBUG_MODE.with(|d| d.set(on));
3241}
3242
3243// ── join cycle detection ─────────────────────────────────────────────────────
3244
3245thread_local! {
3246    /// Heap handles whose join is in progress, innermost last — V8's JoinStack.
3247    static JOIN_STACK: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
3248}
3249
3250/// V8's `JoinStackPush`: record that `v` is being joined, or report `false` if
3251/// it already is.
3252///
3253/// `Array.prototype.join` (and `toString`/`toLocaleString`, which route through
3254/// it) is the one place the language walks an object graph with no depth bound,
3255/// so every engine cuts re-entrance here: a receiver already on the stack
3256/// contributes the EMPTY STRING rather than recursing. Measured on node v26.7.0,
3257/// `const a=[1]; a.push(a); a.push(2); a.join('-')` is `"1--2"`, and
3258/// `String(a)`/`` `${a}` `` on `a=[a]` are both `""`. node-js had no such cut and
3259/// recursed until the native stack overflowed, ABORTING the process (exit 134) —
3260/// uncatchable, where node returns a string.
3261///
3262/// Only re-entrance is cut, not repetition: `[a,a].join('|')` still renders `a`
3263/// twice, because the first render pops before the second pushes.
3264///
3265/// A `true` return MUST be paired with [`join_stack_pop`].
3266pub fn join_stack_push(v: &Value) -> bool {
3267    match v {
3268        Value::Obj(i) => JOIN_STACK.with(|s| {
3269            let mut s = s.borrow_mut();
3270            if s.contains(i) {
3271                false
3272            } else {
3273                s.push(*i);
3274                true
3275            }
3276        }),
3277        _ => true,
3278    }
3279}
3280
3281/// Pop the innermost [`join_stack_push`].
3282pub fn join_stack_pop() {
3283    JOIN_STACK.with(|s| {
3284        s.borrow_mut().pop();
3285    });
3286}
3287
3288// ── native stack guard ───────────────────────────────────────────────────────
3289
3290thread_local! {
3291    /// Lowest stack address a nested run may start from, or 0 before the
3292    /// running thread's bounds have been measured. Cached because the pthread
3293    /// query is a syscall-free but non-trivial read and this is on every call.
3294    static STACK_FLOOR: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
3295}
3296
3297/// Stack left unusable below the floor, as a fraction of the whole stack: the
3298/// throw itself still has to unwind, build an `Error`, capture `.stack` and run
3299/// whatever `catch` receives it, all of which needs room *below* the deepest
3300/// call that was allowed.
3301const STACK_RESERVE_DIVISOR: usize = 8;
3302/// Floor of that reserve, for a thread whose stack is small enough that an
3303/// eighth of it would not cover the unwind.
3304const STACK_RESERVE_MIN: usize = 512 * 1024;
3305/// Reserve assumed on a platform whose stack bounds cannot be queried. Deliberately
3306/// large relative to a default 8 MiB stack — over-reserving costs recursion
3307/// depth, under-reserving costs the process.
3308const STACK_RESERVE_FALLBACK: usize = 1024 * 1024;
3309
3310/// The address of a local in the caller's frame — how far down the stack
3311/// execution currently is. `black_box` keeps the probe from being optimized into
3312/// a different frame.
3313fn stack_pointer() -> usize {
3314    let probe = 0u8;
3315    std::hint::black_box(&probe) as *const u8 as usize
3316}
3317
3318/// The running thread's `(lowest address, size)` stack bounds.
3319///
3320/// Asked of pthread rather than assumed, because the three threads that run JS
3321/// have three different stacks: the `node` binary's own (`main.rs` reserves
3322/// [`crate::JS_STACK_SIZE`]), a `worker_threads` thread's, and a `cargo test`
3323/// harness thread's. A fixed byte budget would be wrong on two of the three.
3324fn stack_bounds() -> Option<(usize, usize)> {
3325    #[cfg(target_vendor = "apple")]
3326    {
3327        // SAFETY: both calls are pure reads of the calling thread's own
3328        // pthread record; neither allocates nor can fail.
3329        unsafe {
3330            let me = libc::pthread_self();
3331            let top = libc::pthread_get_stackaddr_np(me) as usize;
3332            let size = libc::pthread_get_stacksize_np(me);
3333            if size == 0 || top < size {
3334                return None;
3335            }
3336            Some((top - size, size))
3337        }
3338    }
3339    #[cfg(target_os = "linux")]
3340    {
3341        // SAFETY: `attr` is initialized by `pthread_getattr_np` before it is
3342        // read, only read on the success path, and destroyed on every path.
3343        unsafe {
3344            let mut attr: libc::pthread_attr_t = std::mem::zeroed();
3345            if libc::pthread_getattr_np(libc::pthread_self(), &mut attr) != 0 {
3346                return None;
3347            }
3348            let mut low: *mut libc::c_void = std::ptr::null_mut();
3349            let mut size: libc::size_t = 0;
3350            let ok = libc::pthread_attr_getstack(&attr, &mut low, &mut size) == 0;
3351            libc::pthread_attr_destroy(&mut attr);
3352            if ok && size != 0 {
3353                return Some((low as usize, size));
3354            }
3355            None
3356        }
3357    }
3358    #[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
3359    {
3360        None
3361    }
3362}
3363
3364/// The stack address below which a further nested VM run must throw instead of
3365/// recursing.
3366///
3367/// Every JS call is a Rust-level recursion — `run_user_func_nt` pushes a
3368/// [`Frame`], then `run_chunk_on` builds a whole new `fusevm::VM` on the stack
3369/// and runs the body, whose own calls land back here. Unbounded JS recursion
3370/// therefore used to exhaust the OS stack and ABORT: `fatal runtime error:
3371/// stack overflow`, exit 134, which no `try`/`catch` can see. V8 throws a
3372/// catchable `RangeError: Maximum call stack size exceeded` instead (measured on
3373/// node v26.7.0: `let d=0; function f(){d++;f()}` reports depth 9901).
3374///
3375/// The floor is derived from the thread's real bounds rather than a frame count
3376/// because a node-js frame has no fixed size — a debug build spends ~98 KiB per
3377/// JS call (measured: `node -e 'function f(n){…f(n-1)}'` survived 83 on an 8 MiB
3378/// stack and no more), a release build far less, and a native builtin recursing
3379/// through a user callback spends a different amount again.
3380fn stack_floor() -> usize {
3381    let cached = STACK_FLOOR.with(|c| c.get());
3382    if cached != 0 {
3383        return cached;
3384    }
3385    let floor = match stack_bounds() {
3386        Some((low, size)) => low + (size / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN),
3387        None => stack_pointer().saturating_sub(STACK_RESERVE_FALLBACK),
3388    };
3389    STACK_FLOOR.with(|c| c.set(floor));
3390    floor
3391}
3392
3393/// Stack given to each generator/async coroutine.
3394///
3395/// corosensei's default is 1 MiB, which at a debug build's ~98 KiB per JS call
3396/// left a `function*` body barely ten frames of recursion before it walked off
3397/// the end. The mapping is `PROT_NONE` reserved and `mprotect`ed, so the cost of
3398/// a larger one is address space, not resident memory — but it IS per live
3399/// generator, so this stays far below the entry thread's
3400/// [`crate::JS_STACK_SIZE`]: a program with thousands of concurrent async calls
3401/// has thousands of these.
3402const CORO_STACK_SIZE: usize = 16 * 1024 * 1024;
3403
3404/// The [`stack_floor`] that applies while a coroutine on `stack` is running.
3405fn coro_stack_floor(stack: &impl corosensei::stack::Stack) -> usize {
3406    stack.limit().get() + (CORO_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN)
3407}
3408
3409/// corosensei's own `DefaultStack::default()` size, used only when the
3410/// [`CORO_STACK_SIZE`] reservation is refused and the coroutine therefore runs
3411/// on a stack whose bounds are not ours to read.
3412const CORO_FALLBACK_STACK_SIZE: usize = 1024 * 1024;
3413
3414/// Give a coroutine whose stack bounds are unknown a floor measured from where
3415/// its body starts. Called once, at body entry, on the coroutine's own stack.
3416fn ensure_coroutine_floor() {
3417    if STACK_FLOOR.with(|c| c.get()) != 0 {
3418        return;
3419    }
3420    let budget = CORO_FALLBACK_STACK_SIZE
3421        - (CORO_FALLBACK_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN);
3422    STACK_FLOOR.with(|c| c.set(stack_pointer().saturating_sub(budget)));
3423}
3424
3425/// Install `floor` as the current stack floor, returning the previous one.
3426///
3427/// Used around a coroutine resume, which switches to a stack the thread's
3428/// pthread record knows nothing about. A floor of 0 means "not known" and makes
3429/// the next [`stack_floor`] measure again, which is the right answer for the
3430/// entry thread and a conservative one for a fallback coroutine stack.
3431fn swap_stack_floor(floor: usize) -> usize {
3432    STACK_FLOOR.with(|c| c.replace(floor))
3433}
3434
3435/// Whether the native stack is too close to its floor for one more nested run.
3436pub fn stack_exhausted() -> bool {
3437    stack_pointer() <= stack_floor()
3438}
3439
3440/// The error V8 raises when the call stack is exhausted. Catchable, and with the
3441/// `RangeError` constructor node uses — not a `panic!`.
3442pub fn stack_overflow_error() -> String {
3443    range_error("Maximum call stack size exceeded")
3444}
3445
3446/// Pool key for the body of user function `def_id`.
3447pub fn func_key(def_id: usize) -> u64 {
3448    1 << 40 | def_id as u64
3449}
3450
3451/// Pool key for one part of `try` statement `try_id`: 0 = the block, 1 = the
3452/// handler, 2 = the finalizer.
3453pub fn try_key(try_id: usize, part: u64) -> u64 {
3454    2 << 40 | (try_id as u64) << 2 | part
3455}
3456
3457thread_local! {
3458    /// VMs that have finished a run, kept for the next one — grouped by the
3459    /// chunk they still hold.
3460    ///
3461    /// Every JS call, every `try` block and every generator step runs its chunk
3462    /// through [`run_chunk_on`], which used to build a `fusevm::VM` from
3463    /// scratch: three `Vec` allocations, 70 `register_builtin` writes, an `Arc`
3464    /// for the numeric hook, and the JIT enable — per call. `fib(27)` makes
3465    /// 400k calls, so it built 400k VMs to run 23 ops each.
3466    ///
3467    /// Worse, the caller had to hand over an OWNED `Chunk`, so every call also
3468    /// deep-copied the function's whole compiled body: six `Vec`s, a `String`,
3469    /// and `sub_chunks` recursively. Keying the pool by chunk means a repeated
3470    /// call takes back the VM that already holds that body and copies nothing:
3471    /// `VM::reset` is handed the chunk the VM was already carrying.
3472    ///
3473    /// `VM::reset` keeps the builtin table, the hooks and the JIT setting, so a
3474    /// recycled VM needs none of that again. Each key holds a stack of VMs, and
3475    /// a nested (or recursive) call takes the next one, so a key grows to the
3476    /// deepest simultaneous entry into that function and no further.
3477    static VM_POOL: RefCell<rustc_hash::FxHashMap<u64, Vec<VM>>> =
3478        RefCell::new(rustc_hash::FxHashMap::default());
3479}
3480
3481/// An idle VM filed under `key`, if any.
3482fn take_pooled(key: u64) -> Option<VM> {
3483    VM_POOL.with(|p| p.borrow_mut().get_mut(&key).and_then(|v| v.pop()))
3484}
3485
3486/// File a finished VM under `key` for the next run to take.
3487fn put_pooled(key: u64, vm: VM) {
3488    VM_POOL.with(|p| p.borrow_mut().entry(key).or_default().push(vm));
3489}
3490
3491/// Take a VM ready to run `chunk` — recycled if one is idle, otherwise built
3492/// and fitted with the builtins and hooks a fresh VM needs.
3493fn acquire_vm(chunk: Chunk) -> VM {
3494    if let Some(mut vm) = take_pooled(0) {
3495        vm.reset(chunk);
3496        return vm;
3497    }
3498    let mut vm = VM::new(chunk);
3499    crate::builtins::install(&mut vm);
3500    vm.set_numeric_hook(std::sync::Arc::new(|op, a, b| {
3501        crate::builtins::numeric_hook(op, a, b)
3502    }));
3503    // Under `--dap` the tracing JIT would compile hot loops and skip the
3504    // per-statement `DBG_LINE` markers, so debug runs stay on the pure
3505    // interpreter. The `DBG_LINE` builtin fires the debugger line hook; the
3506    // extension seam mirrors pythonrs should the marker emission ever switch.
3507    // The mode is fixed before the first chunk runs, so a pooled VM can never
3508    // come back wearing the wrong one.
3509    if DEBUG_MODE.with(|d| d.get()) {
3510        vm.set_extension_handler(Box::new(|vm, id, _| {
3511            crate::dap::on_ext(vm, id);
3512        }));
3513    } else {
3514        vm.enable_tracing_jit();
3515    }
3516    vm
3517}
3518
3519/// Register every node-js builtin + the numeric hook on a VM, then run it.
3520///
3521/// For a chunk that runs once — a module body, an `eval` — there is nothing to
3522/// key a pool by, so this resets a spare VM with the caller's chunk. Anything
3523/// that runs repeatedly (a function body, a `try` block) goes through
3524/// [`run_chunk_keyed`] instead and never copies its chunk twice.
3525pub fn run_chunk_on(chunk: Chunk) -> Result<Value, String> {
3526    // Checked before the `VM` is built: `VM::new` + `install` are themselves
3527    // several KiB of frame, so a check after them could already have overflowed.
3528    if stack_exhausted() {
3529        return Err(stack_overflow_error());
3530    }
3531    finish_run(0, acquire_vm(chunk))
3532}
3533
3534/// Run the chunk filed under `key`, building it with `make` only if no VM is
3535/// already holding it. A recycled VM re-runs the chunk it kept, so a repeated
3536/// call copies no bytecode at all.
3537pub fn run_chunk_keyed(key: u64, make: impl FnOnce() -> Chunk) -> Result<Value, String> {
3538    if stack_exhausted() {
3539        return Err(stack_overflow_error());
3540    }
3541    let vm = match take_pooled(key) {
3542        Some(mut vm) => {
3543            // Hand the VM back the chunk it is already carrying: `reset` takes
3544            // an owned `Chunk`, and this is the one place where the owned chunk
3545            // costs nothing.
3546            let held = std::mem::take(&mut vm.chunk);
3547            vm.reset(held);
3548            vm
3549        }
3550        None => acquire_vm(make()),
3551    };
3552    finish_run(key, vm)
3553}
3554
3555/// Run a prepared VM to completion and file it back under `key`.
3556fn finish_run(key: u64, mut vm: VM) -> Result<Value, String> {
3557    let outcome = vm.run();
3558    let result = match outcome {
3559        _ if with_host(|h| h.error.is_some()) => {
3560            Err(with_host(|h| h.take_error()).expect("just checked"))
3561        }
3562        VMResult::Ok(v) => Ok(v),
3563        VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
3564        VMResult::Error(e) => Err(e),
3565    };
3566    put_pooled(key, vm);
3567    result
3568}
3569
3570/// Run `chunk` in the GLOBAL scope instead of the caller's.
3571///
3572/// `run_chunk_on` executes on whatever frame is current, so a nested run sees —
3573/// and can shadow — the *calling function's* locals. That is right for a direct
3574/// `eval`, and wrong for every other runtime-source construct: a `new Function`
3575/// body, an indirect `eval` and `vm.runInThisContext` are all specified to run
3576/// in the global scope (ECMA-262 19.2.1.1 `PerformEval` with a null
3577/// `strictCaller`/`direct` pair; `FunctionBody` is instantiated with the *global*
3578/// environment, 20.2.1.1.1 step 26). Measured against node v26.7.0,
3579/// `function outer(){ let loc = 42; return vm.runInThisContext('typeof loc'); }`
3580/// is `"undefined"` there and was `"number"` here.
3581///
3582/// A `var` the chunk itself declares lands in the top-level scope and persists,
3583/// so successive `vm.runInThisContext` calls share it.
3584pub fn run_chunk_in_global_scope(chunk: Chunk) -> Result<Value, String> {
3585    // An INDIRECT eval really is global code (19.2.1.1 step 6): its `var`s bind
3586    // to the global object, not to the entry module's wrapper scope. The flag
3587    // that keeps the entry script's own `var`s out of the globals map has to be
3588    // lifted for the duration, or `(0, eval)('var g = 1')` stopped reaching
3589    // `globalThis.g`.
3590    let prev_scope = with_host(|h| std::mem::take(&mut h.module_scope));
3591    let out = run_chunk_in_global_scope_inner(chunk);
3592    with_host(|h| h.module_scope = prev_scope);
3593    out
3594}
3595
3596fn run_chunk_in_global_scope_inner(chunk: Chunk) -> Result<Value, String> {
3597    let global_env = with_host(|h| h.global_env.clone());
3598    with_host(|h| {
3599        h.frames.push(Frame {
3600            env: global_env.clone(),
3601            base_env: global_env,
3602            this_obj: None,
3603            new_target: None,
3604            home_class: None,
3605            home_static: false,
3606            home_object: None,
3607            strict: false,
3608            line: 0,
3609            owner: None,
3610            is_module: true,
3611            this_state: ThisState::Plain,
3612        })
3613    });
3614    let r = run_chunk_on(chunk);
3615    with_host(|h| {
3616        h.frames.pop();
3617    });
3618    r
3619}
3620
3621/// Run the top-level program chunk, then drain the event loop (microtasks +
3622/// timers) until quiescent — matching Node, which keeps the process alive while
3623/// pending async work remains.
3624pub fn run_main(chunk: Chunk) -> Result<Value, String> {
3625    with_host(|h| h.module_scope = true);
3626    let r = run_chunk_on(chunk);
3627    with_host(|h| h.signal = None);
3628    if r.is_ok() {
3629        run_event_loop()?;
3630        finish_process_events()?;
3631    }
3632    r
3633}
3634
3635/// The shutdown sequence Node runs once the loop has drained on its own: fire
3636/// `beforeExit` (which MAY schedule more work, in which case the loop runs
3637/// again and `beforeExit` fires again), then fire `exit` exactly once.
3638///
3639/// Neither event fired at all before this existed, so `process.on('exit', …)`
3640/// was a registration with no delivery — a listener whose body printed was
3641/// silently dropped, and one that set `process.exitCode` could not affect the
3642/// status. Measured on node v26.7.0,
3643/// `process.on('exit', c => console.log('exit', c))` prints `exit 0`.
3644///
3645/// An explicit `process.exit()` never reaches here (it leaves the process from
3646/// inside the builtin), and neither does an uncaught exception — matching
3647/// Node, where `beforeExit` is skipped on both paths.
3648fn finish_process_events() -> Result<(), String> {
3649    // Bounded: a `beforeExit` listener that re-arms work every time would spin
3650    // forever, exactly as it does in Node, but a runaway here would hang a
3651    // parity run with no output, so it is capped and then treated as drained.
3652    for _ in 0..1000 {
3653        let code = with_host(|h| h.exit_code).unwrap_or(0);
3654        if !crate::stdlib::process::emit_before_exit(code)? {
3655            break;
3656        }
3657        let more =
3658            with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
3659        if !more {
3660            break;
3661        }
3662        run_event_loop()?;
3663    }
3664    let code = with_host(|h| h.exit_code).unwrap_or(0);
3665    crate::stdlib::process::emit_exit_event(code)
3666}
3667
3668// ── formatting ───────────────────────────────────────────────────────────────
3669
3670/// Format a JS number exactly as `Number.prototype.toString` does for the common
3671/// range (no exponential-notation threshold handling for very large/small).
3672pub fn fmt_number(f: f64) -> String {
3673    if f.is_nan() {
3674        return "NaN".into();
3675    }
3676    if f.is_infinite() {
3677        return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
3678    }
3679    if f == 0.0 {
3680        // Covers -0.0 too: (-0).toString() === "0".
3681        return "0".into();
3682    }
3683    if f < 0.0 {
3684        return format!("-{}", js_number_repr(-f));
3685    }
3686    js_number_repr(f)
3687}
3688
3689/// If `k` is an array-index property key, return its numeric value. Per
3690/// ECMAScript, a String property key `P` is an array index iff
3691/// `ToString(ToUint32(P)) === P` and `ToUint32(P) !== 2^32 - 1` — i.e. a
3692/// canonical decimal (no leading zeros, no sign) in the range `0..=2^32-2`.
3693pub fn array_index(k: &str) -> Option<u32> {
3694    if k.is_empty() {
3695        return None;
3696    }
3697    if k == "0" {
3698        return Some(0);
3699    }
3700    // A leading '0' (other than the lone "0" above) is non-canonical.
3701    if k.as_bytes()[0] == b'0' {
3702        return None;
3703    }
3704    if !k.bytes().all(|b| b.is_ascii_digit()) {
3705        return None;
3706    }
3707    match k.parse::<u64>() {
3708        // Array index must be < 2^32-1; u32::MAX == 2^32-1 is excluded.
3709        Ok(n) if n < u32::MAX as u64 => Some(n as u32),
3710        _ => None,
3711    }
3712}
3713
3714/// Compare two own-property keys for `OrdinaryOwnPropertyKeys` enumeration order:
3715/// integer-index keys sort ascending-numeric and precede all string keys; two
3716/// non-index keys compare `Equal` so a *stable* sort leaves them in insertion
3717/// order. (Symbols are stored as `@@…`/`#…` string keys and are non-index, so
3718/// they also fall into the stable-insertion-order tail.)
3719pub fn key_order_cmp(a: &str, b: &str) -> std::cmp::Ordering {
3720    use std::cmp::Ordering;
3721    match (array_index(a), array_index(b)) {
3722        (Some(x), Some(y)) => x.cmp(&y),
3723        (Some(_), None) => Ordering::Less,
3724        (None, Some(_)) => Ordering::Greater,
3725        (None, None) => Ordering::Equal,
3726    }
3727}
3728
3729/// Reorder an object's own-property map into `OrdinaryOwnPropertyKeys` order in
3730/// place: array-index keys ascending first, then the remaining keys in their
3731/// existing (insertion) order. A no-op unless at least one index key is present,
3732/// so the overwhelmingly common all-string-key object keeps its exact order and
3733/// pays nothing. `IndexMap::sort_by` is a stable sort.
3734pub fn canonicalize_own_keys(props: &mut IndexMap<String, Value>) {
3735    if props.keys().any(|k| array_index(k).is_some()) {
3736        props.sort_by(|ak, _, bk, _| key_order_cmp(ak, bk));
3737    }
3738}
3739
3740/// ECMAScript `Number::toString` layout for a positive, finite, nonzero value.
3741///
3742/// Rust's `Display`/`LowerExp` give the shortest round-trip decimal digits, but
3743/// NOT JavaScript's exponential-vs-fixed threshold: Rust prints `1e21` as
3744/// `1000000000000000000000` and `1e-7` as `0.0000001`, whereas JS prints `1e+21`
3745/// and `1e-7`. So we take the shortest digits from `{:e}` and re-lay them out per
3746/// the spec (steps 5–10 of Number::toString): `k` significant digits `s` with
3747/// decimal exponent `n` (value = s × 10^(n−k)); exponential form only when
3748/// `n > 21` or `n ≤ -6`.
3749fn js_number_repr(a: f64) -> String {
3750    // `{:e}` yields `d[.ddd]e<exp>` with the mantissa in [1, 10) and shortest
3751    // round-trip digits. Split it into the digit string `s` and exponent `E`.
3752    let sci = format!("{a:e}");
3753    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
3754    let e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
3755    let s: String = mant.chars().filter(|c| *c != '.').collect();
3756    let k = s.len() as i32; // number of significant digits
3757    let n = e + 1; // value = s × 10^(n−k), 10^(k−1) ≤ s < 10^k
3758
3759    if k <= n && n <= 21 {
3760        // Integer with trailing zeros: all digits, then n−k zeros.
3761        let mut out = s;
3762        out.push_str(&"0".repeat((n - k) as usize));
3763        out
3764    } else if 0 < n && n <= 21 {
3765        // Decimal point inside the digit run: n digits, '.', the rest.
3766        format!("{}.{}", &s[..n as usize], &s[n as usize..])
3767    } else if -6 < n && n <= 0 {
3768        // Leading "0." then (−n) zeros then all digits.
3769        format!("0.{}{}", "0".repeat((-n) as usize), s)
3770    } else {
3771        // Exponential form. Exponent digit is n−1, always signed.
3772        let exp = n - 1;
3773        let sign = if exp >= 0 { '+' } else { '-' };
3774        let mag = exp.abs();
3775        if k == 1 {
3776            format!("{s}e{sign}{mag}")
3777        } else {
3778            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
3779        }
3780    }
3781}
3782
3783impl JsHost {
3784    /// The `typeof` string for `v`.
3785    pub fn type_of(&self, v: &Value) -> &'static str {
3786        match v {
3787            Value::Undef => "undefined",
3788            Value::Bool(_) => "boolean",
3789            Value::Int(_) | Value::Float(_) => "number",
3790            Value::Str(_) => "string",
3791            Value::Obj(_) => match self.get(v) {
3792                Some(JsObj::Str(_)) => "string",
3793                // 10.5's `[[Call]]` slot exists on a proxy exactly when its
3794                // target is callable, so `typeof` classifies by the target —
3795                // `typeof new Proxy(function(){}, {})` is `'function'`. The walk
3796                // is bounded: a proxy of a proxy defers again.
3797                Some(JsObj::Proxy { target, .. }) => {
3798                    let mut cur = target;
3799                    for _ in 0..100 {
3800                        match self.get(cur) {
3801                            Some(JsObj::Proxy { target: t, .. }) => cur = t,
3802                            _ => break,
3803                        }
3804                    }
3805                    if is_callable(self, cur) {
3806                        "function"
3807                    } else {
3808                        "object"
3809                    }
3810                }
3811                Some(JsObj::Func(_))
3812                | Some(JsObj::BoundMethod { .. })
3813                | Some(JsObj::BoundFunc { .. })
3814                | Some(JsObj::Class(_)) => "function",
3815                // A Builtin is a callable (`Array`, `parseInt`, `Math.floor`) —
3816                // `typeof === "function"` — EXCEPT the non-callable namespace
3817                // objects (`Math`, `JSON`, `require('fs')`, …) which are "object".
3818                Some(JsObj::Builtin(n)) => {
3819                    if builtin_is_callable(n) {
3820                        "function"
3821                    } else {
3822                        "object"
3823                    }
3824                }
3825                Some(JsObj::Symbol { .. }) => "symbol",
3826                Some(JsObj::BigInt(_)) => "bigint",
3827                _ => "object", // arrays, objects, null, Map/Set, generators
3828            },
3829            _ => "object",
3830        }
3831    }
3832
3833    /// JS truthiness: false / 0 / -0 / NaN / "" / null / undefined are falsy.
3834    pub fn truthy(&self, v: &Value) -> bool {
3835        match v {
3836            Value::Undef => false,
3837            Value::Bool(b) => *b,
3838            Value::Int(n) => *n != 0,
3839            Value::Float(f) => *f != 0.0 && !f.is_nan(),
3840            Value::Str(s) => !s.is_empty(),
3841            Value::Obj(_) => match self.get(v) {
3842                Some(JsObj::Str(s)) => !s.is_empty(),
3843                Some(JsObj::Null) => false,
3844                Some(JsObj::BigInt(b)) => !num_traits::Zero::is_zero(b),
3845                _ => true, // arrays, objects, functions
3846            },
3847            _ => true,
3848        }
3849    }
3850
3851    /// Coerce to a number (`ToNumber`): the arithmetic-context conversion.
3852    pub fn to_number(&self, v: &Value) -> f64 {
3853        match v {
3854            Value::Undef => f64::NAN,
3855            Value::Bool(b) => {
3856                if *b {
3857                    1.0
3858                } else {
3859                    0.0
3860                }
3861            }
3862            Value::Int(n) => *n as f64,
3863            Value::Float(f) => *f,
3864            Value::Str(s) => str_to_number(s),
3865            Value::Obj(_) => match self.get(v) {
3866                Some(JsObj::Str(s)) => str_to_number(s),
3867                Some(JsObj::Null) => 0.0,
3868                Some(JsObj::BigInt(b)) => bigint_to_f64(b),
3869                Some(JsObj::Array(items)) => {
3870                    // [] -> 0, [x] -> ToNumber(x), else NaN.
3871                    if items.is_empty() {
3872                        0.0
3873                    } else if items.len() == 1 {
3874                        self.to_number(&items[0])
3875                    } else {
3876                        f64::NAN
3877                    }
3878                }
3879                _ => f64::NAN,
3880            },
3881            _ => f64::NAN,
3882        }
3883    }
3884
3885    /// `String(v)` — the string-coercion form (raw, unquoted).
3886    pub fn str_of(&self, v: &Value) -> String {
3887        match v {
3888            Value::Undef => "undefined".into(),
3889            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
3890            Value::Int(n) => n.to_string(),
3891            Value::Float(f) => fmt_number(*f),
3892            Value::Str(s) => (**s).clone(),
3893            Value::Obj(_) => match self.get(v) {
3894                Some(JsObj::Str(s)) => s.clone(),
3895                Some(JsObj::Null) => "null".into(),
3896                Some(JsObj::BigInt(b)) => b.to_string(),
3897                Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
3898                Some(JsObj::Array(items)) => {
3899                    // Array.prototype.toString: comma-join, null/undefined -> "".
3900                    // Guarded by the JoinStack (see `join_stack_push`) so a
3901                    // self-referential array yields "" instead of recursing until
3902                    // the native stack aborts the process.
3903                    if !join_stack_push(v) {
3904                        return String::new();
3905                    }
3906                    let parts: Vec<String> = items
3907                        .iter()
3908                        .map(|x| match x {
3909                            Value::Undef => String::new(),
3910                            _ if self.is_null(x) => String::new(),
3911                            _ => self.str_of(x),
3912                        })
3913                        .collect();
3914                    join_stack_pop();
3915                    parts.join(",")
3916                }
3917                Some(JsObj::Object(props)) => {
3918                    // A native `Buffer` stringifies to its decoded (utf-8)
3919                    // contents, matching `buf.toString()` — needed for `'' + buf`,
3920                    // template interpolation, and `data += chunk` (the pattern
3921                    // Express/body-parser use to read a request body).
3922                    if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Buffer") {
3923                        let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
3924                            Some(JsObj::Array(items)) => {
3925                                items.iter().map(|x| self.to_number(x) as u8).collect()
3926                            }
3927                            _ => Vec::new(),
3928                        };
3929                        String::from_utf8_lossy(&bytes).into_owned()
3930                    } else if let Some(s) = self.error_to_string(v) {
3931                        s
3932                    } else {
3933                        "[object Object]".into()
3934                    }
3935                }
3936                Some(JsObj::Func(f)) => {
3937                    // A function built from runtime source (`new Function`,
3938                    // `vm.compileFunction`) retains the exact text V8 synthesizes
3939                    // for it, so `Function.prototype.toString` reports what Node
3940                    // reports. Ordinary functions carry no source here (the
3941                    // compiler keeps no spans), so they fall back to a placeholder.
3942                    if let Some(src) = self.fn_prop(v, "@@source") {
3943                        return self.str_of(&src);
3944                    }
3945                    let name = self
3946                        .funcs
3947                        .get(f.def_id)
3948                        .map(|d| d.name.clone())
3949                        .unwrap_or_default();
3950                    format!("function {name}() {{ [code] }}")
3951                }
3952                // The native-code form names the FUNCTION, not its key:
3953                // `String(Math.max)` is `function max() { [native code] }`.
3954                Some(JsObj::Builtin(n)) => {
3955                    // The `console` methods are the exception node itself makes:
3956                    // each is a wrapper, so `String(console.log)` is the
3957                    // ANONYMOUS native-code form even though `console.log.name`
3958                    // is `log`. Measured on v26.8.1.
3959                    if n.starts_with("console.") {
3960                        "function () { [native code] }".into()
3961                    } else if let Some(accessor) = crate::builtins::proto_getter_name(n) {
3962                        // An accessor half names itself `get size` / `set
3963                        // arguments`, which `builtin_name` cannot build because
3964                        // it returns a borrowed `&str`.
3965                        format!("function {accessor}() {{ [native code] }}")
3966                    } else {
3967                        format!(
3968                            "function {}() {{ [native code] }}",
3969                            crate::builtins::builtin_name(n)
3970                        )
3971                    }
3972                }
3973                // A method read off an instance names itself the same way the
3974                // prototype method it resolves to does: `String([].slice)` is
3975                // `function slice() { [native code] }`.
3976                Some(JsObj::BoundMethod { name, .. }) => {
3977                    format!("function {name}() {{ [native code] }}")
3978                }
3979                Some(JsObj::BoundFunc { .. }) => "function () { [native code] }".into(),
3980                // `Function.prototype.toString` refuses to expose a proxy's
3981                // target: V8 reports the native-code form for a proxy of ANY
3982                // callable, so `String(new Proxy(function f(){}, {}))` is
3983                // `function () { [native code] }`, not `f`'s source.
3984                Some(JsObj::Proxy { .. }) if is_callable(self, v) => {
3985                    "function () { [native code] }".into()
3986                }
3987                Some(JsObj::Class(c)) => format!("class {} {{ }}", c.name),
3988                Some(JsObj::Symbol { desc, .. }) => {
3989                    // `String(sym)` is allowed (unlike implicit coercion) and yields
3990                    // `Symbol(desc)`.
3991                    match desc {
3992                        Some(d) => format!("Symbol({d})"),
3993                        None => "Symbol()".into(),
3994                    }
3995                }
3996                _ => "[object Object]".into(),
3997            },
3998            _ => "[object Object]".into(),
3999        }
4000    }
4001
4002    /// The `Symbol.toStringTag` string `util.inspect` renders as a `[Tag]`
4003    /// prefix. V8 suppresses the tag when it is an OWN ENUMERABLE property,
4004    /// because it is then already listed as a `Symbol(Symbol.toStringTag): …`
4005    /// entry and showing it twice would be wrong.
4006    ///
4007    /// Only a DATA property is seen. A tag supplied by a prototype getter
4008    /// (`class C { get [Symbol.toStringTag]() { … } }`) would need a JS call,
4009    /// which cannot run under the host borrow `inspect` holds — such an object
4010    /// prints without the prefix.
4011    /// `[String: 'ab']` / `[Number: 1]` / `[Boolean: false]` — how node renders
4012    /// a primitive wrapper, distinguishing it from the bare primitive.
4013    fn inspect_wrapper(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Option<String> {
4014        let prim = match self.get(v) {
4015            Some(JsObj::Object(p)) => p.get("@@primitive").cloned()?,
4016            _ => return None,
4017        };
4018        let ctor = match &prim {
4019            Value::Bool(_) => "Boolean",
4020            Value::Int(_) | Value::Float(_) => "Number",
4021            _ => "String",
4022        };
4023        let head = format!("[{ctor}: {}]", self.inspect_lvl(&prim, indent, st));
4024        // Extra own properties still print, as `[String: 'ab'] { tag: 1 }`. The
4025        // boxed characters are NOT extras — node hides the index properties of
4026        // a String wrapper, showing only what was added to it.
4027        let width = if ctor == "String" {
4028            self.str_of(&prim).chars().count()
4029        } else {
4030            0
4031        };
4032        let extras: Vec<String> = match self.get(v) {
4033            Some(JsObj::Object(p)) => p
4034                .iter()
4035                .filter(|(k, _)| {
4036                    !k.starts_with("@@")
4037                        && !k.starts_with('#')
4038                        && self.prop_attrs(v, k).enumerable
4039                        && !k.parse::<usize>().is_ok_and(|i| i < width)
4040                })
4041                .map(|(k, val)| {
4042                    format!("{}: {}", fmt_key(k), self.inspect_lvl(val, indent + 2, st))
4043                })
4044                .collect(),
4045            _ => Vec::new(),
4046        };
4047        if extras.is_empty() {
4048            return Some(head);
4049        }
4050        Some(self.render_object(&extras, &format!("{head} "), indent, st))
4051    }
4052
4053    /// The `key: value` parts for own properties a script attached to an exotic
4054    /// whose contents are internal slots — `new Map([['k',1]])` with `m.x = 5`
4055    /// prints `Map(1) { 'k' => 1, x: 5 }`.
4056    fn side_table_parts(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Vec<String> {
4057        self.fn_prop_keys(v)
4058            .into_iter()
4059            .filter(|k| {
4060                !k.starts_with("@@")
4061                    && !k.starts_with('#')
4062                    && !is_symbol_key(k)
4063                    && self.prop_attrs(v, k).enumerable
4064            })
4065            .map(|k| {
4066                let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4067                format!(
4068                    "{}: {}",
4069                    fmt_key(&k),
4070                    self.inspect_lvl(&val, indent + 2, st)
4071                )
4072            })
4073            .collect()
4074    }
4075
4076    fn inspect_tag(&self, v: &Value) -> Option<String> {
4077        let own = matches!(self.get(v), Some(JsObj::Object(p)) if p.contains_key("@@toStringTag"));
4078        if own && self.prop_attrs(v, "@@toStringTag").enumerable {
4079            return None;
4080        }
4081        let t = lookup_chain(self, v, "@@toStringTag")?;
4082        self.as_str(&t)
4083    }
4084
4085    /// `console.log`-style rendering of a top-level argument: bare strings print
4086    /// raw; everything else uses `inspect`.
4087    pub fn console_format(&self, v: &Value) -> String {
4088        match v {
4089            Value::Str(_) => self.str_of(v),
4090            Value::Obj(_) if matches!(self.get(v), Some(JsObj::Str(_))) => self.str_of(v),
4091            _ => self.inspect(v),
4092        }
4093    }
4094
4095    /// `util.inspect`-style rendering (nested; strings quoted).
4096    pub fn inspect(&self, v: &Value) -> String {
4097        self.inspect_lvl(v, 0, &mut InspectCycles::default())
4098    }
4099
4100    /// `util.inspect` at a given indentation level, with the cycle guard applied
4101    /// around the object cases.
4102    ///
4103    /// A value already being rendered further up the chain is a CYCLE, and Node
4104    /// marks both ends of it: the back-edge prints `[Circular *N]` and the
4105    /// object it points back at is prefixed `<ref *N>`. Without this the walk
4106    /// only stopped when the depth limit turned the back-edge into `[Object]`,
4107    /// so `const c={a:1}; c.c=c` printed the misleading
4108    /// `{ a: 1, c: { a: 1, c: { a: 1, c: [Object] } } }` instead of
4109    /// `<ref *1> { a: 1, c: [Circular *1] }`.
4110    ///
4111    /// The `*N` id is only assigned when the back-edge is reached, i.e. while
4112    /// the target's own children are being rendered — so the prefix can only be
4113    /// decided after `inspect_value` returns.
4114    /// Whether `v` renders as a LEAF — a finished string produced without
4115    /// recursing into any child.
4116    ///
4117    /// Node assigns `ctx.currentDepth = recurseTimes` in `formatRaw`, but only
4118    /// after the early returns for the shapes that answer immediately: a bare
4119    /// Date is its ISO string, a regex is its literal, an empty container is its
4120    /// braces, and a Buffer is whatever its `[util.inspect.custom]` says. None of
4121    /// those record a depth, so a group containing one is not pushed over the
4122    /// `compact` threshold by it — `util.inspect([new Date(0), null], { compact:
4123    /// 1 })` stays on one line. Charging them a level broke exactly those groups.
4124    fn renders_without_expanding(&self, v: &Value) -> bool {
4125        let plain_props = |p: &IndexMap<String, Value>| {
4126            p.keys().all(|k| k.starts_with("@@") || k.starts_with('#'))
4127        };
4128        match self.get(v) {
4129            // A regex never recurses, with or without its hidden `lastIndex`.
4130            Some(JsObj::RegExp(_)) => true,
4131            Some(JsObj::Map { entries, .. }) => entries.is_empty(),
4132            Some(JsObj::Set { entries, .. }) => entries.is_empty(),
4133            Some(JsObj::Array(items)) => items.is_empty() && self.own_symbol_entries(v).is_empty(),
4134            Some(JsObj::Object(p)) => match p.get("@@native").map(|t| self.str_of(t)).as_deref() {
4135                Some("Buffer") => inspect_custom(),
4136                // Own properties added to a Date DO get expanded after it.
4137                Some("Date") => plain_props(p),
4138                Some(_) => false,
4139                None => plain_props(p) && self.own_symbol_entries(v).is_empty(),
4140            },
4141            _ => false,
4142        }
4143    }
4144
4145    fn inspect_lvl(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4146        if !matches!(v, Value::Obj(_)) {
4147            return self.inspect_value(v, indent, st);
4148        }
4149        if st.seen.iter().any(|p| self.strict_eq(p, v)) {
4150            return format!("[Circular *{}]", st.mark(self, v));
4151        }
4152        st.seen.push(v.clone());
4153        // Node ASSIGNS `ctx.currentDepth = recurseTimes` on entry to each value
4154        // it expands — not a running maximum — so after the children have been
4155        // rendered it holds the depth of the last chain below this group, which
4156        // is what `reduceToSingleString` compares. A value the depth limit
4157        // stubs out as `[Object]` is never expanded and must not count, or an
4158        // object whose deepest level was elided would break where node joins.
4159        // Only a value node actually EXPANDS advances the depth. A string,
4160        // symbol or bigint is a JS primitive that this host happens to store on
4161        // the heap, so it reaches here as `Value::Obj` where an unboxed number
4162        // returns above — and counting it as a level made any group holding one
4163        // look deeper than it was. Under `compact: 1` that is the difference
4164        // between node's `Map(2) { 'k2' => 8, 'j' => 5 }` and breaking the same
4165        // Map across four lines, because its string KEYS were being charged a
4166        // nesting level.
4167        if indent as i64 <= inspect_indent_limit()
4168            && !is_primitive(self, v)
4169            && !self.renders_without_expanding(v)
4170        {
4171            st.deepest = indent;
4172        }
4173        let body = self.inspect_value(v, indent, st);
4174        st.seen.pop();
4175        match st.id_of(self, v) {
4176            Some(id) => format!("<ref *{id}> {body}"),
4177            None => body,
4178        }
4179    }
4180
4181    /// The rendering itself, once `inspect_lvl` has established that `v` is not
4182    /// a back-edge into an object already on the stack.
4183    fn inspect_value(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4184        if let Some(s) = self.inspect_wrapper(v, indent, st) {
4185            return s;
4186        }
4187        match v {
4188            Value::Undef => "undefined".into(),
4189            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
4190            Value::Int(n) => n.to_string(),
4191            // `util.inspect` distinguishes negative zero; `String(-0)` does not.
4192            Value::Float(f) if *f == 0.0 && f.is_sign_negative() => "-0".into(),
4193            Value::Float(f) => fmt_number(*f),
4194            Value::Str(s) => quote_str(s),
4195            Value::Obj(_) => match self.get(v) {
4196                Some(JsObj::Str(s)) => quote_str(s),
4197                Some(JsObj::Null) => "null".into(),
4198                // `util.inspect` renders a bigint with the `n` suffix, a regex bare.
4199                Some(JsObj::BigInt(b)) => format!("{b}n"),
4200                // `lastIndex` is a non-enumerable own property of every regex,
4201                // so `showHidden` (and therefore `%o`) appends it:
4202                // `/x/g { [lastIndex]: 0 }`.
4203                Some(JsObj::RegExp(r)) => {
4204                    let body = format!("/{}/{}", r.source, r.flags);
4205                    if inspect_show_hidden() {
4206                        format!("{body} {{ [lastIndex]: {} }}", r.last_index.get())
4207                    } else {
4208                        body
4209                    }
4210                }
4211                // `util.inspect` on node v26.7.0 renders a proxy as
4212                // `Proxy(<target>)` — the target's own rendering, wrapped. It
4213                // deliberately does NOT run the handler's traps, so this stays a
4214                // pure `&self` read like every other inspect arm.
4215                Some(JsObj::Proxy { target, .. }) => {
4216                    format!("Proxy({})", self.inspect_lvl(target, indent, st))
4217                }
4218                Some(JsObj::Array(items)) => {
4219                    // Own enumerable non-index string props (e.g. a `str.match(re)`
4220                    // result's `index`/`input`/`groups`, or a user-assigned
4221                    // `arr.foo`) render after the elements, as `key: value`.
4222                    let prop_keys: Vec<String> = self
4223                        .fn_prop_keys(v)
4224                        .into_iter()
4225                        .filter(|k| {
4226                            !k.starts_with("@@")
4227                                && !k.starts_with('#')
4228                                && self.prop_attrs(v, k).enumerable
4229                        })
4230                        .collect();
4231                    // An own enumerable SYMBOL-keyed property renders after the
4232                    // string keys as `Symbol(desc): value`, as it does on an
4233                    // object receiver.
4234                    let sym_entries = self.own_symbol_entries(v);
4235                    // Under `showHidden` even an empty array has something to
4236                    // show — node prints `[ [length]: 0 ]`, not `[]`.
4237                    if items.is_empty()
4238                        && prop_keys.is_empty()
4239                        && sym_entries.is_empty()
4240                        && !inspect_show_hidden()
4241                    {
4242                        return "[]".into();
4243                    }
4244                    // Node's default inspect depth is 2 (root = depth 0); deeper
4245                    // nesting collapses to `[Array]`. indent grows by 2 per level.
4246                    if indent as i64 > inspect_indent_limit() {
4247                        return "[Array]".into();
4248                    }
4249                    // `util.inspect`'s `maxArrayLength` (default 100): only the
4250                    // first 100 elements are formatted, and the rest collapse to
4251                    // a `... N more items` entry. Without the cap a 120-element
4252                    // array printed all 120 — and, because the grid column width
4253                    // is computed from what is SHOWN, every column was also one
4254                    // character wider than node's.
4255                    // A SPARSE array takes node's `formatSpecialArray` path: an
4256                    // elided run renders as `<N empty items>` rather than as the
4257                    // `undefined` it reads back as.
4258                    let (mut inner, has_tail) = if self.has_holes(v) {
4259                        self.inspect_sparse(v, items, indent, st)
4260                    } else {
4261                        let shown = items.len().min(inspect_max_array_length());
4262                        let mut inner: Vec<String> = items[..shown]
4263                            .iter()
4264                            .map(|x| self.inspect_lvl(x, indent + 2, st))
4265                            .collect();
4266                        let remaining = items.len() - shown;
4267                        if remaining > 0 {
4268                            let unit = if remaining == 1 { "item" } else { "items" };
4269                            inner.push(format!("... {remaining} more {unit}"));
4270                        }
4271                        (inner, remaining > 0)
4272                    };
4273                    // `showHidden` exposes the non-enumerable `length`, which an
4274                    // array always has. It sorts BEFORE any own property node
4275                    // shows (`[ 1, [length]: 1, x: 2 ]`) and, being an entry
4276                    // rather than an element, it also turns the column grid off —
4277                    // which is why a ten-element array under `showHidden` prints
4278                    // on one line rather than as a grid.
4279                    let show_hidden = inspect_show_hidden();
4280                    if show_hidden {
4281                        inner.push(format!("[length]: {}", items.len()));
4282                    }
4283                    let has_props = show_hidden || !prop_keys.is_empty() || !sym_entries.is_empty();
4284                    for k in &prop_keys {
4285                        let val = self.fn_prop(v, k).unwrap_or(Value::Undef);
4286                        inner.push(format!(
4287                            "{}: {}",
4288                            fmt_key(k),
4289                            self.inspect_lvl(&val, indent + 2, st)
4290                        ));
4291                    }
4292                    for (k, val) in &sym_entries {
4293                        let label = match self.symbol_of_key(k) {
4294                            Some(s) => self.inspect(&s),
4295                            None => continue,
4296                        };
4297                        inner.push(format!(
4298                            "{label}: {}",
4299                            self.inspect_lvl(val, indent + 2, st)
4300                        ));
4301                    }
4302                    self.render_array(
4303                        &inner,
4304                        items,
4305                        indent,
4306                        ArrayLayout {
4307                            has_props,
4308                            has_tail,
4309                            base: "",
4310                        },
4311                        st,
4312                    )
4313                }
4314                // `URLSearchParams` renders its pairs, not its slots:
4315                // `URLSearchParams { 'a' => '1', 'b' => '2' }`. Keys repeat,
4316                // which is why it is a pair list rather than a Map rendering.
4317                Some(JsObj::Object(props))
4318                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4319                        == Some("URLSearchParams") =>
4320                {
4321                    let pairs: Vec<Value> = match props.get("@@pairs").and_then(|a| self.get(a)) {
4322                        Some(JsObj::Array(items)) => items.clone(),
4323                        _ => Vec::new(),
4324                    };
4325                    if pairs.is_empty() {
4326                        return "URLSearchParams {}".into();
4327                    }
4328                    let inner: Vec<String> = pairs
4329                        .iter()
4330                        .filter_map(|kv| match self.get(kv) {
4331                            Some(JsObj::Array(p)) if p.len() == 2 => Some(format!(
4332                                "{} => {}",
4333                                self.inspect_lvl(&p[0], indent + 2, st),
4334                                self.inspect_lvl(&p[1], indent + 2, st)
4335                            )),
4336                            _ => None,
4337                        })
4338                        .collect();
4339                    self.render_object(&inner, "URLSearchParams ", indent, st)
4340                }
4341                // A typed array renders as `Uint8Array(3) [ 1, 2, 3 ]` — its
4342                // constructor and length, then the elements laid out exactly as
4343                // an array's. Without this it fell through to the generic object
4344                // arm and printed the `{ length, byteLength, byteOffset,
4345                // BYTES_PER_ELEMENT }` bookkeeping instead of the CONTENTS,
4346                // which is the whole reason anyone logs one.
4347                Some(JsObj::Object(props))
4348                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4349                        == Some("TypedArray") =>
4350                {
4351                    let kind = props
4352                        .get("@@kind")
4353                        .map(|k| self.str_of(k))
4354                        .unwrap_or_else(|| "TypedArray".into());
4355                    // Rendered as STRINGS: a 64-bit view's elements are BigInts,
4356                    // which this shared borrow cannot allocate as values.
4357                    let elems = crate::stdlib::typedarray::elems_display(self, v);
4358                    // The grid layout sizes its columns from the VALUES; a
4359                    // 64-bit view's come back as `undefined` (no allocation is
4360                    // possible here), which only affects column padding.
4361                    let vals = crate::stdlib::typedarray::elems_with_host(self, v);
4362                    let base = format!("{kind}({}) ", elems.len());
4363                    if indent as i64 > inspect_indent_limit() {
4364                        return format!("[{kind}]");
4365                    }
4366                    let shown = elems.len().min(inspect_max_array_length());
4367                    let mut inner: Vec<String> = elems[..shown].to_vec();
4368                    let remaining = elems.len() - shown;
4369                    if remaining > 0 {
4370                        let unit = if remaining == 1 { "item" } else { "items" };
4371                        inner.push(format!("... {remaining} more {unit}"));
4372                    }
4373                    // A view's whole identity — its element width, its window
4374                    // onto the backing store, and the store itself — is
4375                    // non-enumerable, so `showHidden` is the only way to see it.
4376                    // `util.format('%o', view)` goes through here, since `%o`
4377                    // implies `showHidden`.
4378                    let show_hidden = inspect_show_hidden();
4379                    if show_hidden {
4380                        let bpe = crate::stdlib::typedarray::bytes_per_element(&kind);
4381                        let byte_offset = props
4382                            .get("byteOffset")
4383                            .map(|x| self.to_number(x))
4384                            .unwrap_or(0.0);
4385                        inner.push(format!("[BYTES_PER_ELEMENT]: {bpe}"));
4386                        inner.push(format!("[length]: {}", elems.len()));
4387                        inner.push(format!("[byteLength]: {}", elems.len() * bpe));
4388                        inner.push(format!("[byteOffset]: {}", fmt_number(byte_offset)));
4389                        // An ArrayBuffer reached AS a view's backing store is
4390                        // rendered by node WITHOUT its contents — just
4391                        // `ArrayBuffer { [byteLength]: N }` — even though the
4392                        // same buffer inspected on its own leads with
4393                        // `[Uint8Contents]`. Recursing through the normal
4394                        // ArrayBuffer branch therefore printed the bytes twice,
4395                        // once as the view's elements and again as the store's.
4396                        let buf_len = props
4397                            .get("@@buffer")
4398                            .and_then(|b| self.get(b))
4399                            .and_then(|o| match o {
4400                                JsObj::Object(bp) => bp.get("@@bytes").cloned(),
4401                                _ => None,
4402                            })
4403                            .and_then(|b| {
4404                                self.get(&b).map(|o| match o {
4405                                    JsObj::Array(items) => items.len(),
4406                                    _ => 0,
4407                                })
4408                            })
4409                            .unwrap_or(0);
4410                        inner.push(format!(
4411                            "[buffer]: ArrayBuffer {{ [byteLength]: {buf_len} }}"
4412                        ));
4413                    }
4414                    self.render_array(
4415                        &inner,
4416                        &vals,
4417                        indent,
4418                        ArrayLayout {
4419                            has_props: show_hidden,
4420                            has_tail: remaining > 0,
4421                            base: &base,
4422                        },
4423                        st,
4424                    )
4425                }
4426                // An `ArrayBuffer` renders its CONTENTS, which is the only way
4427                // to see them — it exposes no indices of its own:
4428                // `ArrayBuffer { [Uint8Contents]: <00 01>, [byteLength]: 2 }`.
4429                Some(JsObj::Object(props))
4430                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4431                        == Some("ArrayBuffer") =>
4432                {
4433                    let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4434                        Some(JsObj::Array(items)) => {
4435                            items.iter().map(|x| self.to_number(x) as u8).collect()
4436                        }
4437                        _ => Vec::new(),
4438                    };
4439                    let hex: Vec<String> = bytes.iter().map(|b| format!("{b:02x}")).collect();
4440                    let mut parts = vec![
4441                        format!("[Uint8Contents]: <{}>", hex.join(" ")),
4442                        format!("[byteLength]: {}", bytes.len()),
4443                    ];
4444                    if props.contains_key("@@maxByteLength") {
4445                        let max = props
4446                            .get("@@maxByteLength")
4447                            .map(|m| self.to_number(m))
4448                            .unwrap_or(0.0);
4449                        parts.insert(1, format!("maxByteLength: {}", fmt_number(max)));
4450                    }
4451                    self.render_object(&parts, "ArrayBuffer ", indent, st)
4452                }
4453                // A `Date` renders as its ISO-8601 form. Its time value lives in
4454                // the internal `@@ms` slot, which the generic object branch below
4455                // does not show, so without this arm every Date printed as `{}` —
4456                // including through `console.log(d)`, inside arrays, objects and
4457                // Maps, and in an `assert` diff.
4458                Some(JsObj::Object(props))
4459                    if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Date") =>
4460                {
4461                    let base = crate::stdlib::date::inspect_with_host(self, v);
4462                    // Own properties added to a Date follow the date itself, the
4463                    // way node appends them: `2020-01-01T00:00:00.000Z { x: 1 }`.
4464                    let extra = self.side_table_parts(v, indent, st);
4465                    let mut inner: Vec<String> = props
4466                        .iter()
4467                        .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
4468                        .map(|(k, val)| format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)))
4469                        .collect();
4470                    inner.extend(extra);
4471                    if inner.is_empty() {
4472                        return base;
4473                    }
4474                    self.render_object(&inner, &format!("{base} "), indent, st)
4475                }
4476                // A `Buffer` renders as `<Buffer 01 02 03>` — hex bytes, capped
4477                // at 50 with a `... N more byte(s)` tail, exactly as
4478                // `util.inspect` does. Without this a `console.log(buf)` (the
4479                // single most common thing anyone does with a Buffer) printed
4480                // the internal `{ length, byteLength, … }` bookkeeping.
4481                Some(JsObj::Object(props))
4482                    if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4483                        == Some("Buffer") =>
4484                {
4485                    let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4486                        Some(JsObj::Array(items)) => {
4487                            items.iter().map(|x| self.to_number(x) as u8).collect()
4488                        }
4489                        _ => Vec::new(),
4490                    };
4491                    // `<Buffer …>` is Buffer's `[util.inspect.custom]` hook, not
4492                    // the shape of the object. Under `customInspect: false` node
4493                    // does not call that hook and falls back to the generic
4494                    // byte-view rendering — which is what an `assert` diff shows,
4495                    // since assert inspects with the hook disabled so that a
4496                    // failure names the differing BYTE rather than two opaque hex
4497                    // blobs. The constructor is `Buffer` while the brand is still
4498                    // `Uint8Array`, so node prints both.
4499                    if !inspect_custom() {
4500                        let base = format!("Buffer({}) [Uint8Array] ", bytes.len());
4501                        if indent as i64 > inspect_indent_limit() {
4502                            return "[Buffer [Uint8Array]]".into();
4503                        }
4504                        let shown = bytes.len().min(inspect_max_array_length());
4505                        let mut inner: Vec<String> =
4506                            bytes[..shown].iter().map(|b| b.to_string()).collect();
4507                        let vals: Vec<Value> = bytes[..shown]
4508                            .iter()
4509                            .map(|b| Value::Float(*b as f64))
4510                            .collect();
4511                        let remaining = bytes.len() - shown;
4512                        if remaining > 0 {
4513                            let unit = if remaining == 1 { "item" } else { "items" };
4514                            inner.push(format!("... {remaining} more {unit}"));
4515                        }
4516                        return self.render_array(
4517                            &inner,
4518                            &vals,
4519                            indent,
4520                            ArrayLayout {
4521                                has_props: false,
4522                                has_tail: remaining > 0,
4523                                base: &base,
4524                            },
4525                            st,
4526                        );
4527                    }
4528                    const MAX: usize = 50;
4529                    let shown: Vec<String> =
4530                        bytes.iter().take(MAX).map(|b| format!("{b:02x}")).collect();
4531                    let mut out = format!("<Buffer {}", shown.join(" "));
4532                    if bytes.len() > MAX {
4533                        let more = bytes.len() - MAX;
4534                        let unit = if more == 1 { "byte" } else { "bytes" };
4535                        out.push_str(&format!(" ... {more} more {unit}"));
4536                    }
4537                    out.push('>');
4538                    out
4539                }
4540                // An Error inspects as its `.stack` — never as an object literal
4541                // exposing the internal `message`/`stack` slots. Any own property
4542                // a script added beyond those follows in braces, as V8 renders
4543                // it: `Error: x\n    at … { code: 'C' }`.
4544                Some(JsObj::Object(_)) if self.error_to_string(v).is_some() => {
4545                    let mut stack = lookup_chain(self, v, "stack")
4546                        .map(|s| self.str_of(&s))
4547                        .unwrap_or_else(|| self.error_to_string(v).unwrap_or_default());
4548                    // A `DOMException` prints its CLASS and then its name —
4549                    // `DOMException [AbortError]: m` — where a plain error
4550                    // prints only its stack head.
4551                    if let Some(JsObj::Object(p)) = self.get(v) {
4552                        if let Some(n) = p.get("@@domName") {
4553                            let name = self.str_of(n);
4554                            stack = format!(
4555                                "DOMException [{name}]{}",
4556                                stack.strip_prefix(&name).unwrap_or(&stack)
4557                            );
4558                        }
4559                    }
4560                    let extra: Vec<String> = self
4561                        .own_enum_key_names(v)
4562                        .into_iter()
4563                        .filter(|k| k != "name")
4564                        .map(|k| {
4565                            let val = self.fn_prop(v, &k).unwrap_or_else(|| match self.get(v) {
4566                                Some(JsObj::Object(p)) => {
4567                                    p.get(&k).cloned().unwrap_or(Value::Undef)
4568                                }
4569                                _ => Value::Undef,
4570                            });
4571                            format!(
4572                                "{}: {}",
4573                                fmt_key(&k),
4574                                self.inspect_lvl(&val, indent + 2, st)
4575                            )
4576                        })
4577                        .collect();
4578                    if extra.is_empty() {
4579                        stack
4580                    } else {
4581                        format!("{stack} {{ {} }}", extra.join(", "))
4582                    }
4583                }
4584                Some(JsObj::Object(props)) => {
4585                    // Instances print with their constructor name as a prefix
4586                    // (`C { x: 1 }`); plain objects have none; a null-prototype
4587                    // object (e.g. an `Object.groupBy` result) is tagged
4588                    // `[Object: null prototype]`.
4589                    let ctor = match self.ctor_name(v) {
4590                        n if n.is_empty() => "Object".to_string(),
4591                        n => n,
4592                    };
4593                    let plain_prefix = if ctor == "Object" {
4594                        String::new()
4595                    } else {
4596                        format!("{ctor} ")
4597                    };
4598                    let prefix = if self.inspects_null_proto(v) {
4599                        "[Object: null prototype] ".to_string()
4600                    } else {
4601                        // An inherited `Symbol.toStringTag` shows as `Ctor [Tag] `.
4602                        match self.inspect_tag(v) {
4603                            Some(t) if t != ctor => format!("{ctor} [{t}] "),
4604                            _ => plain_prefix.clone(),
4605                        }
4606                    };
4607                    // Skip node-js's internal slots (`@@native`, `@@bytes`, …) and
4608                    // private class fields; a real symbol-keyed own property is a
4609                    // visible one and renders as `Symbol(desc): value`.
4610                    // An own ACCESSOR has no value to print: node shows the
4611                    // label `[Getter]` / `[Setter]` / `[Getter/Setter]` in its
4612                    // place. It is found through the `@@ord:` marker the
4613                    // property map holds for it, which is also what puts it in
4614                    // declaration order among the data properties. Without this
4615                    // an accessor rendered as nothing at all — `{ get z(){} }`
4616                    // printed `{}`.
4617                    let mut shown: Vec<(String, Result<&Value, &'static str>)> = props
4618                        .iter()
4619                        .filter_map(|(k, val)| match k.strip_prefix(ORD_MARKER) {
4620                            Some(real) => {
4621                                let attrs = self.prop_attrs(v, real);
4622                                let label = match self.own_accessor(v, real)? {
4623                                    (Some(_), Some(_)) => "[Getter/Setter]",
4624                                    (Some(_), None) => "[Getter]",
4625                                    (None, Some(_)) => "[Setter]",
4626                                    (None, None) => return None,
4627                                };
4628                                attrs.enumerable.then(|| (fmt_key(real), Err(label)))
4629                            }
4630                            // Only an ENUMERABLE own property is shown, as node
4631                            // does: a native instance keeps bookkeeping (a
4632                            // `URLSearchParams`'s `size`) as a hidden own slot,
4633                            // and printing it would report a spec getter as data.
4634                            None if !k.starts_with("@@")
4635                                && !k.starts_with('#')
4636                                && self.prop_attrs(v, k).enumerable =>
4637                            {
4638                                Some((fmt_key(k), Ok(val)))
4639                            }
4640                            None => None,
4641                        })
4642                        .collect();
4643                    shown.extend(props.iter().filter_map(|(k, val)| {
4644                        let sym = self.symbol_of_key(k)?;
4645                        self.prop_attrs(v, k)
4646                            .enumerable
4647                            .then(|| (self.inspect(&sym), Ok(val)))
4648                    }));
4649                    if shown.is_empty() {
4650                        return format!("{prefix}{{}}");
4651                    }
4652                    // Depth limit (Node default 2): deeper objects collapse to
4653                    // `[Object]` (or `[ClassName]` for a named instance).
4654                    if indent as i64 > inspect_indent_limit() {
4655                        return if self.inspects_null_proto(v) {
4656                            // Already bracketed (`[Object: null prototype]`).
4657                            prefix.trim_end().to_string()
4658                        } else if plain_prefix.is_empty() {
4659                            "[Object]".into()
4660                        } else {
4661                            format!("[{}]", plain_prefix.trim_end())
4662                        };
4663                    }
4664                    let inner: Vec<String> = shown
4665                        .iter()
4666                        .map(|(k, val)| match val {
4667                            Ok(val) => format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)),
4668                            Err(label) => format!("{k}: {label}"),
4669                        })
4670                        .collect();
4671                    self.render_object(&inner, &prefix, indent, st)
4672                }
4673                Some(JsObj::Symbol { desc, .. }) => match desc {
4674                    Some(d) => format!("Symbol({d})"),
4675                    None => "Symbol()".into(),
4676                },
4677                Some(JsObj::Class(c)) => {
4678                    let base = if c.parent.is_some() {
4679                        let pname = c
4680                            .parent
4681                            .as_ref()
4682                            .map(|p| self.callable_name(p))
4683                            .unwrap_or_default();
4684                        format!("[class {} extends {}]", c.name, pname)
4685                    } else {
4686                        format!("[class {}]", c.name)
4687                    };
4688                    self.with_callable_props(v, base, indent, st)
4689                }
4690                // A Map/Set renders its members at the NEXT nesting level, and
4691                // collapses to `[Map]`/`[Set]` past the depth limit exactly as an
4692                // array collapses to `[Array]`. Both used to recurse through
4693                // `inspect`, which restarts at indent 0, so the depth gate never
4694                // fired: nesting printed one level too deep at every depth
4695                // (measured on node v26.7.0, four nested Maps print
4696                // `Map(1) { 'a' => Map(1) { 'b' => Map(1) { 'c' => [Map] } } }`),
4697                // and a SELF-referential Map or Set recursed forever and aborted
4698                // the process — `const m=new Map(); m.set('m',m); console.log(m)`
4699                // died with `fatal runtime error: stack overflow`, which no
4700                // `try`/`catch` can see. An empty one still prints in full at any
4701                // depth, as `[]`/`{}` do.
4702                // A WEAK collection never shows its contents: node prints
4703                // `WeakMap { <items unknown> }` whether it holds anything or
4704                // not, because the entries are not enumerable by design.
4705                Some(JsObj::Map { weak: true, .. }) => "WeakMap { <items unknown> }".into(),
4706                Some(JsObj::Set { weak: true, .. }) => "WeakSet { <items unknown> }".into(),
4707                Some(JsObj::Map { entries, .. }) => {
4708                    let extra = self.side_table_parts(v, indent, st);
4709                    if entries.is_empty() && extra.is_empty() {
4710                        return "Map(0) {}".into();
4711                    }
4712                    if indent as i64 > inspect_indent_limit() {
4713                        return "[Map]".into();
4714                    }
4715                    let mut inner: Vec<String> = entries
4716                        .values()
4717                        .map(|(k, val)| {
4718                            // Sequenced, not nested in one `format!`: both arms
4719                            // need the same `&mut` cycle state.
4720                            let ks = self.inspect_lvl(k, indent + 2, st);
4721                            let vs = self.inspect_lvl(val, indent + 2, st);
4722                            format!("{ks} => {vs}")
4723                        })
4724                        .collect();
4725                    inner.extend(extra);
4726                    // Laid out by the SAME routine as a plain object, not joined
4727                    // onto one line unconditionally. `Map`/`Set` were the only
4728                    // containers that never consulted `breakLength` or `compact`,
4729                    // so every collection wide enough to wrap printed as one long
4730                    // line: node breaks a seven-member Set of ten-character
4731                    // strings across seven lines, and `util.inspect(m, {compact:
4732                    // false})` — which assert's own diff renderer depends on —
4733                    // could not break a Map at all. Node builds these through
4734                    // `reduceToSingleString` with `braces[0]` of `Map(n) {`, which
4735                    // is this `prefix` (the trailing space is the brace gap).
4736                    let prefix = format!("Map({}) ", entries.len());
4737                    self.render_object(&inner, &prefix, indent, st)
4738                }
4739                Some(JsObj::Set { entries, .. }) => {
4740                    let extra = self.side_table_parts(v, indent, st);
4741                    if entries.is_empty() && extra.is_empty() {
4742                        return "Set(0) {}".into();
4743                    }
4744                    if indent as i64 > inspect_indent_limit() {
4745                        return "[Set]".into();
4746                    }
4747                    let mut inner: Vec<String> = entries
4748                        .values()
4749                        .map(|v| self.inspect_lvl(v, indent + 2, st))
4750                        .collect();
4751                    inner.extend(extra);
4752                    // Same layout routine as a Map (see above). Note node does
4753                    // NOT column-group a wide Set the way it grids an array:
4754                    // `groupArrayElements` is reached only from the list
4755                    // formatter, so a 30-member Set is thirty lines.
4756                    let prefix = format!("Set({}) ", entries.len());
4757                    self.render_object(&inner, &prefix, indent, st)
4758                }
4759                Some(JsObj::Generator { .. }) => "Object [Generator] {}".into(),
4760                Some(JsObj::Promise { id }) => match self.promises.get(*id as usize) {
4761                    Some(c) => match c.state {
4762                        PromiseState::Pending => "Promise { <pending> }".into(),
4763                        PromiseState::Fulfilled => {
4764                            format!("Promise {{ {} }}", self.inspect_lvl(&c.value, 0, st))
4765                        }
4766                        PromiseState::Rejected => {
4767                            format!(
4768                                "Promise {{ <rejected> {} }}",
4769                                self.inspect_lvl(&c.value, 0, st)
4770                            )
4771                        }
4772                    },
4773                    None => "Promise { <pending> }".into(),
4774                },
4775                Some(JsObj::Func(_)) => {
4776                    // `callable_name`, not the FuncDef name: an anonymous
4777                    // function expression gets its name by inference from the
4778                    // binding it initialises (`const f = function(){}`), and
4779                    // that lands as an own `name` property.
4780                    let name = self.callable_name(v);
4781                    let base = if name.is_empty() {
4782                        "[Function (anonymous)]".to_string()
4783                    } else {
4784                        format!("[Function: {name}]")
4785                    };
4786                    self.with_callable_props(v, base, indent, st)
4787                }
4788                Some(JsObj::Builtin(n)) => {
4789                    // A namespace object is not a function and must not be
4790                    // printed as one. The three ECMAScript namespaces carry a
4791                    // `Symbol.toStringTag` and inspect as `Object [Math] {}`;
4792                    // their members are all non-enumerable, so the braces really
4793                    // are empty. A `require()`d module namespace has no tag and
4794                    // node prints its members, which cannot be rendered here —
4795                    // formatting a member means allocating its value, and this
4796                    // runs under the host borrow.
4797                    if !builtin_is_callable(n) {
4798                        match crate::builtins::well_known_tag(self, v) {
4799                            Some(tag) => format!("Object [{tag}] {{}}"),
4800                            // `Set.prototype` inspects under the CONSTRUCTOR's
4801                            // name, not the key: node prints `Object [Set] {}`.
4802                            None => {
4803                                format!("Object [{}] {{}}", n.trim_end_matches(".prototype"))
4804                            }
4805                        }
4806                    } else {
4807                        format!("[Function: {}]", crate::builtins::builtin_name(n))
4808                    }
4809                }
4810                // A bound method is not anonymous: it is the prototype method it
4811                // resolves to, so `console.log(new Uint8Array(1).set)` reports
4812                // `[Function: set]`.
4813                Some(JsObj::BoundMethod { name, .. }) => format!("[Function: {name}]"),
4814                Some(JsObj::BoundFunc { target, .. }) => {
4815                    let n = self.callable_name(target);
4816                    if n.is_empty() {
4817                        "[Function: bound ]".into()
4818                    } else {
4819                        format!("[Function: bound {n}]")
4820                    }
4821                }
4822                _ => "undefined".into(),
4823            },
4824            _ => "undefined".into(),
4825        }
4826    }
4827
4828    /// Append a callable's own enumerable properties to its `[Function: f]` /
4829    /// `[class C]` base, the way `util.inspect` does: `[Function: f] { a: 1 }`.
4830    /// A callable with none renders as the bare base.
4831    fn with_callable_props(
4832        &self,
4833        v: &Value,
4834        base: String,
4835        indent: usize,
4836        st: &mut InspectCycles,
4837    ) -> String {
4838        let mut inner: Vec<String> = self
4839            .own_enum_key_names(v)
4840            .into_iter()
4841            .map(|k| {
4842                let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4843                format!(
4844                    "{}: {}",
4845                    fmt_key(&k),
4846                    self.inspect_lvl(&val, indent + 2, st)
4847                )
4848            })
4849            .collect();
4850        for (k, val) in self.own_symbol_entries(v) {
4851            if let Some(sym) = self.symbol_of_key(&k) {
4852                inner.push(format!(
4853                    "{}: {}",
4854                    self.inspect(&sym),
4855                    self.inspect_lvl(&val, indent + 2, st)
4856                ));
4857            }
4858        }
4859        if inner.is_empty() {
4860            return base;
4861        }
4862        self.render_object(&inner, &format!("{base} "), indent, st)
4863    }
4864
4865    /// Render a non-empty array's already-formatted element strings, applying
4866    /// Node's `util.inspect` layout: a single line when it fits, else a multi-line
4867    /// grid via `groupArrayElements` (for >6 entries), else one element per line.
4868    /// `values` is the raw element list (drives numeric right-alignment); `indent`
4869    /// is the array's own indentation level.
4870    fn render_array(
4871        &self,
4872        output: &[String],
4873        values: &[Value],
4874        indent: usize,
4875        opts: ArrayLayout<'_>,
4876        st: &InspectCycles,
4877    ) -> String {
4878        let ArrayLayout {
4879            has_props,
4880            has_tail,
4881            base,
4882        } = opts;
4883        // Group array elements together if the array has more than six entries.
4884        // Arrays carrying extra own props (`index`/`input`/… on a match result)
4885        // are never grid-grouped — Node lays those out plainly.
4886        // `compact: false` (held as 0) also turns the GRID off, not just the
4887        // single-line join. Node reaches `groupArrayElements` only under
4888        // `ctx.compact >= 1`, so `util.inspect(arr, { compact: false })` is one
4889        // element per line however many there are; without this gate a 30-element
4890        // array still came back column-aligned in three rows, which is the form
4891        // assert's diff renderer splits on — every array diff would have been
4892        // computed over grid rows instead of elements.
4893        let entries = output.len();
4894        let (lines, grouped) = if entries > 6 && !has_props && inspect_compact() >= 1 {
4895            group_array_elements(self, output, values, indent, has_tail)
4896        } else {
4897            (output.to_vec(), false)
4898        };
4899        // A typed array prints its constructor and length ahead of the brackets
4900        // (`Uint8Array(3) [ 1, 2, 3 ]`); node counts that as `base` in the
4901        // break-length seed, so a long tag wraps the list one entry sooner.
4902        if output.is_empty() {
4903            return format!("{base}[]");
4904        }
4905        // If no grouping happened, try to line everything up on a single line.
4906        if !grouped {
4907            // start = output.length + indentationLvl + braces[0].len(1) + base + 10
4908            let start = output.len() + indent + 1 + base.chars().count() + 10;
4909            if self.may_compact(indent, st) && is_below_break_length(output, start) {
4910                return format!("{base}[ {} ]", output.join(", "));
4911            }
4912        }
4913        // Otherwise: one (grouped or single) entry per line, indented by indent+2.
4914        let pad = " ".repeat(indent);
4915        let sep = format!(",\n{pad}  ");
4916        format!("{base}[\n{pad}  {}\n{pad}]", lines.join(&sep))
4917    }
4918
4919    /// Render a non-empty object's already-formatted `key: value` strings with
4920    /// Node's `util.inspect` layout: a single line when it fits `breakLength`,
4921    /// else one property per line indented by `indent + 2`. `prefix` is the
4922    /// constructor/`[Object: null prototype]` tag (with trailing space) or empty.
4923    /// Mirrors `render_array`'s break decision, including the `compact` depth
4924    /// gate.
4925    /// Whether a group at `indent` may be joined onto one line.
4926    ///
4927    /// Node's `reduceToSingleString`: only while the subtree below this group is
4928    /// SHALLOWER than `compact` (default 3). `compact: false` is held as 0, so
4929    /// nothing qualifies and every group breaks.
4930    fn may_compact(&self, indent: usize, st: &InspectCycles) -> bool {
4931        let compact = inspect_compact();
4932        if compact < 1 {
4933            return false;
4934        }
4935        // Levels, not columns: the indent advances by two per level.
4936        let depth_below = (st.deepest.saturating_sub(indent)) / 2;
4937        (depth_below as i64) < compact
4938    }
4939
4940    fn render_object(
4941        &self,
4942        output: &[String],
4943        prefix: &str,
4944        indent: usize,
4945        st: &InspectCycles,
4946    ) -> String {
4947        // start = output.length + indentationLvl + braces[0].len + base(0) + 10.
4948        // For a tagged object Node folds the tag into `braces[0]` (e.g.
4949        // `"Point {"`, `"[Object: null prototype] {"`), so its length is the
4950        // prefix (which carries the trailing space) plus the `{`.
4951        // `sorted: true` orders the RENDERED entries, not the keys. Node sorts
4952        // the finished `key: value` strings (`output.sort()` in `formatRaw` for
4953        // the object shape), which is observably different from sorting keys
4954        // whenever a key needs quoting — `'b-b': 1` sorts under `'`, not `b`.
4955        // `assert`'s diff renderer depends on this: without it two objects
4956        // carrying the same properties in a different insertion order diffed as
4957        // a wholesale rewrite of every line instead of as equal.
4958        let sorted_output;
4959        let output = if inspect_sorted() {
4960            let mut v = output.to_vec();
4961            v.sort();
4962            sorted_output = v;
4963            &sorted_output[..]
4964        } else {
4965            output
4966        };
4967        let braces0 = prefix.chars().count() + 1;
4968        let start = output.len() + indent + braces0 + 10;
4969        if self.may_compact(indent, st) && is_below_break_length(output, start) {
4970            return format!("{prefix}{{ {} }}", output.join(", "));
4971        }
4972        let pad = " ".repeat(indent);
4973        let sep = format!(",\n{pad}  ");
4974        format!("{prefix}{{\n{pad}  {}\n{pad}}}", output.join(&sep))
4975    }
4976
4977    /// The `.name` of any callable (function/class/builtin/bound).
4978    pub fn callable_name(&self, v: &Value) -> String {
4979        // A user-set `.name` own property wins.
4980        if let Some(n) = self.fn_prop(v, "name") {
4981            return self.str_of(&n);
4982        }
4983        match self.get(v) {
4984            Some(JsObj::Func(f)) => self
4985                .funcs
4986                .get(f.def_id)
4987                .map(|d| d.name.clone())
4988                .unwrap_or_default(),
4989            Some(JsObj::Class(c)) => c.name.clone(),
4990            // Not the whole key: a builtin's `.name` is its last segment, and a
4991            // prototype thunk's key is `@proto:<Ctor>:<method>` — which has no
4992            // `.` at all, so this reported the internal spelling verbatim and
4993            // `console.log(Uint8Array.prototype.set)` printed
4994            // `[Function: @proto:TypedArray:set]`.
4995            Some(JsObj::Builtin(n)) => crate::builtins::builtin_name(n).to_string(),
4996            Some(JsObj::BoundFunc { target, .. }) => {
4997                format!("bound {}", self.callable_name(target))
4998            }
4999            Some(JsObj::BoundMethod { name, .. }) => name.clone(),
5000            _ => String::new(),
5001        }
5002    }
5003
5004    // ── equality / comparison / arithmetic (numeric-hook + builtin paths) ──
5005
5006    /// Strict equality (`===`): same type and same value, no coercion.
5007    pub fn strict_eq(&self, a: &Value, b: &Value) -> bool {
5008        match (a, b) {
5009            (Value::Undef, Value::Undef) => true,
5010            (Value::Bool(x), Value::Bool(y)) => x == y,
5011            (Value::Str(x), Value::Str(y)) => x == y,
5012            _ => {
5013                // Numbers (NaN !== NaN, +0 === -0).
5014                let an = matches!(a, Value::Int(_) | Value::Float(_));
5015                let bn = matches!(b, Value::Int(_) | Value::Float(_));
5016                if an && bn {
5017                    let x = self.to_number(a);
5018                    let y = self.to_number(b);
5019                    return x == y;
5020                }
5021                // BigInt === BigInt compares by value (each literal is a distinct
5022                // heap cell, so reference identity would be wrong). BigInt is never
5023                // `===` a Number (different types).
5024                if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5025                    return x == y;
5026                }
5027                // Heap values.
5028                if let (Some(sa), Some(sb)) = (self.as_str(a), self.as_str(b)) {
5029                    return sa == sb;
5030                }
5031                let na = self.is_null(a);
5032                let nb = self.is_null(b);
5033                if na || nb {
5034                    return na && nb;
5035                }
5036                // A builtin namespace/constructor/prototype is a SINGLETON in JS
5037                // (`Math === Math`, `Array.prototype === Array.prototype`), but
5038                // every bare reference here allocates a fresh handle, so compare
5039                // those by name rather than by heap index.
5040                if let (Some(JsObj::Builtin(x)), Some(JsObj::Builtin(y))) =
5041                    (self.get(a), self.get(b))
5042                {
5043                    return x == y;
5044                }
5045                // Reference identity for arrays/objects/functions.
5046                matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
5047            }
5048        }
5049    }
5050
5051    /// Whether `v` is `null` or `undefined`.
5052    pub fn is_nullish(&self, v: &Value) -> bool {
5053        matches!(v, Value::Undef) || self.is_null(v)
5054    }
5055
5056    /// The ECMAScript "loose type" of `v` for the `==` algorithm: `"number"`,
5057    /// `"string"` (primitive or heap string), `"boolean"`, `"undefined"`,
5058    /// `"null"`, or `"object"` (array / plain object / function).
5059    fn js_type(&self, v: &Value) -> &'static str {
5060        match v {
5061            Value::Undef => "undefined",
5062            Value::Bool(_) => "boolean",
5063            Value::Int(_) | Value::Float(_) => "number",
5064            Value::Str(_) => "string",
5065            Value::Obj(_) => match self.get(v) {
5066                Some(JsObj::Str(_)) => "string",
5067                Some(JsObj::Null) => "null",
5068                Some(JsObj::BigInt(_)) => "bigint",
5069                _ => "object",
5070            },
5071            _ => "object",
5072        }
5073    }
5074
5075    /// Loose equality (`==`) following the ECMAScript Abstract Equality Comparison.
5076    /// Objects reduce via `ToPrimitive` (which for our heap objects is always their
5077    /// string `toString`), so `[0] == "0"` is `true` (string compare of `"0"`) but
5078    /// `[0] == ""` is `false` — never a number coercion of the object.
5079    pub fn loose_eq(&self, a: &Value, b: &Value) -> bool {
5080        // Same type: identical to `===` (number==number, string==string, etc.).
5081        if self.strict_eq(a, b) {
5082            return true;
5083        }
5084        let ta = self.js_type(a);
5085        let tb = self.js_type(b);
5086        // null and undefined are loosely equal only to each other.
5087        if self.is_nullish(a) || self.is_nullish(b) {
5088            return self.is_nullish(a) && self.is_nullish(b);
5089        }
5090        // BigInt ⇄ (Number | String | Boolean | Object): compare mathematical
5091        // values (both-BigInt was already settled by the `strict_eq` above).
5092        if ta == "bigint" || tb == "bigint" {
5093            return self.bigint_loose_eq(a, b);
5094        }
5095        if ta == tb {
5096            // Same type but not strict-equal (and not nullish) ⇒ not equal.
5097            return false;
5098        }
5099        // number ⇄ string: compare as numbers.
5100        if (ta == "number" && tb == "string") || (ta == "string" && tb == "number") {
5101            return self.to_number(a) == self.to_number(b);
5102        }
5103        // boolean side coerces to number, then recompares.
5104        if ta == "boolean" {
5105            return self.loose_eq(&Value::Float(self.to_number(a)), b);
5106        }
5107        if tb == "boolean" {
5108            return self.loose_eq(a, &Value::Float(self.to_number(b)));
5109        }
5110        // object ⇄ (number|string): ToPrimitive the object (→ its string form),
5111        // then recompare as string==string or number==string.
5112        if ta == "object" && (tb == "number" || tb == "string") {
5113            let pa = self.str_of(a);
5114            return if tb == "string" {
5115                pa == self.str_of(b)
5116            } else {
5117                str_to_number(&pa) == self.to_number(b)
5118            };
5119        }
5120        if tb == "object" && (ta == "number" || ta == "string") {
5121            let pb = self.str_of(b);
5122            return if ta == "string" {
5123                self.str_of(a) == pb
5124            } else {
5125                self.to_number(a) == str_to_number(&pb)
5126            };
5127        }
5128        false
5129    }
5130
5131    /// The numeric-hook arithmetic/relational fallback for non-native operands
5132    /// (called by fusevm when at least one operand isn't `Int`/`Float`).
5133    pub fn arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5134        use NumOp::*;
5135        match op {
5136            Add => {
5137                // `+`: if either operand is a string, concatenate string forms;
5138                // otherwise numeric addition.
5139                let a_str = self.prefers_string(a);
5140                let b_str = self.prefers_string(b);
5141                if a_str || b_str {
5142                    // String concatenation wins even with a bigint operand
5143                    // (`1n + "x"` → `"1x"`).
5144                    let s = format!("{}{}", self.str_of(a), self.str_of(b));
5145                    Ok(self.new_str(s))
5146                } else if self.is_bigint_val(a) || self.is_bigint_val(b) {
5147                    self.bigint_arith(op, a, b)
5148                } else {
5149                    Ok(Value::Float(self.to_number(a) + self.to_number(b)))
5150                }
5151            }
5152            Sub | Mul | Div | Mod | Pow if self.is_bigint_val(a) || self.is_bigint_val(b) => {
5153                self.bigint_arith(op, a, b)
5154            }
5155            Sub => Ok(Value::Float(self.to_number(a) - self.to_number(b))),
5156            Mul => Ok(Value::Float(self.to_number(a) * self.to_number(b))),
5157            Div => Ok(Value::Float(self.to_number(a) / self.to_number(b))),
5158            Mod => Ok(Value::Float(js_mod(self.to_number(a), self.to_number(b)))),
5159            Pow => Ok(Value::Float(crate::builtins::js_pow(
5160                self.to_number(a),
5161                self.to_number(b),
5162            ))),
5163            Neg if self.is_bigint_val(a) => self.bigint_arith(op, a, b),
5164            Neg => Ok(Value::Float(-self.to_number(a))),
5165            Lt | Le | Gt | Ge => Ok(Value::Bool(self.relational(op, a, b))),
5166            Eq => Ok(Value::Bool(self.loose_eq(a, b))),
5167            Ne => Ok(Value::Bool(!self.loose_eq(a, b))),
5168        }
5169    }
5170
5171    /// Whether `v`'s primitive (`ToPrimitive` with the default hint) is a string,
5172    /// which drives `+` toward concatenation. Primitive strings qualify, and so
5173    /// do heap objects whose default `ToPrimitive` is their (string) `toString`:
5174    /// arrays (`[1,2,3]+3 → "1,2,33"`), plain objects (`{}+[] → "[object Object]"`),
5175    /// and functions. `null`/`undefined`/`boolean`/`number` do not.
5176    fn prefers_string(&self, v: &Value) -> bool {
5177        match v {
5178            Value::Str(_) => true,
5179            // A BigInt's `ToPrimitive` is the bigint itself (numeric), NOT a string,
5180            // so `1n + 2n` is bigint addition, not concatenation. `null` has no
5181            // string primitive either.
5182            Value::Obj(_) => !matches!(
5183                self.get(v),
5184                Some(JsObj::Null) | Some(JsObj::BigInt(_)) | None
5185            ),
5186            _ => false,
5187        }
5188    }
5189
5190    /// Relational comparison (`< <= > >=`) with JS coercion: string/string is
5191    /// lexicographic, otherwise numeric (NaN yields false).
5192    fn relational(&self, op: NumOp, a: &Value, b: &Value) -> bool {
5193        use std::cmp::Ordering;
5194        let ord = if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5195            // BigInt < BigInt: exact (no f64 precision loss for large magnitudes).
5196            x.cmp(&y)
5197        } else if let (Some(x), Some(y)) = (self.as_str(a), self.as_str(b)) {
5198            // 7.2.13 IsLessThan compares CODE UNITS, which is not Rust's `str`
5199            // order once an astral character meets a BMP one — see `utf16`.
5200            crate::utf16::cmp_units(&x, &y)
5201        } else {
5202            let x = self.to_number(a);
5203            let y = self.to_number(b);
5204            match x.partial_cmp(&y) {
5205                Some(o) => o,
5206                None => return false, // NaN operand
5207            }
5208        };
5209        match op {
5210            NumOp::Lt => ord == Ordering::Less,
5211            NumOp::Le => ord != Ordering::Greater,
5212            NumOp::Gt => ord == Ordering::Greater,
5213            NumOp::Ge => ord != Ordering::Less,
5214            _ => false,
5215        }
5216    }
5217
5218    /// Bitwise/shift ops with JS ToInt32/ToUint32 semantics — or true
5219    /// arbitrary-width BigInt bitwise when both operands are BigInt (mixing a
5220    /// BigInt with a Number throws, matching Node).
5221    pub fn bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5222        if self.is_bigint_val(a) || self.is_bigint_val(b) {
5223            return self.bigint_bitwise(tag, a, b);
5224        }
5225        let x = to_int32(self.to_number(a));
5226        let y = to_int32(self.to_number(b));
5227        let r: i64 = match tag {
5228            binop::BITAND => (x & y) as i64,
5229            binop::BITOR => (x | y) as i64,
5230            binop::BITXOR => (x ^ y) as i64,
5231            binop::SHL => (x.wrapping_shl((y as u32) & 31)) as i64,
5232            binop::SHR => (x >> ((y as u32) & 31)) as i64,
5233            binop::USHR => (to_uint32(self.to_number(a)) >> ((y as u32) & 31)) as i64,
5234            _ => 0,
5235        };
5236        Ok(Value::Float(r as f64))
5237    }
5238
5239    // ── BigInt operations ────────────────────────────────────────────────────
5240    /// Whether `v` is a heap `BigInt`.
5241    pub fn is_bigint_val(&self, v: &Value) -> bool {
5242        matches!(self.get(v), Some(JsObj::BigInt(_)))
5243    }
5244    /// The `BigInt` value of `v` (a heap bigint), else `None`.
5245    pub fn as_bigint(&self, v: &Value) -> Option<num_bigint::BigInt> {
5246        match self.get(v) {
5247            Some(JsObj::BigInt(b)) => Some(b.clone()),
5248            _ => None,
5249        }
5250    }
5251    /// Allocate a heap `BigInt`.
5252    pub fn new_bigint(&mut self, b: num_bigint::BigInt) -> Value {
5253        self.alloc(JsObj::BigInt(b))
5254    }
5255
5256    /// BigInt arithmetic (`+ - * / % **`, unary `-`). Requires BOTH operands to be
5257    /// BigInt for a binary op; mixing a BigInt with a Number throws the exact Node
5258    /// `TypeError` (a string operand is handled as concatenation before we get
5259    /// here). Division/`%` truncate toward zero; `**` needs a non-negative
5260    /// exponent.
5261    fn bigint_arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5262        use num_traits::{Signed, Zero};
5263        use NumOp::*;
5264        if op == Neg {
5265            let x = self.as_bigint(a).expect("bigint_arith Neg on non-bigint");
5266            return Ok(self.new_bigint(-x));
5267        }
5268        let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5269            (Some(x), Some(y)) => (x, y),
5270            // Exactly one side is a BigInt → the other is a Number/Boolean: illegal.
5271            _ => {
5272                return Err(type_error(
5273                    "Cannot mix BigInt and other types, use explicit conversions",
5274                ))
5275            }
5276        };
5277        let r = match op {
5278            Add => x + y,
5279            Sub => x - y,
5280            Mul => x * y,
5281            Div => {
5282                if y.is_zero() {
5283                    return Err("RangeError: Division by zero".into());
5284                }
5285                x / y // truncates toward zero (matches JS BigInt division)
5286            }
5287            Mod => {
5288                if y.is_zero() {
5289                    return Err("RangeError: Division by zero".into());
5290                }
5291                x % y // sign follows the dividend (truncated), like JS
5292            }
5293            Pow => {
5294                if y.is_negative() {
5295                    return Err("RangeError: Exponent must be positive".into());
5296                }
5297                let exp = num_traits::ToPrimitive::to_u32(&y)
5298                    .ok_or_else(|| "RangeError: Maximum BigInt size exceeded".to_string())?;
5299                num_traits::Pow::pow(x, exp)
5300            }
5301            _ => return Err(type_error("unsupported BigInt operation")),
5302        };
5303        Ok(self.new_bigint(r))
5304    }
5305
5306    /// BigInt bitwise (`& | ^ << >>`); `>>>` has no BigInt form. Both operands must
5307    /// be BigInt (mixing throws).
5308    fn bigint_bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5309        let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5310            (Some(x), Some(y)) => (x, y),
5311            _ => {
5312                return Err(type_error(
5313                    "Cannot mix BigInt and other types, use explicit conversions",
5314                ))
5315            }
5316        };
5317        let r = match tag {
5318            binop::BITAND => x & y,
5319            binop::BITOR => x | y,
5320            binop::BITXOR => x ^ y,
5321            binop::SHL => {
5322                let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5323                if n >= 0 {
5324                    x << (n as usize)
5325                } else {
5326                    x >> ((-n) as usize)
5327                }
5328            }
5329            binop::SHR => {
5330                let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5331                if n >= 0 {
5332                    x >> (n as usize)
5333                } else {
5334                    x << ((-n) as usize)
5335                }
5336            }
5337            binop::USHR => {
5338                return Err(type_error(
5339                    "BigInts have no unsigned right shift, use >> instead",
5340                ))
5341            }
5342            _ => return Err(type_error("unsupported BigInt operation")),
5343        };
5344        Ok(self.new_bigint(r))
5345    }
5346
5347    /// BigInt ⇄ (Number | Boolean | String | Object) loose equality (`==`). Both
5348    /// being BigInt was already handled by `strict_eq`.
5349    fn bigint_loose_eq(&self, a: &Value, b: &Value) -> bool {
5350        // Order so `big` is the BigInt side and `other` the counterpart.
5351        let (big, other) = match (self.as_bigint(a), self.as_bigint(b)) {
5352            (Some(x), _) => (x, b),
5353            (_, Some(y)) => (y, a),
5354            _ => return false,
5355        };
5356        match other {
5357            Value::Bool(bo) => big == num_bigint::BigInt::from(*bo as i64),
5358            Value::Int(n) => big == num_bigint::BigInt::from(*n),
5359            Value::Float(f) => {
5360                // Equal only when the float is an integer with the same value.
5361                if !f.is_finite() || f.fract() != 0.0 {
5362                    return false;
5363                }
5364                bigint_to_f64(&big) == *f
5365            }
5366            Value::Str(s) => match parse_bigint_str(s) {
5367                Some(bs) => big == bs,
5368                None => false,
5369            },
5370            Value::Obj(_) => match self.get(other) {
5371                // A heap string parses like a primitive string.
5372                Some(JsObj::Str(s)) => parse_bigint_str(s).map(|bs| big == bs).unwrap_or(false),
5373                _ => {
5374                    // Other objects reduce via ToPrimitive (their string form).
5375                    let s = self.str_of(other);
5376                    parse_bigint_str(&s).map(|bs| big == bs).unwrap_or(false)
5377                }
5378            },
5379            _ => false,
5380        }
5381    }
5382}
5383
5384/// Parse a string to a BigInt under JS `StringToBigInt` rules: trimmed, empty →
5385/// `0n`, decimal or `0x`/`0o`/`0b` prefixed; any junk → `None`.
5386pub fn parse_bigint_str(s: &str) -> Option<num_bigint::BigInt> {
5387    let t = crate::utf16::js_trim(s);
5388    if t.is_empty() {
5389        return Some(num_bigint::BigInt::from(0));
5390    }
5391    let (radix, digits) = if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5392        (16, h)
5393    } else if let Some(o) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5394        (8, o)
5395    } else if let Some(bb) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5396        (2, bb)
5397    } else {
5398        (10, t)
5399    };
5400    num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
5401}
5402
5403/// Coerce a BigInt to `f64` (for `Number(bigint)` and mixed relational compares);
5404/// out-of-range magnitudes become ±Infinity, matching Node.
5405pub fn bigint_to_f64(b: &num_bigint::BigInt) -> f64 {
5406    num_traits::ToPrimitive::to_f64(b).unwrap_or_else(|| {
5407        if num_traits::Signed::is_negative(b) {
5408            f64::NEG_INFINITY
5409        } else {
5410            f64::INFINITY
5411        }
5412    })
5413}
5414
5415/// JS `%` remainder (sign follows the dividend; matches `f64::rem`).
5416fn js_mod(a: f64, b: f64) -> f64 {
5417    a % b
5418}
5419
5420/// Cycle bookkeeping for one `util.inspect` render.
5421///
5422/// `seen` is the chain of objects currently being rendered (an entry appearing
5423/// twice is a back-edge), and `refs` records every object a back-edge pointed
5424/// at, in first-encountered order — its position + 1 is the `*N` id Node prints
5425/// in `[Circular *N]` / `<ref *N>`.
5426/// How an array-shaped group is laid out, beyond its entries themselves.
5427#[derive(Clone, Copy)]
5428struct ArrayLayout<'a> {
5429    /// Extra own properties follow the elements, which suppresses grid grouping.
5430    has_props: bool,
5431    /// `output`'s last entry is the `... N more items` tail rather than a real
5432    /// element, so the grid must not size a column to it.
5433    has_tail: bool,
5434    /// A constructor tag printed before the brackets, with a trailing space
5435    /// (`"Uint8Array(3) "`), or empty for a plain array.
5436    base: &'a str,
5437}
5438
5439#[derive(Default)]
5440struct InspectCycles {
5441    seen: Vec<Value>,
5442    refs: Vec<Value>,
5443    /// The indent level of the value most recently EXPANDED — node's
5444    /// `ctx.currentDepth`. `reduceToSingleString` puts a group on one line only
5445    /// while `currentDepth - thisDepth < compact`, so without it a deeply
5446    /// nested object printed on one line where node breaks the outer levels.
5447    deepest: usize,
5448}
5449
5450impl InspectCycles {
5451    /// Record `v` as a cycle target (idempotent) and return its 1-based id.
5452    fn mark(&mut self, h: &JsHost, v: &Value) -> usize {
5453        if let Some(id) = self.id_of(h, v) {
5454            return id;
5455        }
5456        self.refs.push(v.clone());
5457        self.refs.len()
5458    }
5459
5460    /// The `*N` id already assigned to `v`, if any.
5461    fn id_of(&self, h: &JsHost, v: &Value) -> Option<usize> {
5462        self.refs
5463            .iter()
5464            .position(|p| h.strict_eq(p, v))
5465            .map(|i| i + 1)
5466    }
5467}
5468
5469thread_local! {
5470    /// The active `util.inspect` `depth` (nesting levels shown before collapsing
5471    /// to `[Object]`/`[Array]`). Node's default is 2; `util.inspect(v,{depth:N})`
5472    /// overrides it for one call, `console.log`/`util.format` use the default.
5473    /// Signed, because `util.inspect(v, { depth: -1 })` is legal and means
5474    /// "already past the limit" — everything collapses to `[Object]` at the top
5475    /// level. Held as `usize` it read as an enormous depth and expanded fully.
5476    static INSPECT_MAX_DEPTH: std::cell::Cell<i64> = const { std::cell::Cell::new(2) };
5477
5478    /// `util.inspect`'s `compact` option. Node's default is the NUMBER 3: a
5479    /// group is put on one line only when the subtree below it is shallower
5480    /// than this. `compact: false` is held as 0, which no subtree depth is
5481    /// below, so every group breaks — which is exactly what node does.
5482    static INSPECT_COMPACT: std::cell::Cell<i64> = const { std::cell::Cell::new(DEFAULT_COMPACT) };
5483
5484    /// `util.inspect`'s `breakLength`. Node's default is 128, but `util.inspect`
5485    /// itself passes 80.
5486    static INSPECT_BREAK_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(80) };
5487
5488    /// `util.inspect`'s `sorted` option: emit an object's own keys in code-unit
5489    /// order instead of insertion order. Off by default. `assert`'s diff renderer
5490    /// turns it on so that two objects built with the same keys in a different
5491    /// order diff as equal rather than as a wholesale rewrite.
5492    static INSPECT_SORTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5493
5494    /// `util.inspect`'s `maxArrayLength`: how many entries are formatted before
5495    /// the rest collapse into `... N more items`. Node's default is 100;
5496    /// `Infinity`/`null` means "all", held here as `usize::MAX`.
5497    static INSPECT_MAX_ARRAY_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(DEFAULT_MAX_ARRAY_LENGTH) };
5498
5499    /// `util.inspect`'s `customInspect` option: whether a value's own
5500    /// `[util.inspect.custom]` rendering is used. On by default; `assert` turns
5501    /// it off so a diff shows an object's real structure rather than whatever
5502    /// summary it prefers to print.
5503    static INSPECT_CUSTOM: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
5504
5505    /// `util.inspect`'s `showHidden`: reveal the non-enumerable slots a value
5506    /// carries — an array's `length`, a typed array's element width and window
5507    /// onto its backing store. Off by default; `util.format`'s `%o` turns it on.
5508    static INSPECT_SHOW_HIDDEN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5509}
5510
5511/// Set the `util.inspect` `showHidden` option for the next render.
5512pub fn set_inspect_show_hidden(s: bool) {
5513    INSPECT_SHOW_HIDDEN.with(|x| x.set(s));
5514}
5515
5516pub(crate) fn inspect_show_hidden() -> bool {
5517    INSPECT_SHOW_HIDDEN.with(|x| x.get())
5518}
5519
5520/// Set the `util.inspect` `customInspect` option for the next render.
5521pub fn set_inspect_custom(c: bool) {
5522    INSPECT_CUSTOM.with(|x| x.set(c));
5523}
5524
5525pub(crate) fn inspect_custom() -> bool {
5526    INSPECT_CUSTOM.with(|x| x.get())
5527}
5528
5529/// Set the `util.inspect` `sorted` option for the next render.
5530pub fn set_inspect_sorted(s: bool) {
5531    INSPECT_SORTED.with(|x| x.set(s));
5532}
5533
5534pub(crate) fn inspect_sorted() -> bool {
5535    INSPECT_SORTED.with(|x| x.get())
5536}
5537
5538/// Set the `util.inspect` `maxArrayLength` for the next render.
5539pub fn set_inspect_max_array_length(n: usize) {
5540    INSPECT_MAX_ARRAY_LENGTH.with(|x| x.set(n));
5541}
5542
5543pub(crate) fn inspect_max_array_length() -> usize {
5544    INSPECT_MAX_ARRAY_LENGTH.with(|x| x.get())
5545}
5546
5547/// Set the `util.inspect` `compact` option for the next render (0 for `false`).
5548pub fn set_inspect_compact(c: i64) {
5549    INSPECT_COMPACT.with(|x| x.set(c));
5550}
5551
5552/// Set the `util.inspect` `breakLength` for the next render.
5553pub fn set_inspect_break_length(n: usize) {
5554    INSPECT_BREAK_LENGTH.with(|x| x.set(n));
5555}
5556
5557fn inspect_compact() -> i64 {
5558    INSPECT_COMPACT.with(|x| x.get())
5559}
5560
5561/// Set the `util.inspect` depth for the next render (restore to 2 after).
5562pub fn set_inspect_max_depth(d: i64) {
5563    INSPECT_MAX_DEPTH.with(|c| c.set(d));
5564}
5565/// Twice the configured depth, which is what the inspect walk compares its
5566/// indent against. Saturating, because `util.inspect(x, { depth: null })` and
5567/// `{ depth: Infinity }` both set the depth to `usize::MAX`, and doubling that
5568/// overflowed and panicked the process — an abort no script could catch.
5569fn inspect_indent_limit() -> i64 {
5570    inspect_max_depth().saturating_mul(2)
5571}
5572
5573fn inspect_max_depth() -> i64 {
5574    INSPECT_MAX_DEPTH.with(|c| c.get())
5575}
5576
5577/// ECMA-262 `ToInt32` (7.1.6): truncate toward zero, reduce modulo 2^32, then
5578/// reinterpret as signed.
5579///
5580/// The reduction has to happen in `f64`, not by casting through `i64`. Rust
5581/// saturates an out-of-range float-to-int cast, so `1e300 as i64` is `i64::MAX`
5582/// and `1e300 | 0` came out `-1` where every engine says `0`; the same
5583/// saturation made `1e300 >>> 0` report `4294967295`. `rem_euclid` on a
5584/// power-of-two modulus is exact for every finite double, so this is the whole
5585/// fix — and it is the form `Math.clz32` already used.
5586pub(crate) fn to_int32(f: f64) -> i32 {
5587    to_uint32(f) as i32
5588}
5589pub(crate) fn to_uint32(f: f64) -> u32 {
5590    if !f.is_finite() {
5591        return 0;
5592    }
5593    f.trunc().rem_euclid(4294967296.0) as u32
5594}
5595
5596/// Parse a string in numeric context (`ToNumber`): trimmed, empty -> 0.
5597fn str_to_number(s: &str) -> f64 {
5598    let t = crate::utf16::js_trim(s);
5599    if t.is_empty() {
5600        return 0.0;
5601    }
5602    if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5603        return i64::from_str_radix(hex, 16)
5604            .map(|n| n as f64)
5605            .unwrap_or(f64::NAN);
5606    }
5607    if let Some(oct) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5608        return i64::from_str_radix(oct, 8)
5609            .map(|n| n as f64)
5610            .unwrap_or(f64::NAN);
5611    }
5612    if let Some(bin) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5613        return i64::from_str_radix(bin, 2)
5614            .map(|n| n as f64)
5615            .unwrap_or(f64::NAN);
5616    }
5617    match t {
5618        "Infinity" | "+Infinity" => f64::INFINITY,
5619        "-Infinity" => f64::NEG_INFINITY,
5620        _ => t.parse::<f64>().unwrap_or(f64::NAN),
5621    }
5622}
5623
5624/// `util.inspect` break length (the width past which entries wrap). Node's default.
5625fn break_length() -> usize {
5626    INSPECT_BREAK_LENGTH.with(|x| x.get())
5627}
5628/// Node's default `compact` setting (the `compact * 4` column cap term).
5629/// Node's DEFAULT `compact` setting, and the initial value of
5630/// `INSPECT_COMPACT`. The grid's column cap is `compact * 4`, so it has to be
5631/// read through `inspect_compact()` at render time: under `{ compact: 1 }` node
5632/// lays a byte array out four columns wide, and the hardcoded 3 gave twelve.
5633const DEFAULT_COMPACT: i64 = 3;
5634/// Node's default `maxArrayLength` — the initial value of
5635/// `INSPECT_MAX_ARRAY_LENGTH`, which `util.inspect(v, { maxArrayLength: N })`
5636/// overrides per call. Read it through `inspect_max_array_length()`, never
5637/// directly: as a bare constant the option had no effect and a 120-element array
5638/// was truncated at 100 even under `maxArrayLength: Infinity`.
5639pub(crate) const DEFAULT_MAX_ARRAY_LENGTH: usize = 100;
5640
5641/// Whether `output` fits on a single line — a faithful port of Node's
5642/// `isBelowBreakLength` (no colors, no `base`). `start` is the caller's seed
5643/// length (braces + indentation + slack).
5644fn is_below_break_length(output: &[String], start: usize) -> bool {
5645    let limit = break_length();
5646    let mut total = output.len() + start;
5647    if total + output.len() > limit {
5648        return false;
5649    }
5650    for o in output {
5651        if o.contains('\n') {
5652            return false;
5653        }
5654        total += o.chars().count();
5655        if total > limit {
5656            return false;
5657        }
5658    }
5659    true
5660}
5661
5662/// Faithful port of Node's `util.inspect` `groupArrayElements`: lay out the
5663/// already-formatted element strings into an aligned multi-column grid. Returns
5664/// `(lines, grouped)` — `grouped` is false when Node would leave the output
5665/// ungrouped (so the caller falls back to single-line / one-per-line).
5666fn group_array_elements(
5667    host: &JsHost,
5668    output: &[String],
5669    values: &[Value],
5670    indentation_lvl: usize,
5671    has_tail: bool,
5672) -> (Vec<String>, bool) {
5673    let separator_space = 2usize; // ", " between entries
5674                                  // A `... N more items` tail is not an element: node drops it from the grid
5675                                  // (`outputLength--`) so it neither widens a column nor occupies a cell, then
5676                                  // re-appends it as its own final line.
5677    let output_length = output.len() - usize::from(has_tail);
5678    let data_len: Vec<usize> = output.iter().map(|o| o.chars().count()).collect();
5679    let mut total_length = 0usize;
5680    let mut max_length = 0usize;
5681    for &len in &data_len[..output_length] {
5682        total_length += len + separator_space;
5683        if len > max_length {
5684            max_length = len;
5685        }
5686    }
5687    let actual_max = max_length + separator_space;
5688    // Only group when ≥3 entries fit across AND the entries aren't wildly uneven.
5689    if !(actual_max * 3 + indentation_lvl < break_length()
5690        && (total_length as f64 / actual_max as f64 > 5.0 || max_length <= 6))
5691    {
5692        return (output.to_vec(), false);
5693    }
5694    let approx_char_heights = 2.5f64;
5695    let average_bias = (actual_max as f64 - total_length as f64 / output_length as f64).sqrt();
5696    let biased_max = (actual_max as f64 - 3.0 - average_bias).max(1.0);
5697    // Ideally a square grid; capped by break length, compact*4, and 15 columns.
5698    let columns = [
5699        ((approx_char_heights * biased_max * output_length as f64).sqrt() / biased_max).round()
5700            as i64,
5701        ((break_length() - indentation_lvl) as f64 / actual_max as f64).floor() as i64,
5702        inspect_compact().saturating_mul(4),
5703        15,
5704    ]
5705    .into_iter()
5706    .min()
5707    .unwrap();
5708    if columns <= 1 {
5709        return (output.to_vec(), false);
5710    }
5711    let columns = columns as usize;
5712    // The widest entry (plus separator) in each column.
5713    let mut max_line_length = vec![0usize; columns];
5714    for (i, slot) in max_line_length.iter_mut().enumerate() {
5715        let mut line_length = 0;
5716        let mut j = i;
5717        while j < output_length {
5718            if data_len[j] > line_length {
5719                line_length = data_len[j];
5720            }
5721            j += columns;
5722        }
5723        *slot = line_length + separator_space;
5724    }
5725    // Right-align (padStart) only when every element is a number/bigint.
5726    let pad_start = values.iter().all(|v| {
5727        matches!(v, Value::Int(_) | Value::Float(_))
5728            || matches!(host.get(v), Some(JsObj::BigInt(_)))
5729    });
5730    let mut tmp = Vec::new();
5731    let mut i = 0;
5732    while i < output_length {
5733        let max = (i + columns).min(output_length);
5734        let mut str_line = String::new();
5735        let mut j = i;
5736        while j < max.saturating_sub(1) {
5737            // `output[j]` has no colors here, so padding == max_line_length[col].
5738            let col = j - i;
5739            let cell = format!("{}, ", output[j]);
5740            let target = max_line_length[col];
5741            str_line.push_str(&pad_to(&cell, target, pad_start));
5742            j += 1;
5743        }
5744        // The last cell of the row: right-aligned entries pad without the ", ".
5745        if pad_start {
5746            let col = j - i;
5747            let target = max_line_length[col] - separator_space;
5748            str_line.push_str(&pad_to(&output[j], target, true));
5749        } else {
5750            str_line.push_str(&output[j]);
5751        }
5752        tmp.push(str_line);
5753        i += columns;
5754    }
5755    if has_tail {
5756        tmp.push(output[output_length].clone());
5757    }
5758    (tmp, true)
5759}
5760
5761/// Pad `s` to `width` chars: right-justified when `pad_start`, else left-justified.
5762/// (Padding is measured in chars; already ANSI-free here.)
5763fn pad_to(s: &str, width: usize, pad_start: bool) -> String {
5764    let len = s.chars().count();
5765    if len >= width {
5766        return s.to_string();
5767    }
5768    let fill = " ".repeat(width - len);
5769    if pad_start {
5770        format!("{fill}{s}")
5771    } else {
5772        format!("{s}{fill}")
5773    }
5774}
5775
5776/// Quote a string the way `util.inspect` does — a port of `strEscape` in Node's
5777/// `lib/internal/util/inspect.js`.
5778///
5779/// The quote character is chosen so the contents need as little escaping as
5780/// possible: single quotes normally, double quotes when the string contains a
5781/// `'` but no `"`, and a backtick when it contains both (and neither a backtick
5782/// nor a `${`). Only the ACTIVE quote is backslash-escaped, alongside `\` and
5783/// the C0 controls + DEL, which use Node's `meta` table (`\n`, `\t`, `\b`,
5784/// `\f`, `\r` short forms; `\x0B`, `\x1F`, `\x7F` uppercase-hex otherwise).
5785fn quote_str(s: &str) -> String {
5786    let quote = if !s.contains('\'') {
5787        '\''
5788    } else if !s.contains('"') {
5789        '"'
5790    } else if !s.contains('`') && !s.contains("${") {
5791        '`'
5792    } else {
5793        '\''
5794    };
5795    let mut out = String::with_capacity(s.len() + 2);
5796    out.push(quote);
5797    for c in s.chars() {
5798        match c {
5799            _ if c == quote => {
5800                out.push('\\');
5801                out.push(c);
5802            }
5803            '\\' => out.push_str("\\\\"),
5804            '\u{8}' => out.push_str("\\b"),
5805            '\t' => out.push_str("\\t"),
5806            '\n' => out.push_str("\\n"),
5807            '\u{c}' => out.push_str("\\f"),
5808            '\r' => out.push_str("\\r"),
5809            '\u{0}'..='\u{1f}' | '\u{7f}' => out.push_str(&format!("\\x{:02X}", c as u32)),
5810            _ => out.push(c),
5811        }
5812    }
5813    out.push(quote);
5814    out
5815}
5816
5817/// Render an object key: bare if it is a valid identifier, quoted otherwise.
5818fn fmt_key(k: &str) -> String {
5819    let ok = !k.is_empty()
5820        && k.chars()
5821            .next()
5822            .map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
5823            .unwrap_or(false)
5824        && k.chars()
5825            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
5826    if ok {
5827        k.to_string()
5828    } else {
5829        quote_str(k)
5830    }
5831}
5832
5833// ── iteration ────────────────────────────────────────────────────────────────
5834
5835impl JsHost {
5836    /// Collect an iterable into a vector of values (arrays, strings, Map/Set).
5837    /// Generators and user `Symbol.iterator` objects go through `iter_all`, which
5838    /// holds no host borrow across resumes.
5839    pub fn iter_vec(&mut self, v: &Value) -> Result<Vec<Value>, String> {
5840        match self.get(v) {
5841            Some(JsObj::Array(items)) => Ok(items.clone()),
5842            Some(JsObj::Str(s)) => {
5843                let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
5844                Ok(chars.into_iter().map(|c| self.new_str(c)).collect())
5845            }
5846            Some(JsObj::Iter { items, idx }) => Ok(items[*idx..].to_vec()),
5847            Some(JsObj::Set { entries, .. }) => Ok(entries.values().cloned().collect()),
5848            Some(JsObj::Map { entries, .. }) => {
5849                // Map iterates as `[key, value]` pairs.
5850                let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
5851                Ok(pairs
5852                    .into_iter()
5853                    .map(|(k, v)| self.new_array(vec![k, v]))
5854                    .collect())
5855            }
5856            // A `Buffer` iterates over its BYTES and a typed array over its
5857            // ELEMENTS — both are iterable in Node. Only `@@bytes` was handled
5858            // here, so `[...buf]` worked while `[...new Uint8Array([1])]` threw
5859            // "object is not iterable", which is the same invariant holding at
5860            // one of its two sites.
5861            Some(JsObj::Object(props))
5862                if props.contains_key("@@bytes") || props.contains_key("@@buffer") =>
5863            {
5864                // Iterating a view over a DETACHED buffer throws, naming the
5865                // `values` iterator — reading its elements answers zero length,
5866                // but spreading it is a method call and does not.
5867                if crate::stdlib::typedarray::view_detached_h(self, v) {
5868                    return Err(crate::stdlib::typedarray::detached_error(
5869                        "%TypedArray%.prototype",
5870                        "values",
5871                        false,
5872                    ));
5873                }
5874                Ok(crate::stdlib::typedarray::elems_mut_host(self, v))
5875            }
5876            // V8 names the VALUE, not its type: `[...5]` is `5 is not iterable`,
5877            // `[...{}]` is `{} is not iterable`. Reporting `typeof` instead
5878            // produced `number is not iterable`, which no engine emits.
5879            _ => {
5880                let shown = self.inspect(v);
5881                Err(type_error(&format!("{shown} is not iterable")))
5882            }
5883        }
5884    }
5885
5886    /// Enumerable string keys of an object/array (for `for-in`). Internal
5887    /// symbol-keyed props (`@@…`) are not enumerable.
5888    /// `for-in` visits own enumerable keys, then every *inherited* enumerable key
5889    /// not already seen, walking the whole prototype chain. Class methods and the
5890    /// builtin prototypes are non-enumerable, so in practice this only surfaces
5891    /// keys a script put on a prototype itself (`F.prototype.y = 2`) — but that
5892    /// is exactly the constructor-function idiom older packages are written in.
5893    pub fn enum_keys(&mut self, v: &Value) -> Vec<Value> {
5894        let mut keys = self.own_enum_key_names(v);
5895        let mut cur = self.proto_of(v);
5896        let mut hops = 0;
5897        while let Some(p) = cur {
5898            // A cyclic or pathologically deep chain must not hang the loop.
5899            hops += 1;
5900            if hops > 100 || matches!(p, Value::Undef) || self.is_null(&p) {
5901                break;
5902            }
5903            for k in self.own_enum_key_names(&p) {
5904                if !keys.contains(&k) {
5905                    keys.push(k);
5906                }
5907            }
5908            cur = self.proto_of(&p);
5909        }
5910        keys.into_iter().map(|k| self.new_str(k)).collect()
5911    }
5912
5913    /// The own *enumerable* string keys of `v`, in property order — the single
5914    /// source of truth behind `for-in`, `Object.keys`/`values`/`entries`,
5915    /// object spread, `Object.assign` and `JSON.stringify`. Internal slots
5916    /// (`@@…`), private fields (`#…`) and anything marked non-enumerable via
5917    /// `prop_attrs` are excluded.
5918    pub fn own_enum_key_names(&self, v: &Value) -> Vec<String> {
5919        self.own_key_names(v, true)
5920    }
5921
5922    /// Own string keys of `v` in insertion order. `enum_only` drops the
5923    /// non-enumerable ones (`Object.keys`); otherwise every own key is reported
5924    /// (`getOwnPropertyNames`/`Reflect.ownKeys`).
5925    pub fn own_key_names(&self, v: &Value, enum_only: bool) -> Vec<String> {
5926        let mut keys = self.own_enum_data_keys(v, enum_only);
5927        // A global a SCRIPT created (`x = 1` with no declaration) is an own
5928        // ENUMERABLE property of the global object, but lives in the globals map
5929        // rather than in its property map — so no listing saw it, while
5930        // `globalThis.x` read it back and its descriptor called it enumerable.
5931        if self.is_global_object(v) {
5932            for k in self.globals.keys() {
5933                if !keys.contains(k) {
5934                    keys.push(k.clone());
5935                }
5936            }
5937        }
5938        // A RegExp's `lastIndex` is a SYNTHESIZED own property — it lives in the
5939        // `RegExpObj` struct, not a property map — so nothing above can list it.
5940        // Non-enumerable, so only `getOwnPropertyNames` sees it.
5941        if !enum_only && matches!(self.get(v), Some(JsObj::RegExp(_))) {
5942            keys.push("lastIndex".to_string());
5943        }
5944        // An accessor defined before its object had any ordering marker (a class
5945        // prototype accessor, say) still has to appear.
5946        for k in self.own_accessor_keys(v) {
5947            if (!enum_only || self.prop_attrs(v, &k).enumerable) && !keys.contains(&k) {
5948                keys.push(k);
5949            }
5950        }
5951        keys
5952    }
5953
5954    /// The keys that own a slot in the object's property map, in insertion
5955    /// order, resolving accessor ordering markers back to their real key.
5956    /// Every global a SCRIPT created, in creation order — the own enumerable
5957    /// keys of the global object that live in the globals map rather than in
5958    /// its property map. `x = 1` with no declaration makes one, and
5959    /// `Object.keys(globalThis)` reports it in node.
5960    pub fn script_global_names(&self) -> Vec<String> {
5961        self.globals.keys().cloned().collect()
5962    }
5963    /// Drop a global a script created. Reports whether it was there.
5964    pub fn remove_global(&mut self, name: &str) -> bool {
5965        self.globals.shift_remove(name).is_some()
5966    }
5967    fn own_enum_data_keys(&self, v: &Value, enum_only: bool) -> Vec<String> {
5968        match self.get(v) {
5969            // A `Buffer` is an index-keyed exotic: its own enumerable keys are
5970            // `"0".."len-1"` (the bytes live in the hidden `@@bytes` slot), never
5971            // the `length`/`byteLength` view metadata, which V8 keeps on the
5972            // prototype chain or as non-enumerable own slots.
5973            // A `Buffer` and every other typed array are index-keyed exotics:
5974            // their own enumerable keys are `"0".."len-1"` (the elements live in
5975            // a hidden slot), never the `length`/`byteLength` view metadata,
5976            // which V8 keeps on the prototype chain or as non-enumerable own
5977            // slots. Only `Buffer` had this arm, so `Object.keys(u8)` was empty
5978            // and `JSON.stringify(u8)` was `{}` where node gives
5979            // `{"0":10,"1":9}` — `hasOwnProperty(0)` already answered true, so
5980            // the two views of the same question disagreed.
5981            Some(JsObj::Object(props))
5982                if matches!(
5983                    props.get("@@native").map(|t| self.str_of(t)).as_deref(),
5984                    Some("Buffer") | Some("TypedArray")
5985                ) =>
5986            {
5987                // A view over a DETACHED buffer has no index properties at all:
5988                // its own `length` still holds the old count, so reading that
5989                // back left `Object.keys` listing eight names over no bytes.
5990                if crate::stdlib::typedarray::view_detached_h(self, v) {
5991                    return Vec::new();
5992                }
5993                // A Buffer counts its byte store; every other view reports the
5994                // element count of its window onto the ArrayBuffer.
5995                let n = match props.get("@@bytes").and_then(|b| self.get(b)) {
5996                    Some(JsObj::Array(items)) => items.len(),
5997                    _ => props
5998                        .get("length")
5999                        .map(|l| self.to_number(l))
6000                        .unwrap_or(0.0) as usize,
6001                };
6002                (0..n).map(|i| i.to_string()).collect()
6003            }
6004            Some(JsObj::Object(props)) => props
6005                .keys()
6006                .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6007                    Some(real) => Some(real.to_string()),
6008                    None if !k.starts_with("@@") && !k.starts_with('#') => Some(k.clone()),
6009                    None => None,
6010                })
6011                .filter(|k| !enum_only || self.prop_attrs(v, k).enumerable)
6012                .collect(),
6013            // A STRING is an index-keyed exotic too (10.4.3): its own keys are
6014            // its UTF-16 code-unit indices, plus the non-enumerable `length`.
6015            // Without this arm every whole-object view of a string primitive was
6016            // empty — `for (const k in 'ab')` iterated nothing, `Object.keys`
6017            // and `Object.assign({}, 'ab')` reported `{}` — while `'ab'[0]` and
6018            // `'ab'.length` answered normally, so the two views disagreed. The
6019            // spread form `{...'ab'}` went through a different path and was
6020            // already right, which is what made the gap easy to miss.
6021            Some(JsObj::Str(s)) => {
6022                let mut keys: Vec<String> =
6023                    (0..crate::utf16::len(s)).map(|i| i.to_string()).collect();
6024                if !enum_only {
6025                    keys.push("length".into());
6026                }
6027                keys
6028            }
6029            // `OrdinaryOwnPropertyKeys` on an array exotic: the integer indices
6030            // ascending, then the exotic non-enumerable `length`, then the
6031            // ordinary string keys in insertion order. Those ordinary keys have
6032            // no property map to live in — a `str.match()` result's
6033            // `index`/`input`/`groups` and any user-assigned `arr.foo` are kept
6034            // in the fn-prop side table — so they are read back from there.
6035            Some(JsObj::Array(items)) => {
6036                // An ELIDED element is not an own property at all, so it
6037                // contributes no key — the difference behind
6038                // `Object.keys([1,,3])` being `['0','2']`.
6039                let mut keys: Vec<String> = (0..items.len())
6040                    .filter(|i| !self.is_hole(v, *i))
6041                    .map(|i| i.to_string())
6042                    .collect();
6043                if !enum_only {
6044                    keys.push("length".into());
6045                }
6046                keys.extend(self.fn_prop_keys(v).into_iter().filter(|k| {
6047                    !k.starts_with("@@")
6048                        && !k.starts_with('#')
6049                        && (!enum_only || self.prop_attrs(v, k).enumerable)
6050                }));
6051                keys
6052            }
6053            // A function/class keeps every own property in the side table. Its
6054            // exotic `name`/`length`/`prototype` and its class methods are all
6055            // non-enumerable, so under `enum_only` what is left is exactly what
6056            // a script assigned; `getOwnPropertyNames` reports the exotics too,
6057            // in V8's order (`length`, `name`, `prototype`, then the rest).
6058            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
6059                let mut keys: Vec<String> = Vec::new();
6060                if !enum_only {
6061                    keys.push("length".into());
6062                    keys.push("name".into());
6063                    if self.owns_prototype(v) {
6064                        keys.push("prototype".into());
6065                    }
6066                }
6067                let rest: Vec<String> = self
6068                    .fn_prop_keys(v)
6069                    .into_iter()
6070                    // An accessor's ordering marker resolves back to its real
6071                    // key, so a static getter enumerates where it was declared.
6072                    .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6073                        Some(real) => Some(real.to_string()),
6074                        None if !k.starts_with("@@") && !k.starts_with('#') => Some(k),
6075                        None => None,
6076                    })
6077                    .filter(|k| {
6078                        !keys.contains(k) && (!enum_only || self.prop_attrs(v, k).enumerable)
6079                    })
6080                    .collect();
6081                keys.extend(rest);
6082                keys
6083            }
6084            // A builtin namespace (`require('buffer')`, `Buffer`) enumerates the
6085            // members node-js implements, so a package that copies a namespace
6086            // key-by-key gets the working set instead of an empty object.
6087            Some(JsObj::Builtin(ns)) => crate::stdlib::namespace_keys(&ns.clone()),
6088            // A `Map`/`Set`/`Promise`/`RegExp`/generator holds only its internal
6089            // slots, so what a script assigned lives in the side table — and is
6090            // just as much an own property as an object's.
6091            Some(_) => self
6092                .fn_prop_keys(v)
6093                .into_iter()
6094                .filter(|k| {
6095                    !k.starts_with("@@")
6096                        && !k.starts_with('#')
6097                        && (!enum_only || self.prop_attrs(v, k).enumerable)
6098                })
6099                .collect(),
6100            _ => Vec::new(),
6101        }
6102    }
6103
6104    /// The own enumerable `(key, value)` pairs of `v`. Buffer index keys resolve
6105    /// through the byte store; everything else reads the property map. Own
6106    /// accessor keys come back as `Undef` here — `own_enum_entries_deep` runs
6107    /// their getters, which cannot happen under the host borrow.
6108    pub fn own_enum_entries(&self, v: &Value) -> Vec<(String, Value)> {
6109        self.own_enum_key_names(v)
6110            .into_iter()
6111            .map(|k| {
6112                let val = match self.get(v) {
6113                    // A Buffer's index keys read out of the hidden `@@bytes`
6114                    // array; resolve inline rather than through
6115                    // `buffer::byte_get`, which would re-borrow the host.
6116                    Some(JsObj::Object(props)) => props.get(&k).cloned().unwrap_or_else(|| {
6117                        // A Buffer's elements live in `@@bytes` and every
6118                        // other typed array's in `@@elems`; both are index
6119                        // keys with no entry in the property map.
6120                        match k.parse::<usize>() {
6121                            Ok(i) => crate::stdlib::typedarray::elems_with_host(self, v)
6122                                .get(i)
6123                                .cloned()
6124                                .unwrap_or(Value::Undef),
6125                            _ => Value::Undef,
6126                        }
6127                    }),
6128                    // A Map/Set/Promise/RegExp/generator keeps every own
6129                    // property in the side table.
6130                    Some(
6131                        JsObj::Map { .. }
6132                        | JsObj::Set { .. }
6133                        | JsObj::Promise { .. }
6134                        | JsObj::RegExp(_)
6135                        | JsObj::Generator { .. }
6136                        | JsObj::Symbol { .. }
6137                        | JsObj::BigInt(_)
6138                        | JsObj::Iter { .. },
6139                    ) => self.fn_prop(v, &k).unwrap_or(Value::Undef),
6140                    // An index reads the element; any other own key (`foo`,
6141                    // a match result's `index`) lives in the side table.
6142                    Some(JsObj::Array(items)) => k
6143                        .parse::<usize>()
6144                        .ok()
6145                        .and_then(|i| items.get(i).cloned())
6146                        .or_else(|| self.fn_prop(v, &k))
6147                        .unwrap_or(Value::Undef),
6148                    Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
6149                        self.fn_prop(v, &k).unwrap_or(Value::Undef)
6150                    }
6151                    _ => Value::Undef,
6152                };
6153                (k, val)
6154            })
6155            .collect()
6156    }
6157}
6158
6159/// The own enumerable `(key, value)` pairs of `v` with every enumerable own
6160/// accessor's getter invoked — the observable shape `Object.values`,
6161/// `Object.entries`, object spread and `JSON.stringify` all need. Must be called
6162/// outside a `with_host` borrow because a getter re-enters the host.
6163pub fn own_enum_entries_deep(v: &Value) -> Result<Vec<(String, Value)>, String> {
6164    // A Proxy has no property map at all: its own enumerable entries come from
6165    // the `ownKeys` + `getOwnPropertyDescriptor` + `get` traps. A trap that
6166    // throws surfaces as an empty result here because this signature is
6167    // infallible; the callers that MUST propagate a trap throw (`Object.keys`
6168    // and friends) go through `builtins::object_keys`, which does.
6169    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
6170        return crate::proxy::own_enum_entries(v);
6171    }
6172    // A builtin namespace (`require('path')`, `Buffer`) has no property map at
6173    // all: its members are resolved on demand by `namespace_property`, which
6174    // re-enters the host and so cannot run inside `own_enum_entries`'s borrow.
6175    // Without this, spread and `Object.assign` copied the namespace's KEYS with
6176    // `undefined` for every value — measured against node v26.7.0,
6177    // `{...require('path')}.join` was `undefined` here and a function there,
6178    // while `Object.entries(require('path'))` (which resolves through
6179    // `builtins`, not through this borrow) was already correct. Two enumeration
6180    // paths, one of them silently value-less.
6181    if let Some(ns) = with_host(|h| match h.get(v) {
6182        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6183        _ => None,
6184    }) {
6185        return Ok(with_host(|h| h.own_enum_key_names(v))
6186            .into_iter()
6187            .map(|k| {
6188                let val = crate::builtins::namespace_property(&ns, &k);
6189                (k, val)
6190            })
6191            .collect());
6192    }
6193    // A string primitive's own entries are its code units. `own_enum_entries`
6194    // cannot build them: allocating the one-character string for each index
6195    // needs `&mut` host access, and it runs under a shared borrow.
6196    if let Some(sv) = with_host(|h| match h.get(v) {
6197        Some(JsObj::Str(s)) => Some(s.clone()),
6198        _ => None,
6199    }) {
6200        let units = crate::utf16::Units::of(&sv);
6201        return Ok(with_host(|h| {
6202            (0..units.len())
6203                .filter_map(|i| units.unit_str(i).map(|c| (i.to_string(), h.new_str(c))))
6204                .collect()
6205        }));
6206    }
6207    let accessor_keys: Vec<String> = with_host(|h| {
6208        h.own_accessor_keys(v)
6209            .into_iter()
6210            .filter(|k| h.prop_attrs(v, k).enumerable)
6211            .collect()
6212    });
6213    let entries = with_host(|h| h.own_enum_entries(v));
6214    // A getter that THROWS propagates: `Object.entries`, `Object.assign`,
6215    // object spread and `JSON.stringify` all read through here, and every one
6216    // of them swallowed the exception and reported the property as absent (or
6217    // as `null`) instead.
6218    let mut out = Vec::with_capacity(entries.len());
6219    for (k, val) in entries {
6220        if accessor_keys.contains(&k) {
6221            out.push((k.clone(), get_prop_chain(v, &k)?));
6222        } else {
6223            out.push((k, val));
6224        }
6225    }
6226    Ok(out)
6227}
6228
6229// ── function invocation ──────────────────────────────────────────────────────
6230
6231/// Marshal a JS call argument into a native fusevm `Value` for `rust { }` FFI.
6232/// JS strings ride as `Value::Obj(JsObj::Str)` heap handles, which fusevm's
6233/// marshaller cannot read (it calls `Value::to_str`, which returns `"(obj:N)"`
6234/// for a handle); rewrite them to a native `Value::Str`. Numbers are already
6235/// native `Value::Int`/`Value::Float`, so they pass through (fusevm coerces
6236/// Float→i64/f64 per the export signature).
6237fn marshal_ffi_arg(v: &Value) -> Value {
6238    match v {
6239        Value::Obj(_) => match with_host(|h| h.as_str(v)) {
6240            Some(s) => Value::str(s),
6241            None => v.clone(),
6242        },
6243        _ => v.clone(),
6244    }
6245}
6246
6247/// Resolve a bare name and call it (`f(args)`, `parseInt(args)`).
6248pub fn call_named(name: &str, args: Vec<Value>) -> Result<Value, String> {
6249    // Inline Rust FFI: the `rust { ... }` desugar emits `__rust_compile(b64,
6250    // line)`; compile + register the block's exported functions, returning JS
6251    // `undefined` (`Value::Undef`).
6252    if name == "__rust_compile" {
6253        let b64 = args
6254            .first()
6255            .map(|v| with_host(|h| h.str_of(v)))
6256            .unwrap_or_default();
6257        return fusevm::ffi::compile_and_register(&b64).map(|_| Value::Undef);
6258    }
6259    if let Some(v) = with_host(|h| h.read_name(name)) {
6260        return invoke(&v, args, None);
6261    }
6262    // A DIRECT eval — the literal `eval(src)` call form — is the ONLY one that
6263    // evaluates in the CALLER's scope; `(0, eval)(src)`, `const e = eval; e(src)`
6264    // and `[eval][0](src)` all reach the same function value but are INDIRECT
6265    // evals and evaluate in the global scope (ECMA-262 19.2.1.1 `PerformEval`).
6266    // This is the one place the two forms are distinguishable without a compiler
6267    // change: `call_named` is reached only from `ops::CALL`, which the compiler
6268    // emits exclusively for a bare-identifier callee, while every value-call form
6269    // goes through `invoke` → `call_builtin_function`. The `read_name` miss above
6270    // has already established that `eval` is not shadowed by a user binding.
6271    if name == "eval" {
6272        return crate::builtins::eval_source(args.first(), true);
6273    }
6274    if crate::builtins::is_known_builtin(name) {
6275        return crate::builtins::call_builtin_function(name, args);
6276    }
6277    // A `rust { ... }` block's exported functions are callable by bareword.
6278    // Reached only after user names/globals and builtins all miss, so JS code
6279    // always wins; the registry membership check keeps this off the hot path.
6280    if fusevm::ffi::is_registered(name) {
6281        let margs: Vec<Value> = args.iter().map(marshal_ffi_arg).collect();
6282        if let Some(r) = fusevm::ffi::try_call(name, &margs) {
6283            return r;
6284        }
6285    }
6286    Err(ref_error(name))
6287}
6288
6289thread_local! {
6290    /// The constructor a builtin STATIC is currently being invoked on.
6291    ///
6292    /// `A.from(x)` on `class A extends Array` re-dispatches against the `Array`
6293    /// builtin, which is reached by NAME and so cannot see `A`. The species
6294    /// rules need it: `Array.from`, `Array.of` and every `Promise` static build
6295    /// their result with `this`, so on a subclass they must construct through
6296    /// it. A stack, since one static can call another.
6297    static STATIC_THIS: std::cell::RefCell<Vec<Value>> =
6298        const { std::cell::RefCell::new(Vec::new()) };
6299}
6300
6301/// Run `f` with `recv` recorded as the receiver of a builtin static call.
6302pub fn with_static_this<R>(recv: &Value, f: impl FnOnce() -> R) -> R {
6303    STATIC_THIS.with(|s| s.borrow_mut().push(recv.clone()));
6304    let out = f();
6305    STATIC_THIS.with(|s| {
6306        s.borrow_mut().pop();
6307    });
6308    out
6309}
6310
6311/// The constructor the running builtin static was called on, if it was reached
6312/// through a subclass rather than directly.
6313pub fn current_static_this() -> Option<Value> {
6314    STATIC_THIS.with(|s| s.borrow().last().cloned())
6315}
6316
6317/// `recv.name(args)`.
6318pub fn call_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
6319    // `undefined.foo()` is a `[[Get]]` and THEN a call (13.3.6 EvaluateCall), so
6320    // the failure is the property read, not the call: node reports
6321    // `Cannot read properties of undefined (reading 'foo')`. node-js ran the
6322    // whole method dispatch against the nullish receiver, found nothing, and
6323    // reported `undefined.foo is not a function` — the wrong error class of
6324    // message for the single most common runtime fault in JS, and one that
6325    // points at the callee instead of at the base that was nullish.
6326    if with_host(|h| h.is_nullish(recv)) {
6327        return Err(type_error(&format!(
6328            "Cannot read properties of {} (reading '{name}')",
6329            with_host(|h| h.str_of(recv))
6330        )));
6331    }
6332    // `this.#m(…)` is a `[[PrivateGet]]` followed by a call, so the brand check
6333    // comes first: an unbranded receiver throws here rather than reporting the
6334    // method missing. Only a `#`-prefixed name pays the extra probe.
6335    if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
6336        return Err(crate::builtins::private_brand_message(name, false));
6337    }
6338    // `proxy.m(…)` is 13.3.6 `EvaluateCall`: `Get(proxy, "m")` — through the
6339    // `get` trap — then a call with the PROXY as `this`. The `lookup_*` shortcuts
6340    // below all read a property map a proxy does not have.
6341    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
6342        let f = crate::builtins::get_property(recv, name)?;
6343        if !with_host(|h| is_callable(h, &f)) {
6344            return Err(type_error(&format!("{name} is not a function")));
6345        }
6346        // `Function.prototype.call`/`apply`/`bind`/`toString` and the REFLECTIVE
6347        // `Object.prototype` methods are generic over `this`. node-js models each
6348        // as a thunk BOUND to the object it was read off — through a proxy, that
6349        // is the target — so invoking the thunk answers for the target and skips
6350        // the traps entirely: `pf.call(1, 2)` never reached the `apply` trap and
6351        // `p.hasOwnProperty(k)` never reached the descriptor trap. Re-dispatch
6352        // those against the PROXY, which is the `this` the real method receives.
6353        //
6354        // `toString`/`valueOf`/`toLocaleString` are deliberately NOT re-dispatched
6355        // for a non-callable proxy: they resolve by the TARGET's kind (a proxy of
6356        // an array stringifies `1,2` through `Array.prototype.toString`, not
6357        // `[object Object]`), which the bound thunk already gets right.
6358        if with_host(|h| matches!(h.get(&f), Some(JsObj::BoundMethod { .. }))) {
6359            if with_host(|h| is_callable(h, recv)) {
6360                if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6361                    return Ok(r);
6362                }
6363            }
6364            if matches!(
6365                name,
6366                "hasOwnProperty" | "propertyIsEnumerable" | "isPrototypeOf"
6367            ) {
6368                return crate::builtins::object_builtin_method(recv, name, args);
6369            }
6370            // The three above resolve by the TARGET's kind, and the thunk is
6371            // already bound to the target — so it must be invoked WITHOUT a
6372            // receiver override. Passing the proxy as `this` made the
6373            // `BoundMethod` arm of `invoke` prefer it over its own receiver and
6374            // call straight back into this branch, so `String(new Proxy({}, {}))`
6375            // recursed until the stack overflowed and the process aborted.
6376            if matches!(name, "toString" | "valueOf" | "toLocaleString") {
6377                return invoke(&f, args, None);
6378            }
6379        }
6380        return invoke(&f, args, Some(recv.clone()));
6381    }
6382    // Namespace builtins (`console`, `Math`, `JSON`, ...): dispatch by qualified
6383    // name.
6384    if let Some(ns) = with_host(|h| match h.get(recv) {
6385        Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6386        _ => None,
6387    }) {
6388        let qualified = format!("{ns}.{name}");
6389        if crate::builtins::is_known_builtin(&qualified) {
6390            return crate::builtins::call_builtin_function(&qualified, args);
6391        }
6392    }
6393    // Object / instance: an accessor getter that yields a function, an own or
6394    // inherited method (class methods live on the prototype chain), then an
6395    // Object.prototype builtin (hasOwnProperty …). Resolve via `lookup_*`
6396    // directly — NOT get_property — so the Object.prototype-builtin fallback
6397    // never routes back through a BoundMethod and recurses.
6398    if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
6399        // A native stdlib instance (`Buffer`/crypto `Hash`/`EventEmitter`/`URL`/
6400        // fs `Stats`/http `ServerResponse`…) carries a hidden `@@native` tag.
6401        // A user-added or reparented-prototype method takes precedence over the
6402        // native dispatcher — matching JS resolution order (own → prototype
6403        // chain). This is what lets Express work: it does
6404        // `Object.setPrototypeOf(res, app.response)` and calls `res.send(...)`,
6405        // where `send` is a plain function on the reparented prototype. Native
6406        // instance methods (`res.end`/`write`/…) are NOT stored as plain
6407        // function properties, so `lookup_chain` misses them and we fall through
6408        // to `instance_call` for the real native behavior.
6409        if let Some(tag) = crate::stdlib::native_tag(recv) {
6410            if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6411                if with_host(|h| is_callable(h, &f)) {
6412                    return invoke(&f, args, Some(recv.clone()));
6413                }
6414            }
6415            // `Object.prototype` methods reach a native instance too — a Buffer
6416            // inherits `hasOwnProperty`/`isPrototypeOf` through its prototype
6417            // chain, and the native dispatcher has no entry for them.
6418            if crate::builtins::is_object_builtin_method(name)
6419                && !crate::stdlib::instance_has_method(&tag, name)
6420            {
6421                return crate::builtins::object_builtin_method(recv, name, args);
6422            }
6423            return crate::stdlib::instance_call(&tag, recv, name, args);
6424        }
6425        // A primitive wrapper forwards to the primitive's method table, the
6426        // same way a native instance forwards to its tag's. A user method on
6427        // the wrapper or anywhere on its chain still wins first.
6428        if let Some(prim) = crate::builtins::wrapped_primitive(recv) {
6429            if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6430                if with_host(|h| is_callable(h, &f)) {
6431                    return invoke(&f, args, Some(recv.clone()));
6432                }
6433            }
6434            // The reflective `Object.prototype` methods answer for the WRAPPER
6435            // — `w.hasOwnProperty("0")` asks about the wrapper's own index
6436            // properties, not about the string.
6437            if crate::builtins::is_object_builtin_method(name) {
6438                return crate::builtins::object_builtin_method(recv, name, args);
6439            }
6440            return call_method(&prim, name, args);
6441        }
6442        if let Some((Some(getter), _)) = with_host(|h| lookup_accessor(h, recv, name)) {
6443            let f = invoke(&getter, Vec::new(), Some(recv.clone()))?;
6444            if with_host(|h| is_callable(h, &f)) {
6445                return invoke(&f, args, Some(recv.clone()));
6446            }
6447        }
6448        // A Proxy in the prototype chain serves the method through its `get`
6449        // trap. `lookup_chain` below reads property maps, which a proxy has none
6450        // of, so without this `child.m()` on `Object.create(proxy)` reported
6451        // "m is not a function" even though `child.m` already read correctly.
6452        if crate::builtins::proxy_proto_link(recv, name).is_some() {
6453            let f = crate::builtins::get_property(recv, name)?;
6454            if !with_host(|h| is_callable(h, &f)) {
6455                return Err(type_error(&format!("{name} is not a function")));
6456            }
6457            return invoke(&f, args, Some(recv.clone()));
6458        }
6459        if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6460            if with_host(|h| is_callable(h, &f)) {
6461                return invoke(&f, args, Some(recv.clone()));
6462            }
6463            return Err(type_error(&format!("{name} is not a function")));
6464        }
6465        // A method patched onto `Object.prototype`. `lookup_chain` cannot find
6466        // it: a plain object is not LINKED to the intrinsic prototype object,
6467        // its `Object.prototype` members are synthesized instead. So
6468        // `Object.prototype.tap = f; ({}).tap()` reported "is not a function"
6469        // while `({}).tap` already read back as `f`.
6470        if let Some(f) = crate::builtins::inherited_builtin_static(recv, name) {
6471            if with_host(|h| is_callable(h, &f)) {
6472                return invoke(&f, args, Some(recv.clone()));
6473            }
6474        }
6475        // A method from an intrinsic prototype this object's CHAIN passes
6476        // through — `Object.create(Array.prototype).push(1)`. The read already
6477        // resolves it through the same owner oracle; dispatch reported "is not
6478        // a function", the read and the call disagreeing once more.
6479        if let Some(owner) = crate::builtins::inherited_method_owner_pub(recv, name) {
6480            if owner != "Object" {
6481                return crate::builtins::proto_method(recv, &format!("{owner}:{name}"), args);
6482            }
6483        }
6484        if crate::builtins::is_object_builtin_method(name) {
6485            return crate::builtins::object_builtin_method(recv, name, args);
6486        }
6487        if name == "constructor" {
6488            if let Some(r) = call_default_ctor(recv, &args) {
6489                return r;
6490            }
6491        }
6492        return Err(type_error(&format!("{name} is not a function")));
6493    }
6494    // Function value methods: call / apply / bind, then any static method stored
6495    // on the function object.
6496    if matches!(
6497        with_host(|h| h.kind_of(recv)),
6498        Some(ObjKind::Func)
6499            | Some(ObjKind::Class)
6500            | Some(ObjKind::BoundFunc)
6501            | Some(ObjKind::BoundMethod)
6502            | Some(ObjKind::Builtin)
6503    ) {
6504        if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6505            return Ok(r);
6506        }
6507        // A static method (own or inherited): `this` is the constructor (`recv`).
6508        let stat = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6509            with_host(|h| h.class_static(recv, name))
6510        } else {
6511            with_host(|h| h.fn_prop(recv, name))
6512        };
6513        if let Some(f) = stat {
6514            if with_host(|h| is_callable(h, &f)) {
6515                return invoke(&f, args, Some(recv.clone()));
6516            }
6517        }
6518        // `class_static` only walks user-class `extends` links, so a chain that
6519        // bottoms out in a BUILTIN constructor (`class D extends Array {}`)
6520        // could not reach that builtin's statics: `D.from([1,2])` threw
6521        // "from is not a function" even though `typeof D.from` said `function`.
6522        // Re-dispatch the call against that ancestor, which is what reaches a
6523        // builtin namespace's methods.
6524        if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6525            if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
6526                if with_host(|h| h.kind_of(&anc)) == Some(ObjKind::Builtin) {
6527                    // The subclass is recorded so a species-aware static
6528                    // (`Array.from`, `Promise.resolve`, …) builds its result
6529                    // through it rather than through the builtin.
6530                    return with_static_this(recv, || call_method(&anc, name, args));
6531                }
6532            }
6533        }
6534        // A method inherited via the function's [[Prototype]] chain (set with
6535        // `Object.setPrototypeOf(fn, proto)`) — the `router` package's router
6536        // functions inherit `route`/`use`/`get`/… from `Router.prototype`.
6537        if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6538            if with_host(|h| is_callable(h, &f)) {
6539                return invoke(&f, args, Some(recv.clone()));
6540            }
6541        }
6542        // An `Object.prototype` method invoked with a builtin namespace/prototype
6543        // as `this` (`hasOwnProperty.call(Map.prototype, 'get')`, the get-intrinsic
6544        // ownership probe) — dispatch it against the builtin receiver.
6545        if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin)
6546            && crate::builtins::is_object_builtin_method(name)
6547        {
6548            return crate::builtins::object_builtin_method(recv, name, args);
6549        }
6550    }
6551    if name == "constructor" {
6552        if let Some(r) = call_default_ctor(recv, &args) {
6553            return r;
6554        }
6555    }
6556    // Type methods (array/string/number, Map/Set/Symbol/generator methods).
6557    crate::builtins::call_type_method(recv, name, args)
6558}
6559
6560/// `x.constructor(...)` invoked as a CALL when nothing on `x`'s prototype chain
6561/// owns a `constructor` slot.
6562///
6563/// Reading the property already resolves a builtin instance's native constructor
6564/// (the `constructor` arm of `builtins::get_property`), but the CALL path only
6565/// consulted the prototype chain, so the two disagreed:
6566/// `(function(){}).constructor === Function` read `true` while
6567/// `(function(){}).constructor('return 9')` threw
6568/// `TypeError: constructor is not a function`. That call form is exactly how
6569/// `get-intrinsic` — a transitive dependency of express — reaches the `Function`
6570/// constructor. Resolved here through the same one definition the read uses, so
6571/// the two can no longer drift apart. `None` means "not resolvable/callable",
6572/// leaving the caller's original error in place.
6573fn call_default_ctor(recv: &Value, args: &[Value]) -> Option<Result<Value, String>> {
6574    let ctor = crate::builtins::get_property(recv, "constructor").ok()?;
6575    with_host(|h| is_callable(h, &ctor)).then(|| invoke(&ctor, args.to_vec(), None))
6576}
6577
6578/// Call any callable value.
6579pub fn invoke(callable: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6580    // `[[Call]]` on a Proxy runs the `apply` trap (or forwards to the target).
6581    // Probed by kind first so the ordinary call path never clones its arguments.
6582    if with_host(|h| h.kind_of(callable)) == Some(ObjKind::Proxy) {
6583        return crate::proxy::apply(callable, args, this).map(|r| r.expect("kind_of said Proxy"));
6584    }
6585    let obj = with_host(|h| h.get(callable).cloned());
6586    match obj {
6587        // A builtin-prototype method thunk (`Object.prototype.toString`): dispatch
6588        // against the invoke-time `this` (supplied by `.call`/`.apply`).
6589        Some(JsObj::Builtin(name)) if name.starts_with("@proto:") => {
6590            let recv = this.unwrap_or(Value::Undef);
6591            crate::builtins::proto_method(&recv, &name["@proto:".len()..], args)
6592        }
6593        // An intrinsic prototype's GETTER, borrowed off its descriptor — the
6594        // form a library uses to read a slot from an arbitrary receiver
6595        // (`Object.getOwnPropertyDescriptor(Map.prototype, 'size').get
6596        // .call(m)`). It brand-checks `this` and reads, or throws naming
6597        // itself.
6598        // The setter half of the `arguments`/`caller` poison pill — the only
6599        // intrinsic accessor here that has one, and it throws like its getter.
6600        Some(JsObj::Builtin(name)) if name.starts_with("@protoset:") => {
6601            let _ = &name;
6602            let recv = this.unwrap_or(Value::Undef);
6603            // The setter half accepts silently for the same receivers the
6604            // getter answers for, and throws for the rest.
6605            if with_host(|h| h.fn_is_sloppy(&recv)) {
6606                Ok(Value::Undef)
6607            } else {
6608                Err(type_error(crate::builtins::POISON_PILL))
6609            }
6610        }
6611        Some(JsObj::Builtin(name)) if name.starts_with("@protoget:") => {
6612            let recv = this.unwrap_or(Value::Undef);
6613            let rest = &name["@protoget:".len()..];
6614            let (ctor, key) = rest.split_once(':').unwrap_or((rest, ""));
6615            crate::builtins::proto_getter_call(ctor, key, &recv)
6616        }
6617        // `NativeCtor.call(obj, …)` — ES5 "constructor stealing", still shipped by
6618        // libraries that predate `class`. `iconv-lite`'s internal codec is exactly
6619        // this:
6620        //
6621        //     function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
6622        //     InternalDecoder.prototype = StringDecoder.prototype;
6623        //
6624        // A native constructor builds a fresh tagged object, so initializing the
6625        // SUPPLIED object means building one and moving its slots across.
6626        //
6627        // The guard is deliberately narrow: `obj` must already inherit from THIS
6628        // constructor's prototype, i.e. the subclass really did adopt it. Without
6629        // that, `Date.call(x)` and `Buffer.call(x)` — which in JS ignore `this` and
6630        // return a string / a buffer — would start mutating `x` instead.
6631        Some(JsObj::Builtin(ref name)) if steals_ctor(name, this.as_ref()) => {
6632            let target = this.expect("guard checked");
6633            let built = crate::stdlib::construct(name, &args)
6634                .expect("guard checked a native constructor")?;
6635            adopt_native_slots(&target, &built);
6636            Ok(Value::Undef)
6637        }
6638        Some(JsObj::Builtin(name)) => crate::builtins::call_builtin_function(&name, args),
6639        Some(JsObj::Func(fv)) => run_user_func_of(&fv, args, this, Some(callable.clone())),
6640        // A method read off an object is modelled as a thunk BOUND to it, but an
6641        // explicit `.call`/`.apply` receiver still wins — `Function.prototype.call`
6642        // rebinds `this`, and every `Array.prototype` method is generic over it, so
6643        // `[].slice.call(arrayLike)` must run against the ARGUMENT. Dropping the
6644        // override made that read back as the empty array the thunk was read off.
6645        // A nullish override is ignored: it carries no receiver to dispatch on.
6646        Some(JsObj::BoundMethod { recv, name }) => {
6647            let target = match &this {
6648                Some(t) if !matches!(t, Value::Undef) && !with_host(|h| h.is_null(t)) => t,
6649                _ => &recv,
6650            };
6651            // A thunk read off an ARRAY carries an `Array.prototype` method, and
6652            // those are generic over `this` — route the rebound call through
6653            // `proto_method` so an array-LIKE receiver takes the generic path
6654            // instead of being told the method does not exist.
6655            if with_host(|h| h.kind_of(&recv)) == Some(ObjKind::Array) {
6656                return crate::builtins::proto_method(target, &format!("Array:{name}"), args);
6657            }
6658            call_method(target, &name, args)
6659        }
6660        Some(JsObj::BoundFunc {
6661            target,
6662            this: bthis,
6663            args: pre,
6664        }) => {
6665            let mut all = pre;
6666            all.extend(args);
6667            invoke(&target, all, Some(bthis))
6668        }
6669        Some(JsObj::Class(c)) => Err(type_error(&format!(
6670            "Class constructor {} cannot be invoked without 'new'",
6671            c.name
6672        ))),
6673        _ => Err(type_error(&format!(
6674            "{} is not a function",
6675            with_host(|h| h.str_of(callable))
6676        ))),
6677    }
6678}
6679
6680/// Whether calling the native constructor `name` with `this` is the ES5
6681/// constructor-stealing pattern rather than an ordinary call.
6682///
6683/// True only when `name` really is a native stdlib constructor AND `this` is a
6684/// plain object that already inherits from that constructor's prototype — the
6685/// signature of `Sub.prototype = Native.prototype; Native.call(this, …)`. An
6686/// object that merely happens to be passed as `this` does not qualify, so
6687/// `Date.call(x)` / `Buffer.call(x)` keep their JS meaning (ignore `this`).
6688fn steals_ctor(name: &str, this: Option<&Value>) -> bool {
6689    let Some(target) = this else { return false };
6690    if !with_host(|h| matches!(h.get(target), Some(JsObj::Object(_)))) {
6691        return false;
6692    }
6693    // Already initialized (e.g. a re-entrant call) — nothing to steal.
6694    if crate::stdlib::native_tag(target).is_some() {
6695        return false;
6696    }
6697    let Some(proto) = with_host(|h| h.ensure_ctor_proto(name)) else {
6698        return false;
6699    };
6700    let mut cur = with_host(|h| h.proto_of(target));
6701    while let Some(p) = cur {
6702        if p == proto {
6703            return true;
6704        }
6705        cur = with_host(|h| h.proto_of(&p));
6706    }
6707    false
6708}
6709
6710/// Move a freshly-constructed native instance's state onto `target`, so an
6711/// object built by a subclass constructor becomes a working instance of the
6712/// native class. Copies every own key the native constructor set — the hidden
6713/// `@@`-prefixed slots that carry the state AND the plain ones it exposes
6714/// (`StringDecoder`'s `encoding`) — without disturbing keys `target` already has.
6715fn adopt_native_slots(target: &Value, built: &Value) {
6716    let slots: Vec<(String, Value)> = with_host(|h| match h.get(built) {
6717        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
6718        _ => Vec::new(),
6719    });
6720    with_host(|h| {
6721        if let Some(JsObj::Object(p)) = h.get_mut(target) {
6722            for (k, v) in slots {
6723                p.insert(k, v);
6724            }
6725        }
6726    });
6727}
6728
6729/// Execute a user function/closure body on a fresh frame.
6730pub fn run_user_func(fv: &FuncVal, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6731    run_user_func_of(fv, args, this, None)
6732}
6733
6734/// [`run_user_func`] with the function VALUE the call came through, which the
6735/// `arguments` object needs for its `callee`.
6736pub fn run_user_func_of(
6737    fv: &FuncVal,
6738    args: Vec<Value>,
6739    this: Option<Value>,
6740    callee: Option<Value>,
6741) -> Result<Value, String> {
6742    run_user_func_full(fv, args, this, None, callee)
6743}
6744
6745/// As `run_user_func`, but with an explicit `new.target` (set by `new`).
6746pub fn run_user_func_nt(
6747    fv: &FuncVal,
6748    args: Vec<Value>,
6749    this: Option<Value>,
6750    new_target: Option<Value>,
6751) -> Result<Value, String> {
6752    run_user_func_full(fv, args, this, new_target, None)
6753}
6754
6755fn run_user_func_full(
6756    fv: &FuncVal,
6757    args: Vec<Value>,
6758    this: Option<Value>,
6759    new_target: Option<Value>,
6760    callee: Option<Value>,
6761) -> Result<Value, String> {
6762    // Consumed first, before anything here can start another call.
6763    let derived_ctor = with_host(|h| std::mem::take(&mut h.derived_ctor_next));
6764    // Only the light fields: cloning the whole `FuncDef` cloned its `Chunk` —
6765    // the entire compiled body, `sub_chunks` and all — on every single call.
6766    // The chunk is now reached once per pooled VM, in the two arms below.
6767    let (params, is_generator, is_async, is_arrow_def, def_name) = with_host(|h| {
6768        let d = &h.funcs[fv.def_id];
6769        (
6770            d.params.clone(),
6771            d.is_generator,
6772            d.is_async,
6773            d.is_arrow,
6774            d.name.clone(),
6775        )
6776    });
6777    let env = new_env(fv.env.clone());
6778    // Bind the simple/rest arg slots; destructuring + defaults run in the body
6779    // prologue (compiled ahead of the user statements).
6780    let fn_is_sloppy = with_host(|h| !h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
6781    bind_params(
6782        &env,
6783        &params,
6784        args,
6785        is_arrow_def,
6786        callee.as_ref(),
6787        fn_is_sloppy && !is_arrow_def,
6788    );
6789    // Arrow functions capture `this` lexically; regular functions receive it.
6790    let mut this_val = if fv.is_arrow { fv.this.clone() } else { this };
6791    // 10.2.1.2 OrdinaryCallBindThis: in SLOPPY mode an absent or nullish `this`
6792    // becomes the global object. Only a strict function keeps `undefined`, and
6793    // an arrow has no `this` of its own to substitute. Leaving it undefined
6794    // meant a plain `f()`, a detached method, a callback and `f.call(null)` all
6795    // saw `undefined` where node sees `globalThis`.
6796    let sloppy_this = !fv.is_arrow
6797        && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict))
6798        && match &this_val {
6799            None => true,
6800            Some(v) => matches!(v, Value::Undef) || with_host(|h| h.is_null(v)),
6801        };
6802    if sloppy_this {
6803        this_val = Some(with_host(|h| h.global_object()));
6804    } else if !fv.is_arrow && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)) {
6805        // The other half of OrdinaryCallBindThis: a SLOPPY function boxes a
6806        // primitive `this` with `ToObject`, so `f.call(5)` sees a `Number`
6807        // wrapper rather than the number. Only strict mode passes it through.
6808        if let Some(t) = this_val.clone() {
6809            let boxed = crate::builtins::to_object(&t);
6810            this_val = Some(boxed);
6811        }
6812    }
6813    // A generator function does not run its body on call — it returns a suspended
6814    // generator over the already-bound frame.
6815    if is_generator {
6816        let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
6817        let gen = make_generator(
6818            chunk,
6819            env,
6820            this_val,
6821            fv.home_class.clone(),
6822            fv.home_static,
6823            fv.home_object.clone(),
6824            with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
6825        );
6826        if is_async {
6827            if let Some(JsObj::Generator { id }) = with_host(|h| h.get(&gen).cloned()) {
6828                with_host(|h| h.generators[id as usize].async_gen = true);
6829            }
6830        }
6831        return Ok(gen);
6832    }
6833    // An async function runs on a coroutine and returns a Promise: it executes
6834    // synchronously up to the first `await`, then continues via microtasks.
6835    if is_async {
6836        let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
6837        let gen = make_generator(
6838            chunk,
6839            env,
6840            this_val,
6841            fv.home_class.clone(),
6842            fv.home_static,
6843            fv.home_object.clone(),
6844            with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
6845        );
6846        return Ok(run_async(gen));
6847    }
6848    let home = fv
6849        .home_class
6850        .as_ref()
6851        .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
6852    // Resolved BEFORE the borrow below: reading the function table re-enters
6853    // the host, and doing it inside the frame-push closure double-borrows.
6854    let fn_strict = with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
6855    with_host(|h| {
6856        h.frames.push(Frame {
6857            base_env: env.clone(),
6858            env,
6859            this_obj: this_val,
6860            new_target,
6861            home_class: home,
6862            home_static: fv.home_static,
6863            home_object: fv.home_object.clone(),
6864            strict: fn_strict,
6865            line: 0,
6866            owner: Some(def_name),
6867            is_module: false,
6868            this_state: if derived_ctor {
6869                ThisState::Pending
6870            } else {
6871                ThisState::Plain
6872            },
6873        })
6874    });
6875    let r = run_chunk_keyed(func_key(fv.def_id), || {
6876        with_host(|h| h.funcs[fv.def_id].chunk.clone())
6877    });
6878    let (sig, this_state) = with_host(|h| {
6879        let frame = h.frames.pop();
6880        (h.signal.take(), frame.map(|f| f.this_state))
6881    });
6882    let ret = match r {
6883        Err(e) => return Err(e),
6884        Ok(_) => match sig {
6885            Some(Signal::Return(v)) => v,
6886            _ => Value::Undef,
6887        },
6888    };
6889    // 10.2.2 [[Construct]] steps 10-12 for a derived constructor: an object
6890    // return wins; any other non-undefined return is a TypeError; and falling
6891    // off the end (or `return;`) needs `this` to have been bound by `super()`.
6892    if derived_ctor && !returns_object(&ret) {
6893        if !matches!(ret, Value::Undef) {
6894            return Err(type_error(
6895                "Derived constructors may only return object or undefined",
6896            ));
6897        }
6898        if this_state == Some(ThisState::Pending) {
6899            return Err(this_before_super_error());
6900        }
6901    }
6902    Ok(ret)
6903}
6904
6905/// Bind positional args into a fresh call environment. The compiler emits the
6906/// param names in `def.params`; a `...rest` slot collects the tail as an array.
6907fn bind_params(
6908    env: &Env,
6909    params: &[ParamSlot],
6910    args: Vec<Value>,
6911    is_arrow: bool,
6912    callee: Option<&Value>,
6913    sloppy: bool,
6914) {
6915    let mut vars = VarMap::default();
6916    let mut i = 0;
6917    for slot in params {
6918        if slot.rest {
6919            let rest: Vec<Value> = args.get(i..).map(|s| s.to_vec()).unwrap_or_default();
6920            let arr = with_host(|h| h.new_array(rest));
6921            vars.insert(slot.name.clone(), arr);
6922        } else {
6923            let v = args.get(i).cloned().unwrap_or(Value::Undef);
6924            vars.insert(slot.name.clone(), v);
6925            i += 1;
6926        }
6927    }
6928    // `arguments` array (simple approximation — see BUGS.md: it is a real
6929    // Array, not an Arguments exotic). An ARROW function never gets one:
6930    // `FunctionDeclarationInstantiation` (10.2.11) creates the binding only for
6931    // a non-arrow, so `arguments` inside an arrow resolves lexically to the
6932    // enclosing function's. Binding an empty one here made
6933    // `function f(){ const g = () => [...arguments]; }` see zero args.
6934    if !is_arrow {
6935        let args_arr = with_host(|h| {
6936            let a = h.new_array(args);
6937            // Marked so it can be told apart from an ordinary array: node's
6938            // `arguments` is an exotic, and without the mark
6939            // `Array.isArray(arguments)` was true, the brand was
6940            // `[object Array]` and `util.types.isArgumentsObject` was false.
6941            // The backing representation stays an Array, which is what keeps
6942            // indices, `length`, spread and `for-of` working.
6943            h.set_fn_prop(&a, "@@arguments", Value::Bool(true));
6944            // `callee` is the function itself in SLOPPY code (it is a poison
6945            // pill only in strict, which the read path handles). It read back
6946            // `undefined`, so the pre-`class` self-reference idiom
6947            // `(function(){ arguments.callee })` found nothing.
6948            if let Some(f) = callee {
6949                if sloppy {
6950                    h.set_fn_prop(&a, "@@callee", f.clone());
6951                }
6952            }
6953            a
6954        });
6955        vars.entry("arguments".to_string()).or_insert(args_arr);
6956    }
6957    env.borrow_mut().vars = vars;
6958}
6959
6960/// Construct an instance with `new` — creates a fresh object, binds it as
6961/// `this`, runs the constructor, and returns the object (unless the constructor
6962/// returns its own object).
6963pub fn construct(ctor: &Value, args: Vec<Value>) -> Result<Value, String> {
6964    construct_nt(ctor, args, ctor.clone())
6965}
6966
6967/// `new` with an explicit `new.target` (differs from `ctor` when a derived class
6968/// calls `super(...)` — the target stays the originally-`new`ed class).
6969pub fn construct_nt(ctor: &Value, args: Vec<Value>, new_target: Value) -> Result<Value, String> {
6970    // `new proxy(…)` runs the `construct` trap (or forwards to the target).
6971    if with_host(|h| h.kind_of(ctor)) == Some(ObjKind::Proxy) {
6972        return crate::proxy::construct(ctor, args, &new_target)
6973            .map(|r| r.expect("kind_of said Proxy"));
6974    }
6975    let obj = with_host(|h| h.get(ctor).cloned());
6976    match obj {
6977        Some(JsObj::Class(_)) => construct_class(ctor, args, new_target),
6978        Some(JsObj::Func(fv)) => {
6979            // Only an ORDINARY function has a `[[Construct]]` slot. An arrow, a
6980            // `function*` and an `async function` are callable but not
6981            // constructable (10.2.2 is installed only for the ordinary case), so
6982            // `new` on one is a TypeError — node-js instead ran the body and
6983            // handed back a half-built instance (for a generator, an object whose
6984            // constructor had returned a suspended generator).
6985            let non_ctor = with_host(|h| {
6986                h.funcs
6987                    .get(fv.def_id)
6988                    // A MethodDefinition is in the same boat: `new ({m(){}}).m()`
6989                    // is `TypeError: o.m is not a constructor` on node v26.7.0,
6990                    // which is also why a method owns no `prototype`.
6991                    .map(|d| d.is_generator || d.is_async || d.is_method)
6992                    .unwrap_or(false)
6993            });
6994            if fv.is_arrow || non_ctor {
6995                return Err(not_a_constructor(ctor));
6996            }
6997            // A plain constructor function: instance delegates to `fn.prototype`
6998            // (auto-created with a `.constructor` back-link if not yet accessed).
6999            let inst = with_host(|h| {
7000                let o = h.new_object(IndexMap::new());
7001                let proto = h.fn_prop(ctor, "prototype").unwrap_or_else(|| {
7002                    let p = h.new_object(IndexMap::new());
7003                    if let Some(JsObj::Object(pp)) = h.get_mut(&p) {
7004                        pp.insert("constructor".to_string(), ctor.clone());
7005                    }
7006                    // `F.prototype.constructor` is non-enumerable in JS.
7007                    h.hide_prop(&p, "constructor");
7008                    h.set_fn_prop(ctor, "prototype", p.clone());
7009                    p
7010                });
7011                h.set_proto(&o, proto);
7012                o
7013            });
7014            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target))?;
7015            if returns_object(&r) {
7016                Ok(r)
7017            } else {
7018                Ok(inst)
7019            }
7020        }
7021        Some(JsObj::Builtin(name)) => crate::builtins::construct_builtin(&name, args),
7022        Some(JsObj::BoundFunc {
7023            target, args: pre, ..
7024        }) => {
7025            let mut all = pre;
7026            all.extend(args);
7027            construct_nt(&target, all, new_target)
7028        }
7029        _ => Err(not_a_constructor(ctor)),
7030    }
7031}
7032
7033/// `TypeError: <callee> is not a constructor`.
7034///
7035/// V8 names the callee by its SOURCE TEXT (`new g()` reports `g`, `new o.m()`
7036/// reports `o.m`); node-js keeps no spans, so a named callable is reported by
7037/// its name — the same string in the common case — and anything else by its
7038/// value.
7039fn not_a_constructor(ctor: &Value) -> String {
7040    let name = with_host(|h| match h.callable_name(ctor) {
7041        n if n.is_empty() => h.str_of(ctor),
7042        n => n,
7043    });
7044    type_error(&format!("{name} is not a constructor"))
7045}
7046
7047/// Whether a constructor's return value is an object (so `new` yields it instead
7048/// of the fresh instance). In JS "object" includes functions — the `router`
7049/// package's constructor `return router` (a function) must be honored, or the
7050/// returned router loses its callable identity.
7051fn returns_object(r: &Value) -> bool {
7052    matches!(
7053        with_host(|h| h.get(r).cloned()),
7054        Some(JsObj::Object(_))
7055            | Some(JsObj::Array(_))
7056            | Some(JsObj::Map { .. })
7057            | Some(JsObj::Set { .. })
7058            | Some(JsObj::Func(_))
7059            | Some(JsObj::Class(_))
7060            | Some(JsObj::BoundFunc { .. })
7061            | Some(JsObj::BoundMethod { .. })
7062            | Some(JsObj::RegExp(_))
7063    )
7064}
7065
7066/// Construct a `class` instance: allocate the object linked to `C.prototype`,
7067/// run field initializers + the constructor (which may call `super(...)`).
7068fn construct_class(
7069    class_val: &Value,
7070    args: Vec<Value>,
7071    new_target: Value,
7072) -> Result<Value, String> {
7073    let cv = match with_host(|h| h.get(class_val).cloned()) {
7074        Some(JsObj::Class(c)) => c,
7075        _ => return Err(type_error("not a class")),
7076    };
7077    // Resolve the prototype of the *most-derived* class being `new`ed, so an
7078    // instance created through a `super()` chain still delegates to the leaf
7079    // prototype (correct method resolution).
7080    let leaf_proto = match with_host(|h| h.get(&new_target).cloned()) {
7081        Some(JsObj::Class(c)) => c.proto.clone(),
7082        _ => cv.proto.clone(),
7083    };
7084    let inst = with_host(|h| {
7085        let o = h.new_object(IndexMap::new());
7086        h.set_proto(&o, leaf_proto.clone());
7087        o
7088    });
7089    // A `super()` deeper in may substitute the instance; the previous value is
7090    // restored so a `new` inside a constructor body cannot be mistaken for one.
7091    let saved = with_host(|h| h.swap_super_replacement(None));
7092    let ran = run_class_ctor(&cv, &inst, args, &new_target);
7093    let substituted = with_host(|h| {
7094        let s = h.take_super_replacement();
7095        h.swap_super_replacement(saved);
7096        s
7097    });
7098    // A constructor that returns an object replaces the instance (`new`
7099    // semantics); failing that, whatever `super()` substituted for it.
7100    match ran? {
7101        Some(obj) if returns_object(&obj) => Ok(obj),
7102        _ => Ok(substituted.unwrap_or(inst)),
7103    }
7104}
7105
7106/// Run one class's field initializers then its constructor on an existing
7107/// instance. Returns the constructor's explicit object return (if any). For a
7108/// base class this is the whole init; for a derived class the constructor body
7109/// reaches `super(...)` which recurses into the parent.
7110fn run_class_ctor(
7111    cv: &ClassVal,
7112    inst: &Value,
7113    args: Vec<Value>,
7114    new_target: &Value,
7115) -> Result<Option<Value>, String> {
7116    // A derived class must run its fields AFTER super() returns; SUPER_CALL does
7117    // that. A base class initializes fields before the constructor body.
7118    if cv.parent.is_none() {
7119        init_fields(cv, inst)?;
7120    }
7121    match &cv.ctor {
7122        Some(ctor_fn) => {
7123            let fv = match with_host(|h| h.get(ctor_fn).cloned()) {
7124                Some(JsObj::Func(f)) => f,
7125                _ => return Err(type_error("class constructor is not a function")),
7126            };
7127            if cv.parent.is_some() {
7128                with_host(|h| h.mark_next_call_derived_ctor());
7129            }
7130            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7131            return Ok(Some(r));
7132        }
7133        None => {
7134            // Default constructor: `constructor(...a){ super(...a); }` for a
7135            // derived class, empty for a base class.
7136            if let Some(parent) = &cv.parent {
7137                // A base constructor's returned object becomes the instance, so
7138                // the implicit `constructor(...a){ super(...a) }` hands it on.
7139                if let Some(replacement) = super_construct(parent, args, inst, new_target)? {
7140                    init_fields(cv, &replacement)?;
7141                    return Ok(Some(replacement));
7142                }
7143                init_fields(cv, inst)?;
7144            }
7145        }
7146    }
7147    Ok(None)
7148}
7149
7150/// Evaluate and assign a class's instance-field initializers on `inst`.
7151fn init_fields(cv: &ClassVal, inst: &Value) -> Result<(), String> {
7152    for (name, thunk, name_anon) in &cv.fields {
7153        init_one_field(inst, name, thunk, *name_anon)?;
7154    }
7155    Ok(())
7156}
7157
7158/// Evaluate ONE instance-field initializer thunk and install the result on
7159/// `inst`.
7160///
7161/// Shared by the base-class path (`init_fields`) and the derived-class path
7162/// that runs after `super(...)`; the two used to be separate loops, and only the
7163/// first canonicalized an array-index key.
7164///
7165/// `name_anon` carries 15.7.10's NamedEvaluation: `class C { f = function(){} }`
7166/// gives the function the name `f`. It is decided by the compiler from the
7167/// syntax, never from the value.
7168pub fn init_one_field(
7169    inst: &Value,
7170    name: &str,
7171    thunk: &Value,
7172    name_anon: bool,
7173) -> Result<(), String> {
7174    // The thunk is an arrow capturing the class scope; run it with `this`=inst
7175    // so `this.other`-referencing initializers work.
7176    let val = invoke(thunk, Vec::new(), Some(inst.clone()))?;
7177    with_host(|h| {
7178        if name_anon {
7179            let s = h.new_str(name.to_string());
7180            h.set_fn_prop(&val, "name", s);
7181        }
7182        if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7183            let is_new = !props.contains_key(name);
7184            props.insert(name.to_string(), val);
7185            if is_new && array_index(name).is_some() {
7186                canonicalize_own_keys(props);
7187            }
7188        }
7189    });
7190    Ok(())
7191}
7192
7193/// Run a parent constructor as part of `super(...)`: dispatch on the parent's
7194/// kind (class vs plain function vs builtin) using the existing instance.
7195/// Run the parent constructor against `inst`.
7196///
7197/// Returns the object the parent's `[[Construct]]` produced when that is NOT
7198/// `inst` — a base constructor is allowed to `return` one, and 15.7.15 makes it
7199/// the derived instance too. The caller rebinds `this` to it, so the rest of the
7200/// derived constructor writes to the object `new` will hand back.
7201pub fn super_construct(
7202    parent: &Value,
7203    args: Vec<Value>,
7204    inst: &Value,
7205    new_target: &Value,
7206) -> Result<Option<Value>, String> {
7207    match with_host(|h| h.get(parent).cloned()) {
7208        Some(JsObj::Class(pcv)) => Ok(run_class_ctor(&pcv, inst, args, new_target)?
7209            .filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst)))),
7210        Some(JsObj::Func(fv)) => {
7211            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7212            Ok(Some(r).filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst))))
7213        }
7214        Some(JsObj::Builtin(name)) => {
7215            let built = crate::builtins::construct_builtin(&name, args)?;
7216            // An EXOTIC parent (`class A extends Array`) keeps its behaviour in
7217            // the heap variant, not in a property map, so copying own props
7218            // cannot carry it: the instance has to BECOME the built object.
7219            // Without this `new (class extends Array {})().push` was not a
7220            // function, and the same for Map, Set, RegExp, Promise and
7221            // Function — subclassing a builtin produced a plain object.
7222            if !become_exotic(inst, &built) {
7223                // An `Error` subclass is ordinary: its state IS own properties.
7224                adopt_own_props(inst, &built);
7225            }
7226            Ok(None)
7227        }
7228        // A Proxy parent (`class D extends new Proxy(B, {})`): `super(…)` is
7229        // `[[Construct]]` on the proxy, so the `construct` trap runs (or forwards
7230        // to the target). node-js initializes an ALREADY-allocated `inst` rather
7231        // than adopting the constructor's return value, so what the proxy built
7232        // is moved across — the same move the builtin arm makes.
7233        Some(JsObj::Proxy { .. }) => {
7234            let built = construct_nt(parent, args, new_target.clone())?;
7235            if !become_exotic(inst, &built) {
7236                adopt_own_props(inst, &built);
7237            }
7238            Ok(None)
7239        }
7240        _ => Err(type_error("super is not a constructor")),
7241    }
7242}
7243
7244/// Move `built`'s own properties (and their attributes) onto `inst`. Used where
7245/// a parent constructor produces a fresh object but node-js's class model has
7246/// already allocated the instance `this` is bound to.
7247/// Replace `inst`'s heap object with `built`'s, so an instance whose class
7248/// extends a builtin EXOTIC really is one.
7249///
7250/// `inst` keeps its identity and its prototype link — the leaf class's
7251/// prototype, which is what method resolution and `instanceof` walk — while its
7252/// contents become the exotic the parent constructor produced. The side tables
7253/// keyed by heap index (array holes, property attributes, the fn-prop table)
7254/// move across with it.
7255///
7256/// Returns false for a variant whose state is ordinary own properties
7257/// (`Error`), which the caller copies instead.
7258fn become_exotic(inst: &Value, built: &Value) -> bool {
7259    let exotic = matches!(
7260        with_host(|h| h.get(built).cloned()),
7261        Some(JsObj::Array(_))
7262            | Some(JsObj::Map { .. })
7263            | Some(JsObj::Set { .. })
7264            | Some(JsObj::RegExp(_))
7265            | Some(JsObj::Promise { .. })
7266            | Some(JsObj::Func(_))
7267            | Some(JsObj::Str(_))
7268            | Some(JsObj::BigInt(_))
7269            | Some(JsObj::Symbol { .. })
7270    );
7271    if !exotic {
7272        return false;
7273    }
7274    let (Value::Obj(dst), Value::Obj(src)) = (inst, built) else {
7275        return false;
7276    };
7277    let (dst, src) = (*dst, *src);
7278    with_host(|h| {
7279        if let Some(obj) = h.get(built).cloned() {
7280            if let Some(slot) = h.get_mut(inst) {
7281                *slot = obj;
7282            }
7283        }
7284        h.move_index_state(src, dst);
7285    });
7286    true
7287}
7288
7289fn adopt_own_props(inst: &Value, built: &Value) {
7290    let entries: Vec<(String, Value)> = with_host(|h| match h.get(built) {
7291        Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
7292        _ => Vec::new(),
7293    });
7294    with_host(|h| {
7295        let keys: Vec<String> = entries.iter().map(|(k, _)| k.clone()).collect();
7296        if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7297            for (k, v) in entries {
7298                props.insert(k, v);
7299            }
7300            canonicalize_own_keys(props);
7301        }
7302        // The copied slots keep the attributes the source gave them, so
7303        // `class E extends Error` instances hide `message`/`stack` too.
7304        for k in keys {
7305            let a = h.prop_attrs(built, &k);
7306            h.set_prop_attrs(inst, &k, a);
7307        }
7308    });
7309}
7310
7311// ── class construction (runtime) ─────────────────────────────────────────────
7312
7313/// Build a class constructor value from its parts. The compiler emits (via
7314/// `MKCLASS`) the evaluated parent (or undefined) and the constructor closure (or
7315/// undefined for a default constructor); methods/getters/setters/statics/fields
7316/// are installed afterward by `DEF_MEMBER`/`DEF_FIELD`.
7317pub fn build_class(name: &str, parent: Value, ctor: Value) -> Value {
7318    // A Proxy parent (`class D extends new Proxy(B, {})`): `D.prototype`'s
7319    // `[[Prototype]]` is `Get(parent, "prototype")` — a read that runs the `get`
7320    // trap and so re-enters the host, which the borrow below cannot allow.
7321    // Without it the link fell back to `Object.prototype` and every inherited
7322    // method went missing.
7323    let proxy_parent_proto = (with_host(|h| h.kind_of(&parent)) == Some(ObjKind::Proxy))
7324        .then(|| crate::builtins::get_property(&parent, "prototype").ok())
7325        .flatten();
7326    with_host(|h| {
7327        let parent_opt = if matches!(parent, Value::Undef) {
7328            None
7329        } else {
7330            Some(parent.clone())
7331        };
7332        // The class prototype delegates to the parent's prototype (or
7333        // Object.prototype for a base class). Extending a builtin error links to
7334        // that error's prototype so `instanceof Error` holds for the subclass.
7335        let parent_proto = match &parent_opt {
7336            Some(_) if proxy_parent_proto.is_some() => {
7337                proxy_parent_proto.clone().expect("checked is_some")
7338            }
7339            Some(p) => match h.get(p).cloned() {
7340                Some(JsObj::Class(pc)) => pc.proto.clone(),
7341                Some(JsObj::Builtin(bn)) => {
7342                    h.ensure_error_protos();
7343                    h.ensure_native_protos();
7344                    // `class S extends String {}` links to the REAL
7345                    // `String.prototype`, the same way an error subclass links
7346                    // to its error prototype. Without it `S.prototype`'s
7347                    // `[[Prototype]]` fell back to `Object.prototype`, so
7348                    // `new S("hi") instanceof String` read false and
7349                    // `String(new S("hi"))` reported `[object String]` instead
7350                    // of `hi`.
7351                    error_proto_of(h, &bn)
7352                        .or_else(|| h.native_proto(&bn))
7353                        .or_else(|| h.fn_prop(p, "prototype"))
7354                        .unwrap_or_else(|| h.object_proto())
7355                }
7356                _ => h
7357                    .fn_prop(p, "prototype")
7358                    .unwrap_or_else(|| h.object_proto()),
7359            },
7360            None => h.object_proto(),
7361        };
7362        let proto = h.new_object(IndexMap::new());
7363        h.set_proto(&proto, parent_proto);
7364        let ctor_opt = if matches!(ctor, Value::Undef) {
7365            None
7366        } else {
7367            Some(ctor.clone())
7368        };
7369        // Give the constructor closure its home class (for `super.method()`), and
7370        // record its `.name`.
7371        if let Some(cf) = &ctor_opt {
7372            if let Some(JsObj::Func(f)) = h.get_mut(cf) {
7373                f.home_class = Some(name.to_string());
7374            }
7375        }
7376        let cval = ClassVal {
7377            name: name.to_string(),
7378            ctor: ctor_opt,
7379            parent: parent_opt,
7380            proto: proto.clone(),
7381            statics: IndexMap::new(),
7382            fields: Vec::new(),
7383        };
7384        let class_val = h.alloc(JsObj::Class(cval));
7385        h.class_registry.insert(name.to_string(), class_val.clone());
7386        // Link prototype → class (for instance display + `constructor`), and give
7387        // the class its own `prototype` fn-prop so `C.prototype` reads work.
7388        h.tag_proto_class(&proto, class_val.clone());
7389        h.set_fn_prop(&class_val, "prototype", proto.clone());
7390        // `Class.prototype.constructor === Class`.
7391        if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
7392            p.insert("constructor".to_string(), class_val.clone());
7393        }
7394        h.hide_prop(&proto, "constructor");
7395        class_val
7396    })
7397}
7398
7399/// Install a method / getter / setter on a class (`DEF_MEMBER`). `kind` is a
7400/// `member::*` tag; `is_static` targets the constructor side.
7401pub fn define_member(class_val: &Value, name: &str, kind: i64, is_static: bool, func: Value) {
7402    with_host(|h| {
7403        let cname = match h.get(class_val) {
7404            Some(JsObj::Class(c)) => c.name.clone(),
7405            _ => String::new(),
7406        };
7407        // A private method/accessor: remember which class declared it, so a
7408        // brand-check failure can name the class the way node does. A static
7409        // FIELD is data, not a method, so it keeps the field wording.
7410        if name.starts_with('#') && kind != member::STATIC_FIELD {
7411            h.note_private_method(name);
7412        }
7413        // Give the method its home class for `super.x()`, and record whether it
7414        // is static — `super` resolves against a different object either way.
7415        if let Some(JsObj::Func(f)) = h.get_mut(&func) {
7416            f.home_class = Some(cname);
7417            f.home_static = is_static;
7418        }
7419        // Static members live on the constructor (fn-props / static accessors);
7420        // instance members on the prototype.
7421        let target = if is_static {
7422            class_val.clone()
7423        } else {
7424            match h.get(class_val) {
7425                Some(JsObj::Class(c)) => c.proto.clone(),
7426                _ => return,
7427            }
7428        };
7429        match kind {
7430            member::GET => h.set_accessor(&target, name, Some(func), None),
7431            member::SET => h.set_accessor(&target, name, None, Some(func)),
7432            _ => {
7433                // A static field is enumerable (`Object.keys(C)` lists it) unlike
7434                // a method, so it must not reach the `hide_prop` below.
7435                if kind == member::STATIC_FIELD {
7436                    if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7437                        c.statics.insert(name.to_string(), func.clone());
7438                    }
7439                    h.set_fn_prop(class_val, name, func);
7440                    return;
7441                }
7442                if is_static {
7443                    if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7444                        c.statics.insert(name.to_string(), func.clone());
7445                    }
7446                    h.set_fn_prop(class_val, name, func);
7447                } else if let Some(JsObj::Object(p)) = h.get_mut(&target) {
7448                    p.insert(name.to_string(), func);
7449                }
7450            }
7451        }
7452        // Class methods and accessors are non-enumerable (ES2015 ClassDefinition-
7453        // Evaluation), so `for (k in instance)` walking the prototype chain never
7454        // yields them and `Object.keys(C.prototype)` is empty.
7455        h.hide_prop(&target, name);
7456    });
7457}
7458
7459/// Register an instance-field initializer thunk on a class (`DEF_FIELD`).
7460pub fn define_field(class_val: &Value, name: &str, thunk: Value, name_anon: bool) {
7461    with_host(|h| {
7462        if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7463            c.fields.push((name.to_string(), thunk, name_anon));
7464        }
7465    });
7466}
7467
7468/// The `[[Prototype]]` object a constructor value hands to its instances
7469/// (`Ctor.prototype`), for `instanceof`.
7470fn ctor_prototype(h: &JsHost, ctor: &Value) -> Option<Value> {
7471    match h.get(ctor) {
7472        Some(JsObj::Class(c)) => Some(c.proto.clone()),
7473        Some(JsObj::Func(_)) => h.fn_prop(ctor, "prototype"),
7474        // A builtin's prototype lives in one of two registries: the error
7475        // prototypes, or the native exotic prototypes (`Buffer.prototype`,
7476        // `Uint8Array.prototype`). Consulting only the first made `instanceof`
7477        // blind to the real `Buffer.prototype → Uint8Array.prototype` chain, so
7478        // `Buffer.prototype instanceof Uint8Array` read false even though the
7479        // link was there — the instance case only passed via a native-tag
7480        // special case, which a prototype object does not carry.
7481        Some(JsObj::Builtin(name)) => h
7482            .error_protos
7483            .get(name)
7484            .or_else(|| h.native_protos.get(name))
7485            .cloned(),
7486        Some(JsObj::BoundFunc { target, .. }) => ctor_prototype(h, &target.clone()),
7487        _ => None,
7488    }
7489}
7490
7491/// `ctor.prototype` in the SAME representation `builtins::prototype_of` yields,
7492/// so a chain walk driven by that function can compare the two with `strict_eq`.
7493///
7494/// `ctor_prototype` answers only for the constructors whose prototype object
7495/// really exists on the heap (classes, user functions, the error and native
7496/// exotics). A bare builtin like `Object`/`Array` has none there — its instances
7497/// report `h.object_proto()` / a `Builtin("<C>.prototype")` handle — so this
7498/// mirrors that fallback rather than reporting "no prototype" and failing every
7499/// comparison.
7500fn walk_target_prototype(ctor: &Value) -> Option<Value> {
7501    if let Some(p) = with_host(|h| ctor_prototype(h, ctor)) {
7502        return Some(p);
7503    }
7504    let name = with_host(|h| match h.get(ctor) {
7505        Some(JsObj::Builtin(n)) => Some(n.clone()),
7506        _ => None,
7507    })?;
7508    if name == "Object" {
7509        return Some(with_host(|h| h.object_proto()));
7510    }
7511    Some(with_host(|h| {
7512        h.alloc(JsObj::Builtin(format!("{name}.prototype")))
7513    }))
7514}
7515
7516/// V8's "not a function" wording for a value that was expected to be callable.
7517/// A number/string/boolean is named WITH its value (`number 1 is not a
7518/// function`, `string "s" is not a function`); every other type is named by type
7519/// alone (`object is not a function`, `symbol is not a function`).
7520pub fn not_a_function_message(v: &Value) -> String {
7521    with_host(|h| match v {
7522        Value::Undef => "undefined is not a function".into(),
7523        Value::Bool(b) => format!("boolean {b} is not a function"),
7524        Value::Int(_) | Value::Float(_) => format!("number {} is not a function", h.str_of(v)),
7525        Value::Str(s) => format!("string \"{s}\" is not a function"),
7526        Value::Obj(_) => match h.get(v) {
7527            Some(JsObj::Str(s)) => format!("string \"{s}\" is not a function"),
7528            Some(JsObj::Symbol { .. }) => "symbol is not a function".into(),
7529            Some(JsObj::BigInt(_)) => "bigint is not a function".into(),
7530            _ => "object is not a function".into(),
7531        },
7532        _ => "object is not a function".into(),
7533    })
7534}
7535
7536/// `obj instanceof ctor` — walk `obj`'s prototype chain looking for
7537/// `ctor.prototype`.
7538pub fn instance_of(obj: &Value, ctor: &Value) -> Result<bool, String> {
7539    // 13.10.2 InstanceofOperator step 3: a `Symbol.hasInstance` method on the
7540    // right-hand side REPLACES the prototype-chain walk entirely, and it is
7541    // consulted before the callability check — which is why a plain (uncallable)
7542    // object that defines it is a legal `instanceof` right-hand side.
7543    if matches!(ctor, Value::Obj(_)) {
7544        // `class C { static [Symbol.hasInstance](){} }` and a method defined on a
7545        // plain function both land in the fn-prop side table (which
7546        // `class_static` reads, following the `extends` chain), NOT in an object
7547        // property map — so consulting only `lookup_chain` would find the object
7548        // literal form and silently miss the two forms V8 users actually write.
7549        let handler = match with_host(|h| h.class_static(ctor, "@@hasInstance")) {
7550            Some(f) => Some(f),
7551            None => protocol_lookup(ctor, "@@hasInstance")?,
7552        };
7553        // GetMethod (7.3.11) treats only `undefined`/`null` as "absent"; anything
7554        // else that is not callable is a TypeError, so a data property here does
7555        // NOT fall back to the prototype walk.
7556        match handler {
7557            Some(f) if with_host(|h| is_callable(h, &f)) => {
7558                let r = invoke(&f, vec![obj.clone()], Some(ctor.clone()))?;
7559                return Ok(with_host(|h| h.truthy(&r)));
7560            }
7561            Some(f)
7562                if !matches!(f, Value::Undef)
7563                    && !with_host(|h| matches!(h.get(&f), Some(JsObj::Null))) =>
7564            {
7565                return Err(type_error(&not_a_function_message(&f)));
7566            }
7567            _ => {}
7568        }
7569    }
7570    // 13.10.2 InstanceofOperator validates the RIGHT-hand side FIRST, so
7571    // `1 instanceof 3` throws even though the left side could never match.
7572    // Returning early on the left side skipped that check entirely.
7573    let ctor_callable = with_host(|h| {
7574        matches!(
7575            h.get(ctor),
7576            Some(JsObj::Func(_))
7577                | Some(JsObj::Class(_))
7578                | Some(JsObj::Builtin(_))
7579                | Some(JsObj::BoundFunc { .. })
7580        )
7581    });
7582    if !ctor_callable {
7583        // V8 has TWO messages here and they are not interchangeable: a primitive
7584        // right-hand side is "not an object", an object that is merely not
7585        // callable is "not callable". Only the second was implemented, so
7586        // `1 instanceof 3` reported nothing at all.
7587        return Err(type_error(if with_host(|h| !is_primitive(h, ctor)) {
7588            "Right-hand side of 'instanceof' is not callable"
7589        } else {
7590            "Right-hand side of 'instanceof' is not an object"
7591        }));
7592    }
7593    // A non-object left-hand side is never an instance — but only after the
7594    // right-hand side has been validated above.
7595    if !matches!(obj, Value::Obj(_)) {
7596        return Ok(false);
7597    }
7598    // A Proxy shares no heap variant with its target, so the structural arms
7599    // below would misclassify it. 10.5.3 says `OrdinaryHasInstance` walks
7600    // `[[GetPrototypeOf]]`, i.e. the handler's `getPrototypeOf` trap — run that
7601    // walk here, which also gives a custom trap the final say.
7602    if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Proxy) {
7603        with_host(|h| {
7604            h.ensure_error_protos();
7605            h.ensure_native_protos();
7606        });
7607        let Some(target) = walk_target_prototype(ctor) else {
7608            return Ok(false);
7609        };
7610        let mut cur = crate::proxy::get_prototype_of(obj)?.unwrap_or(Value::Undef);
7611        for _ in 0..100 {
7612            if matches!(cur, Value::Undef) || with_host(|h| h.is_null(&cur)) {
7613                return Ok(false);
7614            }
7615            if with_host(|h| h.strict_eq(&cur, &target)) {
7616                return Ok(true);
7617            }
7618            cur = crate::builtins::prototype_of(&cur);
7619        }
7620        return Ok(false);
7621    }
7622    // Builtin constructors whose instances aren't prototype-linked in our model
7623    // (arrays/plain objects/functions) get a structural instanceof.
7624    if let Some(JsObj::Builtin(name)) = with_host(|h| h.get(ctor).cloned()) {
7625        // …but an object whose chain PASSES THROUGH the intrinsic prototype is
7626        // an instance regardless of its own kind, which is the whole of the ES5
7627        // subclassing pattern: `F.prototype = Object.create(Array.prototype)`
7628        // makes `new F() instanceof Array` true. A structural test alone said
7629        // false.
7630        if crate::builtins::chain_intrinsic_ctors_pub(obj).contains(&name.as_str()) {
7631            return Ok(true);
7632        }
7633        let kind = with_host(|h| h.get(obj).cloned());
7634        match name.as_str() {
7635            "Array" => return Ok(matches!(kind, Some(JsObj::Array(_)))),
7636            "Function" => return Ok(with_host(|h| is_callable(h, obj))),
7637            // Map/Set/Promise instances are distinct heap variants, not
7638            // prototype-linked, so match them structurally (a WeakMap/WeakSet is a
7639            // Map/Set with `weak: true`, so `weakMap instanceof Map` is false).
7640            "Map" => return Ok(matches!(kind, Some(JsObj::Map { weak: false, .. }))),
7641            "WeakMap" => return Ok(matches!(kind, Some(JsObj::Map { weak: true, .. }))),
7642            "Set" => return Ok(matches!(kind, Some(JsObj::Set { weak: false, .. }))),
7643            "WeakSet" => return Ok(matches!(kind, Some(JsObj::Set { weak: true, .. }))),
7644            "Promise" => return Ok(matches!(kind, Some(JsObj::Promise { .. }))),
7645            // A RegExp is its own heap variant too, not a prototype-linked object.
7646            "RegExp" => return Ok(matches!(kind, Some(JsObj::RegExp(_)))),
7647            "Object" => {
7648                // Everything object-typed except a null-prototype object is an
7649                // Object instance.
7650                let is_obj = matches!(
7651                    kind,
7652                    Some(JsObj::Object(_))
7653                        | Some(JsObj::Array(_))
7654                        // A namespace object and a builtin function are both
7655                        // `instanceof Object`: `Math instanceof Object` is true.
7656                        | Some(JsObj::Builtin(_))
7657                        | Some(JsObj::Func(_))
7658                        | Some(JsObj::Class(_))
7659                        | Some(JsObj::Map { .. })
7660                        | Some(JsObj::Set { .. })
7661                        | Some(JsObj::Promise { .. })
7662                        | Some(JsObj::Generator { .. })
7663                        | Some(JsObj::RegExp(_))
7664                );
7665                if is_obj {
7666                    // A null-prototype object (Object.create(null) or
7667                    // setPrototypeOf(o, null)) is NOT an Object instance.
7668                    if with_host(|h| h.has_null_proto(obj)) {
7669                        return Ok(false);
7670                    }
7671                    return Ok(true);
7672                }
7673                return Ok(false);
7674            }
7675            // A Node `Buffer` IS a `Uint8Array` subclass instance.
7676            "Uint8Array" if crate::stdlib::native_tag(obj).as_deref() == Some("Buffer") => {
7677                return Ok(true);
7678            }
7679            // Every typed array carries the same `TypedArray` tag; the constructor
7680            // it is an instance of is its ELEMENT KIND.
7681            k if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") => {
7682                return Ok(crate::stdlib::typedarray::kind_of(obj) == k);
7683            }
7684            // A native-tagged instance (`WeakRef`, `FinalizationRegistry`,
7685            // `TextEncoder`, …) is an instance of the builtin whose name matches
7686            // its hidden `@@native` tag.
7687            other => {
7688                if crate::stdlib::native_tag(obj).as_deref() == Some(other) {
7689                    return Ok(true);
7690                }
7691            }
7692        }
7693    }
7694    with_host(|h| h.ensure_error_protos());
7695    // The native exotic prototypes are built lazily; `instanceof` may be the
7696    // first thing to ask for them, so materialise them before the chain walk.
7697    with_host(|h| h.ensure_native_protos());
7698    let target = match with_host(|h| ctor_prototype(h, ctor)) {
7699        Some(p) => p,
7700        None => return Ok(false),
7701    };
7702    let mut cur = with_host(|h| h.proto_of(obj));
7703    while let Some(p) = cur {
7704        if with_host(|h| h.strict_eq(&p, &target)) {
7705            return Ok(true);
7706        }
7707        cur = with_host(|h| h.proto_of(&p));
7708    }
7709    Ok(false)
7710}
7711
7712// ── generators (stackful coroutines, same-thread via corosensei) ─────────────
7713
7714impl JsHost {
7715    /// Swap the volatile execution context in one shot, returning the previous
7716    /// one — installs a generator's context on resume, pulls it back on suspend.
7717    fn install_gen_ctx(&mut self, mut c: GenContext) -> GenContext {
7718        std::mem::swap(&mut self.frames, &mut c.frames);
7719        std::mem::swap(&mut self.error, &mut c.error);
7720        std::mem::swap(&mut self.exc, &mut c.exc);
7721        std::mem::swap(&mut self.signal, &mut c.signal);
7722        c
7723    }
7724    pub fn is_generator_val(&self, v: &Value) -> bool {
7725        matches!(self.get(v), Some(JsObj::Generator { .. }))
7726    }
7727    /// Whether `v` is an ASYNC generator object — the borrow-free form of
7728    /// [`is_async_generator`], usable from code already holding the host.
7729    pub fn is_async_gen_val(&self, v: &Value) -> bool {
7730        match self.get(v) {
7731            Some(JsObj::Generator { id }) => self
7732                .generators
7733                .get(*id as usize)
7734                .map(|g| g.async_gen)
7735                .unwrap_or(false),
7736            _ => false,
7737        }
7738    }
7739    pub fn gen_done(&self, id: u32) -> bool {
7740        self.generators
7741            .get(id as usize)
7742            .map(|g| g.done)
7743            .unwrap_or(true)
7744    }
7745    fn gen_started(&self, id: u32) -> bool {
7746        self.generators
7747            .get(id as usize)
7748            .map(|g| g.started)
7749            .unwrap_or(false)
7750    }
7751}
7752
7753/// Build a suspended generator whose body is `chunk`, run in a frame with the
7754/// already-bound `env`. Nothing executes until the first `gen_resume`.
7755fn make_generator(
7756    chunk: Chunk,
7757    env: Env,
7758    this_val: Option<Value>,
7759    home_class: Option<String>,
7760    home_static: bool,
7761    home_object: Option<Value>,
7762    strict: bool,
7763) -> Value {
7764    let home = home_class
7765        .as_ref()
7766        .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
7767    let frame = Frame {
7768        base_env: env.clone(),
7769        env,
7770        this_obj: this_val,
7771        new_target: None,
7772        home_class: home,
7773        home_static,
7774        home_object,
7775        strict,
7776        line: 0,
7777        owner: None,
7778        is_module: false,
7779        this_state: ThisState::Plain,
7780    };
7781    let id = with_host(|h| {
7782        let id = h.generators.len() as u32;
7783        h.generators.push(GenCell {
7784            coro: None,
7785            yielder: std::ptr::null(),
7786            ctx: GenContext {
7787                frames: vec![frame],
7788                ..GenContext::default()
7789            },
7790            done: false,
7791            started: false,
7792            inject: None,
7793            async_gen: false,
7794            queue: std::collections::VecDeque::new(),
7795            running: false,
7796            stack_floor: 0,
7797        });
7798        id
7799    });
7800    let body = move |yielder: &corosensei::Yielder<Value, Value>, _first: Value| {
7801        ensure_coroutine_floor();
7802        // Same thread → publish the yielder so `yield` (deep in the body's VM)
7803        // can reach it. Valid for the whole body lifetime.
7804        with_host(|h| h.generators[id as usize].yielder = yielder as *const _ as *const ());
7805        let r = run_chunk_on(chunk);
7806        // A `return` inside the body leaves a Return signal carrying the final
7807        // value; capture it so `.next()` reports it as the completion value.
7808        let ret = with_host(|h| match h.signal.take() {
7809            Some(Signal::Return(v)) => v,
7810            _ => Value::Undef,
7811        });
7812        r.map(|_| ret)
7813    };
7814    // The body's stack is allocated here rather than left to `Coroutine::new` so
7815    // that its size is ours to choose and, above all, so its `limit()` is known:
7816    // that address is what `stack_exhausted` must compare against while the body
7817    // runs, since a coroutine does NOT run on the thread stack pthread reports.
7818    // A refused reservation still yields a working generator on corosensei's own
7819    // 1 MiB default, with a floor derived on entry instead.
7820    let (coro, floor) = match corosensei::stack::DefaultStack::new(CORO_STACK_SIZE) {
7821        Ok(stack) => {
7822            let floor = coro_stack_floor(&stack);
7823            (corosensei::Coroutine::with_stack(stack, body), floor)
7824        }
7825        Err(_) => (corosensei::Coroutine::new(body), 0),
7826    };
7827    with_host(|h| {
7828        h.generators[id as usize].coro = Some(coro);
7829        h.generators[id as usize].stack_floor = floor;
7830    });
7831    with_host(|h| h.alloc(JsObj::Generator { id }))
7832}
7833
7834/// `yield v` — suspend the running generator, handing `v` to the resumer; returns
7835/// the value the next `gen_resume(x)` supplies (a `.next(x)` argument).
7836pub fn gen_yield(v: Value) -> Result<Value, String> {
7837    let id = match CUR_GEN.with(|c| c.get()) {
7838        Some(id) => id,
7839        None => return Err(type_error("yield outside a generator")),
7840    };
7841    let yp = with_host(|h| h.generators[id as usize].yielder);
7842    // SAFETY: same-thread coroutine; the yielder lives for the whole body, and we
7843    // only reach here from inside that body (its stack is live).
7844    let yielder = unsafe { &*(yp as *const corosensei::Yielder<Value, Value>) };
7845    let sent = yielder.suspend(v);
7846    // On resume, a `.return(v)`/`.throw(e)` may have queued a forced completion:
7847    // convert it into a Return signal / thrown value so the body unwinds and any
7848    // `finally` runs, exactly as a source-level `return`/`throw` would.
7849    if let Some(inj) = with_host(|h| h.generators[id as usize].inject.take()) {
7850        match inj {
7851            GenInject::Return(rv) => {
7852                with_host(|h| h.signal = Some(Signal::Return(rv)));
7853                return Ok(Value::Undef);
7854            }
7855            GenInject::Throw(ev) => {
7856                let msg = with_host(|h| crate::builtins::error_string(h, &ev));
7857                with_host(|h| h.exc = Some(ev));
7858                return Err(msg);
7859            }
7860        }
7861    }
7862    Ok(sent)
7863}
7864
7865/// `generator.return(v)`: force the generator to complete, running any pending
7866/// `finally`. If it is already done (or never started) it just reports
7867/// `{value:v, done:true}` without executing the body.
7868pub fn gen_return(gen: &Value, v: Value) -> Result<GenStep, String> {
7869    let id = match with_host(|h| h.get(gen).cloned()) {
7870        Some(JsObj::Generator { id }) => id,
7871        _ => return Err(type_error("not a generator")),
7872    };
7873    // Not started yet (coro present, ctx never resumed) OR already done → no body
7874    // to unwind: complete immediately with the supplied value.
7875    let started = with_host(|h| h.gen_started(id));
7876    if with_host(|h| h.generators[id as usize].done) || !started {
7877        with_host(|h| h.generators[id as usize].done = true);
7878        return Ok(GenStep::Done(v));
7879    }
7880    with_host(|h| h.generators[id as usize].inject = Some(GenInject::Return(v)));
7881    gen_resume(gen, Value::Undef)
7882}
7883
7884/// `generator.throw(e)`: inject a throw at the suspension point, running any
7885/// pending `finally` and letting an enclosing `try/catch` in the body handle it.
7886pub fn gen_throw(gen: &Value, e: Value) -> Result<GenStep, String> {
7887    let id = match with_host(|h| h.get(gen).cloned()) {
7888        Some(JsObj::Generator { id }) => id,
7889        _ => return Err(type_error("not a generator")),
7890    };
7891    let started = with_host(|h| h.gen_started(id));
7892    if with_host(|h| h.generators[id as usize].done) || !started {
7893        // A throw into a done/unstarted generator propagates to the caller.
7894        with_host(|h| h.generators[id as usize].done = true);
7895        let msg = with_host(|h| crate::builtins::error_string(h, &e));
7896        with_host(|h| h.exc = Some(e));
7897        return Err(msg);
7898    }
7899    with_host(|h| h.generators[id as usize].inject = Some(GenInject::Throw(e)));
7900    gen_resume(gen, Value::Undef)
7901}
7902
7903/// Outcome of resuming a generator: a yielded value (not done), or the final
7904/// completion value (done).
7905pub enum GenStep {
7906    Yield(Value),
7907    Done(Value),
7908}
7909
7910/// Resume a generator until its next `yield` or its body returns. Preserves the
7911/// shared host: the coroutine is taken out so the body re-enters `with_host`
7912/// freely, and the volatile context is swapped so the caller's frames/signal
7913/// survive the switch.
7914pub fn gen_resume(gen: &Value, send: Value) -> Result<GenStep, String> {
7915    let id = match with_host(|h| h.get(gen).cloned()) {
7916        Some(JsObj::Generator { id }) => id,
7917        _ => return Err(type_error("not a generator")),
7918    };
7919    if with_host(|h| h.generators[id as usize].done) {
7920        return Ok(GenStep::Done(Value::Undef));
7921    }
7922    let mut coro = match with_host(|h| h.generators[id as usize].coro.take()) {
7923        Some(c) => c,
7924        None => return Err("TypeError: generator already executing".into()),
7925    };
7926    with_host(|h| h.generators[id as usize].started = true);
7927    let gen_ctx = with_host(|h| std::mem::take(&mut h.generators[id as usize].ctx));
7928    let caller_ctx = with_host(|h| h.install_gen_ctx(gen_ctx));
7929    let prev = CUR_GEN.with(|c| c.replace(Some(id)));
7930    // The body runs on the coroutine's OWN stack, so the guard's floor has to
7931    // move with it and move back on suspend — generators nest, and a resume from
7932    // inside another generator must restore that one's floor, not the thread's.
7933    let coro_floor = with_host(|h| h.generators[id as usize].stack_floor);
7934    let caller_floor = swap_stack_floor(coro_floor);
7935
7936    let out = coro.resume(send); // no host borrow held; body drives its own VM
7937
7938    let measured = swap_stack_floor(caller_floor);
7939    // A coroutine on corosensei's default stack has no known bounds, so the
7940    // floor it measured for itself on first entry is kept for later resumes.
7941    if coro_floor == 0 && measured != 0 {
7942        with_host(|h| h.generators[id as usize].stack_floor = measured);
7943    }
7944    CUR_GEN.with(|c| c.set(prev));
7945    let mut gen_ctx = with_host(|h| h.install_gen_ctx(caller_ctx));
7946    // A `throw` inside the body left the thrown VALUE in the generator's context,
7947    // which the swap above just stashed away. Hand it to the caller so the
7948    // rejection/catch keeps the original error object instead of a string rebuild.
7949    let thrown = gen_ctx.exc.take();
7950    with_host(|h| {
7951        if let Some(v) = thrown {
7952            h.exc = Some(v);
7953        }
7954        h.generators[id as usize].ctx = gen_ctx;
7955        h.generators[id as usize].coro = Some(coro);
7956    });
7957
7958    match out {
7959        corosensei::CoroutineResult::Yield(y) => Ok(GenStep::Yield(y)),
7960        corosensei::CoroutineResult::Return(r) => {
7961            // Release the coroutine — and with it the mmap'd stack it owns —
7962            // the moment the body completes. `h.generators` only ever grows (an
7963            // id is never reused), so a program that awaits in a loop otherwise
7964            // accumulates one whole [`CORO_STACK_SIZE`] reservation per call for
7965            // the life of the process. A finished generator is never resumed:
7966            // `gen_resume` returns `Done` on the `done` flag before it looks.
7967            with_host(|h| {
7968                let g = &mut h.generators[id as usize];
7969                g.done = true;
7970                g.coro = None;
7971            });
7972            match r {
7973                Ok(v) => Ok(GenStep::Done(v)),
7974                Err(e) => Err(e),
7975            }
7976        }
7977    }
7978}
7979
7980/// Force a generator to completion (used by `.return()` and abandoned loops):
7981/// marks it done without running further.
7982pub fn gen_close(gen: &Value) {
7983    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(gen).cloned()) {
7984        with_host(|h| h.generators[id as usize].done = true);
7985    }
7986}
7987
7988// ── iteration protocol (arrays, strings, Map/Set, generators, Symbol.iterator) ─
7989
7990/// Convert a Map/Set key value into a `MapKey` under SameValueZero.
7991pub fn map_key(h: &JsHost, v: &Value) -> MapKey {
7992    match v {
7993        Value::Undef => MapKey::Undef,
7994        Value::Bool(b) => MapKey::Bool(*b),
7995        Value::Int(n) => MapKey::Num(norm_num_bits(*n as f64)),
7996        Value::Float(f) => MapKey::Num(norm_num_bits(*f)),
7997        Value::Str(s) => MapKey::Str((**s).clone()),
7998        Value::Obj(i) => match h.get(v) {
7999            Some(JsObj::Str(s)) => MapKey::Str(s.clone()),
8000            Some(JsObj::Null) => MapKey::Null,
8001            Some(JsObj::BigInt(b)) => MapKey::Big(b.to_string()),
8002            _ => MapKey::Ref(*i),
8003        },
8004        _ => MapKey::Undef,
8005    }
8006}
8007
8008/// Canonical bit pattern for a Map/Set numeric key: `NaN` → one value, `-0` → `+0`.
8009fn norm_num_bits(f: f64) -> u64 {
8010    if f.is_nan() {
8011        return f64::NAN.to_bits();
8012    }
8013    if f == 0.0 {
8014        return 0.0f64.to_bits(); // fold -0 into +0
8015    }
8016    f.to_bits()
8017}
8018
8019/// Fully materialize any iterable into a vector of values.
8020/// Pull at most `n` values, then close the iterator — 8.6.2
8021/// IteratorBindingInitialization, which is what an array destructuring pattern
8022/// without a `...rest` element performs.
8023///
8024/// The distinction from [`iter_all`] is not an optimization. A pattern names a
8025/// fixed number of targets, so the spec pulls exactly that many and calls
8026/// IteratorClose on whatever is left; draining instead made
8027///
8028/// ```text
8029/// const [first] = infiniteGenerator();
8030/// ```
8031///
8032/// run forever. It is also observable on any finite iterator, as the count of
8033/// `next()` calls and whether `return()` ever ran.
8034///
8035/// A `...rest` element genuinely consumes the remainder, so those patterns keep
8036/// using `iter_all` and an unbounded source hangs there in node too.
8037pub fn iter_take(v: &Value, n: usize) -> Result<Vec<Value>, String> {
8038    // A Proxy iterates through its traps, which materialize eagerly; there is
8039    // no step-wise form to bound, so this keeps the draining behaviour.
8040    if let Some(items) = crate::proxy::iterate(v)? {
8041        return Ok(items.into_iter().take(n).collect());
8042    }
8043    if with_host(|h| h.is_generator_val(v)) {
8044        let mut out = Vec::new();
8045        while out.len() < n {
8046            match gen_resume(v, Value::Undef)? {
8047                GenStep::Yield(x) => out.push(x),
8048                _ => return Ok(out), // ran out on its own; nothing left to close
8049            }
8050        }
8051        // Stopped early: `.return()` resumes it at the yield so `finally` runs.
8052        let _ = gen_return(v, Value::Undef);
8053        return Ok(out);
8054    }
8055    if let Some(iter_fn) = user_iterator_fn(v) {
8056        let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8057        let mut out = Vec::new();
8058        while out.len() < n {
8059            let step = call_method(&iterator, "next", Vec::new())?;
8060            // Read first: resolving the property re-enters the host, so doing
8061            // it inside the `with_host` closure double-borrows and aborts.
8062            let done = get_prop_chain(&step, "done")?;
8063            if with_host(|h| h.truthy(&done)) {
8064                return Ok(out);
8065            }
8066            out.push(get_prop_chain(&step, "value")?);
8067        }
8068        // IteratorClose: `return` is optional on the protocol, and a throw from
8069        // it is swallowed here the way a normal (non-abrupt) completion does.
8070        if let Ok(ret) = get_prop_chain(&iterator, "return") {
8071            if with_host(|h| is_callable(h, &ret)) {
8072                let _ = invoke(&ret, Vec::new(), Some(iterator.clone()));
8073            }
8074        }
8075        return Ok(out);
8076    }
8077    // Arrays, strings, Map/Set: already materialized, and their built-in
8078    // iterators carry no `return`, so there is nothing to close. The same
8079    // reachability rule as `iter_all` applies — this is the DESTRUCTURING
8080    // entry point, and `const [x] = a` bound 1 from an array whose prototype no
8081    // longer carried `Symbol.iterator`.
8082    if !crate::builtins::own_intrinsic_reachable_pub(v) {
8083        let shown = with_host(|h| h.inspect(v));
8084        return Err(type_error(&format!("{shown} is not iterable")));
8085    }
8086    with_host(|h| h.iter_vec(v)).map(|items| items.into_iter().take(n).collect())
8087}
8088
8089pub fn iter_all(v: &Value) -> Result<Vec<Value>, String> {
8090    // A Proxy iterates through its traps (see `crate::proxy::iterate`); it has
8091    // no heap variant `iter_vec` could recognise.
8092    if let Some(items) = crate::proxy::iterate(v)? {
8093        return Ok(items);
8094    }
8095    // Generators / user iterators must resume without a live host borrow.
8096    if with_host(|h| h.is_generator_val(v)) {
8097        let mut out = Vec::new();
8098        while let GenStep::Yield(x) = gen_resume(v, Value::Undef)? {
8099            out.push(x);
8100        }
8101        return Ok(out);
8102    }
8103    // Object with a user-defined Symbol.iterator: drive its iterator protocol.
8104    // Checked BEFORE the reachability guard below, since an own `Symbol
8105    // .iterator` makes a value iterable no matter what its prototype is.
8106    if let Some(iter_fn) = user_iterator_fn(v) {
8107        let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8108        return drain_iterator(&iterator);
8109    }
8110    // The fast paths below read a builtin's backing storage directly, which is
8111    // only legitimate while that builtin's `Symbol.iterator` is still
8112    // reachable: replacing the prototype takes it away, and node then reports
8113    // the value as not iterable. Spread, destructuring and `Array.from`'s
8114    // iterable branch all funnel through here.
8115    if !crate::builtins::own_intrinsic_reachable_pub(v) {
8116        let shown = with_host(|h| h.inspect(v));
8117        return Err(type_error(&format!("{shown} is not iterable")));
8118    }
8119    // A String wrapper iterates its code POINTS, exactly as the primitive does
8120    // (22.1.3.34) — `[...new String("ab")]` is `["a","b"]`, not a TypeError.
8121    if let Some(prim) = crate::builtins::wrapped_primitive(v) {
8122        if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) {
8123            return iter_all(&prim);
8124        }
8125    }
8126    // An array's index ACCESSORS are not in its backing vector, so iterating one
8127    // (spread, `for-of`, `Array.from`) has to resolve them the way the
8128    // `Array.prototype` methods do.
8129    let mut items = with_host(|h| h.iter_vec(v))?;
8130    if with_host(|h| matches!(h.get(v), Some(JsObj::Array(_)))) {
8131        crate::builtins::resolve_index_accessors_pub(v, &mut items);
8132    }
8133    Ok(items)
8134}
8135
8136// ── async iteration (`for await (… of …)`) ───────────────────────────────────
8137
8138/// Obtain an async iterator for `for await`. If `src` has a `Symbol.asyncIterator`
8139/// method, use it (its `.next()` returns a promise of `{value, done}`); otherwise
8140/// fall back to the sync iterable, materialized into a `JsObj::Iter` whose values
8141/// are awaited one at a time by `async_step`.
8142pub fn get_async_iterator(src: &Value) -> Result<Value, String> {
8143    if let Some(f) = user_async_iterator_fn(src) {
8144        return invoke(&f, Vec::new(), Some(src.clone()));
8145    }
8146    // An `async function*` object IS its own async iterator; draining it into a
8147    // list here would run the whole body (and any `finally`) before the consumer
8148    // sees the first value.
8149    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(src).cloned()) {
8150        if with_host(|h| h.generators[id as usize].async_gen) {
8151            return Ok(src.clone());
8152        }
8153    }
8154    let items = iter_all(src)?;
8155    Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
8156}
8157
8158/// If `v` has an own/inherited `Symbol.asyncIterator` method, return it.
8159fn user_async_iterator_fn(v: &Value) -> Option<Value> {
8160    // A PROXY supplies the protocol through its `get` trap and is not a plain
8161    // object, so the shape test below rejects it outright.
8162    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8163        return protocol_lookup(v, "@@asyncIterator")
8164            .ok()
8165            .flatten()
8166            .filter(|f| with_host(|h| is_callable(h, f)));
8167    }
8168    let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8169    if !is_plain {
8170        return None;
8171    }
8172    // Full property resolution, not a stored-property lookup — the same reason
8173    // `user_iterator_fn` does it for the SYNC protocol. A NATIVE-tagged object
8174    // dispatches its methods through the stdlib method table rather than a
8175    // property map, so `lookup_chain` reported no `Symbol.asyncIterator` for one
8176    // even though reading it gives a function: `for await (const v of
8177    // timersPromises.setInterval(…))` said the iterator "is not iterable".
8178    let f = crate::builtins::get_property(v, "@@asyncIterator").ok()?;
8179    with_host(|h| is_callable(h, &f)).then_some(f)
8180}
8181
8182/// One step of a `for await` loop: return a Promise that settles to a
8183/// `{value, done}` record. For a native async iterator this is `iter.next()`
8184/// (already a promise of the record). For the sync fallback it pops the next raw
8185/// value, awaits it, and packages `{value: resolved, done:false}` (or
8186/// `{done:true}` at exhaustion).
8187pub fn async_step(iterator: &Value) -> Result<Value, String> {
8188    // An `async function*` object: resume it through the await-aware driver.
8189    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(iterator).cloned()) {
8190        if with_host(|h| h.generators[id as usize].async_gen) {
8191            return Ok(async_gen_step(iterator, Value::Undef));
8192        }
8193    }
8194    // Sync-fallback iterator: drive it here, awaiting each yielded value.
8195    if let Some(JsObj::Iter { items, idx }) = with_host(|h| h.get(iterator).cloned()) {
8196        if idx >= items.len() {
8197            // `AsyncFromSyncIteratorContinuation` resolves the record THROUGH a
8198            // promise even at exhaustion, so the `done: true` step costs the same
8199            // two microtask ticks a value step does.
8200            let step = with_host(|h| h.new_promise());
8201            let sid = with_host(|h| h.promise_id(&step).unwrap());
8202            with_host(|h| {
8203                h.queue_micro_native(Box::new(move || {
8204                    resolve_promise_val(sid, iter_record(Value::Undef, true));
8205                    Ok(())
8206                }))
8207            });
8208            return Ok(step);
8209        }
8210        let raw = items[idx].clone();
8211        with_host(|h| {
8212            if let Some(JsObj::Iter { idx, .. }) = h.get_mut(iterator) {
8213                *idx += 1;
8214            }
8215        });
8216        // Await the raw value (adopts a promise's resolution), then wrap.
8217        let step = with_host(|h| h.new_promise());
8218        let sid = with_host(|h| h.promise_id(&step).unwrap());
8219        let raw_p = promise_of(&raw);
8220        let raw_id = with_host(|h| h.promise_id(&raw_p).unwrap());
8221        subscribe_native(
8222            raw_id,
8223            Box::new(move |state, val| {
8224                if state == PromiseState::Rejected {
8225                    reject_promise_val(sid, val);
8226                } else {
8227                    resolve_promise_val(sid, iter_record(val, false));
8228                }
8229                Ok(())
8230            }),
8231        );
8232        return Ok(step);
8233    }
8234    // Native async iterator: `iter.next()` returns the {value,done} promise.
8235    let r = call_method(iterator, "next", Vec::new())?;
8236    Ok(promise_of(&r))
8237}
8238
8239/// If `v` has an own/inherited `Symbol.iterator` method (internal key
8240/// `@@iterator`), return it. Arrays/strings use the native fast path instead.
8241pub fn user_iterator_fn(v: &Value) -> Option<Value> {
8242    let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8243    if !is_plain {
8244        return None;
8245    }
8246    // Full property resolution, not a stored-property lookup: a NATIVE-tagged
8247    // object (`URLSearchParams`, `Headers`) dispatches its methods through the
8248    // stdlib method table rather than a property map, so `lookup_chain` reported
8249    // no `Symbol.iterator` for one even though reading it gave a function —
8250    // `[...new URLSearchParams('a=1')]` threw `{} is not iterable`.
8251    let f = crate::builtins::get_property(v, "@@iterator").ok()?;
8252    with_host(|h| is_callable(h, &f)).then_some(f)
8253}
8254
8255/// Drive an iterator object (one with a `.next()` returning `{value, done}`) to
8256/// exhaustion.
8257/// Step `src`'s iterator, handing each value to `f`, and CLOSE the iterator if
8258/// `f` exits abruptly (7.4.9 IteratorClose).
8259///
8260/// The difference from `iter_all` + a loop is that this never materializes the
8261/// whole sequence: `Array.from(infinite, mapFn)` where `mapFn` throws has to
8262/// stop at the first call, and draining first means it never gets there at all.
8263pub fn iter_for_each(
8264    src: &Value,
8265    mut f: impl FnMut(Value, usize) -> Result<(), String>,
8266) -> Result<(), String> {
8267    // Only a USER iterator can be infinite or observe its own close; every
8268    // other shape is already a finite materialized sequence.
8269    let Some(iter_fn) = user_iterator_fn(src) else {
8270        for (i, v) in iter_all(src)?.into_iter().enumerate() {
8271            f(v, i)?;
8272        }
8273        return Ok(());
8274    };
8275    let iterator = invoke(&iter_fn, Vec::new(), Some(src.clone()))?;
8276    let mut i = 0usize;
8277    loop {
8278        let step = call_method(&iterator, "next", Vec::new())?;
8279        let done = get_prop_chain(&step, "done")?;
8280        if with_host(|h| h.truthy(&done)) {
8281            return Ok(());
8282        }
8283        let value = get_prop_chain(&step, "value")?;
8284        if let Err(e) = f(value, i) {
8285            // The callback's error wins over anything `return()` raises, so a
8286            // throwing `return` is swallowed here (7.4.9 step 6).
8287            let _ = close_iterator(&iterator);
8288            return Err(e);
8289        }
8290        i += 1;
8291    }
8292}
8293
8294/// Call `iterator.return()` if it has one, as IteratorClose does.
8295pub fn close_iterator(iterator: &Value) -> Result<(), String> {
8296    let has = crate::builtins::get_property(iterator, "return")?;
8297    if with_host(|h| is_callable(h, &has)) {
8298        call_method(iterator, "return", Vec::new())?;
8299    }
8300    Ok(())
8301}
8302
8303pub(crate) fn drain_iterator(iterator: &Value) -> Result<Vec<Value>, String> {
8304    let mut out = Vec::new();
8305    loop {
8306        let step = call_method(iterator, "next", Vec::new())?;
8307        let done = get_prop_chain(&step, "done")?;
8308        if with_host(|h| h.truthy(&done)) {
8309            break;
8310        }
8311        out.push(get_prop_chain(&step, "value")?);
8312    }
8313    Ok(out)
8314}
8315
8316/// Property read that walks the prototype chain (used by iteration helpers).
8317/// Whether the builtin named `n` is CALLABLE. Most are (`Array`, `parseInt`,
8318/// `Math.floor`); the exceptions are the namespace objects a script can only
8319/// read properties off (`Math`, `JSON`, every `require()`d core module), which
8320/// report `typeof === "object"` and carry no `name`/`length`.
8321pub fn builtin_is_callable(n: &str) -> bool {
8322    // A `<Ctor>.prototype` handle is a namespace of methods, not a function:
8323    // `typeof Set.prototype` is `"object"`, and treating it as callable made it
8324    // brand `[object Function]`, inspect as `[Function: prototype]`, and answer
8325    // `true` to `instanceof Function`. `Function.prototype` is the one that
8326    // really IS callable (10.2.4: it is an anonymous built-in that returns
8327    // undefined), which is why it is not stripped here.
8328    if n != "Function.prototype" && n.ends_with(".prototype") {
8329        return false;
8330    }
8331    // A `match` rather than a slice scan: this runs on every callability test,
8332    // which is every call and every `ToPrimitive`, and a `contains` over the
8333    // list below compares against all 56 entries before answering "callable" —
8334    // the common case. The compiler turns the arms into a length-then-bytes
8335    // decision tree instead.
8336    !matches!(
8337        n,
8338        "Math"
8339            | "JSON"
8340            | "console"
8341            | "Reflect"
8342            | "process"
8343            | "Atomics"
8344            | "performance"
8345            | "fs"
8346            | "path"
8347            | "os"
8348            | "util"
8349            | "crypto"
8350            | "webcrypto"
8351            | "SubtleCrypto"
8352            | "querystring"
8353            | "events"
8354            | "timers"
8355            | "perf_hooks"
8356            | "async_hooks"
8357            | "diagnostics_channel"
8358            | "v8"
8359            | "dns"
8360            | "punycode"
8361            | "child_process"
8362            | "tty"
8363            | "url"
8364            | "zlib"
8365            | "string_decoder"
8366            | "http"
8367            | "net"
8368            | "buffer"
8369            | "function"
8370            | "path/win32"
8371            | "fs/promises"
8372            | "stream/promises"
8373            | "stream/consumers"
8374            | "stream/web"
8375            | "timers/promises"
8376            | "dns/promises"
8377            | "https"
8378            | "http2"
8379            | "tls"
8380            | "dgram"
8381            | "cluster"
8382            | "worker_threads"
8383            | "readline"
8384            | "readline/promises"
8385            | "repl"
8386            | "vm"
8387            | "domain"
8388            | "trace_events"
8389            | "wasi"
8390            | "inspector"
8391            | "object"
8392    )
8393        // The live `require.cache` view is a plain object to a script, not
8394        // something it can call.
8395        && n != crate::builtins::REQUIRE_CACHE
8396}
8397
8398pub fn get_prop_chain(recv: &Value, name: &str) -> Result<Value, String> {
8399    crate::builtins::get_property(recv, name)
8400}
8401
8402/// Whether `v` is an ECMAScript primitive, i.e. `ToPrimitive` is the identity
8403/// on it. `undefined`, `null`, booleans, numbers, strings, symbols and bigints
8404/// qualify; every other heap cell (objects, arrays, functions, `Map`/`Set`,
8405/// native-tagged instances) is an object and must be converted.
8406pub fn is_primitive(h: &JsHost, v: &Value) -> bool {
8407    match v {
8408        Value::Obj(_) => matches!(
8409            h.get(v),
8410            None | Some(JsObj::Null)
8411                | Some(JsObj::Str(_))
8412                | Some(JsObj::Symbol { .. })
8413                | Some(JsObj::BigInt(_))
8414        ),
8415        _ => true,
8416    }
8417}
8418
8419/// `ToPrimitive(v, hint)` — ECMA-262 7.1.1. `hint` is `"default"`, `"number"`
8420/// or `"string"`.
8421///
8422/// An object carrying a `Symbol.toPrimitive` method (internal key
8423/// `@@toPrimitive`) has it called with the hint and must return a primitive.
8424/// Otherwise `OrdinaryToPrimitive` (7.1.1.1) tries `valueOf` then `toString` —
8425/// the order reversed for the string hint — and takes the FIRST call whose
8426/// result is a primitive. An object that yields no primitive (a null-prototype
8427/// object has neither method) throws V8's
8428/// `TypeError: Cannot convert object to primitive value`.
8429///
8430/// This is the conversion behind `+`, `-`/`*`/`/`/`%`/`**`, the relational
8431/// operators, `==` against a primitive, and `ToPropertyKey` — all of which used
8432/// to read `str_of` directly and so never invoked a user `valueOf`.
8433pub fn to_primitive(v: &Value, hint: &str) -> Result<Value, String> {
8434    if with_host(|h| is_primitive(h, v)) {
8435        return Ok(v.clone());
8436    }
8437    if let Some(f) = protocol_lookup(v, "@@toPrimitive")? {
8438        if with_host(|h| is_callable(h, &f)) {
8439            let hv = with_host(|h| h.new_str(hint.to_string()));
8440            let r = invoke(&f, vec![hv], Some(v.clone()))?;
8441            if with_host(|h| is_primitive(h, &r)) {
8442                return Ok(r);
8443            }
8444            return Err(type_error("Cannot convert object to primitive value"));
8445        }
8446    }
8447    // `Date.prototype[@@toPrimitive]` (21.4.4.45) treats the DEFAULT hint as
8448    // `"string"`, which is why `new Date() + 1` concatenates while
8449    // `new Date() - 1` is arithmetic.
8450    let hint = if hint == "default" && crate::stdlib::native_tag(v).as_deref() == Some("Date") {
8451        "string"
8452    } else {
8453        hint
8454    };
8455    let order = if hint == "string" {
8456        ["toString", "valueOf"]
8457    } else {
8458        ["valueOf", "toString"]
8459    };
8460    // Whether either candidate was actually CALLED. The `[object Tag]` fallback
8461    // below is for exotics whose property funnel exposes no callable
8462    // `toString`, not for an object whose own methods ran and returned
8463    // non-primitives — that case is the spec's TypeError, and branding it
8464    // instead meant `({ valueOf: () => ({}), toString: () => ({}) }) + 1`
8465    // quietly produced `"[object Object]1"`.
8466    let mut called_any = false;
8467    for m in order {
8468        let f = crate::builtins::get_property(v, m).unwrap_or(Value::Undef);
8469        if !with_host(|h| is_callable(h, &f)) {
8470            continue;
8471        }
8472        called_any = true;
8473        // On a Proxy the resolved method is a thunk bound to the TARGET, so
8474        // invoking it directly would stringify the target — `String(new
8475        // Proxy(function f(){}, {}))` reported `f`'s source where V8 reports the
8476        // native-code form. `call_method` re-dispatches the generic
8477        // `Function.prototype`/`Object.prototype` methods against the proxy.
8478        let r = if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8479            call_method(v, m, Vec::new())?
8480        } else {
8481            invoke(&f, Vec::new(), Some(v.clone()))?
8482        };
8483        if with_host(|h| is_primitive(h, &r)) {
8484            return Ok(r);
8485        }
8486    }
8487    // Every object except a null-prototype one inherits `Object.prototype
8488    // .toString`, which always returns a string — so the exhausted-methods
8489    // TypeError is reachable only there. The exotics whose property funnel has
8490    // no `toString` entry of its own (`Map`, `Set`, `Promise`, …) land here and
8491    // get the same `[object Tag]` brand V8 gives them.
8492    // A proxy WITH a `get` trap is not one of those exotics: the trap answered
8493    // for both method names, and if what came back was not callable there is
8494    // nothing left to call — `String(new Proxy({}, { get: () => undefined }))`
8495    // is a TypeError on node, where branding it `[object Object]` invented a
8496    // conversion the trap explicitly refused. A TRAPLESS proxy is different: its
8497    // read forwarded to the target, so `String(new Proxy(new Map(), {}))` gets
8498    // the target's `[object Map]` brand exactly as the bare `Map` does.
8499    if !called_any && !with_host(|h| h.has_null_proto(v)) && !crate::proxy::has_trap(v, "get") {
8500        return crate::builtins::proto_method(v, "Object:toString", Vec::new());
8501    }
8502    Err(type_error("Cannot convert object to primitive value"))
8503}
8504
8505/// `ToString(v)` with `ToPrimitive` method dispatch: an object is converted
8506/// with the string hint (so a user `toString` — or `valueOf`, if `toString`
8507/// is absent or returns an object — is invoked), then rendered by `str_of`.
8508/// Returns a heap string value.
8509pub fn to_string_value(v: &Value) -> Result<Value, String> {
8510    let p = to_primitive(v, "string")?;
8511    // `ToString(symbol)` throws (7.1.17 step 2) — the ONLY conversion a symbol
8512    // refuses. `String(sym)` is the documented exception and is handled at that
8513    // call site, not here, so every implicit coercion (`sym + ''`, `` `${sym}` ``,
8514    // `[sym].join()`) rejects the way node does instead of silently rendering
8515    // `Symbol(desc)`.
8516    if with_host(|h| matches!(h.get(&p), Some(JsObj::Symbol { .. }))) {
8517        return Err(type_error("Cannot convert a Symbol value to a string"));
8518    }
8519    Ok(with_host(|h| {
8520        let s = h.str_of(&p);
8521        h.new_str(s)
8522    }))
8523}
8524
8525/// `String(v)` — 22.1.1.1. Identical to [`to_string_value`] except that a
8526/// SYMBOL argument is allowed and renders as `Symbol(desc)` (step 2a).
8527pub fn string_ctor_value(v: &Value) -> Result<Value, String> {
8528    if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8529        return Ok(with_host(|h| {
8530            let s = h.str_of(v);
8531            h.new_str(s)
8532        }));
8533    }
8534    to_string_value(v)
8535}
8536
8537/// `ToNumber(v)` — ECMA-262 7.1.4 — with the object case going through
8538/// `ToPrimitive(v, number)` first, so `+{ valueOf() { return 7 } }` is `7` and
8539/// `+new Date(0)` is `0`. `JsHost::to_number` alone cannot do this: it runs
8540/// under the host borrow and so can never invoke a JS `valueOf`.
8541pub fn to_number_value(v: &Value) -> Result<f64, String> {
8542    // `ToNumber(symbol)` throws (7.1.4 step 2). It is primitive, so without this
8543    // it fell into `to_number` and quietly produced `NaN` — `Number(Symbol())`
8544    // and `+Symbol()` are both `TypeError` on node v26.7.0.
8545    if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8546        return Err(type_error("Cannot convert a Symbol value to a number"));
8547    }
8548    if let Some(n) = with_host(|h| is_primitive(h, v).then(|| h.to_number(v))) {
8549        return Ok(n);
8550    }
8551    let p = to_primitive(v, "number")?;
8552    // Steps 2-3 apply to the ToPrimitive RESULT, not only to the argument. Only
8553    // the argument was checked, so an object whose conversion yields a symbol or
8554    // a BigInt slipped past: `+Object(9n)` answered 9 and
8555    // `+{ [Symbol.toPrimitive]() { return Symbol('s') } }` answered NaN, where
8556    // both are TypeErrors. `Number(x)` is ToNumeric and keeps its own path,
8557    // which is why `Number(Object(9n))` is still 9.
8558    match with_host(|h| h.get(&p).cloned()) {
8559        Some(JsObj::Symbol { .. }) => Err(type_error("Cannot convert a Symbol value to a number")),
8560        Some(JsObj::BigInt(_)) => Err(type_error("Cannot convert a BigInt value to a number")),
8561        _ => Ok(with_host(|h| h.to_number(&p))),
8562    }
8563}
8564
8565/// `ToPropertyKey(v)` — ECMA-262 7.1.19. A symbol keeps its stable internal
8566/// key; anything else is `ToPrimitive(v, string)` then `ToString`, so
8567/// `obj[{ toString() { return 'k' } }]` really reads `obj.k`.
8568pub fn to_property_key(v: &Value) -> Result<String, String> {
8569    // One borrow for the overwhelmingly common primitive key (`a[i]`, `o[s]`,
8570    // `o[sym]`); only an object key pays for the conversion.
8571    if let Some(k) = with_host(|h| is_primitive(h, v).then(|| h.property_key(v))) {
8572        return Ok(k);
8573    }
8574    let p = to_primitive(v, "string")?;
8575    Ok(with_host(|h| h.str_of(&p)))
8576}
8577
8578/// Whether `h.get(v)` is any callable kind. A Proxy is callable exactly when its
8579/// target is (10.5: the `[[Call]]` slot is installed only for a callable
8580/// target), so `typeof` and every `is_callable` guard agree on one answer.
8581pub fn is_callable(h: &JsHost, v: &Value) -> bool {
8582    match h.get(v) {
8583        // Not every builtin is a function: the namespace objects (`Math`,
8584        // `require('fs')`) and the `<Ctor>.prototype` handles are data, and
8585        // calling one is a `TypeError` in node exactly as `typeof` says.
8586        Some(JsObj::Builtin(n)) => builtin_is_callable(n),
8587        Some(JsObj::Func(_))
8588        | Some(JsObj::BoundMethod { .. })
8589        | Some(JsObj::BoundFunc { .. })
8590        | Some(JsObj::Class(_)) => true,
8591        Some(JsObj::Proxy { target, .. }) => is_callable(h, target),
8592        _ => false,
8593    }
8594}
8595
8596/// Walk `recv`'s own props then its prototype chain for `key`, returning the
8597/// stored value (methods, inherited data props). Does NOT invoke accessors.
8598/// A PROTOCOL lookup — `Symbol.toPrimitive`, `Symbol.hasInstance`, `toJSON`,
8599/// `then` and the rest — which the spec performs with `[[Get]]`.
8600///
8601/// That distinction only shows on a PROXY: `lookup_chain` walks the property
8602/// map and never asks the handler, so a proxy supplying a protocol method
8603/// through its `get` trap was invisible and the operation fell back to the
8604/// default. Everything else takes the cheap chain walk.
8605pub fn protocol_lookup(v: &Value, key: &str) -> Result<Option<Value>, String> {
8606    if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8607        let got = crate::builtins::get_property(v, key)?;
8608        return Ok((!matches!(got, Value::Undef)).then_some(got));
8609    }
8610    Ok(with_host(|h| lookup_chain(h, v, key)))
8611}
8612
8613pub fn lookup_chain(h: &JsHost, recv: &Value, key: &str) -> Option<Value> {
8614    if let Some(JsObj::Object(p)) = h.get(recv) {
8615        if let Some(v) = p.get(key) {
8616            return Some(v.clone());
8617        }
8618    }
8619    let mut cur = h.proto_of(recv);
8620    while let Some(p) = cur {
8621        // A chain link may be a plain object OR a function/class (the `router`
8622        // package sets `Router.prototype = function(){}` and hangs its methods off
8623        // that function, so the methods live in the fn-prop side table).
8624        match h.get(&p) {
8625            Some(JsObj::Object(props)) => {
8626                if let Some(v) = props.get(key) {
8627                    return Some(v.clone());
8628                }
8629            }
8630            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
8631                if let Some(v) = h.fn_prop(&p, key) {
8632                    return Some(v);
8633                }
8634            }
8635            _ => {}
8636        }
8637        cur = h.proto_of(&p);
8638    }
8639    None
8640}
8641
8642/// Find a getter/setter accessor for `key` on `recv` or up its prototype chain.
8643pub fn lookup_accessor(
8644    h: &JsHost,
8645    recv: &Value,
8646    key: &str,
8647) -> Option<(Option<Value>, Option<Value>)> {
8648    if let Some(a) = h.own_accessor(recv, key) {
8649        return Some(a);
8650    }
8651    let mut cur = h.proto_of(recv);
8652    while let Some(p) = cur {
8653        if let Some(a) = h.own_accessor(&p, key) {
8654            return Some(a);
8655        }
8656        cur = h.proto_of(&p);
8657    }
8658    // A STATIC accessor declared by an ancestor class. A subclass reaches its
8659    // parent's statics through `ClassVal.parent`, not the `protos` map the walk
8660    // above reads — classes are not linked there, so that walk ended at once.
8661    // Static methods and fields already inherited because `class_static` does
8662    // this same parent walk for `fn_prop`; only accessors had no equivalent:
8663    //
8664    //     class Base { static get kind() { return 'base' } }
8665    //     class Sub extends Base {}
8666    //     Sub.plain()  // worked, a fn_prop
8667    //     Sub.kind     // undefined; node reads 'base'
8668    //
8669    // The caller invokes the getter with the class it was READ off as `this`,
8670    // so a getter reading `this.x` sees the subclass, per 10.2.4.
8671    let mut cls = recv.clone();
8672    while let Some(JsObj::Class(c)) = h.get(&cls) {
8673        let Some(parent) = c.parent.clone() else {
8674            break;
8675        };
8676        if let Some(a) = h.own_accessor(&parent, key) {
8677            return Some(a);
8678        }
8679        cls = parent;
8680    }
8681    None
8682}
8683
8684/// Register a builtin error prototype (for `instanceof Error` etc.).
8685pub fn set_error_proto(name: &str, proto: Value) {
8686    with_host(|h| {
8687        h.error_protos.insert(name.to_string(), proto);
8688    });
8689}
8690pub fn error_proto(name: &str) -> Option<Value> {
8691    with_host(|h| h.error_protos.get(name).cloned())
8692}
8693/// Error prototype lookup with a borrowed host (used inside a `with_host` block).
8694pub fn error_proto_of(h: &JsHost, name: &str) -> Option<Value> {
8695    h.error_protos.get(name).cloned()
8696}
8697
8698impl JsHost {
8699    /// `Error.prototype.toString` for an object whose prototype chain reaches
8700    /// `Error.prototype`: `"Name"` with an empty message, else `"Name: message"`.
8701    /// `None` for anything that is not an error, so the caller keeps its own
8702    /// stringification.
8703    pub fn error_to_string(&self, v: &Value) -> Option<String> {
8704        let base = self.error_protos.get("Error")?;
8705        let mut cur = self.proto_of(v);
8706        let mut is_error = false;
8707        while let Some(p) = cur {
8708            if self.strict_eq(&p, base) {
8709                is_error = true;
8710                break;
8711            }
8712            cur = self.proto_of(&p);
8713        }
8714        if !is_error {
8715            return None;
8716        }
8717        let name = lookup_chain(self, v, "name")
8718            .map(|n| self.str_of(&n))
8719            .unwrap_or_else(|| "Error".into());
8720        let message = lookup_chain(self, v, "message")
8721            .map(|m| self.str_of(&m))
8722            .unwrap_or_default();
8723        // Node's internal coded errors override `toString` as
8724        // `${name} [${code}]: ${message}` (internal/errors.js NodeError). The
8725        // `@@nodeError` tag marks the errors `synth_error` built from a
8726        // `Name [ERR_CODE]: …` string, so a user error that merely has a `.code`
8727        // property still stringifies plainly.
8728        if let Some(JsObj::Object(p)) = self.get(v) {
8729            if p.contains_key("@@nodeError") {
8730                if let Some(code) = p.get("code").map(|c| self.str_of(c)) {
8731                    return Some(format!("{name} [{code}]: {message}"));
8732                }
8733            }
8734        }
8735        Some(match (name.is_empty(), message.is_empty()) {
8736            (true, _) => message,
8737            (false, true) => name,
8738            (false, false) => format!("{name}: {message}"),
8739        })
8740    }
8741}
8742
8743/// The set of builtin error constructor names forming the error hierarchy.
8744pub const ERROR_NAMES: &[&str] = &[
8745    "Error",
8746    "TypeError",
8747    "RangeError",
8748    "SyntaxError",
8749    "ReferenceError",
8750    "EvalError",
8751    "URIError",
8752    "AggregateError",
8753    // `assert`'s error class. It is NOT a global (node exposes it only as
8754    // `assert.AssertionError`, and `GLOBAL_FUNCS` is a separate table), but it
8755    // has to be a name `synth_error` recognizes: without it the head
8756    // `AssertionError [ERR_ASSERTION]: …` failed the class check and fell into
8757    // the `Error` branch with the WHOLE head kept as the message, so `e.name`
8758    // was `Error` and `e.message` carried a prefix node keeps out of it.
8759    "AssertionError",
8760    // The WHATWG error class `AbortSignal.reason` carries. Unlike the others its
8761    // `name` comes from the SECOND constructor argument rather than from the
8762    // class, so its prototype keeps the base default and each instance stamps
8763    // its own name into an internal slot.
8764    "DOMException",
8765];
8766
8767impl JsHost {
8768    /// Lazily build the builtin error prototype chain: `Error.prototype →
8769    /// Object.prototype`, and every specific error's prototype → `Error.prototype`.
8770    /// Populated once; instances link to these so `e instanceof TypeError` and
8771    /// `e instanceof Error` both hold.
8772    /// The real `Buffer.prototype` object, building the
8773    /// `Buffer.prototype → Uint8Array.prototype → Object.prototype` chain on
8774    /// first use.
8775    ///
8776    /// A `Buffer` used to be a bare tagged object with no `[[Prototype]]` at
8777    /// all, so `Object.getPrototypeOf(buf) === Buffer.prototype` read false and
8778    /// `instanceof` had to be special-cased around it. Each prototype is a
8779    /// genuine object carrying `@proto:<Ctor>:<method>` thunks for its instance
8780    /// methods, so `Buffer.prototype.slice.call(buf, 1)` still dispatches the
8781    /// way it did when `Buffer.prototype` was a `Builtin` namespace.
8782    pub fn ensure_native_protos(&mut self) {
8783        // The wrapper prototypes share this registry and this guard would skip
8784        // them, so they are built through their own.
8785        self.ensure_wrapper_protos();
8786        self.ensure_function_kind_protos();
8787        if self.native_protos.contains_key("Buffer") {
8788            return;
8789        }
8790        let obj_proto = self.object_proto();
8791        // `Object.prototype` is the one builtin prototype that already existed as
8792        // a real object (it is the chain root). Register it so `Object.prototype`
8793        // reads resolve to THAT object rather than a fresh `Builtin` namespace —
8794        // otherwise `Object.getPrototypeOf(C.prototype) === Object.prototype`
8795        // compares a real object against a thunk and reads false.
8796        self.native_protos
8797            .insert("Object".to_string(), obj_proto.clone());
8798        for m in crate::builtins::OBJECT_PROTO_METHODS {
8799            let thunk = self.alloc(JsObj::Builtin(format!("@proto:Object:{m}")));
8800            if let Some(JsObj::Object(p)) = self.get_mut(&obj_proto) {
8801                p.insert((*m).to_string(), thunk);
8802            }
8803            self.hide_prop(&obj_proto, m);
8804        }
8805        // `Buffer.prototype → Uint8Array.prototype → %TypedArray%.prototype →
8806        // Object.prototype`, which is the chain node v26.7.0 really has. The
8807        // shared iteration methods (`every`, `map`, `filter`, …) live on the
8808        // `%TypedArray%.prototype` intermediate, NOT on `Uint8Array.prototype`:
8809        // measured, `Uint8Array.prototype.hasOwnProperty('every')` is false in
8810        // Node while the intermediate owns it. `%TypedArray%` is not a global,
8811        // so it is reachable only by walking the chain — exactly as in Node.
8812        // Every element kind gets its own prototype hanging off the shared
8813        // intermediate, so `Object.getPrototypeOf(new Int32Array(1))` is
8814        // `Int32Array.prototype` rather than some other kind's. Linking them all
8815        // to `Uint8Array.prototype` would have been the easy version and would
8816        // have made an `Int32Array` claim the wrong prototype.
8817        let mut chain: Vec<(&str, Value)> = vec![("TypedArray", obj_proto)];
8818        for kind in crate::stdlib::typedarray::ELEMENT_KINDS {
8819            chain.push((kind, Value::Undef)); // parent: %TypedArray%.prototype
8820        }
8821        // `Buffer.prototype`'s parent is `Uint8Array.prototype` specifically.
8822        chain.push(("Buffer", Value::Undef));
8823        let mut prev: Option<Value> = None;
8824        for (ctor, parent) in chain.drain(..) {
8825            let proto = self.new_object(IndexMap::new());
8826            // Each kind hangs off the shared intermediate; `Buffer` hangs off
8827            // `Uint8Array.prototype`; the intermediate itself off
8828            // `Object.prototype`.
8829            let parent = match ctor {
8830                "TypedArray" => parent,
8831                "Buffer" => self
8832                    .native_protos
8833                    .get("Uint8Array")
8834                    .cloned()
8835                    .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
8836                _ => self
8837                    .native_protos
8838                    .get("TypedArray")
8839                    .cloned()
8840                    .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
8841            };
8842            self.set_proto(&proto, parent);
8843            // `%TypedArray%.prototype` has no reachable constructor global, so
8844            // it gets no `constructor` slot (Node's is the anonymous
8845            // `%TypedArray%` intrinsic).
8846            if ctor != "TypedArray" {
8847                let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
8848                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8849                    p.insert("constructor".into(), ctor_val);
8850                }
8851                self.hide_prop(&proto, "constructor");
8852            }
8853            let methods: &[&str] = match ctor {
8854                "Buffer" => crate::stdlib::buffer::INSTANCE_METHODS,
8855                "TypedArray" => crate::stdlib::typedarray::PROTOTYPE_METHODS,
8856                // `Uint8Array` alone owns the base64/hex pair — no other view
8857                // has them, which is the whole reason they cannot live on the
8858                // shared `%TypedArray%` prototype above.
8859                "Uint8Array" => crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS,
8860                // Every other kind's prototype owns no methods; it inherits them
8861                // from the intermediate above. It does own `BYTES_PER_ELEMENT`,
8862                // which is per-kind and which Node really keeps there (measured:
8863                // `Uint8Array.prototype.hasOwnProperty('BYTES_PER_ELEMENT')`).
8864                _ => &[],
8865            };
8866            if crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor) {
8867                let bpe = Value::Float(crate::stdlib::typedarray::bytes_per_element(ctor) as f64);
8868                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8869                    p.insert("BYTES_PER_ELEMENT".into(), bpe);
8870                }
8871                self.hide_prop(&proto, "BYTES_PER_ELEMENT");
8872            }
8873            for m in methods {
8874                let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
8875                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8876                    p.insert((*m).to_string(), thunk);
8877                }
8878                self.hide_prop(&proto, m);
8879            }
8880            self.native_protos.insert(ctor.to_string(), proto.clone());
8881            prev = Some(proto);
8882        }
8883    }
8884
8885    /// `String.prototype`, `Number.prototype` and `Boolean.prototype` as REAL
8886    /// objects.
8887    ///
8888    /// A wrapper built by `new String("a")` needs a genuine `[[Prototype]]`
8889    /// link: `Builtin("String.prototype")` is a thunk namespace that cannot
8890    /// appear on a prototype chain, so `Object.getPrototypeOf(w) ===
8891    /// String.prototype` and `w instanceof String` both read false while the
8892    /// wrapper's methods still resolved through the string funnel. Registering
8893    /// them here puts them on the same footing as `Buffer.prototype`.
8894    /// `GeneratorFunction.prototype`, `AsyncFunction.prototype` and
8895    /// `AsyncGeneratorFunction.prototype` — the intrinsics a generator or async
8896    /// function's `[[Prototype]]` really points at.
8897    ///
8898    /// None are globals (node exposes them only through
8899    /// `Object.getPrototypeOf(function*(){}).constructor`), so they live here
8900    /// rather than among the wrapper constructors. Each hangs off
8901    /// `Function.prototype` and carries the `Symbol.toStringTag` that names it.
8902    pub fn ensure_function_kind_protos(&mut self) {
8903        if self.native_protos.contains_key("GeneratorFunction") {
8904            return;
8905        }
8906        let base = self
8907            .native_protos
8908            .get("Function")
8909            .cloned()
8910            .unwrap_or_else(|| self.object_proto());
8911        for ctor in [
8912            "GeneratorFunction",
8913            "AsyncFunction",
8914            "AsyncGeneratorFunction",
8915        ] {
8916            let proto = self.new_object(IndexMap::new());
8917            self.set_proto(&proto, base.clone());
8918            let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
8919            let tag = self.new_str(ctor);
8920            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8921                p.insert("constructor".into(), ctor_val);
8922                p.insert("@@toStringTag".into(), tag);
8923            }
8924            self.hide_prop(&proto, "constructor");
8925            self.hide_prop(&proto, "@@toStringTag");
8926            self.native_protos.insert(ctor.to_string(), proto);
8927        }
8928    }
8929
8930    pub fn ensure_wrapper_protos(&mut self) {
8931        if self.native_protos.contains_key("String") {
8932            return;
8933        }
8934        let obj_proto = self.object_proto();
8935        for ctor in ["String", "Number", "Boolean", "Symbol", "BigInt"] {
8936            let proto = self.new_object(IndexMap::new());
8937            self.set_proto(&proto, obj_proto.clone());
8938            let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
8939            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8940                p.insert("constructor".into(), ctor_val);
8941            }
8942            self.hide_prop(&proto, "constructor");
8943            // The three conversions must be here because `Object.prototype`
8944            // also defines them: without a shadowing entry a wrapper would
8945            // inherit the object forms and `String(new String("a"))` would
8946            // report `[object Object]`.
8947            //
8948            // The REST are here because the prototype is a real object a script
8949            // can read a method OFF of. Only the three were installed, on the
8950            // reasoning that `charAt`/`toFixed`/… reach the primitive through
8951            // `call_method` anyway — true for `s.charAt(0)` and false for the
8952            // generic-borrowing form: `String.prototype.trim` read `undefined`,
8953            // so `String.prototype.trim.call(s)` — and `Number.prototype
8954            // .toFixed.call(n)`, and every `Array.prototype`-style borrow of a
8955            // wrapper method — threw. `Array.prototype`/`Object.prototype`
8956            // already carried their whole method set; these three did not.
8957            let methods: Vec<&str> = ["toString", "valueOf", "toLocaleString"]
8958                .into_iter()
8959                .chain(match ctor {
8960                    "String" => crate::builtins::STRING_PROTO_METHODS.iter().copied(),
8961                    "Number" => crate::builtins::NUMBER_PROTO_METHODS.iter().copied(),
8962                    _ => [].iter().copied(),
8963                })
8964                // The symbol-keyed methods come from the generated intrinsic
8965                // table, so this object advertises exactly the symbol methods
8966                // node defines on it — `String.prototype[Symbol.iterator]` was
8967                // `undefined` because only the string-keyed lists were walked.
8968                .chain(crate::builtins::proto_symbol_methods(ctor))
8969                .collect();
8970            let mut seen: Vec<&str> = Vec::new();
8971            for m in methods {
8972                if seen.contains(&m) {
8973                    continue;
8974                }
8975                seen.push(m);
8976                let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
8977                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8978                    p.insert(m.to_string(), thunk);
8979                }
8980                self.hide_prop(&proto, m);
8981            }
8982            // `Symbol.prototype` and `BigInt.prototype` are the two wrapper
8983            // prototypes that carry a `@@toStringTag`; the other three are
8984            // branded by their internal slot instead, and node reports
8985            // `undefined` for their tag. Without it
8986            // `Object.prototype.toString.call(Symbol.prototype)` read
8987            // `[object Object]` where node says `[object Symbol]`.
8988            if matches!(ctor, "Symbol" | "BigInt") {
8989                let tag = self.new_str(ctor.to_string());
8990                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8991                    p.insert("@@toStringTag".into(), tag);
8992                }
8993                // Not `hide_prop`: a well-known `@@toStringTag` is read-only as
8994                // well as non-enumerable (20.4.3.6), and `hide_prop` leaves it
8995                // writable.
8996                self.set_prop_attrs(
8997                    &proto,
8998                    "@@toStringTag",
8999                    PropAttrs {
9000                        writable: false,
9001                        enumerable: false,
9002                        configurable: true,
9003                    },
9004                );
9005            }
9006            // `Symbol.prototype[@@toPrimitive]` (20.4.3.5) is what a string or
9007            // numeric conversion of a symbol reaches FIRST. Its absence was
9008            // observable in the failure wording: `String(Symbol.prototype)`
9009            // throws in node because `@@toPrimitive` rejects a non-Symbol
9010            // `this`, and here the conversion fell through to `toString` and
9011            // named that method in the message instead.
9012            if ctor == "Symbol" {
9013                let thunk = self.alloc(JsObj::Builtin("@proto:Symbol:@@toPrimitive".to_string()));
9014                if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9015                    p.insert("@@toPrimitive".into(), thunk);
9016                }
9017                self.hide_prop(&proto, "@@toPrimitive");
9018            }
9019            self.native_protos.insert(ctor.to_string(), proto);
9020        }
9021    }
9022
9023    /// The cached template object for one tagged-template site, if it has been
9024    /// evaluated before.
9025    pub fn template_object(&self, key: (u64, u64)) -> Option<Value> {
9026        self.template_objects.get(&key).cloned()
9027    }
9028    /// Record the template object for one tagged-template site.
9029    pub fn set_template_object(&mut self, key: (u64, u64), v: Value) {
9030        self.template_objects.insert(key, v);
9031    }
9032
9033    /// The real prototype object for a builtin exotic, if it has one.
9034    pub fn native_proto(&self, ctor: &str) -> Option<Value> {
9035        self.native_protos.get(ctor).cloned()
9036    }
9037
9038    /// The constructor name whose `.prototype` object IS `v`, for a prototype
9039    /// this host built as a real object (`String.prototype`, `TypeError
9040    /// .prototype`, `Buffer.prototype`) rather than as a `Builtin` namespace.
9041    ///
9042    /// A prototype is an ORDINARY object: it carries no instance's internal
9043    /// slot, so `Object.prototype.toString.call(TypeError.prototype)` is
9044    /// `[object Object]` and not `[object Error]`. Nothing distinguished the two
9045    /// before, so the brand fell through to the "does it look like an Error"
9046    /// test and answered for the prototype as if it were an instance.
9047    pub fn intrinsic_proto_ctor(&self, v: &Value) -> Option<&str> {
9048        if !matches!(v, Value::Obj(_)) {
9049            return None;
9050        }
9051        self.native_protos
9052            .iter()
9053            .chain(self.error_protos.iter())
9054            .find(|(_, p)| *p == v)
9055            .map(|(name, _)| name.as_str())
9056    }
9057
9058    /// The real `.prototype` object for a native stdlib constructor (`StringDecoder`,
9059    /// `Hash`, `URLSearchParams`, …), built on first read and cached.
9060    ///
9061    /// `Ctor.prototype` used to read `undefined` for every native class outside the
9062    /// hand-written `is_builtin_ctor` list, which broke the ES5 subclassing pattern
9063    /// that libraries still use. `iconv-lite`'s internal codec — reached from
9064    /// `raw-body` on every `express.json()` request — does exactly this:
9065    ///
9066    /// ```text
9067    /// var StringDecoder = require('string_decoder').StringDecoder;
9068    /// if (!StringDecoder.prototype.end) StringDecoder.prototype.end = function () {};
9069    /// function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
9070    /// InternalDecoder.prototype = StringDecoder.prototype;
9071    /// ```
9072    ///
9073    /// The first line threw `Cannot read properties of undefined (reading 'end')`.
9074    ///
9075    /// Methods come from `stdlib::instance_method_lists`, the same table a method
9076    /// READ consults, so the prototype can never advertise a name the dispatcher
9077    /// does not implement. Each is the `@proto:<Ctor>:<method>` thunk that
9078    /// dispatches against its invoke-time `this`, so a subclass instance whose
9079    /// prototype IS this object gets the native implementation. Returns `None` for
9080    /// a tag with no instance methods, leaving those constructors as they were.
9081    pub fn ensure_ctor_proto(&mut self, ctor: &str) -> Option<Value> {
9082        if let Some(p) = self.native_protos.get(ctor) {
9083            return Some(p.clone());
9084        }
9085        // `Buffer` and the typed-array kinds belong to the chain
9086        // `ensure_native_protos` builds. Building one of them here first hung it
9087        // straight off `Object.prototype` AND registered it, which made that
9088        // chain's own `contains_key("Buffer")` guard skip the build for the rest
9089        // of the process: after `Buffer.from([1])`, `Buffer.prototype instanceof
9090        // Uint8Array` read false.
9091        if ctor == "Buffer"
9092            || ctor == "TypedArray"
9093            || crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor)
9094        {
9095            self.ensure_native_protos();
9096            return self.native_protos.get(ctor).cloned();
9097        }
9098        let (own, emitter) = crate::stdlib::instance_method_lists(ctor);
9099        // A class can carry accessors and no methods at all
9100        // (`AsymmetricKeyObject` is only `asymmetricKeyType` and
9101        // `asymmetricKeyDetails`), so an empty method list does not mean there
9102        // is no prototype to build.
9103        let (accessor_list, _) = crate::stdlib::instance_accessors(ctor);
9104        if own.is_empty() && emitter.is_empty() && accessor_list.is_empty() {
9105            return None;
9106        }
9107        // A native class with a real PARENT hangs off that parent's prototype
9108        // rather than straight off `Object.prototype`. The stream hierarchy is
9109        // `Readable → Stream → EventEmitter`, which is what makes
9110        // `new Readable() instanceof Stream` hold and what an ES5 subclass
9111        // doing `Object.create(Stream.prototype)` inherits from.
9112        let parent_proto = match crate::stdlib::native_parent(ctor) {
9113            Some(p) => self
9114                .ensure_ctor_proto(p)
9115                .unwrap_or_else(|| self.object_proto()),
9116            None => self.object_proto(),
9117        };
9118        let proto = self.new_object(IndexMap::new());
9119        self.set_proto(&proto, parent_proto);
9120        let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9121        if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9122            p.insert("constructor".into(), ctor_val);
9123        }
9124        self.hide_prop(&proto, "constructor");
9125        // A prototype member is ENUMERABLE in node for every class but the few
9126        // written as ES classes, so `for (const k in url)` walks `href` and the
9127        // rest. Hiding all of them made that loop find nothing.
9128        let visible = crate::stdlib::instance_members_enumerable(ctor);
9129        let symbols = crate::builtins::proto_symbol_methods(ctor);
9130        for m in own.iter().chain(emitter.iter()).chain(symbols.iter()) {
9131            let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9132            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9133                p.insert((*m).to_string(), thunk);
9134            }
9135            // A symbol-keyed member never enumerates.
9136            if !visible || m.starts_with("@@") {
9137                self.hide_prop(&proto, m);
9138            }
9139        }
9140        // Accessors and the class's `Symbol.toStringTag`, both of which live on
9141        // the PROTOTYPE in node — an instance owns neither.
9142        let (accessors, tag) = crate::stdlib::instance_accessors(ctor);
9143        for (key, settable) in accessors {
9144            let get = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@get@{key}")));
9145            let set =
9146                settable.then(|| self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@set@{key}"))));
9147            self.set_accessor(&proto, key, Some(get), set);
9148            if !visible {
9149                self.hide_prop(&proto, key);
9150            }
9151        }
9152        for m in crate::stdlib::instance_late_methods(ctor) {
9153            let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9154            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9155                p.insert((*m).to_string(), thunk);
9156            }
9157            if !visible {
9158                self.hide_prop(&proto, m);
9159            }
9160        }
9161        if !tag.is_empty() {
9162            let tag = self.new_str(tag.to_string());
9163            if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9164                p.insert("@@toStringTag".into(), tag);
9165            }
9166            self.hide_prop(&proto, "@@toStringTag");
9167        }
9168        self.native_protos.insert(ctor.to_string(), proto.clone());
9169        Some(proto)
9170    }
9171
9172    /// `<ErrorClass>.prototype`, once [`JsHost::ensure_error_protos`] has run.
9173    /// The error prototypes live in their own table, so `ensure_ctor_proto` —
9174    /// which answers from `native_protos` — does not find them.
9175    pub fn error_proto(&self, name: &str) -> Option<Value> {
9176        self.error_protos.get(name).cloned()
9177    }
9178
9179    pub fn ensure_error_protos(&mut self) {
9180        if !self.error_protos.is_empty() {
9181            return;
9182        }
9183        let obj_proto = self.object_proto();
9184        // Error.prototype first (the shared base).
9185        let err_proto = self.new_object(IndexMap::new());
9186        self.set_proto(&err_proto, obj_proto);
9187        let nm = self.new_str("Error");
9188        let empty = self.new_str("");
9189        let ctor = self.alloc(JsObj::Builtin("Error".into()));
9190        // `Error.prototype.toString` (20.5.3.4) has to be an OWN property here,
9191        // not a fallback the stringifier applies when nothing else matches: it
9192        // exists precisely to shadow `Object.prototype.toString`. Without it,
9193        // the first read of `Error.prototype` or `Object.prototype` — which
9194        // `x instanceof Error` performs, so ordinary code triggers it —
9195        // materialised `Object.prototype.toString`, the chain lookup started
9196        // finding it, and `String(err)` flipped from `Error: m` to
9197        // `[object Error]` for the REST OF THE PROCESS, including errors
9198        // created before the read.
9199        let to_string = self.alloc(JsObj::Builtin("@proto:Error:toString".into()));
9200        if let Some(JsObj::Object(p)) = self.get_mut(&err_proto) {
9201            p.insert("name".into(), nm);
9202            p.insert("message".into(), empty);
9203            p.insert("constructor".into(), ctor);
9204            p.insert("toString".into(), to_string);
9205        }
9206        // Everything on `Error.prototype` is non-enumerable in V8.
9207        for k in ["name", "message", "constructor", "toString"] {
9208            self.hide_prop(&err_proto, k);
9209        }
9210        self.error_protos.insert("Error".into(), err_proto.clone());
9211        for name in &ERROR_NAMES[1..] {
9212            let p = self.new_object(IndexMap::new());
9213            self.set_proto(&p, err_proto.clone());
9214            let nm = self.new_str(*name);
9215            let ctor = self.alloc(JsObj::Builtin((*name).to_string()));
9216            if let Some(JsObj::Object(o)) = self.get_mut(&p) {
9217                o.insert("name".into(), nm);
9218                o.insert("constructor".into(), ctor);
9219            }
9220            self.hide_prop(&p, "name");
9221            self.hide_prop(&p, "constructor");
9222            self.error_protos.insert((*name).to_string(), p);
9223        }
9224    }
9225}
9226
9227// ── Map/Set element access (used by builtins) ────────────────────────────────
9228
9229impl JsHost {
9230    /// A function's `.length`: the count of leading params before the first one
9231    /// with a default or the rest element.
9232    pub fn func_arity(&self, v: &Value) -> usize {
9233        // 20.2.3.2: a bound function's `length` is the target's, less the
9234        // arguments already bound, floored at 0. Reporting 0 for every bound
9235        // function breaks arity dispatch — express picks error-handling
9236        // middleware with `fn.length === 4`, so a bound handler was never
9237        // recognised as one.
9238        if let Some(JsObj::BoundFunc { target, args, .. }) = self.get(v) {
9239            return self.func_arity(&target.clone()).saturating_sub(args.len());
9240        }
9241        // A builtin's arity is the specified one, so `Math.max.bind(null,1)`
9242        // reports 1 rather than the 0 a target of unknown arity would give.
9243        if let Some(JsObj::Builtin(n)) = self.get(v) {
9244            return crate::builtins::builtin_meta(n)
9245                .map(|(_, len)| len as usize)
9246                .unwrap_or(0);
9247        }
9248        let def_id = match self.get(v) {
9249            Some(JsObj::Func(f)) => Some(f.def_id),
9250            Some(JsObj::Class(c)) => match c.ctor.as_ref().and_then(|cf| self.get(cf)) {
9251                Some(JsObj::Func(f)) => Some(f.def_id),
9252                _ => None,
9253            },
9254            _ => None,
9255        };
9256        match def_id.and_then(|id| self.funcs.get(id)) {
9257            Some(def) => def
9258                .params
9259                .iter()
9260                .take_while(|p| !p.rest && !p.has_default)
9261                .count(),
9262            None => 0,
9263        }
9264    }
9265
9266    pub fn is_map(&self, v: &Value) -> bool {
9267        matches!(self.get(v), Some(JsObj::Map { .. }))
9268    }
9269    pub fn is_set(&self, v: &Value) -> bool {
9270        matches!(self.get(v), Some(JsObj::Set { .. }))
9271    }
9272}
9273
9274// ── promises & the event loop ────────────────────────────────────────────────
9275
9276impl JsHost {
9277    /// Allocate a fresh pending promise, returning its heap value.
9278    pub fn new_promise(&mut self) -> Value {
9279        let id = self.promises.len() as u32;
9280        self.promises.push(PromiseCell {
9281            state: PromiseState::Pending,
9282            value: Value::Undef,
9283            reactions: Vec::new(),
9284            handled: false,
9285        });
9286        self.alloc(JsObj::Promise { id })
9287    }
9288    pub fn promise_id(&self, v: &Value) -> Option<u32> {
9289        match self.get(v) {
9290            Some(JsObj::Promise { id }) => Some(*id),
9291            _ => None,
9292        }
9293    }
9294    pub fn promise_state(&self, id: u32) -> PromiseState {
9295        self.promises[id as usize].state
9296    }
9297    pub fn promise_value(&self, id: u32) -> Value {
9298        self.promises[id as usize].value.clone()
9299    }
9300    pub fn promise_mark_handled(&mut self, id: u32) {
9301        self.promises[id as usize].handled = true;
9302    }
9303    /// Take the pending reactions of a promise (called on settle).
9304    pub fn take_reactions(&mut self, id: u32) -> Vec<PromiseReaction> {
9305        std::mem::take(&mut self.promises[id as usize].reactions)
9306    }
9307    pub fn add_reaction(&mut self, id: u32, r: PromiseReaction) {
9308        self.promises[id as usize].reactions.push(r);
9309    }
9310    pub fn settle_promise(&mut self, id: u32, state: PromiseState, value: Value) {
9311        let c = &mut self.promises[id as usize];
9312        if c.state != PromiseState::Pending {
9313            return; // already settled — resolve/reject are one-shot
9314        }
9315        c.state = state;
9316        c.value = value;
9317    }
9318    pub fn queue_micro(&mut self, cb: Value, args: Vec<Value>) {
9319        self.microtasks.push_back(Task::Js { cb, args });
9320    }
9321    pub fn queue_nexttick(&mut self, cb: Value, args: Vec<Value>) {
9322        self.nextticks.push_back(Task::Js { cb, args });
9323    }
9324    /// Schedule a native (Rust) microtask — used by Promise reactions and async
9325    /// resumption.
9326    pub fn queue_micro_native(&mut self, f: Box<dyn FnOnce() -> Result<(), String>>) {
9327        self.microtasks.push_back(Task::Native(f));
9328    }
9329    /// Schedule a macrotask. `interval` is the repeat period for `setInterval`
9330    /// (`None` for the one-shot `setTimeout`/`setImmediate`). Returns the timer
9331    /// id, which the `Timeout`/`Immediate` handle object carries so `clear*`,
9332    /// `ref`/`unref` and `refresh` can find this entry again.
9333    pub fn add_timer(
9334        &mut self,
9335        delay: f64,
9336        callback: Value,
9337        args: Vec<Value>,
9338        interval: Option<f64>,
9339    ) -> u64 {
9340        let id = self.next_timer;
9341        self.next_timer += 1;
9342        // Real deadline for the real-clock path; `setImmediate` (delay < 0) is
9343        // clamped to "now". Virtual-clock ordering still uses `delay`/`seq`.
9344        let deadline = Instant::now() + Duration::from_millis(delay.max(0.0) as u64);
9345        self.macrotasks.push(Timer {
9346            id,
9347            delay,
9348            seq: id,
9349            callback,
9350            args,
9351            cancelled: false,
9352            interval,
9353            refed: true,
9354            deadline,
9355        });
9356        id
9357    }
9358    /// Re-arm a repeating timer that is about to fire, keeping its id (so a
9359    /// `clearInterval` from *inside* the callback cancels this very entry) and
9360    /// taking a fresh `seq` so same-delay peers still round-robin.
9361    ///
9362    /// Called BEFORE the callback runs: if it were called after, the entry would
9363    /// be absent while the callback executed and a `clearInterval(t)` there would
9364    /// cancel nothing, resurrecting an interval the program had stopped.
9365    fn rearm_timer(&mut self, t: &Timer, period: f64) {
9366        let seq = self.next_timer;
9367        self.next_timer += 1;
9368        let deadline = Instant::now() + Duration::from_millis(period.max(0.0) as u64);
9369        self.macrotasks.push(Timer {
9370            id: t.id,
9371            delay: t.delay,
9372            seq,
9373            callback: t.callback.clone(),
9374            args: t.args.clone(),
9375            cancelled: false,
9376            interval: Some(period),
9377            refed: t.refed,
9378            deadline,
9379        });
9380    }
9381    /// `timeout.ref()` / `timeout.unref()` — set the handle bit on a pending
9382    /// timer. A no-op once the timer has fired or been cleared (Node likewise
9383    /// treats `ref`/`unref` on a dead timer as inert).
9384    pub fn set_timer_refed(&mut self, id: u64, refed: bool) {
9385        for t in &mut self.macrotasks {
9386            if t.id == id && !t.cancelled {
9387                t.refed = refed;
9388            }
9389        }
9390    }
9391    /// `timeout.hasRef()` — whether a still-pending timer holds the loop open.
9392    /// A fired or cleared timer reports `false`, matching Node.
9393    pub fn timer_has_ref(&self, id: u64) -> bool {
9394        self.macrotasks
9395            .iter()
9396            .any(|t| t.id == id && !t.cancelled && t.refed)
9397    }
9398    /// `timeout.refresh()` — restart the countdown from now, as if the timer had
9399    /// just been scheduled.
9400    pub fn refresh_timer(&mut self, id: u64) {
9401        let now = Instant::now();
9402        for t in &mut self.macrotasks {
9403            if t.id == id && !t.cancelled {
9404                t.deadline = now + Duration::from_millis(t.delay.max(0.0) as u64);
9405            }
9406        }
9407    }
9408    /// Clone the I/O sender for a background I/O thread.
9409    pub fn io_sender(&self) -> Sender<IoTask> {
9410        self.io_tx.clone()
9411    }
9412    /// Register a live handle (listener/socket/ref'd resource) keeping the loop
9413    /// alive.
9414    pub fn incr_handle(&mut self) {
9415        self.open_handles += 1;
9416    }
9417    /// Release a handle; the loop exits once this reaches `0` with empty queues.
9418    pub fn decr_handle(&mut self) {
9419        self.open_handles = self.open_handles.saturating_sub(1);
9420    }
9421    pub fn open_handles(&self) -> usize {
9422        self.open_handles
9423    }
9424    /// Pop the earliest timer whose real deadline is at or before `now` (I/O
9425    /// path). Ties break by `seq`.
9426    fn pop_due_timer(&mut self, now: Instant) -> Option<Timer> {
9427        let idx = self
9428            .macrotasks
9429            .iter()
9430            .enumerate()
9431            .filter(|(_, t)| !t.cancelled && t.deadline <= now)
9432            .min_by(|(_, a), (_, b)| a.deadline.cmp(&b.deadline).then(a.seq.cmp(&b.seq)))
9433            .map(|(i, _)| i);
9434        idx.map(|i| self.macrotasks.remove(i))
9435    }
9436    /// Time until the earliest pending timer's deadline (I/O path blocking bound),
9437    /// or `None` if no timers are pending. Clamped to `0` for already-due timers.
9438    fn next_timer_timeout(&self, now: Instant) -> Option<Duration> {
9439        self.macrotasks
9440            .iter()
9441            .filter(|t| !t.cancelled)
9442            .map(|t| t.deadline)
9443            .min()
9444            .map(|d| d.saturating_duration_since(now))
9445    }
9446    pub fn cancel_timer(&mut self, id: u64) {
9447        for t in &mut self.macrotasks {
9448            if t.id == id {
9449                t.cancelled = true;
9450            }
9451        }
9452    }
9453    fn pop_next_timer(&mut self) -> Option<Timer> {
9454        // Earliest (delay, seq) fires first — a deterministic virtual clock.
9455        let idx = self
9456            .macrotasks
9457            .iter()
9458            .enumerate()
9459            .filter(|(_, t)| !t.cancelled)
9460            .min_by(|(_, a), (_, b)| {
9461                a.delay
9462                    .partial_cmp(&b.delay)
9463                    .unwrap_or(std::cmp::Ordering::Equal)
9464                    .then(a.seq.cmp(&b.seq))
9465            })
9466            .map(|(i, _)| i);
9467        idx.map(|i| self.macrotasks.remove(i))
9468    }
9469    fn next_microtask(&mut self) -> Option<Task> {
9470        // Node's `processTicksAndRejections` runs in ROUNDS: drain the nextTick
9471        // queue, then drain the microtask queue in full, then repeat if the
9472        // microtasks queued more ticks. A tick queued from INSIDE a microtask
9473        // therefore waits for the rest of that microtask queue.
9474        //
9475        // Preferring ticks on every step interleaved the two, so
9476        // `Promise.resolve().then(() => process.nextTick(f))` ran `f` before the
9477        // promise callbacks queued behind it — the one ordering difference a
9478        // library scheduling work from a `.then` can actually observe.
9479        if !self.draining_micro {
9480            if let Some(t) = self.nextticks.pop_front() {
9481                return Some(t);
9482            }
9483        }
9484        if let Some(t) = self.microtasks.pop_front() {
9485            // Stay in the microtask phase until this queue is exhausted.
9486            self.draining_micro = !self.microtasks.is_empty();
9487            return Some(t);
9488        }
9489        self.draining_micro = false;
9490        self.nextticks.pop_front()
9491    }
9492    fn has_microtasks(&self) -> bool {
9493        !self.nextticks.is_empty() || !self.microtasks.is_empty()
9494    }
9495    /// Whether any pending timer is *referenced* — the timer half of Node's
9496    /// handle count. Only these keep the loop alive; unref'd timers still fire
9497    /// while something else holds the loop open, but never hold it themselves.
9498    fn has_refed_macrotasks(&self) -> bool {
9499        self.macrotasks.iter().any(|t| !t.cancelled && t.refed)
9500    }
9501    /// Whether any pending timer repeats. A repeating timer cannot run on the
9502    /// virtual clock: virtual time never advances, so the interval would re-arm
9503    /// at the same instant forever, spinning a core and starving every
9504    /// longer-delay timer behind it. Its presence forces the real clock.
9505    fn has_pending_interval(&self) -> bool {
9506        self.macrotasks
9507            .iter()
9508            .any(|t| !t.cancelled && t.interval.is_some())
9509    }
9510}
9511
9512/// Drive the event loop to quiescence.
9513///
9514/// **Liveness** is Node's handle count: the loop runs while a microtask is
9515/// pending, an open handle is registered (a listening server, a live socket, an
9516/// in-flight async op), or a *referenced* timer is still pending. That last term
9517/// is what makes `setInterval(fn, 1000)` hold the process open forever, as it
9518/// does in Node — the interval re-arms itself, so a ref'd timer is always
9519/// pending and the loop never reaches its exit condition.
9520///
9521/// Two **clock regimes**, selected per iteration:
9522///
9523/// - **Virtual clock** (no open handles and no repeating timer): the original
9524///   deterministic path — fire the earliest `(delay, seq)` timer immediately, no
9525///   real waiting. Parity output and test speed for ordinary `setTimeout`
9526///   scripts are unchanged.
9527/// - **Real clock** (an open handle, or any pending interval): fire every timer
9528///   whose wall-clock deadline has passed, then BLOCK on the I/O channel
9529///   (`recv_timeout` bounded by the next deadline, or unbounded `recv` if no
9530///   timers) and run the received `IoTask` on the main thread. The host keeps
9531///   its own `Sender`, so `recv` never disconnects while the process should stay
9532///   alive.
9533///
9534///   A repeating timer *must* take this path: virtual time never advances, so an
9535///   interval on the virtual clock would re-fire at the same instant forever,
9536///   spinning a core and starving every longer-delay timer behind it.
9537///
9538/// Errors thrown by a task/timer/I/O dispatch abort the loop (uncaught → surfaced).
9539pub fn run_event_loop() -> Result<(), String> {
9540    // Own the receiver for the loop's duration (blocking `recv` cannot hold a
9541    // host borrow); restore it afterward so a re-entrant run reuses the channel.
9542    let rx = with_host(|h| h.io_rx.take());
9543    let result = drive_event_loop(rx.as_ref());
9544    with_host(|h| h.io_rx = rx);
9545    result
9546}
9547
9548fn drive_event_loop(rx: Option<&Receiver<IoTask>>) -> Result<(), String> {
9549    loop {
9550        // 1) Exhaust the microtask queue (nextTick before promise reactions),
9551        //    then report anything that rejected with nobody watching.
9552        while let Some(task) = with_host(|h| h.next_microtask()) {
9553            task.run()?;
9554        }
9555        check_unhandled_rejections()?;
9556
9557        // 2) Liveness (Node's handle count). Nothing referenced left to do ⇒ the
9558        //    process exits, dropping any unref'd timers still pending — which is
9559        //    why `setTimeout(fn, 1000).unref()` never fires, while an unref'd
9560        //    timer behind a ref'd one does.
9561        let alive =
9562            with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
9563        if !alive {
9564            break;
9565        }
9566
9567        // 3) Pick the clock regime for this turn.
9568        let virtual_clock = with_host(|h| h.open_handles() == 0 && !h.has_pending_interval());
9569        if virtual_clock {
9570            // ── virtual-clock regime (unchanged for one-shot timers) ─────────
9571            match with_host(|h| h.pop_next_timer()) {
9572                Some(t) => fire_timer(t)?,
9573                // Unreachable while `alive` holds (a ref'd timer must exist),
9574                // but exiting is the safe reading of "nothing left to run".
9575                None => break,
9576            }
9577            continue;
9578        }
9579
9580        // ── real-clock / blocking-I/O regime ─────────────────────────────────
9581        let now = Instant::now();
9582        if let Some(t) = with_host(|h| h.pop_due_timer(now)) {
9583            fire_timer(t)?;
9584            continue; // re-drain microtasks, re-check deadlines
9585        }
9586        // Nothing due and no pending microtasks: block for the next I/O event,
9587        // bounded by the soonest timer deadline so due timers still fire on time.
9588        let rx = rx.expect("blocking-I/O regime requires the I/O receiver");
9589        let timeout = with_host(|h| h.next_timer_timeout(now));
9590        let recv = match timeout {
9591            Some(d) => rx.recv_timeout(d),
9592            None => rx
9593                .recv()
9594                .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected),
9595        };
9596        match recv {
9597            Ok(task) => task()?,
9598            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} // a timer is now due
9599            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, // no senders left
9600        }
9601    }
9602    Ok(())
9603}
9604
9605/// Run one due timer's callback, first re-arming it if it repeats.
9606///
9607/// The re-arm happens BEFORE the callback runs so that a `clearInterval(t)`
9608/// issued from inside that callback cancels the next occurrence. Re-arming
9609/// afterwards would leave the interval absent from the queue for the duration of
9610/// its own callback, so the `clear` would match nothing and the freshly pushed
9611/// entry would resurrect an interval the program had just stopped.
9612fn fire_timer(t: Timer) -> Result<(), String> {
9613    if let Some(period) = t.interval {
9614        with_host(|h| h.rearm_timer(&t, period));
9615    }
9616    invoke(&t.callback, t.args, None)?;
9617    Ok(())
9618}
9619
9620// ── async functions & promise resolution (native) ────────────────────────────
9621
9622/// Drive a freshly-built async coroutine and return its result promise.
9623fn run_async(gen: Value) -> Value {
9624    let result = with_host(|h| h.new_promise());
9625    let rid = with_host(|h| h.promise_id(&result).unwrap());
9626    drive_async(gen, rid, Value::Undef);
9627    result
9628}
9629
9630/// Resume an async coroutine one step, wiring `await` continuations to promise
9631/// settlement.
9632fn drive_async(gen: Value, rid: u32, send: Value) {
9633    match gen_resume(&gen, send) {
9634        Ok(GenStep::Yield(awaited)) => {
9635            let ap = promise_of(&awaited);
9636            let aid = with_host(|h| h.promise_id(&ap).unwrap());
9637            let gen2 = gen.clone();
9638            subscribe_native(
9639                aid,
9640                Box::new(move |state, val| {
9641                    // Resume the coroutine with a `[tag, value]` packet the AWAIT
9642                    // op unwraps (tag 1 ⇒ the awaited promise rejected → throw).
9643                    let tag = if state == PromiseState::Rejected {
9644                        1.0
9645                    } else {
9646                        0.0
9647                    };
9648                    let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9649                    drive_async(gen2, rid, packet);
9650                    Ok(())
9651                }),
9652            );
9653        }
9654        Ok(GenStep::Done(v)) => resolve_promise_val(rid, v),
9655        Err(e) => {
9656            let ev = take_exc_or_error(&e);
9657            reject_promise_val(rid, ev);
9658        }
9659    }
9660}
9661
9662/// The AWAIT op body (runs inside the async coroutine): suspend, yielding the
9663/// awaited value; on resume, unwrap the settlement packet (throwing on reject).
9664pub fn await_value(awaited: Value) -> Result<Value, String> {
9665    // Inside an `async function*`, `await` and `yield` share one coroutine
9666    // yielder, so an awaited value has to be tagged or the driver would hand it
9667    // to the consumer as if the body had yielded it.
9668    let awaited = match CUR_GEN.with(|c| c.get()) {
9669        Some(id) if with_host(|h| h.generators[id as usize].async_gen) => with_host(|h| {
9670            let mut m = IndexMap::new();
9671            m.insert(AWAIT_MARKER.to_string(), awaited);
9672            h.new_object(m)
9673        }),
9674        _ => awaited,
9675    };
9676    let packet = gen_yield(awaited)?;
9677    let items = with_host(|h| h.iter_vec(&packet)).unwrap_or_default();
9678    let tag = items
9679        .first()
9680        .map(|v| with_host(|h| h.to_number(v)))
9681        .unwrap_or(0.0);
9682    let val = items.get(1).cloned().unwrap_or(Value::Undef);
9683    if tag == 1.0 {
9684        with_host(|h| h.exc = Some(val.clone()));
9685        Err(with_host(|h| crate::builtins::error_string(h, &val)))
9686    } else {
9687        Ok(val)
9688    }
9689}
9690
9691/// Hidden key marking an `await` suspension inside an async generator.
9692const AWAIT_MARKER: &str = "@@await";
9693
9694/// The operand of an `await` suspension, or `None` for a real `yield`.
9695fn await_marker(v: &Value) -> Option<Value> {
9696    with_host(|h| match h.get(v) {
9697        Some(JsObj::Object(props)) if props.len() == 1 => props.get(AWAIT_MARKER).cloned(),
9698        _ => None,
9699    })
9700}
9701
9702/// `AsyncGeneratorEnqueue` — queue one request against an `async function*` and
9703/// hand back the promise its `{value, done}` record (or rejection) will settle.
9704///
9705/// All three of `.next`, `.return` and `.throw` come through here, so a request
9706/// never resumes the body while an earlier one is still suspended on an
9707/// internal `await`.
9708pub fn async_gen_enqueue(gen: &Value, req: GenReq) -> Value {
9709    let step = with_host(|h| h.new_promise());
9710    let sid = with_host(|h| h.promise_id(&step).unwrap());
9711    let id = match with_host(|h| match h.get(gen) {
9712        Some(JsObj::Generator { id }) => Some(*id),
9713        _ => None,
9714    }) {
9715        Some(id) => id,
9716        None => return step,
9717    };
9718    with_host(|h| h.generators[id as usize].queue.push_back((req, sid)));
9719    pump_async_gen(gen.clone(), id);
9720    step
9721}
9722
9723/// One `.next(v)` of an `async function*`.
9724pub fn async_gen_step(gen: &Value, send: Value) -> Value {
9725    async_gen_enqueue(gen, GenReq::Next(send))
9726}
9727
9728/// `AsyncGeneratorResumeNext`: start the oldest queued request, unless one is
9729/// already in flight (the body may only be resumed by one request at a time).
9730fn pump_async_gen(gen: Value, id: u32) {
9731    if with_host(|h| h.generators[id as usize].running) {
9732        return;
9733    }
9734    let Some((req, sid)) = with_host(|h| h.generators[id as usize].queue.pop_front()) else {
9735        return;
9736    };
9737    with_host(|h| h.generators[id as usize].running = true);
9738    start_async_gen_req(gen, sid, req);
9739}
9740
9741/// Begin one queued request: resume the body with the completion it carries,
9742/// then hand the outcome to the shared continuation.
9743///
9744/// A RETURN completion always Awaits its value before the body sees it — via
9745/// `AsyncGeneratorUnwrapYieldResumption` (ECMA-262 27.6.3.7) when the generator
9746/// is suspended at a `yield`, and via `AsyncGeneratorAwaitReturn` (27.6.3.9)
9747/// when it is not yet started or already completed. So a `.return()` settles one
9748/// microtask after a `.next()` or `.throw()` issued in its place would, and the
9749/// `finally` it unwinds through runs a tick later too. Skipping that tick lets a
9750/// `.return()` overtake the reactions of the `.next()` it followed.
9751fn start_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9752    if matches!(req, GenReq::Return(_)) {
9753        with_host(|h| {
9754            h.queue_micro_native(Box::new(move || {
9755                resume_async_gen_req(gen, sid, req);
9756                Ok(())
9757            }))
9758        });
9759        return;
9760    }
9761    resume_async_gen_req(gen, sid, req);
9762}
9763
9764/// Deliver a queued completion to the body and settle its step promise.
9765fn resume_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9766    let step = match req {
9767        GenReq::Next(v) => gen_resume(&gen, v),
9768        GenReq::Return(v) => gen_return(&gen, v),
9769        GenReq::Throw(e) => gen_throw(&gen, e),
9770    };
9771    settle_async_gen_step(gen, sid, step);
9772}
9773
9774/// One request has settled: release the body and start the next queued request.
9775fn finish_async_gen_step(gen: Value, id: u32) {
9776    with_host(|h| h.generators[id as usize].running = false);
9777    pump_async_gen(gen, id);
9778}
9779
9780/// Whether `v` is an `async function*` object (its `.next()` yields promises).
9781pub fn is_async_generator(v: &Value) -> bool {
9782    let id = match with_host(|h| match h.get(v) {
9783        Some(JsObj::Generator { id }) => Some(*id),
9784        _ => None,
9785    }) {
9786        Some(id) => id,
9787        None => return false,
9788    };
9789    with_host(|h| h.generators[id as usize].async_gen)
9790}
9791
9792/// A `{ value, done }` iterator-result object.
9793fn iter_record(value: Value, done: bool) -> Value {
9794    with_host(|h| {
9795        let mut m = IndexMap::new();
9796        m.insert("value".to_string(), value);
9797        m.insert("done".to_string(), Value::Bool(done));
9798        h.new_object(m)
9799    })
9800}
9801
9802/// Resume a request that was suspended on an internal `await` (always a normal
9803/// completion — the awaited promise's outcome rides in `packet`).
9804fn drive_async_gen(gen: Value, sid: u32, packet: Value) {
9805    let step = gen_resume(&gen, packet);
9806    settle_async_gen_step(gen, sid, step);
9807}
9808
9809/// Turn one body resumption into a settled step promise: transparently re-drive
9810/// internal `await` suspensions, and settle on the first REAL yield or on the
9811/// body's completion. Shared by the initial resume of a queued request and by
9812/// every await-resumption of it.
9813fn settle_async_gen_step(gen: Value, sid: u32, step: Result<GenStep, String>) {
9814    let id = match with_host(|h| match h.get(&gen) {
9815        Some(JsObj::Generator { id }) => Some(*id),
9816        _ => None,
9817    }) {
9818        Some(id) => id,
9819        None => return,
9820    };
9821    match step {
9822        Ok(GenStep::Yield(v)) => match await_marker(&v) {
9823            Some(awaited) => {
9824                // An internal `await`: settle it, then resume the body. The
9825                // request stays in flight across the suspension.
9826                let ap = promise_of(&awaited);
9827                let aid = with_host(|h| h.promise_id(&ap).unwrap());
9828                subscribe_native(
9829                    aid,
9830                    Box::new(move |state, val| {
9831                        let tag = if state == PromiseState::Rejected {
9832                            1.0
9833                        } else {
9834                            0.0
9835                        };
9836                        let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9837                        drive_async_gen(gen.clone(), sid, packet);
9838                        Ok(())
9839                    }),
9840                );
9841            }
9842            // ECMA-262 27.6.3.8 AsyncGeneratorYield step 5: the yielded value is
9843            // AWAITED before the step promise settles, so `yield somePromise`
9844            // hands the consumer the RESOLVED value (and costs its microtask).
9845            None => {
9846                let yp = promise_of(&v);
9847                let yid = with_host(|h| h.promise_id(&yp).unwrap());
9848                subscribe_native(
9849                    yid,
9850                    Box::new(move |state, val| {
9851                        if state == PromiseState::Rejected {
9852                            reject_promise_val(sid, val);
9853                        } else {
9854                            resolve_promise_val(sid, iter_record(val, false));
9855                        }
9856                        finish_async_gen_step(gen.clone(), id);
9857                        Ok(())
9858                    }),
9859                );
9860            }
9861        },
9862        Ok(GenStep::Done(v)) => {
9863            resolve_promise_val(sid, iter_record(v, true));
9864            finish_async_gen_step(gen, id);
9865        }
9866        Err(e) => {
9867            let ev = take_exc_or_error(&e);
9868            reject_promise_val(sid, ev);
9869            finish_async_gen_step(gen, id);
9870        }
9871    }
9872}
9873
9874/// A promise for `v`: `v` itself if it is already a promise, else a promise
9875/// resolved with `v`.
9876pub fn promise_of(v: &Value) -> Value {
9877    if with_host(|h| h.promise_id(v)).is_some() {
9878        return v.clone();
9879    }
9880    let p = with_host(|h| h.new_promise());
9881    let id = with_host(|h| h.promise_id(&p).unwrap());
9882    resolve_promise_val(id, v.clone());
9883    p
9884}
9885
9886/// Register a native reaction on promise `id` (schedules immediately if already
9887/// settled).
9888pub fn subscribe_native(id: u32, f: Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>) {
9889    // A native continuation (`await`, promise adoption, `for await`) observes a
9890    // rejection exactly as a `.catch` does, so it is not "unhandled".
9891    with_host(|h| h.promise_mark_handled(id));
9892    let state = with_host(|h| h.promise_state(id));
9893    if state == PromiseState::Pending {
9894        with_host(|h| h.add_reaction(id, PromiseReaction::Native(f)));
9895    } else {
9896        let val = with_host(|h| h.promise_value(id));
9897        with_host(|h| h.queue_micro_native(Box::new(move || f(state, val))));
9898    }
9899}
9900
9901/// The Promise "resolve" operation: adopt `value`'s state if it is a promise,
9902/// else fulfill with it.
9903pub fn resolve_promise_val(id: u32, value: Value) {
9904    if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
9905        return;
9906    }
9907    if let Some(vid) = with_host(|h| h.promise_id(&value)) {
9908        if vid == id {
9909            // Resolving a promise with itself → reject with a TypeError.
9910            let e = with_host(|h| {
9911                crate::builtins::synth_error(h, "TypeError: Chaining cycle detected")
9912            });
9913            reject_promise_val(id, e);
9914            return;
9915        }
9916        // A native promise is still a thenable, so the spec routes it through
9917        // `NewPromiseResolveThenableJob` too — one microtask before the adoption
9918        // is even registered. (`await` does NOT pay this: V8's await optimization
9919        // subscribes to a native promise directly, which `await_value` mirrors.)
9920        with_host(|h| {
9921            h.queue_micro_native(Box::new(move || {
9922                subscribe_native(
9923                    vid,
9924                    Box::new(move |state, val| {
9925                        with_host(|h| h.settle_promise(id, state, val.clone()));
9926                        schedule_reactions(id);
9927                        Ok(())
9928                    }),
9929                );
9930                Ok(())
9931            }))
9932        });
9933        return;
9934    }
9935    // ECMA-262 27.2.1.3.2: any OBJECT carrying a callable `then` is assimilated
9936    // through a dedicated job — the promise adopts what `then` reports, it is
9937    // never fulfilled WITH the thenable itself.
9938    if let Some(then) = thenable_then(&value) {
9939        with_host(|h| {
9940            h.queue_micro_native(Box::new(move || resolve_thenable_job(id, value, then)))
9941        });
9942        return;
9943    }
9944    with_host(|h| h.settle_promise(id, PromiseState::Fulfilled, value));
9945    schedule_reactions(id);
9946}
9947
9948/// `value.then` if `value` is an object with a callable `then` — the test that
9949/// makes a value a *thenable*. Primitives (and objects without one) are `None`.
9950fn thenable_then(value: &Value) -> Option<Value> {
9951    // A PROXY is not a plain object and supplies `then` through its `get` trap,
9952    // so both tests below missed it: `Promise.resolve(proxyThenable)` fulfilled
9953    // WITH the proxy instead of adopting it.
9954    if with_host(|h| h.kind_of(value)) == Some(ObjKind::Proxy) {
9955        return protocol_lookup(value, "then")
9956            .ok()
9957            .flatten()
9958            .filter(|f| with_host(|h| is_callable(h, f)));
9959    }
9960    if !with_host(|h| matches!(h.get(value), Some(JsObj::Object(_)))) {
9961        return None;
9962    }
9963    let then = with_host(|h| lookup_chain(h, value, "then"))?;
9964    with_host(|h| is_callable(h, &then)).then_some(then)
9965}
9966
9967/// `NewPromiseResolveThenableJob`: hand the thenable this promise's own resolve /
9968/// reject continuations and let it settle us. A throw out of `then` rejects.
9969fn resolve_thenable_job(id: u32, thenable: Value, then: Value) -> Result<(), String> {
9970    let res = with_host(|h| h.alloc(JsObj::Builtin(format!("@@presolve:{id}"))));
9971    let rej = with_host(|h| h.alloc(JsObj::Builtin(format!("@@preject:{id}"))));
9972    if let Err(e) = invoke(&then, vec![res, rej], Some(thenable)) {
9973        let ev = take_exc_or_error(&e);
9974        reject_promise_val(id, ev);
9975    }
9976    Ok(())
9977}
9978
9979pub fn reject_promise_val(id: u32, value: Value) {
9980    if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
9981        return;
9982    }
9983    with_host(|h| {
9984        h.settle_promise(id, PromiseState::Rejected, value);
9985        h.pending_rejections.push(id);
9986    });
9987    schedule_reactions(id);
9988}
9989
9990/// Report every promise that settled rejected since the last checkpoint and
9991/// still has no handler. Node's default is `--unhandled-rejections=throw`: the
9992/// rejection becomes an uncaught exception (stderr + exit 1) unless a
9993/// `process.on('unhandledRejection')` listener takes it.
9994fn check_unhandled_rejections() -> Result<(), String> {
9995    loop {
9996        let ids: Vec<u32> = with_host(|h| std::mem::take(&mut h.pending_rejections));
9997        if ids.is_empty() {
9998            return Ok(());
9999        }
10000        for id in ids {
10001            let unhandled = with_host(|h| {
10002                h.promise_state(id) == PromiseState::Rejected && !h.promises[id as usize].handled
10003            });
10004            if !unhandled {
10005                continue;
10006            }
10007            // Report each promise at most once, however many checkpoints pass.
10008            with_host(|h| h.promise_mark_handled(id));
10009            let val = with_host(|h| h.promise_value(id));
10010            let listeners = with_host(|h| h.take_process_listeners("unhandledRejection"));
10011            if listeners.is_empty() {
10012                let msg = with_host(|h| crate::builtins::error_string(h, &val));
10013                with_host(|h| h.exc = Some(val));
10014                return Err(msg);
10015            }
10016            let promise = with_host(|h| h.alloc(JsObj::Promise { id }));
10017            for f in listeners {
10018                invoke(&f, vec![val.clone(), promise.clone()], None)?;
10019            }
10020        }
10021    }
10022}
10023
10024/// Drain a settled promise's reactions into microtasks.
10025fn schedule_reactions(id: u32) {
10026    let reactions = with_host(|h| h.take_reactions(id));
10027    let state = with_host(|h| h.promise_state(id));
10028    let value = with_host(|h| h.promise_value(id));
10029    for r in reactions {
10030        let value = value.clone();
10031        match r {
10032            PromiseReaction::Native(f) => {
10033                with_host(|h| h.queue_micro_native(Box::new(move || f(state, value))));
10034            }
10035            PromiseReaction::Js {
10036                on_ful,
10037                on_rej,
10038                result,
10039            } => {
10040                with_host(|h| {
10041                    h.queue_micro_native(Box::new(move || {
10042                        run_js_reaction(state, value, on_ful, on_rej, result)
10043                    }))
10044                });
10045            }
10046        }
10047    }
10048}
10049
10050/// Run a `.then` reaction: call the appropriate handler and settle the result
10051/// promise with its outcome (or pass through if there is no handler).
10052fn run_js_reaction(
10053    state: PromiseState,
10054    value: Value,
10055    on_ful: Value,
10056    on_rej: Value,
10057    result: Value,
10058) -> Result<(), String> {
10059    let rid = match with_host(|h| h.promise_id(&result)) {
10060        Some(i) => i,
10061        None => return Ok(()),
10062    };
10063    let handler = if state == PromiseState::Rejected {
10064        on_rej
10065    } else {
10066        on_ful
10067    };
10068    if with_host(|h| is_callable(h, &handler)) {
10069        match invoke(&handler, vec![value], None) {
10070            Ok(r) => resolve_promise_val(rid, r),
10071            Err(e) => reject_promise_val(rid, take_exc_or_error(&e)),
10072        }
10073    } else if state == PromiseState::Rejected {
10074        reject_promise_val(rid, value);
10075    } else {
10076        resolve_promise_val(rid, value);
10077    }
10078    Ok(())
10079}
10080
10081/// The JS value of a just-caught error: the live `exc` (a real thrown value) or a
10082/// synthesized `Error` from the internal message.
10083pub fn take_exc_or_error(e: &str) -> Value {
10084    with_host(|h| {
10085        h.error.take();
10086        h.exc
10087            .take()
10088            .unwrap_or_else(|| crate::builtins::synth_error(h, e))
10089    })
10090}
10091
10092/// Register a user `.then` reaction (JS handlers + result promise).
10093pub fn promise_then(p: &Value, on_ful: Value, on_rej: Value) -> Value {
10094    let id = match with_host(|h| h.promise_id(p)) {
10095        Some(i) => i,
10096        None => return Value::Undef,
10097    };
10098    with_host(|h| h.promise_mark_handled(id));
10099    // The result is built with the RECEIVER's species, so a subclass promise
10100    // stays a subclass promise through a `.then` chain.
10101    let result = match crate::builtins::promise_species_from(p) {
10102        Ok(Some(sp)) => sp,
10103        _ => with_host(|h| h.new_promise()),
10104    };
10105    let reaction = PromiseReaction::Js {
10106        on_ful,
10107        on_rej,
10108        result: result.clone(),
10109    };
10110    let state = with_host(|h| h.promise_state(id));
10111    if state == PromiseState::Pending {
10112        with_host(|h| h.add_reaction(id, reaction));
10113    } else {
10114        let value = with_host(|h| h.promise_value(id));
10115        if let PromiseReaction::Js {
10116            on_ful,
10117            on_rej,
10118            result,
10119        } = reaction
10120        {
10121            with_host(|h| {
10122                h.queue_micro_native(Box::new(move || {
10123                    run_js_reaction(state, value, on_ful, on_rej, result)
10124                }))
10125            });
10126        }
10127    }
10128    result
10129}
10130
10131/// The ReferenceError for touching `this` in a derived constructor before
10132/// `super()` — or returning from one without calling it.
10133pub fn this_before_super_error() -> String {
10134    "ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor".to_string()
10135}