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}
98
99/// `DEF_MEMBER` member-kind tags.
100pub mod member {
101    pub const METHOD: i64 = 0;
102    pub const GET: i64 = 1;
103    pub const SET: i64 = 2;
104}
105
106/// Bitwise/shift op tags carried by `ops::BINOP` (JS ToInt32/ToUint32 rules).
107pub mod binop {
108    pub const BITAND: i64 = 0;
109    pub const BITOR: i64 = 1;
110    pub const BITXOR: i64 = 2;
111    pub const SHL: i64 = 3;
112    pub const SHR: i64 = 4;
113    pub const USHR: i64 = 5;
114}
115
116/// Unary op tags carried by `ops::UNARY`.
117pub mod unop {
118    pub const POS: i64 = 0; // unary +
119    pub const BITNOT: i64 = 1; // ~
120}
121
122// ── heap objects ───────────────────────────────────────────────────────────
123
124/// A compiled function template: parameter shape + body chunk. Shared by every
125/// closure created from the same function/arrow.
126#[derive(Clone, serde::Serialize, serde::Deserialize)]
127pub struct FuncDef {
128    pub name: String,
129    /// Parameter binding templates (destructuring lowered by the compiler into
130    /// the body prologue; here we only track the simple arg slots).
131    pub params: Vec<ParamSlot>,
132    pub chunk: Chunk,
133    pub is_arrow: bool,
134    /// True for a `function*`/`*method`/generator arrow: calling it builds a
135    /// suspended generator instead of running the body.
136    pub is_generator: bool,
137    /// True for an `async` function/method/arrow: calling it drives a coroutine
138    /// and returns a Promise; `await` inside suspends via the same yielder.
139    pub is_async: bool,
140}
141
142/// One parameter slot. `name` is the simple bound name; a destructuring pattern
143/// is lowered to a synthetic `.arg{i}` name plus body prologue code.
144#[derive(Clone, serde::Serialize, serde::Deserialize)]
145pub struct ParamSlot {
146    pub name: String,
147    /// True for the `...rest` collector.
148    pub rest: bool,
149    /// True if this slot has a default expression (applied in the body prologue).
150    pub has_default: bool,
151}
152
153/// A compiled `try`/`catch`/`finally` block. Bodies are bare chunks run in the
154/// current scope.
155#[derive(Clone, serde::Serialize, serde::Deserialize)]
156pub struct TryDef {
157    pub block: Chunk,
158    /// `(catch_param_name, catch_body)`.
159    pub handler: Option<(Option<String>, Chunk)>,
160    pub finalizer: Option<Chunk>,
161}
162
163/// A live closure value.
164#[derive(Clone)]
165pub struct FuncVal {
166    pub def_id: usize,
167    /// Captured lexical environment (enclosing scope chain), for free vars.
168    pub env: Option<Env>,
169    /// `this` captured at definition time (arrow functions).
170    pub this: Option<Value>,
171    pub is_arrow: bool,
172    /// The owning class name for a method (drives `super` resolution). `None` for
173    /// plain functions/arrows.
174    pub home_class: Option<String>,
175}
176
177/// A heap object.
178#[derive(Clone)]
179pub enum JsObj {
180    Str(String),
181    Array(Vec<Value>),
182    Object(IndexMap<String, Value>),
183    Func(FuncVal),
184    /// A first-class reference to a builtin function or namespace
185    /// (`console.log`, `Math`, `parseInt`).
186    Builtin(String),
187    /// A bound method value (`obj.method` captured then called): dispatches
188    /// through `call_method(recv, name, args)` when invoked.
189    BoundMethod {
190        recv: Value,
191        name: String,
192    },
193    /// The single canonical `null`.
194    Null,
195    /// A live iterator over a sequence, with a cursor.
196    Iter {
197        items: Vec<Value>,
198        idx: usize,
199    },
200    /// A bound function (`fn.bind(thisArg, ...preargs)`).
201    BoundFunc {
202        target: Value,
203        this: Value,
204        args: Vec<Value>,
205    },
206    /// A class constructor value: the runtime object produced by a `class`.
207    Class(ClassVal),
208    /// A `Symbol` — a unique property key. `registered` marks a `Symbol.for`
209    /// key (shared) vs a fresh `Symbol()`.
210    Symbol {
211        desc: Option<String>,
212        id: u64,
213    },
214    /// A `Map` (or `WeakMap` when `weak`): insertion-ordered key→value entries.
215    Map {
216        entries: IndexMap<MapKey, (Value, Value)>,
217        weak: bool,
218    },
219    /// A `Set` (or `WeakSet` when `weak`): insertion-ordered unique values.
220    Set {
221        entries: IndexMap<MapKey, Value>,
222        weak: bool,
223    },
224    /// A live generator, backed by a stackful `corosensei` coroutine in
225    /// `JsHost.generators`.
226    Generator {
227        id: u32,
228    },
229    /// A Promise, backed by a `PromiseCell` in `JsHost.promises`.
230    Promise {
231        id: u32,
232    },
233    /// An arbitrary-precision `BigInt` (`typeof === "bigint"`).
234    BigInt(num_bigint::BigInt),
235    /// A compiled regular expression (`/pat/flags` or `new RegExp(...)`).
236    RegExp(Box<RegExpObj>),
237}
238
239/// A `RegExp` object: the compiled `fancy_regex::Regex` plus the JS-visible
240/// source, flag booleans, and the mutable `lastIndex` cursor (used by `g`/`y`
241/// matching). fancy-regex adds lookaround + backreferences on top of the Rust
242/// `regex` fast path, so the JS grammar node-js can accept is a near-superset.
243#[derive(Clone)]
244pub struct RegExpObj {
245    /// The translated regex. Construction of a pattern fancy-regex still cannot
246    /// express (documented in BUGS.md) throws at `RegExp` build time, so a live
247    /// `RegExpObj` always holds a compiled engine.
248    pub re: fancy_regex::Regex,
249    pub source: String,
250    pub flags: String,
251    pub global: bool,
252    pub ignore_case: bool,
253    pub multiline: bool,
254    pub dot_all: bool,
255    pub sticky: bool,
256    pub unicode: bool,
257    /// `lastIndex`, in UTF-16 code units-approximated-as-chars; advanced by
258    /// `exec`/`test` under the `g`/`y` flags.
259    pub last_index: usize,
260}
261
262/// A Promise's settled state and pending reactions.
263pub struct PromiseCell {
264    pub state: PromiseState,
265    pub value: Value,
266    /// Reactions registered while still pending; drained (as microtasks) on
267    /// settle.
268    pub reactions: Vec<PromiseReaction>,
269    /// True once a rejection has been observed by a handler (`.then`/`.catch`),
270    /// so the loop doesn't report it as unhandled.
271    pub handled: bool,
272}
273
274/// A pending Promise reaction: a user `.then` (JS handlers + a result promise) or
275/// a native continuation (Promise chaining / async `await` resumption).
276pub enum PromiseReaction {
277    Js {
278        on_ful: Value,
279        on_rej: Value,
280        result: Value,
281    },
282    Native(Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>),
283}
284
285#[derive(Default, Clone, Copy, PartialEq, Eq)]
286pub enum PromiseState {
287    #[default]
288    Pending,
289    Fulfilled,
290    Rejected,
291}
292
293/// A live class constructor. The prototype object (holding instance methods) and
294/// the static-side own properties live on the heap; `parent` is the superclass
295/// constructor value (`None` for a base class).
296#[derive(Clone)]
297pub struct ClassVal {
298    pub name: String,
299    /// The constructor function value (a `JsObj::Func`), or `None` for a class
300    /// with only a synthesized default constructor.
301    pub ctor: Option<Value>,
302    pub parent: Option<Value>,
303    /// `C.prototype` — the object instances delegate to.
304    pub proto: Value,
305    /// Static own properties (static methods/fields), plus `name`/`prototype`.
306    pub statics: IndexMap<String, Value>,
307    /// Instance field initializers: `(name, thunk_fn)`, run per-instance after
308    /// `super()` (or at construction start for a base class).
309    pub fields: Vec<(String, Value)>,
310}
311
312/// The result of resolving `super.name`: a getter to invoke (accessor property)
313/// or a directly-usable value (method / data property).
314pub enum SuperRef {
315    Getter(Value),
316    Data(Value),
317}
318
319/// A `Map`/`Set` key under SameValueZero: `NaN` collapses to one key, `-0` and
320/// `+0` are the same key, primitives compare by value, objects by heap identity.
321#[derive(Clone, PartialEq, Eq, Hash)]
322pub enum MapKey {
323    Undef,
324    Null,
325    Bool(bool),
326    /// f64 bit pattern with `NaN` canonicalized and `-0` normalized to `+0`.
327    Num(u64),
328    /// A `BigInt` key, by its decimal string (SameValueZero: `1n` is one key).
329    Big(String),
330    Str(String),
331    /// Heap identity (objects, arrays, functions, symbols).
332    Ref(u32),
333}
334
335// ── environments ─────────────────────────────────────────────────────────────
336
337/// A local-variable environment, shared (by `Rc`) between a frame and any nested
338/// function that captures it.
339pub struct EnvData {
340    pub vars: IndexMap<String, Value>,
341    pub parent: Option<Env>,
342}
343pub type Env = Rc<RefCell<EnvData>>;
344
345/// An accessor property: `(getter, setter)`, either optional.
346pub type Accessor = (Option<Value>, Option<Value>);
347
348fn new_env(parent: Option<Env>) -> Env {
349    Rc::new(RefCell::new(EnvData {
350        vars: IndexMap::new(),
351        parent,
352    }))
353}
354
355/// One function activation.
356pub struct Frame {
357    pub env: Env,
358    pub this_obj: Option<Value>,
359    /// `new.target` for this activation (the constructor when invoked via `new`).
360    pub new_target: Option<Value>,
361    /// The class value owning the running method (drives `super`); `None` outside
362    /// a class method/constructor.
363    pub home_class: Option<Value>,
364    /// Source line the frame is currently executing (updated by the DAP line hook
365    /// under `--dap`; stays 0 on ordinary runs).
366    pub line: u32,
367    /// The function name that owns this frame, for the DAP `stackTrace`; `None`
368    /// for the module frame and anonymous activations.
369    pub owner: Option<String>,
370}
371
372/// A non-local control signal.
373#[derive(Clone)]
374pub enum Signal {
375    Return(Value),
376    Break,
377    Continue,
378}
379
380/// The JavaScript runtime.
381pub struct JsHost {
382    heap: Vec<JsObj>,
383    /// Function templates, indexed by def id.
384    pub funcs: Vec<FuncDef>,
385    /// try/catch/finally block templates, indexed by try id.
386    pub tries: Vec<TryDef>,
387    /// Module-level (global) names.
388    globals: IndexMap<String, Value>,
389    /// The frame stack (bottom = module).
390    frames: Vec<Frame>,
391    pub error: Option<String>,
392    /// The in-flight thrown value, if any (JS `throw`).
393    pub exc: Option<Value>,
394    pub signal: Option<Signal>,
395    /// The canonical `null` handle (allocated once).
396    null_val: Value,
397    /// `[[Prototype]]` link per heap object, by heap index. Absent = default
398    /// (`Object.prototype` for objects, `null` for the root).
399    protos: HashMap<u32, Value>,
400    /// Heap objects whose `[[Prototype]]` is *explicitly* null — via
401    /// `Object.create(null)` or `Object.setPrototypeOf(o, null)`. Distinct from a
402    /// bare `{}` (absent from `protos` but conceptually `Object.prototype`), which
403    /// is why `Object.create(null) instanceof Object` can read `false`.
404    null_proto_objs: HashSet<u32>,
405    /// Own properties of function objects (functions are objects in JS): a live
406    /// closure's `name`/`prototype`/static-ish members. Keyed by heap index.
407    fn_props: HashMap<u32, IndexMap<String, Value>>,
408    /// Accessor (getter/setter) properties per owning object, by heap index then
409    /// key: `(get, set)`. Class `get x()`/`set x()` install here on the prototype.
410    accessors: HashMap<u32, IndexMap<String, Accessor>>,
411    /// User-assigned static properties on a builtin namespace/constructor, keyed
412    /// by namespace name then property (`Error` → `prepareStackTrace`,
413    /// `stackTraceLimit`). Each bare `Error` reference allocates a fresh
414    /// `Builtin` handle, so these cannot live in `fn_props` (which is per-heap-
415    /// index); this stable side table lets `Error.prepareStackTrace = fn` persist.
416    builtin_statics: HashMap<String, IndexMap<String, Value>>,
417    /// The shared well-known `Object.prototype` object (chain root for objects).
418    object_proto: Value,
419    /// Class name of each class `prototype` object, by heap index — lets an
420    /// instance recover its constructor name (for `util.inspect` prefix and
421    /// `obj.constructor.name`).
422    proto_class: HashMap<u32, Value>,
423    /// Class constructor values by name, so a running method's `home_class` name
424    /// resolves to its class value (for `super`).
425    class_registry: HashMap<String, Value>,
426    /// Well-known prototype objects for the builtin error constructors, by name.
427    error_protos: HashMap<String, Value>,
428    /// `Symbol.for` registry: description → symbol value.
429    symbol_registry: HashMap<String, Value>,
430    /// Monotonic id source for fresh `Symbol()` values.
431    next_symbol: u64,
432    /// Suspended generator coroutines, indexed by `JsObj::Generator.id`.
433    generators: Vec<GenCell>,
434    /// Promise cells, indexed by `JsObj::Promise.id`.
435    promises: Vec<PromiseCell>,
436    /// `process.nextTick` callbacks (drained before promise microtasks).
437    pub nextticks: std::collections::VecDeque<Task>,
438    /// Promise-reaction / `queueMicrotask` microtasks.
439    pub microtasks: std::collections::VecDeque<Task>,
440    /// `setTimeout`/`setInterval`/`setImmediate` macrotasks.
441    pub macrotasks: Vec<Timer>,
442    /// Monotonic timer-id source.
443    next_timer: u64,
444    /// Cloned by I/O worker threads to post `IoTask`s back to the main-thread
445    /// event loop. Kept alive for the host's lifetime so the loop's `recv` never
446    /// sees a spurious `Disconnected` while a server is running.
447    io_tx: Sender<IoTask>,
448    /// Owned by the event loop (taken out for the blocking `recv`). Receives the
449    /// `IoTask`s posted by I/O threads.
450    io_rx: Option<Receiver<IoTask>>,
451    /// Ref-count of "things keeping the process alive": open listeners, live
452    /// sockets, ref'd handles. The loop exits only when this is `0` AND both task
453    /// queues are empty. A pure script never touches it, so it exits exactly as
454    /// before.
455    open_handles: usize,
456}
457
458/// A queued unit of work: either a JS callback invocation (`queueMicrotask`,
459/// `nextTick`, timer body) or a native step (Promise reaction / async resume).
460pub enum Task {
461    Js { cb: Value, args: Vec<Value> },
462    Native(Box<dyn FnOnce() -> Result<(), String>>),
463}
464
465impl Task {
466    fn run(self) -> Result<(), String> {
467        match self {
468            Task::Js { cb, args } => invoke(&cb, args, None).map(|_| ()),
469            Task::Native(f) => f(),
470        }
471    }
472}
473
474/// A scheduled macrotask (`setTimeout`/`setImmediate`). Ordering is by `(delay,
475/// seq)` — a deterministic virtual clock, never wall time.
476pub struct Timer {
477    pub id: u64,
478    pub delay: f64,
479    pub seq: u64,
480    pub callback: Value,
481    pub args: Vec<Value>,
482    pub cancelled: bool,
483    /// Real wall-clock deadline (`now + delay`), used only on the blocking I/O
484    /// path (`open_handles > 0`). With no open handles the loop stays on the
485    /// deterministic virtual clock and ignores this.
486    pub deadline: Instant,
487}
488
489/// One suspended generator. `coro` is `None` only while actively running (taken
490/// out across `Coroutine::resume`); `ctx` holds its volatile execution context
491/// (frames/signal/error/exc) while suspended.
492struct GenCell {
493    coro: Option<corosensei::Coroutine<Value, Value, Result<Value, String>>>,
494    /// Raw pointer to the coroutine body's `Yielder`, published on entry (same
495    /// thread → valid for the body's life). Read by `yield` to suspend.
496    yielder: *const (),
497    ctx: GenContext,
498    done: bool,
499    /// True once the body has been resumed at least once (so it is suspended at a
500    /// `yield`). `.return()`/`.throw()` only unwind a *started* generator.
501    started: bool,
502    /// A completion injected by `.return(v)` / `.throw(e)`: consumed by the next
503    /// `yield` resume so the body unwinds (running any pending `finally`).
504    inject: Option<GenInject>,
505}
506
507/// A forced completion pushed into a suspended generator by `.return()`/`.throw()`.
508enum GenInject {
509    Return(Value),
510    Throw(Value),
511}
512
513/// The mutable "execution registers" swapped at every generator resume/suspend
514/// boundary so a suspended generator's half-finished frame/signal state never
515/// leaks into the resuming caller. The heap, function/class tables and globals
516/// are shared and never swapped.
517#[derive(Default)]
518struct GenContext {
519    frames: Vec<Frame>,
520    error: Option<String>,
521    exc: Option<Value>,
522    signal: Option<Signal>,
523}
524
525thread_local! {
526    /// Id of the generator whose body is currently executing, or `None` at the
527    /// root. `yield` suspends this generator.
528    static CUR_GEN: std::cell::Cell<Option<u32>> = const { std::cell::Cell::new(None) };
529}
530
531thread_local! {
532    static HOST: RefCell<JsHost> = RefCell::new(JsHost::new());
533}
534
535/// Run `f` with mutable access to the thread-local host.
536pub fn with_host<R>(f: impl FnOnce(&mut JsHost) -> R) -> R {
537    HOST.with(|h| f(&mut h.borrow_mut()))
538}
539
540/// Reset the host to a clean slate (fresh module frame).
541pub fn reset_host() {
542    with_host(|h| *h = JsHost::new());
543    // Drop any cached module handles / factory closure — they index the old heap.
544    crate::module::reset();
545}
546
547impl Default for JsHost {
548    fn default() -> Self {
549        Self::new()
550    }
551}
552
553impl JsHost {
554    pub fn new() -> JsHost {
555        let module_env = new_env(None);
556        let (io_tx, io_rx) = std::sync::mpsc::channel();
557        let mut h = JsHost {
558            heap: Vec::new(),
559            funcs: Vec::new(),
560            tries: Vec::new(),
561            globals: IndexMap::new(),
562            frames: vec![Frame {
563                env: module_env,
564                this_obj: None,
565                new_target: None,
566                home_class: None,
567                line: 0,
568                owner: None,
569            }],
570            error: None,
571            exc: None,
572            signal: None,
573            null_val: Value::Undef,
574            protos: HashMap::new(),
575            null_proto_objs: HashSet::new(),
576            fn_props: HashMap::new(),
577            accessors: HashMap::new(),
578            builtin_statics: HashMap::new(),
579            object_proto: Value::Undef,
580            proto_class: HashMap::new(),
581            class_registry: HashMap::new(),
582            error_protos: HashMap::new(),
583            symbol_registry: HashMap::new(),
584            next_symbol: 1,
585            generators: Vec::new(),
586            promises: Vec::new(),
587            microtasks: std::collections::VecDeque::new(),
588            nextticks: std::collections::VecDeque::new(),
589            macrotasks: Vec::new(),
590            next_timer: 1,
591            io_tx,
592            io_rx: Some(io_rx),
593            open_handles: 0,
594        };
595        h.null_val = h.alloc(JsObj::Null);
596        // `Object.prototype`: the chain root, its own `[[Prototype]]` is null.
597        h.object_proto = h.new_object(IndexMap::new());
598        h
599    }
600
601    // ── prototype chain ──────────────────────────────────────────────────
602    /// The `[[Prototype]]` of a heap value, if explicitly linked.
603    pub fn proto_of(&self, v: &Value) -> Option<Value> {
604        if let Value::Obj(i) = v {
605            self.protos.get(i).cloned()
606        } else {
607            None
608        }
609    }
610    /// Set `v`'s `[[Prototype]]` to `proto`. Null links the object as an explicit
611    /// null-prototype object (recorded so `instanceof Object` reads false);
612    /// undefined just clears any link without the null marker.
613    pub fn set_proto(&mut self, v: &Value, proto: Value) {
614        if let Value::Obj(i) = v {
615            if self.is_null(&proto) {
616                self.protos.remove(i);
617                self.null_proto_objs.insert(*i);
618            } else if matches!(proto, Value::Undef) {
619                self.protos.remove(i);
620            } else {
621                self.protos.insert(*i, proto);
622                self.null_proto_objs.remove(i);
623            }
624        }
625    }
626    /// Whether `v`'s `[[Prototype]]` was explicitly set to null.
627    pub fn has_null_proto(&self, v: &Value) -> bool {
628        matches!(v, Value::Obj(i) if self.null_proto_objs.contains(i))
629    }
630    pub fn object_proto(&self) -> Value {
631        self.object_proto.clone()
632    }
633    /// Record that the prototype object `proto` belongs to the class constructor
634    /// `class_val` (so instances can recover their constructor).
635    pub fn tag_proto_class(&mut self, proto: &Value, class_val: Value) {
636        if let Value::Obj(i) = proto {
637            self.proto_class.insert(*i, class_val);
638        }
639    }
640    /// The class constructor value nearest in `obj`'s prototype chain, if any.
641    pub fn class_of(&self, obj: &Value) -> Option<Value> {
642        let mut cur = self.proto_of(obj);
643        while let Some(p) = cur {
644            if let Value::Obj(i) = &p {
645                if let Some(c) = self.proto_class.get(i) {
646                    return Some(c.clone());
647                }
648            }
649            cur = self.proto_of(&p);
650        }
651        None
652    }
653    /// The constructor display name of `obj` for `util.inspect` (empty ⇒ plain
654    /// object, no prefix).
655    pub fn ctor_name(&self, obj: &Value) -> String {
656        match self.class_of(obj) {
657            Some(c) => match self.get(&c) {
658                Some(JsObj::Class(cv)) => cv.name.clone(),
659                _ => String::new(),
660            },
661            None => String::new(),
662        }
663    }
664
665    /// A function's own-property table (created on demand).
666    pub fn fn_prop(&self, v: &Value, name: &str) -> Option<Value> {
667        if let Value::Obj(i) = v {
668            self.fn_props.get(i).and_then(|m| m.get(name).cloned())
669        } else {
670            None
671        }
672    }
673
674    /// A class static member, inherited down the constructor chain: a subclass
675    /// sees its superclass's `static` methods/fields (`Sub.create` → `Base.create`).
676    pub fn class_static(&self, class_val: &Value, name: &str) -> Option<Value> {
677        let mut cur = class_val.clone();
678        loop {
679            if let Some(v) = self.fn_prop(&cur, name) {
680                return Some(v);
681            }
682            match self.get(&cur) {
683                Some(JsObj::Class(c)) => cur = c.parent.clone()?,
684                _ => return None,
685            }
686        }
687    }
688    pub fn set_fn_prop(&mut self, v: &Value, name: &str, val: Value) {
689        if let Value::Obj(i) = v {
690            self.fn_props
691                .entry(*i)
692                .or_default()
693                .insert(name.to_string(), val);
694        }
695    }
696    /// A user-assigned static on a builtin namespace (`Error.prepareStackTrace`).
697    pub fn builtin_static(&self, ns: &str, name: &str) -> Option<Value> {
698        self.builtin_statics
699            .get(ns)
700            .and_then(|m| m.get(name).cloned())
701    }
702    /// Assign a static on a builtin namespace (persists across fresh `Builtin`
703    /// handles for the same namespace).
704    pub fn set_builtin_static(&mut self, ns: &str, name: &str, val: Value) {
705        self.builtin_statics
706            .entry(ns.to_string())
707            .or_default()
708            .insert(name.to_string(), val);
709    }
710    pub fn fn_prop_keys(&self, v: &Value) -> Vec<String> {
711        if let Value::Obj(i) = v {
712            self.fn_props
713                .get(i)
714                .map(|m| m.keys().cloned().collect())
715                .unwrap_or_default()
716        } else {
717            Vec::new()
718        }
719    }
720
721    /// Install an accessor `(get, set)` for `key` on the object `owner`.
722    pub fn set_accessor(
723        &mut self,
724        owner: &Value,
725        key: &str,
726        get: Option<Value>,
727        set: Option<Value>,
728    ) {
729        if let Value::Obj(i) = owner {
730            let slot = self
731                .accessors
732                .entry(*i)
733                .or_default()
734                .entry(key.to_string())
735                .or_insert((None, None));
736            if get.is_some() {
737                slot.0 = get;
738            }
739            if set.is_some() {
740                slot.1 = set;
741            }
742        }
743    }
744    /// The accessor `(get, set)` for `key` directly on `owner` (no chain walk).
745    pub fn own_accessor(&self, owner: &Value, key: &str) -> Option<(Option<Value>, Option<Value>)> {
746        if let Value::Obj(i) = owner {
747            self.accessors.get(i).and_then(|m| m.get(key).cloned())
748        } else {
749            None
750        }
751    }
752
753    /// A fresh unique `Symbol(desc)` value.
754    pub fn new_symbol(&mut self, desc: Option<String>) -> Value {
755        let id = self.next_symbol;
756        self.next_symbol += 1;
757        self.alloc(JsObj::Symbol { desc, id })
758    }
759    /// The shared `Symbol.for(key)` value (interned by description).
760    pub fn symbol_for(&mut self, key: &str) -> Value {
761        if let Some(v) = self.symbol_registry.get(key) {
762            return v.clone();
763        }
764        let s = self.new_symbol(Some(key.to_string()));
765        self.symbol_registry.insert(key.to_string(), s.clone());
766        s
767    }
768    /// The well-known `Symbol.iterator` (a fixed shared symbol whose internal
769    /// property key is `@@iterator`).
770    pub fn well_known_iterator(&mut self) -> Value {
771        self.symbol_for("@@Symbol.iterator")
772    }
773    /// The well-known `Symbol.asyncIterator` (internal key `@@asyncIterator`).
774    pub fn well_known_async_iterator(&mut self) -> Value {
775        self.symbol_for("@@Symbol.asyncIterator")
776    }
777    /// The internal property-key string for a value used as a key. A `Symbol`
778    /// maps to a stable per-symbol string so symbol-keyed props round-trip;
779    /// `Symbol.iterator` maps to the sentinel `@@iterator`.
780    pub fn property_key(&self, v: &Value) -> String {
781        if let Some(JsObj::Symbol { desc, id }) = self.get(v) {
782            if desc.as_deref() == Some("@@Symbol.iterator") {
783                return "@@iterator".to_string();
784            }
785            if desc.as_deref() == Some("@@Symbol.asyncIterator") {
786                return "@@asyncIterator".to_string();
787            }
788            return format!("@@sym:{id}");
789        }
790        self.str_of(v)
791    }
792
793    pub fn null(&self) -> Value {
794        self.null_val.clone()
795    }
796    pub fn is_null(&self, v: &Value) -> bool {
797        matches!(self.get(v), Some(JsObj::Null))
798    }
799
800    // ── program loading ──────────────────────────────────────────────────
801    pub fn program_offsets(&self) -> (usize, usize) {
802        (self.funcs.len(), self.tries.len())
803    }
804    pub fn load_program(&mut self, funcs: Vec<FuncDef>, tries: Vec<TryDef>) {
805        self.funcs.extend(funcs);
806        self.tries.extend(tries);
807    }
808    pub fn try_def(&self, id: usize) -> Option<TryDef> {
809        self.tries.get(id).cloned()
810    }
811
812    // ── heap allocation / accessors ──────────────────────────────────────
813    pub fn alloc(&mut self, obj: JsObj) -> Value {
814        self.heap.push(obj);
815        Value::Obj((self.heap.len() - 1) as u32)
816    }
817    pub fn get(&self, v: &Value) -> Option<&JsObj> {
818        if let Value::Obj(i) = v {
819            self.heap.get(*i as usize)
820        } else {
821            None
822        }
823    }
824    pub fn get_mut(&mut self, v: &Value) -> Option<&mut JsObj> {
825        if let Value::Obj(i) = v {
826            self.heap.get_mut(*i as usize)
827        } else {
828            None
829        }
830    }
831    pub fn new_str(&mut self, s: impl Into<String>) -> Value {
832        self.alloc(JsObj::Str(s.into()))
833    }
834    pub fn new_array(&mut self, items: Vec<Value>) -> Value {
835        self.alloc(JsObj::Array(items))
836    }
837    pub fn new_object(&mut self, mut props: IndexMap<String, Value>) -> Value {
838        // Integer-index keys enumerate ascending-first regardless of the order
839        // they were supplied in (object literal, spread, Object.assign result).
840        canonicalize_own_keys(&mut props);
841        self.alloc(JsObj::Object(props))
842    }
843    pub fn as_str(&self, v: &Value) -> Option<String> {
844        match v {
845            Value::Str(s) => Some((**s).clone()),
846            Value::Obj(_) => match self.get(v) {
847                Some(JsObj::Str(s)) => Some(s.clone()),
848                _ => None,
849            },
850            _ => None,
851        }
852    }
853
854    // ── scope / names ────────────────────────────────────────────────────
855    fn frame(&self) -> &Frame {
856        self.frames.last().unwrap()
857    }
858    fn cur_env(&self) -> Env {
859        self.frame().env.clone()
860    }
861
862    // ── DAP debug introspection (used only under `--dap`) ────────────────────
863    /// Number of active call frames (the debugger's step-depth reference).
864    pub fn frame_depth(&self) -> usize {
865        self.frames.len()
866    }
867    /// Record the source line the innermost frame is executing (DAP line hook).
868    pub fn set_cur_line(&mut self, line: u32) {
869        if let Some(f) = self.frames.last_mut() {
870            f.line = line;
871        }
872    }
873    /// The call stack as (frame name, line) pairs, innermost first — for the DAP
874    /// `stackTrace`. `owner` carries the function name where known.
875    pub fn dbg_stack(&self) -> Vec<(String, u32)> {
876        self.frames
877            .iter()
878            .rev()
879            .map(|f| {
880                let name = f.owner.clone().unwrap_or_else(|| "<module>".to_string());
881                (name, f.line)
882            })
883            .collect()
884    }
885    /// The innermost frame's locals as (name, inspect) pairs — for DAP `variables`.
886    pub fn dbg_locals(&self) -> Vec<(String, String)> {
887        let env = self.cur_env();
888        let names: Vec<String> = env.borrow().vars.keys().cloned().collect();
889        names
890            .into_iter()
891            .map(|n| {
892                let v = self.read_name(&n).unwrap_or(Value::Undef);
893                (n, self.inspect(&v))
894            })
895            .collect()
896    }
897
898    /// Scope-chain read: local + enclosing chain, then globals.
899    pub fn read_name(&self, name: &str) -> Option<Value> {
900        let mut env = Some(self.cur_env());
901        while let Some(e) = env {
902            if let Some(v) = e.borrow().vars.get(name) {
903                return Some(v.clone());
904            }
905            env = e.borrow().parent.clone();
906        }
907        self.globals.get(name).cloned()
908    }
909    pub fn read_global(&self, name: &str) -> Option<Value> {
910        self.globals.get(name).cloned()
911    }
912
913    /// Assign to an existing binding up the scope chain, else create a global
914    /// (JS assignment to an undeclared name targets the global object).
915    pub fn set_name(&mut self, name: &str, val: Value) {
916        let mut env = Some(self.cur_env());
917        while let Some(e) = env {
918            if e.borrow().vars.contains_key(name) {
919                e.borrow_mut().vars.insert(name.to_string(), val);
920                return;
921            }
922            env = e.borrow().parent.clone();
923        }
924        self.globals.insert(name.to_string(), val);
925    }
926
927    /// Declare a new binding in the current scope (`let`/`const`/`var`).
928    pub fn declare_name(&mut self, name: &str, val: Value) {
929        if self.frames.len() == 1 {
930            self.globals.insert(name.to_string(), val);
931        } else {
932            self.cur_env()
933                .borrow_mut()
934                .vars
935                .insert(name.to_string(), val);
936        }
937    }
938    pub fn set_global(&mut self, name: &str, val: Value) {
939        self.globals.insert(name.to_string(), val);
940    }
941    pub fn del_name(&mut self, name: &str) {
942        if self
943            .cur_env()
944            .borrow_mut()
945            .vars
946            .shift_remove(name)
947            .is_some()
948        {
949            return;
950        }
951        self.globals.shift_remove(name);
952    }
953
954    pub fn current_this(&self) -> Option<Value> {
955        self.frame().this_obj.clone()
956    }
957    pub fn current_env_capture(&self) -> Env {
958        self.frame().env.clone()
959    }
960    pub fn current_new_target(&self) -> Option<Value> {
961        self.frame().new_target.clone()
962    }
963    fn current_home_class(&self) -> Option<Value> {
964        self.frame().home_class.clone()
965    }
966
967    /// The `(parent_ctor, this_class_fields)` for a running constructor's
968    /// `super(...)`, derived from the frame's home class.
969    pub fn super_context(&self) -> (Option<Value>, Vec<(String, Value)>) {
970        match self.current_home_class() {
971            Some(cv) => match self.get(&cv) {
972                Some(JsObj::Class(c)) => (c.parent.clone(), c.fields.clone()),
973                _ => (None, Vec::new()),
974            },
975            None => (None, Vec::new()),
976        }
977    }
978
979    /// Resolve `super.name` to either the parent-prototype getter (to be invoked
980    /// by the caller, outside any host borrow) or a directly-usable value.
981    pub fn super_resolve(&self, name: &str) -> SuperRef {
982        let parent = match self
983            .current_home_class()
984            .and_then(|cv| match self.get(&cv) {
985                Some(JsObj::Class(c)) => c.parent.clone(),
986                _ => None,
987            }) {
988            Some(p) => p,
989            None => return SuperRef::Data(Value::Undef),
990        };
991        let parent_proto = match self.get(&parent) {
992            Some(JsObj::Class(pc)) => pc.proto.clone(),
993            _ => self.fn_prop(&parent, "prototype").unwrap_or(Value::Undef),
994        };
995        if let Some((Some(getter), _)) = lookup_accessor(self, &parent_proto, name) {
996            return SuperRef::Getter(getter);
997        }
998        SuperRef::Data(lookup_chain(self, &parent_proto, name).unwrap_or(Value::Undef))
999    }
1000
1001    // ── signals / errors ─────────────────────────────────────────────────
1002    pub fn take_error(&mut self) -> Option<String> {
1003        self.error.take()
1004    }
1005    pub fn raise_str(&mut self, class: &str, msg: &str) -> String {
1006        let s = if msg.is_empty() {
1007            class.to_string()
1008        } else {
1009            format!("{class}: {msg}")
1010        };
1011        self.error = Some(s.clone());
1012        s
1013    }
1014}
1015
1016// ── error constructors ───────────────────────────────────────────────────────
1017
1018pub fn type_error(msg: &str) -> String {
1019    format!("TypeError: {msg}")
1020}
1021pub fn ref_error(name: &str) -> String {
1022    format!("ReferenceError: {name} is not defined")
1023}
1024pub fn range_error(msg: &str) -> String {
1025    format!("RangeError: {msg}")
1026}
1027
1028// ── the fusevm run plumbing ──────────────────────────────────────────────────
1029
1030thread_local! {
1031    static DEBUG_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1032}
1033
1034/// Enable/disable DAP debug execution (`node --dap`).
1035pub fn set_debug_mode(on: bool) {
1036    DEBUG_MODE.with(|d| d.set(on));
1037}
1038
1039/// Register every node-js builtin + the numeric hook on a VM, then run it.
1040pub fn run_chunk_on(chunk: Chunk) -> Result<Value, String> {
1041    let mut vm = VM::new(chunk);
1042    crate::builtins::install(&mut vm);
1043    vm.set_numeric_hook(std::sync::Arc::new(|op, a, b| {
1044        crate::builtins::numeric_hook(op, a, b)
1045    }));
1046    // Under `--dap` the tracing JIT would compile hot loops and skip the
1047    // per-statement `DBG_LINE` markers, so debug runs stay on the pure
1048    // interpreter. The `DBG_LINE` builtin fires the debugger line hook; the
1049    // extension seam mirrors pythonrs should the marker emission ever switch.
1050    if DEBUG_MODE.with(|d| d.get()) {
1051        vm.set_extension_handler(Box::new(|vm, id, _| {
1052            crate::dap::on_ext(vm, id);
1053        }));
1054    } else {
1055        vm.enable_tracing_jit();
1056    }
1057    let outcome = vm.run();
1058    if let Some(e) = with_host(|h| h.take_error()) {
1059        return Err(e);
1060    }
1061    match outcome {
1062        VMResult::Ok(v) => Ok(v),
1063        VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
1064        VMResult::Error(e) => Err(e),
1065    }
1066}
1067
1068/// Run the top-level program chunk, then drain the event loop (microtasks +
1069/// timers) until quiescent — matching Node, which keeps the process alive while
1070/// pending async work remains.
1071pub fn run_main(chunk: Chunk) -> Result<Value, String> {
1072    let r = run_chunk_on(chunk);
1073    with_host(|h| h.signal = None);
1074    if r.is_ok() {
1075        run_event_loop()?;
1076    }
1077    r
1078}
1079
1080// ── formatting ───────────────────────────────────────────────────────────────
1081
1082/// Format a JS number exactly as `Number.prototype.toString` does for the common
1083/// range (no exponential-notation threshold handling for very large/small).
1084pub fn fmt_number(f: f64) -> String {
1085    if f.is_nan() {
1086        return "NaN".into();
1087    }
1088    if f.is_infinite() {
1089        return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
1090    }
1091    if f == 0.0 {
1092        // Covers -0.0 too: (-0).toString() === "0".
1093        return "0".into();
1094    }
1095    if f < 0.0 {
1096        return format!("-{}", js_number_repr(-f));
1097    }
1098    js_number_repr(f)
1099}
1100
1101/// If `k` is an array-index property key, return its numeric value. Per
1102/// ECMAScript, a String property key `P` is an array index iff
1103/// `ToString(ToUint32(P)) === P` and `ToUint32(P) !== 2^32 - 1` — i.e. a
1104/// canonical decimal (no leading zeros, no sign) in the range `0..=2^32-2`.
1105pub fn array_index(k: &str) -> Option<u32> {
1106    if k.is_empty() {
1107        return None;
1108    }
1109    if k == "0" {
1110        return Some(0);
1111    }
1112    // A leading '0' (other than the lone "0" above) is non-canonical.
1113    if k.as_bytes()[0] == b'0' {
1114        return None;
1115    }
1116    if !k.bytes().all(|b| b.is_ascii_digit()) {
1117        return None;
1118    }
1119    match k.parse::<u64>() {
1120        // Array index must be < 2^32-1; u32::MAX == 2^32-1 is excluded.
1121        Ok(n) if n < u32::MAX as u64 => Some(n as u32),
1122        _ => None,
1123    }
1124}
1125
1126/// Compare two own-property keys for `OrdinaryOwnPropertyKeys` enumeration order:
1127/// integer-index keys sort ascending-numeric and precede all string keys; two
1128/// non-index keys compare `Equal` so a *stable* sort leaves them in insertion
1129/// order. (Symbols are stored as `@@…`/`#…` string keys and are non-index, so
1130/// they also fall into the stable-insertion-order tail.)
1131pub fn key_order_cmp(a: &str, b: &str) -> std::cmp::Ordering {
1132    use std::cmp::Ordering;
1133    match (array_index(a), array_index(b)) {
1134        (Some(x), Some(y)) => x.cmp(&y),
1135        (Some(_), None) => Ordering::Less,
1136        (None, Some(_)) => Ordering::Greater,
1137        (None, None) => Ordering::Equal,
1138    }
1139}
1140
1141/// Reorder an object's own-property map into `OrdinaryOwnPropertyKeys` order in
1142/// place: array-index keys ascending first, then the remaining keys in their
1143/// existing (insertion) order. A no-op unless at least one index key is present,
1144/// so the overwhelmingly common all-string-key object keeps its exact order and
1145/// pays nothing. `IndexMap::sort_by` is a stable sort.
1146pub fn canonicalize_own_keys(props: &mut IndexMap<String, Value>) {
1147    if props.keys().any(|k| array_index(k).is_some()) {
1148        props.sort_by(|ak, _, bk, _| key_order_cmp(ak, bk));
1149    }
1150}
1151
1152/// ECMAScript `Number::toString` layout for a positive, finite, nonzero value.
1153///
1154/// Rust's `Display`/`LowerExp` give the shortest round-trip decimal digits, but
1155/// NOT JavaScript's exponential-vs-fixed threshold: Rust prints `1e21` as
1156/// `1000000000000000000000` and `1e-7` as `0.0000001`, whereas JS prints `1e+21`
1157/// and `1e-7`. So we take the shortest digits from `{:e}` and re-lay them out per
1158/// the spec (steps 5–10 of Number::toString): `k` significant digits `s` with
1159/// decimal exponent `n` (value = s × 10^(n−k)); exponential form only when
1160/// `n > 21` or `n ≤ -6`.
1161fn js_number_repr(a: f64) -> String {
1162    // `{:e}` yields `d[.ddd]e<exp>` with the mantissa in [1, 10) and shortest
1163    // round-trip digits. Split it into the digit string `s` and exponent `E`.
1164    let sci = format!("{a:e}");
1165    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
1166    let e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
1167    let s: String = mant.chars().filter(|c| *c != '.').collect();
1168    let k = s.len() as i32; // number of significant digits
1169    let n = e + 1; // value = s × 10^(n−k), 10^(k−1) ≤ s < 10^k
1170
1171    if k <= n && n <= 21 {
1172        // Integer with trailing zeros: all digits, then n−k zeros.
1173        let mut out = s;
1174        out.push_str(&"0".repeat((n - k) as usize));
1175        out
1176    } else if 0 < n && n <= 21 {
1177        // Decimal point inside the digit run: n digits, '.', the rest.
1178        format!("{}.{}", &s[..n as usize], &s[n as usize..])
1179    } else if -6 < n && n <= 0 {
1180        // Leading "0." then (−n) zeros then all digits.
1181        format!("0.{}{}", "0".repeat((-n) as usize), s)
1182    } else {
1183        // Exponential form. Exponent digit is n−1, always signed.
1184        let exp = n - 1;
1185        let sign = if exp >= 0 { '+' } else { '-' };
1186        let mag = exp.abs();
1187        if k == 1 {
1188            format!("{s}e{sign}{mag}")
1189        } else {
1190            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
1191        }
1192    }
1193}
1194
1195impl JsHost {
1196    /// The `typeof` string for `v`.
1197    pub fn type_of(&self, v: &Value) -> &'static str {
1198        match v {
1199            Value::Undef => "undefined",
1200            Value::Bool(_) => "boolean",
1201            Value::Int(_) | Value::Float(_) => "number",
1202            Value::Str(_) => "string",
1203            Value::Obj(_) => match self.get(v) {
1204                Some(JsObj::Str(_)) => "string",
1205                Some(JsObj::Func(_))
1206                | Some(JsObj::BoundMethod { .. })
1207                | Some(JsObj::BoundFunc { .. })
1208                | Some(JsObj::Class(_)) => "function",
1209                // A Builtin is a callable (`Array`, `parseInt`, `Math.floor`) —
1210                // `typeof === "function"` — EXCEPT the non-callable namespace
1211                // objects (`Math`, `JSON`, `require('fs')`, …) which are "object".
1212                Some(JsObj::Builtin(n)) => {
1213                    const NON_CALLABLE_NS: &[&str] = &[
1214                        "Math",
1215                        "JSON",
1216                        "console",
1217                        "Reflect",
1218                        "process",
1219                        "Atomics",
1220                        "performance",
1221                        "fs",
1222                        "path",
1223                        "os",
1224                        "util",
1225                        "crypto",
1226                        "querystring",
1227                        "events",
1228                        "stream",
1229                        "timers",
1230                        "perf_hooks",
1231                        "async_hooks",
1232                        "diagnostics_channel",
1233                        "v8",
1234                        "dns",
1235                        "punycode",
1236                        "child_process",
1237                        "tty",
1238                        "url",
1239                        "zlib",
1240                        "string_decoder",
1241                        "assert",
1242                        "http",
1243                        "net",
1244                        "buffer",
1245                    ];
1246                    if NON_CALLABLE_NS.contains(&n.as_str()) {
1247                        "object"
1248                    } else {
1249                        "function"
1250                    }
1251                }
1252                Some(JsObj::Symbol { .. }) => "symbol",
1253                Some(JsObj::BigInt(_)) => "bigint",
1254                _ => "object", // arrays, objects, null, Map/Set, generators
1255            },
1256            _ => "object",
1257        }
1258    }
1259
1260    /// JS truthiness: false / 0 / -0 / NaN / "" / null / undefined are falsy.
1261    pub fn truthy(&self, v: &Value) -> bool {
1262        match v {
1263            Value::Undef => false,
1264            Value::Bool(b) => *b,
1265            Value::Int(n) => *n != 0,
1266            Value::Float(f) => *f != 0.0 && !f.is_nan(),
1267            Value::Str(s) => !s.is_empty(),
1268            Value::Obj(_) => match self.get(v) {
1269                Some(JsObj::Str(s)) => !s.is_empty(),
1270                Some(JsObj::Null) => false,
1271                Some(JsObj::BigInt(b)) => !num_traits::Zero::is_zero(b),
1272                _ => true, // arrays, objects, functions
1273            },
1274            _ => true,
1275        }
1276    }
1277
1278    /// Coerce to a number (`ToNumber`): the arithmetic-context conversion.
1279    pub fn to_number(&self, v: &Value) -> f64 {
1280        match v {
1281            Value::Undef => f64::NAN,
1282            Value::Bool(b) => {
1283                if *b {
1284                    1.0
1285                } else {
1286                    0.0
1287                }
1288            }
1289            Value::Int(n) => *n as f64,
1290            Value::Float(f) => *f,
1291            Value::Str(s) => str_to_number(s),
1292            Value::Obj(_) => match self.get(v) {
1293                Some(JsObj::Str(s)) => str_to_number(s),
1294                Some(JsObj::Null) => 0.0,
1295                Some(JsObj::BigInt(b)) => bigint_to_f64(b),
1296                Some(JsObj::Array(items)) => {
1297                    // [] -> 0, [x] -> ToNumber(x), else NaN.
1298                    if items.is_empty() {
1299                        0.0
1300                    } else if items.len() == 1 {
1301                        self.to_number(&items[0])
1302                    } else {
1303                        f64::NAN
1304                    }
1305                }
1306                _ => f64::NAN,
1307            },
1308            _ => f64::NAN,
1309        }
1310    }
1311
1312    /// `String(v)` — the string-coercion form (raw, unquoted).
1313    pub fn str_of(&self, v: &Value) -> String {
1314        match v {
1315            Value::Undef => "undefined".into(),
1316            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
1317            Value::Int(n) => n.to_string(),
1318            Value::Float(f) => fmt_number(*f),
1319            Value::Str(s) => (**s).clone(),
1320            Value::Obj(_) => match self.get(v) {
1321                Some(JsObj::Str(s)) => s.clone(),
1322                Some(JsObj::Null) => "null".into(),
1323                Some(JsObj::BigInt(b)) => b.to_string(),
1324                Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
1325                Some(JsObj::Array(items)) => {
1326                    // Array.prototype.toString: comma-join, null/undefined -> "".
1327                    let parts: Vec<String> = items
1328                        .iter()
1329                        .map(|x| match x {
1330                            Value::Undef => String::new(),
1331                            _ if self.is_null(x) => String::new(),
1332                            _ => self.str_of(x),
1333                        })
1334                        .collect();
1335                    parts.join(",")
1336                }
1337                Some(JsObj::Object(props)) => {
1338                    // A native `Buffer` stringifies to its decoded (utf-8)
1339                    // contents, matching `buf.toString()` — needed for `'' + buf`,
1340                    // template interpolation, and `data += chunk` (the pattern
1341                    // Express/body-parser use to read a request body).
1342                    if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Buffer") {
1343                        let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
1344                            Some(JsObj::Array(items)) => {
1345                                items.iter().map(|x| self.to_number(x) as u8).collect()
1346                            }
1347                            _ => Vec::new(),
1348                        };
1349                        String::from_utf8_lossy(&bytes).into_owned()
1350                    } else {
1351                        "[object Object]".into()
1352                    }
1353                }
1354                Some(JsObj::Func(f)) => {
1355                    let name = self
1356                        .funcs
1357                        .get(f.def_id)
1358                        .map(|d| d.name.clone())
1359                        .unwrap_or_default();
1360                    format!("function {name}() {{ [code] }}")
1361                }
1362                Some(JsObj::Builtin(n)) => format!("function {n}() {{ [native code] }}"),
1363                Some(JsObj::BoundMethod { .. }) | Some(JsObj::BoundFunc { .. }) => {
1364                    "function () { [native code] }".into()
1365                }
1366                Some(JsObj::Class(c)) => format!("class {} {{ }}", c.name),
1367                Some(JsObj::Symbol { desc, .. }) => {
1368                    // `String(sym)` is allowed (unlike implicit coercion) and yields
1369                    // `Symbol(desc)`.
1370                    match desc {
1371                        Some(d) => format!("Symbol({d})"),
1372                        None => "Symbol()".into(),
1373                    }
1374                }
1375                _ => "[object Object]".into(),
1376            },
1377            _ => "[object Object]".into(),
1378        }
1379    }
1380
1381    /// `console.log`-style rendering of a top-level argument: bare strings print
1382    /// raw; everything else uses `inspect`.
1383    pub fn console_format(&self, v: &Value) -> String {
1384        match v {
1385            Value::Str(_) => self.str_of(v),
1386            Value::Obj(_) if matches!(self.get(v), Some(JsObj::Str(_))) => self.str_of(v),
1387            _ => self.inspect(v),
1388        }
1389    }
1390
1391    /// `util.inspect`-style rendering (nested; strings quoted).
1392    pub fn inspect(&self, v: &Value) -> String {
1393        self.inspect_lvl(v, 0)
1394    }
1395
1396    /// `util.inspect` at a given indentation level (drives array multi-line
1397    /// grouping and nested indentation).
1398    fn inspect_lvl(&self, v: &Value, indent: usize) -> String {
1399        match v {
1400            Value::Undef => "undefined".into(),
1401            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
1402            Value::Int(n) => n.to_string(),
1403            // `util.inspect` distinguishes negative zero; `String(-0)` does not.
1404            Value::Float(f) if *f == 0.0 && f.is_sign_negative() => "-0".into(),
1405            Value::Float(f) => fmt_number(*f),
1406            Value::Str(s) => quote_str(s),
1407            Value::Obj(_) => match self.get(v) {
1408                Some(JsObj::Str(s)) => quote_str(s),
1409                Some(JsObj::Null) => "null".into(),
1410                // `util.inspect` renders a bigint with the `n` suffix, a regex bare.
1411                Some(JsObj::BigInt(b)) => format!("{b}n"),
1412                Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
1413                Some(JsObj::Array(items)) => {
1414                    // Own enumerable non-index string props (e.g. a `str.match(re)`
1415                    // result's `index`/`input`/`groups`, or a user-assigned
1416                    // `arr.foo`) render after the elements, as `key: value`.
1417                    let prop_keys: Vec<String> = self
1418                        .fn_prop_keys(v)
1419                        .into_iter()
1420                        .filter(|k| !k.starts_with("@@") && !k.starts_with('#'))
1421                        .collect();
1422                    if items.is_empty() && prop_keys.is_empty() {
1423                        return "[]".into();
1424                    }
1425                    // Node's default inspect depth is 2 (root = depth 0); deeper
1426                    // nesting collapses to `[Array]`. indent grows by 2 per level.
1427                    if indent > 2 * inspect_max_depth() {
1428                        return "[Array]".into();
1429                    }
1430                    let mut inner: Vec<String> = items
1431                        .iter()
1432                        .map(|x| self.inspect_lvl(x, indent + 2))
1433                        .collect();
1434                    let has_props = !prop_keys.is_empty();
1435                    for k in &prop_keys {
1436                        let val = self.fn_prop(v, k).unwrap_or(Value::Undef);
1437                        inner.push(format!(
1438                            "{}: {}",
1439                            fmt_key(k),
1440                            self.inspect_lvl(&val, indent + 2)
1441                        ));
1442                    }
1443                    self.render_array(&inner, items, indent, has_props)
1444                }
1445                Some(JsObj::Object(props)) => {
1446                    // Instances print with their constructor name as a prefix
1447                    // (`C { x: 1 }`); plain objects have none; a null-prototype
1448                    // object (e.g. an `Object.groupBy` result) is tagged
1449                    // `[Object: null prototype]`.
1450                    let prefix = if self.has_null_proto(v) {
1451                        "[Object: null prototype] ".to_string()
1452                    } else {
1453                        match self.ctor_name(v) {
1454                            n if n.is_empty() || n == "Object" => String::new(),
1455                            n => format!("{n} "),
1456                        }
1457                    };
1458                    // Skip internal symbol-keyed props (`@@…`) in the display.
1459                    let shown: Vec<(&String, &Value)> = props
1460                        .iter()
1461                        .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
1462                        .collect();
1463                    if shown.is_empty() {
1464                        return format!("{prefix}{{}}");
1465                    }
1466                    // Depth limit (Node default 2): deeper objects collapse to
1467                    // `[Object]` (or `[ClassName]` for a named instance).
1468                    if indent > 2 * inspect_max_depth() {
1469                        return if prefix.is_empty() {
1470                            "[Object]".into()
1471                        } else if self.has_null_proto(v) {
1472                            // Already bracketed (`[Object: null prototype]`).
1473                            prefix.trim_end().to_string()
1474                        } else {
1475                            format!("[{}]", prefix.trim_end())
1476                        };
1477                    }
1478                    let inner: Vec<String> = shown
1479                        .iter()
1480                        .map(|(k, val)| {
1481                            format!("{}: {}", fmt_key(k), self.inspect_lvl(val, indent + 2))
1482                        })
1483                        .collect();
1484                    self.render_object(&inner, &prefix, indent)
1485                }
1486                Some(JsObj::Symbol { desc, .. }) => match desc {
1487                    Some(d) => format!("Symbol({d})"),
1488                    None => "Symbol()".into(),
1489                },
1490                Some(JsObj::Class(c)) => {
1491                    if c.parent.is_some() {
1492                        let pname = c
1493                            .parent
1494                            .as_ref()
1495                            .map(|p| self.callable_name(p))
1496                            .unwrap_or_default();
1497                        format!("[class {} extends {}]", c.name, pname)
1498                    } else {
1499                        format!("[class {}]", c.name)
1500                    }
1501                }
1502                Some(JsObj::Map { entries, .. }) => {
1503                    if entries.is_empty() {
1504                        return "Map(0) {}".into();
1505                    }
1506                    let inner: Vec<String> = entries
1507                        .values()
1508                        .map(|(k, val)| format!("{} => {}", self.inspect(k), self.inspect(val)))
1509                        .collect();
1510                    format!("Map({}) {{ {} }}", entries.len(), inner.join(", "))
1511                }
1512                Some(JsObj::Set { entries, .. }) => {
1513                    if entries.is_empty() {
1514                        return "Set(0) {}".into();
1515                    }
1516                    let inner: Vec<String> = entries.values().map(|v| self.inspect(v)).collect();
1517                    format!("Set({}) {{ {} }}", entries.len(), inner.join(", "))
1518                }
1519                Some(JsObj::Generator { .. }) => "Object [Generator] {}".into(),
1520                Some(JsObj::Promise { id }) => match self.promises.get(*id as usize) {
1521                    Some(c) => match c.state {
1522                        PromiseState::Pending => "Promise { <pending> }".into(),
1523                        PromiseState::Fulfilled => {
1524                            format!("Promise {{ {} }}", self.inspect(&c.value))
1525                        }
1526                        PromiseState::Rejected => {
1527                            format!("Promise {{ <rejected> {} }}", self.inspect(&c.value))
1528                        }
1529                    },
1530                    None => "Promise { <pending> }".into(),
1531                },
1532                Some(JsObj::Func(f)) => {
1533                    let name = self
1534                        .funcs
1535                        .get(f.def_id)
1536                        .map(|d| d.name.clone())
1537                        .unwrap_or_default();
1538                    if name.is_empty() {
1539                        "[Function (anonymous)]".into()
1540                    } else {
1541                        format!("[Function: {name}]")
1542                    }
1543                }
1544                Some(JsObj::Builtin(n)) => {
1545                    let short = n.rsplit('.').next().unwrap_or(n);
1546                    format!("[Function: {short}]")
1547                }
1548                Some(JsObj::BoundMethod { .. }) => "[Function (anonymous)]".into(),
1549                Some(JsObj::BoundFunc { target, .. }) => {
1550                    let n = self.callable_name(target);
1551                    if n.is_empty() {
1552                        "[Function: bound ]".into()
1553                    } else {
1554                        format!("[Function: bound {n}]")
1555                    }
1556                }
1557                _ => "undefined".into(),
1558            },
1559            _ => "undefined".into(),
1560        }
1561    }
1562
1563    /// Render a non-empty array's already-formatted element strings, applying
1564    /// Node's `util.inspect` layout: a single line when it fits, else a multi-line
1565    /// grid via `groupArrayElements` (for >6 entries), else one element per line.
1566    /// `values` is the raw element list (drives numeric right-alignment); `indent`
1567    /// is the array's own indentation level.
1568    fn render_array(
1569        &self,
1570        output: &[String],
1571        values: &[Value],
1572        indent: usize,
1573        has_props: bool,
1574    ) -> String {
1575        // Group array elements together if the array has more than six entries.
1576        // Arrays carrying extra own props (`index`/`input`/… on a match result)
1577        // are never grid-grouped — Node lays those out plainly.
1578        let entries = output.len();
1579        let (lines, grouped) = if entries > 6 && !has_props {
1580            group_array_elements(self, output, values, indent)
1581        } else {
1582            (output.to_vec(), false)
1583        };
1584        // If no grouping happened, try to line everything up on a single line.
1585        if !grouped {
1586            // start = output.length + indentationLvl + braces[0].len(1) + base(0) + 10
1587            let start = output.len() + indent + 1 + 10;
1588            if is_below_break_length(output, start) {
1589                return format!("[ {} ]", output.join(", "));
1590            }
1591        }
1592        // Otherwise: one (grouped or single) entry per line, indented by indent+2.
1593        let pad = " ".repeat(indent);
1594        let sep = format!(",\n{pad}  ");
1595        format!("[\n{pad}  {}\n{pad}]", lines.join(&sep))
1596    }
1597
1598    /// Render a non-empty object's already-formatted `key: value` strings with
1599    /// Node's `util.inspect` layout: a single line when it fits `breakLength`,
1600    /// else one property per line indented by `indent + 2`. `prefix` is the
1601    /// constructor/`[Object: null prototype]` tag (with trailing space) or empty.
1602    /// Mirrors `render_array`'s break decision. (Node's `compact` depth gate is a
1603    /// no-op at `console.log`'s default depth of 2, so only length matters here.)
1604    fn render_object(&self, output: &[String], prefix: &str, indent: usize) -> String {
1605        // start = output.length + indentationLvl + braces[0].len + base(0) + 10.
1606        // For a tagged object Node folds the tag into `braces[0]` (e.g.
1607        // `"Point {"`, `"[Object: null prototype] {"`), so its length is the
1608        // prefix (which carries the trailing space) plus the `{`.
1609        let braces0 = prefix.chars().count() + 1;
1610        let start = output.len() + indent + braces0 + 10;
1611        if is_below_break_length(output, start) {
1612            return format!("{prefix}{{ {} }}", output.join(", "));
1613        }
1614        let pad = " ".repeat(indent);
1615        let sep = format!(",\n{pad}  ");
1616        format!("{prefix}{{\n{pad}  {}\n{pad}}}", output.join(&sep))
1617    }
1618
1619    /// The `.name` of any callable (function/class/builtin/bound).
1620    pub fn callable_name(&self, v: &Value) -> String {
1621        // A user-set `.name` own property wins.
1622        if let Some(n) = self.fn_prop(v, "name") {
1623            return self.str_of(&n);
1624        }
1625        match self.get(v) {
1626            Some(JsObj::Func(f)) => self
1627                .funcs
1628                .get(f.def_id)
1629                .map(|d| d.name.clone())
1630                .unwrap_or_default(),
1631            Some(JsObj::Class(c)) => c.name.clone(),
1632            Some(JsObj::Builtin(n)) => n.rsplit('.').next().unwrap_or(n).to_string(),
1633            Some(JsObj::BoundFunc { target, .. }) => {
1634                format!("bound {}", self.callable_name(target))
1635            }
1636            _ => String::new(),
1637        }
1638    }
1639
1640    // ── equality / comparison / arithmetic (numeric-hook + builtin paths) ──
1641
1642    /// Strict equality (`===`): same type and same value, no coercion.
1643    pub fn strict_eq(&self, a: &Value, b: &Value) -> bool {
1644        match (a, b) {
1645            (Value::Undef, Value::Undef) => true,
1646            (Value::Bool(x), Value::Bool(y)) => x == y,
1647            (Value::Str(x), Value::Str(y)) => x == y,
1648            _ => {
1649                // Numbers (NaN !== NaN, +0 === -0).
1650                let an = matches!(a, Value::Int(_) | Value::Float(_));
1651                let bn = matches!(b, Value::Int(_) | Value::Float(_));
1652                if an && bn {
1653                    let x = self.to_number(a);
1654                    let y = self.to_number(b);
1655                    return x == y;
1656                }
1657                // BigInt === BigInt compares by value (each literal is a distinct
1658                // heap cell, so reference identity would be wrong). BigInt is never
1659                // `===` a Number (different types).
1660                if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
1661                    return x == y;
1662                }
1663                // Heap values.
1664                if let (Some(sa), Some(sb)) = (self.as_str(a), self.as_str(b)) {
1665                    return sa == sb;
1666                }
1667                let na = self.is_null(a);
1668                let nb = self.is_null(b);
1669                if na || nb {
1670                    return na && nb;
1671                }
1672                // Reference identity for arrays/objects/functions.
1673                matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
1674            }
1675        }
1676    }
1677
1678    /// Whether `v` is `null` or `undefined`.
1679    pub fn is_nullish(&self, v: &Value) -> bool {
1680        matches!(v, Value::Undef) || self.is_null(v)
1681    }
1682
1683    /// The ECMAScript "loose type" of `v` for the `==` algorithm: `"number"`,
1684    /// `"string"` (primitive or heap string), `"boolean"`, `"undefined"`,
1685    /// `"null"`, or `"object"` (array / plain object / function).
1686    fn js_type(&self, v: &Value) -> &'static str {
1687        match v {
1688            Value::Undef => "undefined",
1689            Value::Bool(_) => "boolean",
1690            Value::Int(_) | Value::Float(_) => "number",
1691            Value::Str(_) => "string",
1692            Value::Obj(_) => match self.get(v) {
1693                Some(JsObj::Str(_)) => "string",
1694                Some(JsObj::Null) => "null",
1695                Some(JsObj::BigInt(_)) => "bigint",
1696                _ => "object",
1697            },
1698            _ => "object",
1699        }
1700    }
1701
1702    /// Loose equality (`==`) following the ECMAScript Abstract Equality Comparison.
1703    /// Objects reduce via `ToPrimitive` (which for our heap objects is always their
1704    /// string `toString`), so `[0] == "0"` is `true` (string compare of `"0"`) but
1705    /// `[0] == ""` is `false` — never a number coercion of the object.
1706    pub fn loose_eq(&self, a: &Value, b: &Value) -> bool {
1707        // Same type: identical to `===` (number==number, string==string, etc.).
1708        if self.strict_eq(a, b) {
1709            return true;
1710        }
1711        let ta = self.js_type(a);
1712        let tb = self.js_type(b);
1713        // null and undefined are loosely equal only to each other.
1714        if self.is_nullish(a) || self.is_nullish(b) {
1715            return self.is_nullish(a) && self.is_nullish(b);
1716        }
1717        // BigInt ⇄ (Number | String | Boolean | Object): compare mathematical
1718        // values (both-BigInt was already settled by the `strict_eq` above).
1719        if ta == "bigint" || tb == "bigint" {
1720            return self.bigint_loose_eq(a, b);
1721        }
1722        if ta == tb {
1723            // Same type but not strict-equal (and not nullish) ⇒ not equal.
1724            return false;
1725        }
1726        // number ⇄ string: compare as numbers.
1727        if (ta == "number" && tb == "string") || (ta == "string" && tb == "number") {
1728            return self.to_number(a) == self.to_number(b);
1729        }
1730        // boolean side coerces to number, then recompares.
1731        if ta == "boolean" {
1732            return self.loose_eq(&Value::Float(self.to_number(a)), b);
1733        }
1734        if tb == "boolean" {
1735            return self.loose_eq(a, &Value::Float(self.to_number(b)));
1736        }
1737        // object ⇄ (number|string): ToPrimitive the object (→ its string form),
1738        // then recompare as string==string or number==string.
1739        if ta == "object" && (tb == "number" || tb == "string") {
1740            let pa = self.str_of(a);
1741            return if tb == "string" {
1742                pa == self.str_of(b)
1743            } else {
1744                str_to_number(&pa) == self.to_number(b)
1745            };
1746        }
1747        if tb == "object" && (ta == "number" || ta == "string") {
1748            let pb = self.str_of(b);
1749            return if ta == "string" {
1750                self.str_of(a) == pb
1751            } else {
1752                self.to_number(a) == str_to_number(&pb)
1753            };
1754        }
1755        false
1756    }
1757
1758    /// The numeric-hook arithmetic/relational fallback for non-native operands
1759    /// (called by fusevm when at least one operand isn't `Int`/`Float`).
1760    pub fn arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
1761        use NumOp::*;
1762        match op {
1763            Add => {
1764                // `+`: if either operand is a string, concatenate string forms;
1765                // otherwise numeric addition.
1766                let a_str = self.prefers_string(a);
1767                let b_str = self.prefers_string(b);
1768                if a_str || b_str {
1769                    // String concatenation wins even with a bigint operand
1770                    // (`1n + "x"` → `"1x"`).
1771                    let s = format!("{}{}", self.str_of(a), self.str_of(b));
1772                    Ok(self.new_str(s))
1773                } else if self.is_bigint_val(a) || self.is_bigint_val(b) {
1774                    self.bigint_arith(op, a, b)
1775                } else {
1776                    Ok(Value::Float(self.to_number(a) + self.to_number(b)))
1777                }
1778            }
1779            Sub | Mul | Div | Mod | Pow if self.is_bigint_val(a) || self.is_bigint_val(b) => {
1780                self.bigint_arith(op, a, b)
1781            }
1782            Sub => Ok(Value::Float(self.to_number(a) - self.to_number(b))),
1783            Mul => Ok(Value::Float(self.to_number(a) * self.to_number(b))),
1784            Div => Ok(Value::Float(self.to_number(a) / self.to_number(b))),
1785            Mod => Ok(Value::Float(js_mod(self.to_number(a), self.to_number(b)))),
1786            Pow => Ok(Value::Float(self.to_number(a).powf(self.to_number(b)))),
1787            Neg if self.is_bigint_val(a) => self.bigint_arith(op, a, b),
1788            Neg => Ok(Value::Float(-self.to_number(a))),
1789            Lt | Le | Gt | Ge => Ok(Value::Bool(self.relational(op, a, b))),
1790            Eq => Ok(Value::Bool(self.loose_eq(a, b))),
1791            Ne => Ok(Value::Bool(!self.loose_eq(a, b))),
1792        }
1793    }
1794
1795    /// Whether `v`'s primitive (`ToPrimitive` with the default hint) is a string,
1796    /// which drives `+` toward concatenation. Primitive strings qualify, and so
1797    /// do heap objects whose default `ToPrimitive` is their (string) `toString`:
1798    /// arrays (`[1,2,3]+3 → "1,2,33"`), plain objects (`{}+[] → "[object Object]"`),
1799    /// and functions. `null`/`undefined`/`boolean`/`number` do not.
1800    fn prefers_string(&self, v: &Value) -> bool {
1801        match v {
1802            Value::Str(_) => true,
1803            // A BigInt's `ToPrimitive` is the bigint itself (numeric), NOT a string,
1804            // so `1n + 2n` is bigint addition, not concatenation. `null` has no
1805            // string primitive either.
1806            Value::Obj(_) => !matches!(
1807                self.get(v),
1808                Some(JsObj::Null) | Some(JsObj::BigInt(_)) | None
1809            ),
1810            _ => false,
1811        }
1812    }
1813
1814    /// Relational comparison (`< <= > >=`) with JS coercion: string/string is
1815    /// lexicographic, otherwise numeric (NaN yields false).
1816    fn relational(&self, op: NumOp, a: &Value, b: &Value) -> bool {
1817        use std::cmp::Ordering;
1818        let ord = if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
1819            // BigInt < BigInt: exact (no f64 precision loss for large magnitudes).
1820            x.cmp(&y)
1821        } else if let (Some(x), Some(y)) = (self.as_str(a), self.as_str(b)) {
1822            x.cmp(&y)
1823        } else {
1824            let x = self.to_number(a);
1825            let y = self.to_number(b);
1826            match x.partial_cmp(&y) {
1827                Some(o) => o,
1828                None => return false, // NaN operand
1829            }
1830        };
1831        match op {
1832            NumOp::Lt => ord == Ordering::Less,
1833            NumOp::Le => ord != Ordering::Greater,
1834            NumOp::Gt => ord == Ordering::Greater,
1835            NumOp::Ge => ord != Ordering::Less,
1836            _ => false,
1837        }
1838    }
1839
1840    /// Bitwise/shift ops with JS ToInt32/ToUint32 semantics — or true
1841    /// arbitrary-width BigInt bitwise when both operands are BigInt (mixing a
1842    /// BigInt with a Number throws, matching Node).
1843    pub fn bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
1844        if self.is_bigint_val(a) || self.is_bigint_val(b) {
1845            return self.bigint_bitwise(tag, a, b);
1846        }
1847        let x = to_int32(self.to_number(a));
1848        let y = to_int32(self.to_number(b));
1849        let r: i64 = match tag {
1850            binop::BITAND => (x & y) as i64,
1851            binop::BITOR => (x | y) as i64,
1852            binop::BITXOR => (x ^ y) as i64,
1853            binop::SHL => (x.wrapping_shl((y as u32) & 31)) as i64,
1854            binop::SHR => (x >> ((y as u32) & 31)) as i64,
1855            binop::USHR => (to_uint32(self.to_number(a)) >> ((y as u32) & 31)) as i64,
1856            _ => 0,
1857        };
1858        Ok(Value::Float(r as f64))
1859    }
1860
1861    // ── BigInt operations ────────────────────────────────────────────────────
1862    /// Whether `v` is a heap `BigInt`.
1863    pub fn is_bigint_val(&self, v: &Value) -> bool {
1864        matches!(self.get(v), Some(JsObj::BigInt(_)))
1865    }
1866    /// The `BigInt` value of `v` (a heap bigint), else `None`.
1867    pub fn as_bigint(&self, v: &Value) -> Option<num_bigint::BigInt> {
1868        match self.get(v) {
1869            Some(JsObj::BigInt(b)) => Some(b.clone()),
1870            _ => None,
1871        }
1872    }
1873    /// Allocate a heap `BigInt`.
1874    pub fn new_bigint(&mut self, b: num_bigint::BigInt) -> Value {
1875        self.alloc(JsObj::BigInt(b))
1876    }
1877
1878    /// BigInt arithmetic (`+ - * / % **`, unary `-`). Requires BOTH operands to be
1879    /// BigInt for a binary op; mixing a BigInt with a Number throws the exact Node
1880    /// `TypeError` (a string operand is handled as concatenation before we get
1881    /// here). Division/`%` truncate toward zero; `**` needs a non-negative
1882    /// exponent.
1883    fn bigint_arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
1884        use num_traits::{Signed, Zero};
1885        use NumOp::*;
1886        if op == Neg {
1887            let x = self.as_bigint(a).expect("bigint_arith Neg on non-bigint");
1888            return Ok(self.new_bigint(-x));
1889        }
1890        let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
1891            (Some(x), Some(y)) => (x, y),
1892            // Exactly one side is a BigInt → the other is a Number/Boolean: illegal.
1893            _ => {
1894                return Err(type_error(
1895                    "Cannot mix BigInt and other types, use explicit conversions",
1896                ))
1897            }
1898        };
1899        let r = match op {
1900            Add => x + y,
1901            Sub => x - y,
1902            Mul => x * y,
1903            Div => {
1904                if y.is_zero() {
1905                    return Err("RangeError: Division by zero".into());
1906                }
1907                x / y // truncates toward zero (matches JS BigInt division)
1908            }
1909            Mod => {
1910                if y.is_zero() {
1911                    return Err("RangeError: Division by zero".into());
1912                }
1913                x % y // sign follows the dividend (truncated), like JS
1914            }
1915            Pow => {
1916                if y.is_negative() {
1917                    return Err("RangeError: Exponent must be positive".into());
1918                }
1919                let exp = num_traits::ToPrimitive::to_u32(&y)
1920                    .ok_or_else(|| "RangeError: Maximum BigInt size exceeded".to_string())?;
1921                num_traits::Pow::pow(x, exp)
1922            }
1923            _ => return Err(type_error("unsupported BigInt operation")),
1924        };
1925        Ok(self.new_bigint(r))
1926    }
1927
1928    /// BigInt bitwise (`& | ^ << >>`); `>>>` has no BigInt form. Both operands must
1929    /// be BigInt (mixing throws).
1930    fn bigint_bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
1931        let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
1932            (Some(x), Some(y)) => (x, y),
1933            _ => {
1934                return Err(type_error(
1935                    "Cannot mix BigInt and other types, use explicit conversions",
1936                ))
1937            }
1938        };
1939        let r = match tag {
1940            binop::BITAND => x & y,
1941            binop::BITOR => x | y,
1942            binop::BITXOR => x ^ y,
1943            binop::SHL => {
1944                let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
1945                if n >= 0 {
1946                    x << (n as usize)
1947                } else {
1948                    x >> ((-n) as usize)
1949                }
1950            }
1951            binop::SHR => {
1952                let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
1953                if n >= 0 {
1954                    x >> (n as usize)
1955                } else {
1956                    x << ((-n) as usize)
1957                }
1958            }
1959            binop::USHR => {
1960                return Err(type_error(
1961                    "BigInts have no unsigned right shift, use >> instead",
1962                ))
1963            }
1964            _ => return Err(type_error("unsupported BigInt operation")),
1965        };
1966        Ok(self.new_bigint(r))
1967    }
1968
1969    /// BigInt ⇄ (Number | Boolean | String | Object) loose equality (`==`). Both
1970    /// being BigInt was already handled by `strict_eq`.
1971    fn bigint_loose_eq(&self, a: &Value, b: &Value) -> bool {
1972        // Order so `big` is the BigInt side and `other` the counterpart.
1973        let (big, other) = match (self.as_bigint(a), self.as_bigint(b)) {
1974            (Some(x), _) => (x, b),
1975            (_, Some(y)) => (y, a),
1976            _ => return false,
1977        };
1978        match other {
1979            Value::Bool(bo) => big == num_bigint::BigInt::from(*bo as i64),
1980            Value::Int(n) => big == num_bigint::BigInt::from(*n),
1981            Value::Float(f) => {
1982                // Equal only when the float is an integer with the same value.
1983                if !f.is_finite() || f.fract() != 0.0 {
1984                    return false;
1985                }
1986                bigint_to_f64(&big) == *f
1987            }
1988            Value::Str(s) => match parse_bigint_str(s) {
1989                Some(bs) => big == bs,
1990                None => false,
1991            },
1992            Value::Obj(_) => match self.get(other) {
1993                // A heap string parses like a primitive string.
1994                Some(JsObj::Str(s)) => parse_bigint_str(s).map(|bs| big == bs).unwrap_or(false),
1995                _ => {
1996                    // Other objects reduce via ToPrimitive (their string form).
1997                    let s = self.str_of(other);
1998                    parse_bigint_str(&s).map(|bs| big == bs).unwrap_or(false)
1999                }
2000            },
2001            _ => false,
2002        }
2003    }
2004}
2005
2006/// Parse a string to a BigInt under JS `StringToBigInt` rules: trimmed, empty →
2007/// `0n`, decimal or `0x`/`0o`/`0b` prefixed; any junk → `None`.
2008pub fn parse_bigint_str(s: &str) -> Option<num_bigint::BigInt> {
2009    let t = s.trim();
2010    if t.is_empty() {
2011        return Some(num_bigint::BigInt::from(0));
2012    }
2013    let (radix, digits) = if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
2014        (16, h)
2015    } else if let Some(o) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
2016        (8, o)
2017    } else if let Some(bb) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
2018        (2, bb)
2019    } else {
2020        (10, t)
2021    };
2022    num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
2023}
2024
2025/// Coerce a BigInt to `f64` (for `Number(bigint)` and mixed relational compares);
2026/// out-of-range magnitudes become ±Infinity, matching Node.
2027fn bigint_to_f64(b: &num_bigint::BigInt) -> f64 {
2028    num_traits::ToPrimitive::to_f64(b).unwrap_or_else(|| {
2029        if num_traits::Signed::is_negative(b) {
2030            f64::NEG_INFINITY
2031        } else {
2032            f64::INFINITY
2033        }
2034    })
2035}
2036
2037/// JS `%` remainder (sign follows the dividend; matches `f64::rem`).
2038fn js_mod(a: f64, b: f64) -> f64 {
2039    a % b
2040}
2041
2042thread_local! {
2043    /// The active `util.inspect` `depth` (nesting levels shown before collapsing
2044    /// to `[Object]`/`[Array]`). Node's default is 2; `util.inspect(v,{depth:N})`
2045    /// overrides it for one call, `console.log`/`util.format` use the default.
2046    static INSPECT_MAX_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(2) };
2047}
2048
2049/// Set the `util.inspect` depth for the next render (restore to 2 after).
2050pub fn set_inspect_max_depth(d: usize) {
2051    INSPECT_MAX_DEPTH.with(|c| c.set(d));
2052}
2053fn inspect_max_depth() -> usize {
2054    INSPECT_MAX_DEPTH.with(|c| c.get())
2055}
2056
2057fn to_int32(f: f64) -> i32 {
2058    if !f.is_finite() {
2059        return 0;
2060    }
2061    let n = f.trunc();
2062    (n as i64 as u32) as i32
2063}
2064fn to_uint32(f: f64) -> u32 {
2065    if !f.is_finite() {
2066        return 0;
2067    }
2068    f.trunc() as i64 as u32
2069}
2070
2071/// Parse a string in numeric context (`ToNumber`): trimmed, empty -> 0.
2072fn str_to_number(s: &str) -> f64 {
2073    let t = s.trim();
2074    if t.is_empty() {
2075        return 0.0;
2076    }
2077    if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
2078        return i64::from_str_radix(hex, 16)
2079            .map(|n| n as f64)
2080            .unwrap_or(f64::NAN);
2081    }
2082    if let Some(oct) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
2083        return i64::from_str_radix(oct, 8)
2084            .map(|n| n as f64)
2085            .unwrap_or(f64::NAN);
2086    }
2087    if let Some(bin) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
2088        return i64::from_str_radix(bin, 2)
2089            .map(|n| n as f64)
2090            .unwrap_or(f64::NAN);
2091    }
2092    match t {
2093        "Infinity" | "+Infinity" => f64::INFINITY,
2094        "-Infinity" => f64::NEG_INFINITY,
2095        _ => t.parse::<f64>().unwrap_or(f64::NAN),
2096    }
2097}
2098
2099/// `util.inspect` break length (the width past which entries wrap). Node's default.
2100const BREAK_LENGTH: usize = 80;
2101/// Node's default `compact` setting (the `compact * 4` column cap term).
2102const COMPACT: usize = 3;
2103
2104/// Whether `output` fits on a single line — a faithful port of Node's
2105/// `isBelowBreakLength` (no colors, no `base`). `start` is the caller's seed
2106/// length (braces + indentation + slack).
2107fn is_below_break_length(output: &[String], start: usize) -> bool {
2108    let mut total = output.len() + start;
2109    if total + output.len() > BREAK_LENGTH {
2110        return false;
2111    }
2112    for o in output {
2113        if o.contains('\n') {
2114            return false;
2115        }
2116        total += o.chars().count();
2117        if total > BREAK_LENGTH {
2118            return false;
2119        }
2120    }
2121    true
2122}
2123
2124/// Faithful port of Node's `util.inspect` `groupArrayElements`: lay out the
2125/// already-formatted element strings into an aligned multi-column grid. Returns
2126/// `(lines, grouped)` — `grouped` is false when Node would leave the output
2127/// ungrouped (so the caller falls back to single-line / one-per-line).
2128fn group_array_elements(
2129    host: &JsHost,
2130    output: &[String],
2131    values: &[Value],
2132    indentation_lvl: usize,
2133) -> (Vec<String>, bool) {
2134    let separator_space = 2usize; // ", " between entries
2135    let output_length = output.len();
2136    let data_len: Vec<usize> = output.iter().map(|o| o.chars().count()).collect();
2137    let mut total_length = 0usize;
2138    let mut max_length = 0usize;
2139    for &len in &data_len {
2140        total_length += len + separator_space;
2141        if len > max_length {
2142            max_length = len;
2143        }
2144    }
2145    let actual_max = max_length + separator_space;
2146    // Only group when ≥3 entries fit across AND the entries aren't wildly uneven.
2147    if !(actual_max * 3 + indentation_lvl < BREAK_LENGTH
2148        && (total_length as f64 / actual_max as f64 > 5.0 || max_length <= 6))
2149    {
2150        return (output.to_vec(), false);
2151    }
2152    let approx_char_heights = 2.5f64;
2153    let average_bias = (actual_max as f64 - total_length as f64 / output_length as f64).sqrt();
2154    let biased_max = (actual_max as f64 - 3.0 - average_bias).max(1.0);
2155    // Ideally a square grid; capped by break length, compact*4, and 15 columns.
2156    let columns = [
2157        ((approx_char_heights * biased_max * output_length as f64).sqrt() / biased_max).round()
2158            as i64,
2159        ((BREAK_LENGTH - indentation_lvl) as f64 / actual_max as f64).floor() as i64,
2160        (COMPACT * 4) as i64,
2161        15,
2162    ]
2163    .into_iter()
2164    .min()
2165    .unwrap();
2166    if columns <= 1 {
2167        return (output.to_vec(), false);
2168    }
2169    let columns = columns as usize;
2170    // The widest entry (plus separator) in each column.
2171    let mut max_line_length = vec![0usize; columns];
2172    for (i, slot) in max_line_length.iter_mut().enumerate() {
2173        let mut line_length = 0;
2174        let mut j = i;
2175        while j < output_length {
2176            if data_len[j] > line_length {
2177                line_length = data_len[j];
2178            }
2179            j += columns;
2180        }
2181        *slot = line_length + separator_space;
2182    }
2183    // Right-align (padStart) only when every element is a number/bigint.
2184    let pad_start = values.iter().all(|v| {
2185        matches!(v, Value::Int(_) | Value::Float(_))
2186            || matches!(host.get(v), Some(JsObj::BigInt(_)))
2187    });
2188    let mut tmp = Vec::new();
2189    let mut i = 0;
2190    while i < output_length {
2191        let max = (i + columns).min(output_length);
2192        let mut str_line = String::new();
2193        let mut j = i;
2194        while j < max.saturating_sub(1) {
2195            // `output[j]` has no colors here, so padding == max_line_length[col].
2196            let col = j - i;
2197            let cell = format!("{}, ", output[j]);
2198            let target = max_line_length[col];
2199            str_line.push_str(&pad_to(&cell, target, pad_start));
2200            j += 1;
2201        }
2202        // The last cell of the row: right-aligned entries pad without the ", ".
2203        if pad_start {
2204            let col = j - i;
2205            let target = max_line_length[col] - separator_space;
2206            str_line.push_str(&pad_to(&output[j], target, true));
2207        } else {
2208            str_line.push_str(&output[j]);
2209        }
2210        tmp.push(str_line);
2211        i += columns;
2212    }
2213    (tmp, true)
2214}
2215
2216/// Pad `s` to `width` chars: right-justified when `pad_start`, else left-justified.
2217/// (Padding is measured in chars; already ANSI-free here.)
2218fn pad_to(s: &str, width: usize, pad_start: bool) -> String {
2219    let len = s.chars().count();
2220    if len >= width {
2221        return s.to_string();
2222    }
2223    let fill = " ".repeat(width - len);
2224    if pad_start {
2225        format!("{fill}{s}")
2226    } else {
2227        format!("{s}{fill}")
2228    }
2229}
2230
2231/// Quote a string the way `util.inspect` does (single quotes, escaped).
2232fn quote_str(s: &str) -> String {
2233    let mut out = String::from("'");
2234    for c in s.chars() {
2235        match c {
2236            '\'' => out.push_str("\\'"),
2237            '\\' => out.push_str("\\\\"),
2238            '\n' => out.push_str("\\n"),
2239            '\t' => out.push_str("\\t"),
2240            '\r' => out.push_str("\\r"),
2241            _ => out.push(c),
2242        }
2243    }
2244    out.push('\'');
2245    out
2246}
2247
2248/// Render an object key: bare if it is a valid identifier, quoted otherwise.
2249fn fmt_key(k: &str) -> String {
2250    let ok = !k.is_empty()
2251        && k.chars()
2252            .next()
2253            .map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
2254            .unwrap_or(false)
2255        && k.chars()
2256            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
2257    if ok {
2258        k.to_string()
2259    } else {
2260        quote_str(k)
2261    }
2262}
2263
2264// ── iteration ────────────────────────────────────────────────────────────────
2265
2266impl JsHost {
2267    /// Collect an iterable into a vector of values (arrays, strings, Map/Set).
2268    /// Generators and user `Symbol.iterator` objects go through `iter_all`, which
2269    /// holds no host borrow across resumes.
2270    pub fn iter_vec(&mut self, v: &Value) -> Result<Vec<Value>, String> {
2271        match self.get(v) {
2272            Some(JsObj::Array(items)) => Ok(items.clone()),
2273            Some(JsObj::Str(s)) => {
2274                let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
2275                Ok(chars.into_iter().map(|c| self.new_str(c)).collect())
2276            }
2277            Some(JsObj::Iter { items, idx }) => Ok(items[*idx..].to_vec()),
2278            Some(JsObj::Set { entries, .. }) => Ok(entries.values().cloned().collect()),
2279            Some(JsObj::Map { entries, .. }) => {
2280                // Map iterates as `[key, value]` pairs.
2281                let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
2282                Ok(pairs
2283                    .into_iter()
2284                    .map(|(k, v)| self.new_array(vec![k, v]))
2285                    .collect())
2286            }
2287            _ => Err(type_error(&format!("{} is not iterable", self.type_of(v)))),
2288        }
2289    }
2290
2291    /// Enumerable string keys of an object/array (for `for-in`). Internal
2292    /// symbol-keyed props (`@@…`) are not enumerable.
2293    pub fn enum_keys(&mut self, v: &Value) -> Vec<Value> {
2294        let keys: Vec<String> = match self.get(v) {
2295            Some(JsObj::Object(props)) => props
2296                .keys()
2297                .filter(|k| !k.starts_with("@@") && !k.starts_with('#'))
2298                .cloned()
2299                .collect(),
2300            Some(JsObj::Array(items)) => (0..items.len()).map(|i| i.to_string()).collect(),
2301            _ => Vec::new(),
2302        };
2303        keys.into_iter().map(|k| self.new_str(k)).collect()
2304    }
2305}
2306
2307// ── function invocation ──────────────────────────────────────────────────────
2308
2309/// Marshal a JS call argument into a native fusevm `Value` for `rust { }` FFI.
2310/// JS strings ride as `Value::Obj(JsObj::Str)` heap handles, which fusevm's
2311/// marshaller cannot read (it calls `Value::to_str`, which returns `"(obj:N)"`
2312/// for a handle); rewrite them to a native `Value::Str`. Numbers are already
2313/// native `Value::Int`/`Value::Float`, so they pass through (fusevm coerces
2314/// Float→i64/f64 per the export signature).
2315fn marshal_ffi_arg(v: &Value) -> Value {
2316    match v {
2317        Value::Obj(_) => match with_host(|h| h.as_str(v)) {
2318            Some(s) => Value::str(s),
2319            None => v.clone(),
2320        },
2321        _ => v.clone(),
2322    }
2323}
2324
2325/// Resolve a bare name and call it (`f(args)`, `parseInt(args)`).
2326pub fn call_named(name: &str, args: Vec<Value>) -> Result<Value, String> {
2327    // Inline Rust FFI: the `rust { ... }` desugar emits `__rust_compile(b64,
2328    // line)`; compile + register the block's exported functions, returning JS
2329    // `undefined` (`Value::Undef`).
2330    if name == "__rust_compile" {
2331        let b64 = args
2332            .first()
2333            .map(|v| with_host(|h| h.str_of(v)))
2334            .unwrap_or_default();
2335        return fusevm::ffi::compile_and_register(&b64).map(|_| Value::Undef);
2336    }
2337    if let Some(v) = with_host(|h| h.read_name(name)) {
2338        return invoke(&v, args, None);
2339    }
2340    if crate::builtins::is_known_builtin(name) {
2341        return crate::builtins::call_builtin_function(name, args);
2342    }
2343    // A `rust { ... }` block's exported functions are callable by bareword.
2344    // Reached only after user names/globals and builtins all miss, so JS code
2345    // always wins; the registry membership check keeps this off the hot path.
2346    if fusevm::ffi::is_registered(name) {
2347        let margs: Vec<Value> = args.iter().map(marshal_ffi_arg).collect();
2348        if let Some(r) = fusevm::ffi::try_call(name, &margs) {
2349            return r;
2350        }
2351    }
2352    Err(ref_error(name))
2353}
2354
2355/// `recv.name(args)`.
2356pub fn call_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
2357    // Namespace builtins (`console`, `Math`, `JSON`, ...): dispatch by qualified
2358    // name.
2359    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(recv).cloned()) {
2360        let qualified = format!("{ns}.{name}");
2361        if crate::builtins::is_known_builtin(&qualified) {
2362            return crate::builtins::call_builtin_function(&qualified, args);
2363        }
2364    }
2365    // Object / instance: an accessor getter that yields a function, an own or
2366    // inherited method (class methods live on the prototype chain), then an
2367    // Object.prototype builtin (hasOwnProperty …). Resolve via `lookup_*`
2368    // directly — NOT get_property — so the Object.prototype-builtin fallback
2369    // never routes back through a BoundMethod and recurses.
2370    if matches!(with_host(|h| h.get(recv).cloned()), Some(JsObj::Object(_))) {
2371        // A native stdlib instance (`Buffer`/crypto `Hash`/`EventEmitter`/`URL`/
2372        // fs `Stats`/http `ServerResponse`…) carries a hidden `@@native` tag.
2373        // A user-added or reparented-prototype method takes precedence over the
2374        // native dispatcher — matching JS resolution order (own → prototype
2375        // chain). This is what lets Express work: it does
2376        // `Object.setPrototypeOf(res, app.response)` and calls `res.send(...)`,
2377        // where `send` is a plain function on the reparented prototype. Native
2378        // instance methods (`res.end`/`write`/…) are NOT stored as plain
2379        // function properties, so `lookup_chain` misses them and we fall through
2380        // to `instance_call` for the real native behavior.
2381        if let Some(tag) = crate::stdlib::native_tag(recv) {
2382            if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
2383                if with_host(|h| is_callable(h, &f)) {
2384                    return invoke(&f, args, Some(recv.clone()));
2385                }
2386            }
2387            return crate::stdlib::instance_call(&tag, recv, name, args);
2388        }
2389        if let Some((Some(getter), _)) = with_host(|h| lookup_accessor(h, recv, name)) {
2390            let f = invoke(&getter, Vec::new(), Some(recv.clone()))?;
2391            if with_host(|h| is_callable(h, &f)) {
2392                return invoke(&f, args, Some(recv.clone()));
2393            }
2394        }
2395        if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
2396            if with_host(|h| is_callable(h, &f)) {
2397                return invoke(&f, args, Some(recv.clone()));
2398            }
2399            return Err(type_error(&format!("{name} is not a function")));
2400        }
2401        if crate::builtins::is_object_builtin_method(name) {
2402            return crate::builtins::object_builtin_method(recv, name, args);
2403        }
2404        return Err(type_error(&format!("{name} is not a function")));
2405    }
2406    // Function value methods: call / apply / bind, then any static method stored
2407    // on the function object.
2408    if matches!(
2409        with_host(|h| h.get(recv).cloned()),
2410        Some(JsObj::Func(_))
2411            | Some(JsObj::Class(_))
2412            | Some(JsObj::BoundFunc { .. })
2413            | Some(JsObj::BoundMethod { .. })
2414            | Some(JsObj::Builtin(_))
2415    ) {
2416        if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
2417            return Ok(r);
2418        }
2419        // A static method (own or inherited): `this` is the constructor (`recv`).
2420        let stat = if matches!(with_host(|h| h.get(recv).cloned()), Some(JsObj::Class(_))) {
2421            with_host(|h| h.class_static(recv, name))
2422        } else {
2423            with_host(|h| h.fn_prop(recv, name))
2424        };
2425        if let Some(f) = stat {
2426            if with_host(|h| is_callable(h, &f)) {
2427                return invoke(&f, args, Some(recv.clone()));
2428            }
2429        }
2430        // A method inherited via the function's [[Prototype]] chain (set with
2431        // `Object.setPrototypeOf(fn, proto)`) — the `router` package's router
2432        // functions inherit `route`/`use`/`get`/… from `Router.prototype`.
2433        if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
2434            if with_host(|h| is_callable(h, &f)) {
2435                return invoke(&f, args, Some(recv.clone()));
2436            }
2437        }
2438        // An `Object.prototype` method invoked with a builtin namespace/prototype
2439        // as `this` (`hasOwnProperty.call(Map.prototype, 'get')`, the get-intrinsic
2440        // ownership probe) — dispatch it against the builtin receiver.
2441        if matches!(with_host(|h| h.get(recv).cloned()), Some(JsObj::Builtin(_)))
2442            && crate::builtins::is_object_builtin_method(name)
2443        {
2444            return crate::builtins::object_builtin_method(recv, name, args);
2445        }
2446    }
2447    // Type methods (array/string/number, Map/Set/Symbol/generator methods).
2448    crate::builtins::call_type_method(recv, name, args)
2449}
2450
2451/// Call any callable value.
2452pub fn invoke(callable: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
2453    let obj = with_host(|h| h.get(callable).cloned());
2454    match obj {
2455        // A builtin-prototype method thunk (`Object.prototype.toString`): dispatch
2456        // against the invoke-time `this` (supplied by `.call`/`.apply`).
2457        Some(JsObj::Builtin(name)) if name.starts_with("@proto:") => {
2458            let recv = this.unwrap_or(Value::Undef);
2459            crate::builtins::proto_method(&recv, &name["@proto:".len()..], args)
2460        }
2461        Some(JsObj::Builtin(name)) => crate::builtins::call_builtin_function(&name, args),
2462        Some(JsObj::Func(fv)) => run_user_func(&fv, args, this),
2463        Some(JsObj::BoundMethod { recv, name }) => call_method(&recv, &name, args),
2464        Some(JsObj::BoundFunc {
2465            target,
2466            this: bthis,
2467            args: pre,
2468        }) => {
2469            let mut all = pre;
2470            all.extend(args);
2471            invoke(&target, all, Some(bthis))
2472        }
2473        Some(JsObj::Class(c)) => Err(type_error(&format!(
2474            "Class constructor {} cannot be invoked without 'new'",
2475            c.name
2476        ))),
2477        _ => Err(type_error(&format!(
2478            "{} is not a function",
2479            with_host(|h| h.str_of(callable))
2480        ))),
2481    }
2482}
2483
2484/// Execute a user function/closure body on a fresh frame.
2485pub fn run_user_func(fv: &FuncVal, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
2486    run_user_func_nt(fv, args, this, None)
2487}
2488
2489/// As `run_user_func`, but with an explicit `new.target` (set by `new`).
2490pub fn run_user_func_nt(
2491    fv: &FuncVal,
2492    args: Vec<Value>,
2493    this: Option<Value>,
2494    new_target: Option<Value>,
2495) -> Result<Value, String> {
2496    let def = with_host(|h| h.funcs[fv.def_id].clone());
2497    let env = new_env(fv.env.clone());
2498    // Bind the simple/rest arg slots; destructuring + defaults run in the body
2499    // prologue (compiled ahead of the user statements).
2500    bind_params(&env, &def, args);
2501    // Arrow functions capture `this` lexically; regular functions receive it.
2502    let this_val = if fv.is_arrow { fv.this.clone() } else { this };
2503    // A generator function does not run its body on call — it returns a suspended
2504    // generator over the already-bound frame.
2505    if def.is_generator {
2506        return Ok(make_generator(
2507            def.chunk.clone(),
2508            env,
2509            this_val,
2510            fv.home_class.clone(),
2511        ));
2512    }
2513    // An async function runs on a coroutine and returns a Promise: it executes
2514    // synchronously up to the first `await`, then continues via microtasks.
2515    if def.is_async {
2516        let gen = make_generator(def.chunk.clone(), env, this_val, fv.home_class.clone());
2517        return Ok(run_async(gen));
2518    }
2519    let home = fv
2520        .home_class
2521        .as_ref()
2522        .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
2523    with_host(|h| {
2524        h.frames.push(Frame {
2525            env,
2526            this_obj: this_val,
2527            new_target,
2528            home_class: home,
2529            line: 0,
2530            owner: Some(def.name.clone()),
2531        })
2532    });
2533    let r = run_chunk_on(def.chunk.clone());
2534    let sig = with_host(|h| {
2535        h.frames.pop();
2536        h.signal.take()
2537    });
2538    match r {
2539        Err(e) => Err(e),
2540        Ok(_) => Ok(match sig {
2541            Some(Signal::Return(v)) => v,
2542            _ => Value::Undef,
2543        }),
2544    }
2545}
2546
2547/// Bind positional args into a fresh call environment. The compiler emits the
2548/// param names in `def.params`; a `...rest` slot collects the tail as an array.
2549fn bind_params(env: &Env, def: &FuncDef, args: Vec<Value>) {
2550    let mut vars: IndexMap<String, Value> = IndexMap::new();
2551    let mut i = 0;
2552    for slot in &def.params {
2553        if slot.rest {
2554            let rest: Vec<Value> = args.get(i..).map(|s| s.to_vec()).unwrap_or_default();
2555            let arr = with_host(|h| h.new_array(rest));
2556            vars.insert(slot.name.clone(), arr);
2557        } else {
2558            let v = args.get(i).cloned().unwrap_or(Value::Undef);
2559            vars.insert(slot.name.clone(), v);
2560            i += 1;
2561        }
2562    }
2563    // `arguments` array (simple approximation).
2564    let args_arr = with_host(|h| h.new_array(args));
2565    vars.entry("arguments".to_string()).or_insert(args_arr);
2566    env.borrow_mut().vars = vars;
2567}
2568
2569/// Construct an instance with `new` — creates a fresh object, binds it as
2570/// `this`, runs the constructor, and returns the object (unless the constructor
2571/// returns its own object).
2572pub fn construct(ctor: &Value, args: Vec<Value>) -> Result<Value, String> {
2573    construct_nt(ctor, args, ctor.clone())
2574}
2575
2576/// `new` with an explicit `new.target` (differs from `ctor` when a derived class
2577/// calls `super(...)` — the target stays the originally-`new`ed class).
2578pub fn construct_nt(ctor: &Value, args: Vec<Value>, new_target: Value) -> Result<Value, String> {
2579    let obj = with_host(|h| h.get(ctor).cloned());
2580    match obj {
2581        Some(JsObj::Class(_)) => construct_class(ctor, args, new_target),
2582        Some(JsObj::Func(fv)) => {
2583            // A plain constructor function: instance delegates to `fn.prototype`
2584            // (auto-created with a `.constructor` back-link if not yet accessed).
2585            let inst = with_host(|h| {
2586                let o = h.new_object(IndexMap::new());
2587                let proto = h.fn_prop(ctor, "prototype").unwrap_or_else(|| {
2588                    let p = h.new_object(IndexMap::new());
2589                    if let Some(JsObj::Object(pp)) = h.get_mut(&p) {
2590                        pp.insert("constructor".to_string(), ctor.clone());
2591                    }
2592                    h.set_fn_prop(ctor, "prototype", p.clone());
2593                    p
2594                });
2595                h.set_proto(&o, proto);
2596                o
2597            });
2598            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target))?;
2599            if returns_object(&r) {
2600                Ok(r)
2601            } else {
2602                Ok(inst)
2603            }
2604        }
2605        Some(JsObj::Builtin(name)) => crate::builtins::construct_builtin(&name, args),
2606        Some(JsObj::BoundFunc {
2607            target, args: pre, ..
2608        }) => {
2609            let mut all = pre;
2610            all.extend(args);
2611            construct_nt(&target, all, new_target)
2612        }
2613        _ => Err(type_error(&format!(
2614            "{} is not a constructor",
2615            with_host(|h| h.str_of(ctor))
2616        ))),
2617    }
2618}
2619
2620/// Whether a constructor's return value is an object (so `new` yields it instead
2621/// of the fresh instance). In JS "object" includes functions — the `router`
2622/// package's constructor `return router` (a function) must be honored, or the
2623/// returned router loses its callable identity.
2624fn returns_object(r: &Value) -> bool {
2625    matches!(
2626        with_host(|h| h.get(r).cloned()),
2627        Some(JsObj::Object(_))
2628            | Some(JsObj::Array(_))
2629            | Some(JsObj::Map { .. })
2630            | Some(JsObj::Set { .. })
2631            | Some(JsObj::Func(_))
2632            | Some(JsObj::Class(_))
2633            | Some(JsObj::BoundFunc { .. })
2634            | Some(JsObj::BoundMethod { .. })
2635            | Some(JsObj::RegExp(_))
2636    )
2637}
2638
2639/// Construct a `class` instance: allocate the object linked to `C.prototype`,
2640/// run field initializers + the constructor (which may call `super(...)`).
2641fn construct_class(
2642    class_val: &Value,
2643    args: Vec<Value>,
2644    new_target: Value,
2645) -> Result<Value, String> {
2646    let cv = match with_host(|h| h.get(class_val).cloned()) {
2647        Some(JsObj::Class(c)) => c,
2648        _ => return Err(type_error("not a class")),
2649    };
2650    // Resolve the prototype of the *most-derived* class being `new`ed, so an
2651    // instance created through a `super()` chain still delegates to the leaf
2652    // prototype (correct method resolution).
2653    let leaf_proto = match with_host(|h| h.get(&new_target).cloned()) {
2654        Some(JsObj::Class(c)) => c.proto.clone(),
2655        _ => cv.proto.clone(),
2656    };
2657    let inst = with_host(|h| {
2658        let o = h.new_object(IndexMap::new());
2659        h.set_proto(&o, leaf_proto.clone());
2660        o
2661    });
2662    // A constructor that returns an object replaces the instance (`new` semantics).
2663    match run_class_ctor(&cv, &inst, args, &new_target)? {
2664        Some(obj) if returns_object(&obj) => Ok(obj),
2665        _ => Ok(inst),
2666    }
2667}
2668
2669/// Run one class's field initializers then its constructor on an existing
2670/// instance. Returns the constructor's explicit object return (if any). For a
2671/// base class this is the whole init; for a derived class the constructor body
2672/// reaches `super(...)` which recurses into the parent.
2673fn run_class_ctor(
2674    cv: &ClassVal,
2675    inst: &Value,
2676    args: Vec<Value>,
2677    new_target: &Value,
2678) -> Result<Option<Value>, String> {
2679    // A derived class must run its fields AFTER super() returns; SUPER_CALL does
2680    // that. A base class initializes fields before the constructor body.
2681    if cv.parent.is_none() {
2682        init_fields(cv, inst)?;
2683    }
2684    match &cv.ctor {
2685        Some(ctor_fn) => {
2686            let fv = match with_host(|h| h.get(ctor_fn).cloned()) {
2687                Some(JsObj::Func(f)) => f,
2688                _ => return Err(type_error("class constructor is not a function")),
2689            };
2690            let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
2691            return Ok(Some(r));
2692        }
2693        None => {
2694            // Default constructor: `constructor(...a){ super(...a); }` for a
2695            // derived class, empty for a base class.
2696            if let Some(parent) = &cv.parent {
2697                super_construct(parent, args, inst, new_target)?;
2698                init_fields(cv, inst)?;
2699            }
2700        }
2701    }
2702    Ok(None)
2703}
2704
2705/// Evaluate and assign a class's instance-field initializers on `inst`.
2706fn init_fields(cv: &ClassVal, inst: &Value) -> Result<(), String> {
2707    for (name, thunk) in &cv.fields {
2708        // The thunk is an arrow capturing the class scope; run it with `this`=inst
2709        // so `this.other`-referencing initializers work.
2710        let val = invoke(thunk, Vec::new(), Some(inst.clone()))?;
2711        with_host(|h| {
2712            if let Some(JsObj::Object(props)) = h.get_mut(inst) {
2713                let is_new = !props.contains_key(name);
2714                props.insert(name.clone(), val);
2715                if is_new && array_index(name).is_some() {
2716                    canonicalize_own_keys(props);
2717                }
2718            }
2719        });
2720    }
2721    Ok(())
2722}
2723
2724/// Run a parent constructor as part of `super(...)`: dispatch on the parent's
2725/// kind (class vs plain function vs builtin) using the existing instance.
2726pub fn super_construct(
2727    parent: &Value,
2728    args: Vec<Value>,
2729    inst: &Value,
2730    new_target: &Value,
2731) -> Result<(), String> {
2732    match with_host(|h| h.get(parent).cloned()) {
2733        Some(JsObj::Class(pcv)) => run_class_ctor(&pcv, inst, args, new_target).map(|_| ()),
2734        Some(JsObj::Func(fv)) => {
2735            run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
2736            Ok(())
2737        }
2738        Some(JsObj::Builtin(name)) => {
2739            // Extending a builtin (e.g. `class E extends Error`): copy the built
2740            // object's own props onto the instance so the subclass instance
2741            // carries them.
2742            let built = crate::builtins::construct_builtin(&name, args)?;
2743            let entries: Vec<(String, Value)> = with_host(|h| match h.get(&built) {
2744                Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
2745                _ => Vec::new(),
2746            });
2747            with_host(|h| {
2748                if let Some(JsObj::Object(props)) = h.get_mut(inst) {
2749                    for (k, v) in entries {
2750                        props.insert(k, v);
2751                    }
2752                    canonicalize_own_keys(props);
2753                }
2754            });
2755            Ok(())
2756        }
2757        _ => Err(type_error("super is not a constructor")),
2758    }
2759}
2760
2761// ── class construction (runtime) ─────────────────────────────────────────────
2762
2763/// Build a class constructor value from its parts. The compiler emits (via
2764/// `MKCLASS`) the evaluated parent (or undefined) and the constructor closure (or
2765/// undefined for a default constructor); methods/getters/setters/statics/fields
2766/// are installed afterward by `DEF_MEMBER`/`DEF_FIELD`.
2767pub fn build_class(name: &str, parent: Value, ctor: Value) -> Value {
2768    with_host(|h| {
2769        let parent_opt = if matches!(parent, Value::Undef) {
2770            None
2771        } else {
2772            Some(parent.clone())
2773        };
2774        // The class prototype delegates to the parent's prototype (or
2775        // Object.prototype for a base class). Extending a builtin error links to
2776        // that error's prototype so `instanceof Error` holds for the subclass.
2777        let parent_proto = match &parent_opt {
2778            Some(p) => match h.get(p).cloned() {
2779                Some(JsObj::Class(pc)) => pc.proto.clone(),
2780                Some(JsObj::Builtin(bn)) => {
2781                    h.ensure_error_protos();
2782                    error_proto_of(h, &bn)
2783                        .or_else(|| h.fn_prop(p, "prototype"))
2784                        .unwrap_or_else(|| h.object_proto())
2785                }
2786                _ => h
2787                    .fn_prop(p, "prototype")
2788                    .unwrap_or_else(|| h.object_proto()),
2789            },
2790            None => h.object_proto(),
2791        };
2792        let proto = h.new_object(IndexMap::new());
2793        h.set_proto(&proto, parent_proto);
2794        let ctor_opt = if matches!(ctor, Value::Undef) {
2795            None
2796        } else {
2797            Some(ctor.clone())
2798        };
2799        // Give the constructor closure its home class (for `super.method()`), and
2800        // record its `.name`.
2801        if let Some(cf) = &ctor_opt {
2802            if let Some(JsObj::Func(f)) = h.get_mut(cf) {
2803                f.home_class = Some(name.to_string());
2804            }
2805        }
2806        let cval = ClassVal {
2807            name: name.to_string(),
2808            ctor: ctor_opt,
2809            parent: parent_opt,
2810            proto: proto.clone(),
2811            statics: IndexMap::new(),
2812            fields: Vec::new(),
2813        };
2814        let class_val = h.alloc(JsObj::Class(cval));
2815        h.class_registry.insert(name.to_string(), class_val.clone());
2816        // Link prototype → class (for instance display + `constructor`), and give
2817        // the class its own `prototype` fn-prop so `C.prototype` reads work.
2818        h.tag_proto_class(&proto, class_val.clone());
2819        h.set_fn_prop(&class_val, "prototype", proto.clone());
2820        // `Class.prototype.constructor === Class`.
2821        if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
2822            p.insert("constructor".to_string(), class_val.clone());
2823        }
2824        class_val
2825    })
2826}
2827
2828/// Install a method / getter / setter on a class (`DEF_MEMBER`). `kind` is a
2829/// `member::*` tag; `is_static` targets the constructor side.
2830pub fn define_member(class_val: &Value, name: &str, kind: i64, is_static: bool, func: Value) {
2831    with_host(|h| {
2832        let cname = match h.get(class_val) {
2833            Some(JsObj::Class(c)) => c.name.clone(),
2834            _ => String::new(),
2835        };
2836        // Give the method its home class for `super.x()`.
2837        if let Some(JsObj::Func(f)) = h.get_mut(&func) {
2838            f.home_class = Some(cname);
2839        }
2840        // Static members live on the constructor (fn-props / static accessors);
2841        // instance members on the prototype.
2842        let target = if is_static {
2843            class_val.clone()
2844        } else {
2845            match h.get(class_val) {
2846                Some(JsObj::Class(c)) => c.proto.clone(),
2847                _ => return,
2848            }
2849        };
2850        match kind {
2851            member::GET => h.set_accessor(&target, name, Some(func), None),
2852            member::SET => h.set_accessor(&target, name, None, Some(func)),
2853            _ => {
2854                if is_static {
2855                    if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
2856                        c.statics.insert(name.to_string(), func.clone());
2857                    }
2858                    h.set_fn_prop(class_val, name, func);
2859                } else if let Some(JsObj::Object(p)) = h.get_mut(&target) {
2860                    p.insert(name.to_string(), func);
2861                }
2862            }
2863        }
2864    });
2865}
2866
2867/// Register an instance-field initializer thunk on a class (`DEF_FIELD`).
2868pub fn define_field(class_val: &Value, name: &str, thunk: Value) {
2869    with_host(|h| {
2870        if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
2871            c.fields.push((name.to_string(), thunk));
2872        }
2873    });
2874}
2875
2876/// The `[[Prototype]]` object a constructor value hands to its instances
2877/// (`Ctor.prototype`), for `instanceof`.
2878fn ctor_prototype(h: &JsHost, ctor: &Value) -> Option<Value> {
2879    match h.get(ctor) {
2880        Some(JsObj::Class(c)) => Some(c.proto.clone()),
2881        Some(JsObj::Func(_)) => h.fn_prop(ctor, "prototype"),
2882        Some(JsObj::Builtin(name)) => h.error_protos.get(name).cloned(),
2883        Some(JsObj::BoundFunc { target, .. }) => ctor_prototype(h, &target.clone()),
2884        _ => None,
2885    }
2886}
2887
2888/// `obj instanceof ctor` — walk `obj`'s prototype chain looking for
2889/// `ctor.prototype`.
2890pub fn instance_of(obj: &Value, ctor: &Value) -> Result<bool, String> {
2891    // Not an object → never an instance (no error for our purposes).
2892    if !matches!(obj, Value::Obj(_)) {
2893        return Ok(false);
2894    }
2895    let ctor_callable = with_host(|h| {
2896        matches!(
2897            h.get(ctor),
2898            Some(JsObj::Func(_))
2899                | Some(JsObj::Class(_))
2900                | Some(JsObj::Builtin(_))
2901                | Some(JsObj::BoundFunc { .. })
2902        )
2903    });
2904    if !ctor_callable {
2905        return Err(type_error(
2906            "Right-hand side of 'instanceof' is not callable",
2907        ));
2908    }
2909    // Builtin constructors whose instances aren't prototype-linked in our model
2910    // (arrays/plain objects/functions) get a structural instanceof.
2911    if let Some(JsObj::Builtin(name)) = with_host(|h| h.get(ctor).cloned()) {
2912        let kind = with_host(|h| h.get(obj).cloned());
2913        match name.as_str() {
2914            "Array" => return Ok(matches!(kind, Some(JsObj::Array(_)))),
2915            "Function" => return Ok(with_host(|h| is_callable(h, obj))),
2916            // Map/Set/Promise instances are distinct heap variants, not
2917            // prototype-linked, so match them structurally (a WeakMap/WeakSet is a
2918            // Map/Set with `weak: true`, so `weakMap instanceof Map` is false).
2919            "Map" => return Ok(matches!(kind, Some(JsObj::Map { weak: false, .. }))),
2920            "WeakMap" => return Ok(matches!(kind, Some(JsObj::Map { weak: true, .. }))),
2921            "Set" => return Ok(matches!(kind, Some(JsObj::Set { weak: false, .. }))),
2922            "WeakSet" => return Ok(matches!(kind, Some(JsObj::Set { weak: true, .. }))),
2923            "Promise" => return Ok(matches!(kind, Some(JsObj::Promise { .. }))),
2924            "Object" => {
2925                // Everything object-typed except a null-prototype object is an
2926                // Object instance.
2927                let is_obj = matches!(
2928                    kind,
2929                    Some(JsObj::Object(_))
2930                        | Some(JsObj::Array(_))
2931                        | Some(JsObj::Func(_))
2932                        | Some(JsObj::Class(_))
2933                        | Some(JsObj::Map { .. })
2934                        | Some(JsObj::Set { .. })
2935                        | Some(JsObj::Promise { .. })
2936                        | Some(JsObj::Generator { .. })
2937                );
2938                if is_obj {
2939                    // A null-prototype object (Object.create(null) or
2940                    // setPrototypeOf(o, null)) is NOT an Object instance.
2941                    if with_host(|h| h.has_null_proto(obj)) {
2942                        return Ok(false);
2943                    }
2944                    return Ok(true);
2945                }
2946                return Ok(false);
2947            }
2948            // A native-tagged instance (`WeakRef`, `FinalizationRegistry`,
2949            // `TextEncoder`, …) is an instance of the builtin whose name matches
2950            // its hidden `@@native` tag.
2951            other => {
2952                if crate::stdlib::native_tag(obj).as_deref() == Some(other) {
2953                    return Ok(true);
2954                }
2955            }
2956        }
2957    }
2958    with_host(|h| h.ensure_error_protos());
2959    let target = match with_host(|h| ctor_prototype(h, ctor)) {
2960        Some(p) => p,
2961        None => return Ok(false),
2962    };
2963    let mut cur = with_host(|h| h.proto_of(obj));
2964    while let Some(p) = cur {
2965        if with_host(|h| h.strict_eq(&p, &target)) {
2966            return Ok(true);
2967        }
2968        cur = with_host(|h| h.proto_of(&p));
2969    }
2970    Ok(false)
2971}
2972
2973// ── generators (stackful coroutines, same-thread via corosensei) ─────────────
2974
2975impl JsHost {
2976    /// Swap the volatile execution context in one shot, returning the previous
2977    /// one — installs a generator's context on resume, pulls it back on suspend.
2978    fn install_gen_ctx(&mut self, mut c: GenContext) -> GenContext {
2979        std::mem::swap(&mut self.frames, &mut c.frames);
2980        std::mem::swap(&mut self.error, &mut c.error);
2981        std::mem::swap(&mut self.exc, &mut c.exc);
2982        std::mem::swap(&mut self.signal, &mut c.signal);
2983        c
2984    }
2985    pub fn is_generator_val(&self, v: &Value) -> bool {
2986        matches!(self.get(v), Some(JsObj::Generator { .. }))
2987    }
2988    pub fn gen_done(&self, id: u32) -> bool {
2989        self.generators
2990            .get(id as usize)
2991            .map(|g| g.done)
2992            .unwrap_or(true)
2993    }
2994    fn gen_started(&self, id: u32) -> bool {
2995        self.generators
2996            .get(id as usize)
2997            .map(|g| g.started)
2998            .unwrap_or(false)
2999    }
3000}
3001
3002/// Build a suspended generator whose body is `chunk`, run in a frame with the
3003/// already-bound `env`. Nothing executes until the first `gen_resume`.
3004fn make_generator(
3005    chunk: Chunk,
3006    env: Env,
3007    this_val: Option<Value>,
3008    home_class: Option<String>,
3009) -> Value {
3010    let home = home_class
3011        .as_ref()
3012        .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
3013    let frame = Frame {
3014        env,
3015        this_obj: this_val,
3016        new_target: None,
3017        home_class: home,
3018        line: 0,
3019        owner: None,
3020    };
3021    let id = with_host(|h| {
3022        let id = h.generators.len() as u32;
3023        h.generators.push(GenCell {
3024            coro: None,
3025            yielder: std::ptr::null(),
3026            ctx: GenContext {
3027                frames: vec![frame],
3028                ..GenContext::default()
3029            },
3030            done: false,
3031            started: false,
3032            inject: None,
3033        });
3034        id
3035    });
3036    let coro = corosensei::Coroutine::new(
3037        move |yielder: &corosensei::Yielder<Value, Value>, _first: Value| {
3038            // Same thread → publish the yielder so `yield` (deep in the body's VM)
3039            // can reach it. Valid for the whole body lifetime.
3040            with_host(|h| h.generators[id as usize].yielder = yielder as *const _ as *const ());
3041            let r = run_chunk_on(chunk);
3042            // A `return` inside the body leaves a Return signal carrying the final
3043            // value; capture it so `.next()` reports it as the completion value.
3044            let ret = with_host(|h| match h.signal.take() {
3045                Some(Signal::Return(v)) => v,
3046                _ => Value::Undef,
3047            });
3048            r.map(|_| ret)
3049        },
3050    );
3051    with_host(|h| h.generators[id as usize].coro = Some(coro));
3052    with_host(|h| h.alloc(JsObj::Generator { id }))
3053}
3054
3055/// `yield v` — suspend the running generator, handing `v` to the resumer; returns
3056/// the value the next `gen_resume(x)` supplies (a `.next(x)` argument).
3057pub fn gen_yield(v: Value) -> Result<Value, String> {
3058    let id = match CUR_GEN.with(|c| c.get()) {
3059        Some(id) => id,
3060        None => return Err(type_error("yield outside a generator")),
3061    };
3062    let yp = with_host(|h| h.generators[id as usize].yielder);
3063    // SAFETY: same-thread coroutine; the yielder lives for the whole body, and we
3064    // only reach here from inside that body (its stack is live).
3065    let yielder = unsafe { &*(yp as *const corosensei::Yielder<Value, Value>) };
3066    let sent = yielder.suspend(v);
3067    // On resume, a `.return(v)`/`.throw(e)` may have queued a forced completion:
3068    // convert it into a Return signal / thrown value so the body unwinds and any
3069    // `finally` runs, exactly as a source-level `return`/`throw` would.
3070    if let Some(inj) = with_host(|h| h.generators[id as usize].inject.take()) {
3071        match inj {
3072            GenInject::Return(rv) => {
3073                with_host(|h| h.signal = Some(Signal::Return(rv)));
3074                return Ok(Value::Undef);
3075            }
3076            GenInject::Throw(ev) => {
3077                let msg = with_host(|h| crate::builtins::error_string(h, &ev));
3078                with_host(|h| h.exc = Some(ev));
3079                return Err(msg);
3080            }
3081        }
3082    }
3083    Ok(sent)
3084}
3085
3086/// `generator.return(v)`: force the generator to complete, running any pending
3087/// `finally`. If it is already done (or never started) it just reports
3088/// `{value:v, done:true}` without executing the body.
3089pub fn gen_return(gen: &Value, v: Value) -> Result<GenStep, String> {
3090    let id = match with_host(|h| h.get(gen).cloned()) {
3091        Some(JsObj::Generator { id }) => id,
3092        _ => return Err(type_error("not a generator")),
3093    };
3094    // Not started yet (coro present, ctx never resumed) OR already done → no body
3095    // to unwind: complete immediately with the supplied value.
3096    let started = with_host(|h| h.gen_started(id));
3097    if with_host(|h| h.generators[id as usize].done) || !started {
3098        with_host(|h| h.generators[id as usize].done = true);
3099        return Ok(GenStep::Done(v));
3100    }
3101    with_host(|h| h.generators[id as usize].inject = Some(GenInject::Return(v)));
3102    gen_resume(gen, Value::Undef)
3103}
3104
3105/// `generator.throw(e)`: inject a throw at the suspension point, running any
3106/// pending `finally` and letting an enclosing `try/catch` in the body handle it.
3107pub fn gen_throw(gen: &Value, e: Value) -> Result<GenStep, String> {
3108    let id = match with_host(|h| h.get(gen).cloned()) {
3109        Some(JsObj::Generator { id }) => id,
3110        _ => return Err(type_error("not a generator")),
3111    };
3112    let started = with_host(|h| h.gen_started(id));
3113    if with_host(|h| h.generators[id as usize].done) || !started {
3114        // A throw into a done/unstarted generator propagates to the caller.
3115        with_host(|h| h.generators[id as usize].done = true);
3116        let msg = with_host(|h| crate::builtins::error_string(h, &e));
3117        with_host(|h| h.exc = Some(e));
3118        return Err(msg);
3119    }
3120    with_host(|h| h.generators[id as usize].inject = Some(GenInject::Throw(e)));
3121    gen_resume(gen, Value::Undef)
3122}
3123
3124/// Outcome of resuming a generator: a yielded value (not done), or the final
3125/// completion value (done).
3126pub enum GenStep {
3127    Yield(Value),
3128    Done(Value),
3129}
3130
3131/// Resume a generator until its next `yield` or its body returns. Preserves the
3132/// shared host: the coroutine is taken out so the body re-enters `with_host`
3133/// freely, and the volatile context is swapped so the caller's frames/signal
3134/// survive the switch.
3135pub fn gen_resume(gen: &Value, send: Value) -> Result<GenStep, String> {
3136    let id = match with_host(|h| h.get(gen).cloned()) {
3137        Some(JsObj::Generator { id }) => id,
3138        _ => return Err(type_error("not a generator")),
3139    };
3140    if with_host(|h| h.generators[id as usize].done) {
3141        return Ok(GenStep::Done(Value::Undef));
3142    }
3143    let mut coro = match with_host(|h| h.generators[id as usize].coro.take()) {
3144        Some(c) => c,
3145        None => return Err("TypeError: generator already executing".into()),
3146    };
3147    with_host(|h| h.generators[id as usize].started = true);
3148    let gen_ctx = with_host(|h| std::mem::take(&mut h.generators[id as usize].ctx));
3149    let caller_ctx = with_host(|h| h.install_gen_ctx(gen_ctx));
3150    let prev = CUR_GEN.with(|c| c.replace(Some(id)));
3151
3152    let out = coro.resume(send); // no host borrow held; body drives its own VM
3153
3154    CUR_GEN.with(|c| c.set(prev));
3155    let gen_ctx = with_host(|h| h.install_gen_ctx(caller_ctx));
3156    with_host(|h| {
3157        h.generators[id as usize].ctx = gen_ctx;
3158        h.generators[id as usize].coro = Some(coro);
3159    });
3160
3161    match out {
3162        corosensei::CoroutineResult::Yield(y) => Ok(GenStep::Yield(y)),
3163        corosensei::CoroutineResult::Return(r) => {
3164            with_host(|h| h.generators[id as usize].done = true);
3165            match r {
3166                Ok(v) => Ok(GenStep::Done(v)),
3167                Err(e) => Err(e),
3168            }
3169        }
3170    }
3171}
3172
3173/// Force a generator to completion (used by `.return()` and abandoned loops):
3174/// marks it done without running further.
3175pub fn gen_close(gen: &Value) {
3176    if let Some(JsObj::Generator { id }) = with_host(|h| h.get(gen).cloned()) {
3177        with_host(|h| h.generators[id as usize].done = true);
3178    }
3179}
3180
3181// ── iteration protocol (arrays, strings, Map/Set, generators, Symbol.iterator) ─
3182
3183/// Convert a Map/Set key value into a `MapKey` under SameValueZero.
3184pub fn map_key(h: &JsHost, v: &Value) -> MapKey {
3185    match v {
3186        Value::Undef => MapKey::Undef,
3187        Value::Bool(b) => MapKey::Bool(*b),
3188        Value::Int(n) => MapKey::Num(norm_num_bits(*n as f64)),
3189        Value::Float(f) => MapKey::Num(norm_num_bits(*f)),
3190        Value::Str(s) => MapKey::Str((**s).clone()),
3191        Value::Obj(i) => match h.get(v) {
3192            Some(JsObj::Str(s)) => MapKey::Str(s.clone()),
3193            Some(JsObj::Null) => MapKey::Null,
3194            Some(JsObj::BigInt(b)) => MapKey::Big(b.to_string()),
3195            _ => MapKey::Ref(*i),
3196        },
3197        _ => MapKey::Undef,
3198    }
3199}
3200
3201/// Canonical bit pattern for a Map/Set numeric key: `NaN` → one value, `-0` → `+0`.
3202fn norm_num_bits(f: f64) -> u64 {
3203    if f.is_nan() {
3204        return f64::NAN.to_bits();
3205    }
3206    if f == 0.0 {
3207        return 0.0f64.to_bits(); // fold -0 into +0
3208    }
3209    f.to_bits()
3210}
3211
3212/// Fully materialize any iterable into a vector of values.
3213pub fn iter_all(v: &Value) -> Result<Vec<Value>, String> {
3214    // Generators / user iterators must resume without a live host borrow.
3215    if with_host(|h| h.is_generator_val(v)) {
3216        let mut out = Vec::new();
3217        while let GenStep::Yield(x) = gen_resume(v, Value::Undef)? {
3218            out.push(x);
3219        }
3220        return Ok(out);
3221    }
3222    // Object with a user-defined Symbol.iterator: drive its iterator protocol.
3223    if let Some(iter_fn) = user_iterator_fn(v) {
3224        let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
3225        return drain_iterator(&iterator);
3226    }
3227    with_host(|h| h.iter_vec(v))
3228}
3229
3230// ── async iteration (`for await (… of …)`) ───────────────────────────────────
3231
3232/// Obtain an async iterator for `for await`. If `src` has a `Symbol.asyncIterator`
3233/// method, use it (its `.next()` returns a promise of `{value, done}`); otherwise
3234/// fall back to the sync iterable, materialized into a `JsObj::Iter` whose values
3235/// are awaited one at a time by `async_step`.
3236pub fn get_async_iterator(src: &Value) -> Result<Value, String> {
3237    if let Some(f) = user_async_iterator_fn(src) {
3238        return invoke(&f, Vec::new(), Some(src.clone()));
3239    }
3240    let items = iter_all(src)?;
3241    Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
3242}
3243
3244/// If `v` has an own/inherited `Symbol.asyncIterator` method, return it.
3245fn user_async_iterator_fn(v: &Value) -> Option<Value> {
3246    let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
3247    if !is_plain {
3248        return None;
3249    }
3250    let f = with_host(|h| lookup_chain(h, v, "@@asyncIterator"));
3251    match f {
3252        Some(f) if with_host(|h| is_callable(h, &f)) => Some(f),
3253        _ => None,
3254    }
3255}
3256
3257/// One step of a `for await` loop: return a Promise that settles to a
3258/// `{value, done}` record. For a native async iterator this is `iter.next()`
3259/// (already a promise of the record). For the sync fallback it pops the next raw
3260/// value, awaits it, and packages `{value: resolved, done:false}` (or
3261/// `{done:true}` at exhaustion).
3262pub fn async_step(iterator: &Value) -> Result<Value, String> {
3263    // Sync-fallback iterator: drive it here, awaiting each yielded value.
3264    if let Some(JsObj::Iter { items, idx }) = with_host(|h| h.get(iterator).cloned()) {
3265        if idx >= items.len() {
3266            let rec = with_host(|h| {
3267                let mut m = IndexMap::new();
3268                m.insert("value".to_string(), Value::Undef);
3269                m.insert("done".to_string(), Value::Bool(true));
3270                h.new_object(m)
3271            });
3272            return Ok(promise_of(&rec));
3273        }
3274        let raw = items[idx].clone();
3275        with_host(|h| {
3276            if let Some(JsObj::Iter { idx, .. }) = h.get_mut(iterator) {
3277                *idx += 1;
3278            }
3279        });
3280        // Await the raw value (adopts a promise's resolution), then wrap.
3281        let step = with_host(|h| h.new_promise());
3282        let sid = with_host(|h| h.promise_id(&step).unwrap());
3283        let raw_p = promise_of(&raw);
3284        let raw_id = with_host(|h| h.promise_id(&raw_p).unwrap());
3285        subscribe_native(
3286            raw_id,
3287            Box::new(move |state, val| {
3288                if state == PromiseState::Rejected {
3289                    reject_promise_val(sid, val);
3290                } else {
3291                    let rec = with_host(|h| {
3292                        let mut m = IndexMap::new();
3293                        m.insert("value".to_string(), val.clone());
3294                        m.insert("done".to_string(), Value::Bool(false));
3295                        h.new_object(m)
3296                    });
3297                    resolve_promise_val(sid, rec);
3298                }
3299                Ok(())
3300            }),
3301        );
3302        return Ok(step);
3303    }
3304    // Native async iterator: `iter.next()` returns the {value,done} promise.
3305    let r = call_method(iterator, "next", Vec::new())?;
3306    Ok(promise_of(&r))
3307}
3308
3309/// If `v` has an own/inherited `Symbol.iterator` method (internal key
3310/// `@@iterator`), return it. Arrays/strings use the native fast path instead.
3311fn user_iterator_fn(v: &Value) -> Option<Value> {
3312    let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
3313    if !is_plain {
3314        return None;
3315    }
3316    let f = with_host(|h| lookup_chain(h, v, "@@iterator"));
3317    match f {
3318        Some(f) if with_host(|h| is_callable(h, &f)) => Some(f),
3319        _ => None,
3320    }
3321}
3322
3323/// Drive an iterator object (one with a `.next()` returning `{value, done}`) to
3324/// exhaustion.
3325fn drain_iterator(iterator: &Value) -> Result<Vec<Value>, String> {
3326    let mut out = Vec::new();
3327    loop {
3328        let step = call_method(iterator, "next", Vec::new())?;
3329        let done = get_prop_chain(&step, "done")?;
3330        if with_host(|h| h.truthy(&done)) {
3331            break;
3332        }
3333        out.push(get_prop_chain(&step, "value")?);
3334    }
3335    Ok(out)
3336}
3337
3338/// Property read that walks the prototype chain (used by iteration helpers).
3339pub fn get_prop_chain(recv: &Value, name: &str) -> Result<Value, String> {
3340    crate::builtins::get_property(recv, name)
3341}
3342
3343/// `ToString(v)` with `ToPrimitive` method dispatch: an object with a user
3344/// `toString` (or `valueOf`) on its prototype chain has it invoked; everything
3345/// else uses the raw `str_of`. Returns a heap string value.
3346pub fn to_string_value(v: &Value) -> Result<Value, String> {
3347    if with_host(|h| matches!(h.get(v), Some(JsObj::Object(_)))) {
3348        // Prefer a user toString; fall back to valueOf if it returns a primitive.
3349        for m in ["toString", "valueOf"] {
3350            if let Some(f) = with_host(|h| lookup_chain(h, v, m)) {
3351                if with_host(|h| is_callable(h, &f)) {
3352                    let r = invoke(&f, Vec::new(), Some(v.clone()))?;
3353                    // A primitive result is used directly; an object result from
3354                    // toString is still stringified (matches V8's OrdinaryToPrimitive
3355                    // fallthrough closely enough for our surface).
3356                    if !matches!(with_host(|h| h.get(&r).cloned()), Some(JsObj::Object(_))) {
3357                        return Ok(with_host(|h| {
3358                            let s = h.str_of(&r);
3359                            h.new_str(s)
3360                        }));
3361                    }
3362                }
3363            }
3364        }
3365    }
3366    Ok(with_host(|h| {
3367        let s = h.str_of(v);
3368        h.new_str(s)
3369    }))
3370}
3371
3372/// Whether `h.get(v)` is any callable kind.
3373pub fn is_callable(h: &JsHost, v: &Value) -> bool {
3374    matches!(
3375        h.get(v),
3376        Some(JsObj::Func(_))
3377            | Some(JsObj::Builtin(_))
3378            | Some(JsObj::BoundMethod { .. })
3379            | Some(JsObj::BoundFunc { .. })
3380            | Some(JsObj::Class(_))
3381    )
3382}
3383
3384/// Walk `recv`'s own props then its prototype chain for `key`, returning the
3385/// stored value (methods, inherited data props). Does NOT invoke accessors.
3386pub fn lookup_chain(h: &JsHost, recv: &Value, key: &str) -> Option<Value> {
3387    if let Some(JsObj::Object(p)) = h.get(recv) {
3388        if let Some(v) = p.get(key) {
3389            return Some(v.clone());
3390        }
3391    }
3392    let mut cur = h.proto_of(recv);
3393    while let Some(p) = cur {
3394        // A chain link may be a plain object OR a function/class (the `router`
3395        // package sets `Router.prototype = function(){}` and hangs its methods off
3396        // that function, so the methods live in the fn-prop side table).
3397        match h.get(&p) {
3398            Some(JsObj::Object(props)) => {
3399                if let Some(v) = props.get(key) {
3400                    return Some(v.clone());
3401                }
3402            }
3403            Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
3404                if let Some(v) = h.fn_prop(&p, key) {
3405                    return Some(v);
3406                }
3407            }
3408            _ => {}
3409        }
3410        cur = h.proto_of(&p);
3411    }
3412    None
3413}
3414
3415/// Find a getter/setter accessor for `key` on `recv` or up its prototype chain.
3416pub fn lookup_accessor(
3417    h: &JsHost,
3418    recv: &Value,
3419    key: &str,
3420) -> Option<(Option<Value>, Option<Value>)> {
3421    if let Some(a) = h.own_accessor(recv, key) {
3422        return Some(a);
3423    }
3424    let mut cur = h.proto_of(recv);
3425    while let Some(p) = cur {
3426        if let Some(a) = h.own_accessor(&p, key) {
3427            return Some(a);
3428        }
3429        cur = h.proto_of(&p);
3430    }
3431    None
3432}
3433
3434/// Register a builtin error prototype (for `instanceof Error` etc.).
3435pub fn set_error_proto(name: &str, proto: Value) {
3436    with_host(|h| {
3437        h.error_protos.insert(name.to_string(), proto);
3438    });
3439}
3440pub fn error_proto(name: &str) -> Option<Value> {
3441    with_host(|h| h.error_protos.get(name).cloned())
3442}
3443/// Error prototype lookup with a borrowed host (used inside a `with_host` block).
3444pub fn error_proto_of(h: &JsHost, name: &str) -> Option<Value> {
3445    h.error_protos.get(name).cloned()
3446}
3447
3448/// The set of builtin error constructor names forming the error hierarchy.
3449pub const ERROR_NAMES: &[&str] = &[
3450    "Error",
3451    "TypeError",
3452    "RangeError",
3453    "SyntaxError",
3454    "ReferenceError",
3455    "EvalError",
3456    "URIError",
3457];
3458
3459impl JsHost {
3460    /// Lazily build the builtin error prototype chain: `Error.prototype →
3461    /// Object.prototype`, and every specific error's prototype → `Error.prototype`.
3462    /// Populated once; instances link to these so `e instanceof TypeError` and
3463    /// `e instanceof Error` both hold.
3464    pub fn ensure_error_protos(&mut self) {
3465        if !self.error_protos.is_empty() {
3466            return;
3467        }
3468        let obj_proto = self.object_proto();
3469        // Error.prototype first (the shared base).
3470        let err_proto = self.new_object(IndexMap::new());
3471        self.set_proto(&err_proto, obj_proto);
3472        let nm = self.new_str("Error");
3473        let empty = self.new_str("");
3474        let ctor = self.alloc(JsObj::Builtin("Error".into()));
3475        if let Some(JsObj::Object(p)) = self.get_mut(&err_proto) {
3476            p.insert("name".into(), nm);
3477            p.insert("message".into(), empty);
3478            p.insert("constructor".into(), ctor);
3479        }
3480        self.error_protos.insert("Error".into(), err_proto.clone());
3481        for name in &ERROR_NAMES[1..] {
3482            let p = self.new_object(IndexMap::new());
3483            self.set_proto(&p, err_proto.clone());
3484            let nm = self.new_str(*name);
3485            let ctor = self.alloc(JsObj::Builtin((*name).to_string()));
3486            if let Some(JsObj::Object(o)) = self.get_mut(&p) {
3487                o.insert("name".into(), nm);
3488                o.insert("constructor".into(), ctor);
3489            }
3490            self.error_protos.insert((*name).to_string(), p);
3491        }
3492    }
3493}
3494
3495// ── Map/Set element access (used by builtins) ────────────────────────────────
3496
3497impl JsHost {
3498    /// A function's `.length`: the count of leading params before the first one
3499    /// with a default or the rest element.
3500    pub fn func_arity(&self, v: &Value) -> usize {
3501        let def_id = match self.get(v) {
3502            Some(JsObj::Func(f)) => Some(f.def_id),
3503            Some(JsObj::Class(c)) => match c.ctor.as_ref().and_then(|cf| self.get(cf)) {
3504                Some(JsObj::Func(f)) => Some(f.def_id),
3505                _ => None,
3506            },
3507            _ => None,
3508        };
3509        match def_id.and_then(|id| self.funcs.get(id)) {
3510            Some(def) => def
3511                .params
3512                .iter()
3513                .take_while(|p| !p.rest && !p.has_default)
3514                .count(),
3515            None => 0,
3516        }
3517    }
3518
3519    pub fn is_map(&self, v: &Value) -> bool {
3520        matches!(self.get(v), Some(JsObj::Map { .. }))
3521    }
3522    pub fn is_set(&self, v: &Value) -> bool {
3523        matches!(self.get(v), Some(JsObj::Set { .. }))
3524    }
3525}
3526
3527// ── promises & the event loop ────────────────────────────────────────────────
3528
3529impl JsHost {
3530    /// Allocate a fresh pending promise, returning its heap value.
3531    pub fn new_promise(&mut self) -> Value {
3532        let id = self.promises.len() as u32;
3533        self.promises.push(PromiseCell {
3534            state: PromiseState::Pending,
3535            value: Value::Undef,
3536            reactions: Vec::new(),
3537            handled: false,
3538        });
3539        self.alloc(JsObj::Promise { id })
3540    }
3541    pub fn promise_id(&self, v: &Value) -> Option<u32> {
3542        match self.get(v) {
3543            Some(JsObj::Promise { id }) => Some(*id),
3544            _ => None,
3545        }
3546    }
3547    pub fn promise_state(&self, id: u32) -> PromiseState {
3548        self.promises[id as usize].state
3549    }
3550    pub fn promise_value(&self, id: u32) -> Value {
3551        self.promises[id as usize].value.clone()
3552    }
3553    pub fn promise_mark_handled(&mut self, id: u32) {
3554        self.promises[id as usize].handled = true;
3555    }
3556    /// Take the pending reactions of a promise (called on settle).
3557    pub fn take_reactions(&mut self, id: u32) -> Vec<PromiseReaction> {
3558        std::mem::take(&mut self.promises[id as usize].reactions)
3559    }
3560    pub fn add_reaction(&mut self, id: u32, r: PromiseReaction) {
3561        self.promises[id as usize].reactions.push(r);
3562    }
3563    pub fn settle_promise(&mut self, id: u32, state: PromiseState, value: Value) {
3564        let c = &mut self.promises[id as usize];
3565        if c.state != PromiseState::Pending {
3566            return; // already settled — resolve/reject are one-shot
3567        }
3568        c.state = state;
3569        c.value = value;
3570    }
3571    pub fn queue_micro(&mut self, cb: Value, args: Vec<Value>) {
3572        self.microtasks.push_back(Task::Js { cb, args });
3573    }
3574    pub fn queue_nexttick(&mut self, cb: Value, args: Vec<Value>) {
3575        self.nextticks.push_back(Task::Js { cb, args });
3576    }
3577    /// Schedule a native (Rust) microtask — used by Promise reactions and async
3578    /// resumption.
3579    pub fn queue_micro_native(&mut self, f: Box<dyn FnOnce() -> Result<(), String>>) {
3580        self.microtasks.push_back(Task::Native(f));
3581    }
3582    pub fn add_timer(&mut self, delay: f64, callback: Value, args: Vec<Value>) -> u64 {
3583        let id = self.next_timer;
3584        self.next_timer += 1;
3585        // Real deadline for the blocking I/O path; `setImmediate` (delay < 0) is
3586        // clamped to "now". Virtual-clock ordering still uses `delay`/`seq`.
3587        let deadline = Instant::now() + Duration::from_millis(delay.max(0.0) as u64);
3588        self.macrotasks.push(Timer {
3589            id,
3590            delay,
3591            seq: id,
3592            callback,
3593            args,
3594            cancelled: false,
3595            deadline,
3596        });
3597        id
3598    }
3599    /// Clone the I/O sender for a background I/O thread.
3600    pub fn io_sender(&self) -> Sender<IoTask> {
3601        self.io_tx.clone()
3602    }
3603    /// Register a live handle (listener/socket/ref'd resource) keeping the loop
3604    /// alive.
3605    pub fn incr_handle(&mut self) {
3606        self.open_handles += 1;
3607    }
3608    /// Release a handle; the loop exits once this reaches `0` with empty queues.
3609    pub fn decr_handle(&mut self) {
3610        self.open_handles = self.open_handles.saturating_sub(1);
3611    }
3612    pub fn open_handles(&self) -> usize {
3613        self.open_handles
3614    }
3615    /// Pop the earliest timer whose real deadline is at or before `now` (I/O
3616    /// path). Ties break by `seq`.
3617    fn pop_due_timer(&mut self, now: Instant) -> Option<Timer> {
3618        let idx = self
3619            .macrotasks
3620            .iter()
3621            .enumerate()
3622            .filter(|(_, t)| !t.cancelled && t.deadline <= now)
3623            .min_by(|(_, a), (_, b)| a.deadline.cmp(&b.deadline).then(a.seq.cmp(&b.seq)))
3624            .map(|(i, _)| i);
3625        idx.map(|i| self.macrotasks.remove(i))
3626    }
3627    /// Time until the earliest pending timer's deadline (I/O path blocking bound),
3628    /// or `None` if no timers are pending. Clamped to `0` for already-due timers.
3629    fn next_timer_timeout(&self, now: Instant) -> Option<Duration> {
3630        self.macrotasks
3631            .iter()
3632            .filter(|t| !t.cancelled)
3633            .map(|t| t.deadline)
3634            .min()
3635            .map(|d| d.saturating_duration_since(now))
3636    }
3637    pub fn cancel_timer(&mut self, id: u64) {
3638        for t in &mut self.macrotasks {
3639            if t.id == id {
3640                t.cancelled = true;
3641            }
3642        }
3643    }
3644    fn pop_next_timer(&mut self) -> Option<Timer> {
3645        // Earliest (delay, seq) fires first — a deterministic virtual clock.
3646        let idx = self
3647            .macrotasks
3648            .iter()
3649            .enumerate()
3650            .filter(|(_, t)| !t.cancelled)
3651            .min_by(|(_, a), (_, b)| {
3652                a.delay
3653                    .partial_cmp(&b.delay)
3654                    .unwrap_or(std::cmp::Ordering::Equal)
3655                    .then(a.seq.cmp(&b.seq))
3656            })
3657            .map(|(i, _)| i);
3658        idx.map(|i| self.macrotasks.remove(i))
3659    }
3660    fn next_microtask(&mut self) -> Option<Task> {
3661        // nextTick drains before promise microtasks (Node ordering).
3662        self.nextticks
3663            .pop_front()
3664            .or_else(|| self.microtasks.pop_front())
3665    }
3666    fn has_microtasks(&self) -> bool {
3667        !self.nextticks.is_empty() || !self.microtasks.is_empty()
3668    }
3669    fn has_macrotasks(&self) -> bool {
3670        self.macrotasks.iter().any(|t| !t.cancelled)
3671    }
3672}
3673
3674/// Drive the event loop to quiescence.
3675///
3676/// Two regimes, selected per iteration by `open_handles`:
3677///
3678/// - **No open handles (pure script / timers only):** the original deterministic
3679///   virtual clock — drain microtasks, fire the earliest `(delay, seq)` timer
3680///   immediately (no real waiting), repeat until both queues empty, then EXIT.
3681///   Parity output and test speed are unchanged.
3682/// - **Open handles (a server is listening / sockets are live):** drain
3683///   microtasks, fire every timer whose real deadline has passed, then BLOCK on
3684///   the I/O channel (`recv_timeout` bounded by the next timer's real deadline,
3685///   or unbounded `recv` if no timers) and run the received `IoTask` on the main
3686///   thread. The host keeps its own `Sender`, so `recv` never disconnects while
3687///   the process should stay alive.
3688///
3689/// Errors thrown by a task/timer/I/O dispatch abort the loop (uncaught → surfaced).
3690pub fn run_event_loop() -> Result<(), String> {
3691    // Own the receiver for the loop's duration (blocking `recv` cannot hold a
3692    // host borrow); restore it afterward so a re-entrant run reuses the channel.
3693    let rx = with_host(|h| h.io_rx.take());
3694    let result = drive_event_loop(rx.as_ref());
3695    with_host(|h| h.io_rx = rx);
3696    result
3697}
3698
3699fn drive_event_loop(rx: Option<&Receiver<IoTask>>) -> Result<(), String> {
3700    loop {
3701        // 1) Exhaust the microtask queue (nextTick before promise reactions).
3702        while let Some(task) = with_host(|h| h.next_microtask()) {
3703            task.run()?;
3704        }
3705
3706        if with_host(|h| h.open_handles()) == 0 {
3707            // ── virtual-clock regime (unchanged behavior) ────────────────────
3708            match with_host(|h| h.pop_next_timer()) {
3709                Some(t) => {
3710                    invoke(&t.callback, t.args, None)?;
3711                }
3712                None => {
3713                    if !with_host(|h| h.has_microtasks()) {
3714                        break;
3715                    }
3716                }
3717            }
3718            if !with_host(|h| h.has_microtasks() || h.has_macrotasks()) {
3719                break;
3720            }
3721            continue;
3722        }
3723
3724        // ── real-clock / blocking-I/O regime ─────────────────────────────────
3725        let now = Instant::now();
3726        if let Some(t) = with_host(|h| h.pop_due_timer(now)) {
3727            invoke(&t.callback, t.args, None)?;
3728            continue; // re-drain microtasks, re-check deadlines
3729        }
3730        // Nothing due and no pending microtasks: block for the next I/O event,
3731        // bounded by the soonest timer deadline so due timers still fire on time.
3732        let rx = rx.expect("blocking-I/O regime requires the I/O receiver");
3733        let timeout = with_host(|h| h.next_timer_timeout(now));
3734        let recv = match timeout {
3735            Some(d) => rx.recv_timeout(d),
3736            None => rx
3737                .recv()
3738                .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected),
3739        };
3740        match recv {
3741            Ok(task) => task()?,
3742            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} // a timer is now due
3743            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, // no senders left
3744        }
3745    }
3746    Ok(())
3747}
3748
3749// ── async functions & promise resolution (native) ────────────────────────────
3750
3751/// Drive a freshly-built async coroutine and return its result promise.
3752fn run_async(gen: Value) -> Value {
3753    let result = with_host(|h| h.new_promise());
3754    let rid = with_host(|h| h.promise_id(&result).unwrap());
3755    drive_async(gen, rid, Value::Undef);
3756    result
3757}
3758
3759/// Resume an async coroutine one step, wiring `await` continuations to promise
3760/// settlement.
3761fn drive_async(gen: Value, rid: u32, send: Value) {
3762    match gen_resume(&gen, send) {
3763        Ok(GenStep::Yield(awaited)) => {
3764            let ap = promise_of(&awaited);
3765            let aid = with_host(|h| h.promise_id(&ap).unwrap());
3766            let gen2 = gen.clone();
3767            subscribe_native(
3768                aid,
3769                Box::new(move |state, val| {
3770                    // Resume the coroutine with a `[tag, value]` packet the AWAIT
3771                    // op unwraps (tag 1 ⇒ the awaited promise rejected → throw).
3772                    let tag = if state == PromiseState::Rejected {
3773                        1.0
3774                    } else {
3775                        0.0
3776                    };
3777                    let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
3778                    drive_async(gen2, rid, packet);
3779                    Ok(())
3780                }),
3781            );
3782        }
3783        Ok(GenStep::Done(v)) => resolve_promise_val(rid, v),
3784        Err(e) => {
3785            let ev = take_exc_or_error(&e);
3786            reject_promise_val(rid, ev);
3787        }
3788    }
3789}
3790
3791/// The AWAIT op body (runs inside the async coroutine): suspend, yielding the
3792/// awaited value; on resume, unwrap the settlement packet (throwing on reject).
3793pub fn await_value(awaited: Value) -> Result<Value, String> {
3794    let packet = gen_yield(awaited)?;
3795    let items = with_host(|h| h.iter_vec(&packet)).unwrap_or_default();
3796    let tag = items
3797        .first()
3798        .map(|v| with_host(|h| h.to_number(v)))
3799        .unwrap_or(0.0);
3800    let val = items.get(1).cloned().unwrap_or(Value::Undef);
3801    if tag == 1.0 {
3802        with_host(|h| h.exc = Some(val.clone()));
3803        Err(with_host(|h| crate::builtins::error_string(h, &val)))
3804    } else {
3805        Ok(val)
3806    }
3807}
3808
3809/// A promise for `v`: `v` itself if it is already a promise, else a promise
3810/// resolved with `v`.
3811pub fn promise_of(v: &Value) -> Value {
3812    if with_host(|h| h.promise_id(v)).is_some() {
3813        return v.clone();
3814    }
3815    let p = with_host(|h| h.new_promise());
3816    let id = with_host(|h| h.promise_id(&p).unwrap());
3817    resolve_promise_val(id, v.clone());
3818    p
3819}
3820
3821/// Register a native reaction on promise `id` (schedules immediately if already
3822/// settled).
3823pub fn subscribe_native(id: u32, f: Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>) {
3824    let state = with_host(|h| h.promise_state(id));
3825    if state == PromiseState::Pending {
3826        with_host(|h| h.add_reaction(id, PromiseReaction::Native(f)));
3827    } else {
3828        let val = with_host(|h| h.promise_value(id));
3829        with_host(|h| h.queue_micro_native(Box::new(move || f(state, val))));
3830    }
3831}
3832
3833/// The Promise "resolve" operation: adopt `value`'s state if it is a promise,
3834/// else fulfill with it.
3835pub fn resolve_promise_val(id: u32, value: Value) {
3836    if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
3837        return;
3838    }
3839    if let Some(vid) = with_host(|h| h.promise_id(&value)) {
3840        if vid == id {
3841            // Resolving a promise with itself → reject with a TypeError.
3842            let e = with_host(|h| {
3843                crate::builtins::synth_error(h, "TypeError: Chaining cycle detected")
3844            });
3845            reject_promise_val(id, e);
3846            return;
3847        }
3848        subscribe_native(
3849            vid,
3850            Box::new(move |state, val| {
3851                with_host(|h| h.settle_promise(id, state, val.clone()));
3852                schedule_reactions(id);
3853                Ok(())
3854            }),
3855        );
3856        return;
3857    }
3858    with_host(|h| h.settle_promise(id, PromiseState::Fulfilled, value));
3859    schedule_reactions(id);
3860}
3861
3862pub fn reject_promise_val(id: u32, value: Value) {
3863    if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
3864        return;
3865    }
3866    with_host(|h| h.settle_promise(id, PromiseState::Rejected, value));
3867    schedule_reactions(id);
3868}
3869
3870/// Drain a settled promise's reactions into microtasks.
3871fn schedule_reactions(id: u32) {
3872    let reactions = with_host(|h| h.take_reactions(id));
3873    let state = with_host(|h| h.promise_state(id));
3874    let value = with_host(|h| h.promise_value(id));
3875    for r in reactions {
3876        let value = value.clone();
3877        match r {
3878            PromiseReaction::Native(f) => {
3879                with_host(|h| h.queue_micro_native(Box::new(move || f(state, value))));
3880            }
3881            PromiseReaction::Js {
3882                on_ful,
3883                on_rej,
3884                result,
3885            } => {
3886                with_host(|h| {
3887                    h.queue_micro_native(Box::new(move || {
3888                        run_js_reaction(state, value, on_ful, on_rej, result)
3889                    }))
3890                });
3891            }
3892        }
3893    }
3894}
3895
3896/// Run a `.then` reaction: call the appropriate handler and settle the result
3897/// promise with its outcome (or pass through if there is no handler).
3898fn run_js_reaction(
3899    state: PromiseState,
3900    value: Value,
3901    on_ful: Value,
3902    on_rej: Value,
3903    result: Value,
3904) -> Result<(), String> {
3905    let rid = match with_host(|h| h.promise_id(&result)) {
3906        Some(i) => i,
3907        None => return Ok(()),
3908    };
3909    let handler = if state == PromiseState::Rejected {
3910        on_rej
3911    } else {
3912        on_ful
3913    };
3914    if with_host(|h| is_callable(h, &handler)) {
3915        match invoke(&handler, vec![value], None) {
3916            Ok(r) => resolve_promise_val(rid, r),
3917            Err(e) => reject_promise_val(rid, take_exc_or_error(&e)),
3918        }
3919    } else if state == PromiseState::Rejected {
3920        reject_promise_val(rid, value);
3921    } else {
3922        resolve_promise_val(rid, value);
3923    }
3924    Ok(())
3925}
3926
3927/// The JS value of a just-caught error: the live `exc` (a real thrown value) or a
3928/// synthesized `Error` from the internal message.
3929pub fn take_exc_or_error(e: &str) -> Value {
3930    with_host(|h| {
3931        h.error.take();
3932        h.exc
3933            .take()
3934            .unwrap_or_else(|| crate::builtins::synth_error(h, e))
3935    })
3936}
3937
3938/// Register a user `.then` reaction (JS handlers + result promise).
3939pub fn promise_then(p: &Value, on_ful: Value, on_rej: Value) -> Value {
3940    let id = match with_host(|h| h.promise_id(p)) {
3941        Some(i) => i,
3942        None => return Value::Undef,
3943    };
3944    with_host(|h| h.promise_mark_handled(id));
3945    let result = with_host(|h| h.new_promise());
3946    let reaction = PromiseReaction::Js {
3947        on_ful,
3948        on_rej,
3949        result: result.clone(),
3950    };
3951    let state = with_host(|h| h.promise_state(id));
3952    if state == PromiseState::Pending {
3953        with_host(|h| h.add_reaction(id, reaction));
3954    } else {
3955        let value = with_host(|h| h.promise_value(id));
3956        if let PromiseReaction::Js {
3957            on_ful,
3958            on_rej,
3959            result,
3960        } = reaction
3961        {
3962            with_host(|h| {
3963                h.queue_micro_native(Box::new(move || {
3964                    run_js_reaction(state, value, on_ful, on_rej, result)
3965                }))
3966            });
3967        }
3968    }
3969    result
3970}