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