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