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