nodejs/host.rs
1//! The JavaScript object heap and runtime, reached from fusevm through
2//! registered builtins (`register_builtin`) and the strict numeric hook.
3//!
4//! node-js owns no VM and no JIT: the compiler lowers JS to `fusevm::Chunk`, and
5//! every JS-specific operation the VM can't do natively is a builtin call that
6//! lands here. Local variables live in `Rc<RefCell>` environments chained
7//! parent-to-child, so a nested function/closure captures its enclosing scope by
8//! reference.
9//!
10//! Value representation:
11//! - immediate: `Value::Float` (every JS number — one IEEE-754 f64 type),
12//! `Value::Bool` (true/false), `Value::Undef` (undefined);
13//! - heap `Value::Obj(u32)` handles: string, array, object, function,
14//! builtin-namespace, and the canonical `null` — the reference types.
15
16use fusevm::{Chunk, NumOp, VMResult, Value, VM};
17use indexmap::IndexMap;
18use std::cell::RefCell;
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::rc::Rc;
22use std::sync::mpsc::{Receiver, Sender};
23use std::time::{Duration, Instant};
24
25/// A unit of I/O work handed from a background I/O thread to the main-thread
26/// event loop. It is a boxed closure so `host.rs` stays agnostic of `net`/`http`:
27/// the I/O thread captures only plain `Send` data (bytes, ids, `TcpStream`s) and
28/// the closure runs the JS-touching dispatch on the main thread (where the
29/// thread-local host lives). I/O threads NEVER touch the host directly.
30pub type IoTask = Box<dyn FnOnce() -> Result<(), String> + Send>;
31
32/// Builtin ids emitted by the compiler and registered on every VM. The compiler
33/// (`compiler.rs`) and the handler table (`builtins.rs::install`) must agree on
34/// these exactly.
35pub mod ops {
36 pub const GETLOCAL: u16 = 1; // [name] -> value (scope-chain read)
37 pub const SETLOCAL: u16 = 2; // [name, value] -> value (assignment)
38 pub const DECLARE: u16 = 3; // [name, value] -> value (let/const/var into current scope)
39 pub const DELNAME: u16 = 4; // [name]
40 pub const GETATTR: u16 = 5; // [recv, name] -> value (member .x)
41 pub const SETATTR: u16 = 6; // [recv, name, value]
42 pub const GETITEM: u16 = 7; // [recv, idx] -> value (computed [k])
43 pub const SETITEM: u16 = 8; // [recv, idx, value]
44 pub const DELITEM: u16 = 9; // [recv, idx] -> Bool (delete obj[k])
45 pub const MKSTR: u16 = 10; // [parts...] -> str (concat)
46 pub const MKARR: u16 = 11; // [items...] -> array
47 pub const MKOBJ: u16 = 12; // [tag,k,v,...] -> object (tag 1 = ...spread of k)
48 pub const CALL: u16 = 13; // [name, args...] -> resolve name & call
49 pub const CALL_METHOD: u16 = 14; // [recv, name, args...]
50 pub const CALL_VALUE: u16 = 15; // [callable, args...]
51 pub const NEW: u16 = 16; // [ctor, args...] -> instance
52 pub const TRUTHY: u16 = 17; // [v] -> Bool (JS truthiness)
53 pub const TOSTR: u16 = 18; // [v] -> str via String(v)
54 pub const MKFUNC: u16 = 19; // [func_id, defaults...] -> closure
55 pub const GETITER: u16 = 20; // [iterable] -> iterator (left on stack)
56 pub const FORITER: u16 = 21; // peek iterator -> pushes value + Bool(has_next)
57 pub const FORIN_KEYS: u16 = 22; // [obj] -> array of enumerable keys
58 pub const CONTAINS: u16 = 23; // [key, obj] -> Bool (`in`)
59 pub const SIG_RETURN: u16 = 24; // [v] -> return v from the function
60 pub const BINOP: u16 = 25; // [tag, a, b] -> bitwise/shift result (JS int32 semantics)
61 pub const UNARY: u16 = 26; // [tag, v] -> unary +/~ result
62 pub const STRICT_EQ: u16 = 27; // [a, b] -> Bool (===)
63 pub const LOOSE_EQ: u16 = 28; // [a, b] -> Bool (==)
64 pub const TYPEOF: u16 = 29; // [v] -> str
65 pub const LOAD_NULL: u16 = 30; // [] -> the canonical null
66 pub const THROW: u16 = 31; // [v] -> throw
67 pub const TRY: u16 = 32; // [try_id] -> run a try/catch/finally block
68 pub const NULLISH: u16 = 33; // [v] -> Bool (v is null or undefined)
69 pub const UNPACK: u16 = 34; // [iterable, count, star] -> pushes count values
70 pub const BUILD_ARGS: u16 = 35; // [tag,val,...] -> flat array (tag 1 = ...spread)
71 pub const THIS: u16 = 36; // [] -> current `this`
72 pub const INSTANCEOF: u16 = 37; // [a, b] -> Bool
73 pub const DELPROP_NAME: u16 = 38; // [recv, name] -> Bool (delete obj.name)
74 pub const APPLY: u16 = 39; // [callable, argsArray] -> call with spread args
75 pub const APPLY_METHOD: u16 = 40; // [recv, name, argsArray] -> method call with spread
76 pub const OBJ_REST: u16 = 41; // [obj, excludedKeys] -> object of remaining keys
77 pub const DIV: u16 = 42; // [a, b] -> IEEE `a / b` (JS: x/0 = ±Infinity, 0/0 = NaN)
78 pub const MKCLASS: u16 = 43; // [parent_or_undef, ctor_fn] -> class constructor value
79 pub const DEF_MEMBER: u16 = 44; // [class, name, kind, is_static, fn] -> define method/get/set
80 pub const SUPER_CALL: u16 = 45; // [args...] -> invoke parent ctor on `this`, then init fields
81 pub const SUPER_GET: u16 = 46; // [name] -> resolve `super.name` (method up the parent chain)
82 pub const YIELD: u16 = 47; // [v] -> suspend the running generator, yield v
83 pub const PROPKEY: u16 = 48; // [v] -> property-key string (Symbol -> internal key, else String())
84 pub const NEW_TARGET: u16 = 49; // [] -> the current frame's new.target (undefined if not `new`)
85 pub const DEF_FIELD: u16 = 50; // [class, name, thunk] -> register an instance field initializer
86 pub const AWAIT: u16 = 51; // [v] -> await v (suspend the async coroutine until v settles)
87 pub const DEF_ACCESSOR: u16 = 52; // [obj, name, kind, fn] -> install a getter/setter on obj
88 pub const DBG_LINE: u16 = 53; // [line] -> DAP statement marker (debug only)
89 pub const MKBIGINT: u16 = 54; // [decimal_str] -> heap BigInt value
90 pub const MKREGEX: u16 = 55; // [pattern, flags] -> heap RegExp value
91 pub const TAG_TMPL: u16 = 56; // [tag, cooked..., raw..., n, values...] -> tagged-template call
92 pub const GET_ASYNC_ITER: u16 = 57; // [iterable] -> async iterator (for-await-of)
93 pub const ASYNC_STEP: u16 = 58; // [asyncIterator] -> Promise of {value, done}
94 pub const NUM_STEP: u16 = 59; // [tag(±1), old] -> pushes ToNumeric(old), returns old±1 (type-preserving; BigInt-aware ++/--)
95 pub const ITER_CLOSE: u16 = 60; // [iterator] -> close it (for-of break: run a generator's finally / call .return())
96 pub const TYPEOF_NAME: u16 = 61; // [name] -> str; `typeof <ident>` reads the name WITHOUT throwing (unbound -> "undefined")
97 pub const SIG_BREAK: u16 = 62; // [label|""] -> raise a Break signal and halt the chunk (break out of a `try`)
98 pub const SIG_CONTINUE: u16 = 63; // [label|""] -> raise a Continue signal and halt the chunk
99 pub const SIG_UNWIND: u16 = 64; // [tag] -> 0 none / 1 break here / 2 continue here; halts the chunk to propagate
100 pub const PUSH_SCOPE: u16 = 65; // [] -> enter a fresh block scope (`let`/`const` live here)
101 pub const POP_SCOPE: u16 = 66; // [] -> leave the innermost block scope
102 pub const COPY_SCOPE: u16 = 67; // [] -> replace the innermost block scope with a COPY (per-iteration `let`)
103 pub const DECLARE_VAR: u16 = 68; // [name, value] -> declare at FUNCTION scope, ignoring block scopes (`var`)
104 pub const NAMED_EVAL: u16 = 69; // [key, kind, fn] -> fn; SetFunctionName for a COMPUTED key (kind picks the `get `/`set ` prefix)
105 pub const POW: u16 = 70; // [a, b] -> JS `a ** b` (NOT native `Op::Pow`: IEEE pow answers 1 for `(-1) ** Infinity` and `1 ** NaN`)
106 pub const DECLARE_CONST: u16 = 71; // [name, value] -> value; like DECLARE but the binding is IMMUTABLE (`const`)
107 pub const MARK_HOLE: u16 = 72; // [arr, index] -> arr; record an ELIDED array-literal element
108 pub const SETLOCAL_STRICT: u16 = 73; // [name, value] -> value; like SETLOCAL but an UNRESOLVABLE name throws ReferenceError instead of creating a global (strict-mode PutValue)
109 pub const HOIST_VAR: u16 = 74; // [name] -> create the `var` binding as undefined IF ABSENT (hoisting)
110 pub const FORIN_ALIVE: u16 = 75; // [obj, key] -> Bool; is `key` STILL an enumerable property of `obj`? (a `for-in` body may have deleted it)
111 pub const HOIST_TDZ: u16 = 76; // [name] -> declare `name` in the CURRENT scope as UNINITIALIZED (the `let`/`const`/`class` temporal dead zone)
112 pub const NEW_SPREAD: u16 = 77; // [ctor, argsArray] -> instance; `new C(...xs)`, where the argument list is built at run time
113 pub const SUPER_CALL_SPREAD: u16 = 78; // [argsArray] -> invoke the parent ctor with a run-time argument list (`super(...xs)`)
114}
115
116/// Per-call-site callee SOURCE TEXT, for the `TypeError` a failed call raises.
117///
118/// V8 reports the callee the way the source wrote it — `z.f is not a function`,
119/// not `f is not a function` — by re-printing the AST of the call it was
120/// evaluating. The text is therefore a static property of the SITE, so the
121/// compiler records it once per call op and nothing is carried at run time: the
122/// table is consulted only on the error path.
123///
124/// Keyed by the chunk's `op_hash` (which `ChunkBuilder::build` computes anyway)
125/// paired with the op index. `op_hash` covers the op vector but not the name
126/// pool, so two chunks that compile to the same ops with different names share a
127/// key; the consequence is confined to which receiver text an error message
128/// names, never to what a program does.
129mod call_sites {
130 use std::cell::RefCell;
131
132 thread_local! {
133 pub(super) static SITES: RefCell<rustc_hash::FxHashMap<(u64, usize), String>> =
134 RefCell::new(rustc_hash::FxHashMap::default());
135 }
136
137 /// Record every call site of a freshly built chunk.
138 pub fn register(op_hash: u64, sites: Vec<(usize, String)>) {
139 if sites.is_empty() {
140 return;
141 }
142 SITES.with(|m| {
143 let mut m = m.borrow_mut();
144 for (ip, text) in sites {
145 m.insert((op_hash, ip), text);
146 }
147 });
148 }
149
150 /// The callee text recorded for the op at `ip` of the chunk `op_hash`.
151 pub fn text(op_hash: u64, ip: usize) -> Option<String> {
152 SITES.with(|m| m.borrow().get(&(op_hash, ip)).cloned())
153 }
154
155 pub fn clear() {
156 SITES.with(|m| m.borrow_mut().clear());
157 }
158}
159
160pub use call_sites::{clear as clear_call_sites, register as register_call_sites};
161
162/// How many `for…of` / `yield*` iterators are parked on the VM stack at each
163/// `yield` op, recorded by the compiler the same way callee text is.
164///
165/// A `.return()`/`.throw()` injected at a suspension point halts the generator's
166/// chunk outright, which jumps past the loop exits that would have closed those
167/// iterators — so the halt path has to close them itself, and this is how it
168/// knows how many are there and that they are the top of the stack.
169mod yield_sites {
170 use std::cell::RefCell;
171
172 thread_local! {
173 pub(super) static DEPTHS: RefCell<rustc_hash::FxHashMap<(u64, usize), usize>> =
174 RefCell::new(rustc_hash::FxHashMap::default());
175 }
176
177 pub fn register(op_hash: u64, sites: Vec<(usize, usize)>) {
178 if sites.is_empty() {
179 return;
180 }
181 DEPTHS.with(|m| {
182 let mut m = m.borrow_mut();
183 for (ip, depth) in sites {
184 m.insert((op_hash, ip), depth);
185 }
186 });
187 }
188
189 pub fn depth(op_hash: u64, ip: usize) -> usize {
190 DEPTHS.with(|m| m.borrow().get(&(op_hash, ip)).copied().unwrap_or(0))
191 }
192
193 pub fn clear() {
194 DEPTHS.with(|m| m.borrow_mut().clear());
195 }
196}
197
198pub use yield_sites::{clear as clear_yield_sites, register as register_yield_sites};
199
200/// Every call site and yield site registered so far, as the cache stores them:
201/// `(op_hash, ip)` keys with their recorded value.
202///
203/// The tables are built by the COMPILER (`finish_chunk`), so a run that loads a
204/// program from the bytecode cache never fills them — and everything that reads
205/// them silently degrades: a generator's parked `for…of`/`yield*` iterators are
206/// not closed on an injected `.return()`, so their `finally` never runs, and a
207/// `TypeError` loses the callee's source text. Storing them alongside the
208/// program is what makes a cache hit behave like a compile.
209pub type SiteTables = (Vec<((u64, usize), String)>, Vec<((u64, usize), usize)>);
210
211/// Snapshot both registries.
212pub fn site_tables() -> SiteTables {
213 let calls = call_sites::SITES.with(|m| {
214 m.borrow()
215 .iter()
216 .map(|(k, v)| (*k, v.clone()))
217 .collect::<Vec<_>>()
218 });
219 let yields =
220 yield_sites::DEPTHS.with(|m| m.borrow().iter().map(|(k, v)| (*k, *v)).collect::<Vec<_>>());
221 (calls, yields)
222}
223
224/// Put a snapshot back — what a cache hit does in place of compiling.
225pub fn restore_site_tables(t: &SiteTables) {
226 call_sites::SITES.with(|m| {
227 let mut m = m.borrow_mut();
228 for (k, v) in &t.0 {
229 m.insert(*k, v.clone());
230 }
231 });
232 yield_sites::DEPTHS.with(|m| {
233 let mut m = m.borrow_mut();
234 for (k, v) in &t.1 {
235 m.insert(*k, *v);
236 }
237 });
238}
239
240/// The number of loop iterators parked on the stack at the op currently
241/// executing, for the abrupt-completion close in `b_yield`.
242pub fn parked_iters(vm: &fusevm::VM) -> usize {
243 yield_sites::depth(vm.chunk.op_hash, vm.ip.saturating_sub(1))
244}
245
246/// Rewrite a `<subject> is not a function` / `is not a constructor` message with
247/// the SOURCE TEXT of the callee at the currently executing op, as V8 does.
248///
249/// `subject` is what the raising code named — the method name, or the callee's
250/// rendered value. The message's own subject must END WITH it, which is the
251/// guard that keeps an unrelated error raised deeper inside a native method from
252/// being relabelled with this call's text. (A native dispatcher may prefix its
253/// own receiver word, e.g. `map.get is not a function`, so the whole subject is
254/// replaced rather than trimmed by length.)
255///
256/// Returns the message unchanged when no site was recorded, so a shape the
257/// printer declines to print keeps the old wording rather than an invented one.
258/// The source text recorded for the op currently executing, if any. `vm.ip` has
259/// already advanced past it.
260pub fn call_site_text(vm: &fusevm::VM) -> Option<String> {
261 call_sites::text(vm.chunk.op_hash, vm.ip.saturating_sub(1))
262}
263
264pub fn name_call_site(vm: &fusevm::VM, subject: &str, msg: String) -> String {
265 for tail in [
266 " is not a function",
267 " is not a constructor",
268 " is not iterable",
269 ] {
270 let Some(head) = msg.strip_suffix(tail) else {
271 continue;
272 };
273 // The prefix is the error class (`TypeError: `); the rest is the subject.
274 // The FIRST separator, not the last: a rendered VALUE can contain one —
275 // `{ a: 1 } is not iterable` split at the last `": "` left the subject
276 // as `1 }`, which matched nothing and silently skipped the rename.
277 let (prefix, found) = match head.find(": ") {
278 Some(i) => (&head[..i + 2], &head[i + 2..]),
279 None => ("", head),
280 };
281 if !found.ends_with(subject) {
282 return msg;
283 }
284 // `vm.ip` has already advanced past the op being executed.
285 let Some(text) = call_sites::text(vm.chunk.op_hash, vm.ip.saturating_sub(1)) else {
286 return msg;
287 };
288 return format!("{prefix}{text}{tail}");
289 }
290 msg
291}
292
293/// `SIG_UNWIND` scope tags: what the emitting site is nested in.
294pub mod unwind {
295 /// No enclosing loop in this chunk — any pending signal propagates outward.
296 pub const NO_LOOP: &str = "";
297 /// An enclosing UNLABELED loop in this chunk.
298 pub const PLAIN_LOOP: &str = "\u{0}";
299 /// `SIG_UNWIND` result codes.
300 pub const NONE: i64 = 0;
301 pub const BREAK: i64 = 1;
302 pub const CONTINUE: i64 = 2;
303}
304
305/// `DEF_MEMBER` member-kind tags.
306pub mod member {
307 pub const METHOD: i64 = 0;
308 pub const GET: i64 = 1;
309 pub const SET: i64 = 2;
310 /// A static FIELD (`static x = 1`), which is a data property of the
311 /// constructor rather than a method. Only distinguished from `METHOD` for a
312 /// PRIVATE name, where the declaration must install the private element
313 /// without tripping the brand check an ordinary write to `#x` gets — and
314 /// where node's brand-check message words a field differently from a method.
315 pub const STATIC_FIELD: i64 = 3;
316}
317
318/// Bitwise/shift op tags carried by `ops::BINOP` (JS ToInt32/ToUint32 rules).
319pub mod binop {
320 pub const BITAND: i64 = 0;
321 pub const BITOR: i64 = 1;
322 pub const BITXOR: i64 = 2;
323 pub const SHL: i64 = 3;
324 pub const SHR: i64 = 4;
325 pub const USHR: i64 = 5;
326}
327
328/// Unary op tags carried by `ops::UNARY`.
329pub mod unop {
330 pub const POS: i64 = 0; // unary +
331 pub const BITNOT: i64 = 1; // ~
332}
333
334// ── heap objects ───────────────────────────────────────────────────────────
335
336/// A compiled function template: parameter shape + body chunk. Shared by every
337/// closure created from the same function/arrow.
338#[derive(Clone, serde::Serialize, serde::Deserialize)]
339pub struct FuncDef {
340 pub name: String,
341 /// Parameter binding templates (destructuring lowered by the compiler into
342 /// the body prologue; here we only track the simple arg slots).
343 pub params: Vec<ParamSlot>,
344 pub chunk: Chunk,
345 pub is_arrow: bool,
346 /// True for a `function*`/`*method`/generator arrow: calling it builds a
347 /// suspended generator instead of running the body.
348 pub is_generator: bool,
349 /// True for an `async` function/method/arrow: calling it drives a coroutine
350 /// and returns a Promise; `await` inside suspends via the same yielder.
351 pub is_async: bool,
352 /// True when the function body (or the enclosing program) is strict. A
353 /// SLOPPY function called with no receiver gets the GLOBAL object as
354 /// `this`; a strict one keeps `undefined` (10.2.1.2 OrdinaryCallBindThis).
355 #[serde(default)]
356 pub strict: bool,
357 /// True for a MethodDefinition (`{ m(){} }`, a class method/accessor). A
358 /// non-generator method is not a constructor, so it owns no `prototype`.
359 #[serde(default)]
360 pub is_method: bool,
361 /// True for a NAMED function *expression* (`const f = function fact(n) {…}`):
362 /// the closure gets an extra environment binding its own name to itself, so
363 /// the body can recurse through that name even when the outer binding differs.
364 #[serde(default)]
365 pub self_name: bool,
366}
367
368/// One parameter slot. `name` is the simple bound name; a destructuring pattern
369/// is lowered to a synthetic `.arg{i}` name plus body prologue code.
370#[derive(Clone, serde::Serialize, serde::Deserialize)]
371pub struct ParamSlot {
372 pub name: String,
373 /// True for the `...rest` collector.
374 pub rest: bool,
375 /// True if this slot has a default expression (applied in the body prologue).
376 pub has_default: bool,
377}
378
379/// A compiled `try`/`catch`/`finally` block. Bodies are bare chunks run in the
380/// current scope.
381#[derive(Clone, serde::Serialize, serde::Deserialize)]
382pub struct TryDef {
383 pub block: Chunk,
384 /// `(catch_param_name, catch_body)`.
385 pub handler: Option<(Option<String>, Chunk)>,
386 pub finalizer: Option<Chunk>,
387}
388
389/// A live closure value.
390#[derive(Clone)]
391pub struct FuncVal {
392 pub def_id: usize,
393 /// Captured lexical environment (enclosing scope chain), for free vars.
394 pub env: Option<Env>,
395 /// `this` captured at definition time (arrow functions).
396 pub this: Option<Value>,
397 pub is_arrow: bool,
398 /// The owning class name for a method (drives `super` resolution). `None` for
399 /// plain functions/arrows.
400 pub home_class: Option<String>,
401 /// Whether that method is a STATIC one. `super.x` resolves against a
402 /// different object in each case — the parent constructor for a static
403 /// method, the parent's prototype for an instance method — and the class
404 /// name alone cannot tell them apart, since both carry the same one.
405 pub home_static: bool,
406 /// The object literal a shorthand method was defined in, for `super` inside
407 /// it. A class method resolves `super` through `home_class` instead; this
408 /// is the `[[HomeObject]]` an ordinary `{ m() { super.x } }` needs, and
409 /// without it there was nothing to resolve against.
410 pub home_object: Option<Value>,
411}
412
413/// A heap object.
414#[derive(Clone)]
415pub enum JsObj {
416 Str(String),
417 Array(Vec<Value>),
418 Object(IndexMap<String, Value>),
419 Func(FuncVal),
420 /// A first-class reference to a builtin function or namespace
421 /// (`console.log`, `Math`, `parseInt`).
422 Builtin(String),
423 /// A bound method value (`obj.method` captured then called): dispatches
424 /// through `call_method(recv, name, args)` when invoked.
425 BoundMethod {
426 recv: Value,
427 name: String,
428 },
429 /// The single canonical `null`.
430 Null,
431 /// A live iterator over a sequence, with a cursor.
432 Iter {
433 items: Vec<Value>,
434 idx: usize,
435 },
436 /// A bound function (`fn.bind(thisArg, ...preargs)`).
437 BoundFunc {
438 target: Value,
439 this: Value,
440 args: Vec<Value>,
441 },
442 /// A class constructor value: the runtime object produced by a `class`.
443 Class(ClassVal),
444 /// A `Symbol` — a unique property key. `registered` marks a `Symbol.for`
445 /// key (shared) vs a fresh `Symbol()`.
446 Symbol {
447 desc: Option<String>,
448 id: u64,
449 },
450 /// A `Map` (or `WeakMap` when `weak`): insertion-ordered key→value entries.
451 Map {
452 entries: IndexMap<MapKey, (Value, Value)>,
453 weak: bool,
454 },
455 /// A `Set` (or `WeakSet` when `weak`): insertion-ordered unique values.
456 Set {
457 entries: IndexMap<MapKey, Value>,
458 weak: bool,
459 },
460 /// A live generator, backed by a stackful `corosensei` coroutine in
461 /// `JsHost.generators`.
462 Generator {
463 id: u32,
464 },
465 /// A Promise, backed by a `PromiseCell` in `JsHost.promises`.
466 Promise {
467 id: u32,
468 },
469 /// An arbitrary-precision `BigInt` (`typeof === "bigint"`).
470 BigInt(num_bigint::BigInt),
471 /// A compiled regular expression (`/pat/flags` or `new RegExp(...)`).
472 RegExp(Box<RegExpObj>),
473 /// A `Proxy`: every essential internal method is diverted to `handler`'s
474 /// traps (see `crate::proxy`). `revoked` is set by the thunk
475 /// `Proxy.revocable` hands back, after which every operation throws.
476 Proxy {
477 target: Value,
478 handler: Value,
479 revoked: bool,
480 },
481}
482
483/// Which variant a heap object is, carrying none of its contents.
484///
485/// Property access has to pick a branch by variant, but the code inside a branch
486/// re-enters the host (`bound_method`, `lookup_chain`, `invoke`), so it cannot
487/// hold a `&JsObj` borrow across the match. The way out used to be
488/// `h.get(v).cloned()` — which deep-copies the entire backing store (a whole
489/// `Vec<Value>`, `IndexMap`, or `String`) just to read its tag. That made one
490/// property read O(len) and any loop over a collection O(n^2). This type is the
491/// same discriminant with nothing attached, so the probe is O(1) and each branch
492/// re-borrows for only the one field it actually needs.
493/// The well-known symbols node-js actually honors. `Symbol.<name>` is the
494/// interned symbol `@@Symbol.<name>`, and using it as a property key stores
495/// under the sentinel string `@@<name>` (`property_key`) so the internal
496/// lookups (`@@iterator`, `@@toPrimitive`, …) can find it without a symbol
497/// table walk. Symbols V8 defines but node-js does not act on are deliberately
498/// absent: a symbol that reads back while the operator it names ignores it would
499/// be a silent fake. `hasInstance` is listed because `instance_of` consults it.
500pub const WELL_KNOWN_SYMBOLS: &[&str] = &[
501 "iterator",
502 "asyncIterator",
503 "toPrimitive",
504 "toStringTag",
505 "hasInstance",
506 // Nine more the table was missing entirely, so `Symbol.species` and friends
507 // read `undefined` and no protocol keyed on them could be expressed.
508 "species",
509 "isConcatSpreadable",
510 "match",
511 "matchAll",
512 "replace",
513 "search",
514 "split",
515 "unscopables",
516 "dispose",
517 "asyncDispose",
518];
519
520/// Whether the internal key `k` came from a SYMBOL used as a property key
521/// (`@@sym:<id>`, or a well-known `@@iterator`), as opposed to one of node-js's
522/// hidden slots (`@@native`, `@@bytes`, `@@ms`, `@@kind`, …). Only the former
523/// is an observable JavaScript property.
524pub fn is_symbol_key(k: &str) -> bool {
525 match k.strip_prefix("@@") {
526 Some(rest) => rest
527 .strip_prefix("sym:")
528 .map(|i| i.parse::<u64>().is_ok())
529 .unwrap_or_else(|| WELL_KNOWN_SYMBOLS.contains(&rest)),
530 None => false,
531 }
532}
533
534#[derive(Clone, Copy, PartialEq, Eq, Debug)]
535pub enum ObjKind {
536 Str,
537 Array,
538 Object,
539 Func,
540 Builtin,
541 BoundMethod,
542 Null,
543 Iter,
544 BoundFunc,
545 Class,
546 Symbol,
547 Map,
548 Set,
549 Generator,
550 Promise,
551 BigInt,
552 RegExp,
553 Proxy,
554}
555
556impl JsObj {
557 /// This object's variant, without touching its contents.
558 pub fn kind(&self) -> ObjKind {
559 match self {
560 JsObj::Str(_) => ObjKind::Str,
561 JsObj::Array(_) => ObjKind::Array,
562 JsObj::Object(_) => ObjKind::Object,
563 JsObj::Func(_) => ObjKind::Func,
564 JsObj::Builtin(_) => ObjKind::Builtin,
565 JsObj::BoundMethod { .. } => ObjKind::BoundMethod,
566 JsObj::Null => ObjKind::Null,
567 JsObj::Iter { .. } => ObjKind::Iter,
568 JsObj::BoundFunc { .. } => ObjKind::BoundFunc,
569 JsObj::Class(_) => ObjKind::Class,
570 JsObj::Symbol { .. } => ObjKind::Symbol,
571 JsObj::Map { .. } => ObjKind::Map,
572 JsObj::Set { .. } => ObjKind::Set,
573 JsObj::Generator { .. } => ObjKind::Generator,
574 JsObj::Promise { .. } => ObjKind::Promise,
575 JsObj::BigInt(_) => ObjKind::BigInt,
576 JsObj::RegExp(_) => ObjKind::RegExp,
577 JsObj::Proxy { .. } => ObjKind::Proxy,
578 }
579 }
580}
581
582/// A `RegExp` object: the compiled `fancy_regex::Regex` plus the JS-visible
583/// source, flag booleans, and the mutable `lastIndex` cursor (used by `g`/`y`
584/// matching). fancy-regex adds lookaround + backreferences on top of the Rust
585/// `regex` fast path, so the JS grammar node-js can accept is a near-superset.
586#[derive(Clone)]
587pub struct RegExpObj {
588 /// The translated regex. Construction of a pattern fancy-regex still cannot
589 /// express (documented in BUGS.md) throws at `RegExp` build time, so a live
590 /// `RegExpObj` always holds a compiled engine.
591 ///
592 /// Shared (`Rc`) rather than owned, because a regex LITERAL builds a fresh
593 /// `RegExpObj` on every evaluation — it has to, since `lastIndex` is
594 /// per-object mutable state — while the compiled engine behind it is
595 /// immutable and identical every time. See `regexp::compiled`.
596 pub re: std::rc::Rc<fancy_regex::Regex>,
597 pub source: String,
598 pub flags: String,
599 pub global: bool,
600 pub ignore_case: bool,
601 pub multiline: bool,
602 pub dot_all: bool,
603 pub sticky: bool,
604 pub unicode: bool,
605 /// `lastIndex`, in UTF-16 code units; advanced by `exec`/`test` under the
606 /// `g`/`y` flags. The newtype keeps it from being confused with the regex
607 /// engine's byte offsets, which are the same shape and differ off the BMP.
608 pub last_index: crate::utf16::U16Index,
609}
610
611/// A Promise's settled state and pending reactions.
612pub struct PromiseCell {
613 pub state: PromiseState,
614 pub value: Value,
615 /// Reactions registered while still pending; drained (as microtasks) on
616 /// settle.
617 pub reactions: Vec<PromiseReaction>,
618 /// True once a rejection has been observed by a handler (`.then`/`.catch`),
619 /// so the loop doesn't report it as unhandled.
620 pub handled: bool,
621}
622
623/// A pending Promise reaction: a user `.then` (JS handlers + a result promise) or
624/// a native continuation (Promise chaining / async `await` resumption).
625pub enum PromiseReaction {
626 Js {
627 on_ful: Value,
628 on_rej: Value,
629 result: Value,
630 },
631 Native(Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>),
632}
633
634#[derive(Default, Clone, Copy, PartialEq, Eq)]
635pub enum PromiseState {
636 #[default]
637 Pending,
638 Fulfilled,
639 Rejected,
640}
641
642/// A live class constructor. The prototype object (holding instance methods) and
643/// the static-side own properties live on the heap; `parent` is the superclass
644/// constructor value (`None` for a base class).
645#[derive(Clone)]
646pub struct ClassVal {
647 pub name: String,
648 /// The constructor function value (a `JsObj::Func`), or `None` for a class
649 /// with only a synthesized default constructor.
650 pub ctor: Option<Value>,
651 pub parent: Option<Value>,
652 /// `C.prototype` — the object instances delegate to.
653 pub proto: Value,
654 /// Static own properties (static methods/fields), plus `name`/`prototype`.
655 pub statics: IndexMap<String, Value>,
656 /// Instance field initializers: `(name, thunk_fn, name_anon_init)`, run
657 /// per-instance after `super()` (or at construction start for a base class).
658 /// `name_anon_init` records the SYNTACTIC fact that the initializer was an
659 /// anonymous function definition, so 15.7.10 NamedEvaluation applies to its
660 /// result — it cannot be re-derived at run time (a field initialised from an
661 /// already-anonymous function held elsewhere must not be renamed).
662 pub fields: Vec<(String, Value, bool)>,
663}
664
665/// The result of resolving `super.name`: a getter to invoke (accessor property)
666/// or a directly-usable value (method / data property).
667pub enum SuperRef {
668 Getter(Value),
669 Data(Value),
670}
671
672/// A `Map`/`Set` key under SameValueZero: `NaN` collapses to one key, `-0` and
673/// `+0` are the same key, primitives compare by value, objects by heap identity.
674#[derive(Clone, PartialEq, Eq, Hash)]
675pub enum MapKey {
676 Undef,
677 Null,
678 Bool(bool),
679 /// f64 bit pattern with `NaN` canonicalized and `-0` normalized to `+0`.
680 Num(u64),
681 /// A `BigInt` key, by its decimal string (SameValueZero: `1n` is one key).
682 Big(String),
683 Str(String),
684 /// Heap identity (objects, arrays, functions, symbols).
685 Ref(u32),
686}
687
688// ── environments ─────────────────────────────────────────────────────────────
689
690/// The map behind a scope. Hashing these with `FxHash` instead of the default
691/// was measured SLOWER, not faster — fib went 652ms to 1086ms and a 5M-iteration
692/// counting loop 1894ms to 2381ms on the same machine — so the default stands.
693pub type VarMap = IndexMap<String, Value>;
694
695/// A local-variable environment, shared (by `Rc`) between a frame and any nested
696/// function that captures it.
697pub struct EnvData {
698 pub vars: VarMap,
699 /// The names in `vars` that were declared `const`, so an assignment to one
700 /// throws (16.1.3 / 8.5.2 — an immutable binding rejects SetMutableBinding).
701 ///
702 /// A separate set rather than a flag inside `VarMap`'s value, because
703 /// `set_name` is a hot path — the common case is an env with NO consts,
704 /// where `is_empty()` settles it without hashing the name a second time.
705 pub consts: rustc_hash::FxHashSet<String>,
706 pub parent: Option<Env>,
707}
708pub type Env = Rc<RefCell<EnvData>>;
709
710/// An accessor property: `(getter, setter)`, either optional.
711pub type Accessor = (Option<Value>, Option<Value>);
712
713/// Prefix of the hidden property-map entry that reserves an accessor's slot in
714/// own-key insertion order (see `set_accessor`).
715pub const ORD_MARKER: &str = "@@ord:";
716
717/// The three ECMAScript own-property attributes. `PropAttrs::default()` is the
718/// all-true shape a plain `o.k = v` assignment produces, which is why only
719/// deviations need storing.
720#[derive(Clone, Copy, Debug, PartialEq, Eq)]
721pub struct PropAttrs {
722 pub writable: bool,
723 pub enumerable: bool,
724 pub configurable: bool,
725}
726
727impl Default for PropAttrs {
728 fn default() -> Self {
729 PropAttrs {
730 writable: true,
731 enumerable: true,
732 configurable: true,
733 }
734 }
735}
736
737impl PropAttrs {
738 /// The attribute shape V8 gives an internal-but-inspectable slot such as
739 /// `Error.prototype.message`, `err.stack` or a `Buffer`'s view metadata:
740 /// readable and replaceable, but never enumerated.
741 pub const HIDDEN: PropAttrs = PropAttrs {
742 writable: true,
743 enumerable: false,
744 configurable: true,
745 };
746}
747
748fn new_env(parent: Option<Env>) -> Env {
749 Rc::new(RefCell::new(EnvData {
750 vars: VarMap::default(),
751 consts: rustc_hash::FxHashSet::default(),
752 parent,
753 }))
754}
755
756/// A fresh empty scope chained under `parent`.
757pub fn child_env(parent: Env) -> Env {
758 new_env(Some(parent))
759}
760
761/// One function activation.
762pub struct Frame {
763 pub env: Env,
764 /// The env this activation started in — the FUNCTION scope. `var` and hoisted
765 /// function declarations bind here no matter how many block scopes are open.
766 pub base_env: Env,
767 pub this_obj: Option<Value>,
768 /// `new.target` for this activation (the constructor when invoked via `new`).
769 pub new_target: Option<Value>,
770 /// The class value owning the running method (drives `super`); `None` outside
771 /// a class method/constructor.
772 pub home_class: Option<Value>,
773 /// Whether the running method is a static one — see `FuncVal::home_static`.
774 pub home_static: bool,
775 /// The object literal owning the running method — see
776 /// `FuncVal::home_object`.
777 pub home_object: Option<Value>,
778 /// Whether the code in this activation is strict. A write the object
779 /// refuses is a silent no-op in sloppy mode and a `TypeError` here, so the
780 /// ASSIGNMENT SITE decides — not the object being written to.
781 pub strict: bool,
782 /// Source line the frame is currently executing (updated by the DAP line hook
783 /// under `--dap`; stays 0 on ordinary runs).
784 pub line: u32,
785 /// The function name that owns this frame, for the DAP `stackTrace`; `None`
786 /// for the module frame and anonymous activations.
787 pub owner: Option<String>,
788 /// True ONLY for the program's module frame. A generator/async body runs on a
789 /// coroutine whose swapped-in context holds just ITS OWN frame, so the frame
790 /// COUNT cannot tell "module scope" from "coroutine body scope" — without this
791 /// flag every top-level `let`/`var` in such a body declared a GLOBAL, shared
792 /// across concurrent activations of the same function.
793 pub is_module: bool,
794 /// Whether this activation's `this` is bound yet — see [`ThisState`].
795 pub this_state: ThisState,
796}
797
798/// The `[[ThisBindingStatus]]` of a function environment (9.1.1.3), as far as it
799/// is observable: only a DERIVED class constructor starts with `this`
800/// uninitialized, and only `super()` binds it.
801///
802/// The instance is still allocated up front (`construct_class`), so the
803/// state is what makes it unreachable until then: `this` before `super()`, a
804/// second `super()`, and returning without one are each the error node raises
805/// rather than a silent write to the pre-allocated object.
806#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
807pub enum ThisState {
808 /// Every other activation: `this` is whatever was passed in.
809 #[default]
810 Plain,
811 /// A derived constructor before `super()` has returned.
812 Pending,
813 /// A derived constructor after `super()`.
814 Bound,
815}
816
817/// A non-local control signal. `Break`/`Continue` carry the optional loop label
818/// and are only raised when the target loop lives in an ENCLOSING chunk (a
819/// `break` inside a `try` block, which the host runs as its own chunk); a
820/// same-chunk `break` is a plain compiler-resolved jump.
821#[derive(Clone)]
822pub enum Signal {
823 Return(Value),
824 Break(Option<String>),
825 Continue(Option<String>),
826}
827
828/// The JavaScript runtime.
829pub struct JsHost {
830 heap: Vec<JsObj>,
831 /// Function templates, indexed by def id.
832 pub funcs: Vec<FuncDef>,
833 /// try/catch/finally block templates, indexed by try id.
834 pub tries: Vec<TryDef>,
835 /// Module-level (global) names.
836 globals: VarMap,
837 /// The one uninitialized-binding marker, allocated on first use. See
838 /// [`JsHost::tdz_marker`].
839 tdz: Option<Value>,
840 /// Module-top-level names still in their temporal dead zone. Kept out of
841 /// `globals` so the marker is never reachable as `globalThis.<name>`.
842 tdz_globals: rustc_hash::FxHashSet<String>,
843 /// Top-level `const` names (a module frame declares into `globals`), so an
844 /// assignment to one throws the same way a block-scoped `const` does.
845 global_consts: rustc_hash::FxHashSet<String>,
846 /// The frame stack (bottom = module).
847 frames: Vec<Frame>,
848 /// The program's top-level scope — the scope runtime-compiled source runs in
849 /// (`new Function`, indirect `eval`, `vm.runInThisContext`; see
850 /// `run_chunk_in_global_scope`), as opposed to whatever function frame
851 /// happens to be executing when that source is compiled.
852 ///
853 /// Held as its own field rather than read off `frames[0]` because a coroutine
854 /// body runs with `frames` SWAPPED for its own one-frame context
855 /// (`install_gen_ctx`), so the bottom frame is not the top-level frame there.
856 ///
857 /// Note this is node-js's ONE top-level scope. Node distinguishes the global
858 /// scope from a CommonJS module's scope (a module body is a wrapper
859 /// function), so in Node a file's top-level `var` is invisible to dynamic
860 /// code; here the entry file is evaluated with Script semantics, so it stays
861 /// visible. That is the same entry-file-is-a-Script divergence `BUGS.md`
862 /// records for top-level `return`, not a separate one — and `node -e`, which
863 /// really is a Script, matches Node exactly.
864 global_env: Env,
865 pub error: Option<String>,
866 /// The in-flight thrown value, if any (JS `throw`).
867 pub exc: Option<Value>,
868 pub signal: Option<Signal>,
869 /// Promises that settled REJECTED this tick. Drained at each microtask
870 /// checkpoint: any still without a handler is an unhandled rejection.
871 pub pending_rejections: Vec<u32>,
872 /// `process.on(event, fn)` listeners, by event name.
873 pub process_listeners: IndexMap<String, Vec<ProcListener>>,
874 /// The canonical `null` handle (allocated once).
875 null_val: Value,
876 /// `[[Prototype]]` link per heap object, by heap index. Absent = default
877 /// (`Object.prototype` for objects, `null` for the root).
878 protos: HashMap<u32, Value>,
879 /// Heap objects whose `[[Prototype]]` is *explicitly* null — via
880 /// `Object.create(null)` or `Object.setPrototypeOf(o, null)`. Distinct from a
881 /// bare `{}` (absent from `protos` but conceptually `Object.prototype`), which
882 /// is why `Object.create(null) instanceof Object` can read `false`.
883 null_proto_objs: HashSet<u32>,
884 /// Own properties of function objects (functions are objects in JS): a live
885 /// closure's `name`/`prototype`/static-ish members. Keyed by heap index.
886 fn_props: HashMap<u32, IndexMap<String, Value>>,
887 /// Accessor (getter/setter) properties per owning object, by heap index then
888 /// key: `(get, set)`. Class `get x()`/`set x()` install here on the prototype.
889 accessors: HashMap<u32, IndexMap<String, Accessor>>,
890 /// Own-property attributes that deviate from the plain-assignment default
891 /// (`{writable, enumerable, configurable}` all true), by heap index then key.
892 /// Only non-default entries are stored, so an ordinary object costs nothing;
893 /// `prop_attrs` returns the default for any key absent here. This is what
894 /// makes `Object.defineProperty(o, k, {enumerable: false})` invisible to
895 /// `Object.keys`/`for-in`/`JSON.stringify` while `getOwnPropertyNames` still
896 /// reports it, and what hides `Error`'s `message`/`stack` the way V8 does.
897 prop_attrs: HashMap<u32, IndexMap<String, PropAttrs>>,
898 /// Heap objects sealed against new properties by `Object.preventExtensions`,
899 /// `Object.seal` or `Object.freeze`.
900 non_extensible: HashSet<u32>,
901 /// Private names (`#m`) declared as a METHOD or accessor rather than as a
902 /// field, for the brand-check error text: node distinguishes `Receiver must
903 /// be an instance of class C` (a private method or accessor) from `Cannot
904 /// read private member #x …` (a private field). Which class is answered by
905 /// the running method's home class, not by this set, so two classes
906 /// declaring the same private method name stay exact.
907 private_methods: HashSet<String>,
908 /// The ELIDED element positions of each array, by heap index. Absent (the
909 /// overwhelmingly common case) means the array is dense.
910 ///
911 /// A hole is deliberately NOT a `Value` variant. A sentinel value would have
912 /// to be mapped back to `undefined` at every element read in the runtime, and
913 /// a single missed read would leak an un-nameable value into user code — a
914 /// worse failure than storing `undefined` and losing the distinction. Keeping
915 /// the marker OUTSIDE the value domain makes that leak structurally
916 /// impossible: the element vector still holds a perfectly ordinary
917 /// `Value::Undef` at a hole, so any code path that has not been taught about
918 /// holes degrades to exactly the pre-existing behaviour (a visible
919 /// `undefined`) instead of producing something unrepresentable.
920 ///
921 /// Sized like the array it describes in the worst case (`new Array(n)` marks
922 /// every index), which is the same order as the `Vec<Value>` already paid for
923 /// that array — so it cannot turn a working allocation into an OOM.
924 array_holes: HashMap<u32, rustc_hash::FxHashSet<usize>>,
925 /// See `take_super_replacement`.
926 super_replacement: Option<Value>,
927 /// Set by `run_class_ctor` for the one call that follows: the next user
928 /// function activation is a derived constructor and starts `Pending`.
929 derived_ctor_next: bool,
930 /// Whether the entry script's top-level `var`s bind to its own scope rather
931 /// than to the globals map — the CommonJS wrapper Node puts every file in.
932 module_scope: bool,
933 /// User-assigned static properties on a builtin namespace/constructor, keyed
934 /// by namespace name then property (`Error` → `prepareStackTrace`,
935 /// `stackTraceLimit`). Each bare `Error` reference allocates a fresh
936 /// `Builtin` handle, so these cannot live in `fn_props` (which is per-heap-
937 /// index); this stable side table lets `Error.prepareStackTrace = fn` persist.
938 builtin_statics: HashMap<String, IndexMap<String, Value>>,
939 /// The shared well-known `Object.prototype` object (chain root for objects).
940 object_proto: Value,
941 /// Class name of each class `prototype` object, by heap index — lets an
942 /// instance recover its constructor name (for `util.inspect` prefix and
943 /// `obj.constructor.name`).
944 proto_class: HashMap<u32, Value>,
945 /// Class constructor values by name, so a running method's `home_class` name
946 /// resolves to its class value (for `super`).
947 class_registry: HashMap<String, Value>,
948 /// Well-known prototype objects for the builtin error constructors, by name.
949 error_protos: HashMap<String, Value>,
950 /// The template object of each tagged-template SITE, keyed by the chunk that
951 /// holds the site and the site's ordinal within its compilation.
952 ///
953 /// GetTemplateObject (13.2.8.4) caches by Parse Node, so a site evaluated
954 /// twice hands back the SAME object: ``const t = () => tag`x`;`` makes
955 /// `t() === t()` true, and a tag that memoizes on the strings array — the
956 /// documented reason the object is cached, and how `lit-html` and `graphql`
957 /// avoid re-parsing — saw a fresh array every call here. Two sites with
958 /// identical text are still distinct objects, which the chunk hash plus the
959 /// ordinal keep apart.
960 template_objects: HashMap<(u64, u64), Value>,
961 /// Real prototype *objects* for the builtin exotics whose instances need a
962 /// genuine `[[Prototype]]` link (`Buffer`, `Uint8Array`). Most builtin
963 /// prototypes are `Builtin("<Ctor>.prototype")` thunk namespaces, which
964 /// cannot appear on a prototype chain and report `typeof "function"`.
965 native_protos: HashMap<String, Value>,
966 /// `Symbol.for` registry: description → symbol value.
967 symbol_registry: HashMap<String, Value>,
968 /// Monotonic id source for fresh `Symbol()` values.
969 next_symbol: u64,
970 /// Every live symbol by its id, so a `@@sym:<id>` property key can be
971 /// turned back into the symbol VALUE for `Object.getOwnPropertySymbols`.
972 symbols_by_id: HashMap<u64, Value>,
973 /// Well-known symbol ids (`Symbol.iterator` …) to their ECMAScript name.
974 /// Identity is by id, not description, so a user `Symbol("Symbol.iterator")`
975 /// is a distinct key.
976 well_known_ids: HashMap<u64, String>,
977 /// Suspended generator coroutines, indexed by `JsObj::Generator.id`.
978 generators: Vec<GenCell>,
979 /// Promise cells, indexed by `JsObj::Promise.id`.
980 promises: Vec<PromiseCell>,
981 /// Whether the loop is part-way through draining the microtask queue, so a
982 /// `nextTick` queued by one of them waits for the round to finish. See
983 /// `next_microtask`.
984 draining_micro: bool,
985 /// `process.nextTick` callbacks (drained before promise microtasks).
986 pub nextticks: std::collections::VecDeque<Task>,
987 /// Promise-reaction / `queueMicrotask` microtasks.
988 pub microtasks: std::collections::VecDeque<Task>,
989 /// `setTimeout`/`setInterval`/`setImmediate` macrotasks.
990 pub macrotasks: Vec<Timer>,
991 /// Monotonic timer-id source.
992 next_timer: u64,
993 /// Cloned by I/O worker threads to post `IoTask`s back to the main-thread
994 /// event loop. Kept alive for the host's lifetime so the loop's `recv` never
995 /// sees a spurious `Disconnected` while a server is running.
996 io_tx: Sender<IoTask>,
997 /// Owned by the event loop (taken out for the blocking `recv`). Receives the
998 /// `IoTask`s posted by I/O threads.
999 io_rx: Option<Receiver<IoTask>>,
1000 /// Ref-count of "things keeping the process alive": open listeners, live
1001 /// sockets, ref'd handles. The loop exits only when this is `0` AND both task
1002 /// queues are empty. A pure script never touches it, so it exits exactly as
1003 /// before.
1004 open_handles: usize,
1005 /// In-process output sink. When `Some`, everything the program writes to
1006 /// stdout/stderr is appended here instead of reaching the process streams —
1007 /// what an embedder (a TUI that owns the terminal) needs so a `console.log`
1008 /// cannot corrupt its display. `None` (the default) is the ordinary
1009 /// standalone `node` behaviour: writes go straight to the real streams.
1010 ///
1011 /// Bytes, not `String`: a program may legitimately write output that is not
1012 /// valid UTF-8 (`process.stdout.write(Buffer.from([0xff]))`), and a `String`
1013 /// buffer can only hold the lossy `U+FFFD` transcription of it.
1014 capture: Option<Vec<u8>>,
1015 /// `process.exitCode`: the code the process exits with when the event loop
1016 /// drains, or `None` while unset. Separate from an explicit
1017 /// `process.exit(n)`, which exits immediately with `n`.
1018 pub exit_code: Option<i32>,
1019 /// Whether the `exit` event has already been emitted, so the `process.exit`
1020 /// path and the end-of-loop path cannot both fire it (Node's `_exiting`).
1021 pub exiting: bool,
1022 /// The one `globalThis` object. It has to be a singleton: `globalThis` is an
1023 /// identity in JS, so `globalThis === globalThis` is `true` and a property
1024 /// written through one read is visible through the next. Minting a fresh
1025 /// object per read made both false.
1026 global_obj: Value,
1027}
1028
1029/// One `process.on`/`process.once` registration. `once` is not decoration: a
1030/// `once` listener must be UNREGISTERED before it runs, so a second `emit` of
1031/// the same event does not reach it. Treating `once` as an alias of `on` made
1032/// `process.once('e', f); process.emit('e'); process.emit('e')` call `f` twice
1033/// and leave it in `process.listeners('e')` — node v26.7.0 calls it once and
1034/// reports zero listeners afterwards.
1035#[derive(Clone)]
1036pub struct ProcListener {
1037 pub f: Value,
1038 pub once: bool,
1039}
1040
1041/// A queued unit of work: either a JS callback invocation (`queueMicrotask`,
1042/// `nextTick`, timer body) or a native step (Promise reaction / async resume).
1043pub enum Task {
1044 Js { cb: Value, args: Vec<Value> },
1045 Native(Box<dyn FnOnce() -> Result<(), String>>),
1046}
1047
1048impl Task {
1049 fn run(self) -> Result<(), String> {
1050 match self {
1051 Task::Js { cb, args } => invoke(&cb, args, None).map(|_| ()),
1052 Task::Native(f) => f(),
1053 }
1054 }
1055}
1056
1057/// A scheduled macrotask (`setTimeout`/`setInterval`/`setImmediate`). Ordering
1058/// is by `(delay, seq)` — a deterministic virtual clock, never wall time.
1059pub struct Timer {
1060 pub id: u64,
1061 pub delay: f64,
1062 pub seq: u64,
1063 pub callback: Value,
1064 pub args: Vec<Value>,
1065 pub cancelled: bool,
1066 /// Repeat period in ms for a `setInterval` timer; `None` for the one-shot
1067 /// `setTimeout`/`setImmediate`. A repeating timer is re-armed with a fresh
1068 /// deadline each time it fires, so it keeps the loop alive indefinitely —
1069 /// exactly like Node, where `setInterval` runs until cleared.
1070 pub interval: Option<f64>,
1071 /// Node's `ref`/`unref` handle bit. Only a *referenced* pending timer keeps
1072 /// the event loop alive; an unref'd one still fires while the loop happens
1073 /// to be alive for another reason, but never holds it open by itself.
1074 pub refed: bool,
1075 /// Real wall-clock deadline (`now + delay`), used only on the real-clock
1076 /// path (an open handle or a pending interval). On the pure virtual clock
1077 /// this is ignored.
1078 pub deadline: Instant,
1079}
1080
1081/// One suspended generator. `coro` is `None` only while actively running (taken
1082/// out across `Coroutine::resume`); `ctx` holds its volatile execution context
1083/// (frames/signal/error/exc) while suspended.
1084struct GenCell {
1085 coro: Option<corosensei::Coroutine<Value, Value, Result<Value, String>>>,
1086 /// Raw pointer to the coroutine body's `Yielder`, published on entry (same
1087 /// thread → valid for the body's life). Read by `yield` to suspend.
1088 yielder: *const (),
1089 ctx: GenContext,
1090 done: bool,
1091 /// True once the body has been resumed at least once (so it is suspended at a
1092 /// `yield`). `.return()`/`.throw()` only unwind a *started* generator.
1093 started: bool,
1094 /// A completion injected by `.return(v)` / `.throw(e)`: consumed by the next
1095 /// `yield` resume so the body unwinds (running any pending `finally`).
1096 inject: Option<GenInject>,
1097 /// True for an `async function*` body, where `await` AND `yield` share one
1098 /// coroutine yielder: `await` wraps its operand in an await marker so the
1099 /// driver can tell an internal suspension from a real yield.
1100 async_gen: bool,
1101 /// `[[AsyncGeneratorQueue]]` — pending requests as
1102 /// `(completion, step promise id)`. ECMA-262 27.6.3.6 keeps this queue so
1103 /// overlapping requests resume the body ONE AT A TIME and settle in request
1104 /// order; without it a second request issued before the first settles races
1105 /// past it and the results arrive swapped. `.next`, `.return` AND `.throw`
1106 /// all enqueue — a `.return()` that skipped the queue would terminate the
1107 /// body while an earlier `.next()` was still suspended on an `await`, and
1108 /// that `.next()` would then wrongly report `{done: true}`.
1109 queue: std::collections::VecDeque<(GenReq, u32)>,
1110 /// True while a queued request is being driven.
1111 running: bool,
1112 /// The [`stack_floor`] that applies while this generator's body is running.
1113 ///
1114 /// A corosensei coroutine executes on its OWN mmap'd stack, so the address
1115 /// range the thread's pthread record describes says nothing about how much
1116 /// room the body has left. Recorded from the coroutine's `Stack::limit()` at
1117 /// construction and swapped in around every resume; without it the guard
1118 /// compared a coroutine stack pointer against the main stack's floor and
1119 /// (depending on where mmap landed) either fired immediately or never.
1120 stack_floor: usize,
1121}
1122
1123/// A forced completion pushed into a suspended generator by `.return()`/`.throw()`.
1124enum GenInject {
1125 Return(Value),
1126 Throw(Value),
1127}
1128
1129/// One queued `[[AsyncGeneratorQueue]]` request. ECMA-262 27.6.3.6
1130/// `AsyncGeneratorEnqueue` records a *completion*, not just a sent value, which
1131/// is why `.return()` and `.throw()` queue behind pending `.next()` calls
1132/// instead of unwinding the body on the spot.
1133#[derive(Clone)]
1134pub enum GenReq {
1135 /// `.next(v)` — resume normally with `v`.
1136 Next(Value),
1137 /// `.return(v)` — resume with a forced return completion.
1138 Return(Value),
1139 /// `.throw(e)` — resume with a forced throw completion.
1140 Throw(Value),
1141}
1142
1143/// The mutable "execution registers" swapped at every generator resume/suspend
1144/// boundary so a suspended generator's half-finished frame/signal state never
1145/// leaks into the resuming caller. The heap, function/class tables and globals
1146/// are shared and never swapped.
1147#[derive(Default)]
1148struct GenContext {
1149 frames: Vec<Frame>,
1150 error: Option<String>,
1151 exc: Option<Value>,
1152 signal: Option<Signal>,
1153}
1154
1155thread_local! {
1156 /// Id of the generator whose body is currently executing, or `None` at the
1157 /// root. `yield` suspends this generator.
1158 static CUR_GEN: std::cell::Cell<Option<u32>> = const { std::cell::Cell::new(None) };
1159}
1160
1161thread_local! {
1162 static HOST: RefCell<JsHost> = RefCell::new(JsHost::new());
1163}
1164
1165/// Run `f` with mutable access to the thread-local host.
1166pub fn with_host<R>(f: impl FnOnce(&mut JsHost) -> R) -> R {
1167 HOST.with(|h| f(&mut h.borrow_mut()))
1168}
1169
1170/// Reset the host to a clean slate (fresh module frame).
1171pub fn reset_host() {
1172 with_host(|h| *h = JsHost::new());
1173 // Drop any cached module handles / factory closure — they index the old heap.
1174 crate::module::reset();
1175}
1176
1177impl Default for JsHost {
1178 fn default() -> Self {
1179 Self::new()
1180 }
1181}
1182
1183impl JsHost {
1184 pub fn new() -> JsHost {
1185 let global_env = new_env(None);
1186 let (io_tx, io_rx) = std::sync::mpsc::channel();
1187 let mut h = JsHost {
1188 tdz: None,
1189 tdz_globals: Default::default(),
1190 heap: Vec::new(),
1191 funcs: Vec::new(),
1192 tries: Vec::new(),
1193 globals: VarMap::default(),
1194 global_consts: rustc_hash::FxHashSet::default(),
1195 frames: vec![Frame {
1196 env: global_env.clone(),
1197 base_env: global_env.clone(),
1198 this_obj: None,
1199 new_target: None,
1200 home_class: None,
1201 home_static: false,
1202 home_object: None,
1203 strict: false,
1204 line: 0,
1205 owner: None,
1206 is_module: true,
1207 this_state: ThisState::Plain,
1208 }],
1209 global_env,
1210 error: None,
1211 exc: None,
1212 signal: None,
1213 pending_rejections: Vec::new(),
1214 process_listeners: IndexMap::new(),
1215 null_val: Value::Undef,
1216 protos: HashMap::new(),
1217 null_proto_objs: HashSet::new(),
1218 fn_props: HashMap::new(),
1219 accessors: HashMap::new(),
1220 prop_attrs: HashMap::new(),
1221 non_extensible: HashSet::new(),
1222 private_methods: HashSet::new(),
1223 array_holes: HashMap::new(),
1224 super_replacement: None,
1225 derived_ctor_next: false,
1226 module_scope: false,
1227 builtin_statics: HashMap::new(),
1228 object_proto: Value::Undef,
1229 proto_class: HashMap::new(),
1230 class_registry: HashMap::new(),
1231 error_protos: HashMap::new(),
1232 template_objects: HashMap::new(),
1233 native_protos: HashMap::new(),
1234 symbol_registry: HashMap::new(),
1235 next_symbol: 1,
1236 symbols_by_id: HashMap::new(),
1237 well_known_ids: HashMap::new(),
1238 generators: Vec::new(),
1239 promises: Vec::new(),
1240 microtasks: std::collections::VecDeque::new(),
1241 draining_micro: false,
1242 nextticks: std::collections::VecDeque::new(),
1243 macrotasks: Vec::new(),
1244 next_timer: 1,
1245 io_tx,
1246 io_rx: Some(io_rx),
1247 open_handles: 0,
1248 capture: None,
1249 exit_code: None,
1250 exiting: false,
1251 global_obj: Value::Undef,
1252 };
1253 h.null_val = h.alloc(JsObj::Null);
1254 // `Object.prototype`: the chain root, its own `[[Prototype]]` is null.
1255 h.object_proto = h.new_object(IndexMap::new());
1256 h.global_obj = h.new_object(IndexMap::new());
1257 h
1258 }
1259
1260 /// Whether `v` IS the one `globalThis` object (not merely an object).
1261 pub fn is_global_object(&self, v: &Value) -> bool {
1262 !matches!(self.global_obj, Value::Undef) && self.global_obj == *v
1263 }
1264
1265 /// The `globalThis` object — one per host, so its identity and its
1266 /// properties both survive across reads.
1267 pub fn global_object(&mut self) -> Value {
1268 if matches!(self.global_obj, Value::Undef) {
1269 self.global_obj = self.new_object(IndexMap::new());
1270 }
1271 self.global_obj.clone()
1272 }
1273
1274 // ── prototype chain ──────────────────────────────────────────────────
1275 /// The `[[Prototype]]` of a heap value, if explicitly linked.
1276 pub fn proto_of(&self, v: &Value) -> Option<Value> {
1277 if let Value::Obj(i) = v {
1278 self.protos.get(i).cloned()
1279 } else {
1280 None
1281 }
1282 }
1283 /// Set `v`'s `[[Prototype]]` to `proto`. Null links the object as an explicit
1284 /// null-prototype object (recorded so `instanceof Object` reads false);
1285 /// undefined just clears any link without the null marker.
1286 pub fn set_proto(&mut self, v: &Value, proto: Value) {
1287 if let Value::Obj(i) = v {
1288 if self.is_null(&proto) {
1289 self.protos.remove(i);
1290 self.null_proto_objs.insert(*i);
1291 } else if matches!(proto, Value::Undef) {
1292 self.protos.remove(i);
1293 } else {
1294 self.protos.insert(*i, proto);
1295 self.null_proto_objs.remove(i);
1296 }
1297 }
1298 }
1299 /// Whether `v`'s `[[Prototype]]` was explicitly set to null.
1300 pub fn has_null_proto(&self, v: &Value) -> bool {
1301 matches!(v, Value::Obj(i) if self.null_proto_objs.contains(i))
1302 }
1303 /// Whether `util.inspect` renders `v` with the `[Object: null prototype]`
1304 /// tag. That is a question about the object's ACTUAL `[[Prototype]]`, which
1305 /// for `Object.prototype` is null even though nothing ever set it so: it is
1306 /// the chain root and was never passed through `set_proto`, so the
1307 /// explicitly-nulled registry does not hold it and `console.log(Object
1308 /// .prototype)` printed a bare `{}` where node prints the tag.
1309 ///
1310 /// Kept apart from [`Self::has_null_proto`], which nine other call sites ask
1311 /// about whether Object.prototype's own methods and `__proto__` accessor are
1312 /// INHERITED. `Object.prototype` inherits nothing and still owns all of them.
1313 pub fn inspects_null_proto(&self, v: &Value) -> bool {
1314 self.has_null_proto(v) || *v == self.object_proto
1315 }
1316 pub fn object_proto(&self) -> Value {
1317 self.object_proto.clone()
1318 }
1319 /// Record that the prototype object `proto` belongs to the class constructor
1320 /// `class_val` (so instances can recover their constructor).
1321 pub fn tag_proto_class(&mut self, proto: &Value, class_val: Value) {
1322 if let Value::Obj(i) = proto {
1323 self.proto_class.insert(*i, class_val);
1324 }
1325 }
1326 /// The class whose `prototype` object IS `v`, if `v` is one.
1327 pub fn class_owning_proto(&self, v: &Value) -> Option<Value> {
1328 match v {
1329 Value::Obj(i) => self.proto_class.get(i).cloned(),
1330 _ => None,
1331 }
1332 }
1333 /// The class constructor value nearest in `obj`'s prototype chain, if any.
1334 pub fn class_of(&self, obj: &Value) -> Option<Value> {
1335 let mut cur = self.proto_of(obj);
1336 while let Some(p) = cur {
1337 if let Value::Obj(i) = &p {
1338 if let Some(c) = self.proto_class.get(i) {
1339 return Some(c.clone());
1340 }
1341 }
1342 cur = self.proto_of(&p);
1343 }
1344 None
1345 }
1346 /// The constructor display name of `obj` for `util.inspect` (empty ⇒ plain
1347 /// object, no prefix).
1348 pub fn ctor_name(&self, obj: &Value) -> String {
1349 if let Some(c) = self.class_of(obj) {
1350 if let Some(JsObj::Class(cv)) = self.get(&c) {
1351 return cv.name.clone();
1352 }
1353 }
1354 // A `function F(){}` constructor is not a `class`, so it has no
1355 // `proto_class` entry. V8's `getConstructorName` walks the prototype
1356 // chain for an own `constructor` that is a named function — which is
1357 // what makes `console.log(new F())` print `F { y: 2 }`.
1358 let mut cur = self.proto_of(obj);
1359 while let Some(p) = cur {
1360 let ctor = match self.get(&p) {
1361 Some(JsObj::Object(props)) => props.get("constructor").cloned(),
1362 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => self.fn_prop(&p, "constructor"),
1363 _ => None,
1364 };
1365 if let Some(f) = ctor {
1366 let n = self.callable_name(&f);
1367 if !n.is_empty() {
1368 return n;
1369 }
1370 }
1371 cur = self.proto_of(&p);
1372 }
1373 String::new()
1374 }
1375
1376 /// Whether a callable owns a `prototype` property. `MakeConstructor`
1377 /// (10.2.5) runs for an ordinary function definition and for every
1378 /// generator; an arrow, a `MethodDefinition`, an async function and a bound
1379 /// function are not constructors and own none.
1380 pub fn owns_prototype(&self, v: &Value) -> bool {
1381 match self.get(v) {
1382 Some(JsObj::Class(_)) => true,
1383 Some(JsObj::Func(f)) => match self.funcs.get(f.def_id) {
1384 Some(d) => d.is_generator || !(d.is_arrow || d.is_async || d.is_method),
1385 None => false,
1386 },
1387 _ => false,
1388 }
1389 }
1390
1391 /// A function's own-property table (created on demand).
1392 pub fn fn_prop(&self, v: &Value, name: &str) -> Option<Value> {
1393 if let Value::Obj(i) = v {
1394 self.fn_props.get(i).and_then(|m| m.get(name).cloned())
1395 } else {
1396 None
1397 }
1398 }
1399
1400 /// A class static member, inherited down the constructor chain: a subclass
1401 /// sees its superclass's `static` methods/fields (`Sub.create` → `Base.create`).
1402 pub fn class_static(&self, class_val: &Value, name: &str) -> Option<Value> {
1403 let mut cur = class_val.clone();
1404 loop {
1405 if let Some(v) = self.fn_prop(&cur, name) {
1406 return Some(v);
1407 }
1408 match self.get(&cur) {
1409 Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1410 _ => return None,
1411 }
1412 }
1413 }
1414
1415 /// The first `extends` ancestor that is NOT a user class — the builtin
1416 /// constructor a class chain bottoms out in (`class D extends Array {}` →
1417 /// the `Array` builtin), or `None` for a chain of user classes only.
1418 ///
1419 /// `class_static` walks `ClassVal.parent` and gives up the moment the parent
1420 /// stops being a `Class`, so a static declared by the BUILTIN half of the
1421 /// chain was unreachable: `D.from` read `undefined` where node inherits
1422 /// `Array.from`. Returning the ancestor lets the caller finish the lookup
1423 /// with an ordinary property read, which is what reaches a builtin's
1424 /// statics.
1425 pub fn class_builtin_ancestor(&self, class_val: &Value) -> Option<Value> {
1426 let mut cur = class_val.clone();
1427 loop {
1428 match self.get(&cur) {
1429 Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1430 _ => return Some(cur),
1431 }
1432 }
1433 }
1434 pub fn set_fn_prop(&mut self, v: &Value, name: &str, val: Value) {
1435 if let Value::Obj(i) = v {
1436 self.fn_props
1437 .entry(*i)
1438 .or_default()
1439 .insert(name.to_string(), val);
1440 }
1441 // `name` and `prototype` are own properties of every function/class, but
1442 // never enumerable ones (SetFunctionName 10.2.9, MakeConstructor
1443 // 10.2.5), so `Object.keys(fn)` and `for (k in fn)` report only what a
1444 // script assigned. An ARRAY receiver reaching the same side table has no
1445 // such exotic keys — `arr.name = 'x'` is an ordinary enumerable property.
1446 if !matches!(self.kind_of(v), Some(ObjKind::Func) | Some(ObjKind::Class)) {
1447 return;
1448 }
1449 let attrs = match name {
1450 "name" => PropAttrs {
1451 writable: false,
1452 enumerable: false,
1453 configurable: true,
1454 },
1455 "prototype" => PropAttrs {
1456 writable: true,
1457 enumerable: false,
1458 configurable: false,
1459 },
1460 _ => return,
1461 };
1462 self.set_prop_attrs(v, name, attrs);
1463 }
1464 /// A user-assigned static on a builtin namespace (`Error.prepareStackTrace`).
1465 pub fn builtin_static(&self, ns: &str, name: &str) -> Option<Value> {
1466 self.builtin_statics
1467 .get(ns)
1468 .and_then(|m| m.get(name).cloned())
1469 }
1470 /// Assign a static on a builtin namespace (persists across fresh `Builtin`
1471 /// handles for the same namespace).
1472 pub fn set_builtin_static(&mut self, ns: &str, name: &str, val: Value) {
1473 self.builtin_statics
1474 .entry(ns.to_string())
1475 .or_default()
1476 .insert(name.to_string(), val);
1477 }
1478 /// `delete <ns>.<name>` for a script-assigned static. Reports whether the
1479 /// key was there — without this, `delete Array.prototype.patch` answered
1480 /// true and left the entry in place, so the patch outlived its own removal.
1481 pub fn remove_builtin_static(&mut self, ns: &str, name: &str) -> bool {
1482 self.builtin_statics
1483 .get_mut(ns)
1484 .is_some_and(|m| m.shift_remove(name).is_some())
1485 }
1486 /// Every namespace a script has assigned a static onto, with that
1487 /// namespace's assigned keys — the source of the user-added half of
1488 /// `Object.getOwnPropertyNames(Array.prototype)`.
1489 pub fn builtin_static_keys(&self, ns: &str) -> Vec<String> {
1490 self.builtin_statics
1491 .get(ns)
1492 .map(|m| m.keys().cloned().collect())
1493 .unwrap_or_default()
1494 }
1495 /// Drop an own property from the side table (`delete arr.foo`,
1496 /// `delete fn.tag`). Reports whether the key was there.
1497 pub fn remove_fn_prop(&mut self, v: &Value, name: &str) -> bool {
1498 match v {
1499 Value::Obj(i) => self
1500 .fn_props
1501 .get_mut(i)
1502 .map(|m| m.shift_remove(name).is_some())
1503 .unwrap_or(false),
1504 _ => false,
1505 }
1506 }
1507 pub fn fn_prop_keys(&self, v: &Value) -> Vec<String> {
1508 if let Value::Obj(i) = v {
1509 self.fn_props
1510 .get(i)
1511 .map(|m| m.keys().cloned().collect())
1512 .unwrap_or_default()
1513 } else {
1514 Vec::new()
1515 }
1516 }
1517
1518 /// Install an accessor `(get, set)` for `key` on the object `owner`.
1519 pub fn set_accessor(
1520 &mut self,
1521 owner: &Value,
1522 key: &str,
1523 get: Option<Value>,
1524 set: Option<Value>,
1525 ) {
1526 if let Value::Obj(i) = owner {
1527 // Accessors live in their own table, but JS reports own keys in a
1528 // single insertion order across data AND accessor properties. Drop an
1529 // ordering marker into the property map so
1530 // `{ a: 1, get b() {}, c: 3 }` enumerates a, b, c — not a, c, b.
1531 // The marker is `@@`-prefixed, so it is invisible to every reader.
1532 let marker = format!("{ORD_MARKER}{key}");
1533 match self.get_mut(owner) {
1534 Some(JsObj::Object(props)) => {
1535 if !props.contains_key(key) && !props.contains_key(&marker) {
1536 props.insert(marker, Value::Undef);
1537 }
1538 }
1539 // A function or class keeps its own properties in the fn-prop
1540 // side table, so its ordering marker belongs there. Without it a
1541 // static accessor enumerated AFTER every static field and method
1542 // regardless of where the class body declared it: node reports
1543 // `class A { static s = 2; static get sv(){} static m(){} }` as
1544 // `['sv', 'm', 's']` — the methods and accessors in source order
1545 // first, then the fields — and this reported `['m', 's', 'sv']`.
1546 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1547 let table = self.fn_props.entry(*i).or_default();
1548 if !table.contains_key(key) && !table.contains_key(&marker) {
1549 table.insert(marker, Value::Undef);
1550 }
1551 }
1552 _ => {}
1553 }
1554 let slot = self
1555 .accessors
1556 .entry(*i)
1557 .or_default()
1558 .entry(key.to_string())
1559 .or_insert((None, None));
1560 if get.is_some() {
1561 slot.0 = get;
1562 }
1563 if set.is_some() {
1564 slot.1 = set;
1565 }
1566 }
1567 }
1568 /// The accessor `(get, set)` for `key` directly on `owner` (no chain walk).
1569 /// Drop an own accessor property entirely, marker and all.
1570 ///
1571 /// `delete obj.accessorProp` used to clear only the property map, and an
1572 /// accessor does not live there — so the delete reported success while the
1573 /// getter kept answering and `in` kept reporting the key.
1574 pub fn remove_accessor(&mut self, owner: &Value, key: &str) {
1575 if let Value::Obj(i) = owner {
1576 if let Some(m) = self.accessors.get_mut(i) {
1577 m.shift_remove(key);
1578 }
1579 }
1580 let marker = format!("{ORD_MARKER}{key}");
1581 match self.get_mut(owner) {
1582 Some(JsObj::Object(props)) => {
1583 props.shift_remove(&marker);
1584 }
1585 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1586 if let Value::Obj(i) = owner {
1587 if let Some(t) = self.fn_props.get_mut(i) {
1588 t.shift_remove(&marker);
1589 }
1590 }
1591 }
1592 _ => {}
1593 }
1594 }
1595
1596 /// Turn an own accessor property into a data property carrying `value`,
1597 /// keeping its place in the own-key order.
1598 ///
1599 /// `set_accessor` records that order with an `@@ord:` marker in the
1600 /// property map rather than a real key, so deleting the accessor and
1601 /// inserting the value would append the key at the end instead. Node
1602 /// reports `{ a: 1, get b() {}, c: 3 }` redefined through
1603 /// `Object.defineProperty(o, 'b', { value })` as `a, b, c`.
1604 pub fn accessor_to_data(&mut self, owner: &Value, key: &str, value: Value) {
1605 if let Value::Obj(i) = owner {
1606 if let Some(m) = self.accessors.get_mut(i) {
1607 m.shift_remove(key);
1608 }
1609 }
1610 let marker = format!("{ORD_MARKER}{key}");
1611 let swap = |map: &mut IndexMap<String, Value>| match map.get_index_of(&marker) {
1612 Some(pos) => {
1613 *map = map
1614 .iter()
1615 .enumerate()
1616 .map(|(n, (k, v))| {
1617 if n == pos {
1618 (key.to_string(), value.clone())
1619 } else {
1620 (k.clone(), v.clone())
1621 }
1622 })
1623 .collect();
1624 }
1625 None => {
1626 map.insert(key.to_string(), value.clone());
1627 }
1628 };
1629 let fn_table = matches!(
1630 self.get(owner),
1631 Some(JsObj::Func(_)) | Some(JsObj::Class(_))
1632 );
1633 if fn_table {
1634 if let Value::Obj(i) = owner {
1635 swap(self.fn_props.entry(*i).or_default());
1636 }
1637 } else if let Some(JsObj::Object(props)) = self.get_mut(owner) {
1638 swap(props);
1639 }
1640 }
1641
1642 /// Move the per-heap-index bookkeeping of `src` onto `dst`.
1643 ///
1644 /// Used when one object becomes another in place (a class extending a
1645 /// builtin exotic). The prototype link is deliberately NOT moved: `dst`
1646 /// already points at the leaf class's prototype, which is the one its
1647 /// methods must resolve through.
1648 pub fn move_index_state(&mut self, src: u32, dst: u32) {
1649 if let Some(holes) = self.array_holes.remove(&src) {
1650 self.array_holes.insert(dst, holes);
1651 }
1652 if let Some(attrs) = self.prop_attrs.remove(&src) {
1653 self.prop_attrs.entry(dst).or_default().extend(attrs);
1654 }
1655 if let Some(props) = self.fn_props.remove(&src) {
1656 self.fn_props.entry(dst).or_default().extend(props);
1657 }
1658 if let Some(acc) = self.accessors.remove(&src) {
1659 self.accessors.entry(dst).or_default().extend(acc);
1660 }
1661 }
1662
1663 pub fn own_accessor(&self, owner: &Value, key: &str) -> Option<(Option<Value>, Option<Value>)> {
1664 if let Value::Obj(i) = owner {
1665 self.accessors.get(i).and_then(|m| m.get(key).cloned())
1666 } else {
1667 None
1668 }
1669 }
1670
1671 /// The own accessor-property keys of `owner`, in installation order.
1672 pub fn own_accessor_keys(&self, owner: &Value) -> Vec<String> {
1673 match owner {
1674 Value::Obj(i) => self
1675 .accessors
1676 .get(i)
1677 .map(|m| m.keys().cloned().collect())
1678 .unwrap_or_default(),
1679 _ => Vec::new(),
1680 }
1681 }
1682
1683 // ── own-property attributes ──────────────────────────────────────────
1684
1685 /// Record non-default attributes for `owner[key]`. Storing the default shape
1686 /// clears the entry so the table only ever holds deviations.
1687 pub fn set_prop_attrs(&mut self, owner: &Value, key: &str, attrs: PropAttrs) {
1688 if let Value::Obj(i) = owner {
1689 if attrs == PropAttrs::default() {
1690 if let Some(m) = self.prop_attrs.get_mut(i) {
1691 m.shift_remove(key);
1692 }
1693 } else {
1694 self.prop_attrs
1695 .entry(*i)
1696 .or_default()
1697 .insert(key.to_string(), attrs);
1698 }
1699 }
1700 }
1701
1702 /// Copy every recorded property attribute from `from` to `to`. A pass that
1703 /// rebuilds an object (`JSON.stringify`'s `toJSON` walk) must carry them
1704 /// across or the copy silently re-exposes non-enumerable slots.
1705 pub fn copy_prop_attrs(&mut self, from: &Value, to: &Value) {
1706 if let (Value::Obj(f), Value::Obj(_)) = (from, to) {
1707 if let Some(m) = self.prop_attrs.get(f).cloned() {
1708 for (k, a) in m {
1709 self.set_prop_attrs(to, &k, a);
1710 }
1711 }
1712 }
1713 }
1714
1715 /// The attributes of own property `owner[key]` (all-true when unrecorded).
1716 pub fn prop_attrs(&self, owner: &Value, key: &str) -> PropAttrs {
1717 // An array's `length` is the array exotic's own property (10.4.2):
1718 // never enumerated and never configurable, and writable until
1719 // `Object.freeze` clears that — which is what stops a `push` from
1720 // extending a frozen array. Reporting it unconditionally writable made
1721 // `Object.isFrozen(Object.freeze([]))` false once the elements started
1722 // being sealed, because `length` was then the one key that never
1723 // followed.
1724 if key == "length" && matches!(self.get(owner), Some(JsObj::Array(_))) {
1725 let writable = match owner {
1726 Value::Obj(i) => self
1727 .prop_attrs
1728 .get(i)
1729 .and_then(|m| m.get(key))
1730 .map(|a| a.writable)
1731 .unwrap_or(true),
1732 _ => true,
1733 };
1734 return PropAttrs {
1735 writable,
1736 enumerable: false,
1737 // An ARGUMENTS object's `length` is an ordinary data property
1738 // (10.4.4.6), so it is configurable where a real array's is
1739 // not. The two share a backing representation here, so the
1740 // exotic's attributes have to be told apart explicitly.
1741 configurable: crate::builtins::is_arguments_h(self, owner),
1742 };
1743 }
1744 match owner {
1745 Value::Obj(i) => self
1746 .prop_attrs
1747 .get(i)
1748 .and_then(|m| m.get(key))
1749 .copied()
1750 .unwrap_or_default(),
1751 _ => PropAttrs::default(),
1752 }
1753 }
1754
1755 /// Whether own property `owner[key]` shows up in `for-in`/`Object.keys`.
1756 /// Internal slots (`@@…`) and private class fields (`#…`) never do.
1757 pub fn is_enumerable(&self, owner: &Value, key: &str) -> bool {
1758 !key.starts_with("@@") && !key.starts_with('#') && self.prop_attrs(owner, key).enumerable
1759 }
1760
1761 /// Mark `owner[key]` non-enumerable, leaving it writable/configurable — the
1762 /// shape of every V8 "hidden but real" own property.
1763 pub fn hide_prop(&mut self, owner: &Value, key: &str) {
1764 self.set_prop_attrs(owner, key, PropAttrs::HIDDEN);
1765 }
1766
1767 /// Whether a plain `owner[key] = v` assignment is allowed to land. A
1768 /// non-writable data property silently ignores the write in sloppy mode,
1769 /// which is the mode every script here runs in; so does adding a *new* key to
1770 /// a non-extensible object.
1771 pub fn can_write_prop(&self, owner: &Value, key: &str) -> bool {
1772 if !self.prop_attrs(owner, key).writable {
1773 return false;
1774 }
1775 // An intrinsic prototype on the chain may define the name NON-WRITABLE,
1776 // and those members own no map entry for the walk below to find:
1777 // `o[Symbol.toStringTag] = 'x'` where `o` inherits from `Map.prototype`
1778 // is refused in node and was creating an own property here, which then
1779 // changed the object's brand.
1780 // The receiver's OWN kind counts too, not only the prototypes an
1781 // explicit link reaches: a plain array inherits `Array.prototype`
1782 // implicitly, with no link for the walk to follow, and
1783 // `a[Symbol.unscopables] = 'x'` is refused there just the same.
1784 //
1785 // Restricted to SYMBOL-keyed members. The string-keyed non-writable
1786 // ones — `Function.prototype.length`/`name`, `String.prototype.length`
1787 // — are also OWN properties of every instance, so the inherited rule
1788 // never decides them; applying it anyway blocked `SetFunctionName`
1789 // itself, and naming the setter in `Object.defineProperty(o, 'v', {set
1790 // (x) {…}})` then threw.
1791 if key.starts_with("@@")
1792 && crate::builtins::own_ctor_name(self, owner)
1793 .into_iter()
1794 .chain(crate::builtins::chain_intrinsic_ctors_h(self, owner))
1795 .any(|c| crate::builtins::is_proto_readonly(c, key))
1796 {
1797 return false;
1798 }
1799 // 10.1.9.2: with no OWN property, the inherited one decides. A
1800 // non-writable data property up the chain blocks the write rather than
1801 // being shadowed — including one on a frozen prototype. Only own
1802 // attributes were consulted, so `Object.create(frozenBase).f = 2`
1803 // quietly created an own property node refuses to create.
1804 //
1805 // An inherited ACCESSOR does not block: its setter runs, and the write
1806 // path checks for one before reaching here.
1807 let has_own = match self.get(owner) {
1808 Some(JsObj::Object(p)) => p.contains_key(key),
1809 _ => true,
1810 };
1811 if !has_own {
1812 let mut cur = self.proto_of(owner);
1813 while let Some(proto) = cur {
1814 if self.own_accessor(&proto, key).is_some() {
1815 break;
1816 }
1817 let present =
1818 matches!(self.get(&proto), Some(JsObj::Object(p)) if p.contains_key(key));
1819 if present {
1820 if !self.prop_attrs(&proto, key).writable {
1821 return false;
1822 }
1823 break;
1824 }
1825 cur = self.proto_of(&proto);
1826 }
1827 }
1828 if self.is_extensible(owner) {
1829 return true;
1830 }
1831 // A non-extensible object refuses a NEW key. Only the plain-object arm
1832 // could name its own keys, so every other shape answered "own" for any
1833 // key at all: `Object.freeze(arr).extra = 1` landed, and so did a write
1834 // to the frozen template object a tagged template hands its tag.
1835 match self.get(owner) {
1836 Some(JsObj::Object(p)) => p.contains_key(key),
1837 Some(JsObj::Array(items)) => {
1838 key == "length"
1839 || key
1840 .parse::<usize>()
1841 .map(|i| i < items.len())
1842 .unwrap_or(false)
1843 || self.fn_prop(owner, key).is_some()
1844 }
1845 // A RegExp's `lastIndex` is an own property, so a merely
1846 // NON-EXTENSIBLE regexp still accepts a write to it.
1847 Some(JsObj::RegExp(_)) => key == "lastIndex" || self.fn_prop(owner, key).is_some(),
1848 // Every other shape keeps its own properties in the fn-prop side
1849 // table (a function's statics, a Map's assigned properties), so
1850 // "does it already own this key" is that table's question. Answering
1851 // a blanket `true` let a NEW key land on a frozen function and a
1852 // frozen Map.
1853 _ => self.fn_prop(owner, key).is_some(),
1854 }
1855 }
1856
1857 /// Mark `v` closed to new properties (`Object.preventExtensions`).
1858 pub fn prevent_extensions(&mut self, v: &Value) {
1859 if let Value::Obj(i) = v {
1860 self.non_extensible.insert(*i);
1861 }
1862 }
1863
1864 pub fn is_extensible(&self, v: &Value) -> bool {
1865 !matches!(v, Value::Obj(i) if self.non_extensible.contains(i))
1866 }
1867
1868 /// Apply `Object.seal` (`freeze == false`) or `Object.freeze` (`true`): close
1869 /// the object and strip `configurable` — and, when freezing, `writable` —
1870 /// from every own property, data and accessor alike.
1871 pub fn seal_object(&mut self, v: &Value, freeze: bool) {
1872 self.prevent_extensions(v);
1873 let mut keys = self.integrity_keys(v);
1874 keys.extend(self.own_accessor_keys(v));
1875 for k in keys {
1876 let mut a = self.prop_attrs(v, &k);
1877 a.configurable = false;
1878 if freeze {
1879 a.writable = false;
1880 }
1881 self.set_prop_attrs(v, &k, a);
1882 }
1883 }
1884
1885 /// The own DATA-property keys SetIntegrityLevel (7.3.15) walks.
1886 ///
1887 /// An array's elements are own properties too, and only the `Object` arm was
1888 /// walked — so `Object.freeze([1, 2])` sealed nothing: `a[0] = 9` wrote
1889 /// through, and the elements still reported `writable: true,
1890 /// configurable: true` while `Object.isFrozen` answered true over an empty
1891 /// key list. `length` is an own property as well, and freezing it is what
1892 /// stops a `push` from extending a frozen array.
1893 fn integrity_keys(&self, v: &Value) -> Vec<String> {
1894 let side_table_keys = |v: &Value| -> Vec<String> {
1895 match v {
1896 Value::Obj(i) => self
1897 .fn_props
1898 .get(i)
1899 .map(|m| m.keys().cloned().collect())
1900 .unwrap_or_default(),
1901 _ => Vec::new(),
1902 }
1903 };
1904 match self.get(v) {
1905 Some(JsObj::Object(p)) => p.keys().cloned().collect(),
1906 // A RegExp's only own property is its `lastIndex` cursor, which
1907 // lives in the `RegExpObj` struct. Without it here `Object.freeze`
1908 // sealed nothing and a frozen regexp's cursor still moved.
1909 Some(JsObj::RegExp(_)) => vec!["lastIndex".to_string()],
1910 // A function's statics and a Map's assigned properties live in the
1911 // fn-prop side table, and freezing has to reach them too.
1912 Some(JsObj::Func(_))
1913 | Some(JsObj::Class(_))
1914 | Some(JsObj::Map { .. })
1915 | Some(JsObj::Set { .. })
1916 | Some(JsObj::Promise { .. }) => side_table_keys(v),
1917 Some(JsObj::Array(items)) => (0..items.len())
1918 .map(|i| i.to_string())
1919 .chain(std::iter::once("length".to_string()))
1920 // A named property stuck on an array (`a.tag = 't'`, a match
1921 // array's `.index`/`.groups`) is an own property too, and
1922 // freezing has to reach it.
1923 .chain(match v {
1924 Value::Obj(i) => self
1925 .fn_props
1926 .get(i)
1927 .map(|m| m.keys().cloned().collect::<Vec<_>>())
1928 .unwrap_or_default(),
1929 _ => Vec::new(),
1930 })
1931 .collect(),
1932 _ => Vec::new(),
1933 }
1934 }
1935
1936 /// `Object.isSealed` (`freeze == false`) / `Object.isFrozen` (`true`).
1937 pub fn is_sealed(&self, v: &Value, freeze: bool) -> bool {
1938 if self.is_extensible(v) {
1939 return false;
1940 }
1941 let mut keys = self.integrity_keys(v);
1942 keys.extend(self.own_accessor_keys(v));
1943 keys.iter().all(|k| {
1944 let a = self.prop_attrs(v, k);
1945 !a.configurable && (!freeze || !a.writable)
1946 })
1947 }
1948
1949 /// A fresh unique `Symbol(desc)` value.
1950 pub fn new_symbol(&mut self, desc: Option<String>) -> Value {
1951 let id = self.next_symbol;
1952 self.next_symbol += 1;
1953 let v = self.alloc(JsObj::Symbol { desc, id });
1954 self.symbols_by_id.insert(id, v.clone());
1955 v
1956 }
1957
1958 /// The symbol VALUE an internal symbol property key (`@@sym:<id>` or a
1959 /// well-known `@@iterator`) came from.
1960 pub fn symbol_of_key(&self, k: &str) -> Option<Value> {
1961 if let Some(id) = k.strip_prefix("@@sym:").and_then(|i| i.parse::<u64>().ok()) {
1962 return self.symbols_by_id.get(&id).cloned();
1963 }
1964 let name = k.strip_prefix("@@")?;
1965 WELL_KNOWN_SYMBOLS
1966 .contains(&name)
1967 .then(|| {
1968 self.symbol_registry
1969 .get(&format!("@@Symbol.{name}"))
1970 .cloned()
1971 })
1972 .flatten()
1973 }
1974
1975 /// The own symbol-keyed property keys of `v` as SYMBOL values —
1976 /// `Object.getOwnPropertySymbols` / the symbol half of `Reflect.ownKeys`.
1977 pub fn own_symbol_keys(&self, v: &Value) -> Vec<Value> {
1978 let keys: Vec<String> = match self.get(v) {
1979 Some(JsObj::Object(p)) => p.keys().cloned().collect(),
1980 // An Array/Function receiver has no property map: its non-index own
1981 // properties — symbol-keyed ones included — live in the fn-prop side
1982 // table, and are just as much own properties as an object's.
1983 Some(_) => self.fn_prop_keys(v),
1984 None => return Vec::new(),
1985 };
1986 keys.iter().filter_map(|k| self.symbol_of_key(k)).collect()
1987 }
1988
1989 /// The own SYMBOL-keyed enumerable `(internal key, value)` pairs of `v` —
1990 /// what `CopyDataProperties` (object spread, `Object.assign`) copies
1991 /// alongside the string keys, and what `Object.keys` / `for-in` /
1992 /// `JSON.stringify` deliberately skip.
1993 pub fn own_symbol_entries(&self, v: &Value) -> Vec<(String, Value)> {
1994 match self.get(v) {
1995 Some(JsObj::Object(p)) => p
1996 .iter()
1997 .filter(|(k, _)| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
1998 .map(|(k, val)| (k.clone(), val.clone()))
1999 .collect(),
2000 // Array/Function: the side table (see `own_symbol_keys`).
2001 Some(_) => self
2002 .fn_prop_keys(v)
2003 .into_iter()
2004 .filter(|k| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2005 .map(|k| {
2006 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
2007 (k, val)
2008 })
2009 .collect(),
2010 None => Vec::new(),
2011 }
2012 }
2013 /// The shared `Symbol.for(key)` value (interned by description).
2014 pub fn symbol_for(&mut self, key: &str) -> Value {
2015 if let Some(v) = self.symbol_registry.get(key) {
2016 return v.clone();
2017 }
2018 let s = self.new_symbol(Some(key.to_string()));
2019 self.symbol_registry.insert(key.to_string(), s.clone());
2020 s
2021 }
2022 /// `Symbol.keyFor(sym)`: the registry key `Symbol.for` interned `sym` under,
2023 /// or `undefined` for a symbol that is not in the registry at all.
2024 ///
2025 /// Matched by symbol IDENTITY, not by description — `Symbol.for('k')` and
2026 /// `Symbol('k')` share a description and only the first is registered. The
2027 /// `@@Symbol.*` well-known entries are registry-internal and never a
2028 /// `keyFor` answer, matching node: `Symbol.keyFor(Symbol.iterator)` is
2029 /// `undefined` there.
2030 pub fn symbol_registry_key(&mut self, sym: &Value) -> Value {
2031 let Some(key) = self
2032 .symbol_registry
2033 .iter()
2034 .find(|(k, v)| self.strict_eq(v, sym) && !k.starts_with("@@Symbol."))
2035 .map(|(k, _)| k.clone())
2036 else {
2037 return Value::Undef;
2038 };
2039 self.new_str(key)
2040 }
2041 /// The well-known `Symbol.iterator` (a fixed shared symbol whose internal
2042 /// property key is `@@iterator`).
2043 pub fn well_known_iterator(&mut self) -> Value {
2044 self.symbol_for("@@Symbol.iterator")
2045 }
2046 /// The well-known `Symbol.asyncIterator` (internal key `@@asyncIterator`).
2047 pub fn well_known_async_iterator(&mut self) -> Value {
2048 self.symbol_for("@@Symbol.asyncIterator")
2049 }
2050 /// A well-known symbol by its ECMAScript name (`toPrimitive`,
2051 /// `toStringTag`, …). Its internal property key is `@@<name>` — see
2052 /// [`WELL_KNOWN_SYMBOLS`] and `property_key`.
2053 ///
2054 /// Its DESCRIPTION is `Symbol.<name>`, so `String(Symbol.iterator)` prints
2055 /// `Symbol(Symbol.iterator)` as V8 does, while the registry key keeps the
2056 /// `@@` prefix — `Symbol.for('Symbol.iterator')` therefore stays a
2057 /// different symbol, and identification is by id, so a user-made
2058 /// `Symbol('Symbol.iterator')` is not mistaken for the well-known one.
2059 pub fn well_known_symbol(&mut self, name: &str) -> Value {
2060 let key = format!("@@Symbol.{name}");
2061 if let Some(v) = self.symbol_registry.get(&key) {
2062 return v.clone();
2063 }
2064 let s = self.new_symbol(Some(format!("Symbol.{name}")));
2065 if let Some(JsObj::Symbol { id, .. }) = self.get(&s) {
2066 self.well_known_ids.insert(*id, name.to_string());
2067 }
2068 self.symbol_registry.insert(key, s.clone());
2069 s
2070 }
2071 /// The internal property-key string for a value used as a key. A `Symbol`
2072 /// maps to a stable per-symbol string so symbol-keyed props round-trip;
2073 /// `Symbol.iterator` maps to the sentinel `@@iterator`.
2074 pub fn property_key(&self, v: &Value) -> String {
2075 if let Some(JsObj::Symbol { id, .. }) = self.get(v) {
2076 if let Some(n) = self.well_known_ids.get(id) {
2077 return format!("@@{n}");
2078 }
2079 return format!("@@sym:{id}");
2080 }
2081 self.str_of(v)
2082 }
2083
2084 pub fn null(&self) -> Value {
2085 self.null_val.clone()
2086 }
2087 pub fn is_null(&self, v: &Value) -> bool {
2088 matches!(self.get(v), Some(JsObj::Null))
2089 }
2090
2091 // ── program loading ──────────────────────────────────────────────────
2092 pub fn program_offsets(&self) -> (usize, usize) {
2093 (self.funcs.len(), self.tries.len())
2094 }
2095 pub fn load_program(&mut self, funcs: Vec<FuncDef>, tries: Vec<TryDef>) {
2096 self.funcs.extend(funcs);
2097 self.tries.extend(tries);
2098 }
2099 pub fn try_def(&self, id: usize) -> Option<TryDef> {
2100 self.tries.get(id).cloned()
2101 }
2102
2103 /// What `try` statement `id` HAS — `(has handler, catch parameter name, has
2104 /// finalizer)` — without copying its chunks. Running a `try` used to clone
2105 /// the whole `TryDef`, so a `try` inside a loop deep-copied its block, its
2106 /// handler and its finalizer on every iteration just to learn its shape.
2107 pub fn try_shape(&self, id: usize) -> Option<(bool, Option<String>, bool)> {
2108 let t = self.tries.get(id)?;
2109 Some((
2110 t.handler.is_some(),
2111 t.handler.as_ref().and_then(|(bind, _)| bind.clone()),
2112 t.finalizer.is_some(),
2113 ))
2114 }
2115
2116 /// One `try` part's bytecode: 0 = block, 1 = handler body, 2 = finalizer.
2117 /// Reached only when no pooled VM already holds that chunk.
2118 pub fn try_chunk(&self, id: usize, part: u64) -> Option<Chunk> {
2119 let t = self.tries.get(id)?;
2120 match part {
2121 0 => Some(t.block.clone()),
2122 1 => t.handler.as_ref().map(|(_, body)| body.clone()),
2123 _ => t.finalizer.clone(),
2124 }
2125 }
2126
2127 // ── heap allocation / accessors ──────────────────────────────────────
2128 pub fn alloc(&mut self, obj: JsObj) -> Value {
2129 self.heap.push(obj);
2130 Value::Obj((self.heap.len() - 1) as u32)
2131 }
2132 pub fn get(&self, v: &Value) -> Option<&JsObj> {
2133 if let Value::Obj(i) = v {
2134 self.heap.get(*i as usize)
2135 } else {
2136 None
2137 }
2138 }
2139 pub fn get_mut(&mut self, v: &Value) -> Option<&mut JsObj> {
2140 if let Value::Obj(i) = v {
2141 self.heap.get_mut(*i as usize)
2142 } else {
2143 None
2144 }
2145 }
2146 /// Which variant `v` points at, without copying its contents. Use this in
2147 /// place of `get(v).cloned()` whenever only the tag is needed — see
2148 /// [`ObjKind`].
2149 pub fn kind_of(&self, v: &Value) -> Option<ObjKind> {
2150 self.get(v).map(JsObj::kind)
2151 }
2152 pub fn new_str(&mut self, s: impl Into<String>) -> Value {
2153 self.alloc(JsObj::Str(s.into()))
2154 }
2155 pub fn new_array(&mut self, items: Vec<Value>) -> Value {
2156 self.alloc(JsObj::Array(items))
2157 }
2158
2159 /// Record that `name` was declared as a private method or accessor.
2160 pub fn note_private_method(&mut self, name: &str) {
2161 self.private_methods.insert(name.to_string());
2162 }
2163
2164 /// Whether `name` was declared as a private method/accessor by some class,
2165 /// as opposed to a private field.
2166 pub fn is_private_method(&self, name: &str) -> bool {
2167 self.private_methods.contains(name)
2168 }
2169
2170 /// The name of the class whose body the running function belongs to. Only a
2171 /// method of that class can even mention its private names, so this is the
2172 /// class a failed brand check must name.
2173 /// The `super` binding of the frame now running: the owning class name,
2174 /// whether the method is static, and the home object of an object-literal
2175 /// method. An ARROW captures all three at creation, the way it captures
2176 /// `this` — `super` inside an arrow means the enclosing METHOD's `super`.
2177 /// Whether the activation now running is strict code.
2178 /// Whether `v` is a function whose own body is SLOPPY — not an arrow, and
2179 /// with no `'use strict'` of its own or inherited from its script. This is
2180 /// the receiver test the `arguments`/`caller` poison pill keys on: node
2181 /// decides by the FUNCTION, never by the code doing the reading.
2182 pub fn fn_is_sloppy(&self, v: &Value) -> bool {
2183 match self.get(v) {
2184 Some(JsObj::Func(fv)) => {
2185 !fv.is_arrow && !self.funcs.get(fv.def_id).is_some_and(|d| d.strict)
2186 }
2187 _ => false,
2188 }
2189 }
2190 pub fn current_strict(&self) -> bool {
2191 self.frame().strict
2192 }
2193
2194 /// Mark the frame about to run as STRICT — used for a program whose own top
2195 /// level says `'use strict'`, which has no `FuncDef` to carry the flag.
2196 pub fn set_current_strict(&mut self) {
2197 if let Some(f) = self.frames.last_mut() {
2198 f.strict = true;
2199 }
2200 }
2201
2202 pub fn current_home(&self) -> (Option<String>, bool, Option<Value>) {
2203 (
2204 self.current_home_class_name(),
2205 self.frame().home_static,
2206 self.frame().home_object.clone(),
2207 )
2208 }
2209
2210 pub fn current_home_class_name(&self) -> Option<String> {
2211 match self.get(&self.current_home_class()?) {
2212 Some(JsObj::Class(c)) => Some(c.name.clone()),
2213 _ => None,
2214 }
2215 }
2216
2217 /// Whether `recv` — or anything on its prototype chain — carries the private
2218 /// name `key`. A private FIELD is an own property of the instance; a private
2219 /// METHOD lives on the class prototype, one link up.
2220 pub fn has_private(&self, recv: &Value, key: &str) -> bool {
2221 let mut cur = Some(recv.clone());
2222 while let Some(v) = cur {
2223 let owns = match self.get(&v) {
2224 Some(JsObj::Object(p)) => p.contains_key(key),
2225 Some(JsObj::Class(c)) => c.statics.contains_key(key),
2226 _ => false,
2227 };
2228 if owns || self.own_accessor(&v, key).is_some() || self.fn_prop(&v, key).is_some() {
2229 return true;
2230 }
2231 cur = self.proto_of(&v);
2232 }
2233 false
2234 }
2235
2236 // ── array holes ──────────────────────────────────────────────────────
2237 //
2238 // Every read/write of an array's elision set goes through this block. See
2239 // the `array_holes` field for why the marker lives here rather than in
2240 // `Value`.
2241
2242 /// Whether element `i` of array `arr` is an elided element (a "hole"), as
2243 /// opposed to a stored `undefined`. `false` for anything that is not an
2244 /// array, and for every index of a dense one.
2245 pub fn is_hole(&self, arr: &Value, i: usize) -> bool {
2246 match (arr, ()) {
2247 (Value::Obj(idx), ()) => self.array_holes.get(idx).is_some_and(|hs| hs.contains(&i)),
2248 _ => false,
2249 }
2250 }
2251
2252 /// Whether `arr` has any elided element at all — one hash probe, and the
2253 /// guard every hole-aware code path takes before doing anything slower.
2254 pub fn has_holes(&self, arr: &Value) -> bool {
2255 matches!(arr, Value::Obj(i) if self.array_holes.contains_key(i))
2256 }
2257
2258 /// `arr`'s hole positions in ASCENDING order, or an empty vec if dense.
2259 /// Sorted because every consumer (own-key enumeration, `util.inspect`
2260 /// run-grouping) needs index order, and the backing set has none.
2261 pub fn hole_indices(&self, arr: &Value) -> Vec<usize> {
2262 let Value::Obj(i) = arr else {
2263 return Vec::new();
2264 };
2265 let Some(hs) = self.array_holes.get(i) else {
2266 return Vec::new();
2267 };
2268 let mut v: Vec<usize> = hs.iter().copied().collect();
2269 v.sort_unstable();
2270 v
2271 }
2272
2273 /// Record element `i` of `arr` as elided.
2274 pub fn mark_hole(&mut self, arr: &Value, i: usize) {
2275 if let Value::Obj(idx) = arr {
2276 self.array_holes.entry(*idx).or_default().insert(i);
2277 }
2278 }
2279
2280 /// Record `range` of `arr` as elided (a `new Array(n)`, a `length` grow, or
2281 /// the gap a write past the end opens).
2282 pub fn mark_hole_range(&mut self, arr: &Value, range: std::ops::Range<usize>) {
2283 if range.is_empty() {
2284 return;
2285 }
2286 if let Value::Obj(idx) = arr {
2287 self.array_holes.entry(*idx).or_default().extend(range);
2288 }
2289 }
2290
2291 /// Element `i` now holds a real value: it is no longer a hole. Every write
2292 /// to an array index calls this, which is what keeps a stale hole record
2293 /// from outliving the elision it described.
2294 pub fn clear_hole(&mut self, arr: &Value, i: usize) {
2295 let Value::Obj(idx) = arr else { return };
2296 let Some(hs) = self.array_holes.get_mut(idx) else {
2297 return;
2298 };
2299 hs.remove(&i);
2300 if hs.is_empty() {
2301 self.array_holes.remove(idx);
2302 }
2303 }
2304
2305 /// `arr` is dense from here on (`fill` over the whole array, a fresh
2306 /// dense assignment into an existing handle).
2307 pub fn clear_holes(&mut self, arr: &Value) {
2308 if let Value::Obj(idx) = arr {
2309 self.array_holes.remove(idx);
2310 }
2311 }
2312
2313 /// Copy `src`'s elision set onto `dst`, optionally shifting each position by
2314 /// `f`. Used by every method that derives a new array whose holes track the
2315 /// source's (`slice`, `concat`, `map`).
2316 pub fn copy_holes(&mut self, src: &Value, dst: &Value, f: impl Fn(usize) -> Option<usize>) {
2317 if !self.has_holes(src) {
2318 return;
2319 }
2320 let moved: rustc_hash::FxHashSet<usize> =
2321 self.hole_indices(src).into_iter().filter_map(f).collect();
2322 self.install_holes(dst, moved);
2323 }
2324
2325 /// Rewrite `arr`'s own elision set in place: `f(i)` gives the position each
2326 /// existing hole moves to, or `None` if the mutation removed it. This is the
2327 /// one primitive behind every structural array mutation — `shift` is
2328 /// `i.checked_sub(1)`, `unshift(k)` is `i + k`, `reverse` is `len-1-i`, and
2329 /// `splice` is the general case.
2330 pub fn remap_holes(&mut self, arr: &Value, f: impl Fn(usize) -> Option<usize>) {
2331 if !self.has_holes(arr) {
2332 return;
2333 }
2334 let moved: rustc_hash::FxHashSet<usize> =
2335 self.hole_indices(arr).into_iter().filter_map(f).collect();
2336 self.install_holes(arr, moved);
2337 }
2338
2339 /// Replace `arr`'s elision set outright, dropping the record entirely when
2340 /// the new set is empty so `has_holes` stays a single negative probe for the
2341 /// dense case.
2342 pub fn install_holes(&mut self, arr: &Value, holes: rustc_hash::FxHashSet<usize>) {
2343 let Value::Obj(idx) = arr else { return };
2344 if holes.is_empty() {
2345 self.array_holes.remove(idx);
2346 } else {
2347 self.array_holes.insert(*idx, holes);
2348 }
2349 }
2350
2351 /// Forget any hole at or past `len` — what a `pop`, a `length` shrink or a
2352 /// truncating `splice` leaves behind.
2353 pub fn truncate_holes(&mut self, arr: &Value, len: usize) {
2354 self.remap_holes(arr, |i| (i < len).then_some(i));
2355 }
2356
2357 /// `util.inspect`'s `formatSpecialArray`: the element strings of a SPARSE
2358 /// array, where each maximal run of elided positions collapses to a single
2359 /// `<N empty items>` entry. Returns the entries and whether the last of them
2360 /// is the `... N more items` tail (which the grid layout must not size a
2361 /// column to).
2362 ///
2363 /// The `maxArrayLength` cap counts ENTRIES, not indices, so a run costs one
2364 /// slot however long it is — matching node, where `[ ...Array(200) ]`-style
2365 /// sparse arrays print a single `<200 empty items>`.
2366 fn inspect_sparse(
2367 &self,
2368 v: &Value,
2369 items: &[Value],
2370 indent: usize,
2371 st: &mut InspectCycles,
2372 ) -> (Vec<String>, bool) {
2373 let holes: rustc_hash::FxHashSet<usize> = self.hole_indices(v).into_iter().collect();
2374 let empties = |n: usize| {
2375 let unit = if n == 1 { "item" } else { "items" };
2376 format!("<{n} empty {unit}>")
2377 };
2378 let mut out: Vec<String> = Vec::new();
2379 // The first index not yet accounted for by an entry.
2380 let mut index = 0usize;
2381 for (i, it) in items.iter().enumerate() {
2382 if out.len() >= inspect_max_array_length() {
2383 break;
2384 }
2385 if holes.contains(&i) {
2386 continue;
2387 }
2388 if i > index {
2389 out.push(empties(i - index));
2390 index = i;
2391 if out.len() >= inspect_max_array_length() {
2392 break;
2393 }
2394 }
2395 out.push(self.inspect_lvl(it, indent + 2, st));
2396 index = i + 1;
2397 }
2398 let remaining = items.len() - index;
2399 if remaining == 0 {
2400 return (out, false);
2401 }
2402 if out.len() < inspect_max_array_length() {
2403 // Trailing holes are still `<N empty items>`, not a truncation.
2404 out.push(empties(remaining));
2405 (out, false)
2406 } else {
2407 let unit = if remaining == 1 { "item" } else { "items" };
2408 out.push(format!("... {remaining} more {unit}"));
2409 (out, true)
2410 }
2411 }
2412 pub fn new_object(&mut self, mut props: IndexMap<String, Value>) -> Value {
2413 // Integer-index keys enumerate ascending-first regardless of the order
2414 // they were supplied in (object literal, spread, Object.assign result).
2415 canonicalize_own_keys(&mut props);
2416 // A map carrying the hidden `@@native` tag IS an instance of that native
2417 // class, so it hangs off the class prototype rather than
2418 // `Object.prototype`. Eleven classes — `Hash`, `Cipheriv`,
2419 // `StringDecoder`, `Script`, `URLSearchParams`, `Console`,
2420 // `AbortController` among them — built plain objects instead, so
2421 // `x.constructor.name` read `"Object"` and a chain walk found none of
2422 // the class's methods. Linking HERE means a construction site cannot
2423 // forget it; the tag is already in the map at every one of them.
2424 let tag = props.get("@@native").and_then(|v| self.as_str(v));
2425 let obj = self.alloc(JsObj::Object(props));
2426 if let Some(proto) = tag.and_then(|t| self.ensure_ctor_proto(&t)) {
2427 self.set_proto(&obj, proto);
2428 }
2429 obj
2430 }
2431 pub fn as_str(&self, v: &Value) -> Option<String> {
2432 match v {
2433 Value::Str(s) => Some((**s).clone()),
2434 Value::Obj(_) => match self.get(v) {
2435 Some(JsObj::Str(s)) => Some(s.clone()),
2436 _ => None,
2437 },
2438 _ => None,
2439 }
2440 }
2441
2442 // ── scope / names ────────────────────────────────────────────────────
2443 fn frame(&self) -> &Frame {
2444 self.frames.last().unwrap()
2445 }
2446 fn cur_env(&self) -> Env {
2447 self.frame().env.clone()
2448 }
2449
2450 // ── DAP debug introspection (used only under `--dap`) ────────────────────
2451 /// Number of active call frames (the debugger's step-depth reference).
2452 pub fn frame_depth(&self) -> usize {
2453 self.frames.len()
2454 }
2455 /// Record the source line the innermost frame is executing (DAP line hook).
2456 pub fn set_cur_line(&mut self, line: u32) {
2457 if let Some(f) = self.frames.last_mut() {
2458 f.line = line;
2459 }
2460 }
2461 /// The `.stack` tail for an error created right now: one ` at <name>`
2462 /// line per live frame, innermost first, ending at the module frame.
2463 ///
2464 /// These are the REAL user frames — node-js has no `file:line:column` (the
2465 /// per-frame line is only tracked under `--dap`) and no Node-internal
2466 /// module-loader frames, so `.stack` names the call chain but can never be
2467 /// byte-identical to V8's. The names are what makes a thrown error
2468 /// diagnosable; the missing positions are documented in BUGS.md.
2469 /// V8's `Error.stackTraceLimit` — how many frames a captured stack keeps.
2470 ///
2471 /// The default is 10, it is settable, and setting it to 0 is the documented
2472 /// way to make error construction cheap. It did not exist, so the read was
2473 /// `undefined` and every stack carried every frame regardless.
2474 pub fn stack_trace_limit(&self) -> usize {
2475 match self.builtin_static("Error", "stackTraceLimit") {
2476 Some(v) => {
2477 let n = self.to_number(&v);
2478 if n.is_finite() && n > 0.0 {
2479 n as usize
2480 } else if n.is_nan() || n <= 0.0 {
2481 0
2482 } else {
2483 usize::MAX
2484 }
2485 }
2486 None => 10,
2487 }
2488 }
2489
2490 pub fn stack_frames(&self) -> String {
2491 let limit = self.stack_trace_limit();
2492 if limit == 0 {
2493 return String::new();
2494 }
2495 let mut out = String::new();
2496 for (i, f) in self.frames.iter().enumerate().rev().take(limit) {
2497 let name = match (&f.owner, i) {
2498 (Some(n), _) => n.clone(),
2499 (None, 0) => "Object.<anonymous>".to_string(),
2500 (None, _) => "<anonymous>".to_string(),
2501 };
2502 out.push_str("\n at ");
2503 out.push_str(&name);
2504 }
2505 if out.is_empty() && limit > 0 {
2506 out.push_str("\n at <anonymous>");
2507 }
2508 out
2509 }
2510
2511 /// The call stack as (frame name, line) pairs, innermost first — for the DAP
2512 /// `stackTrace`. `owner` carries the function name where known.
2513 pub fn dbg_stack(&self) -> Vec<(String, u32)> {
2514 self.frames
2515 .iter()
2516 .rev()
2517 .map(|f| {
2518 let name = f.owner.clone().unwrap_or_else(|| "<module>".to_string());
2519 (name, f.line)
2520 })
2521 .collect()
2522 }
2523 /// The innermost frame's locals as (name, inspect) pairs — for DAP `variables`.
2524 pub fn dbg_locals(&self) -> Vec<(String, String)> {
2525 let env = self.cur_env();
2526 let names: Vec<String> = env.borrow().vars.keys().cloned().collect();
2527 names
2528 .into_iter()
2529 .map(|n| {
2530 let v = self.read_name(&n).unwrap_or(Value::Undef);
2531 (n, self.inspect(&v))
2532 })
2533 .collect()
2534 }
2535
2536 /// Scope-chain read: local + enclosing chain, then globals.
2537 /// Whether `name` is a module-top-level binding that has not reached its
2538 /// declaration yet. Separate from [`JsHost::is_tdz`], which answers for a
2539 /// block-scoped one by inspecting the value it holds.
2540 pub fn is_tdz_global(&self, name: &str) -> bool {
2541 self.tdz_globals.contains(name)
2542 }
2543
2544 pub fn read_name(&self, name: &str) -> Option<Value> {
2545 let mut env = Some(self.cur_env());
2546 while let Some(e) = env {
2547 if let Some(v) = e.borrow().vars.get(name) {
2548 return Some(v.clone());
2549 }
2550 env = e.borrow().parent.clone();
2551 }
2552 self.globals.get(name).cloned()
2553 }
2554 pub fn read_global(&self, name: &str) -> Option<Value> {
2555 self.globals.get(name).cloned()
2556 }
2557
2558 /// Whether `name` is bound anywhere on the scope chain or in the globals —
2559 /// `read_name(..).is_some()` without cloning the value it finds. The
2560 /// strict-mode assignment path asks this and nothing else.
2561 pub fn has_name(&self, name: &str) -> bool {
2562 let mut env = Some(self.cur_env());
2563 while let Some(e) = env {
2564 if e.borrow().vars.contains_key(name) {
2565 return true;
2566 }
2567 env = e.borrow().parent.clone();
2568 }
2569 self.globals.contains_key(name)
2570 }
2571
2572 /// Assign to an existing binding up the scope chain, else create a global
2573 /// (JS assignment to an undeclared name targets the global object).
2574 /// Assign to an existing binding, or create a global. Returns `false` when
2575 /// the nearest binding is an immutable (`const`) one, which the caller turns
2576 /// into `TypeError: Assignment to constant variable.` — assigning to a
2577 /// `const` used to succeed SILENTLY, so code that node rejects ran on with
2578 /// a mutated constant.
2579 #[must_use]
2580 pub fn set_name(&mut self, name: &str, val: Value) -> bool {
2581 let mut env = Some(self.cur_env());
2582 while let Some(e) = env {
2583 // `get_mut`, not `contains_key` + `insert`: overwriting an existing
2584 // binding hashed the name twice and allocated a fresh `String` key
2585 // for a key that was already there — once per assignment, so once
2586 // per loop iteration in any counting loop.
2587 //
2588 // The const check runs only at the env that OWNS the name, and the
2589 // `is_empty` guard settles the common (no consts here) case without
2590 // hashing the name again.
2591 let mut b = e.borrow_mut();
2592 if b.vars.contains_key(name) {
2593 if !b.consts.is_empty() && b.consts.contains(name) {
2594 return false;
2595 }
2596 if let Some(slot) = b.vars.get_mut(name) {
2597 *slot = val;
2598 }
2599 return true;
2600 }
2601 drop(b);
2602 env = e.borrow().parent.clone();
2603 }
2604 if self.global_consts.contains(name) {
2605 return false;
2606 }
2607 match self.globals.get_mut(name) {
2608 Some(slot) => *slot = val,
2609 None => {
2610 self.globals.insert(name.to_string(), val);
2611 }
2612 }
2613 true
2614 }
2615
2616 /// Declare a `const` binding: the same placement as [`Self::declare_name`],
2617 /// plus recording the name as immutable in whichever scope received it.
2618 pub fn declare_const_name(&mut self, name: &str, val: Value) {
2619 let f = self.frame();
2620 let to_globals = f.is_module && Rc::ptr_eq(&f.env, &f.base_env);
2621 self.declare_name(name, val);
2622 if to_globals {
2623 self.global_consts.insert(name.to_string());
2624 } else {
2625 self.cur_env().borrow_mut().consts.insert(name.to_string());
2626 }
2627 }
2628
2629 /// The value a lexical binding holds between entering its scope and reaching
2630 /// its declaration — its TEMPORAL DEAD ZONE. One heap object for the whole
2631 /// process, so the check is a heap-index comparison and the marker cannot be
2632 /// produced by any JavaScript expression. It never escapes: every path that
2633 /// could read it throws first.
2634 pub fn tdz_marker(&mut self) -> Value {
2635 if let Some(v) = &self.tdz {
2636 return v.clone();
2637 }
2638 let v = self.alloc(JsObj::Builtin("@@tdz".into()));
2639 self.tdz = Some(v.clone());
2640 v
2641 }
2642
2643 /// Whether `v` is the uninitialized-binding marker.
2644 pub fn is_tdz(&self, v: &Value) -> bool {
2645 matches!((&self.tdz, v), (Some(Value::Obj(a)), Value::Obj(b)) if a == b)
2646 }
2647
2648 /// Declare `name` in the CURRENT scope as uninitialized, unless that scope
2649 /// already binds it. Emitted at the top of every scope for each `let`,
2650 /// `const` and `class` declared directly in it, so a read before the
2651 /// declaration throws instead of finding an OUTER binding of the same name —
2652 /// `let x = 1; { x; let x = 2 }` used to read the outer `1`.
2653 pub fn hoist_tdz(&mut self, name: &str) {
2654 let marker = self.tdz_marker();
2655 let f = self.frame();
2656 // At module top level a lexical binding lives in `globals`, which is ALSO
2657 // what backs `globalThis.<name>` — so parking the marker there exposes it
2658 // to JavaScript, and `const crypto = …` made `globalThis.crypto` read
2659 // back as the marker. Top-level dead zones are tracked in a separate set
2660 // that only the name-read path consults.
2661 if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2662 if !self.globals.contains_key(name) {
2663 self.tdz_globals.insert(name.to_string());
2664 }
2665 return;
2666 }
2667 let env = self.cur_env();
2668 let mut e = env.borrow_mut();
2669 if !e.vars.contains_key(name) {
2670 e.vars.insert(name.to_string(), marker);
2671 }
2672 }
2673
2674 /// Declare a new binding in the current scope (`let`/`const`). At the top of
2675 /// the module frame there is no local env, so those names become globals; once
2676 /// a block scope is open the binding belongs to that block.
2677 pub fn declare_name(&mut self, name: &str, val: Value) {
2678 let f = self.frame();
2679 if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2680 self.tdz_globals.remove(name);
2681 self.globals.insert(name.to_string(), val);
2682 } else {
2683 self.cur_env()
2684 .borrow_mut()
2685 .vars
2686 .insert(name.to_string(), val);
2687 }
2688 }
2689
2690 /// Declare a `var` (or a hoisted function declaration): FUNCTION-scoped, so it
2691 /// skips every open block scope and lands in the activation's base env.
2692 /// Create a hoisted `var` binding, initialised to `undefined`, only when the
2693 /// name is not already bound in this activation.
2694 ///
2695 /// `var` bindings come into existence when the scope is entered, not where
2696 /// the declaration is written — `f(){ x; var x = 1 }` reads `undefined`
2697 /// rather than throwing. "If absent" is what keeps a parameter intact: in
2698 /// `function f(a) { var a; }` the `var` names a binding that already exists
2699 /// and must not be reset, which is also why a bare `var x;` emits nothing at
2700 /// its own position.
2701 pub fn hoist_var_name(&mut self, name: &str) {
2702 // The ENTRY script's top level is a CommonJS module body, not global
2703 // scope: node wraps every file in a function, so a top-level `var` is a
2704 // local of that wrapper. Binding it into the globals map made
2705 // `var x = 3` at the top of the entry readable as `globalThis.x`, where
2706 // node says `undefined` — a REQUIRED module already ran inside a real
2707 // frame and behaved correctly, so only the entry file differed.
2708 if self.frame().is_module && !self.module_scope {
2709 self.globals.entry(name.to_string()).or_insert(Value::Undef);
2710 return;
2711 }
2712 let base = self.frame().base_env.clone();
2713 let mut env = base.borrow_mut();
2714 if !env.vars.contains_key(name) {
2715 env.vars.insert(name.to_string(), Value::Undef);
2716 }
2717 }
2718
2719 pub fn declare_var_name(&mut self, name: &str, val: Value) {
2720 if self.frame().is_module && !self.module_scope {
2721 self.globals.insert(name.to_string(), val);
2722 return;
2723 }
2724 let base = self.frame().base_env.clone();
2725 base.borrow_mut().vars.insert(name.to_string(), val);
2726 }
2727
2728 /// Enter a fresh block scope.
2729 pub fn push_scope(&mut self) {
2730 let env = self.cur_env();
2731 self.frames.last_mut().unwrap().env = child_env(env);
2732 }
2733
2734 /// Open a scope that is also the activation's VARIABLE environment, and
2735 /// return the previous one so the caller can restore it.
2736 ///
2737 /// A block scope is not enough for a strict direct `eval`: `var` and a
2738 /// hoisted function declaration bind to `base_env`, so they walked straight
2739 /// past a plain `push_scope` and still landed in the caller's function
2740 /// scope. Only `let`/`const` were contained.
2741 pub fn push_var_scope(&mut self) -> Env {
2742 let env = child_env(self.cur_env());
2743 let f = self.frames.last_mut().unwrap();
2744 let prev = std::mem::replace(&mut f.base_env, env.clone());
2745 f.env = env;
2746 prev
2747 }
2748
2749 /// Restore the variable environment a `push_var_scope` replaced.
2750 pub fn pop_var_scope(&mut self, prev: Env) {
2751 let f = self.frames.last_mut().unwrap();
2752 f.env = prev.clone();
2753 f.base_env = prev;
2754 }
2755
2756 /// Leave the innermost block scope (never pops past the activation's base).
2757 pub fn pop_scope(&mut self) {
2758 let cur = self.cur_env();
2759 if Rc::ptr_eq(&cur, &self.frame().base_env) {
2760 return;
2761 }
2762 let parent = cur.borrow().parent.clone();
2763 if let Some(p) = parent {
2764 self.frames.last_mut().unwrap().env = p;
2765 }
2766 }
2767
2768 /// Replace the innermost block scope with a fresh copy of its bindings — the
2769 /// per-iteration environment a `for (let i …)` loop creates, so a closure made
2770 /// in one iteration keeps that iteration's value.
2771 pub fn copy_scope(&mut self) {
2772 let cur = self.cur_env();
2773 if Rc::ptr_eq(&cur, &self.frame().base_env) {
2774 return;
2775 }
2776 let parent = cur.borrow().parent.clone();
2777 let fresh = new_env(parent);
2778 fresh.borrow_mut().vars = cur.borrow().vars.clone();
2779 self.frames.last_mut().unwrap().env = fresh;
2780 }
2781
2782 /// The current block-scope env, for save/restore across a nested chunk.
2783 pub fn scope_snapshot(&self) -> Env {
2784 self.cur_env()
2785 }
2786 pub fn restore_scope(&mut self, env: Env) {
2787 self.frames.last_mut().unwrap().env = env;
2788 }
2789 pub fn set_global(&mut self, name: &str, val: Value) {
2790 self.globals.insert(name.to_string(), val);
2791 }
2792
2793 // ── output capture ───────────────────────────────────────────────────
2794 //
2795 // Every write a *program* makes — `console.log`, `process.stdout.write`,
2796 // `print` — funnels through `write_out`, so turning capture on redirects all
2797 // of them at once. Diagnostics the runtime itself emits (the REPL banner, a
2798 // crash traceback from `main`) deliberately do not: they belong to the
2799 // process, not to the program.
2800
2801 /// Start capturing program output in-process. Any text already captured is
2802 /// discarded, so each run starts clean.
2803 pub fn begin_capture(&mut self) {
2804 self.capture = Some(Vec::new());
2805 }
2806
2807 /// Stop capturing and take everything written since [`begin_capture`],
2808 /// returning the empty string when capture was not on. The captured bytes
2809 /// are rendered lossily: this API hands back a `String`, so a program that
2810 /// wrote non-UTF-8 gets `U+FFFD` here even though the same write reaches a
2811 /// real stdout byte-exact. Use [`end_capture_bytes`] to keep those bytes.
2812 ///
2813 /// [`begin_capture`]: JsHost::begin_capture
2814 /// [`end_capture_bytes`]: JsHost::end_capture_bytes
2815 pub fn end_capture(&mut self) -> String {
2816 String::from_utf8_lossy(&self.capture.take().unwrap_or_default()).into_owned()
2817 }
2818
2819 /// Stop capturing and take the raw bytes, without the lossy transcription
2820 /// [`end_capture`] applies.
2821 ///
2822 /// [`end_capture`]: JsHost::end_capture
2823 pub fn end_capture_bytes(&mut self) -> Vec<u8> {
2824 self.capture.take().unwrap_or_default()
2825 }
2826
2827 /// Whether output is being captured — the one thing a caller needs to know
2828 /// before asking the real stream a question (`isTTY`, cursor position).
2829 pub fn capturing(&self) -> bool {
2830 self.capture.is_some()
2831 }
2832
2833 /// Write program output: into the capture buffer when capturing, else to the
2834 /// process stream `stderr` selects. `s` is written verbatim — callers add
2835 /// their own line ending, as `console.log` does and `process.stdout.write`
2836 /// does not.
2837 pub fn write_out(&mut self, s: &str, stderr: bool) {
2838 self.write_out_bytes(s.as_bytes(), stderr);
2839 }
2840
2841 /// Write program output as raw BYTES. `process.stdout.write(buf)` hands Node
2842 /// a byte string and Node writes it through untouched, so a `Buffer` holding
2843 /// `ff fe 41` reaches stdout as those three bytes. Routing it through a Rust
2844 /// `String` first replaced every non-UTF-8 byte with `U+FFFD` — three bytes
2845 /// became seven — so the byte path exists separately from [`write_out`].
2846 ///
2847 /// [`write_out`]: JsHost::write_out
2848 pub fn write_out_bytes(&mut self, bytes: &[u8], stderr: bool) {
2849 if let Some(buf) = &mut self.capture {
2850 buf.extend_from_slice(bytes);
2851 return;
2852 }
2853 use std::io::Write as _;
2854 if stderr {
2855 let mut e = std::io::stderr();
2856 let _ = e.write_all(bytes);
2857 let _ = e.flush();
2858 } else {
2859 let mut o = std::io::stdout();
2860 let _ = o.write_all(bytes);
2861 let _ = o.flush();
2862 }
2863 }
2864 pub fn del_name(&mut self, name: &str) {
2865 if self
2866 .cur_env()
2867 .borrow_mut()
2868 .vars
2869 .shift_remove(name)
2870 .is_some()
2871 {
2872 return;
2873 }
2874 self.globals.shift_remove(name);
2875 }
2876
2877 pub fn current_this(&self) -> Option<Value> {
2878 self.frame().this_obj.clone()
2879 }
2880
2881 /// The running activation's [`ThisState`].
2882 pub fn this_state(&self) -> ThisState {
2883 self.frame().this_state
2884 }
2885
2886 /// Mark the next user-function activation as a derived constructor.
2887 pub fn mark_next_call_derived_ctor(&mut self) {
2888 self.derived_ctor_next = true;
2889 }
2890
2891 /// BindThisValue (9.1.1.3.1) for a `super()` that has just returned: the
2892 /// nearest derived-constructor activation becomes `Bound`. That is the top
2893 /// frame, or — for `super()` inside an arrow — the constructor below the
2894 /// arrow's own frame. `false` when it was already bound: the second call.
2895 pub fn bind_super_this(&mut self) -> bool {
2896 let Some(f) = self
2897 .frames
2898 .iter_mut()
2899 .rev()
2900 .find(|f| f.this_state != ThisState::Plain)
2901 else {
2902 return true;
2903 };
2904 if f.this_state == ThisState::Bound {
2905 return false;
2906 }
2907 f.this_state = ThisState::Bound;
2908 true
2909 }
2910
2911 /// The object a `super()` call substituted for the instance, if any.
2912 ///
2913 /// `construct_class` allocates the instance up front, so when a base
2914 /// constructor RETURNS an object the substitution happens deep inside the
2915 /// VM, after that allocation. This carries it back out. Each
2916 /// `construct_class` saves and restores the previous value around its own
2917 /// run, so a `new` inside a constructor body cannot steal it.
2918 pub fn take_super_replacement(&mut self) -> Option<Value> {
2919 self.super_replacement.take()
2920 }
2921
2922 pub fn swap_super_replacement(&mut self, v: Option<Value>) -> Option<Value> {
2923 std::mem::replace(&mut self.super_replacement, v)
2924 }
2925
2926 /// Rebind the running activation's `this`.
2927 ///
2928 /// Only `super()` does this: when the parent constructor RETURNS an object,
2929 /// 15.7.15 makes that object the derived instance, so the rest of the
2930 /// derived constructor has to write to it rather than to the one allocated
2931 /// before the call.
2932 pub fn set_current_this(&mut self, v: Value) {
2933 if let Some(f) = self.frames.last_mut() {
2934 f.this_obj = Some(v.clone());
2935 }
2936 self.super_replacement = Some(v);
2937 }
2938 /// The callbacks to run for `event`, consuming any `once` registration in
2939 /// the same step — so a listener that re-emits the event cannot re-enter a
2940 /// one-shot handler.
2941 pub fn take_process_listeners(&mut self, event: &str) -> Vec<Value> {
2942 let Some(list) = self.process_listeners.get_mut(event) else {
2943 return Vec::new();
2944 };
2945 let fired: Vec<Value> = list.iter().map(|l| l.f.clone()).collect();
2946 list.retain(|l| !l.once);
2947 fired
2948 }
2949
2950 /// Bind the TOP-LEVEL `this` — the value a `this` outside any function sees.
2951 ///
2952 /// Node answers differently per entry point and both answers are objects:
2953 /// `node f.js` runs a CommonJS module, so top-level `this` is
2954 /// `module.exports`; `node -e` and `node -` run a Script, so it is
2955 /// `globalThis`. Verified on node v26.7.0 —
2956 /// `console.log(this === globalThis, this === module.exports)` is
2957 /// `false true` from a file and `true false` from `-e` and from stdin. It
2958 /// was `undefined` at every entry point here, so `this.x = 1` at module
2959 /// scope threw instead of populating the exports object.
2960 ///
2961 /// Only the base frame is touched: a plain function call still gets its own
2962 /// (`undefined`) binding rather than inheriting this one.
2963 pub fn set_top_this(&mut self, v: Value) {
2964 if let Some(f) = self.frames.first_mut() {
2965 f.this_obj = Some(v);
2966 }
2967 }
2968 pub fn current_env_capture(&self) -> Env {
2969 self.frame().env.clone()
2970 }
2971 pub fn current_new_target(&self) -> Option<Value> {
2972 self.frame().new_target.clone()
2973 }
2974 fn current_home_class(&self) -> Option<Value> {
2975 self.frame().home_class.clone()
2976 }
2977
2978 /// The `(parent_ctor, this_class_fields)` for a running constructor's
2979 /// `super(...)`, derived from the frame's home class.
2980 pub fn super_context(&self) -> (Option<Value>, Vec<(String, Value, bool)>) {
2981 match self.current_home_class() {
2982 Some(cv) => match self.get(&cv) {
2983 Some(JsObj::Class(c)) => (c.parent.clone(), c.fields.clone()),
2984 _ => (None, Vec::new()),
2985 },
2986 None => (None, Vec::new()),
2987 }
2988 }
2989
2990 /// Resolve `super.name` to either the parent-prototype getter (to be invoked
2991 /// by the caller, outside any host borrow) or a directly-usable value.
2992 pub fn super_resolve(&self, name: &str) -> SuperRef {
2993 // A shorthand method in an OBJECT LITERAL resolves `super` through its
2994 // home object's prototype; only a class method has a home CLASS. With
2995 // nothing tracked for the literal case, `{ m() { super.x() } }` had no
2996 // parent to look in and reported the method missing.
2997 if let Some(home) = self.frame().home_object.clone() {
2998 let target = self.proto_of(&home).unwrap_or(Value::Undef);
2999 if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3000 return SuperRef::Getter(getter);
3001 }
3002 return SuperRef::Data(lookup_chain(self, &target, name).unwrap_or(Value::Undef));
3003 }
3004 let parent = match self
3005 .current_home_class()
3006 .and_then(|cv| match self.get(&cv) {
3007 Some(JsObj::Class(c)) => c.parent.clone(),
3008 _ => None,
3009 }) {
3010 Some(p) => p,
3011 None => return SuperRef::Data(Value::Undef),
3012 };
3013 // A STATIC method's home object is the constructor, so `super.x` reads
3014 // off the parent CONSTRUCTOR; an instance method's is the prototype
3015 // object, so it reads off the parent's prototype. Always taking the
3016 // prototype meant `static s() { return super.s(); }` found nothing and
3017 // then tried to call it.
3018 let target = if self.frame().home_static {
3019 parent.clone()
3020 } else {
3021 match self.get(&parent) {
3022 Some(JsObj::Class(pc)) => pc.proto.clone(),
3023 _ => self.fn_prop(&parent, "prototype").unwrap_or(Value::Undef),
3024 }
3025 };
3026 if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3027 return SuperRef::Getter(getter);
3028 }
3029 if let Some(v) = lookup_chain(self, &target, name) {
3030 return SuperRef::Data(v);
3031 }
3032 // A static method lives in the fn-prop side table, not the property map.
3033 SuperRef::Data(self.fn_prop(&target, name).unwrap_or(Value::Undef))
3034 }
3035
3036 // ── signals / errors ─────────────────────────────────────────────────
3037 pub fn take_error(&mut self) -> Option<String> {
3038 self.error.take()
3039 }
3040 pub fn raise_str(&mut self, class: &str, msg: &str) -> String {
3041 let s = if msg.is_empty() {
3042 class.to_string()
3043 } else {
3044 format!("{class}: {msg}")
3045 };
3046 self.error = Some(s.clone());
3047 s
3048 }
3049}
3050
3051// ── error constructors ───────────────────────────────────────────────────────
3052
3053pub fn type_error(msg: &str) -> String {
3054 format!("TypeError: {msg}")
3055}
3056pub fn ref_error(name: &str) -> String {
3057 format!("ReferenceError: {name} is not defined")
3058}
3059
3060/// The error a read of a lexical binding still in its TEMPORAL DEAD ZONE
3061/// raises. Distinct from [`ref_error`] on purpose: node says which of the two
3062/// happened, and the difference is how a reader tells a misspelled name from a
3063/// `let` used above its declaration.
3064pub fn tdz_error(name: &str) -> String {
3065 format!("ReferenceError: Cannot access '{name}' before initialization")
3066}
3067pub fn range_error(msg: &str) -> String {
3068 format!("RangeError: {msg}")
3069}
3070
3071/// V8's `String::kMaxLength` on a 64-bit build, in UTF-16 code units — the
3072/// largest string the engine will materialize.
3073///
3074/// Measured on node v26.7.0 (darwin arm64):
3075/// `require('buffer').constants.MAX_STRING_LENGTH` is `536870888`,
3076/// `'a'.repeat(536870888)` succeeds with that length, and
3077/// `'a'.repeat(536870889)` is `RangeError: Invalid string length`.
3078pub const MAX_STRING_LENGTH: usize = 536_870_888;
3079
3080/// The error V8 raises for a string operation whose RESULT would exceed
3081/// [`MAX_STRING_LENGTH`]. It is raised from the length arithmetic, before any
3082/// allocation: `'a'.repeat(2**40)` throws promptly on node where node-js used to
3083/// sit building a 1 TiB `String` until it was killed.
3084pub fn invalid_string_length() -> String {
3085 range_error("Invalid string length")
3086}
3087
3088/// `ToUint32`-validated array length — ECMA-262 10.4.2.2 `ArrayCreate` step 1
3089/// and 10.4.2.4 `ArraySetLength` step 3.
3090///
3091/// A length is legal only if `ToUint32(v)` equals `ToNumber(v)` exactly, so
3092/// `-1`, `1.5`, `NaN`, `Infinity`, `'x'` and `2**32` are all
3093/// `RangeError: Invalid array length` while `'3'` is `3` and `-0` is `0`
3094/// (measured on node v26.7.0: `new Array(-0).length` is `0`, `a.length = '3'`
3095/// leaves `3`, `a.length = 'x'` throws). node-js validated none of them — it
3096/// built `[-1]` from `new Array(-1)`, silently ignored `a.length = -1`, and sat
3097/// materializing four billion elements for `a.length = 2**32`.
3098pub fn to_array_length(v: &Value) -> Result<usize, String> {
3099 // 10.4.2.4 steps 2-3 run TWO conversions: `ToUint32(value)` and then
3100 // `ToNumber(value)`, compared against each other. Both are observable — a
3101 // counting `valueOf` sees two calls in node and saw one here — and the
3102 // second is what makes `arr.length = 1.5` a RangeError rather than 1.
3103 let u32_pass = to_number_value(v)?;
3104 let n = to_number_value(v)?;
3105 let _ = u32_pass;
3106 // `ToUint32`: truncate toward zero, then modulo 2^32.
3107 let u = if n.is_finite() {
3108 (n.trunc() as i64).rem_euclid(1i64 << 32) as u32
3109 } else {
3110 0
3111 };
3112 // `-0` compares equal to `0` here, which is what makes `new Array(-0)` legal.
3113 if (u as f64) != n {
3114 return Err(range_error("Invalid array length"));
3115 }
3116 Ok(u as usize)
3117}
3118
3119/// A Node *coded* error raised from the JS layer: `Name [ERR_CODE]: message`.
3120///
3121/// `builtins::synth_error` parses that head back apart, so the bracketed code
3122/// becomes the enumerable `err.code` that `err.code === 'ERR_INVALID_URL'`-style
3123/// handling reads. Writing the head by hand at each throw site is what left a
3124/// dozen of them with `err.code === undefined` while the message matched.
3125///
3126/// Use this for errors Node raises from `lib/internal/errors.js`, whose `.name`
3127/// is left bracketed while the stack is captured and therefore shows up in both
3128/// `String(err)` and `err.stack` — measured on v26.7.0:
3129///
3130/// ```text
3131/// process.exit(1.5) -> RangeError [ERR_OUT_OF_RANGE]: The value of "code" …
3132/// ```
3133pub fn coded_error(class: &str, code: &str, msg: &str) -> String {
3134 format!("{class} [{code}]: {msg}")
3135}
3136
3137/// The marker `plain_coded_error` hides a code behind, and `synth_error` strips.
3138pub const CODE_MARK: &str = "\u{1}code:";
3139
3140/// Marks an error string as a `DOMException` carrying a WHATWG error NAME
3141/// rather than one of the ECMAScript error classes. WebCrypto and the abort
3142/// APIs reject with these, and the name (`NotSupportedError`) is not a class
3143/// `synth_error` could otherwise recognise.
3144pub const DOM_MARK: &str = "\u{1}dom:";
3145
3146/// A `DOMException` error string: `name` is the WHATWG error name.
3147pub fn dom_error(name: &str, msg: &str) -> String {
3148 format!("{DOM_MARK}{name}\u{1}{msg}")
3149}
3150
3151/// A Node coded error raised from the *native* layer: `.code` is set, but the
3152/// name is never bracketed, so `String(err)` is the plain `Name: message`.
3153///
3154/// The distinction is observable and is not a stylistic choice — on v26.7.0,
3155/// `String(new URL("/x") error)` is `TypeError: Invalid URL` with
3156/// `.code === 'ERR_INVALID_URL'`, while the JS-layer `process.exit(1.5)` error
3157/// brackets its code into the very same two reads. Encoding both through one
3158/// `Name [CODE]:` head would have to pick one and be wrong about the other.
3159///
3160/// The code rides in a marker at the head of the message rather than in the
3161/// error class, because the class text is exactly what must NOT carry it. The
3162/// marker is an internal wire format between a throw site and `synth_error`; it
3163/// never survives into a `.message`.
3164pub fn plain_coded_error(class: &str, code: &str, msg: &str) -> String {
3165 format!("{class}: {CODE_MARK}{code}\u{1}{msg}")
3166}
3167
3168/// Marks the start of the extra string own properties a
3169/// [`plain_coded_error_with`] error carries after its message.
3170pub const FIELDS_MARK: char = '\u{2}';
3171
3172/// [`plain_coded_error`] plus extra enumerable string own properties, set after
3173/// `code` in the order given — `new URL('x', 'nope')` throws with
3174/// `Object.keys(e)` reading `["code","input","base"]`.
3175///
3176/// Each field is `key\u{3}<byte length>\u{3}value`, so a value (a URL input is
3177/// arbitrary user text) may carry any character, the separators included.
3178pub fn plain_coded_error_with(class: &str, code: &str, msg: &str, fields: &[(&str, &str)]) -> String {
3179 let mut s = plain_coded_error(class, code, msg);
3180 s.push(FIELDS_MARK);
3181 for (k, v) in fields {
3182 s.push_str(&format!("{k}\u{3}{}\u{3}{v}", v.len()));
3183 }
3184 s
3185}
3186
3187/// An error string as a person reads it: `TypeError: Invalid URL`, with the
3188/// internal code and field markers of [`plain_coded_error`] /
3189/// [`plain_coded_error_with`] removed. An uncaught native error is printed
3190/// from its string, and printed the wire format (`\u{1}code:ERR_INVALID_URL…`).
3191pub fn plain_error_text(e: &str) -> String {
3192 let Some(i) = e.find(CODE_MARK) else {
3193 return e.to_string();
3194 };
3195 let (head, rest) = e.split_at(i);
3196 match rest[CODE_MARK.len()..].split_once('\u{1}') {
3197 Some((_, m)) => format!("{head}{}", split_error_fields(m).0),
3198 None => e.to_string(),
3199 }
3200}
3201
3202/// Split a [`plain_coded_error_with`] message back into the message and its
3203/// fields. A message with no field mark comes back whole with no fields.
3204pub fn split_error_fields(msg: &str) -> (&str, Vec<(&str, &str)>) {
3205 let Some((head, mut rest)) = msg.split_once(FIELDS_MARK) else {
3206 return (msg, Vec::new());
3207 };
3208 let mut fields = Vec::new();
3209 while let Some((k, tail)) = rest.split_once('\u{3}') {
3210 let Some((len, tail)) = tail.split_once('\u{3}') else { break };
3211 let Ok(len) = len.parse::<usize>() else { break };
3212 let Some(v) = tail.get(..len) else { break };
3213 fields.push((k, v));
3214 rest = &tail[len..];
3215 }
3216 (head, fields)
3217}
3218
3219/// `TypeError [ERR_INVALID_ARG_TYPE]: The "<name>" <kind> must be of type
3220/// <expected>. Received …` — Node's single most common argument rejection.
3221pub fn invalid_arg_type(name: &str, kind: &str, expected: &str, v: &Value) -> String {
3222 coded_error(
3223 "TypeError",
3224 "ERR_INVALID_ARG_TYPE",
3225 &format!(
3226 "The \"{name}\" {kind} must be of type {expected}. Received {}",
3227 crate::stdlib::received_desc(v)
3228 ),
3229 )
3230}
3231
3232// ── the fusevm run plumbing ──────────────────────────────────────────────────
3233
3234thread_local! {
3235 static DEBUG_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3236}
3237
3238/// Enable/disable DAP debug execution (`node --dap`).
3239pub fn set_debug_mode(on: bool) {
3240 DEBUG_MODE.with(|d| d.set(on));
3241}
3242
3243// ── join cycle detection ─────────────────────────────────────────────────────
3244
3245thread_local! {
3246 /// Heap handles whose join is in progress, innermost last — V8's JoinStack.
3247 static JOIN_STACK: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
3248}
3249
3250/// V8's `JoinStackPush`: record that `v` is being joined, or report `false` if
3251/// it already is.
3252///
3253/// `Array.prototype.join` (and `toString`/`toLocaleString`, which route through
3254/// it) is the one place the language walks an object graph with no depth bound,
3255/// so every engine cuts re-entrance here: a receiver already on the stack
3256/// contributes the EMPTY STRING rather than recursing. Measured on node v26.7.0,
3257/// `const a=[1]; a.push(a); a.push(2); a.join('-')` is `"1--2"`, and
3258/// `String(a)`/`` `${a}` `` on `a=[a]` are both `""`. node-js had no such cut and
3259/// recursed until the native stack overflowed, ABORTING the process (exit 134) —
3260/// uncatchable, where node returns a string.
3261///
3262/// Only re-entrance is cut, not repetition: `[a,a].join('|')` still renders `a`
3263/// twice, because the first render pops before the second pushes.
3264///
3265/// A `true` return MUST be paired with [`join_stack_pop`].
3266pub fn join_stack_push(v: &Value) -> bool {
3267 match v {
3268 Value::Obj(i) => JOIN_STACK.with(|s| {
3269 let mut s = s.borrow_mut();
3270 if s.contains(i) {
3271 false
3272 } else {
3273 s.push(*i);
3274 true
3275 }
3276 }),
3277 _ => true,
3278 }
3279}
3280
3281/// Pop the innermost [`join_stack_push`].
3282pub fn join_stack_pop() {
3283 JOIN_STACK.with(|s| {
3284 s.borrow_mut().pop();
3285 });
3286}
3287
3288// ── native stack guard ───────────────────────────────────────────────────────
3289
3290thread_local! {
3291 /// Lowest stack address a nested run may start from, or 0 before the
3292 /// running thread's bounds have been measured. Cached because the pthread
3293 /// query is a syscall-free but non-trivial read and this is on every call.
3294 static STACK_FLOOR: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
3295}
3296
3297/// Stack left unusable below the floor, as a fraction of the whole stack: the
3298/// throw itself still has to unwind, build an `Error`, capture `.stack` and run
3299/// whatever `catch` receives it, all of which needs room *below* the deepest
3300/// call that was allowed.
3301const STACK_RESERVE_DIVISOR: usize = 8;
3302/// Floor of that reserve, for a thread whose stack is small enough that an
3303/// eighth of it would not cover the unwind.
3304const STACK_RESERVE_MIN: usize = 512 * 1024;
3305/// Reserve assumed on a platform whose stack bounds cannot be queried. Deliberately
3306/// large relative to a default 8 MiB stack — over-reserving costs recursion
3307/// depth, under-reserving costs the process.
3308const STACK_RESERVE_FALLBACK: usize = 1024 * 1024;
3309
3310/// The address of a local in the caller's frame — how far down the stack
3311/// execution currently is. `black_box` keeps the probe from being optimized into
3312/// a different frame.
3313fn stack_pointer() -> usize {
3314 let probe = 0u8;
3315 std::hint::black_box(&probe) as *const u8 as usize
3316}
3317
3318/// The running thread's `(lowest address, size)` stack bounds.
3319///
3320/// Asked of pthread rather than assumed, because the three threads that run JS
3321/// have three different stacks: the `node` binary's own (`main.rs` reserves
3322/// [`crate::JS_STACK_SIZE`]), a `worker_threads` thread's, and a `cargo test`
3323/// harness thread's. A fixed byte budget would be wrong on two of the three.
3324fn stack_bounds() -> Option<(usize, usize)> {
3325 #[cfg(target_vendor = "apple")]
3326 {
3327 // SAFETY: both calls are pure reads of the calling thread's own
3328 // pthread record; neither allocates nor can fail.
3329 unsafe {
3330 let me = libc::pthread_self();
3331 let top = libc::pthread_get_stackaddr_np(me) as usize;
3332 let size = libc::pthread_get_stacksize_np(me);
3333 if size == 0 || top < size {
3334 return None;
3335 }
3336 Some((top - size, size))
3337 }
3338 }
3339 #[cfg(target_os = "linux")]
3340 {
3341 // SAFETY: `attr` is initialized by `pthread_getattr_np` before it is
3342 // read, only read on the success path, and destroyed on every path.
3343 unsafe {
3344 let mut attr: libc::pthread_attr_t = std::mem::zeroed();
3345 if libc::pthread_getattr_np(libc::pthread_self(), &mut attr) != 0 {
3346 return None;
3347 }
3348 let mut low: *mut libc::c_void = std::ptr::null_mut();
3349 let mut size: libc::size_t = 0;
3350 let ok = libc::pthread_attr_getstack(&attr, &mut low, &mut size) == 0;
3351 libc::pthread_attr_destroy(&mut attr);
3352 if ok && size != 0 {
3353 return Some((low as usize, size));
3354 }
3355 None
3356 }
3357 }
3358 #[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
3359 {
3360 None
3361 }
3362}
3363
3364/// The stack address below which a further nested VM run must throw instead of
3365/// recursing.
3366///
3367/// Every JS call is a Rust-level recursion — `run_user_func_nt` pushes a
3368/// [`Frame`], then `run_chunk_on` builds a whole new `fusevm::VM` on the stack
3369/// and runs the body, whose own calls land back here. Unbounded JS recursion
3370/// therefore used to exhaust the OS stack and ABORT: `fatal runtime error:
3371/// stack overflow`, exit 134, which no `try`/`catch` can see. V8 throws a
3372/// catchable `RangeError: Maximum call stack size exceeded` instead (measured on
3373/// node v26.7.0: `let d=0; function f(){d++;f()}` reports depth 9901).
3374///
3375/// The floor is derived from the thread's real bounds rather than a frame count
3376/// because a node-js frame has no fixed size — a debug build spends ~98 KiB per
3377/// JS call (measured: `node -e 'function f(n){…f(n-1)}'` survived 83 on an 8 MiB
3378/// stack and no more), a release build far less, and a native builtin recursing
3379/// through a user callback spends a different amount again.
3380fn stack_floor() -> usize {
3381 let cached = STACK_FLOOR.with(|c| c.get());
3382 if cached != 0 {
3383 return cached;
3384 }
3385 let floor = match stack_bounds() {
3386 Some((low, size)) => low + (size / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN),
3387 None => stack_pointer().saturating_sub(STACK_RESERVE_FALLBACK),
3388 };
3389 STACK_FLOOR.with(|c| c.set(floor));
3390 floor
3391}
3392
3393/// Stack given to each generator/async coroutine.
3394///
3395/// corosensei's default is 1 MiB, which at a debug build's ~98 KiB per JS call
3396/// left a `function*` body barely ten frames of recursion before it walked off
3397/// the end. The mapping is `PROT_NONE` reserved and `mprotect`ed, so the cost of
3398/// a larger one is address space, not resident memory — but it IS per live
3399/// generator, so this stays far below the entry thread's
3400/// [`crate::JS_STACK_SIZE`]: a program with thousands of concurrent async calls
3401/// has thousands of these.
3402const CORO_STACK_SIZE: usize = 16 * 1024 * 1024;
3403
3404/// The [`stack_floor`] that applies while a coroutine on `stack` is running.
3405fn coro_stack_floor(stack: &impl corosensei::stack::Stack) -> usize {
3406 stack.limit().get() + (CORO_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN)
3407}
3408
3409/// corosensei's own `DefaultStack::default()` size, used only when the
3410/// [`CORO_STACK_SIZE`] reservation is refused and the coroutine therefore runs
3411/// on a stack whose bounds are not ours to read.
3412const CORO_FALLBACK_STACK_SIZE: usize = 1024 * 1024;
3413
3414/// Give a coroutine whose stack bounds are unknown a floor measured from where
3415/// its body starts. Called once, at body entry, on the coroutine's own stack.
3416fn ensure_coroutine_floor() {
3417 if STACK_FLOOR.with(|c| c.get()) != 0 {
3418 return;
3419 }
3420 let budget = CORO_FALLBACK_STACK_SIZE
3421 - (CORO_FALLBACK_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN);
3422 STACK_FLOOR.with(|c| c.set(stack_pointer().saturating_sub(budget)));
3423}
3424
3425/// Install `floor` as the current stack floor, returning the previous one.
3426///
3427/// Used around a coroutine resume, which switches to a stack the thread's
3428/// pthread record knows nothing about. A floor of 0 means "not known" and makes
3429/// the next [`stack_floor`] measure again, which is the right answer for the
3430/// entry thread and a conservative one for a fallback coroutine stack.
3431fn swap_stack_floor(floor: usize) -> usize {
3432 STACK_FLOOR.with(|c| c.replace(floor))
3433}
3434
3435/// Whether the native stack is too close to its floor for one more nested run.
3436pub fn stack_exhausted() -> bool {
3437 stack_pointer() <= stack_floor()
3438}
3439
3440/// The error V8 raises when the call stack is exhausted. Catchable, and with the
3441/// `RangeError` constructor node uses — not a `panic!`.
3442pub fn stack_overflow_error() -> String {
3443 range_error("Maximum call stack size exceeded")
3444}
3445
3446/// Pool key for the body of user function `def_id`.
3447pub fn func_key(def_id: usize) -> u64 {
3448 1 << 40 | def_id as u64
3449}
3450
3451/// Pool key for one part of `try` statement `try_id`: 0 = the block, 1 = the
3452/// handler, 2 = the finalizer.
3453pub fn try_key(try_id: usize, part: u64) -> u64 {
3454 2 << 40 | (try_id as u64) << 2 | part
3455}
3456
3457thread_local! {
3458 /// VMs that have finished a run, kept for the next one — grouped by the
3459 /// chunk they still hold.
3460 ///
3461 /// Every JS call, every `try` block and every generator step runs its chunk
3462 /// through [`run_chunk_on`], which used to build a `fusevm::VM` from
3463 /// scratch: three `Vec` allocations, 70 `register_builtin` writes, an `Arc`
3464 /// for the numeric hook, and the JIT enable — per call. `fib(27)` makes
3465 /// 400k calls, so it built 400k VMs to run 23 ops each.
3466 ///
3467 /// Worse, the caller had to hand over an OWNED `Chunk`, so every call also
3468 /// deep-copied the function's whole compiled body: six `Vec`s, a `String`,
3469 /// and `sub_chunks` recursively. Keying the pool by chunk means a repeated
3470 /// call takes back the VM that already holds that body and copies nothing:
3471 /// `VM::reset` is handed the chunk the VM was already carrying.
3472 ///
3473 /// `VM::reset` keeps the builtin table, the hooks and the JIT setting, so a
3474 /// recycled VM needs none of that again. Each key holds a stack of VMs, and
3475 /// a nested (or recursive) call takes the next one, so a key grows to the
3476 /// deepest simultaneous entry into that function and no further.
3477 static VM_POOL: RefCell<rustc_hash::FxHashMap<u64, Vec<VM>>> =
3478 RefCell::new(rustc_hash::FxHashMap::default());
3479}
3480
3481/// An idle VM filed under `key`, if any.
3482fn take_pooled(key: u64) -> Option<VM> {
3483 VM_POOL.with(|p| p.borrow_mut().get_mut(&key).and_then(|v| v.pop()))
3484}
3485
3486/// File a finished VM under `key` for the next run to take.
3487fn put_pooled(key: u64, vm: VM) {
3488 VM_POOL.with(|p| p.borrow_mut().entry(key).or_default().push(vm));
3489}
3490
3491/// Take a VM ready to run `chunk` — recycled if one is idle, otherwise built
3492/// and fitted with the builtins and hooks a fresh VM needs.
3493fn acquire_vm(chunk: Chunk) -> VM {
3494 if let Some(mut vm) = take_pooled(0) {
3495 vm.reset(chunk);
3496 return vm;
3497 }
3498 let mut vm = VM::new(chunk);
3499 crate::builtins::install(&mut vm);
3500 vm.set_numeric_hook(std::sync::Arc::new(|op, a, b| {
3501 crate::builtins::numeric_hook(op, a, b)
3502 }));
3503 // Under `--dap` the tracing JIT would compile hot loops and skip the
3504 // per-statement `DBG_LINE` markers, so debug runs stay on the pure
3505 // interpreter. The `DBG_LINE` builtin fires the debugger line hook; the
3506 // extension seam mirrors pythonrs should the marker emission ever switch.
3507 // The mode is fixed before the first chunk runs, so a pooled VM can never
3508 // come back wearing the wrong one.
3509 if DEBUG_MODE.with(|d| d.get()) {
3510 vm.set_extension_handler(Box::new(|vm, id, _| {
3511 crate::dap::on_ext(vm, id);
3512 }));
3513 } else {
3514 vm.enable_tracing_jit();
3515 }
3516 vm
3517}
3518
3519/// Register every node-js builtin + the numeric hook on a VM, then run it.
3520///
3521/// For a chunk that runs once — a module body, an `eval` — there is nothing to
3522/// key a pool by, so this resets a spare VM with the caller's chunk. Anything
3523/// that runs repeatedly (a function body, a `try` block) goes through
3524/// [`run_chunk_keyed`] instead and never copies its chunk twice.
3525pub fn run_chunk_on(chunk: Chunk) -> Result<Value, String> {
3526 // Checked before the `VM` is built: `VM::new` + `install` are themselves
3527 // several KiB of frame, so a check after them could already have overflowed.
3528 if stack_exhausted() {
3529 return Err(stack_overflow_error());
3530 }
3531 finish_run(0, acquire_vm(chunk))
3532}
3533
3534/// Run the chunk filed under `key`, building it with `make` only if no VM is
3535/// already holding it. A recycled VM re-runs the chunk it kept, so a repeated
3536/// call copies no bytecode at all.
3537pub fn run_chunk_keyed(key: u64, make: impl FnOnce() -> Chunk) -> Result<Value, String> {
3538 if stack_exhausted() {
3539 return Err(stack_overflow_error());
3540 }
3541 let vm = match take_pooled(key) {
3542 Some(mut vm) => {
3543 // Hand the VM back the chunk it is already carrying: `reset` takes
3544 // an owned `Chunk`, and this is the one place where the owned chunk
3545 // costs nothing.
3546 let held = std::mem::take(&mut vm.chunk);
3547 vm.reset(held);
3548 vm
3549 }
3550 None => acquire_vm(make()),
3551 };
3552 finish_run(key, vm)
3553}
3554
3555/// Run a prepared VM to completion and file it back under `key`.
3556fn finish_run(key: u64, mut vm: VM) -> Result<Value, String> {
3557 let outcome = vm.run();
3558 let result = match outcome {
3559 _ if with_host(|h| h.error.is_some()) => {
3560 Err(with_host(|h| h.take_error()).expect("just checked"))
3561 }
3562 VMResult::Ok(v) => Ok(v),
3563 VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
3564 VMResult::Error(e) => Err(e),
3565 };
3566 put_pooled(key, vm);
3567 result
3568}
3569
3570/// Run `chunk` in the GLOBAL scope instead of the caller's.
3571///
3572/// `run_chunk_on` executes on whatever frame is current, so a nested run sees —
3573/// and can shadow — the *calling function's* locals. That is right for a direct
3574/// `eval`, and wrong for every other runtime-source construct: a `new Function`
3575/// body, an indirect `eval` and `vm.runInThisContext` are all specified to run
3576/// in the global scope (ECMA-262 19.2.1.1 `PerformEval` with a null
3577/// `strictCaller`/`direct` pair; `FunctionBody` is instantiated with the *global*
3578/// environment, 20.2.1.1.1 step 26). Measured against node v26.7.0,
3579/// `function outer(){ let loc = 42; return vm.runInThisContext('typeof loc'); }`
3580/// is `"undefined"` there and was `"number"` here.
3581///
3582/// A `var` the chunk itself declares lands in the top-level scope and persists,
3583/// so successive `vm.runInThisContext` calls share it.
3584pub fn run_chunk_in_global_scope(chunk: Chunk) -> Result<Value, String> {
3585 // An INDIRECT eval really is global code (19.2.1.1 step 6): its `var`s bind
3586 // to the global object, not to the entry module's wrapper scope. The flag
3587 // that keeps the entry script's own `var`s out of the globals map has to be
3588 // lifted for the duration, or `(0, eval)('var g = 1')` stopped reaching
3589 // `globalThis.g`.
3590 let prev_scope = with_host(|h| std::mem::take(&mut h.module_scope));
3591 let out = run_chunk_in_global_scope_inner(chunk);
3592 with_host(|h| h.module_scope = prev_scope);
3593 out
3594}
3595
3596fn run_chunk_in_global_scope_inner(chunk: Chunk) -> Result<Value, String> {
3597 let global_env = with_host(|h| h.global_env.clone());
3598 with_host(|h| {
3599 h.frames.push(Frame {
3600 env: global_env.clone(),
3601 base_env: global_env,
3602 this_obj: None,
3603 new_target: None,
3604 home_class: None,
3605 home_static: false,
3606 home_object: None,
3607 strict: false,
3608 line: 0,
3609 owner: None,
3610 is_module: true,
3611 this_state: ThisState::Plain,
3612 })
3613 });
3614 let r = run_chunk_on(chunk);
3615 with_host(|h| {
3616 h.frames.pop();
3617 });
3618 r
3619}
3620
3621/// Run the top-level program chunk, then drain the event loop (microtasks +
3622/// timers) until quiescent — matching Node, which keeps the process alive while
3623/// pending async work remains.
3624pub fn run_main(chunk: Chunk) -> Result<Value, String> {
3625 with_host(|h| h.module_scope = true);
3626 let r = run_chunk_on(chunk);
3627 with_host(|h| h.signal = None);
3628 if r.is_ok() {
3629 run_event_loop()?;
3630 finish_process_events()?;
3631 }
3632 r
3633}
3634
3635/// The shutdown sequence Node runs once the loop has drained on its own: fire
3636/// `beforeExit` (which MAY schedule more work, in which case the loop runs
3637/// again and `beforeExit` fires again), then fire `exit` exactly once.
3638///
3639/// Neither event fired at all before this existed, so `process.on('exit', …)`
3640/// was a registration with no delivery — a listener whose body printed was
3641/// silently dropped, and one that set `process.exitCode` could not affect the
3642/// status. Measured on node v26.7.0,
3643/// `process.on('exit', c => console.log('exit', c))` prints `exit 0`.
3644///
3645/// An explicit `process.exit()` never reaches here (it leaves the process from
3646/// inside the builtin), and neither does an uncaught exception — matching
3647/// Node, where `beforeExit` is skipped on both paths.
3648fn finish_process_events() -> Result<(), String> {
3649 // Bounded: a `beforeExit` listener that re-arms work every time would spin
3650 // forever, exactly as it does in Node, but a runaway here would hang a
3651 // parity run with no output, so it is capped and then treated as drained.
3652 for _ in 0..1000 {
3653 let code = with_host(|h| h.exit_code).unwrap_or(0);
3654 if !crate::stdlib::process::emit_before_exit(code)? {
3655 break;
3656 }
3657 let more =
3658 with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
3659 if !more {
3660 break;
3661 }
3662 run_event_loop()?;
3663 }
3664 let code = with_host(|h| h.exit_code).unwrap_or(0);
3665 crate::stdlib::process::emit_exit_event(code)
3666}
3667
3668// ── formatting ───────────────────────────────────────────────────────────────
3669
3670/// Format a JS number exactly as `Number.prototype.toString` does for the common
3671/// range (no exponential-notation threshold handling for very large/small).
3672pub fn fmt_number(f: f64) -> String {
3673 if f.is_nan() {
3674 return "NaN".into();
3675 }
3676 if f.is_infinite() {
3677 return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
3678 }
3679 if f == 0.0 {
3680 // Covers -0.0 too: (-0).toString() === "0".
3681 return "0".into();
3682 }
3683 if f < 0.0 {
3684 return format!("-{}", js_number_repr(-f));
3685 }
3686 js_number_repr(f)
3687}
3688
3689/// If `k` is an array-index property key, return its numeric value. Per
3690/// ECMAScript, a String property key `P` is an array index iff
3691/// `ToString(ToUint32(P)) === P` and `ToUint32(P) !== 2^32 - 1` — i.e. a
3692/// canonical decimal (no leading zeros, no sign) in the range `0..=2^32-2`.
3693pub fn array_index(k: &str) -> Option<u32> {
3694 if k.is_empty() {
3695 return None;
3696 }
3697 if k == "0" {
3698 return Some(0);
3699 }
3700 // A leading '0' (other than the lone "0" above) is non-canonical.
3701 if k.as_bytes()[0] == b'0' {
3702 return None;
3703 }
3704 if !k.bytes().all(|b| b.is_ascii_digit()) {
3705 return None;
3706 }
3707 match k.parse::<u64>() {
3708 // Array index must be < 2^32-1; u32::MAX == 2^32-1 is excluded.
3709 Ok(n) if n < u32::MAX as u64 => Some(n as u32),
3710 _ => None,
3711 }
3712}
3713
3714/// Compare two own-property keys for `OrdinaryOwnPropertyKeys` enumeration order:
3715/// integer-index keys sort ascending-numeric and precede all string keys; two
3716/// non-index keys compare `Equal` so a *stable* sort leaves them in insertion
3717/// order. (Symbols are stored as `@@…`/`#…` string keys and are non-index, so
3718/// they also fall into the stable-insertion-order tail.)
3719pub fn key_order_cmp(a: &str, b: &str) -> std::cmp::Ordering {
3720 use std::cmp::Ordering;
3721 match (array_index(a), array_index(b)) {
3722 (Some(x), Some(y)) => x.cmp(&y),
3723 (Some(_), None) => Ordering::Less,
3724 (None, Some(_)) => Ordering::Greater,
3725 (None, None) => Ordering::Equal,
3726 }
3727}
3728
3729/// Reorder an object's own-property map into `OrdinaryOwnPropertyKeys` order in
3730/// place: array-index keys ascending first, then the remaining keys in their
3731/// existing (insertion) order. A no-op unless at least one index key is present,
3732/// so the overwhelmingly common all-string-key object keeps its exact order and
3733/// pays nothing. `IndexMap::sort_by` is a stable sort.
3734pub fn canonicalize_own_keys(props: &mut IndexMap<String, Value>) {
3735 if props.keys().any(|k| array_index(k).is_some()) {
3736 props.sort_by(|ak, _, bk, _| key_order_cmp(ak, bk));
3737 }
3738}
3739
3740/// ECMAScript `Number::toString` layout for a positive, finite, nonzero value.
3741///
3742/// Rust's `Display`/`LowerExp` give the shortest round-trip decimal digits, but
3743/// NOT JavaScript's exponential-vs-fixed threshold: Rust prints `1e21` as
3744/// `1000000000000000000000` and `1e-7` as `0.0000001`, whereas JS prints `1e+21`
3745/// and `1e-7`. So we take the shortest digits from `{:e}` and re-lay them out per
3746/// the spec (steps 5–10 of Number::toString): `k` significant digits `s` with
3747/// decimal exponent `n` (value = s × 10^(n−k)); exponential form only when
3748/// `n > 21` or `n ≤ -6`.
3749fn js_number_repr(a: f64) -> String {
3750 // `{:e}` yields `d[.ddd]e<exp>` with the mantissa in [1, 10) and shortest
3751 // round-trip digits. Split it into the digit string `s` and exponent `E`.
3752 let sci = format!("{a:e}");
3753 let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
3754 let e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
3755 let s: String = mant.chars().filter(|c| *c != '.').collect();
3756 let k = s.len() as i32; // number of significant digits
3757 let n = e + 1; // value = s × 10^(n−k), 10^(k−1) ≤ s < 10^k
3758
3759 if k <= n && n <= 21 {
3760 // Integer with trailing zeros: all digits, then n−k zeros.
3761 let mut out = s;
3762 out.push_str(&"0".repeat((n - k) as usize));
3763 out
3764 } else if 0 < n && n <= 21 {
3765 // Decimal point inside the digit run: n digits, '.', the rest.
3766 format!("{}.{}", &s[..n as usize], &s[n as usize..])
3767 } else if -6 < n && n <= 0 {
3768 // Leading "0." then (−n) zeros then all digits.
3769 format!("0.{}{}", "0".repeat((-n) as usize), s)
3770 } else {
3771 // Exponential form. Exponent digit is n−1, always signed.
3772 let exp = n - 1;
3773 let sign = if exp >= 0 { '+' } else { '-' };
3774 let mag = exp.abs();
3775 if k == 1 {
3776 format!("{s}e{sign}{mag}")
3777 } else {
3778 format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
3779 }
3780 }
3781}
3782
3783impl JsHost {
3784 /// The `typeof` string for `v`.
3785 pub fn type_of(&self, v: &Value) -> &'static str {
3786 match v {
3787 Value::Undef => "undefined",
3788 Value::Bool(_) => "boolean",
3789 Value::Int(_) | Value::Float(_) => "number",
3790 Value::Str(_) => "string",
3791 Value::Obj(_) => match self.get(v) {
3792 Some(JsObj::Str(_)) => "string",
3793 // 10.5's `[[Call]]` slot exists on a proxy exactly when its
3794 // target is callable, so `typeof` classifies by the target —
3795 // `typeof new Proxy(function(){}, {})` is `'function'`. The walk
3796 // is bounded: a proxy of a proxy defers again.
3797 Some(JsObj::Proxy { target, .. }) => {
3798 let mut cur = target;
3799 for _ in 0..100 {
3800 match self.get(cur) {
3801 Some(JsObj::Proxy { target: t, .. }) => cur = t,
3802 _ => break,
3803 }
3804 }
3805 if is_callable(self, cur) {
3806 "function"
3807 } else {
3808 "object"
3809 }
3810 }
3811 Some(JsObj::Func(_))
3812 | Some(JsObj::BoundMethod { .. })
3813 | Some(JsObj::BoundFunc { .. })
3814 | Some(JsObj::Class(_)) => "function",
3815 // A Builtin is a callable (`Array`, `parseInt`, `Math.floor`) —
3816 // `typeof === "function"` — EXCEPT the non-callable namespace
3817 // objects (`Math`, `JSON`, `require('fs')`, …) which are "object".
3818 Some(JsObj::Builtin(n)) => {
3819 if builtin_is_callable(n) {
3820 "function"
3821 } else {
3822 "object"
3823 }
3824 }
3825 Some(JsObj::Symbol { .. }) => "symbol",
3826 Some(JsObj::BigInt(_)) => "bigint",
3827 _ => "object", // arrays, objects, null, Map/Set, generators
3828 },
3829 _ => "object",
3830 }
3831 }
3832
3833 /// JS truthiness: false / 0 / -0 / NaN / "" / null / undefined are falsy.
3834 pub fn truthy(&self, v: &Value) -> bool {
3835 match v {
3836 Value::Undef => false,
3837 Value::Bool(b) => *b,
3838 Value::Int(n) => *n != 0,
3839 Value::Float(f) => *f != 0.0 && !f.is_nan(),
3840 Value::Str(s) => !s.is_empty(),
3841 Value::Obj(_) => match self.get(v) {
3842 Some(JsObj::Str(s)) => !s.is_empty(),
3843 Some(JsObj::Null) => false,
3844 Some(JsObj::BigInt(b)) => !num_traits::Zero::is_zero(b),
3845 _ => true, // arrays, objects, functions
3846 },
3847 _ => true,
3848 }
3849 }
3850
3851 /// Coerce to a number (`ToNumber`): the arithmetic-context conversion.
3852 pub fn to_number(&self, v: &Value) -> f64 {
3853 match v {
3854 Value::Undef => f64::NAN,
3855 Value::Bool(b) => {
3856 if *b {
3857 1.0
3858 } else {
3859 0.0
3860 }
3861 }
3862 Value::Int(n) => *n as f64,
3863 Value::Float(f) => *f,
3864 Value::Str(s) => str_to_number(s),
3865 Value::Obj(_) => match self.get(v) {
3866 Some(JsObj::Str(s)) => str_to_number(s),
3867 Some(JsObj::Null) => 0.0,
3868 Some(JsObj::BigInt(b)) => bigint_to_f64(b),
3869 Some(JsObj::Array(items)) => {
3870 // [] -> 0, [x] -> ToNumber(x), else NaN.
3871 if items.is_empty() {
3872 0.0
3873 } else if items.len() == 1 {
3874 self.to_number(&items[0])
3875 } else {
3876 f64::NAN
3877 }
3878 }
3879 _ => f64::NAN,
3880 },
3881 _ => f64::NAN,
3882 }
3883 }
3884
3885 /// `String(v)` — the string-coercion form (raw, unquoted).
3886 pub fn str_of(&self, v: &Value) -> String {
3887 match v {
3888 Value::Undef => "undefined".into(),
3889 Value::Bool(b) => if *b { "true" } else { "false" }.into(),
3890 Value::Int(n) => n.to_string(),
3891 Value::Float(f) => fmt_number(*f),
3892 Value::Str(s) => (**s).clone(),
3893 Value::Obj(_) => match self.get(v) {
3894 Some(JsObj::Str(s)) => s.clone(),
3895 Some(JsObj::Null) => "null".into(),
3896 Some(JsObj::BigInt(b)) => b.to_string(),
3897 Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
3898 Some(JsObj::Array(items)) => {
3899 // Array.prototype.toString: comma-join, null/undefined -> "".
3900 // Guarded by the JoinStack (see `join_stack_push`) so a
3901 // self-referential array yields "" instead of recursing until
3902 // the native stack aborts the process.
3903 if !join_stack_push(v) {
3904 return String::new();
3905 }
3906 let parts: Vec<String> = items
3907 .iter()
3908 .map(|x| match x {
3909 Value::Undef => String::new(),
3910 _ if self.is_null(x) => String::new(),
3911 _ => self.str_of(x),
3912 })
3913 .collect();
3914 join_stack_pop();
3915 parts.join(",")
3916 }
3917 Some(JsObj::Object(props)) => {
3918 // A native `Buffer` stringifies to its decoded (utf-8)
3919 // contents, matching `buf.toString()` — needed for `'' + buf`,
3920 // template interpolation, and `data += chunk` (the pattern
3921 // Express/body-parser use to read a request body).
3922 if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Buffer") {
3923 let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
3924 Some(JsObj::Array(items)) => {
3925 items.iter().map(|x| self.to_number(x) as u8).collect()
3926 }
3927 _ => Vec::new(),
3928 };
3929 String::from_utf8_lossy(&bytes).into_owned()
3930 } else if let Some(s) = self.error_to_string(v) {
3931 s
3932 } else {
3933 "[object Object]".into()
3934 }
3935 }
3936 Some(JsObj::Func(f)) => {
3937 // A function built from runtime source (`new Function`,
3938 // `vm.compileFunction`) retains the exact text V8 synthesizes
3939 // for it, so `Function.prototype.toString` reports what Node
3940 // reports. Ordinary functions carry no source here (the
3941 // compiler keeps no spans), so they fall back to a placeholder.
3942 if let Some(src) = self.fn_prop(v, "@@source") {
3943 return self.str_of(&src);
3944 }
3945 let name = self
3946 .funcs
3947 .get(f.def_id)
3948 .map(|d| d.name.clone())
3949 .unwrap_or_default();
3950 format!("function {name}() {{ [code] }}")
3951 }
3952 // The native-code form names the FUNCTION, not its key:
3953 // `String(Math.max)` is `function max() { [native code] }`.
3954 Some(JsObj::Builtin(n)) => {
3955 // The `console` methods are the exception node itself makes:
3956 // each is a wrapper, so `String(console.log)` is the
3957 // ANONYMOUS native-code form even though `console.log.name`
3958 // is `log`. Measured on v26.8.1.
3959 if n.starts_with("console.") {
3960 "function () { [native code] }".into()
3961 } else if let Some(accessor) = crate::builtins::proto_getter_name(n) {
3962 // An accessor half names itself `get size` / `set
3963 // arguments`, which `builtin_name` cannot build because
3964 // it returns a borrowed `&str`.
3965 format!("function {accessor}() {{ [native code] }}")
3966 } else {
3967 format!(
3968 "function {}() {{ [native code] }}",
3969 crate::builtins::builtin_name(n)
3970 )
3971 }
3972 }
3973 // A method read off an instance names itself the same way the
3974 // prototype method it resolves to does: `String([].slice)` is
3975 // `function slice() { [native code] }`.
3976 Some(JsObj::BoundMethod { name, .. }) => {
3977 format!("function {name}() {{ [native code] }}")
3978 }
3979 Some(JsObj::BoundFunc { .. }) => "function () { [native code] }".into(),
3980 // `Function.prototype.toString` refuses to expose a proxy's
3981 // target: V8 reports the native-code form for a proxy of ANY
3982 // callable, so `String(new Proxy(function f(){}, {}))` is
3983 // `function () { [native code] }`, not `f`'s source.
3984 Some(JsObj::Proxy { .. }) if is_callable(self, v) => {
3985 "function () { [native code] }".into()
3986 }
3987 Some(JsObj::Class(c)) => format!("class {} {{ }}", c.name),
3988 Some(JsObj::Symbol { desc, .. }) => {
3989 // `String(sym)` is allowed (unlike implicit coercion) and yields
3990 // `Symbol(desc)`.
3991 match desc {
3992 Some(d) => format!("Symbol({d})"),
3993 None => "Symbol()".into(),
3994 }
3995 }
3996 _ => "[object Object]".into(),
3997 },
3998 _ => "[object Object]".into(),
3999 }
4000 }
4001
4002 /// The `Symbol.toStringTag` string `util.inspect` renders as a `[Tag]`
4003 /// prefix. V8 suppresses the tag when it is an OWN ENUMERABLE property,
4004 /// because it is then already listed as a `Symbol(Symbol.toStringTag): …`
4005 /// entry and showing it twice would be wrong.
4006 ///
4007 /// Only a DATA property is seen. A tag supplied by a prototype getter
4008 /// (`class C { get [Symbol.toStringTag]() { … } }`) would need a JS call,
4009 /// which cannot run under the host borrow `inspect` holds — such an object
4010 /// prints without the prefix.
4011 /// `[String: 'ab']` / `[Number: 1]` / `[Boolean: false]` — how node renders
4012 /// a primitive wrapper, distinguishing it from the bare primitive.
4013 fn inspect_wrapper(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Option<String> {
4014 let prim = match self.get(v) {
4015 Some(JsObj::Object(p)) => p.get("@@primitive").cloned()?,
4016 _ => return None,
4017 };
4018 let ctor = match &prim {
4019 Value::Bool(_) => "Boolean",
4020 Value::Int(_) | Value::Float(_) => "Number",
4021 _ => "String",
4022 };
4023 let head = format!("[{ctor}: {}]", self.inspect_lvl(&prim, indent, st));
4024 // Extra own properties still print, as `[String: 'ab'] { tag: 1 }`. The
4025 // boxed characters are NOT extras — node hides the index properties of
4026 // a String wrapper, showing only what was added to it.
4027 let width = if ctor == "String" {
4028 self.str_of(&prim).chars().count()
4029 } else {
4030 0
4031 };
4032 let extras: Vec<String> = match self.get(v) {
4033 Some(JsObj::Object(p)) => p
4034 .iter()
4035 .filter(|(k, _)| {
4036 !k.starts_with("@@")
4037 && !k.starts_with('#')
4038 && self.prop_attrs(v, k).enumerable
4039 && !k.parse::<usize>().is_ok_and(|i| i < width)
4040 })
4041 .map(|(k, val)| {
4042 format!("{}: {}", fmt_key(k), self.inspect_lvl(val, indent + 2, st))
4043 })
4044 .collect(),
4045 _ => Vec::new(),
4046 };
4047 if extras.is_empty() {
4048 return Some(head);
4049 }
4050 Some(self.render_object(&extras, &format!("{head} "), indent, st))
4051 }
4052
4053 /// The `key: value` parts for own properties a script attached to an exotic
4054 /// whose contents are internal slots — `new Map([['k',1]])` with `m.x = 5`
4055 /// prints `Map(1) { 'k' => 1, x: 5 }`.
4056 fn side_table_parts(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Vec<String> {
4057 self.fn_prop_keys(v)
4058 .into_iter()
4059 .filter(|k| {
4060 !k.starts_with("@@")
4061 && !k.starts_with('#')
4062 && !is_symbol_key(k)
4063 && self.prop_attrs(v, k).enumerable
4064 })
4065 .map(|k| {
4066 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4067 format!(
4068 "{}: {}",
4069 fmt_key(&k),
4070 self.inspect_lvl(&val, indent + 2, st)
4071 )
4072 })
4073 .collect()
4074 }
4075
4076 fn inspect_tag(&self, v: &Value) -> Option<String> {
4077 let own = matches!(self.get(v), Some(JsObj::Object(p)) if p.contains_key("@@toStringTag"));
4078 if own && self.prop_attrs(v, "@@toStringTag").enumerable {
4079 return None;
4080 }
4081 let t = lookup_chain(self, v, "@@toStringTag")?;
4082 self.as_str(&t)
4083 }
4084
4085 /// `console.log`-style rendering of a top-level argument: bare strings print
4086 /// raw; everything else uses `inspect`.
4087 pub fn console_format(&self, v: &Value) -> String {
4088 match v {
4089 Value::Str(_) => self.str_of(v),
4090 Value::Obj(_) if matches!(self.get(v), Some(JsObj::Str(_))) => self.str_of(v),
4091 _ => self.inspect(v),
4092 }
4093 }
4094
4095 /// `util.inspect`-style rendering (nested; strings quoted).
4096 pub fn inspect(&self, v: &Value) -> String {
4097 self.inspect_lvl(v, 0, &mut InspectCycles::default())
4098 }
4099
4100 /// `util.inspect` at a given indentation level, with the cycle guard applied
4101 /// around the object cases.
4102 ///
4103 /// A value already being rendered further up the chain is a CYCLE, and Node
4104 /// marks both ends of it: the back-edge prints `[Circular *N]` and the
4105 /// object it points back at is prefixed `<ref *N>`. Without this the walk
4106 /// only stopped when the depth limit turned the back-edge into `[Object]`,
4107 /// so `const c={a:1}; c.c=c` printed the misleading
4108 /// `{ a: 1, c: { a: 1, c: { a: 1, c: [Object] } } }` instead of
4109 /// `<ref *1> { a: 1, c: [Circular *1] }`.
4110 ///
4111 /// The `*N` id is only assigned when the back-edge is reached, i.e. while
4112 /// the target's own children are being rendered — so the prefix can only be
4113 /// decided after `inspect_value` returns.
4114 /// Whether `v` renders as a LEAF — a finished string produced without
4115 /// recursing into any child.
4116 ///
4117 /// Node assigns `ctx.currentDepth = recurseTimes` in `formatRaw`, but only
4118 /// after the early returns for the shapes that answer immediately: a bare
4119 /// Date is its ISO string, a regex is its literal, an empty container is its
4120 /// braces, and a Buffer is whatever its `[util.inspect.custom]` says. None of
4121 /// those record a depth, so a group containing one is not pushed over the
4122 /// `compact` threshold by it — `util.inspect([new Date(0), null], { compact:
4123 /// 1 })` stays on one line. Charging them a level broke exactly those groups.
4124 fn renders_without_expanding(&self, v: &Value) -> bool {
4125 let plain_props = |p: &IndexMap<String, Value>| {
4126 p.keys().all(|k| k.starts_with("@@") || k.starts_with('#'))
4127 };
4128 match self.get(v) {
4129 // A regex never recurses, with or without its hidden `lastIndex`.
4130 Some(JsObj::RegExp(_)) => true,
4131 Some(JsObj::Map { entries, .. }) => entries.is_empty(),
4132 Some(JsObj::Set { entries, .. }) => entries.is_empty(),
4133 Some(JsObj::Array(items)) => items.is_empty() && self.own_symbol_entries(v).is_empty(),
4134 Some(JsObj::Object(p)) => match p.get("@@native").map(|t| self.str_of(t)).as_deref() {
4135 Some("Buffer") => inspect_custom(),
4136 // Own properties added to a Date DO get expanded after it.
4137 Some("Date") => plain_props(p),
4138 Some(_) => false,
4139 None => plain_props(p) && self.own_symbol_entries(v).is_empty(),
4140 },
4141 _ => false,
4142 }
4143 }
4144
4145 fn inspect_lvl(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4146 if !matches!(v, Value::Obj(_)) {
4147 return self.inspect_value(v, indent, st);
4148 }
4149 if st.seen.iter().any(|p| self.strict_eq(p, v)) {
4150 return format!("[Circular *{}]", st.mark(self, v));
4151 }
4152 st.seen.push(v.clone());
4153 // Node ASSIGNS `ctx.currentDepth = recurseTimes` on entry to each value
4154 // it expands — not a running maximum — so after the children have been
4155 // rendered it holds the depth of the last chain below this group, which
4156 // is what `reduceToSingleString` compares. A value the depth limit
4157 // stubs out as `[Object]` is never expanded and must not count, or an
4158 // object whose deepest level was elided would break where node joins.
4159 // Only a value node actually EXPANDS advances the depth. A string,
4160 // symbol or bigint is a JS primitive that this host happens to store on
4161 // the heap, so it reaches here as `Value::Obj` where an unboxed number
4162 // returns above — and counting it as a level made any group holding one
4163 // look deeper than it was. Under `compact: 1` that is the difference
4164 // between node's `Map(2) { 'k2' => 8, 'j' => 5 }` and breaking the same
4165 // Map across four lines, because its string KEYS were being charged a
4166 // nesting level.
4167 if indent as i64 <= inspect_indent_limit()
4168 && !is_primitive(self, v)
4169 && !self.renders_without_expanding(v)
4170 {
4171 st.deepest = indent;
4172 }
4173 let body = self.inspect_value(v, indent, st);
4174 st.seen.pop();
4175 match st.id_of(self, v) {
4176 Some(id) => format!("<ref *{id}> {body}"),
4177 None => body,
4178 }
4179 }
4180
4181 /// The rendering itself, once `inspect_lvl` has established that `v` is not
4182 /// a back-edge into an object already on the stack.
4183 fn inspect_value(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4184 if let Some(s) = self.inspect_wrapper(v, indent, st) {
4185 return s;
4186 }
4187 match v {
4188 Value::Undef => "undefined".into(),
4189 Value::Bool(b) => if *b { "true" } else { "false" }.into(),
4190 Value::Int(n) => n.to_string(),
4191 // `util.inspect` distinguishes negative zero; `String(-0)` does not.
4192 Value::Float(f) if *f == 0.0 && f.is_sign_negative() => "-0".into(),
4193 Value::Float(f) => fmt_number(*f),
4194 Value::Str(s) => quote_str(s),
4195 Value::Obj(_) => match self.get(v) {
4196 Some(JsObj::Str(s)) => quote_str(s),
4197 Some(JsObj::Null) => "null".into(),
4198 // `util.inspect` renders a bigint with the `n` suffix, a regex bare.
4199 Some(JsObj::BigInt(b)) => format!("{b}n"),
4200 // `lastIndex` is a non-enumerable own property of every regex,
4201 // so `showHidden` (and therefore `%o`) appends it:
4202 // `/x/g { [lastIndex]: 0 }`.
4203 Some(JsObj::RegExp(r)) => {
4204 let body = format!("/{}/{}", r.source, r.flags);
4205 if inspect_show_hidden() {
4206 format!("{body} {{ [lastIndex]: {} }}", r.last_index.get())
4207 } else {
4208 body
4209 }
4210 }
4211 // `util.inspect` on node v26.7.0 renders a proxy as
4212 // `Proxy(<target>)` — the target's own rendering, wrapped. It
4213 // deliberately does NOT run the handler's traps, so this stays a
4214 // pure `&self` read like every other inspect arm.
4215 Some(JsObj::Proxy { target, .. }) => {
4216 format!("Proxy({})", self.inspect_lvl(target, indent, st))
4217 }
4218 Some(JsObj::Array(items)) => {
4219 // Own enumerable non-index string props (e.g. a `str.match(re)`
4220 // result's `index`/`input`/`groups`, or a user-assigned
4221 // `arr.foo`) render after the elements, as `key: value`.
4222 let prop_keys: Vec<String> = self
4223 .fn_prop_keys(v)
4224 .into_iter()
4225 .filter(|k| {
4226 !k.starts_with("@@")
4227 && !k.starts_with('#')
4228 && self.prop_attrs(v, k).enumerable
4229 })
4230 .collect();
4231 // An own enumerable SYMBOL-keyed property renders after the
4232 // string keys as `Symbol(desc): value`, as it does on an
4233 // object receiver.
4234 let sym_entries = self.own_symbol_entries(v);
4235 // Under `showHidden` even an empty array has something to
4236 // show — node prints `[ [length]: 0 ]`, not `[]`.
4237 if items.is_empty()
4238 && prop_keys.is_empty()
4239 && sym_entries.is_empty()
4240 && !inspect_show_hidden()
4241 {
4242 return "[]".into();
4243 }
4244 // Node's default inspect depth is 2 (root = depth 0); deeper
4245 // nesting collapses to `[Array]`. indent grows by 2 per level.
4246 if indent as i64 > inspect_indent_limit() {
4247 return "[Array]".into();
4248 }
4249 // `util.inspect`'s `maxArrayLength` (default 100): only the
4250 // first 100 elements are formatted, and the rest collapse to
4251 // a `... N more items` entry. Without the cap a 120-element
4252 // array printed all 120 — and, because the grid column width
4253 // is computed from what is SHOWN, every column was also one
4254 // character wider than node's.
4255 // A SPARSE array takes node's `formatSpecialArray` path: an
4256 // elided run renders as `<N empty items>` rather than as the
4257 // `undefined` it reads back as.
4258 let (mut inner, has_tail) = if self.has_holes(v) {
4259 self.inspect_sparse(v, items, indent, st)
4260 } else {
4261 let shown = items.len().min(inspect_max_array_length());
4262 let mut inner: Vec<String> = items[..shown]
4263 .iter()
4264 .map(|x| self.inspect_lvl(x, indent + 2, st))
4265 .collect();
4266 let remaining = items.len() - shown;
4267 if remaining > 0 {
4268 let unit = if remaining == 1 { "item" } else { "items" };
4269 inner.push(format!("... {remaining} more {unit}"));
4270 }
4271 (inner, remaining > 0)
4272 };
4273 // `showHidden` exposes the non-enumerable `length`, which an
4274 // array always has. It sorts BEFORE any own property node
4275 // shows (`[ 1, [length]: 1, x: 2 ]`) and, being an entry
4276 // rather than an element, it also turns the column grid off —
4277 // which is why a ten-element array under `showHidden` prints
4278 // on one line rather than as a grid.
4279 let show_hidden = inspect_show_hidden();
4280 if show_hidden {
4281 inner.push(format!("[length]: {}", items.len()));
4282 }
4283 let has_props = show_hidden || !prop_keys.is_empty() || !sym_entries.is_empty();
4284 for k in &prop_keys {
4285 let val = self.fn_prop(v, k).unwrap_or(Value::Undef);
4286 inner.push(format!(
4287 "{}: {}",
4288 fmt_key(k),
4289 self.inspect_lvl(&val, indent + 2, st)
4290 ));
4291 }
4292 for (k, val) in &sym_entries {
4293 let label = match self.symbol_of_key(k) {
4294 Some(s) => self.inspect(&s),
4295 None => continue,
4296 };
4297 inner.push(format!(
4298 "{label}: {}",
4299 self.inspect_lvl(val, indent + 2, st)
4300 ));
4301 }
4302 self.render_array(
4303 &inner,
4304 items,
4305 indent,
4306 ArrayLayout {
4307 has_props,
4308 has_tail,
4309 base: "",
4310 },
4311 st,
4312 )
4313 }
4314 // `URLSearchParams` renders its pairs, not its slots:
4315 // `URLSearchParams { 'a' => '1', 'b' => '2' }`. Keys repeat,
4316 // which is why it is a pair list rather than a Map rendering.
4317 Some(JsObj::Object(props))
4318 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4319 == Some("URLSearchParams") =>
4320 {
4321 let pairs: Vec<Value> = match props.get("@@pairs").and_then(|a| self.get(a)) {
4322 Some(JsObj::Array(items)) => items.clone(),
4323 _ => Vec::new(),
4324 };
4325 if pairs.is_empty() {
4326 return "URLSearchParams {}".into();
4327 }
4328 let inner: Vec<String> = pairs
4329 .iter()
4330 .filter_map(|kv| match self.get(kv) {
4331 Some(JsObj::Array(p)) if p.len() == 2 => Some(format!(
4332 "{} => {}",
4333 self.inspect_lvl(&p[0], indent + 2, st),
4334 self.inspect_lvl(&p[1], indent + 2, st)
4335 )),
4336 _ => None,
4337 })
4338 .collect();
4339 self.render_object(&inner, "URLSearchParams ", indent, st)
4340 }
4341 // A typed array renders as `Uint8Array(3) [ 1, 2, 3 ]` — its
4342 // constructor and length, then the elements laid out exactly as
4343 // an array's. Without this it fell through to the generic object
4344 // arm and printed the `{ length, byteLength, byteOffset,
4345 // BYTES_PER_ELEMENT }` bookkeeping instead of the CONTENTS,
4346 // which is the whole reason anyone logs one.
4347 Some(JsObj::Object(props))
4348 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4349 == Some("TypedArray") =>
4350 {
4351 let kind = props
4352 .get("@@kind")
4353 .map(|k| self.str_of(k))
4354 .unwrap_or_else(|| "TypedArray".into());
4355 // Rendered as STRINGS: a 64-bit view's elements are BigInts,
4356 // which this shared borrow cannot allocate as values.
4357 let elems = crate::stdlib::typedarray::elems_display(self, v);
4358 // The grid layout sizes its columns from the VALUES; a
4359 // 64-bit view's come back as `undefined` (no allocation is
4360 // possible here), which only affects column padding.
4361 let vals = crate::stdlib::typedarray::elems_with_host(self, v);
4362 let base = format!("{kind}({}) ", elems.len());
4363 if indent as i64 > inspect_indent_limit() {
4364 return format!("[{kind}]");
4365 }
4366 let shown = elems.len().min(inspect_max_array_length());
4367 let mut inner: Vec<String> = elems[..shown].to_vec();
4368 let remaining = elems.len() - shown;
4369 if remaining > 0 {
4370 let unit = if remaining == 1 { "item" } else { "items" };
4371 inner.push(format!("... {remaining} more {unit}"));
4372 }
4373 // A view's whole identity — its element width, its window
4374 // onto the backing store, and the store itself — is
4375 // non-enumerable, so `showHidden` is the only way to see it.
4376 // `util.format('%o', view)` goes through here, since `%o`
4377 // implies `showHidden`.
4378 let show_hidden = inspect_show_hidden();
4379 if show_hidden {
4380 let bpe = crate::stdlib::typedarray::bytes_per_element(&kind);
4381 let byte_offset = props
4382 .get("byteOffset")
4383 .map(|x| self.to_number(x))
4384 .unwrap_or(0.0);
4385 inner.push(format!("[BYTES_PER_ELEMENT]: {bpe}"));
4386 inner.push(format!("[length]: {}", elems.len()));
4387 inner.push(format!("[byteLength]: {}", elems.len() * bpe));
4388 inner.push(format!("[byteOffset]: {}", fmt_number(byte_offset)));
4389 // An ArrayBuffer reached AS a view's backing store is
4390 // rendered by node WITHOUT its contents — just
4391 // `ArrayBuffer { [byteLength]: N }` — even though the
4392 // same buffer inspected on its own leads with
4393 // `[Uint8Contents]`. Recursing through the normal
4394 // ArrayBuffer branch therefore printed the bytes twice,
4395 // once as the view's elements and again as the store's.
4396 let buf_len = props
4397 .get("@@buffer")
4398 .and_then(|b| self.get(b))
4399 .and_then(|o| match o {
4400 JsObj::Object(bp) => bp.get("@@bytes").cloned(),
4401 _ => None,
4402 })
4403 .and_then(|b| {
4404 self.get(&b).map(|o| match o {
4405 JsObj::Array(items) => items.len(),
4406 _ => 0,
4407 })
4408 })
4409 .unwrap_or(0);
4410 inner.push(format!(
4411 "[buffer]: ArrayBuffer {{ [byteLength]: {buf_len} }}"
4412 ));
4413 }
4414 self.render_array(
4415 &inner,
4416 &vals,
4417 indent,
4418 ArrayLayout {
4419 has_props: show_hidden,
4420 has_tail: remaining > 0,
4421 base: &base,
4422 },
4423 st,
4424 )
4425 }
4426 // An `ArrayBuffer` renders its CONTENTS, which is the only way
4427 // to see them — it exposes no indices of its own:
4428 // `ArrayBuffer { [Uint8Contents]: <00 01>, [byteLength]: 2 }`.
4429 Some(JsObj::Object(props))
4430 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4431 == Some("ArrayBuffer") =>
4432 {
4433 let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4434 Some(JsObj::Array(items)) => {
4435 items.iter().map(|x| self.to_number(x) as u8).collect()
4436 }
4437 _ => Vec::new(),
4438 };
4439 let hex: Vec<String> = bytes.iter().map(|b| format!("{b:02x}")).collect();
4440 let mut parts = vec![
4441 format!("[Uint8Contents]: <{}>", hex.join(" ")),
4442 format!("[byteLength]: {}", bytes.len()),
4443 ];
4444 if props.contains_key("@@maxByteLength") {
4445 let max = props
4446 .get("@@maxByteLength")
4447 .map(|m| self.to_number(m))
4448 .unwrap_or(0.0);
4449 parts.insert(1, format!("maxByteLength: {}", fmt_number(max)));
4450 }
4451 self.render_object(&parts, "ArrayBuffer ", indent, st)
4452 }
4453 // A `Date` renders as its ISO-8601 form. Its time value lives in
4454 // the internal `@@ms` slot, which the generic object branch below
4455 // does not show, so without this arm every Date printed as `{}` —
4456 // including through `console.log(d)`, inside arrays, objects and
4457 // Maps, and in an `assert` diff.
4458 Some(JsObj::Object(props))
4459 if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Date") =>
4460 {
4461 let base = crate::stdlib::date::inspect_with_host(self, v);
4462 // Own properties added to a Date follow the date itself, the
4463 // way node appends them: `2020-01-01T00:00:00.000Z { x: 1 }`.
4464 let extra = self.side_table_parts(v, indent, st);
4465 let mut inner: Vec<String> = props
4466 .iter()
4467 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
4468 .map(|(k, val)| format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)))
4469 .collect();
4470 inner.extend(extra);
4471 if inner.is_empty() {
4472 return base;
4473 }
4474 self.render_object(&inner, &format!("{base} "), indent, st)
4475 }
4476 // A `Buffer` renders as `<Buffer 01 02 03>` — hex bytes, capped
4477 // at 50 with a `... N more byte(s)` tail, exactly as
4478 // `util.inspect` does. Without this a `console.log(buf)` (the
4479 // single most common thing anyone does with a Buffer) printed
4480 // the internal `{ length, byteLength, … }` bookkeeping.
4481 Some(JsObj::Object(props))
4482 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4483 == Some("Buffer") =>
4484 {
4485 let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4486 Some(JsObj::Array(items)) => {
4487 items.iter().map(|x| self.to_number(x) as u8).collect()
4488 }
4489 _ => Vec::new(),
4490 };
4491 // `<Buffer …>` is Buffer's `[util.inspect.custom]` hook, not
4492 // the shape of the object. Under `customInspect: false` node
4493 // does not call that hook and falls back to the generic
4494 // byte-view rendering — which is what an `assert` diff shows,
4495 // since assert inspects with the hook disabled so that a
4496 // failure names the differing BYTE rather than two opaque hex
4497 // blobs. The constructor is `Buffer` while the brand is still
4498 // `Uint8Array`, so node prints both.
4499 if !inspect_custom() {
4500 let base = format!("Buffer({}) [Uint8Array] ", bytes.len());
4501 if indent as i64 > inspect_indent_limit() {
4502 return "[Buffer [Uint8Array]]".into();
4503 }
4504 let shown = bytes.len().min(inspect_max_array_length());
4505 let mut inner: Vec<String> =
4506 bytes[..shown].iter().map(|b| b.to_string()).collect();
4507 let vals: Vec<Value> = bytes[..shown]
4508 .iter()
4509 .map(|b| Value::Float(*b as f64))
4510 .collect();
4511 let remaining = bytes.len() - shown;
4512 if remaining > 0 {
4513 let unit = if remaining == 1 { "item" } else { "items" };
4514 inner.push(format!("... {remaining} more {unit}"));
4515 }
4516 return self.render_array(
4517 &inner,
4518 &vals,
4519 indent,
4520 ArrayLayout {
4521 has_props: false,
4522 has_tail: remaining > 0,
4523 base: &base,
4524 },
4525 st,
4526 );
4527 }
4528 const MAX: usize = 50;
4529 let shown: Vec<String> =
4530 bytes.iter().take(MAX).map(|b| format!("{b:02x}")).collect();
4531 let mut out = format!("<Buffer {}", shown.join(" "));
4532 if bytes.len() > MAX {
4533 let more = bytes.len() - MAX;
4534 let unit = if more == 1 { "byte" } else { "bytes" };
4535 out.push_str(&format!(" ... {more} more {unit}"));
4536 }
4537 out.push('>');
4538 out
4539 }
4540 // An Error inspects as its `.stack` — never as an object literal
4541 // exposing the internal `message`/`stack` slots. Any own property
4542 // a script added beyond those follows in braces, as V8 renders
4543 // it: `Error: x\n at … { code: 'C' }`.
4544 Some(JsObj::Object(_)) if self.error_to_string(v).is_some() => {
4545 let mut stack = lookup_chain(self, v, "stack")
4546 .map(|s| self.str_of(&s))
4547 .unwrap_or_else(|| self.error_to_string(v).unwrap_or_default());
4548 // A `DOMException` prints its CLASS and then its name —
4549 // `DOMException [AbortError]: m` — where a plain error
4550 // prints only its stack head.
4551 if let Some(JsObj::Object(p)) = self.get(v) {
4552 if let Some(n) = p.get("@@domName") {
4553 let name = self.str_of(n);
4554 stack = format!(
4555 "DOMException [{name}]{}",
4556 stack.strip_prefix(&name).unwrap_or(&stack)
4557 );
4558 }
4559 }
4560 let extra: Vec<String> = self
4561 .own_enum_key_names(v)
4562 .into_iter()
4563 .filter(|k| k != "name")
4564 .map(|k| {
4565 let val = self.fn_prop(v, &k).unwrap_or_else(|| match self.get(v) {
4566 Some(JsObj::Object(p)) => {
4567 p.get(&k).cloned().unwrap_or(Value::Undef)
4568 }
4569 _ => Value::Undef,
4570 });
4571 format!(
4572 "{}: {}",
4573 fmt_key(&k),
4574 self.inspect_lvl(&val, indent + 2, st)
4575 )
4576 })
4577 .collect();
4578 if extra.is_empty() {
4579 stack
4580 } else {
4581 format!("{stack} {{ {} }}", extra.join(", "))
4582 }
4583 }
4584 Some(JsObj::Object(props)) => {
4585 // Instances print with their constructor name as a prefix
4586 // (`C { x: 1 }`); plain objects have none; a null-prototype
4587 // object (e.g. an `Object.groupBy` result) is tagged
4588 // `[Object: null prototype]`.
4589 let ctor = match self.ctor_name(v) {
4590 n if n.is_empty() => "Object".to_string(),
4591 n => n,
4592 };
4593 let plain_prefix = if ctor == "Object" {
4594 String::new()
4595 } else {
4596 format!("{ctor} ")
4597 };
4598 let prefix = if self.inspects_null_proto(v) {
4599 "[Object: null prototype] ".to_string()
4600 } else {
4601 // An inherited `Symbol.toStringTag` shows as `Ctor [Tag] `.
4602 match self.inspect_tag(v) {
4603 Some(t) if t != ctor => format!("{ctor} [{t}] "),
4604 _ => plain_prefix.clone(),
4605 }
4606 };
4607 // Skip node-js's internal slots (`@@native`, `@@bytes`, …) and
4608 // private class fields; a real symbol-keyed own property is a
4609 // visible one and renders as `Symbol(desc): value`.
4610 // An own ACCESSOR has no value to print: node shows the
4611 // label `[Getter]` / `[Setter]` / `[Getter/Setter]` in its
4612 // place. It is found through the `@@ord:` marker the
4613 // property map holds for it, which is also what puts it in
4614 // declaration order among the data properties. Without this
4615 // an accessor rendered as nothing at all — `{ get z(){} }`
4616 // printed `{}`.
4617 let mut shown: Vec<(String, Result<&Value, &'static str>)> = props
4618 .iter()
4619 .filter_map(|(k, val)| match k.strip_prefix(ORD_MARKER) {
4620 Some(real) => {
4621 let attrs = self.prop_attrs(v, real);
4622 let label = match self.own_accessor(v, real)? {
4623 (Some(_), Some(_)) => "[Getter/Setter]",
4624 (Some(_), None) => "[Getter]",
4625 (None, Some(_)) => "[Setter]",
4626 (None, None) => return None,
4627 };
4628 attrs.enumerable.then(|| (fmt_key(real), Err(label)))
4629 }
4630 // Only an ENUMERABLE own property is shown, as node
4631 // does: a native instance keeps bookkeeping (a
4632 // `URLSearchParams`'s `size`) as a hidden own slot,
4633 // and printing it would report a spec getter as data.
4634 None if !k.starts_with("@@")
4635 && !k.starts_with('#')
4636 && self.prop_attrs(v, k).enumerable =>
4637 {
4638 Some((fmt_key(k), Ok(val)))
4639 }
4640 None => None,
4641 })
4642 .collect();
4643 shown.extend(props.iter().filter_map(|(k, val)| {
4644 let sym = self.symbol_of_key(k)?;
4645 self.prop_attrs(v, k)
4646 .enumerable
4647 .then(|| (self.inspect(&sym), Ok(val)))
4648 }));
4649 if shown.is_empty() {
4650 return format!("{prefix}{{}}");
4651 }
4652 // Depth limit (Node default 2): deeper objects collapse to
4653 // `[Object]` (or `[ClassName]` for a named instance).
4654 if indent as i64 > inspect_indent_limit() {
4655 return if self.inspects_null_proto(v) {
4656 // Already bracketed (`[Object: null prototype]`).
4657 prefix.trim_end().to_string()
4658 } else if plain_prefix.is_empty() {
4659 "[Object]".into()
4660 } else {
4661 format!("[{}]", plain_prefix.trim_end())
4662 };
4663 }
4664 let inner: Vec<String> = shown
4665 .iter()
4666 .map(|(k, val)| match val {
4667 Ok(val) => format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)),
4668 Err(label) => format!("{k}: {label}"),
4669 })
4670 .collect();
4671 self.render_object(&inner, &prefix, indent, st)
4672 }
4673 Some(JsObj::Symbol { desc, .. }) => match desc {
4674 Some(d) => format!("Symbol({d})"),
4675 None => "Symbol()".into(),
4676 },
4677 Some(JsObj::Class(c)) => {
4678 let base = if c.parent.is_some() {
4679 let pname = c
4680 .parent
4681 .as_ref()
4682 .map(|p| self.callable_name(p))
4683 .unwrap_or_default();
4684 format!("[class {} extends {}]", c.name, pname)
4685 } else {
4686 format!("[class {}]", c.name)
4687 };
4688 self.with_callable_props(v, base, indent, st)
4689 }
4690 // A Map/Set renders its members at the NEXT nesting level, and
4691 // collapses to `[Map]`/`[Set]` past the depth limit exactly as an
4692 // array collapses to `[Array]`. Both used to recurse through
4693 // `inspect`, which restarts at indent 0, so the depth gate never
4694 // fired: nesting printed one level too deep at every depth
4695 // (measured on node v26.7.0, four nested Maps print
4696 // `Map(1) { 'a' => Map(1) { 'b' => Map(1) { 'c' => [Map] } } }`),
4697 // and a SELF-referential Map or Set recursed forever and aborted
4698 // the process — `const m=new Map(); m.set('m',m); console.log(m)`
4699 // died with `fatal runtime error: stack overflow`, which no
4700 // `try`/`catch` can see. An empty one still prints in full at any
4701 // depth, as `[]`/`{}` do.
4702 // A WEAK collection never shows its contents: node prints
4703 // `WeakMap { <items unknown> }` whether it holds anything or
4704 // not, because the entries are not enumerable by design.
4705 Some(JsObj::Map { weak: true, .. }) => "WeakMap { <items unknown> }".into(),
4706 Some(JsObj::Set { weak: true, .. }) => "WeakSet { <items unknown> }".into(),
4707 Some(JsObj::Map { entries, .. }) => {
4708 let extra = self.side_table_parts(v, indent, st);
4709 if entries.is_empty() && extra.is_empty() {
4710 return "Map(0) {}".into();
4711 }
4712 if indent as i64 > inspect_indent_limit() {
4713 return "[Map]".into();
4714 }
4715 let mut inner: Vec<String> = entries
4716 .values()
4717 .map(|(k, val)| {
4718 // Sequenced, not nested in one `format!`: both arms
4719 // need the same `&mut` cycle state.
4720 let ks = self.inspect_lvl(k, indent + 2, st);
4721 let vs = self.inspect_lvl(val, indent + 2, st);
4722 format!("{ks} => {vs}")
4723 })
4724 .collect();
4725 inner.extend(extra);
4726 // Laid out by the SAME routine as a plain object, not joined
4727 // onto one line unconditionally. `Map`/`Set` were the only
4728 // containers that never consulted `breakLength` or `compact`,
4729 // so every collection wide enough to wrap printed as one long
4730 // line: node breaks a seven-member Set of ten-character
4731 // strings across seven lines, and `util.inspect(m, {compact:
4732 // false})` — which assert's own diff renderer depends on —
4733 // could not break a Map at all. Node builds these through
4734 // `reduceToSingleString` with `braces[0]` of `Map(n) {`, which
4735 // is this `prefix` (the trailing space is the brace gap).
4736 let prefix = format!("Map({}) ", entries.len());
4737 self.render_object(&inner, &prefix, indent, st)
4738 }
4739 Some(JsObj::Set { entries, .. }) => {
4740 let extra = self.side_table_parts(v, indent, st);
4741 if entries.is_empty() && extra.is_empty() {
4742 return "Set(0) {}".into();
4743 }
4744 if indent as i64 > inspect_indent_limit() {
4745 return "[Set]".into();
4746 }
4747 let mut inner: Vec<String> = entries
4748 .values()
4749 .map(|v| self.inspect_lvl(v, indent + 2, st))
4750 .collect();
4751 inner.extend(extra);
4752 // Same layout routine as a Map (see above). Note node does
4753 // NOT column-group a wide Set the way it grids an array:
4754 // `groupArrayElements` is reached only from the list
4755 // formatter, so a 30-member Set is thirty lines.
4756 let prefix = format!("Set({}) ", entries.len());
4757 self.render_object(&inner, &prefix, indent, st)
4758 }
4759 Some(JsObj::Generator { .. }) => "Object [Generator] {}".into(),
4760 Some(JsObj::Promise { id }) => match self.promises.get(*id as usize) {
4761 Some(c) => match c.state {
4762 PromiseState::Pending => "Promise { <pending> }".into(),
4763 PromiseState::Fulfilled => {
4764 format!("Promise {{ {} }}", self.inspect_lvl(&c.value, 0, st))
4765 }
4766 PromiseState::Rejected => {
4767 format!(
4768 "Promise {{ <rejected> {} }}",
4769 self.inspect_lvl(&c.value, 0, st)
4770 )
4771 }
4772 },
4773 None => "Promise { <pending> }".into(),
4774 },
4775 Some(JsObj::Func(_)) => {
4776 // `callable_name`, not the FuncDef name: an anonymous
4777 // function expression gets its name by inference from the
4778 // binding it initialises (`const f = function(){}`), and
4779 // that lands as an own `name` property.
4780 let name = self.callable_name(v);
4781 let base = if name.is_empty() {
4782 "[Function (anonymous)]".to_string()
4783 } else {
4784 format!("[Function: {name}]")
4785 };
4786 self.with_callable_props(v, base, indent, st)
4787 }
4788 Some(JsObj::Builtin(n)) => {
4789 // A namespace object is not a function and must not be
4790 // printed as one. The three ECMAScript namespaces carry a
4791 // `Symbol.toStringTag` and inspect as `Object [Math] {}`;
4792 // their members are all non-enumerable, so the braces really
4793 // are empty. A `require()`d module namespace has no tag and
4794 // node prints its members, which cannot be rendered here —
4795 // formatting a member means allocating its value, and this
4796 // runs under the host borrow.
4797 if !builtin_is_callable(n) {
4798 match crate::builtins::well_known_tag(self, v) {
4799 Some(tag) => format!("Object [{tag}] {{}}"),
4800 // `Set.prototype` inspects under the CONSTRUCTOR's
4801 // name, not the key: node prints `Object [Set] {}`.
4802 None => {
4803 format!("Object [{}] {{}}", n.trim_end_matches(".prototype"))
4804 }
4805 }
4806 } else {
4807 format!("[Function: {}]", crate::builtins::builtin_name(n))
4808 }
4809 }
4810 // A bound method is not anonymous: it is the prototype method it
4811 // resolves to, so `console.log(new Uint8Array(1).set)` reports
4812 // `[Function: set]`.
4813 Some(JsObj::BoundMethod { name, .. }) => format!("[Function: {name}]"),
4814 Some(JsObj::BoundFunc { target, .. }) => {
4815 let n = self.callable_name(target);
4816 if n.is_empty() {
4817 "[Function: bound ]".into()
4818 } else {
4819 format!("[Function: bound {n}]")
4820 }
4821 }
4822 _ => "undefined".into(),
4823 },
4824 _ => "undefined".into(),
4825 }
4826 }
4827
4828 /// Append a callable's own enumerable properties to its `[Function: f]` /
4829 /// `[class C]` base, the way `util.inspect` does: `[Function: f] { a: 1 }`.
4830 /// A callable with none renders as the bare base.
4831 fn with_callable_props(
4832 &self,
4833 v: &Value,
4834 base: String,
4835 indent: usize,
4836 st: &mut InspectCycles,
4837 ) -> String {
4838 let mut inner: Vec<String> = self
4839 .own_enum_key_names(v)
4840 .into_iter()
4841 .map(|k| {
4842 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4843 format!(
4844 "{}: {}",
4845 fmt_key(&k),
4846 self.inspect_lvl(&val, indent + 2, st)
4847 )
4848 })
4849 .collect();
4850 for (k, val) in self.own_symbol_entries(v) {
4851 if let Some(sym) = self.symbol_of_key(&k) {
4852 inner.push(format!(
4853 "{}: {}",
4854 self.inspect(&sym),
4855 self.inspect_lvl(&val, indent + 2, st)
4856 ));
4857 }
4858 }
4859 if inner.is_empty() {
4860 return base;
4861 }
4862 self.render_object(&inner, &format!("{base} "), indent, st)
4863 }
4864
4865 /// Render a non-empty array's already-formatted element strings, applying
4866 /// Node's `util.inspect` layout: a single line when it fits, else a multi-line
4867 /// grid via `groupArrayElements` (for >6 entries), else one element per line.
4868 /// `values` is the raw element list (drives numeric right-alignment); `indent`
4869 /// is the array's own indentation level.
4870 fn render_array(
4871 &self,
4872 output: &[String],
4873 values: &[Value],
4874 indent: usize,
4875 opts: ArrayLayout<'_>,
4876 st: &InspectCycles,
4877 ) -> String {
4878 let ArrayLayout {
4879 has_props,
4880 has_tail,
4881 base,
4882 } = opts;
4883 // Group array elements together if the array has more than six entries.
4884 // Arrays carrying extra own props (`index`/`input`/… on a match result)
4885 // are never grid-grouped — Node lays those out plainly.
4886 // `compact: false` (held as 0) also turns the GRID off, not just the
4887 // single-line join. Node reaches `groupArrayElements` only under
4888 // `ctx.compact >= 1`, so `util.inspect(arr, { compact: false })` is one
4889 // element per line however many there are; without this gate a 30-element
4890 // array still came back column-aligned in three rows, which is the form
4891 // assert's diff renderer splits on — every array diff would have been
4892 // computed over grid rows instead of elements.
4893 let entries = output.len();
4894 let (lines, grouped) = if entries > 6 && !has_props && inspect_compact() >= 1 {
4895 group_array_elements(self, output, values, indent, has_tail)
4896 } else {
4897 (output.to_vec(), false)
4898 };
4899 // A typed array prints its constructor and length ahead of the brackets
4900 // (`Uint8Array(3) [ 1, 2, 3 ]`); node counts that as `base` in the
4901 // break-length seed, so a long tag wraps the list one entry sooner.
4902 if output.is_empty() {
4903 return format!("{base}[]");
4904 }
4905 // If no grouping happened, try to line everything up on a single line.
4906 if !grouped {
4907 // start = output.length + indentationLvl + braces[0].len(1) + base + 10
4908 let start = output.len() + indent + 1 + base.chars().count() + 10;
4909 if self.may_compact(indent, st) && is_below_break_length(output, start) {
4910 return format!("{base}[ {} ]", output.join(", "));
4911 }
4912 }
4913 // Otherwise: one (grouped or single) entry per line, indented by indent+2.
4914 let pad = " ".repeat(indent);
4915 let sep = format!(",\n{pad} ");
4916 format!("{base}[\n{pad} {}\n{pad}]", lines.join(&sep))
4917 }
4918
4919 /// Render a non-empty object's already-formatted `key: value` strings with
4920 /// Node's `util.inspect` layout: a single line when it fits `breakLength`,
4921 /// else one property per line indented by `indent + 2`. `prefix` is the
4922 /// constructor/`[Object: null prototype]` tag (with trailing space) or empty.
4923 /// Mirrors `render_array`'s break decision, including the `compact` depth
4924 /// gate.
4925 /// Whether a group at `indent` may be joined onto one line.
4926 ///
4927 /// Node's `reduceToSingleString`: only while the subtree below this group is
4928 /// SHALLOWER than `compact` (default 3). `compact: false` is held as 0, so
4929 /// nothing qualifies and every group breaks.
4930 fn may_compact(&self, indent: usize, st: &InspectCycles) -> bool {
4931 let compact = inspect_compact();
4932 if compact < 1 {
4933 return false;
4934 }
4935 // Levels, not columns: the indent advances by two per level.
4936 let depth_below = (st.deepest.saturating_sub(indent)) / 2;
4937 (depth_below as i64) < compact
4938 }
4939
4940 fn render_object(
4941 &self,
4942 output: &[String],
4943 prefix: &str,
4944 indent: usize,
4945 st: &InspectCycles,
4946 ) -> String {
4947 // start = output.length + indentationLvl + braces[0].len + base(0) + 10.
4948 // For a tagged object Node folds the tag into `braces[0]` (e.g.
4949 // `"Point {"`, `"[Object: null prototype] {"`), so its length is the
4950 // prefix (which carries the trailing space) plus the `{`.
4951 // `sorted: true` orders the RENDERED entries, not the keys. Node sorts
4952 // the finished `key: value` strings (`output.sort()` in `formatRaw` for
4953 // the object shape), which is observably different from sorting keys
4954 // whenever a key needs quoting — `'b-b': 1` sorts under `'`, not `b`.
4955 // `assert`'s diff renderer depends on this: without it two objects
4956 // carrying the same properties in a different insertion order diffed as
4957 // a wholesale rewrite of every line instead of as equal.
4958 let sorted_output;
4959 let output = if inspect_sorted() {
4960 let mut v = output.to_vec();
4961 v.sort();
4962 sorted_output = v;
4963 &sorted_output[..]
4964 } else {
4965 output
4966 };
4967 let braces0 = prefix.chars().count() + 1;
4968 let start = output.len() + indent + braces0 + 10;
4969 if self.may_compact(indent, st) && is_below_break_length(output, start) {
4970 return format!("{prefix}{{ {} }}", output.join(", "));
4971 }
4972 let pad = " ".repeat(indent);
4973 let sep = format!(",\n{pad} ");
4974 format!("{prefix}{{\n{pad} {}\n{pad}}}", output.join(&sep))
4975 }
4976
4977 /// The `.name` of any callable (function/class/builtin/bound).
4978 pub fn callable_name(&self, v: &Value) -> String {
4979 // A user-set `.name` own property wins.
4980 if let Some(n) = self.fn_prop(v, "name") {
4981 return self.str_of(&n);
4982 }
4983 match self.get(v) {
4984 Some(JsObj::Func(f)) => self
4985 .funcs
4986 .get(f.def_id)
4987 .map(|d| d.name.clone())
4988 .unwrap_or_default(),
4989 Some(JsObj::Class(c)) => c.name.clone(),
4990 // Not the whole key: a builtin's `.name` is its last segment, and a
4991 // prototype thunk's key is `@proto:<Ctor>:<method>` — which has no
4992 // `.` at all, so this reported the internal spelling verbatim and
4993 // `console.log(Uint8Array.prototype.set)` printed
4994 // `[Function: @proto:TypedArray:set]`.
4995 Some(JsObj::Builtin(n)) => crate::builtins::builtin_name(n).to_string(),
4996 Some(JsObj::BoundFunc { target, .. }) => {
4997 format!("bound {}", self.callable_name(target))
4998 }
4999 Some(JsObj::BoundMethod { name, .. }) => name.clone(),
5000 _ => String::new(),
5001 }
5002 }
5003
5004 // ── equality / comparison / arithmetic (numeric-hook + builtin paths) ──
5005
5006 /// Strict equality (`===`): same type and same value, no coercion.
5007 pub fn strict_eq(&self, a: &Value, b: &Value) -> bool {
5008 match (a, b) {
5009 (Value::Undef, Value::Undef) => true,
5010 (Value::Bool(x), Value::Bool(y)) => x == y,
5011 (Value::Str(x), Value::Str(y)) => x == y,
5012 _ => {
5013 // Numbers (NaN !== NaN, +0 === -0).
5014 let an = matches!(a, Value::Int(_) | Value::Float(_));
5015 let bn = matches!(b, Value::Int(_) | Value::Float(_));
5016 if an && bn {
5017 let x = self.to_number(a);
5018 let y = self.to_number(b);
5019 return x == y;
5020 }
5021 // BigInt === BigInt compares by value (each literal is a distinct
5022 // heap cell, so reference identity would be wrong). BigInt is never
5023 // `===` a Number (different types).
5024 if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5025 return x == y;
5026 }
5027 // Heap values.
5028 if let (Some(sa), Some(sb)) = (self.as_str(a), self.as_str(b)) {
5029 return sa == sb;
5030 }
5031 let na = self.is_null(a);
5032 let nb = self.is_null(b);
5033 if na || nb {
5034 return na && nb;
5035 }
5036 // A builtin namespace/constructor/prototype is a SINGLETON in JS
5037 // (`Math === Math`, `Array.prototype === Array.prototype`), but
5038 // every bare reference here allocates a fresh handle, so compare
5039 // those by name rather than by heap index.
5040 if let (Some(JsObj::Builtin(x)), Some(JsObj::Builtin(y))) =
5041 (self.get(a), self.get(b))
5042 {
5043 return x == y;
5044 }
5045 // Reference identity for arrays/objects/functions.
5046 matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
5047 }
5048 }
5049 }
5050
5051 /// Whether `v` is `null` or `undefined`.
5052 pub fn is_nullish(&self, v: &Value) -> bool {
5053 matches!(v, Value::Undef) || self.is_null(v)
5054 }
5055
5056 /// The ECMAScript "loose type" of `v` for the `==` algorithm: `"number"`,
5057 /// `"string"` (primitive or heap string), `"boolean"`, `"undefined"`,
5058 /// `"null"`, or `"object"` (array / plain object / function).
5059 fn js_type(&self, v: &Value) -> &'static str {
5060 match v {
5061 Value::Undef => "undefined",
5062 Value::Bool(_) => "boolean",
5063 Value::Int(_) | Value::Float(_) => "number",
5064 Value::Str(_) => "string",
5065 Value::Obj(_) => match self.get(v) {
5066 Some(JsObj::Str(_)) => "string",
5067 Some(JsObj::Null) => "null",
5068 Some(JsObj::BigInt(_)) => "bigint",
5069 _ => "object",
5070 },
5071 _ => "object",
5072 }
5073 }
5074
5075 /// Loose equality (`==`) following the ECMAScript Abstract Equality Comparison.
5076 /// Objects reduce via `ToPrimitive` (which for our heap objects is always their
5077 /// string `toString`), so `[0] == "0"` is `true` (string compare of `"0"`) but
5078 /// `[0] == ""` is `false` — never a number coercion of the object.
5079 pub fn loose_eq(&self, a: &Value, b: &Value) -> bool {
5080 // Same type: identical to `===` (number==number, string==string, etc.).
5081 if self.strict_eq(a, b) {
5082 return true;
5083 }
5084 let ta = self.js_type(a);
5085 let tb = self.js_type(b);
5086 // null and undefined are loosely equal only to each other.
5087 if self.is_nullish(a) || self.is_nullish(b) {
5088 return self.is_nullish(a) && self.is_nullish(b);
5089 }
5090 // BigInt ⇄ (Number | String | Boolean | Object): compare mathematical
5091 // values (both-BigInt was already settled by the `strict_eq` above).
5092 if ta == "bigint" || tb == "bigint" {
5093 return self.bigint_loose_eq(a, b);
5094 }
5095 if ta == tb {
5096 // Same type but not strict-equal (and not nullish) ⇒ not equal.
5097 return false;
5098 }
5099 // number ⇄ string: compare as numbers.
5100 if (ta == "number" && tb == "string") || (ta == "string" && tb == "number") {
5101 return self.to_number(a) == self.to_number(b);
5102 }
5103 // boolean side coerces to number, then recompares.
5104 if ta == "boolean" {
5105 return self.loose_eq(&Value::Float(self.to_number(a)), b);
5106 }
5107 if tb == "boolean" {
5108 return self.loose_eq(a, &Value::Float(self.to_number(b)));
5109 }
5110 // object ⇄ (number|string): ToPrimitive the object (→ its string form),
5111 // then recompare as string==string or number==string.
5112 if ta == "object" && (tb == "number" || tb == "string") {
5113 let pa = self.str_of(a);
5114 return if tb == "string" {
5115 pa == self.str_of(b)
5116 } else {
5117 str_to_number(&pa) == self.to_number(b)
5118 };
5119 }
5120 if tb == "object" && (ta == "number" || ta == "string") {
5121 let pb = self.str_of(b);
5122 return if ta == "string" {
5123 self.str_of(a) == pb
5124 } else {
5125 self.to_number(a) == str_to_number(&pb)
5126 };
5127 }
5128 false
5129 }
5130
5131 /// The numeric-hook arithmetic/relational fallback for non-native operands
5132 /// (called by fusevm when at least one operand isn't `Int`/`Float`).
5133 pub fn arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5134 use NumOp::*;
5135 match op {
5136 Add => {
5137 // `+`: if either operand is a string, concatenate string forms;
5138 // otherwise numeric addition.
5139 let a_str = self.prefers_string(a);
5140 let b_str = self.prefers_string(b);
5141 if a_str || b_str {
5142 // String concatenation wins even with a bigint operand
5143 // (`1n + "x"` → `"1x"`).
5144 let s = format!("{}{}", self.str_of(a), self.str_of(b));
5145 Ok(self.new_str(s))
5146 } else if self.is_bigint_val(a) || self.is_bigint_val(b) {
5147 self.bigint_arith(op, a, b)
5148 } else {
5149 Ok(Value::Float(self.to_number(a) + self.to_number(b)))
5150 }
5151 }
5152 Sub | Mul | Div | Mod | Pow if self.is_bigint_val(a) || self.is_bigint_val(b) => {
5153 self.bigint_arith(op, a, b)
5154 }
5155 Sub => Ok(Value::Float(self.to_number(a) - self.to_number(b))),
5156 Mul => Ok(Value::Float(self.to_number(a) * self.to_number(b))),
5157 Div => Ok(Value::Float(self.to_number(a) / self.to_number(b))),
5158 Mod => Ok(Value::Float(js_mod(self.to_number(a), self.to_number(b)))),
5159 Pow => Ok(Value::Float(crate::builtins::js_pow(
5160 self.to_number(a),
5161 self.to_number(b),
5162 ))),
5163 Neg if self.is_bigint_val(a) => self.bigint_arith(op, a, b),
5164 Neg => Ok(Value::Float(-self.to_number(a))),
5165 Lt | Le | Gt | Ge => Ok(Value::Bool(self.relational(op, a, b))),
5166 Eq => Ok(Value::Bool(self.loose_eq(a, b))),
5167 Ne => Ok(Value::Bool(!self.loose_eq(a, b))),
5168 }
5169 }
5170
5171 /// Whether `v`'s primitive (`ToPrimitive` with the default hint) is a string,
5172 /// which drives `+` toward concatenation. Primitive strings qualify, and so
5173 /// do heap objects whose default `ToPrimitive` is their (string) `toString`:
5174 /// arrays (`[1,2,3]+3 → "1,2,33"`), plain objects (`{}+[] → "[object Object]"`),
5175 /// and functions. `null`/`undefined`/`boolean`/`number` do not.
5176 fn prefers_string(&self, v: &Value) -> bool {
5177 match v {
5178 Value::Str(_) => true,
5179 // A BigInt's `ToPrimitive` is the bigint itself (numeric), NOT a string,
5180 // so `1n + 2n` is bigint addition, not concatenation. `null` has no
5181 // string primitive either.
5182 Value::Obj(_) => !matches!(
5183 self.get(v),
5184 Some(JsObj::Null) | Some(JsObj::BigInt(_)) | None
5185 ),
5186 _ => false,
5187 }
5188 }
5189
5190 /// Relational comparison (`< <= > >=`) with JS coercion: string/string is
5191 /// lexicographic, otherwise numeric (NaN yields false).
5192 fn relational(&self, op: NumOp, a: &Value, b: &Value) -> bool {
5193 use std::cmp::Ordering;
5194 let ord = if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5195 // BigInt < BigInt: exact (no f64 precision loss for large magnitudes).
5196 x.cmp(&y)
5197 } else if let (Some(x), Some(y)) = (self.as_str(a), self.as_str(b)) {
5198 // 7.2.13 IsLessThan compares CODE UNITS, which is not Rust's `str`
5199 // order once an astral character meets a BMP one — see `utf16`.
5200 crate::utf16::cmp_units(&x, &y)
5201 } else {
5202 let x = self.to_number(a);
5203 let y = self.to_number(b);
5204 match x.partial_cmp(&y) {
5205 Some(o) => o,
5206 None => return false, // NaN operand
5207 }
5208 };
5209 match op {
5210 NumOp::Lt => ord == Ordering::Less,
5211 NumOp::Le => ord != Ordering::Greater,
5212 NumOp::Gt => ord == Ordering::Greater,
5213 NumOp::Ge => ord != Ordering::Less,
5214 _ => false,
5215 }
5216 }
5217
5218 /// Bitwise/shift ops with JS ToInt32/ToUint32 semantics — or true
5219 /// arbitrary-width BigInt bitwise when both operands are BigInt (mixing a
5220 /// BigInt with a Number throws, matching Node).
5221 pub fn bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5222 if self.is_bigint_val(a) || self.is_bigint_val(b) {
5223 return self.bigint_bitwise(tag, a, b);
5224 }
5225 let x = to_int32(self.to_number(a));
5226 let y = to_int32(self.to_number(b));
5227 let r: i64 = match tag {
5228 binop::BITAND => (x & y) as i64,
5229 binop::BITOR => (x | y) as i64,
5230 binop::BITXOR => (x ^ y) as i64,
5231 binop::SHL => (x.wrapping_shl((y as u32) & 31)) as i64,
5232 binop::SHR => (x >> ((y as u32) & 31)) as i64,
5233 binop::USHR => (to_uint32(self.to_number(a)) >> ((y as u32) & 31)) as i64,
5234 _ => 0,
5235 };
5236 Ok(Value::Float(r as f64))
5237 }
5238
5239 // ── BigInt operations ────────────────────────────────────────────────────
5240 /// Whether `v` is a heap `BigInt`.
5241 pub fn is_bigint_val(&self, v: &Value) -> bool {
5242 matches!(self.get(v), Some(JsObj::BigInt(_)))
5243 }
5244 /// The `BigInt` value of `v` (a heap bigint), else `None`.
5245 pub fn as_bigint(&self, v: &Value) -> Option<num_bigint::BigInt> {
5246 match self.get(v) {
5247 Some(JsObj::BigInt(b)) => Some(b.clone()),
5248 _ => None,
5249 }
5250 }
5251 /// Allocate a heap `BigInt`.
5252 pub fn new_bigint(&mut self, b: num_bigint::BigInt) -> Value {
5253 self.alloc(JsObj::BigInt(b))
5254 }
5255
5256 /// BigInt arithmetic (`+ - * / % **`, unary `-`). Requires BOTH operands to be
5257 /// BigInt for a binary op; mixing a BigInt with a Number throws the exact Node
5258 /// `TypeError` (a string operand is handled as concatenation before we get
5259 /// here). Division/`%` truncate toward zero; `**` needs a non-negative
5260 /// exponent.
5261 fn bigint_arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5262 use num_traits::{Signed, Zero};
5263 use NumOp::*;
5264 if op == Neg {
5265 let x = self.as_bigint(a).expect("bigint_arith Neg on non-bigint");
5266 return Ok(self.new_bigint(-x));
5267 }
5268 let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5269 (Some(x), Some(y)) => (x, y),
5270 // Exactly one side is a BigInt → the other is a Number/Boolean: illegal.
5271 _ => {
5272 return Err(type_error(
5273 "Cannot mix BigInt and other types, use explicit conversions",
5274 ))
5275 }
5276 };
5277 let r = match op {
5278 Add => x + y,
5279 Sub => x - y,
5280 Mul => x * y,
5281 Div => {
5282 if y.is_zero() {
5283 return Err("RangeError: Division by zero".into());
5284 }
5285 x / y // truncates toward zero (matches JS BigInt division)
5286 }
5287 Mod => {
5288 if y.is_zero() {
5289 return Err("RangeError: Division by zero".into());
5290 }
5291 x % y // sign follows the dividend (truncated), like JS
5292 }
5293 Pow => {
5294 if y.is_negative() {
5295 return Err("RangeError: Exponent must be positive".into());
5296 }
5297 let exp = num_traits::ToPrimitive::to_u32(&y)
5298 .ok_or_else(|| "RangeError: Maximum BigInt size exceeded".to_string())?;
5299 num_traits::Pow::pow(x, exp)
5300 }
5301 _ => return Err(type_error("unsupported BigInt operation")),
5302 };
5303 Ok(self.new_bigint(r))
5304 }
5305
5306 /// BigInt bitwise (`& | ^ << >>`); `>>>` has no BigInt form. Both operands must
5307 /// be BigInt (mixing throws).
5308 fn bigint_bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5309 let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5310 (Some(x), Some(y)) => (x, y),
5311 _ => {
5312 return Err(type_error(
5313 "Cannot mix BigInt and other types, use explicit conversions",
5314 ))
5315 }
5316 };
5317 let r = match tag {
5318 binop::BITAND => x & y,
5319 binop::BITOR => x | y,
5320 binop::BITXOR => x ^ y,
5321 binop::SHL => {
5322 let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5323 if n >= 0 {
5324 x << (n as usize)
5325 } else {
5326 x >> ((-n) as usize)
5327 }
5328 }
5329 binop::SHR => {
5330 let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5331 if n >= 0 {
5332 x >> (n as usize)
5333 } else {
5334 x << ((-n) as usize)
5335 }
5336 }
5337 binop::USHR => {
5338 return Err(type_error(
5339 "BigInts have no unsigned right shift, use >> instead",
5340 ))
5341 }
5342 _ => return Err(type_error("unsupported BigInt operation")),
5343 };
5344 Ok(self.new_bigint(r))
5345 }
5346
5347 /// BigInt ⇄ (Number | Boolean | String | Object) loose equality (`==`). Both
5348 /// being BigInt was already handled by `strict_eq`.
5349 fn bigint_loose_eq(&self, a: &Value, b: &Value) -> bool {
5350 // Order so `big` is the BigInt side and `other` the counterpart.
5351 let (big, other) = match (self.as_bigint(a), self.as_bigint(b)) {
5352 (Some(x), _) => (x, b),
5353 (_, Some(y)) => (y, a),
5354 _ => return false,
5355 };
5356 match other {
5357 Value::Bool(bo) => big == num_bigint::BigInt::from(*bo as i64),
5358 Value::Int(n) => big == num_bigint::BigInt::from(*n),
5359 Value::Float(f) => {
5360 // Equal only when the float is an integer with the same value.
5361 if !f.is_finite() || f.fract() != 0.0 {
5362 return false;
5363 }
5364 bigint_to_f64(&big) == *f
5365 }
5366 Value::Str(s) => match parse_bigint_str(s) {
5367 Some(bs) => big == bs,
5368 None => false,
5369 },
5370 Value::Obj(_) => match self.get(other) {
5371 // A heap string parses like a primitive string.
5372 Some(JsObj::Str(s)) => parse_bigint_str(s).map(|bs| big == bs).unwrap_or(false),
5373 _ => {
5374 // Other objects reduce via ToPrimitive (their string form).
5375 let s = self.str_of(other);
5376 parse_bigint_str(&s).map(|bs| big == bs).unwrap_or(false)
5377 }
5378 },
5379 _ => false,
5380 }
5381 }
5382}
5383
5384/// Parse a string to a BigInt under JS `StringToBigInt` rules: trimmed, empty →
5385/// `0n`, decimal or `0x`/`0o`/`0b` prefixed; any junk → `None`.
5386pub fn parse_bigint_str(s: &str) -> Option<num_bigint::BigInt> {
5387 let t = crate::utf16::js_trim(s);
5388 if t.is_empty() {
5389 return Some(num_bigint::BigInt::from(0));
5390 }
5391 let (radix, digits) = if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5392 (16, h)
5393 } else if let Some(o) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5394 (8, o)
5395 } else if let Some(bb) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5396 (2, bb)
5397 } else {
5398 (10, t)
5399 };
5400 num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
5401}
5402
5403/// Coerce a BigInt to `f64` (for `Number(bigint)` and mixed relational compares);
5404/// out-of-range magnitudes become ±Infinity, matching Node.
5405pub fn bigint_to_f64(b: &num_bigint::BigInt) -> f64 {
5406 num_traits::ToPrimitive::to_f64(b).unwrap_or_else(|| {
5407 if num_traits::Signed::is_negative(b) {
5408 f64::NEG_INFINITY
5409 } else {
5410 f64::INFINITY
5411 }
5412 })
5413}
5414
5415/// JS `%` remainder (sign follows the dividend; matches `f64::rem`).
5416fn js_mod(a: f64, b: f64) -> f64 {
5417 a % b
5418}
5419
5420/// Cycle bookkeeping for one `util.inspect` render.
5421///
5422/// `seen` is the chain of objects currently being rendered (an entry appearing
5423/// twice is a back-edge), and `refs` records every object a back-edge pointed
5424/// at, in first-encountered order — its position + 1 is the `*N` id Node prints
5425/// in `[Circular *N]` / `<ref *N>`.
5426/// How an array-shaped group is laid out, beyond its entries themselves.
5427#[derive(Clone, Copy)]
5428struct ArrayLayout<'a> {
5429 /// Extra own properties follow the elements, which suppresses grid grouping.
5430 has_props: bool,
5431 /// `output`'s last entry is the `... N more items` tail rather than a real
5432 /// element, so the grid must not size a column to it.
5433 has_tail: bool,
5434 /// A constructor tag printed before the brackets, with a trailing space
5435 /// (`"Uint8Array(3) "`), or empty for a plain array.
5436 base: &'a str,
5437}
5438
5439#[derive(Default)]
5440struct InspectCycles {
5441 seen: Vec<Value>,
5442 refs: Vec<Value>,
5443 /// The indent level of the value most recently EXPANDED — node's
5444 /// `ctx.currentDepth`. `reduceToSingleString` puts a group on one line only
5445 /// while `currentDepth - thisDepth < compact`, so without it a deeply
5446 /// nested object printed on one line where node breaks the outer levels.
5447 deepest: usize,
5448}
5449
5450impl InspectCycles {
5451 /// Record `v` as a cycle target (idempotent) and return its 1-based id.
5452 fn mark(&mut self, h: &JsHost, v: &Value) -> usize {
5453 if let Some(id) = self.id_of(h, v) {
5454 return id;
5455 }
5456 self.refs.push(v.clone());
5457 self.refs.len()
5458 }
5459
5460 /// The `*N` id already assigned to `v`, if any.
5461 fn id_of(&self, h: &JsHost, v: &Value) -> Option<usize> {
5462 self.refs
5463 .iter()
5464 .position(|p| h.strict_eq(p, v))
5465 .map(|i| i + 1)
5466 }
5467}
5468
5469thread_local! {
5470 /// The active `util.inspect` `depth` (nesting levels shown before collapsing
5471 /// to `[Object]`/`[Array]`). Node's default is 2; `util.inspect(v,{depth:N})`
5472 /// overrides it for one call, `console.log`/`util.format` use the default.
5473 /// Signed, because `util.inspect(v, { depth: -1 })` is legal and means
5474 /// "already past the limit" — everything collapses to `[Object]` at the top
5475 /// level. Held as `usize` it read as an enormous depth and expanded fully.
5476 static INSPECT_MAX_DEPTH: std::cell::Cell<i64> = const { std::cell::Cell::new(2) };
5477
5478 /// `util.inspect`'s `compact` option. Node's default is the NUMBER 3: a
5479 /// group is put on one line only when the subtree below it is shallower
5480 /// than this. `compact: false` is held as 0, which no subtree depth is
5481 /// below, so every group breaks — which is exactly what node does.
5482 static INSPECT_COMPACT: std::cell::Cell<i64> = const { std::cell::Cell::new(DEFAULT_COMPACT) };
5483
5484 /// `util.inspect`'s `breakLength`. Node's default is 128, but `util.inspect`
5485 /// itself passes 80.
5486 static INSPECT_BREAK_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(80) };
5487
5488 /// `util.inspect`'s `sorted` option: emit an object's own keys in code-unit
5489 /// order instead of insertion order. Off by default. `assert`'s diff renderer
5490 /// turns it on so that two objects built with the same keys in a different
5491 /// order diff as equal rather than as a wholesale rewrite.
5492 static INSPECT_SORTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5493
5494 /// `util.inspect`'s `maxArrayLength`: how many entries are formatted before
5495 /// the rest collapse into `... N more items`. Node's default is 100;
5496 /// `Infinity`/`null` means "all", held here as `usize::MAX`.
5497 static INSPECT_MAX_ARRAY_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(DEFAULT_MAX_ARRAY_LENGTH) };
5498
5499 /// `util.inspect`'s `customInspect` option: whether a value's own
5500 /// `[util.inspect.custom]` rendering is used. On by default; `assert` turns
5501 /// it off so a diff shows an object's real structure rather than whatever
5502 /// summary it prefers to print.
5503 static INSPECT_CUSTOM: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
5504
5505 /// `util.inspect`'s `showHidden`: reveal the non-enumerable slots a value
5506 /// carries — an array's `length`, a typed array's element width and window
5507 /// onto its backing store. Off by default; `util.format`'s `%o` turns it on.
5508 static INSPECT_SHOW_HIDDEN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5509}
5510
5511/// Set the `util.inspect` `showHidden` option for the next render.
5512pub fn set_inspect_show_hidden(s: bool) {
5513 INSPECT_SHOW_HIDDEN.with(|x| x.set(s));
5514}
5515
5516pub(crate) fn inspect_show_hidden() -> bool {
5517 INSPECT_SHOW_HIDDEN.with(|x| x.get())
5518}
5519
5520/// Set the `util.inspect` `customInspect` option for the next render.
5521pub fn set_inspect_custom(c: bool) {
5522 INSPECT_CUSTOM.with(|x| x.set(c));
5523}
5524
5525pub(crate) fn inspect_custom() -> bool {
5526 INSPECT_CUSTOM.with(|x| x.get())
5527}
5528
5529/// Set the `util.inspect` `sorted` option for the next render.
5530pub fn set_inspect_sorted(s: bool) {
5531 INSPECT_SORTED.with(|x| x.set(s));
5532}
5533
5534pub(crate) fn inspect_sorted() -> bool {
5535 INSPECT_SORTED.with(|x| x.get())
5536}
5537
5538/// Set the `util.inspect` `maxArrayLength` for the next render.
5539pub fn set_inspect_max_array_length(n: usize) {
5540 INSPECT_MAX_ARRAY_LENGTH.with(|x| x.set(n));
5541}
5542
5543pub(crate) fn inspect_max_array_length() -> usize {
5544 INSPECT_MAX_ARRAY_LENGTH.with(|x| x.get())
5545}
5546
5547/// Set the `util.inspect` `compact` option for the next render (0 for `false`).
5548pub fn set_inspect_compact(c: i64) {
5549 INSPECT_COMPACT.with(|x| x.set(c));
5550}
5551
5552/// Set the `util.inspect` `breakLength` for the next render.
5553pub fn set_inspect_break_length(n: usize) {
5554 INSPECT_BREAK_LENGTH.with(|x| x.set(n));
5555}
5556
5557fn inspect_compact() -> i64 {
5558 INSPECT_COMPACT.with(|x| x.get())
5559}
5560
5561/// Set the `util.inspect` depth for the next render (restore to 2 after).
5562pub fn set_inspect_max_depth(d: i64) {
5563 INSPECT_MAX_DEPTH.with(|c| c.set(d));
5564}
5565/// Twice the configured depth, which is what the inspect walk compares its
5566/// indent against. Saturating, because `util.inspect(x, { depth: null })` and
5567/// `{ depth: Infinity }` both set the depth to `usize::MAX`, and doubling that
5568/// overflowed and panicked the process — an abort no script could catch.
5569fn inspect_indent_limit() -> i64 {
5570 inspect_max_depth().saturating_mul(2)
5571}
5572
5573fn inspect_max_depth() -> i64 {
5574 INSPECT_MAX_DEPTH.with(|c| c.get())
5575}
5576
5577/// ECMA-262 `ToInt32` (7.1.6): truncate toward zero, reduce modulo 2^32, then
5578/// reinterpret as signed.
5579///
5580/// The reduction has to happen in `f64`, not by casting through `i64`. Rust
5581/// saturates an out-of-range float-to-int cast, so `1e300 as i64` is `i64::MAX`
5582/// and `1e300 | 0` came out `-1` where every engine says `0`; the same
5583/// saturation made `1e300 >>> 0` report `4294967295`. `rem_euclid` on a
5584/// power-of-two modulus is exact for every finite double, so this is the whole
5585/// fix — and it is the form `Math.clz32` already used.
5586pub(crate) fn to_int32(f: f64) -> i32 {
5587 to_uint32(f) as i32
5588}
5589pub(crate) fn to_uint32(f: f64) -> u32 {
5590 if !f.is_finite() {
5591 return 0;
5592 }
5593 f.trunc().rem_euclid(4294967296.0) as u32
5594}
5595
5596/// Parse a string in numeric context (`ToNumber`): trimmed, empty -> 0.
5597fn str_to_number(s: &str) -> f64 {
5598 let t = crate::utf16::js_trim(s);
5599 if t.is_empty() {
5600 return 0.0;
5601 }
5602 if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5603 return i64::from_str_radix(hex, 16)
5604 .map(|n| n as f64)
5605 .unwrap_or(f64::NAN);
5606 }
5607 if let Some(oct) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5608 return i64::from_str_radix(oct, 8)
5609 .map(|n| n as f64)
5610 .unwrap_or(f64::NAN);
5611 }
5612 if let Some(bin) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5613 return i64::from_str_radix(bin, 2)
5614 .map(|n| n as f64)
5615 .unwrap_or(f64::NAN);
5616 }
5617 match t {
5618 "Infinity" | "+Infinity" => f64::INFINITY,
5619 "-Infinity" => f64::NEG_INFINITY,
5620 _ => t.parse::<f64>().unwrap_or(f64::NAN),
5621 }
5622}
5623
5624/// `util.inspect` break length (the width past which entries wrap). Node's default.
5625fn break_length() -> usize {
5626 INSPECT_BREAK_LENGTH.with(|x| x.get())
5627}
5628/// Node's default `compact` setting (the `compact * 4` column cap term).
5629/// Node's DEFAULT `compact` setting, and the initial value of
5630/// `INSPECT_COMPACT`. The grid's column cap is `compact * 4`, so it has to be
5631/// read through `inspect_compact()` at render time: under `{ compact: 1 }` node
5632/// lays a byte array out four columns wide, and the hardcoded 3 gave twelve.
5633const DEFAULT_COMPACT: i64 = 3;
5634/// Node's default `maxArrayLength` — the initial value of
5635/// `INSPECT_MAX_ARRAY_LENGTH`, which `util.inspect(v, { maxArrayLength: N })`
5636/// overrides per call. Read it through `inspect_max_array_length()`, never
5637/// directly: as a bare constant the option had no effect and a 120-element array
5638/// was truncated at 100 even under `maxArrayLength: Infinity`.
5639pub(crate) const DEFAULT_MAX_ARRAY_LENGTH: usize = 100;
5640
5641/// Whether `output` fits on a single line — a faithful port of Node's
5642/// `isBelowBreakLength` (no colors, no `base`). `start` is the caller's seed
5643/// length (braces + indentation + slack).
5644fn is_below_break_length(output: &[String], start: usize) -> bool {
5645 let limit = break_length();
5646 let mut total = output.len() + start;
5647 if total + output.len() > limit {
5648 return false;
5649 }
5650 for o in output {
5651 if o.contains('\n') {
5652 return false;
5653 }
5654 total += o.chars().count();
5655 if total > limit {
5656 return false;
5657 }
5658 }
5659 true
5660}
5661
5662/// Faithful port of Node's `util.inspect` `groupArrayElements`: lay out the
5663/// already-formatted element strings into an aligned multi-column grid. Returns
5664/// `(lines, grouped)` — `grouped` is false when Node would leave the output
5665/// ungrouped (so the caller falls back to single-line / one-per-line).
5666fn group_array_elements(
5667 host: &JsHost,
5668 output: &[String],
5669 values: &[Value],
5670 indentation_lvl: usize,
5671 has_tail: bool,
5672) -> (Vec<String>, bool) {
5673 let separator_space = 2usize; // ", " between entries
5674 // A `... N more items` tail is not an element: node drops it from the grid
5675 // (`outputLength--`) so it neither widens a column nor occupies a cell, then
5676 // re-appends it as its own final line.
5677 let output_length = output.len() - usize::from(has_tail);
5678 let data_len: Vec<usize> = output.iter().map(|o| o.chars().count()).collect();
5679 let mut total_length = 0usize;
5680 let mut max_length = 0usize;
5681 for &len in &data_len[..output_length] {
5682 total_length += len + separator_space;
5683 if len > max_length {
5684 max_length = len;
5685 }
5686 }
5687 let actual_max = max_length + separator_space;
5688 // Only group when ≥3 entries fit across AND the entries aren't wildly uneven.
5689 if !(actual_max * 3 + indentation_lvl < break_length()
5690 && (total_length as f64 / actual_max as f64 > 5.0 || max_length <= 6))
5691 {
5692 return (output.to_vec(), false);
5693 }
5694 let approx_char_heights = 2.5f64;
5695 let average_bias = (actual_max as f64 - total_length as f64 / output_length as f64).sqrt();
5696 let biased_max = (actual_max as f64 - 3.0 - average_bias).max(1.0);
5697 // Ideally a square grid; capped by break length, compact*4, and 15 columns.
5698 let columns = [
5699 ((approx_char_heights * biased_max * output_length as f64).sqrt() / biased_max).round()
5700 as i64,
5701 ((break_length() - indentation_lvl) as f64 / actual_max as f64).floor() as i64,
5702 inspect_compact().saturating_mul(4),
5703 15,
5704 ]
5705 .into_iter()
5706 .min()
5707 .unwrap();
5708 if columns <= 1 {
5709 return (output.to_vec(), false);
5710 }
5711 let columns = columns as usize;
5712 // The widest entry (plus separator) in each column.
5713 let mut max_line_length = vec![0usize; columns];
5714 for (i, slot) in max_line_length.iter_mut().enumerate() {
5715 let mut line_length = 0;
5716 let mut j = i;
5717 while j < output_length {
5718 if data_len[j] > line_length {
5719 line_length = data_len[j];
5720 }
5721 j += columns;
5722 }
5723 *slot = line_length + separator_space;
5724 }
5725 // Right-align (padStart) only when every element is a number/bigint.
5726 let pad_start = values.iter().all(|v| {
5727 matches!(v, Value::Int(_) | Value::Float(_))
5728 || matches!(host.get(v), Some(JsObj::BigInt(_)))
5729 });
5730 let mut tmp = Vec::new();
5731 let mut i = 0;
5732 while i < output_length {
5733 let max = (i + columns).min(output_length);
5734 let mut str_line = String::new();
5735 let mut j = i;
5736 while j < max.saturating_sub(1) {
5737 // `output[j]` has no colors here, so padding == max_line_length[col].
5738 let col = j - i;
5739 let cell = format!("{}, ", output[j]);
5740 let target = max_line_length[col];
5741 str_line.push_str(&pad_to(&cell, target, pad_start));
5742 j += 1;
5743 }
5744 // The last cell of the row: right-aligned entries pad without the ", ".
5745 if pad_start {
5746 let col = j - i;
5747 let target = max_line_length[col] - separator_space;
5748 str_line.push_str(&pad_to(&output[j], target, true));
5749 } else {
5750 str_line.push_str(&output[j]);
5751 }
5752 tmp.push(str_line);
5753 i += columns;
5754 }
5755 if has_tail {
5756 tmp.push(output[output_length].clone());
5757 }
5758 (tmp, true)
5759}
5760
5761/// Pad `s` to `width` chars: right-justified when `pad_start`, else left-justified.
5762/// (Padding is measured in chars; already ANSI-free here.)
5763fn pad_to(s: &str, width: usize, pad_start: bool) -> String {
5764 let len = s.chars().count();
5765 if len >= width {
5766 return s.to_string();
5767 }
5768 let fill = " ".repeat(width - len);
5769 if pad_start {
5770 format!("{fill}{s}")
5771 } else {
5772 format!("{s}{fill}")
5773 }
5774}
5775
5776/// Quote a string the way `util.inspect` does — a port of `strEscape` in Node's
5777/// `lib/internal/util/inspect.js`.
5778///
5779/// The quote character is chosen so the contents need as little escaping as
5780/// possible: single quotes normally, double quotes when the string contains a
5781/// `'` but no `"`, and a backtick when it contains both (and neither a backtick
5782/// nor a `${`). Only the ACTIVE quote is backslash-escaped, alongside `\` and
5783/// the C0 controls + DEL, which use Node's `meta` table (`\n`, `\t`, `\b`,
5784/// `\f`, `\r` short forms; `\x0B`, `\x1F`, `\x7F` uppercase-hex otherwise).
5785fn quote_str(s: &str) -> String {
5786 let quote = if !s.contains('\'') {
5787 '\''
5788 } else if !s.contains('"') {
5789 '"'
5790 } else if !s.contains('`') && !s.contains("${") {
5791 '`'
5792 } else {
5793 '\''
5794 };
5795 let mut out = String::with_capacity(s.len() + 2);
5796 out.push(quote);
5797 for c in s.chars() {
5798 match c {
5799 _ if c == quote => {
5800 out.push('\\');
5801 out.push(c);
5802 }
5803 '\\' => out.push_str("\\\\"),
5804 '\u{8}' => out.push_str("\\b"),
5805 '\t' => out.push_str("\\t"),
5806 '\n' => out.push_str("\\n"),
5807 '\u{c}' => out.push_str("\\f"),
5808 '\r' => out.push_str("\\r"),
5809 '\u{0}'..='\u{1f}' | '\u{7f}' => out.push_str(&format!("\\x{:02X}", c as u32)),
5810 _ => out.push(c),
5811 }
5812 }
5813 out.push(quote);
5814 out
5815}
5816
5817/// Render an object key: bare if it is a valid identifier, quoted otherwise.
5818fn fmt_key(k: &str) -> String {
5819 let ok = !k.is_empty()
5820 && k.chars()
5821 .next()
5822 .map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
5823 .unwrap_or(false)
5824 && k.chars()
5825 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
5826 if ok {
5827 k.to_string()
5828 } else {
5829 quote_str(k)
5830 }
5831}
5832
5833// ── iteration ────────────────────────────────────────────────────────────────
5834
5835impl JsHost {
5836 /// Collect an iterable into a vector of values (arrays, strings, Map/Set).
5837 /// Generators and user `Symbol.iterator` objects go through `iter_all`, which
5838 /// holds no host borrow across resumes.
5839 pub fn iter_vec(&mut self, v: &Value) -> Result<Vec<Value>, String> {
5840 match self.get(v) {
5841 Some(JsObj::Array(items)) => Ok(items.clone()),
5842 Some(JsObj::Str(s)) => {
5843 let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
5844 Ok(chars.into_iter().map(|c| self.new_str(c)).collect())
5845 }
5846 Some(JsObj::Iter { items, idx }) => Ok(items[*idx..].to_vec()),
5847 Some(JsObj::Set { entries, .. }) => Ok(entries.values().cloned().collect()),
5848 Some(JsObj::Map { entries, .. }) => {
5849 // Map iterates as `[key, value]` pairs.
5850 let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
5851 Ok(pairs
5852 .into_iter()
5853 .map(|(k, v)| self.new_array(vec![k, v]))
5854 .collect())
5855 }
5856 // A `Buffer` iterates over its BYTES and a typed array over its
5857 // ELEMENTS — both are iterable in Node. Only `@@bytes` was handled
5858 // here, so `[...buf]` worked while `[...new Uint8Array([1])]` threw
5859 // "object is not iterable", which is the same invariant holding at
5860 // one of its two sites.
5861 Some(JsObj::Object(props))
5862 if props.contains_key("@@bytes") || props.contains_key("@@buffer") =>
5863 {
5864 // Iterating a view over a DETACHED buffer throws, naming the
5865 // `values` iterator — reading its elements answers zero length,
5866 // but spreading it is a method call and does not.
5867 if crate::stdlib::typedarray::view_detached_h(self, v) {
5868 return Err(crate::stdlib::typedarray::detached_error(
5869 "%TypedArray%.prototype",
5870 "values",
5871 false,
5872 ));
5873 }
5874 Ok(crate::stdlib::typedarray::elems_mut_host(self, v))
5875 }
5876 // V8 names the VALUE, not its type: `[...5]` is `5 is not iterable`,
5877 // `[...{}]` is `{} is not iterable`. Reporting `typeof` instead
5878 // produced `number is not iterable`, which no engine emits.
5879 _ => {
5880 let shown = self.inspect(v);
5881 Err(type_error(&format!("{shown} is not iterable")))
5882 }
5883 }
5884 }
5885
5886 /// Enumerable string keys of an object/array (for `for-in`). Internal
5887 /// symbol-keyed props (`@@…`) are not enumerable.
5888 /// `for-in` visits own enumerable keys, then every *inherited* enumerable key
5889 /// not already seen, walking the whole prototype chain. Class methods and the
5890 /// builtin prototypes are non-enumerable, so in practice this only surfaces
5891 /// keys a script put on a prototype itself (`F.prototype.y = 2`) — but that
5892 /// is exactly the constructor-function idiom older packages are written in.
5893 pub fn enum_keys(&mut self, v: &Value) -> Vec<Value> {
5894 let mut keys = self.own_enum_key_names(v);
5895 let mut cur = self.proto_of(v);
5896 let mut hops = 0;
5897 while let Some(p) = cur {
5898 // A cyclic or pathologically deep chain must not hang the loop.
5899 hops += 1;
5900 if hops > 100 || matches!(p, Value::Undef) || self.is_null(&p) {
5901 break;
5902 }
5903 for k in self.own_enum_key_names(&p) {
5904 if !keys.contains(&k) {
5905 keys.push(k);
5906 }
5907 }
5908 cur = self.proto_of(&p);
5909 }
5910 keys.into_iter().map(|k| self.new_str(k)).collect()
5911 }
5912
5913 /// The own *enumerable* string keys of `v`, in property order — the single
5914 /// source of truth behind `for-in`, `Object.keys`/`values`/`entries`,
5915 /// object spread, `Object.assign` and `JSON.stringify`. Internal slots
5916 /// (`@@…`), private fields (`#…`) and anything marked non-enumerable via
5917 /// `prop_attrs` are excluded.
5918 pub fn own_enum_key_names(&self, v: &Value) -> Vec<String> {
5919 self.own_key_names(v, true)
5920 }
5921
5922 /// Own string keys of `v` in insertion order. `enum_only` drops the
5923 /// non-enumerable ones (`Object.keys`); otherwise every own key is reported
5924 /// (`getOwnPropertyNames`/`Reflect.ownKeys`).
5925 pub fn own_key_names(&self, v: &Value, enum_only: bool) -> Vec<String> {
5926 let mut keys = self.own_enum_data_keys(v, enum_only);
5927 // A global a SCRIPT created (`x = 1` with no declaration) is an own
5928 // ENUMERABLE property of the global object, but lives in the globals map
5929 // rather than in its property map — so no listing saw it, while
5930 // `globalThis.x` read it back and its descriptor called it enumerable.
5931 if self.is_global_object(v) {
5932 for k in self.globals.keys() {
5933 if !keys.contains(k) {
5934 keys.push(k.clone());
5935 }
5936 }
5937 }
5938 // A RegExp's `lastIndex` is a SYNTHESIZED own property — it lives in the
5939 // `RegExpObj` struct, not a property map — so nothing above can list it.
5940 // Non-enumerable, so only `getOwnPropertyNames` sees it.
5941 if !enum_only && matches!(self.get(v), Some(JsObj::RegExp(_))) {
5942 keys.push("lastIndex".to_string());
5943 }
5944 // An accessor defined before its object had any ordering marker (a class
5945 // prototype accessor, say) still has to appear.
5946 for k in self.own_accessor_keys(v) {
5947 if (!enum_only || self.prop_attrs(v, &k).enumerable) && !keys.contains(&k) {
5948 keys.push(k);
5949 }
5950 }
5951 keys
5952 }
5953
5954 /// The keys that own a slot in the object's property map, in insertion
5955 /// order, resolving accessor ordering markers back to their real key.
5956 /// Every global a SCRIPT created, in creation order — the own enumerable
5957 /// keys of the global object that live in the globals map rather than in
5958 /// its property map. `x = 1` with no declaration makes one, and
5959 /// `Object.keys(globalThis)` reports it in node.
5960 pub fn script_global_names(&self) -> Vec<String> {
5961 self.globals.keys().cloned().collect()
5962 }
5963 /// Drop a global a script created. Reports whether it was there.
5964 pub fn remove_global(&mut self, name: &str) -> bool {
5965 self.globals.shift_remove(name).is_some()
5966 }
5967 fn own_enum_data_keys(&self, v: &Value, enum_only: bool) -> Vec<String> {
5968 match self.get(v) {
5969 // A `Buffer` is an index-keyed exotic: its own enumerable keys are
5970 // `"0".."len-1"` (the bytes live in the hidden `@@bytes` slot), never
5971 // the `length`/`byteLength` view metadata, which V8 keeps on the
5972 // prototype chain or as non-enumerable own slots.
5973 // A `Buffer` and every other typed array are index-keyed exotics:
5974 // their own enumerable keys are `"0".."len-1"` (the elements live in
5975 // a hidden slot), never the `length`/`byteLength` view metadata,
5976 // which V8 keeps on the prototype chain or as non-enumerable own
5977 // slots. Only `Buffer` had this arm, so `Object.keys(u8)` was empty
5978 // and `JSON.stringify(u8)` was `{}` where node gives
5979 // `{"0":10,"1":9}` — `hasOwnProperty(0)` already answered true, so
5980 // the two views of the same question disagreed.
5981 Some(JsObj::Object(props))
5982 if matches!(
5983 props.get("@@native").map(|t| self.str_of(t)).as_deref(),
5984 Some("Buffer") | Some("TypedArray")
5985 ) =>
5986 {
5987 // A view over a DETACHED buffer has no index properties at all:
5988 // its own `length` still holds the old count, so reading that
5989 // back left `Object.keys` listing eight names over no bytes.
5990 if crate::stdlib::typedarray::view_detached_h(self, v) {
5991 return Vec::new();
5992 }
5993 // A Buffer counts its byte store; every other view reports the
5994 // element count of its window onto the ArrayBuffer.
5995 let n = match props.get("@@bytes").and_then(|b| self.get(b)) {
5996 Some(JsObj::Array(items)) => items.len(),
5997 _ => props
5998 .get("length")
5999 .map(|l| self.to_number(l))
6000 .unwrap_or(0.0) as usize,
6001 };
6002 (0..n).map(|i| i.to_string()).collect()
6003 }
6004 Some(JsObj::Object(props)) => props
6005 .keys()
6006 .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6007 Some(real) => Some(real.to_string()),
6008 None if !k.starts_with("@@") && !k.starts_with('#') => Some(k.clone()),
6009 None => None,
6010 })
6011 .filter(|k| !enum_only || self.prop_attrs(v, k).enumerable)
6012 .collect(),
6013 // A STRING is an index-keyed exotic too (10.4.3): its own keys are
6014 // its UTF-16 code-unit indices, plus the non-enumerable `length`.
6015 // Without this arm every whole-object view of a string primitive was
6016 // empty — `for (const k in 'ab')` iterated nothing, `Object.keys`
6017 // and `Object.assign({}, 'ab')` reported `{}` — while `'ab'[0]` and
6018 // `'ab'.length` answered normally, so the two views disagreed. The
6019 // spread form `{...'ab'}` went through a different path and was
6020 // already right, which is what made the gap easy to miss.
6021 Some(JsObj::Str(s)) => {
6022 let mut keys: Vec<String> =
6023 (0..crate::utf16::len(s)).map(|i| i.to_string()).collect();
6024 if !enum_only {
6025 keys.push("length".into());
6026 }
6027 keys
6028 }
6029 // `OrdinaryOwnPropertyKeys` on an array exotic: the integer indices
6030 // ascending, then the exotic non-enumerable `length`, then the
6031 // ordinary string keys in insertion order. Those ordinary keys have
6032 // no property map to live in — a `str.match()` result's
6033 // `index`/`input`/`groups` and any user-assigned `arr.foo` are kept
6034 // in the fn-prop side table — so they are read back from there.
6035 Some(JsObj::Array(items)) => {
6036 // An ELIDED element is not an own property at all, so it
6037 // contributes no key — the difference behind
6038 // `Object.keys([1,,3])` being `['0','2']`.
6039 let mut keys: Vec<String> = (0..items.len())
6040 .filter(|i| !self.is_hole(v, *i))
6041 .map(|i| i.to_string())
6042 .collect();
6043 if !enum_only {
6044 keys.push("length".into());
6045 }
6046 keys.extend(self.fn_prop_keys(v).into_iter().filter(|k| {
6047 !k.starts_with("@@")
6048 && !k.starts_with('#')
6049 && (!enum_only || self.prop_attrs(v, k).enumerable)
6050 }));
6051 keys
6052 }
6053 // A function/class keeps every own property in the side table. Its
6054 // exotic `name`/`length`/`prototype` and its class methods are all
6055 // non-enumerable, so under `enum_only` what is left is exactly what
6056 // a script assigned; `getOwnPropertyNames` reports the exotics too,
6057 // in V8's order (`length`, `name`, `prototype`, then the rest).
6058 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
6059 let mut keys: Vec<String> = Vec::new();
6060 if !enum_only {
6061 keys.push("length".into());
6062 keys.push("name".into());
6063 if self.owns_prototype(v) {
6064 keys.push("prototype".into());
6065 }
6066 }
6067 let rest: Vec<String> = self
6068 .fn_prop_keys(v)
6069 .into_iter()
6070 // An accessor's ordering marker resolves back to its real
6071 // key, so a static getter enumerates where it was declared.
6072 .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6073 Some(real) => Some(real.to_string()),
6074 None if !k.starts_with("@@") && !k.starts_with('#') => Some(k),
6075 None => None,
6076 })
6077 .filter(|k| {
6078 !keys.contains(k) && (!enum_only || self.prop_attrs(v, k).enumerable)
6079 })
6080 .collect();
6081 keys.extend(rest);
6082 keys
6083 }
6084 // A builtin namespace (`require('buffer')`, `Buffer`) enumerates the
6085 // members node-js implements, so a package that copies a namespace
6086 // key-by-key gets the working set instead of an empty object.
6087 Some(JsObj::Builtin(ns)) => crate::stdlib::namespace_keys(&ns.clone()),
6088 // A `Map`/`Set`/`Promise`/`RegExp`/generator holds only its internal
6089 // slots, so what a script assigned lives in the side table — and is
6090 // just as much an own property as an object's.
6091 Some(_) => self
6092 .fn_prop_keys(v)
6093 .into_iter()
6094 .filter(|k| {
6095 !k.starts_with("@@")
6096 && !k.starts_with('#')
6097 && (!enum_only || self.prop_attrs(v, k).enumerable)
6098 })
6099 .collect(),
6100 _ => Vec::new(),
6101 }
6102 }
6103
6104 /// The own enumerable `(key, value)` pairs of `v`. Buffer index keys resolve
6105 /// through the byte store; everything else reads the property map. Own
6106 /// accessor keys come back as `Undef` here — `own_enum_entries_deep` runs
6107 /// their getters, which cannot happen under the host borrow.
6108 pub fn own_enum_entries(&self, v: &Value) -> Vec<(String, Value)> {
6109 self.own_enum_key_names(v)
6110 .into_iter()
6111 .map(|k| {
6112 let val = match self.get(v) {
6113 // A Buffer's index keys read out of the hidden `@@bytes`
6114 // array; resolve inline rather than through
6115 // `buffer::byte_get`, which would re-borrow the host.
6116 Some(JsObj::Object(props)) => props.get(&k).cloned().unwrap_or_else(|| {
6117 // A Buffer's elements live in `@@bytes` and every
6118 // other typed array's in `@@elems`; both are index
6119 // keys with no entry in the property map.
6120 match k.parse::<usize>() {
6121 Ok(i) => crate::stdlib::typedarray::elems_with_host(self, v)
6122 .get(i)
6123 .cloned()
6124 .unwrap_or(Value::Undef),
6125 _ => Value::Undef,
6126 }
6127 }),
6128 // A Map/Set/Promise/RegExp/generator keeps every own
6129 // property in the side table.
6130 Some(
6131 JsObj::Map { .. }
6132 | JsObj::Set { .. }
6133 | JsObj::Promise { .. }
6134 | JsObj::RegExp(_)
6135 | JsObj::Generator { .. }
6136 | JsObj::Symbol { .. }
6137 | JsObj::BigInt(_)
6138 | JsObj::Iter { .. },
6139 ) => self.fn_prop(v, &k).unwrap_or(Value::Undef),
6140 // An index reads the element; any other own key (`foo`,
6141 // a match result's `index`) lives in the side table.
6142 Some(JsObj::Array(items)) => k
6143 .parse::<usize>()
6144 .ok()
6145 .and_then(|i| items.get(i).cloned())
6146 .or_else(|| self.fn_prop(v, &k))
6147 .unwrap_or(Value::Undef),
6148 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
6149 self.fn_prop(v, &k).unwrap_or(Value::Undef)
6150 }
6151 _ => Value::Undef,
6152 };
6153 (k, val)
6154 })
6155 .collect()
6156 }
6157}
6158
6159/// The own enumerable `(key, value)` pairs of `v` with every enumerable own
6160/// accessor's getter invoked — the observable shape `Object.values`,
6161/// `Object.entries`, object spread and `JSON.stringify` all need. Must be called
6162/// outside a `with_host` borrow because a getter re-enters the host.
6163pub fn own_enum_entries_deep(v: &Value) -> Result<Vec<(String, Value)>, String> {
6164 // A Proxy has no property map at all: its own enumerable entries come from
6165 // the `ownKeys` + `getOwnPropertyDescriptor` + `get` traps. A trap that
6166 // throws surfaces as an empty result here because this signature is
6167 // infallible; the callers that MUST propagate a trap throw (`Object.keys`
6168 // and friends) go through `builtins::object_keys`, which does.
6169 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
6170 return crate::proxy::own_enum_entries(v);
6171 }
6172 // A builtin namespace (`require('path')`, `Buffer`) has no property map at
6173 // all: its members are resolved on demand by `namespace_property`, which
6174 // re-enters the host and so cannot run inside `own_enum_entries`'s borrow.
6175 // Without this, spread and `Object.assign` copied the namespace's KEYS with
6176 // `undefined` for every value — measured against node v26.7.0,
6177 // `{...require('path')}.join` was `undefined` here and a function there,
6178 // while `Object.entries(require('path'))` (which resolves through
6179 // `builtins`, not through this borrow) was already correct. Two enumeration
6180 // paths, one of them silently value-less.
6181 if let Some(ns) = with_host(|h| match h.get(v) {
6182 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6183 _ => None,
6184 }) {
6185 return Ok(with_host(|h| h.own_enum_key_names(v))
6186 .into_iter()
6187 .map(|k| {
6188 let val = crate::builtins::namespace_property(&ns, &k);
6189 (k, val)
6190 })
6191 .collect());
6192 }
6193 // A string primitive's own entries are its code units. `own_enum_entries`
6194 // cannot build them: allocating the one-character string for each index
6195 // needs `&mut` host access, and it runs under a shared borrow.
6196 if let Some(sv) = with_host(|h| match h.get(v) {
6197 Some(JsObj::Str(s)) => Some(s.clone()),
6198 _ => None,
6199 }) {
6200 let units = crate::utf16::Units::of(&sv);
6201 return Ok(with_host(|h| {
6202 (0..units.len())
6203 .filter_map(|i| units.unit_str(i).map(|c| (i.to_string(), h.new_str(c))))
6204 .collect()
6205 }));
6206 }
6207 let accessor_keys: Vec<String> = with_host(|h| {
6208 h.own_accessor_keys(v)
6209 .into_iter()
6210 .filter(|k| h.prop_attrs(v, k).enumerable)
6211 .collect()
6212 });
6213 let entries = with_host(|h| h.own_enum_entries(v));
6214 // A getter that THROWS propagates: `Object.entries`, `Object.assign`,
6215 // object spread and `JSON.stringify` all read through here, and every one
6216 // of them swallowed the exception and reported the property as absent (or
6217 // as `null`) instead.
6218 let mut out = Vec::with_capacity(entries.len());
6219 for (k, val) in entries {
6220 if accessor_keys.contains(&k) {
6221 out.push((k.clone(), get_prop_chain(v, &k)?));
6222 } else {
6223 out.push((k, val));
6224 }
6225 }
6226 Ok(out)
6227}
6228
6229// ── function invocation ──────────────────────────────────────────────────────
6230
6231/// Marshal a JS call argument into a native fusevm `Value` for `rust { }` FFI.
6232/// JS strings ride as `Value::Obj(JsObj::Str)` heap handles, which fusevm's
6233/// marshaller cannot read (it calls `Value::to_str`, which returns `"(obj:N)"`
6234/// for a handle); rewrite them to a native `Value::Str`. Numbers are already
6235/// native `Value::Int`/`Value::Float`, so they pass through (fusevm coerces
6236/// Float→i64/f64 per the export signature).
6237fn marshal_ffi_arg(v: &Value) -> Value {
6238 match v {
6239 Value::Obj(_) => match with_host(|h| h.as_str(v)) {
6240 Some(s) => Value::str(s),
6241 None => v.clone(),
6242 },
6243 _ => v.clone(),
6244 }
6245}
6246
6247/// Resolve a bare name and call it (`f(args)`, `parseInt(args)`).
6248pub fn call_named(name: &str, args: Vec<Value>) -> Result<Value, String> {
6249 // Inline Rust FFI: the `rust { ... }` desugar emits `__rust_compile(b64,
6250 // line)`; compile + register the block's exported functions, returning JS
6251 // `undefined` (`Value::Undef`).
6252 if name == "__rust_compile" {
6253 let b64 = args
6254 .first()
6255 .map(|v| with_host(|h| h.str_of(v)))
6256 .unwrap_or_default();
6257 return fusevm::ffi::compile_and_register(&b64).map(|_| Value::Undef);
6258 }
6259 if let Some(v) = with_host(|h| h.read_name(name)) {
6260 return invoke(&v, args, None);
6261 }
6262 // A DIRECT eval — the literal `eval(src)` call form — is the ONLY one that
6263 // evaluates in the CALLER's scope; `(0, eval)(src)`, `const e = eval; e(src)`
6264 // and `[eval][0](src)` all reach the same function value but are INDIRECT
6265 // evals and evaluate in the global scope (ECMA-262 19.2.1.1 `PerformEval`).
6266 // This is the one place the two forms are distinguishable without a compiler
6267 // change: `call_named` is reached only from `ops::CALL`, which the compiler
6268 // emits exclusively for a bare-identifier callee, while every value-call form
6269 // goes through `invoke` → `call_builtin_function`. The `read_name` miss above
6270 // has already established that `eval` is not shadowed by a user binding.
6271 if name == "eval" {
6272 return crate::builtins::eval_source(args.first(), true);
6273 }
6274 if crate::builtins::is_known_builtin(name) {
6275 return crate::builtins::call_builtin_function(name, args);
6276 }
6277 // A `rust { ... }` block's exported functions are callable by bareword.
6278 // Reached only after user names/globals and builtins all miss, so JS code
6279 // always wins; the registry membership check keeps this off the hot path.
6280 if fusevm::ffi::is_registered(name) {
6281 let margs: Vec<Value> = args.iter().map(marshal_ffi_arg).collect();
6282 if let Some(r) = fusevm::ffi::try_call(name, &margs) {
6283 return r;
6284 }
6285 }
6286 Err(ref_error(name))
6287}
6288
6289thread_local! {
6290 /// The constructor a builtin STATIC is currently being invoked on.
6291 ///
6292 /// `A.from(x)` on `class A extends Array` re-dispatches against the `Array`
6293 /// builtin, which is reached by NAME and so cannot see `A`. The species
6294 /// rules need it: `Array.from`, `Array.of` and every `Promise` static build
6295 /// their result with `this`, so on a subclass they must construct through
6296 /// it. A stack, since one static can call another.
6297 static STATIC_THIS: std::cell::RefCell<Vec<Value>> =
6298 const { std::cell::RefCell::new(Vec::new()) };
6299}
6300
6301/// Run `f` with `recv` recorded as the receiver of a builtin static call.
6302pub fn with_static_this<R>(recv: &Value, f: impl FnOnce() -> R) -> R {
6303 STATIC_THIS.with(|s| s.borrow_mut().push(recv.clone()));
6304 let out = f();
6305 STATIC_THIS.with(|s| {
6306 s.borrow_mut().pop();
6307 });
6308 out
6309}
6310
6311/// The constructor the running builtin static was called on, if it was reached
6312/// through a subclass rather than directly.
6313pub fn current_static_this() -> Option<Value> {
6314 STATIC_THIS.with(|s| s.borrow().last().cloned())
6315}
6316
6317/// `recv.name(args)`.
6318pub fn call_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
6319 // `undefined.foo()` is a `[[Get]]` and THEN a call (13.3.6 EvaluateCall), so
6320 // the failure is the property read, not the call: node reports
6321 // `Cannot read properties of undefined (reading 'foo')`. node-js ran the
6322 // whole method dispatch against the nullish receiver, found nothing, and
6323 // reported `undefined.foo is not a function` — the wrong error class of
6324 // message for the single most common runtime fault in JS, and one that
6325 // points at the callee instead of at the base that was nullish.
6326 if with_host(|h| h.is_nullish(recv)) {
6327 return Err(type_error(&format!(
6328 "Cannot read properties of {} (reading '{name}')",
6329 with_host(|h| h.str_of(recv))
6330 )));
6331 }
6332 // `this.#m(…)` is a `[[PrivateGet]]` followed by a call, so the brand check
6333 // comes first: an unbranded receiver throws here rather than reporting the
6334 // method missing. Only a `#`-prefixed name pays the extra probe.
6335 if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
6336 return Err(crate::builtins::private_brand_message(name, false));
6337 }
6338 // `proxy.m(…)` is 13.3.6 `EvaluateCall`: `Get(proxy, "m")` — through the
6339 // `get` trap — then a call with the PROXY as `this`. The `lookup_*` shortcuts
6340 // below all read a property map a proxy does not have.
6341 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
6342 let f = crate::builtins::get_property(recv, name)?;
6343 if !with_host(|h| is_callable(h, &f)) {
6344 return Err(type_error(&format!("{name} is not a function")));
6345 }
6346 // `Function.prototype.call`/`apply`/`bind`/`toString` and the REFLECTIVE
6347 // `Object.prototype` methods are generic over `this`. node-js models each
6348 // as a thunk BOUND to the object it was read off — through a proxy, that
6349 // is the target — so invoking the thunk answers for the target and skips
6350 // the traps entirely: `pf.call(1, 2)` never reached the `apply` trap and
6351 // `p.hasOwnProperty(k)` never reached the descriptor trap. Re-dispatch
6352 // those against the PROXY, which is the `this` the real method receives.
6353 //
6354 // `toString`/`valueOf`/`toLocaleString` are deliberately NOT re-dispatched
6355 // for a non-callable proxy: they resolve by the TARGET's kind (a proxy of
6356 // an array stringifies `1,2` through `Array.prototype.toString`, not
6357 // `[object Object]`), which the bound thunk already gets right.
6358 if with_host(|h| matches!(h.get(&f), Some(JsObj::BoundMethod { .. }))) {
6359 if with_host(|h| is_callable(h, recv)) {
6360 if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6361 return Ok(r);
6362 }
6363 }
6364 if matches!(
6365 name,
6366 "hasOwnProperty" | "propertyIsEnumerable" | "isPrototypeOf"
6367 ) {
6368 return crate::builtins::object_builtin_method(recv, name, args);
6369 }
6370 // The three above resolve by the TARGET's kind, and the thunk is
6371 // already bound to the target — so it must be invoked WITHOUT a
6372 // receiver override. Passing the proxy as `this` made the
6373 // `BoundMethod` arm of `invoke` prefer it over its own receiver and
6374 // call straight back into this branch, so `String(new Proxy({}, {}))`
6375 // recursed until the stack overflowed and the process aborted.
6376 if matches!(name, "toString" | "valueOf" | "toLocaleString") {
6377 return invoke(&f, args, None);
6378 }
6379 }
6380 return invoke(&f, args, Some(recv.clone()));
6381 }
6382 // Namespace builtins (`console`, `Math`, `JSON`, ...): dispatch by qualified
6383 // name.
6384 if let Some(ns) = with_host(|h| match h.get(recv) {
6385 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6386 _ => None,
6387 }) {
6388 let qualified = format!("{ns}.{name}");
6389 if crate::builtins::is_known_builtin(&qualified) {
6390 return crate::builtins::call_builtin_function(&qualified, args);
6391 }
6392 }
6393 // Object / instance: an accessor getter that yields a function, an own or
6394 // inherited method (class methods live on the prototype chain), then an
6395 // Object.prototype builtin (hasOwnProperty …). Resolve via `lookup_*`
6396 // directly — NOT get_property — so the Object.prototype-builtin fallback
6397 // never routes back through a BoundMethod and recurses.
6398 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
6399 // A native stdlib instance (`Buffer`/crypto `Hash`/`EventEmitter`/`URL`/
6400 // fs `Stats`/http `ServerResponse`…) carries a hidden `@@native` tag.
6401 // A user-added or reparented-prototype method takes precedence over the
6402 // native dispatcher — matching JS resolution order (own → prototype
6403 // chain). This is what lets Express work: it does
6404 // `Object.setPrototypeOf(res, app.response)` and calls `res.send(...)`,
6405 // where `send` is a plain function on the reparented prototype. Native
6406 // instance methods (`res.end`/`write`/…) are NOT stored as plain
6407 // function properties, so `lookup_chain` misses them and we fall through
6408 // to `instance_call` for the real native behavior.
6409 if let Some(tag) = crate::stdlib::native_tag(recv) {
6410 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6411 if with_host(|h| is_callable(h, &f)) {
6412 return invoke(&f, args, Some(recv.clone()));
6413 }
6414 }
6415 // `Object.prototype` methods reach a native instance too — a Buffer
6416 // inherits `hasOwnProperty`/`isPrototypeOf` through its prototype
6417 // chain, and the native dispatcher has no entry for them.
6418 if crate::builtins::is_object_builtin_method(name)
6419 && !crate::stdlib::instance_has_method(&tag, name)
6420 {
6421 return crate::builtins::object_builtin_method(recv, name, args);
6422 }
6423 return crate::stdlib::instance_call(&tag, recv, name, args);
6424 }
6425 // A primitive wrapper forwards to the primitive's method table, the
6426 // same way a native instance forwards to its tag's. A user method on
6427 // the wrapper or anywhere on its chain still wins first.
6428 if let Some(prim) = crate::builtins::wrapped_primitive(recv) {
6429 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6430 if with_host(|h| is_callable(h, &f)) {
6431 return invoke(&f, args, Some(recv.clone()));
6432 }
6433 }
6434 // The reflective `Object.prototype` methods answer for the WRAPPER
6435 // — `w.hasOwnProperty("0")` asks about the wrapper's own index
6436 // properties, not about the string.
6437 if crate::builtins::is_object_builtin_method(name) {
6438 return crate::builtins::object_builtin_method(recv, name, args);
6439 }
6440 return call_method(&prim, name, args);
6441 }
6442 if let Some((Some(getter), _)) = with_host(|h| lookup_accessor(h, recv, name)) {
6443 let f = invoke(&getter, Vec::new(), Some(recv.clone()))?;
6444 if with_host(|h| is_callable(h, &f)) {
6445 return invoke(&f, args, Some(recv.clone()));
6446 }
6447 }
6448 // A Proxy in the prototype chain serves the method through its `get`
6449 // trap. `lookup_chain` below reads property maps, which a proxy has none
6450 // of, so without this `child.m()` on `Object.create(proxy)` reported
6451 // "m is not a function" even though `child.m` already read correctly.
6452 if crate::builtins::proxy_proto_link(recv, name).is_some() {
6453 let f = crate::builtins::get_property(recv, name)?;
6454 if !with_host(|h| is_callable(h, &f)) {
6455 return Err(type_error(&format!("{name} is not a function")));
6456 }
6457 return invoke(&f, args, Some(recv.clone()));
6458 }
6459 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6460 if with_host(|h| is_callable(h, &f)) {
6461 return invoke(&f, args, Some(recv.clone()));
6462 }
6463 return Err(type_error(&format!("{name} is not a function")));
6464 }
6465 // A method patched onto `Object.prototype`. `lookup_chain` cannot find
6466 // it: a plain object is not LINKED to the intrinsic prototype object,
6467 // its `Object.prototype` members are synthesized instead. So
6468 // `Object.prototype.tap = f; ({}).tap()` reported "is not a function"
6469 // while `({}).tap` already read back as `f`.
6470 if let Some(f) = crate::builtins::inherited_builtin_static(recv, name) {
6471 if with_host(|h| is_callable(h, &f)) {
6472 return invoke(&f, args, Some(recv.clone()));
6473 }
6474 }
6475 // A method from an intrinsic prototype this object's CHAIN passes
6476 // through — `Object.create(Array.prototype).push(1)`. The read already
6477 // resolves it through the same owner oracle; dispatch reported "is not
6478 // a function", the read and the call disagreeing once more.
6479 if let Some(owner) = crate::builtins::inherited_method_owner_pub(recv, name) {
6480 if owner != "Object" {
6481 return crate::builtins::proto_method(recv, &format!("{owner}:{name}"), args);
6482 }
6483 }
6484 if crate::builtins::is_object_builtin_method(name) {
6485 return crate::builtins::object_builtin_method(recv, name, args);
6486 }
6487 if name == "constructor" {
6488 if let Some(r) = call_default_ctor(recv, &args) {
6489 return r;
6490 }
6491 }
6492 return Err(type_error(&format!("{name} is not a function")));
6493 }
6494 // Function value methods: call / apply / bind, then any static method stored
6495 // on the function object.
6496 if matches!(
6497 with_host(|h| h.kind_of(recv)),
6498 Some(ObjKind::Func)
6499 | Some(ObjKind::Class)
6500 | Some(ObjKind::BoundFunc)
6501 | Some(ObjKind::BoundMethod)
6502 | Some(ObjKind::Builtin)
6503 ) {
6504 if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6505 return Ok(r);
6506 }
6507 // A static method (own or inherited): `this` is the constructor (`recv`).
6508 let stat = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6509 with_host(|h| h.class_static(recv, name))
6510 } else {
6511 with_host(|h| h.fn_prop(recv, name))
6512 };
6513 if let Some(f) = stat {
6514 if with_host(|h| is_callable(h, &f)) {
6515 return invoke(&f, args, Some(recv.clone()));
6516 }
6517 }
6518 // `class_static` only walks user-class `extends` links, so a chain that
6519 // bottoms out in a BUILTIN constructor (`class D extends Array {}`)
6520 // could not reach that builtin's statics: `D.from([1,2])` threw
6521 // "from is not a function" even though `typeof D.from` said `function`.
6522 // Re-dispatch the call against that ancestor, which is what reaches a
6523 // builtin namespace's methods.
6524 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6525 if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
6526 if with_host(|h| h.kind_of(&anc)) == Some(ObjKind::Builtin) {
6527 // The subclass is recorded so a species-aware static
6528 // (`Array.from`, `Promise.resolve`, …) builds its result
6529 // through it rather than through the builtin.
6530 return with_static_this(recv, || call_method(&anc, name, args));
6531 }
6532 }
6533 }
6534 // A method inherited via the function's [[Prototype]] chain (set with
6535 // `Object.setPrototypeOf(fn, proto)`) — the `router` package's router
6536 // functions inherit `route`/`use`/`get`/… from `Router.prototype`.
6537 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6538 if with_host(|h| is_callable(h, &f)) {
6539 return invoke(&f, args, Some(recv.clone()));
6540 }
6541 }
6542 // An `Object.prototype` method invoked with a builtin namespace/prototype
6543 // as `this` (`hasOwnProperty.call(Map.prototype, 'get')`, the get-intrinsic
6544 // ownership probe) — dispatch it against the builtin receiver.
6545 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin)
6546 && crate::builtins::is_object_builtin_method(name)
6547 {
6548 return crate::builtins::object_builtin_method(recv, name, args);
6549 }
6550 }
6551 if name == "constructor" {
6552 if let Some(r) = call_default_ctor(recv, &args) {
6553 return r;
6554 }
6555 }
6556 // Type methods (array/string/number, Map/Set/Symbol/generator methods).
6557 crate::builtins::call_type_method(recv, name, args)
6558}
6559
6560/// `x.constructor(...)` invoked as a CALL when nothing on `x`'s prototype chain
6561/// owns a `constructor` slot.
6562///
6563/// Reading the property already resolves a builtin instance's native constructor
6564/// (the `constructor` arm of `builtins::get_property`), but the CALL path only
6565/// consulted the prototype chain, so the two disagreed:
6566/// `(function(){}).constructor === Function` read `true` while
6567/// `(function(){}).constructor('return 9')` threw
6568/// `TypeError: constructor is not a function`. That call form is exactly how
6569/// `get-intrinsic` — a transitive dependency of express — reaches the `Function`
6570/// constructor. Resolved here through the same one definition the read uses, so
6571/// the two can no longer drift apart. `None` means "not resolvable/callable",
6572/// leaving the caller's original error in place.
6573fn call_default_ctor(recv: &Value, args: &[Value]) -> Option<Result<Value, String>> {
6574 let ctor = crate::builtins::get_property(recv, "constructor").ok()?;
6575 with_host(|h| is_callable(h, &ctor)).then(|| invoke(&ctor, args.to_vec(), None))
6576}
6577
6578/// Call any callable value.
6579pub fn invoke(callable: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6580 // `[[Call]]` on a Proxy runs the `apply` trap (or forwards to the target).
6581 // Probed by kind first so the ordinary call path never clones its arguments.
6582 if with_host(|h| h.kind_of(callable)) == Some(ObjKind::Proxy) {
6583 return crate::proxy::apply(callable, args, this).map(|r| r.expect("kind_of said Proxy"));
6584 }
6585 let obj = with_host(|h| h.get(callable).cloned());
6586 match obj {
6587 // A builtin-prototype method thunk (`Object.prototype.toString`): dispatch
6588 // against the invoke-time `this` (supplied by `.call`/`.apply`).
6589 Some(JsObj::Builtin(name)) if name.starts_with("@proto:") => {
6590 let recv = this.unwrap_or(Value::Undef);
6591 crate::builtins::proto_method(&recv, &name["@proto:".len()..], args)
6592 }
6593 // An intrinsic prototype's GETTER, borrowed off its descriptor — the
6594 // form a library uses to read a slot from an arbitrary receiver
6595 // (`Object.getOwnPropertyDescriptor(Map.prototype, 'size').get
6596 // .call(m)`). It brand-checks `this` and reads, or throws naming
6597 // itself.
6598 // The setter half of the `arguments`/`caller` poison pill — the only
6599 // intrinsic accessor here that has one, and it throws like its getter.
6600 Some(JsObj::Builtin(name)) if name.starts_with("@protoset:") => {
6601 let _ = &name;
6602 let recv = this.unwrap_or(Value::Undef);
6603 // The setter half accepts silently for the same receivers the
6604 // getter answers for, and throws for the rest.
6605 if with_host(|h| h.fn_is_sloppy(&recv)) {
6606 Ok(Value::Undef)
6607 } else {
6608 Err(type_error(crate::builtins::POISON_PILL))
6609 }
6610 }
6611 Some(JsObj::Builtin(name)) if name.starts_with("@protoget:") => {
6612 let recv = this.unwrap_or(Value::Undef);
6613 let rest = &name["@protoget:".len()..];
6614 let (ctor, key) = rest.split_once(':').unwrap_or((rest, ""));
6615 crate::builtins::proto_getter_call(ctor, key, &recv)
6616 }
6617 // `NativeCtor.call(obj, …)` — ES5 "constructor stealing", still shipped by
6618 // libraries that predate `class`. `iconv-lite`'s internal codec is exactly
6619 // this:
6620 //
6621 // function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
6622 // InternalDecoder.prototype = StringDecoder.prototype;
6623 //
6624 // A native constructor builds a fresh tagged object, so initializing the
6625 // SUPPLIED object means building one and moving its slots across.
6626 //
6627 // The guard is deliberately narrow: `obj` must already inherit from THIS
6628 // constructor's prototype, i.e. the subclass really did adopt it. Without
6629 // that, `Date.call(x)` and `Buffer.call(x)` — which in JS ignore `this` and
6630 // return a string / a buffer — would start mutating `x` instead.
6631 Some(JsObj::Builtin(ref name)) if steals_ctor(name, this.as_ref()) => {
6632 let target = this.expect("guard checked");
6633 let built = crate::stdlib::construct(name, &args)
6634 .expect("guard checked a native constructor")?;
6635 adopt_native_slots(&target, &built);
6636 Ok(Value::Undef)
6637 }
6638 Some(JsObj::Builtin(name)) => crate::builtins::call_builtin_function(&name, args),
6639 Some(JsObj::Func(fv)) => run_user_func_of(&fv, args, this, Some(callable.clone())),
6640 // A method read off an object is modelled as a thunk BOUND to it, but an
6641 // explicit `.call`/`.apply` receiver still wins — `Function.prototype.call`
6642 // rebinds `this`, and every `Array.prototype` method is generic over it, so
6643 // `[].slice.call(arrayLike)` must run against the ARGUMENT. Dropping the
6644 // override made that read back as the empty array the thunk was read off.
6645 // A nullish override is ignored: it carries no receiver to dispatch on.
6646 Some(JsObj::BoundMethod { recv, name }) => {
6647 let target = match &this {
6648 Some(t) if !matches!(t, Value::Undef) && !with_host(|h| h.is_null(t)) => t,
6649 _ => &recv,
6650 };
6651 // A thunk read off an ARRAY carries an `Array.prototype` method, and
6652 // those are generic over `this` — route the rebound call through
6653 // `proto_method` so an array-LIKE receiver takes the generic path
6654 // instead of being told the method does not exist.
6655 if with_host(|h| h.kind_of(&recv)) == Some(ObjKind::Array) {
6656 return crate::builtins::proto_method(target, &format!("Array:{name}"), args);
6657 }
6658 call_method(target, &name, args)
6659 }
6660 Some(JsObj::BoundFunc {
6661 target,
6662 this: bthis,
6663 args: pre,
6664 }) => {
6665 let mut all = pre;
6666 all.extend(args);
6667 invoke(&target, all, Some(bthis))
6668 }
6669 Some(JsObj::Class(c)) => Err(type_error(&format!(
6670 "Class constructor {} cannot be invoked without 'new'",
6671 c.name
6672 ))),
6673 _ => Err(type_error(&format!(
6674 "{} is not a function",
6675 with_host(|h| h.str_of(callable))
6676 ))),
6677 }
6678}
6679
6680/// Whether calling the native constructor `name` with `this` is the ES5
6681/// constructor-stealing pattern rather than an ordinary call.
6682///
6683/// True only when `name` really is a native stdlib constructor AND `this` is a
6684/// plain object that already inherits from that constructor's prototype — the
6685/// signature of `Sub.prototype = Native.prototype; Native.call(this, …)`. An
6686/// object that merely happens to be passed as `this` does not qualify, so
6687/// `Date.call(x)` / `Buffer.call(x)` keep their JS meaning (ignore `this`).
6688fn steals_ctor(name: &str, this: Option<&Value>) -> bool {
6689 let Some(target) = this else { return false };
6690 if !with_host(|h| matches!(h.get(target), Some(JsObj::Object(_)))) {
6691 return false;
6692 }
6693 // Already initialized (e.g. a re-entrant call) — nothing to steal.
6694 if crate::stdlib::native_tag(target).is_some() {
6695 return false;
6696 }
6697 let Some(proto) = with_host(|h| h.ensure_ctor_proto(name)) else {
6698 return false;
6699 };
6700 let mut cur = with_host(|h| h.proto_of(target));
6701 while let Some(p) = cur {
6702 if p == proto {
6703 return true;
6704 }
6705 cur = with_host(|h| h.proto_of(&p));
6706 }
6707 false
6708}
6709
6710/// Move a freshly-constructed native instance's state onto `target`, so an
6711/// object built by a subclass constructor becomes a working instance of the
6712/// native class. Copies every own key the native constructor set — the hidden
6713/// `@@`-prefixed slots that carry the state AND the plain ones it exposes
6714/// (`StringDecoder`'s `encoding`) — without disturbing keys `target` already has.
6715fn adopt_native_slots(target: &Value, built: &Value) {
6716 let slots: Vec<(String, Value)> = with_host(|h| match h.get(built) {
6717 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
6718 _ => Vec::new(),
6719 });
6720 with_host(|h| {
6721 if let Some(JsObj::Object(p)) = h.get_mut(target) {
6722 for (k, v) in slots {
6723 p.insert(k, v);
6724 }
6725 }
6726 });
6727}
6728
6729/// Execute a user function/closure body on a fresh frame.
6730pub fn run_user_func(fv: &FuncVal, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6731 run_user_func_of(fv, args, this, None)
6732}
6733
6734/// [`run_user_func`] with the function VALUE the call came through, which the
6735/// `arguments` object needs for its `callee`.
6736pub fn run_user_func_of(
6737 fv: &FuncVal,
6738 args: Vec<Value>,
6739 this: Option<Value>,
6740 callee: Option<Value>,
6741) -> Result<Value, String> {
6742 run_user_func_full(fv, args, this, None, callee)
6743}
6744
6745/// As `run_user_func`, but with an explicit `new.target` (set by `new`).
6746pub fn run_user_func_nt(
6747 fv: &FuncVal,
6748 args: Vec<Value>,
6749 this: Option<Value>,
6750 new_target: Option<Value>,
6751) -> Result<Value, String> {
6752 run_user_func_full(fv, args, this, new_target, None)
6753}
6754
6755fn run_user_func_full(
6756 fv: &FuncVal,
6757 args: Vec<Value>,
6758 this: Option<Value>,
6759 new_target: Option<Value>,
6760 callee: Option<Value>,
6761) -> Result<Value, String> {
6762 // Consumed first, before anything here can start another call.
6763 let derived_ctor = with_host(|h| std::mem::take(&mut h.derived_ctor_next));
6764 // Only the light fields: cloning the whole `FuncDef` cloned its `Chunk` —
6765 // the entire compiled body, `sub_chunks` and all — on every single call.
6766 // The chunk is now reached once per pooled VM, in the two arms below.
6767 let (params, is_generator, is_async, is_arrow_def, def_name) = with_host(|h| {
6768 let d = &h.funcs[fv.def_id];
6769 (
6770 d.params.clone(),
6771 d.is_generator,
6772 d.is_async,
6773 d.is_arrow,
6774 d.name.clone(),
6775 )
6776 });
6777 let env = new_env(fv.env.clone());
6778 // Bind the simple/rest arg slots; destructuring + defaults run in the body
6779 // prologue (compiled ahead of the user statements).
6780 let fn_is_sloppy = with_host(|h| !h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
6781 bind_params(
6782 &env,
6783 ¶ms,
6784 args,
6785 is_arrow_def,
6786 callee.as_ref(),
6787 fn_is_sloppy && !is_arrow_def,
6788 );
6789 // Arrow functions capture `this` lexically; regular functions receive it.
6790 let mut this_val = if fv.is_arrow { fv.this.clone() } else { this };
6791 // 10.2.1.2 OrdinaryCallBindThis: in SLOPPY mode an absent or nullish `this`
6792 // becomes the global object. Only a strict function keeps `undefined`, and
6793 // an arrow has no `this` of its own to substitute. Leaving it undefined
6794 // meant a plain `f()`, a detached method, a callback and `f.call(null)` all
6795 // saw `undefined` where node sees `globalThis`.
6796 let sloppy_this = !fv.is_arrow
6797 && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict))
6798 && match &this_val {
6799 None => true,
6800 Some(v) => matches!(v, Value::Undef) || with_host(|h| h.is_null(v)),
6801 };
6802 if sloppy_this {
6803 this_val = Some(with_host(|h| h.global_object()));
6804 } else if !fv.is_arrow && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)) {
6805 // The other half of OrdinaryCallBindThis: a SLOPPY function boxes a
6806 // primitive `this` with `ToObject`, so `f.call(5)` sees a `Number`
6807 // wrapper rather than the number. Only strict mode passes it through.
6808 if let Some(t) = this_val.clone() {
6809 let boxed = crate::builtins::to_object(&t);
6810 this_val = Some(boxed);
6811 }
6812 }
6813 // A generator function does not run its body on call — it returns a suspended
6814 // generator over the already-bound frame.
6815 if is_generator {
6816 let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
6817 let gen = make_generator(
6818 chunk,
6819 env,
6820 this_val,
6821 fv.home_class.clone(),
6822 fv.home_static,
6823 fv.home_object.clone(),
6824 with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
6825 );
6826 if is_async {
6827 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(&gen).cloned()) {
6828 with_host(|h| h.generators[id as usize].async_gen = true);
6829 }
6830 }
6831 return Ok(gen);
6832 }
6833 // An async function runs on a coroutine and returns a Promise: it executes
6834 // synchronously up to the first `await`, then continues via microtasks.
6835 if is_async {
6836 let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
6837 let gen = make_generator(
6838 chunk,
6839 env,
6840 this_val,
6841 fv.home_class.clone(),
6842 fv.home_static,
6843 fv.home_object.clone(),
6844 with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
6845 );
6846 return Ok(run_async(gen));
6847 }
6848 let home = fv
6849 .home_class
6850 .as_ref()
6851 .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
6852 // Resolved BEFORE the borrow below: reading the function table re-enters
6853 // the host, and doing it inside the frame-push closure double-borrows.
6854 let fn_strict = with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
6855 with_host(|h| {
6856 h.frames.push(Frame {
6857 base_env: env.clone(),
6858 env,
6859 this_obj: this_val,
6860 new_target,
6861 home_class: home,
6862 home_static: fv.home_static,
6863 home_object: fv.home_object.clone(),
6864 strict: fn_strict,
6865 line: 0,
6866 owner: Some(def_name),
6867 is_module: false,
6868 this_state: if derived_ctor {
6869 ThisState::Pending
6870 } else {
6871 ThisState::Plain
6872 },
6873 })
6874 });
6875 let r = run_chunk_keyed(func_key(fv.def_id), || {
6876 with_host(|h| h.funcs[fv.def_id].chunk.clone())
6877 });
6878 let (sig, this_state) = with_host(|h| {
6879 let frame = h.frames.pop();
6880 (h.signal.take(), frame.map(|f| f.this_state))
6881 });
6882 let ret = match r {
6883 Err(e) => return Err(e),
6884 Ok(_) => match sig {
6885 Some(Signal::Return(v)) => v,
6886 _ => Value::Undef,
6887 },
6888 };
6889 // 10.2.2 [[Construct]] steps 10-12 for a derived constructor: an object
6890 // return wins; any other non-undefined return is a TypeError; and falling
6891 // off the end (or `return;`) needs `this` to have been bound by `super()`.
6892 if derived_ctor && !returns_object(&ret) {
6893 if !matches!(ret, Value::Undef) {
6894 return Err(type_error(
6895 "Derived constructors may only return object or undefined",
6896 ));
6897 }
6898 if this_state == Some(ThisState::Pending) {
6899 return Err(this_before_super_error());
6900 }
6901 }
6902 Ok(ret)
6903}
6904
6905/// Bind positional args into a fresh call environment. The compiler emits the
6906/// param names in `def.params`; a `...rest` slot collects the tail as an array.
6907fn bind_params(
6908 env: &Env,
6909 params: &[ParamSlot],
6910 args: Vec<Value>,
6911 is_arrow: bool,
6912 callee: Option<&Value>,
6913 sloppy: bool,
6914) {
6915 let mut vars = VarMap::default();
6916 let mut i = 0;
6917 for slot in params {
6918 if slot.rest {
6919 let rest: Vec<Value> = args.get(i..).map(|s| s.to_vec()).unwrap_or_default();
6920 let arr = with_host(|h| h.new_array(rest));
6921 vars.insert(slot.name.clone(), arr);
6922 } else {
6923 let v = args.get(i).cloned().unwrap_or(Value::Undef);
6924 vars.insert(slot.name.clone(), v);
6925 i += 1;
6926 }
6927 }
6928 // `arguments` array (simple approximation — see BUGS.md: it is a real
6929 // Array, not an Arguments exotic). An ARROW function never gets one:
6930 // `FunctionDeclarationInstantiation` (10.2.11) creates the binding only for
6931 // a non-arrow, so `arguments` inside an arrow resolves lexically to the
6932 // enclosing function's. Binding an empty one here made
6933 // `function f(){ const g = () => [...arguments]; }` see zero args.
6934 if !is_arrow {
6935 let args_arr = with_host(|h| {
6936 let a = h.new_array(args);
6937 // Marked so it can be told apart from an ordinary array: node's
6938 // `arguments` is an exotic, and without the mark
6939 // `Array.isArray(arguments)` was true, the brand was
6940 // `[object Array]` and `util.types.isArgumentsObject` was false.
6941 // The backing representation stays an Array, which is what keeps
6942 // indices, `length`, spread and `for-of` working.
6943 h.set_fn_prop(&a, "@@arguments", Value::Bool(true));
6944 // `callee` is the function itself in SLOPPY code (it is a poison
6945 // pill only in strict, which the read path handles). It read back
6946 // `undefined`, so the pre-`class` self-reference idiom
6947 // `(function(){ arguments.callee })` found nothing.
6948 if let Some(f) = callee {
6949 if sloppy {
6950 h.set_fn_prop(&a, "@@callee", f.clone());
6951 }
6952 }
6953 a
6954 });
6955 vars.entry("arguments".to_string()).or_insert(args_arr);
6956 }
6957 env.borrow_mut().vars = vars;
6958}
6959
6960/// Construct an instance with `new` — creates a fresh object, binds it as
6961/// `this`, runs the constructor, and returns the object (unless the constructor
6962/// returns its own object).
6963pub fn construct(ctor: &Value, args: Vec<Value>) -> Result<Value, String> {
6964 construct_nt(ctor, args, ctor.clone())
6965}
6966
6967/// `new` with an explicit `new.target` (differs from `ctor` when a derived class
6968/// calls `super(...)` — the target stays the originally-`new`ed class).
6969pub fn construct_nt(ctor: &Value, args: Vec<Value>, new_target: Value) -> Result<Value, String> {
6970 // `new proxy(…)` runs the `construct` trap (or forwards to the target).
6971 if with_host(|h| h.kind_of(ctor)) == Some(ObjKind::Proxy) {
6972 return crate::proxy::construct(ctor, args, &new_target)
6973 .map(|r| r.expect("kind_of said Proxy"));
6974 }
6975 let obj = with_host(|h| h.get(ctor).cloned());
6976 match obj {
6977 Some(JsObj::Class(_)) => construct_class(ctor, args, new_target),
6978 Some(JsObj::Func(fv)) => {
6979 // Only an ORDINARY function has a `[[Construct]]` slot. An arrow, a
6980 // `function*` and an `async function` are callable but not
6981 // constructable (10.2.2 is installed only for the ordinary case), so
6982 // `new` on one is a TypeError — node-js instead ran the body and
6983 // handed back a half-built instance (for a generator, an object whose
6984 // constructor had returned a suspended generator).
6985 let non_ctor = with_host(|h| {
6986 h.funcs
6987 .get(fv.def_id)
6988 // A MethodDefinition is in the same boat: `new ({m(){}}).m()`
6989 // is `TypeError: o.m is not a constructor` on node v26.7.0,
6990 // which is also why a method owns no `prototype`.
6991 .map(|d| d.is_generator || d.is_async || d.is_method)
6992 .unwrap_or(false)
6993 });
6994 if fv.is_arrow || non_ctor {
6995 return Err(not_a_constructor(ctor));
6996 }
6997 // A plain constructor function: instance delegates to `fn.prototype`
6998 // (auto-created with a `.constructor` back-link if not yet accessed).
6999 let inst = with_host(|h| {
7000 let o = h.new_object(IndexMap::new());
7001 let proto = h.fn_prop(ctor, "prototype").unwrap_or_else(|| {
7002 let p = h.new_object(IndexMap::new());
7003 if let Some(JsObj::Object(pp)) = h.get_mut(&p) {
7004 pp.insert("constructor".to_string(), ctor.clone());
7005 }
7006 // `F.prototype.constructor` is non-enumerable in JS.
7007 h.hide_prop(&p, "constructor");
7008 h.set_fn_prop(ctor, "prototype", p.clone());
7009 p
7010 });
7011 h.set_proto(&o, proto);
7012 o
7013 });
7014 let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target))?;
7015 if returns_object(&r) {
7016 Ok(r)
7017 } else {
7018 Ok(inst)
7019 }
7020 }
7021 Some(JsObj::Builtin(name)) => crate::builtins::construct_builtin(&name, args),
7022 Some(JsObj::BoundFunc {
7023 target, args: pre, ..
7024 }) => {
7025 let mut all = pre;
7026 all.extend(args);
7027 construct_nt(&target, all, new_target)
7028 }
7029 _ => Err(not_a_constructor(ctor)),
7030 }
7031}
7032
7033/// `TypeError: <callee> is not a constructor`.
7034///
7035/// V8 names the callee by its SOURCE TEXT (`new g()` reports `g`, `new o.m()`
7036/// reports `o.m`); node-js keeps no spans, so a named callable is reported by
7037/// its name — the same string in the common case — and anything else by its
7038/// value.
7039fn not_a_constructor(ctor: &Value) -> String {
7040 let name = with_host(|h| match h.callable_name(ctor) {
7041 n if n.is_empty() => h.str_of(ctor),
7042 n => n,
7043 });
7044 type_error(&format!("{name} is not a constructor"))
7045}
7046
7047/// Whether a constructor's return value is an object (so `new` yields it instead
7048/// of the fresh instance). In JS "object" includes functions — the `router`
7049/// package's constructor `return router` (a function) must be honored, or the
7050/// returned router loses its callable identity.
7051fn returns_object(r: &Value) -> bool {
7052 matches!(
7053 with_host(|h| h.get(r).cloned()),
7054 Some(JsObj::Object(_))
7055 | Some(JsObj::Array(_))
7056 | Some(JsObj::Map { .. })
7057 | Some(JsObj::Set { .. })
7058 | Some(JsObj::Func(_))
7059 | Some(JsObj::Class(_))
7060 | Some(JsObj::BoundFunc { .. })
7061 | Some(JsObj::BoundMethod { .. })
7062 | Some(JsObj::RegExp(_))
7063 )
7064}
7065
7066/// Construct a `class` instance: allocate the object linked to `C.prototype`,
7067/// run field initializers + the constructor (which may call `super(...)`).
7068fn construct_class(
7069 class_val: &Value,
7070 args: Vec<Value>,
7071 new_target: Value,
7072) -> Result<Value, String> {
7073 let cv = match with_host(|h| h.get(class_val).cloned()) {
7074 Some(JsObj::Class(c)) => c,
7075 _ => return Err(type_error("not a class")),
7076 };
7077 // Resolve the prototype of the *most-derived* class being `new`ed, so an
7078 // instance created through a `super()` chain still delegates to the leaf
7079 // prototype (correct method resolution).
7080 let leaf_proto = match with_host(|h| h.get(&new_target).cloned()) {
7081 Some(JsObj::Class(c)) => c.proto.clone(),
7082 _ => cv.proto.clone(),
7083 };
7084 let inst = with_host(|h| {
7085 let o = h.new_object(IndexMap::new());
7086 h.set_proto(&o, leaf_proto.clone());
7087 o
7088 });
7089 // A `super()` deeper in may substitute the instance; the previous value is
7090 // restored so a `new` inside a constructor body cannot be mistaken for one.
7091 let saved = with_host(|h| h.swap_super_replacement(None));
7092 let ran = run_class_ctor(&cv, &inst, args, &new_target);
7093 let substituted = with_host(|h| {
7094 let s = h.take_super_replacement();
7095 h.swap_super_replacement(saved);
7096 s
7097 });
7098 // A constructor that returns an object replaces the instance (`new`
7099 // semantics); failing that, whatever `super()` substituted for it.
7100 match ran? {
7101 Some(obj) if returns_object(&obj) => Ok(obj),
7102 _ => Ok(substituted.unwrap_or(inst)),
7103 }
7104}
7105
7106/// Run one class's field initializers then its constructor on an existing
7107/// instance. Returns the constructor's explicit object return (if any). For a
7108/// base class this is the whole init; for a derived class the constructor body
7109/// reaches `super(...)` which recurses into the parent.
7110fn run_class_ctor(
7111 cv: &ClassVal,
7112 inst: &Value,
7113 args: Vec<Value>,
7114 new_target: &Value,
7115) -> Result<Option<Value>, String> {
7116 // A derived class must run its fields AFTER super() returns; SUPER_CALL does
7117 // that. A base class initializes fields before the constructor body.
7118 if cv.parent.is_none() {
7119 init_fields(cv, inst)?;
7120 }
7121 match &cv.ctor {
7122 Some(ctor_fn) => {
7123 let fv = match with_host(|h| h.get(ctor_fn).cloned()) {
7124 Some(JsObj::Func(f)) => f,
7125 _ => return Err(type_error("class constructor is not a function")),
7126 };
7127 if cv.parent.is_some() {
7128 with_host(|h| h.mark_next_call_derived_ctor());
7129 }
7130 let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7131 return Ok(Some(r));
7132 }
7133 None => {
7134 // Default constructor: `constructor(...a){ super(...a); }` for a
7135 // derived class, empty for a base class.
7136 if let Some(parent) = &cv.parent {
7137 // A base constructor's returned object becomes the instance, so
7138 // the implicit `constructor(...a){ super(...a) }` hands it on.
7139 if let Some(replacement) = super_construct(parent, args, inst, new_target)? {
7140 init_fields(cv, &replacement)?;
7141 return Ok(Some(replacement));
7142 }
7143 init_fields(cv, inst)?;
7144 }
7145 }
7146 }
7147 Ok(None)
7148}
7149
7150/// Evaluate and assign a class's instance-field initializers on `inst`.
7151fn init_fields(cv: &ClassVal, inst: &Value) -> Result<(), String> {
7152 for (name, thunk, name_anon) in &cv.fields {
7153 init_one_field(inst, name, thunk, *name_anon)?;
7154 }
7155 Ok(())
7156}
7157
7158/// Evaluate ONE instance-field initializer thunk and install the result on
7159/// `inst`.
7160///
7161/// Shared by the base-class path (`init_fields`) and the derived-class path
7162/// that runs after `super(...)`; the two used to be separate loops, and only the
7163/// first canonicalized an array-index key.
7164///
7165/// `name_anon` carries 15.7.10's NamedEvaluation: `class C { f = function(){} }`
7166/// gives the function the name `f`. It is decided by the compiler from the
7167/// syntax, never from the value.
7168pub fn init_one_field(
7169 inst: &Value,
7170 name: &str,
7171 thunk: &Value,
7172 name_anon: bool,
7173) -> Result<(), String> {
7174 // The thunk is an arrow capturing the class scope; run it with `this`=inst
7175 // so `this.other`-referencing initializers work.
7176 let val = invoke(thunk, Vec::new(), Some(inst.clone()))?;
7177 with_host(|h| {
7178 if name_anon {
7179 let s = h.new_str(name.to_string());
7180 h.set_fn_prop(&val, "name", s);
7181 }
7182 if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7183 let is_new = !props.contains_key(name);
7184 props.insert(name.to_string(), val);
7185 if is_new && array_index(name).is_some() {
7186 canonicalize_own_keys(props);
7187 }
7188 }
7189 });
7190 Ok(())
7191}
7192
7193/// Run a parent constructor as part of `super(...)`: dispatch on the parent's
7194/// kind (class vs plain function vs builtin) using the existing instance.
7195/// Run the parent constructor against `inst`.
7196///
7197/// Returns the object the parent's `[[Construct]]` produced when that is NOT
7198/// `inst` — a base constructor is allowed to `return` one, and 15.7.15 makes it
7199/// the derived instance too. The caller rebinds `this` to it, so the rest of the
7200/// derived constructor writes to the object `new` will hand back.
7201pub fn super_construct(
7202 parent: &Value,
7203 args: Vec<Value>,
7204 inst: &Value,
7205 new_target: &Value,
7206) -> Result<Option<Value>, String> {
7207 match with_host(|h| h.get(parent).cloned()) {
7208 Some(JsObj::Class(pcv)) => Ok(run_class_ctor(&pcv, inst, args, new_target)?
7209 .filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst)))),
7210 Some(JsObj::Func(fv)) => {
7211 let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7212 Ok(Some(r).filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst))))
7213 }
7214 Some(JsObj::Builtin(name)) => {
7215 let built = crate::builtins::construct_builtin(&name, args)?;
7216 // An EXOTIC parent (`class A extends Array`) keeps its behaviour in
7217 // the heap variant, not in a property map, so copying own props
7218 // cannot carry it: the instance has to BECOME the built object.
7219 // Without this `new (class extends Array {})().push` was not a
7220 // function, and the same for Map, Set, RegExp, Promise and
7221 // Function — subclassing a builtin produced a plain object.
7222 if !become_exotic(inst, &built) {
7223 // An `Error` subclass is ordinary: its state IS own properties.
7224 adopt_own_props(inst, &built);
7225 }
7226 Ok(None)
7227 }
7228 // A Proxy parent (`class D extends new Proxy(B, {})`): `super(…)` is
7229 // `[[Construct]]` on the proxy, so the `construct` trap runs (or forwards
7230 // to the target). node-js initializes an ALREADY-allocated `inst` rather
7231 // than adopting the constructor's return value, so what the proxy built
7232 // is moved across — the same move the builtin arm makes.
7233 Some(JsObj::Proxy { .. }) => {
7234 let built = construct_nt(parent, args, new_target.clone())?;
7235 if !become_exotic(inst, &built) {
7236 adopt_own_props(inst, &built);
7237 }
7238 Ok(None)
7239 }
7240 _ => Err(type_error("super is not a constructor")),
7241 }
7242}
7243
7244/// Move `built`'s own properties (and their attributes) onto `inst`. Used where
7245/// a parent constructor produces a fresh object but node-js's class model has
7246/// already allocated the instance `this` is bound to.
7247/// Replace `inst`'s heap object with `built`'s, so an instance whose class
7248/// extends a builtin EXOTIC really is one.
7249///
7250/// `inst` keeps its identity and its prototype link — the leaf class's
7251/// prototype, which is what method resolution and `instanceof` walk — while its
7252/// contents become the exotic the parent constructor produced. The side tables
7253/// keyed by heap index (array holes, property attributes, the fn-prop table)
7254/// move across with it.
7255///
7256/// Returns false for a variant whose state is ordinary own properties
7257/// (`Error`), which the caller copies instead.
7258fn become_exotic(inst: &Value, built: &Value) -> bool {
7259 let exotic = matches!(
7260 with_host(|h| h.get(built).cloned()),
7261 Some(JsObj::Array(_))
7262 | Some(JsObj::Map { .. })
7263 | Some(JsObj::Set { .. })
7264 | Some(JsObj::RegExp(_))
7265 | Some(JsObj::Promise { .. })
7266 | Some(JsObj::Func(_))
7267 | Some(JsObj::Str(_))
7268 | Some(JsObj::BigInt(_))
7269 | Some(JsObj::Symbol { .. })
7270 );
7271 if !exotic {
7272 return false;
7273 }
7274 let (Value::Obj(dst), Value::Obj(src)) = (inst, built) else {
7275 return false;
7276 };
7277 let (dst, src) = (*dst, *src);
7278 with_host(|h| {
7279 if let Some(obj) = h.get(built).cloned() {
7280 if let Some(slot) = h.get_mut(inst) {
7281 *slot = obj;
7282 }
7283 }
7284 h.move_index_state(src, dst);
7285 });
7286 true
7287}
7288
7289fn adopt_own_props(inst: &Value, built: &Value) {
7290 let entries: Vec<(String, Value)> = with_host(|h| match h.get(built) {
7291 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
7292 _ => Vec::new(),
7293 });
7294 with_host(|h| {
7295 let keys: Vec<String> = entries.iter().map(|(k, _)| k.clone()).collect();
7296 if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7297 for (k, v) in entries {
7298 props.insert(k, v);
7299 }
7300 canonicalize_own_keys(props);
7301 }
7302 // The copied slots keep the attributes the source gave them, so
7303 // `class E extends Error` instances hide `message`/`stack` too.
7304 for k in keys {
7305 let a = h.prop_attrs(built, &k);
7306 h.set_prop_attrs(inst, &k, a);
7307 }
7308 });
7309}
7310
7311// ── class construction (runtime) ─────────────────────────────────────────────
7312
7313/// Build a class constructor value from its parts. The compiler emits (via
7314/// `MKCLASS`) the evaluated parent (or undefined) and the constructor closure (or
7315/// undefined for a default constructor); methods/getters/setters/statics/fields
7316/// are installed afterward by `DEF_MEMBER`/`DEF_FIELD`.
7317pub fn build_class(name: &str, parent: Value, ctor: Value) -> Value {
7318 // A Proxy parent (`class D extends new Proxy(B, {})`): `D.prototype`'s
7319 // `[[Prototype]]` is `Get(parent, "prototype")` — a read that runs the `get`
7320 // trap and so re-enters the host, which the borrow below cannot allow.
7321 // Without it the link fell back to `Object.prototype` and every inherited
7322 // method went missing.
7323 let proxy_parent_proto = (with_host(|h| h.kind_of(&parent)) == Some(ObjKind::Proxy))
7324 .then(|| crate::builtins::get_property(&parent, "prototype").ok())
7325 .flatten();
7326 with_host(|h| {
7327 let parent_opt = if matches!(parent, Value::Undef) {
7328 None
7329 } else {
7330 Some(parent.clone())
7331 };
7332 // The class prototype delegates to the parent's prototype (or
7333 // Object.prototype for a base class). Extending a builtin error links to
7334 // that error's prototype so `instanceof Error` holds for the subclass.
7335 let parent_proto = match &parent_opt {
7336 Some(_) if proxy_parent_proto.is_some() => {
7337 proxy_parent_proto.clone().expect("checked is_some")
7338 }
7339 Some(p) => match h.get(p).cloned() {
7340 Some(JsObj::Class(pc)) => pc.proto.clone(),
7341 Some(JsObj::Builtin(bn)) => {
7342 h.ensure_error_protos();
7343 h.ensure_native_protos();
7344 // `class S extends String {}` links to the REAL
7345 // `String.prototype`, the same way an error subclass links
7346 // to its error prototype. Without it `S.prototype`'s
7347 // `[[Prototype]]` fell back to `Object.prototype`, so
7348 // `new S("hi") instanceof String` read false and
7349 // `String(new S("hi"))` reported `[object String]` instead
7350 // of `hi`.
7351 error_proto_of(h, &bn)
7352 .or_else(|| h.native_proto(&bn))
7353 .or_else(|| h.fn_prop(p, "prototype"))
7354 .unwrap_or_else(|| h.object_proto())
7355 }
7356 _ => h
7357 .fn_prop(p, "prototype")
7358 .unwrap_or_else(|| h.object_proto()),
7359 },
7360 None => h.object_proto(),
7361 };
7362 let proto = h.new_object(IndexMap::new());
7363 h.set_proto(&proto, parent_proto);
7364 let ctor_opt = if matches!(ctor, Value::Undef) {
7365 None
7366 } else {
7367 Some(ctor.clone())
7368 };
7369 // Give the constructor closure its home class (for `super.method()`), and
7370 // record its `.name`.
7371 if let Some(cf) = &ctor_opt {
7372 if let Some(JsObj::Func(f)) = h.get_mut(cf) {
7373 f.home_class = Some(name.to_string());
7374 }
7375 }
7376 let cval = ClassVal {
7377 name: name.to_string(),
7378 ctor: ctor_opt,
7379 parent: parent_opt,
7380 proto: proto.clone(),
7381 statics: IndexMap::new(),
7382 fields: Vec::new(),
7383 };
7384 let class_val = h.alloc(JsObj::Class(cval));
7385 h.class_registry.insert(name.to_string(), class_val.clone());
7386 // Link prototype → class (for instance display + `constructor`), and give
7387 // the class its own `prototype` fn-prop so `C.prototype` reads work.
7388 h.tag_proto_class(&proto, class_val.clone());
7389 h.set_fn_prop(&class_val, "prototype", proto.clone());
7390 // `Class.prototype.constructor === Class`.
7391 if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
7392 p.insert("constructor".to_string(), class_val.clone());
7393 }
7394 h.hide_prop(&proto, "constructor");
7395 class_val
7396 })
7397}
7398
7399/// Install a method / getter / setter on a class (`DEF_MEMBER`). `kind` is a
7400/// `member::*` tag; `is_static` targets the constructor side.
7401pub fn define_member(class_val: &Value, name: &str, kind: i64, is_static: bool, func: Value) {
7402 with_host(|h| {
7403 let cname = match h.get(class_val) {
7404 Some(JsObj::Class(c)) => c.name.clone(),
7405 _ => String::new(),
7406 };
7407 // A private method/accessor: remember which class declared it, so a
7408 // brand-check failure can name the class the way node does. A static
7409 // FIELD is data, not a method, so it keeps the field wording.
7410 if name.starts_with('#') && kind != member::STATIC_FIELD {
7411 h.note_private_method(name);
7412 }
7413 // Give the method its home class for `super.x()`, and record whether it
7414 // is static — `super` resolves against a different object either way.
7415 if let Some(JsObj::Func(f)) = h.get_mut(&func) {
7416 f.home_class = Some(cname);
7417 f.home_static = is_static;
7418 }
7419 // Static members live on the constructor (fn-props / static accessors);
7420 // instance members on the prototype.
7421 let target = if is_static {
7422 class_val.clone()
7423 } else {
7424 match h.get(class_val) {
7425 Some(JsObj::Class(c)) => c.proto.clone(),
7426 _ => return,
7427 }
7428 };
7429 match kind {
7430 member::GET => h.set_accessor(&target, name, Some(func), None),
7431 member::SET => h.set_accessor(&target, name, None, Some(func)),
7432 _ => {
7433 // A static field is enumerable (`Object.keys(C)` lists it) unlike
7434 // a method, so it must not reach the `hide_prop` below.
7435 if kind == member::STATIC_FIELD {
7436 if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7437 c.statics.insert(name.to_string(), func.clone());
7438 }
7439 h.set_fn_prop(class_val, name, func);
7440 return;
7441 }
7442 if is_static {
7443 if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7444 c.statics.insert(name.to_string(), func.clone());
7445 }
7446 h.set_fn_prop(class_val, name, func);
7447 } else if let Some(JsObj::Object(p)) = h.get_mut(&target) {
7448 p.insert(name.to_string(), func);
7449 }
7450 }
7451 }
7452 // Class methods and accessors are non-enumerable (ES2015 ClassDefinition-
7453 // Evaluation), so `for (k in instance)` walking the prototype chain never
7454 // yields them and `Object.keys(C.prototype)` is empty.
7455 h.hide_prop(&target, name);
7456 });
7457}
7458
7459/// Register an instance-field initializer thunk on a class (`DEF_FIELD`).
7460pub fn define_field(class_val: &Value, name: &str, thunk: Value, name_anon: bool) {
7461 with_host(|h| {
7462 if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7463 c.fields.push((name.to_string(), thunk, name_anon));
7464 }
7465 });
7466}
7467
7468/// The `[[Prototype]]` object a constructor value hands to its instances
7469/// (`Ctor.prototype`), for `instanceof`.
7470fn ctor_prototype(h: &JsHost, ctor: &Value) -> Option<Value> {
7471 match h.get(ctor) {
7472 Some(JsObj::Class(c)) => Some(c.proto.clone()),
7473 Some(JsObj::Func(_)) => h.fn_prop(ctor, "prototype"),
7474 // A builtin's prototype lives in one of two registries: the error
7475 // prototypes, or the native exotic prototypes (`Buffer.prototype`,
7476 // `Uint8Array.prototype`). Consulting only the first made `instanceof`
7477 // blind to the real `Buffer.prototype → Uint8Array.prototype` chain, so
7478 // `Buffer.prototype instanceof Uint8Array` read false even though the
7479 // link was there — the instance case only passed via a native-tag
7480 // special case, which a prototype object does not carry.
7481 Some(JsObj::Builtin(name)) => h
7482 .error_protos
7483 .get(name)
7484 .or_else(|| h.native_protos.get(name))
7485 .cloned(),
7486 Some(JsObj::BoundFunc { target, .. }) => ctor_prototype(h, &target.clone()),
7487 _ => None,
7488 }
7489}
7490
7491/// `ctor.prototype` in the SAME representation `builtins::prototype_of` yields,
7492/// so a chain walk driven by that function can compare the two with `strict_eq`.
7493///
7494/// `ctor_prototype` answers only for the constructors whose prototype object
7495/// really exists on the heap (classes, user functions, the error and native
7496/// exotics). A bare builtin like `Object`/`Array` has none there — its instances
7497/// report `h.object_proto()` / a `Builtin("<C>.prototype")` handle — so this
7498/// mirrors that fallback rather than reporting "no prototype" and failing every
7499/// comparison.
7500fn walk_target_prototype(ctor: &Value) -> Option<Value> {
7501 if let Some(p) = with_host(|h| ctor_prototype(h, ctor)) {
7502 return Some(p);
7503 }
7504 let name = with_host(|h| match h.get(ctor) {
7505 Some(JsObj::Builtin(n)) => Some(n.clone()),
7506 _ => None,
7507 })?;
7508 if name == "Object" {
7509 return Some(with_host(|h| h.object_proto()));
7510 }
7511 Some(with_host(|h| {
7512 h.alloc(JsObj::Builtin(format!("{name}.prototype")))
7513 }))
7514}
7515
7516/// V8's "not a function" wording for a value that was expected to be callable.
7517/// A number/string/boolean is named WITH its value (`number 1 is not a
7518/// function`, `string "s" is not a function`); every other type is named by type
7519/// alone (`object is not a function`, `symbol is not a function`).
7520pub fn not_a_function_message(v: &Value) -> String {
7521 with_host(|h| match v {
7522 Value::Undef => "undefined is not a function".into(),
7523 Value::Bool(b) => format!("boolean {b} is not a function"),
7524 Value::Int(_) | Value::Float(_) => format!("number {} is not a function", h.str_of(v)),
7525 Value::Str(s) => format!("string \"{s}\" is not a function"),
7526 Value::Obj(_) => match h.get(v) {
7527 Some(JsObj::Str(s)) => format!("string \"{s}\" is not a function"),
7528 Some(JsObj::Symbol { .. }) => "symbol is not a function".into(),
7529 Some(JsObj::BigInt(_)) => "bigint is not a function".into(),
7530 _ => "object is not a function".into(),
7531 },
7532 _ => "object is not a function".into(),
7533 })
7534}
7535
7536/// `obj instanceof ctor` — walk `obj`'s prototype chain looking for
7537/// `ctor.prototype`.
7538pub fn instance_of(obj: &Value, ctor: &Value) -> Result<bool, String> {
7539 // 13.10.2 InstanceofOperator step 3: a `Symbol.hasInstance` method on the
7540 // right-hand side REPLACES the prototype-chain walk entirely, and it is
7541 // consulted before the callability check — which is why a plain (uncallable)
7542 // object that defines it is a legal `instanceof` right-hand side.
7543 if matches!(ctor, Value::Obj(_)) {
7544 // `class C { static [Symbol.hasInstance](){} }` and a method defined on a
7545 // plain function both land in the fn-prop side table (which
7546 // `class_static` reads, following the `extends` chain), NOT in an object
7547 // property map — so consulting only `lookup_chain` would find the object
7548 // literal form and silently miss the two forms V8 users actually write.
7549 let handler = match with_host(|h| h.class_static(ctor, "@@hasInstance")) {
7550 Some(f) => Some(f),
7551 None => protocol_lookup(ctor, "@@hasInstance")?,
7552 };
7553 // GetMethod (7.3.11) treats only `undefined`/`null` as "absent"; anything
7554 // else that is not callable is a TypeError, so a data property here does
7555 // NOT fall back to the prototype walk.
7556 match handler {
7557 Some(f) if with_host(|h| is_callable(h, &f)) => {
7558 let r = invoke(&f, vec![obj.clone()], Some(ctor.clone()))?;
7559 return Ok(with_host(|h| h.truthy(&r)));
7560 }
7561 Some(f)
7562 if !matches!(f, Value::Undef)
7563 && !with_host(|h| matches!(h.get(&f), Some(JsObj::Null))) =>
7564 {
7565 return Err(type_error(¬_a_function_message(&f)));
7566 }
7567 _ => {}
7568 }
7569 }
7570 // 13.10.2 InstanceofOperator validates the RIGHT-hand side FIRST, so
7571 // `1 instanceof 3` throws even though the left side could never match.
7572 // Returning early on the left side skipped that check entirely.
7573 let ctor_callable = with_host(|h| {
7574 matches!(
7575 h.get(ctor),
7576 Some(JsObj::Func(_))
7577 | Some(JsObj::Class(_))
7578 | Some(JsObj::Builtin(_))
7579 | Some(JsObj::BoundFunc { .. })
7580 )
7581 });
7582 if !ctor_callable {
7583 // V8 has TWO messages here and they are not interchangeable: a primitive
7584 // right-hand side is "not an object", an object that is merely not
7585 // callable is "not callable". Only the second was implemented, so
7586 // `1 instanceof 3` reported nothing at all.
7587 return Err(type_error(if with_host(|h| !is_primitive(h, ctor)) {
7588 "Right-hand side of 'instanceof' is not callable"
7589 } else {
7590 "Right-hand side of 'instanceof' is not an object"
7591 }));
7592 }
7593 // A non-object left-hand side is never an instance — but only after the
7594 // right-hand side has been validated above.
7595 if !matches!(obj, Value::Obj(_)) {
7596 return Ok(false);
7597 }
7598 // A Proxy shares no heap variant with its target, so the structural arms
7599 // below would misclassify it. 10.5.3 says `OrdinaryHasInstance` walks
7600 // `[[GetPrototypeOf]]`, i.e. the handler's `getPrototypeOf` trap — run that
7601 // walk here, which also gives a custom trap the final say.
7602 if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Proxy) {
7603 with_host(|h| {
7604 h.ensure_error_protos();
7605 h.ensure_native_protos();
7606 });
7607 let Some(target) = walk_target_prototype(ctor) else {
7608 return Ok(false);
7609 };
7610 let mut cur = crate::proxy::get_prototype_of(obj)?.unwrap_or(Value::Undef);
7611 for _ in 0..100 {
7612 if matches!(cur, Value::Undef) || with_host(|h| h.is_null(&cur)) {
7613 return Ok(false);
7614 }
7615 if with_host(|h| h.strict_eq(&cur, &target)) {
7616 return Ok(true);
7617 }
7618 cur = crate::builtins::prototype_of(&cur);
7619 }
7620 return Ok(false);
7621 }
7622 // Builtin constructors whose instances aren't prototype-linked in our model
7623 // (arrays/plain objects/functions) get a structural instanceof.
7624 if let Some(JsObj::Builtin(name)) = with_host(|h| h.get(ctor).cloned()) {
7625 // …but an object whose chain PASSES THROUGH the intrinsic prototype is
7626 // an instance regardless of its own kind, which is the whole of the ES5
7627 // subclassing pattern: `F.prototype = Object.create(Array.prototype)`
7628 // makes `new F() instanceof Array` true. A structural test alone said
7629 // false.
7630 if crate::builtins::chain_intrinsic_ctors_pub(obj).contains(&name.as_str()) {
7631 return Ok(true);
7632 }
7633 let kind = with_host(|h| h.get(obj).cloned());
7634 match name.as_str() {
7635 "Array" => return Ok(matches!(kind, Some(JsObj::Array(_)))),
7636 "Function" => return Ok(with_host(|h| is_callable(h, obj))),
7637 // Map/Set/Promise instances are distinct heap variants, not
7638 // prototype-linked, so match them structurally (a WeakMap/WeakSet is a
7639 // Map/Set with `weak: true`, so `weakMap instanceof Map` is false).
7640 "Map" => return Ok(matches!(kind, Some(JsObj::Map { weak: false, .. }))),
7641 "WeakMap" => return Ok(matches!(kind, Some(JsObj::Map { weak: true, .. }))),
7642 "Set" => return Ok(matches!(kind, Some(JsObj::Set { weak: false, .. }))),
7643 "WeakSet" => return Ok(matches!(kind, Some(JsObj::Set { weak: true, .. }))),
7644 "Promise" => return Ok(matches!(kind, Some(JsObj::Promise { .. }))),
7645 // A RegExp is its own heap variant too, not a prototype-linked object.
7646 "RegExp" => return Ok(matches!(kind, Some(JsObj::RegExp(_)))),
7647 "Object" => {
7648 // Everything object-typed except a null-prototype object is an
7649 // Object instance.
7650 let is_obj = matches!(
7651 kind,
7652 Some(JsObj::Object(_))
7653 | Some(JsObj::Array(_))
7654 // A namespace object and a builtin function are both
7655 // `instanceof Object`: `Math instanceof Object` is true.
7656 | Some(JsObj::Builtin(_))
7657 | Some(JsObj::Func(_))
7658 | Some(JsObj::Class(_))
7659 | Some(JsObj::Map { .. })
7660 | Some(JsObj::Set { .. })
7661 | Some(JsObj::Promise { .. })
7662 | Some(JsObj::Generator { .. })
7663 | Some(JsObj::RegExp(_))
7664 );
7665 if is_obj {
7666 // A null-prototype object (Object.create(null) or
7667 // setPrototypeOf(o, null)) is NOT an Object instance.
7668 if with_host(|h| h.has_null_proto(obj)) {
7669 return Ok(false);
7670 }
7671 return Ok(true);
7672 }
7673 return Ok(false);
7674 }
7675 // A Node `Buffer` IS a `Uint8Array` subclass instance.
7676 "Uint8Array" if crate::stdlib::native_tag(obj).as_deref() == Some("Buffer") => {
7677 return Ok(true);
7678 }
7679 // Every typed array carries the same `TypedArray` tag; the constructor
7680 // it is an instance of is its ELEMENT KIND.
7681 k if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") => {
7682 return Ok(crate::stdlib::typedarray::kind_of(obj) == k);
7683 }
7684 // A native-tagged instance (`WeakRef`, `FinalizationRegistry`,
7685 // `TextEncoder`, …) is an instance of the builtin whose name matches
7686 // its hidden `@@native` tag.
7687 other => {
7688 if crate::stdlib::native_tag(obj).as_deref() == Some(other) {
7689 return Ok(true);
7690 }
7691 }
7692 }
7693 }
7694 with_host(|h| h.ensure_error_protos());
7695 // The native exotic prototypes are built lazily; `instanceof` may be the
7696 // first thing to ask for them, so materialise them before the chain walk.
7697 with_host(|h| h.ensure_native_protos());
7698 let target = match with_host(|h| ctor_prototype(h, ctor)) {
7699 Some(p) => p,
7700 None => return Ok(false),
7701 };
7702 let mut cur = with_host(|h| h.proto_of(obj));
7703 while let Some(p) = cur {
7704 if with_host(|h| h.strict_eq(&p, &target)) {
7705 return Ok(true);
7706 }
7707 cur = with_host(|h| h.proto_of(&p));
7708 }
7709 Ok(false)
7710}
7711
7712// ── generators (stackful coroutines, same-thread via corosensei) ─────────────
7713
7714impl JsHost {
7715 /// Swap the volatile execution context in one shot, returning the previous
7716 /// one — installs a generator's context on resume, pulls it back on suspend.
7717 fn install_gen_ctx(&mut self, mut c: GenContext) -> GenContext {
7718 std::mem::swap(&mut self.frames, &mut c.frames);
7719 std::mem::swap(&mut self.error, &mut c.error);
7720 std::mem::swap(&mut self.exc, &mut c.exc);
7721 std::mem::swap(&mut self.signal, &mut c.signal);
7722 c
7723 }
7724 pub fn is_generator_val(&self, v: &Value) -> bool {
7725 matches!(self.get(v), Some(JsObj::Generator { .. }))
7726 }
7727 /// Whether `v` is an ASYNC generator object — the borrow-free form of
7728 /// [`is_async_generator`], usable from code already holding the host.
7729 pub fn is_async_gen_val(&self, v: &Value) -> bool {
7730 match self.get(v) {
7731 Some(JsObj::Generator { id }) => self
7732 .generators
7733 .get(*id as usize)
7734 .map(|g| g.async_gen)
7735 .unwrap_or(false),
7736 _ => false,
7737 }
7738 }
7739 pub fn gen_done(&self, id: u32) -> bool {
7740 self.generators
7741 .get(id as usize)
7742 .map(|g| g.done)
7743 .unwrap_or(true)
7744 }
7745 fn gen_started(&self, id: u32) -> bool {
7746 self.generators
7747 .get(id as usize)
7748 .map(|g| g.started)
7749 .unwrap_or(false)
7750 }
7751}
7752
7753/// Build a suspended generator whose body is `chunk`, run in a frame with the
7754/// already-bound `env`. Nothing executes until the first `gen_resume`.
7755fn make_generator(
7756 chunk: Chunk,
7757 env: Env,
7758 this_val: Option<Value>,
7759 home_class: Option<String>,
7760 home_static: bool,
7761 home_object: Option<Value>,
7762 strict: bool,
7763) -> Value {
7764 let home = home_class
7765 .as_ref()
7766 .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
7767 let frame = Frame {
7768 base_env: env.clone(),
7769 env,
7770 this_obj: this_val,
7771 new_target: None,
7772 home_class: home,
7773 home_static,
7774 home_object,
7775 strict,
7776 line: 0,
7777 owner: None,
7778 is_module: false,
7779 this_state: ThisState::Plain,
7780 };
7781 let id = with_host(|h| {
7782 let id = h.generators.len() as u32;
7783 h.generators.push(GenCell {
7784 coro: None,
7785 yielder: std::ptr::null(),
7786 ctx: GenContext {
7787 frames: vec![frame],
7788 ..GenContext::default()
7789 },
7790 done: false,
7791 started: false,
7792 inject: None,
7793 async_gen: false,
7794 queue: std::collections::VecDeque::new(),
7795 running: false,
7796 stack_floor: 0,
7797 });
7798 id
7799 });
7800 let body = move |yielder: &corosensei::Yielder<Value, Value>, _first: Value| {
7801 ensure_coroutine_floor();
7802 // Same thread → publish the yielder so `yield` (deep in the body's VM)
7803 // can reach it. Valid for the whole body lifetime.
7804 with_host(|h| h.generators[id as usize].yielder = yielder as *const _ as *const ());
7805 let r = run_chunk_on(chunk);
7806 // A `return` inside the body leaves a Return signal carrying the final
7807 // value; capture it so `.next()` reports it as the completion value.
7808 let ret = with_host(|h| match h.signal.take() {
7809 Some(Signal::Return(v)) => v,
7810 _ => Value::Undef,
7811 });
7812 r.map(|_| ret)
7813 };
7814 // The body's stack is allocated here rather than left to `Coroutine::new` so
7815 // that its size is ours to choose and, above all, so its `limit()` is known:
7816 // that address is what `stack_exhausted` must compare against while the body
7817 // runs, since a coroutine does NOT run on the thread stack pthread reports.
7818 // A refused reservation still yields a working generator on corosensei's own
7819 // 1 MiB default, with a floor derived on entry instead.
7820 let (coro, floor) = match corosensei::stack::DefaultStack::new(CORO_STACK_SIZE) {
7821 Ok(stack) => {
7822 let floor = coro_stack_floor(&stack);
7823 (corosensei::Coroutine::with_stack(stack, body), floor)
7824 }
7825 Err(_) => (corosensei::Coroutine::new(body), 0),
7826 };
7827 with_host(|h| {
7828 h.generators[id as usize].coro = Some(coro);
7829 h.generators[id as usize].stack_floor = floor;
7830 });
7831 with_host(|h| h.alloc(JsObj::Generator { id }))
7832}
7833
7834/// `yield v` — suspend the running generator, handing `v` to the resumer; returns
7835/// the value the next `gen_resume(x)` supplies (a `.next(x)` argument).
7836pub fn gen_yield(v: Value) -> Result<Value, String> {
7837 let id = match CUR_GEN.with(|c| c.get()) {
7838 Some(id) => id,
7839 None => return Err(type_error("yield outside a generator")),
7840 };
7841 let yp = with_host(|h| h.generators[id as usize].yielder);
7842 // SAFETY: same-thread coroutine; the yielder lives for the whole body, and we
7843 // only reach here from inside that body (its stack is live).
7844 let yielder = unsafe { &*(yp as *const corosensei::Yielder<Value, Value>) };
7845 let sent = yielder.suspend(v);
7846 // On resume, a `.return(v)`/`.throw(e)` may have queued a forced completion:
7847 // convert it into a Return signal / thrown value so the body unwinds and any
7848 // `finally` runs, exactly as a source-level `return`/`throw` would.
7849 if let Some(inj) = with_host(|h| h.generators[id as usize].inject.take()) {
7850 match inj {
7851 GenInject::Return(rv) => {
7852 with_host(|h| h.signal = Some(Signal::Return(rv)));
7853 return Ok(Value::Undef);
7854 }
7855 GenInject::Throw(ev) => {
7856 let msg = with_host(|h| crate::builtins::error_string(h, &ev));
7857 with_host(|h| h.exc = Some(ev));
7858 return Err(msg);
7859 }
7860 }
7861 }
7862 Ok(sent)
7863}
7864
7865/// `generator.return(v)`: force the generator to complete, running any pending
7866/// `finally`. If it is already done (or never started) it just reports
7867/// `{value:v, done:true}` without executing the body.
7868pub fn gen_return(gen: &Value, v: Value) -> Result<GenStep, String> {
7869 let id = match with_host(|h| h.get(gen).cloned()) {
7870 Some(JsObj::Generator { id }) => id,
7871 _ => return Err(type_error("not a generator")),
7872 };
7873 // Not started yet (coro present, ctx never resumed) OR already done → no body
7874 // to unwind: complete immediately with the supplied value.
7875 let started = with_host(|h| h.gen_started(id));
7876 if with_host(|h| h.generators[id as usize].done) || !started {
7877 with_host(|h| h.generators[id as usize].done = true);
7878 return Ok(GenStep::Done(v));
7879 }
7880 with_host(|h| h.generators[id as usize].inject = Some(GenInject::Return(v)));
7881 gen_resume(gen, Value::Undef)
7882}
7883
7884/// `generator.throw(e)`: inject a throw at the suspension point, running any
7885/// pending `finally` and letting an enclosing `try/catch` in the body handle it.
7886pub fn gen_throw(gen: &Value, e: Value) -> Result<GenStep, String> {
7887 let id = match with_host(|h| h.get(gen).cloned()) {
7888 Some(JsObj::Generator { id }) => id,
7889 _ => return Err(type_error("not a generator")),
7890 };
7891 let started = with_host(|h| h.gen_started(id));
7892 if with_host(|h| h.generators[id as usize].done) || !started {
7893 // A throw into a done/unstarted generator propagates to the caller.
7894 with_host(|h| h.generators[id as usize].done = true);
7895 let msg = with_host(|h| crate::builtins::error_string(h, &e));
7896 with_host(|h| h.exc = Some(e));
7897 return Err(msg);
7898 }
7899 with_host(|h| h.generators[id as usize].inject = Some(GenInject::Throw(e)));
7900 gen_resume(gen, Value::Undef)
7901}
7902
7903/// Outcome of resuming a generator: a yielded value (not done), or the final
7904/// completion value (done).
7905pub enum GenStep {
7906 Yield(Value),
7907 Done(Value),
7908}
7909
7910/// Resume a generator until its next `yield` or its body returns. Preserves the
7911/// shared host: the coroutine is taken out so the body re-enters `with_host`
7912/// freely, and the volatile context is swapped so the caller's frames/signal
7913/// survive the switch.
7914pub fn gen_resume(gen: &Value, send: Value) -> Result<GenStep, String> {
7915 let id = match with_host(|h| h.get(gen).cloned()) {
7916 Some(JsObj::Generator { id }) => id,
7917 _ => return Err(type_error("not a generator")),
7918 };
7919 if with_host(|h| h.generators[id as usize].done) {
7920 return Ok(GenStep::Done(Value::Undef));
7921 }
7922 let mut coro = match with_host(|h| h.generators[id as usize].coro.take()) {
7923 Some(c) => c,
7924 None => return Err("TypeError: generator already executing".into()),
7925 };
7926 with_host(|h| h.generators[id as usize].started = true);
7927 let gen_ctx = with_host(|h| std::mem::take(&mut h.generators[id as usize].ctx));
7928 let caller_ctx = with_host(|h| h.install_gen_ctx(gen_ctx));
7929 let prev = CUR_GEN.with(|c| c.replace(Some(id)));
7930 // The body runs on the coroutine's OWN stack, so the guard's floor has to
7931 // move with it and move back on suspend — generators nest, and a resume from
7932 // inside another generator must restore that one's floor, not the thread's.
7933 let coro_floor = with_host(|h| h.generators[id as usize].stack_floor);
7934 let caller_floor = swap_stack_floor(coro_floor);
7935
7936 let out = coro.resume(send); // no host borrow held; body drives its own VM
7937
7938 let measured = swap_stack_floor(caller_floor);
7939 // A coroutine on corosensei's default stack has no known bounds, so the
7940 // floor it measured for itself on first entry is kept for later resumes.
7941 if coro_floor == 0 && measured != 0 {
7942 with_host(|h| h.generators[id as usize].stack_floor = measured);
7943 }
7944 CUR_GEN.with(|c| c.set(prev));
7945 let mut gen_ctx = with_host(|h| h.install_gen_ctx(caller_ctx));
7946 // A `throw` inside the body left the thrown VALUE in the generator's context,
7947 // which the swap above just stashed away. Hand it to the caller so the
7948 // rejection/catch keeps the original error object instead of a string rebuild.
7949 let thrown = gen_ctx.exc.take();
7950 with_host(|h| {
7951 if let Some(v) = thrown {
7952 h.exc = Some(v);
7953 }
7954 h.generators[id as usize].ctx = gen_ctx;
7955 h.generators[id as usize].coro = Some(coro);
7956 });
7957
7958 match out {
7959 corosensei::CoroutineResult::Yield(y) => Ok(GenStep::Yield(y)),
7960 corosensei::CoroutineResult::Return(r) => {
7961 // Release the coroutine — and with it the mmap'd stack it owns —
7962 // the moment the body completes. `h.generators` only ever grows (an
7963 // id is never reused), so a program that awaits in a loop otherwise
7964 // accumulates one whole [`CORO_STACK_SIZE`] reservation per call for
7965 // the life of the process. A finished generator is never resumed:
7966 // `gen_resume` returns `Done` on the `done` flag before it looks.
7967 with_host(|h| {
7968 let g = &mut h.generators[id as usize];
7969 g.done = true;
7970 g.coro = None;
7971 });
7972 match r {
7973 Ok(v) => Ok(GenStep::Done(v)),
7974 Err(e) => Err(e),
7975 }
7976 }
7977 }
7978}
7979
7980/// Force a generator to completion (used by `.return()` and abandoned loops):
7981/// marks it done without running further.
7982pub fn gen_close(gen: &Value) {
7983 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(gen).cloned()) {
7984 with_host(|h| h.generators[id as usize].done = true);
7985 }
7986}
7987
7988// ── iteration protocol (arrays, strings, Map/Set, generators, Symbol.iterator) ─
7989
7990/// Convert a Map/Set key value into a `MapKey` under SameValueZero.
7991pub fn map_key(h: &JsHost, v: &Value) -> MapKey {
7992 match v {
7993 Value::Undef => MapKey::Undef,
7994 Value::Bool(b) => MapKey::Bool(*b),
7995 Value::Int(n) => MapKey::Num(norm_num_bits(*n as f64)),
7996 Value::Float(f) => MapKey::Num(norm_num_bits(*f)),
7997 Value::Str(s) => MapKey::Str((**s).clone()),
7998 Value::Obj(i) => match h.get(v) {
7999 Some(JsObj::Str(s)) => MapKey::Str(s.clone()),
8000 Some(JsObj::Null) => MapKey::Null,
8001 Some(JsObj::BigInt(b)) => MapKey::Big(b.to_string()),
8002 _ => MapKey::Ref(*i),
8003 },
8004 _ => MapKey::Undef,
8005 }
8006}
8007
8008/// Canonical bit pattern for a Map/Set numeric key: `NaN` → one value, `-0` → `+0`.
8009fn norm_num_bits(f: f64) -> u64 {
8010 if f.is_nan() {
8011 return f64::NAN.to_bits();
8012 }
8013 if f == 0.0 {
8014 return 0.0f64.to_bits(); // fold -0 into +0
8015 }
8016 f.to_bits()
8017}
8018
8019/// Fully materialize any iterable into a vector of values.
8020/// Pull at most `n` values, then close the iterator — 8.6.2
8021/// IteratorBindingInitialization, which is what an array destructuring pattern
8022/// without a `...rest` element performs.
8023///
8024/// The distinction from [`iter_all`] is not an optimization. A pattern names a
8025/// fixed number of targets, so the spec pulls exactly that many and calls
8026/// IteratorClose on whatever is left; draining instead made
8027///
8028/// ```text
8029/// const [first] = infiniteGenerator();
8030/// ```
8031///
8032/// run forever. It is also observable on any finite iterator, as the count of
8033/// `next()` calls and whether `return()` ever ran.
8034///
8035/// A `...rest` element genuinely consumes the remainder, so those patterns keep
8036/// using `iter_all` and an unbounded source hangs there in node too.
8037pub fn iter_take(v: &Value, n: usize) -> Result<Vec<Value>, String> {
8038 // A Proxy iterates through its traps, which materialize eagerly; there is
8039 // no step-wise form to bound, so this keeps the draining behaviour.
8040 if let Some(items) = crate::proxy::iterate(v)? {
8041 return Ok(items.into_iter().take(n).collect());
8042 }
8043 if with_host(|h| h.is_generator_val(v)) {
8044 let mut out = Vec::new();
8045 while out.len() < n {
8046 match gen_resume(v, Value::Undef)? {
8047 GenStep::Yield(x) => out.push(x),
8048 _ => return Ok(out), // ran out on its own; nothing left to close
8049 }
8050 }
8051 // Stopped early: `.return()` resumes it at the yield so `finally` runs.
8052 let _ = gen_return(v, Value::Undef);
8053 return Ok(out);
8054 }
8055 if let Some(iter_fn) = user_iterator_fn(v) {
8056 let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8057 let mut out = Vec::new();
8058 while out.len() < n {
8059 let step = call_method(&iterator, "next", Vec::new())?;
8060 // Read first: resolving the property re-enters the host, so doing
8061 // it inside the `with_host` closure double-borrows and aborts.
8062 let done = get_prop_chain(&step, "done")?;
8063 if with_host(|h| h.truthy(&done)) {
8064 return Ok(out);
8065 }
8066 out.push(get_prop_chain(&step, "value")?);
8067 }
8068 // IteratorClose: `return` is optional on the protocol, and a throw from
8069 // it is swallowed here the way a normal (non-abrupt) completion does.
8070 if let Ok(ret) = get_prop_chain(&iterator, "return") {
8071 if with_host(|h| is_callable(h, &ret)) {
8072 let _ = invoke(&ret, Vec::new(), Some(iterator.clone()));
8073 }
8074 }
8075 return Ok(out);
8076 }
8077 // Arrays, strings, Map/Set: already materialized, and their built-in
8078 // iterators carry no `return`, so there is nothing to close. The same
8079 // reachability rule as `iter_all` applies — this is the DESTRUCTURING
8080 // entry point, and `const [x] = a` bound 1 from an array whose prototype no
8081 // longer carried `Symbol.iterator`.
8082 if !crate::builtins::own_intrinsic_reachable_pub(v) {
8083 let shown = with_host(|h| h.inspect(v));
8084 return Err(type_error(&format!("{shown} is not iterable")));
8085 }
8086 with_host(|h| h.iter_vec(v)).map(|items| items.into_iter().take(n).collect())
8087}
8088
8089pub fn iter_all(v: &Value) -> Result<Vec<Value>, String> {
8090 // A Proxy iterates through its traps (see `crate::proxy::iterate`); it has
8091 // no heap variant `iter_vec` could recognise.
8092 if let Some(items) = crate::proxy::iterate(v)? {
8093 return Ok(items);
8094 }
8095 // Generators / user iterators must resume without a live host borrow.
8096 if with_host(|h| h.is_generator_val(v)) {
8097 let mut out = Vec::new();
8098 while let GenStep::Yield(x) = gen_resume(v, Value::Undef)? {
8099 out.push(x);
8100 }
8101 return Ok(out);
8102 }
8103 // Object with a user-defined Symbol.iterator: drive its iterator protocol.
8104 // Checked BEFORE the reachability guard below, since an own `Symbol
8105 // .iterator` makes a value iterable no matter what its prototype is.
8106 if let Some(iter_fn) = user_iterator_fn(v) {
8107 let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8108 return drain_iterator(&iterator);
8109 }
8110 // The fast paths below read a builtin's backing storage directly, which is
8111 // only legitimate while that builtin's `Symbol.iterator` is still
8112 // reachable: replacing the prototype takes it away, and node then reports
8113 // the value as not iterable. Spread, destructuring and `Array.from`'s
8114 // iterable branch all funnel through here.
8115 if !crate::builtins::own_intrinsic_reachable_pub(v) {
8116 let shown = with_host(|h| h.inspect(v));
8117 return Err(type_error(&format!("{shown} is not iterable")));
8118 }
8119 // A String wrapper iterates its code POINTS, exactly as the primitive does
8120 // (22.1.3.34) — `[...new String("ab")]` is `["a","b"]`, not a TypeError.
8121 if let Some(prim) = crate::builtins::wrapped_primitive(v) {
8122 if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) {
8123 return iter_all(&prim);
8124 }
8125 }
8126 // An array's index ACCESSORS are not in its backing vector, so iterating one
8127 // (spread, `for-of`, `Array.from`) has to resolve them the way the
8128 // `Array.prototype` methods do.
8129 let mut items = with_host(|h| h.iter_vec(v))?;
8130 if with_host(|h| matches!(h.get(v), Some(JsObj::Array(_)))) {
8131 crate::builtins::resolve_index_accessors_pub(v, &mut items);
8132 }
8133 Ok(items)
8134}
8135
8136// ── async iteration (`for await (… of …)`) ───────────────────────────────────
8137
8138/// Obtain an async iterator for `for await`. If `src` has a `Symbol.asyncIterator`
8139/// method, use it (its `.next()` returns a promise of `{value, done}`); otherwise
8140/// fall back to the sync iterable, materialized into a `JsObj::Iter` whose values
8141/// are awaited one at a time by `async_step`.
8142pub fn get_async_iterator(src: &Value) -> Result<Value, String> {
8143 if let Some(f) = user_async_iterator_fn(src) {
8144 return invoke(&f, Vec::new(), Some(src.clone()));
8145 }
8146 // An `async function*` object IS its own async iterator; draining it into a
8147 // list here would run the whole body (and any `finally`) before the consumer
8148 // sees the first value.
8149 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(src).cloned()) {
8150 if with_host(|h| h.generators[id as usize].async_gen) {
8151 return Ok(src.clone());
8152 }
8153 }
8154 let items = iter_all(src)?;
8155 Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
8156}
8157
8158/// If `v` has an own/inherited `Symbol.asyncIterator` method, return it.
8159fn user_async_iterator_fn(v: &Value) -> Option<Value> {
8160 // A PROXY supplies the protocol through its `get` trap and is not a plain
8161 // object, so the shape test below rejects it outright.
8162 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8163 return protocol_lookup(v, "@@asyncIterator")
8164 .ok()
8165 .flatten()
8166 .filter(|f| with_host(|h| is_callable(h, f)));
8167 }
8168 let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8169 if !is_plain {
8170 return None;
8171 }
8172 // Full property resolution, not a stored-property lookup — the same reason
8173 // `user_iterator_fn` does it for the SYNC protocol. A NATIVE-tagged object
8174 // dispatches its methods through the stdlib method table rather than a
8175 // property map, so `lookup_chain` reported no `Symbol.asyncIterator` for one
8176 // even though reading it gives a function: `for await (const v of
8177 // timersPromises.setInterval(…))` said the iterator "is not iterable".
8178 let f = crate::builtins::get_property(v, "@@asyncIterator").ok()?;
8179 with_host(|h| is_callable(h, &f)).then_some(f)
8180}
8181
8182/// One step of a `for await` loop: return a Promise that settles to a
8183/// `{value, done}` record. For a native async iterator this is `iter.next()`
8184/// (already a promise of the record). For the sync fallback it pops the next raw
8185/// value, awaits it, and packages `{value: resolved, done:false}` (or
8186/// `{done:true}` at exhaustion).
8187pub fn async_step(iterator: &Value) -> Result<Value, String> {
8188 // An `async function*` object: resume it through the await-aware driver.
8189 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(iterator).cloned()) {
8190 if with_host(|h| h.generators[id as usize].async_gen) {
8191 return Ok(async_gen_step(iterator, Value::Undef));
8192 }
8193 }
8194 // Sync-fallback iterator: drive it here, awaiting each yielded value.
8195 if let Some(JsObj::Iter { items, idx }) = with_host(|h| h.get(iterator).cloned()) {
8196 if idx >= items.len() {
8197 // `AsyncFromSyncIteratorContinuation` resolves the record THROUGH a
8198 // promise even at exhaustion, so the `done: true` step costs the same
8199 // two microtask ticks a value step does.
8200 let step = with_host(|h| h.new_promise());
8201 let sid = with_host(|h| h.promise_id(&step).unwrap());
8202 with_host(|h| {
8203 h.queue_micro_native(Box::new(move || {
8204 resolve_promise_val(sid, iter_record(Value::Undef, true));
8205 Ok(())
8206 }))
8207 });
8208 return Ok(step);
8209 }
8210 let raw = items[idx].clone();
8211 with_host(|h| {
8212 if let Some(JsObj::Iter { idx, .. }) = h.get_mut(iterator) {
8213 *idx += 1;
8214 }
8215 });
8216 // Await the raw value (adopts a promise's resolution), then wrap.
8217 let step = with_host(|h| h.new_promise());
8218 let sid = with_host(|h| h.promise_id(&step).unwrap());
8219 let raw_p = promise_of(&raw);
8220 let raw_id = with_host(|h| h.promise_id(&raw_p).unwrap());
8221 subscribe_native(
8222 raw_id,
8223 Box::new(move |state, val| {
8224 if state == PromiseState::Rejected {
8225 reject_promise_val(sid, val);
8226 } else {
8227 resolve_promise_val(sid, iter_record(val, false));
8228 }
8229 Ok(())
8230 }),
8231 );
8232 return Ok(step);
8233 }
8234 // Native async iterator: `iter.next()` returns the {value,done} promise.
8235 let r = call_method(iterator, "next", Vec::new())?;
8236 Ok(promise_of(&r))
8237}
8238
8239/// If `v` has an own/inherited `Symbol.iterator` method (internal key
8240/// `@@iterator`), return it. Arrays/strings use the native fast path instead.
8241pub fn user_iterator_fn(v: &Value) -> Option<Value> {
8242 let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8243 if !is_plain {
8244 return None;
8245 }
8246 // Full property resolution, not a stored-property lookup: a NATIVE-tagged
8247 // object (`URLSearchParams`, `Headers`) dispatches its methods through the
8248 // stdlib method table rather than a property map, so `lookup_chain` reported
8249 // no `Symbol.iterator` for one even though reading it gave a function —
8250 // `[...new URLSearchParams('a=1')]` threw `{} is not iterable`.
8251 let f = crate::builtins::get_property(v, "@@iterator").ok()?;
8252 with_host(|h| is_callable(h, &f)).then_some(f)
8253}
8254
8255/// Drive an iterator object (one with a `.next()` returning `{value, done}`) to
8256/// exhaustion.
8257/// Step `src`'s iterator, handing each value to `f`, and CLOSE the iterator if
8258/// `f` exits abruptly (7.4.9 IteratorClose).
8259///
8260/// The difference from `iter_all` + a loop is that this never materializes the
8261/// whole sequence: `Array.from(infinite, mapFn)` where `mapFn` throws has to
8262/// stop at the first call, and draining first means it never gets there at all.
8263pub fn iter_for_each(
8264 src: &Value,
8265 mut f: impl FnMut(Value, usize) -> Result<(), String>,
8266) -> Result<(), String> {
8267 // Only a USER iterator can be infinite or observe its own close; every
8268 // other shape is already a finite materialized sequence.
8269 let Some(iter_fn) = user_iterator_fn(src) else {
8270 for (i, v) in iter_all(src)?.into_iter().enumerate() {
8271 f(v, i)?;
8272 }
8273 return Ok(());
8274 };
8275 let iterator = invoke(&iter_fn, Vec::new(), Some(src.clone()))?;
8276 let mut i = 0usize;
8277 loop {
8278 let step = call_method(&iterator, "next", Vec::new())?;
8279 let done = get_prop_chain(&step, "done")?;
8280 if with_host(|h| h.truthy(&done)) {
8281 return Ok(());
8282 }
8283 let value = get_prop_chain(&step, "value")?;
8284 if let Err(e) = f(value, i) {
8285 // The callback's error wins over anything `return()` raises, so a
8286 // throwing `return` is swallowed here (7.4.9 step 6).
8287 let _ = close_iterator(&iterator);
8288 return Err(e);
8289 }
8290 i += 1;
8291 }
8292}
8293
8294/// Call `iterator.return()` if it has one, as IteratorClose does.
8295pub fn close_iterator(iterator: &Value) -> Result<(), String> {
8296 let has = crate::builtins::get_property(iterator, "return")?;
8297 if with_host(|h| is_callable(h, &has)) {
8298 call_method(iterator, "return", Vec::new())?;
8299 }
8300 Ok(())
8301}
8302
8303pub(crate) fn drain_iterator(iterator: &Value) -> Result<Vec<Value>, String> {
8304 let mut out = Vec::new();
8305 loop {
8306 let step = call_method(iterator, "next", Vec::new())?;
8307 let done = get_prop_chain(&step, "done")?;
8308 if with_host(|h| h.truthy(&done)) {
8309 break;
8310 }
8311 out.push(get_prop_chain(&step, "value")?);
8312 }
8313 Ok(out)
8314}
8315
8316/// Property read that walks the prototype chain (used by iteration helpers).
8317/// Whether the builtin named `n` is CALLABLE. Most are (`Array`, `parseInt`,
8318/// `Math.floor`); the exceptions are the namespace objects a script can only
8319/// read properties off (`Math`, `JSON`, every `require()`d core module), which
8320/// report `typeof === "object"` and carry no `name`/`length`.
8321pub fn builtin_is_callable(n: &str) -> bool {
8322 // A `<Ctor>.prototype` handle is a namespace of methods, not a function:
8323 // `typeof Set.prototype` is `"object"`, and treating it as callable made it
8324 // brand `[object Function]`, inspect as `[Function: prototype]`, and answer
8325 // `true` to `instanceof Function`. `Function.prototype` is the one that
8326 // really IS callable (10.2.4: it is an anonymous built-in that returns
8327 // undefined), which is why it is not stripped here.
8328 if n != "Function.prototype" && n.ends_with(".prototype") {
8329 return false;
8330 }
8331 // A `match` rather than a slice scan: this runs on every callability test,
8332 // which is every call and every `ToPrimitive`, and a `contains` over the
8333 // list below compares against all 56 entries before answering "callable" —
8334 // the common case. The compiler turns the arms into a length-then-bytes
8335 // decision tree instead.
8336 !matches!(
8337 n,
8338 "Math"
8339 | "JSON"
8340 | "console"
8341 | "Reflect"
8342 | "process"
8343 | "Atomics"
8344 | "performance"
8345 | "fs"
8346 | "path"
8347 | "os"
8348 | "util"
8349 | "crypto"
8350 | "webcrypto"
8351 | "SubtleCrypto"
8352 | "querystring"
8353 | "events"
8354 | "timers"
8355 | "perf_hooks"
8356 | "async_hooks"
8357 | "diagnostics_channel"
8358 | "v8"
8359 | "dns"
8360 | "punycode"
8361 | "child_process"
8362 | "tty"
8363 | "url"
8364 | "zlib"
8365 | "string_decoder"
8366 | "http"
8367 | "net"
8368 | "buffer"
8369 | "function"
8370 | "path/win32"
8371 | "fs/promises"
8372 | "stream/promises"
8373 | "stream/consumers"
8374 | "stream/web"
8375 | "timers/promises"
8376 | "dns/promises"
8377 | "https"
8378 | "http2"
8379 | "tls"
8380 | "dgram"
8381 | "cluster"
8382 | "worker_threads"
8383 | "readline"
8384 | "readline/promises"
8385 | "repl"
8386 | "vm"
8387 | "domain"
8388 | "trace_events"
8389 | "wasi"
8390 | "inspector"
8391 | "object"
8392 )
8393 // The live `require.cache` view is a plain object to a script, not
8394 // something it can call.
8395 && n != crate::builtins::REQUIRE_CACHE
8396}
8397
8398pub fn get_prop_chain(recv: &Value, name: &str) -> Result<Value, String> {
8399 crate::builtins::get_property(recv, name)
8400}
8401
8402/// Whether `v` is an ECMAScript primitive, i.e. `ToPrimitive` is the identity
8403/// on it. `undefined`, `null`, booleans, numbers, strings, symbols and bigints
8404/// qualify; every other heap cell (objects, arrays, functions, `Map`/`Set`,
8405/// native-tagged instances) is an object and must be converted.
8406pub fn is_primitive(h: &JsHost, v: &Value) -> bool {
8407 match v {
8408 Value::Obj(_) => matches!(
8409 h.get(v),
8410 None | Some(JsObj::Null)
8411 | Some(JsObj::Str(_))
8412 | Some(JsObj::Symbol { .. })
8413 | Some(JsObj::BigInt(_))
8414 ),
8415 _ => true,
8416 }
8417}
8418
8419/// `ToPrimitive(v, hint)` — ECMA-262 7.1.1. `hint` is `"default"`, `"number"`
8420/// or `"string"`.
8421///
8422/// An object carrying a `Symbol.toPrimitive` method (internal key
8423/// `@@toPrimitive`) has it called with the hint and must return a primitive.
8424/// Otherwise `OrdinaryToPrimitive` (7.1.1.1) tries `valueOf` then `toString` —
8425/// the order reversed for the string hint — and takes the FIRST call whose
8426/// result is a primitive. An object that yields no primitive (a null-prototype
8427/// object has neither method) throws V8's
8428/// `TypeError: Cannot convert object to primitive value`.
8429///
8430/// This is the conversion behind `+`, `-`/`*`/`/`/`%`/`**`, the relational
8431/// operators, `==` against a primitive, and `ToPropertyKey` — all of which used
8432/// to read `str_of` directly and so never invoked a user `valueOf`.
8433pub fn to_primitive(v: &Value, hint: &str) -> Result<Value, String> {
8434 if with_host(|h| is_primitive(h, v)) {
8435 return Ok(v.clone());
8436 }
8437 if let Some(f) = protocol_lookup(v, "@@toPrimitive")? {
8438 if with_host(|h| is_callable(h, &f)) {
8439 let hv = with_host(|h| h.new_str(hint.to_string()));
8440 let r = invoke(&f, vec![hv], Some(v.clone()))?;
8441 if with_host(|h| is_primitive(h, &r)) {
8442 return Ok(r);
8443 }
8444 return Err(type_error("Cannot convert object to primitive value"));
8445 }
8446 }
8447 // `Date.prototype[@@toPrimitive]` (21.4.4.45) treats the DEFAULT hint as
8448 // `"string"`, which is why `new Date() + 1` concatenates while
8449 // `new Date() - 1` is arithmetic.
8450 let hint = if hint == "default" && crate::stdlib::native_tag(v).as_deref() == Some("Date") {
8451 "string"
8452 } else {
8453 hint
8454 };
8455 let order = if hint == "string" {
8456 ["toString", "valueOf"]
8457 } else {
8458 ["valueOf", "toString"]
8459 };
8460 // Whether either candidate was actually CALLED. The `[object Tag]` fallback
8461 // below is for exotics whose property funnel exposes no callable
8462 // `toString`, not for an object whose own methods ran and returned
8463 // non-primitives — that case is the spec's TypeError, and branding it
8464 // instead meant `({ valueOf: () => ({}), toString: () => ({}) }) + 1`
8465 // quietly produced `"[object Object]1"`.
8466 let mut called_any = false;
8467 for m in order {
8468 let f = crate::builtins::get_property(v, m).unwrap_or(Value::Undef);
8469 if !with_host(|h| is_callable(h, &f)) {
8470 continue;
8471 }
8472 called_any = true;
8473 // On a Proxy the resolved method is a thunk bound to the TARGET, so
8474 // invoking it directly would stringify the target — `String(new
8475 // Proxy(function f(){}, {}))` reported `f`'s source where V8 reports the
8476 // native-code form. `call_method` re-dispatches the generic
8477 // `Function.prototype`/`Object.prototype` methods against the proxy.
8478 let r = if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8479 call_method(v, m, Vec::new())?
8480 } else {
8481 invoke(&f, Vec::new(), Some(v.clone()))?
8482 };
8483 if with_host(|h| is_primitive(h, &r)) {
8484 return Ok(r);
8485 }
8486 }
8487 // Every object except a null-prototype one inherits `Object.prototype
8488 // .toString`, which always returns a string — so the exhausted-methods
8489 // TypeError is reachable only there. The exotics whose property funnel has
8490 // no `toString` entry of its own (`Map`, `Set`, `Promise`, …) land here and
8491 // get the same `[object Tag]` brand V8 gives them.
8492 // A proxy WITH a `get` trap is not one of those exotics: the trap answered
8493 // for both method names, and if what came back was not callable there is
8494 // nothing left to call — `String(new Proxy({}, { get: () => undefined }))`
8495 // is a TypeError on node, where branding it `[object Object]` invented a
8496 // conversion the trap explicitly refused. A TRAPLESS proxy is different: its
8497 // read forwarded to the target, so `String(new Proxy(new Map(), {}))` gets
8498 // the target's `[object Map]` brand exactly as the bare `Map` does.
8499 if !called_any && !with_host(|h| h.has_null_proto(v)) && !crate::proxy::has_trap(v, "get") {
8500 return crate::builtins::proto_method(v, "Object:toString", Vec::new());
8501 }
8502 Err(type_error("Cannot convert object to primitive value"))
8503}
8504
8505/// `ToString(v)` with `ToPrimitive` method dispatch: an object is converted
8506/// with the string hint (so a user `toString` — or `valueOf`, if `toString`
8507/// is absent or returns an object — is invoked), then rendered by `str_of`.
8508/// Returns a heap string value.
8509pub fn to_string_value(v: &Value) -> Result<Value, String> {
8510 let p = to_primitive(v, "string")?;
8511 // `ToString(symbol)` throws (7.1.17 step 2) — the ONLY conversion a symbol
8512 // refuses. `String(sym)` is the documented exception and is handled at that
8513 // call site, not here, so every implicit coercion (`sym + ''`, `` `${sym}` ``,
8514 // `[sym].join()`) rejects the way node does instead of silently rendering
8515 // `Symbol(desc)`.
8516 if with_host(|h| matches!(h.get(&p), Some(JsObj::Symbol { .. }))) {
8517 return Err(type_error("Cannot convert a Symbol value to a string"));
8518 }
8519 Ok(with_host(|h| {
8520 let s = h.str_of(&p);
8521 h.new_str(s)
8522 }))
8523}
8524
8525/// `String(v)` — 22.1.1.1. Identical to [`to_string_value`] except that a
8526/// SYMBOL argument is allowed and renders as `Symbol(desc)` (step 2a).
8527pub fn string_ctor_value(v: &Value) -> Result<Value, String> {
8528 if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8529 return Ok(with_host(|h| {
8530 let s = h.str_of(v);
8531 h.new_str(s)
8532 }));
8533 }
8534 to_string_value(v)
8535}
8536
8537/// `ToNumber(v)` — ECMA-262 7.1.4 — with the object case going through
8538/// `ToPrimitive(v, number)` first, so `+{ valueOf() { return 7 } }` is `7` and
8539/// `+new Date(0)` is `0`. `JsHost::to_number` alone cannot do this: it runs
8540/// under the host borrow and so can never invoke a JS `valueOf`.
8541pub fn to_number_value(v: &Value) -> Result<f64, String> {
8542 // `ToNumber(symbol)` throws (7.1.4 step 2). It is primitive, so without this
8543 // it fell into `to_number` and quietly produced `NaN` — `Number(Symbol())`
8544 // and `+Symbol()` are both `TypeError` on node v26.7.0.
8545 if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8546 return Err(type_error("Cannot convert a Symbol value to a number"));
8547 }
8548 if let Some(n) = with_host(|h| is_primitive(h, v).then(|| h.to_number(v))) {
8549 return Ok(n);
8550 }
8551 let p = to_primitive(v, "number")?;
8552 // Steps 2-3 apply to the ToPrimitive RESULT, not only to the argument. Only
8553 // the argument was checked, so an object whose conversion yields a symbol or
8554 // a BigInt slipped past: `+Object(9n)` answered 9 and
8555 // `+{ [Symbol.toPrimitive]() { return Symbol('s') } }` answered NaN, where
8556 // both are TypeErrors. `Number(x)` is ToNumeric and keeps its own path,
8557 // which is why `Number(Object(9n))` is still 9.
8558 match with_host(|h| h.get(&p).cloned()) {
8559 Some(JsObj::Symbol { .. }) => Err(type_error("Cannot convert a Symbol value to a number")),
8560 Some(JsObj::BigInt(_)) => Err(type_error("Cannot convert a BigInt value to a number")),
8561 _ => Ok(with_host(|h| h.to_number(&p))),
8562 }
8563}
8564
8565/// `ToPropertyKey(v)` — ECMA-262 7.1.19. A symbol keeps its stable internal
8566/// key; anything else is `ToPrimitive(v, string)` then `ToString`, so
8567/// `obj[{ toString() { return 'k' } }]` really reads `obj.k`.
8568pub fn to_property_key(v: &Value) -> Result<String, String> {
8569 // One borrow for the overwhelmingly common primitive key (`a[i]`, `o[s]`,
8570 // `o[sym]`); only an object key pays for the conversion.
8571 if let Some(k) = with_host(|h| is_primitive(h, v).then(|| h.property_key(v))) {
8572 return Ok(k);
8573 }
8574 let p = to_primitive(v, "string")?;
8575 Ok(with_host(|h| h.str_of(&p)))
8576}
8577
8578/// Whether `h.get(v)` is any callable kind. A Proxy is callable exactly when its
8579/// target is (10.5: the `[[Call]]` slot is installed only for a callable
8580/// target), so `typeof` and every `is_callable` guard agree on one answer.
8581pub fn is_callable(h: &JsHost, v: &Value) -> bool {
8582 match h.get(v) {
8583 // Not every builtin is a function: the namespace objects (`Math`,
8584 // `require('fs')`) and the `<Ctor>.prototype` handles are data, and
8585 // calling one is a `TypeError` in node exactly as `typeof` says.
8586 Some(JsObj::Builtin(n)) => builtin_is_callable(n),
8587 Some(JsObj::Func(_))
8588 | Some(JsObj::BoundMethod { .. })
8589 | Some(JsObj::BoundFunc { .. })
8590 | Some(JsObj::Class(_)) => true,
8591 Some(JsObj::Proxy { target, .. }) => is_callable(h, target),
8592 _ => false,
8593 }
8594}
8595
8596/// Walk `recv`'s own props then its prototype chain for `key`, returning the
8597/// stored value (methods, inherited data props). Does NOT invoke accessors.
8598/// A PROTOCOL lookup — `Symbol.toPrimitive`, `Symbol.hasInstance`, `toJSON`,
8599/// `then` and the rest — which the spec performs with `[[Get]]`.
8600///
8601/// That distinction only shows on a PROXY: `lookup_chain` walks the property
8602/// map and never asks the handler, so a proxy supplying a protocol method
8603/// through its `get` trap was invisible and the operation fell back to the
8604/// default. Everything else takes the cheap chain walk.
8605pub fn protocol_lookup(v: &Value, key: &str) -> Result<Option<Value>, String> {
8606 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8607 let got = crate::builtins::get_property(v, key)?;
8608 return Ok((!matches!(got, Value::Undef)).then_some(got));
8609 }
8610 Ok(with_host(|h| lookup_chain(h, v, key)))
8611}
8612
8613pub fn lookup_chain(h: &JsHost, recv: &Value, key: &str) -> Option<Value> {
8614 if let Some(JsObj::Object(p)) = h.get(recv) {
8615 if let Some(v) = p.get(key) {
8616 return Some(v.clone());
8617 }
8618 }
8619 let mut cur = h.proto_of(recv);
8620 while let Some(p) = cur {
8621 // A chain link may be a plain object OR a function/class (the `router`
8622 // package sets `Router.prototype = function(){}` and hangs its methods off
8623 // that function, so the methods live in the fn-prop side table).
8624 match h.get(&p) {
8625 Some(JsObj::Object(props)) => {
8626 if let Some(v) = props.get(key) {
8627 return Some(v.clone());
8628 }
8629 }
8630 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
8631 if let Some(v) = h.fn_prop(&p, key) {
8632 return Some(v);
8633 }
8634 }
8635 _ => {}
8636 }
8637 cur = h.proto_of(&p);
8638 }
8639 None
8640}
8641
8642/// Find a getter/setter accessor for `key` on `recv` or up its prototype chain.
8643pub fn lookup_accessor(
8644 h: &JsHost,
8645 recv: &Value,
8646 key: &str,
8647) -> Option<(Option<Value>, Option<Value>)> {
8648 if let Some(a) = h.own_accessor(recv, key) {
8649 return Some(a);
8650 }
8651 let mut cur = h.proto_of(recv);
8652 while let Some(p) = cur {
8653 if let Some(a) = h.own_accessor(&p, key) {
8654 return Some(a);
8655 }
8656 cur = h.proto_of(&p);
8657 }
8658 // A STATIC accessor declared by an ancestor class. A subclass reaches its
8659 // parent's statics through `ClassVal.parent`, not the `protos` map the walk
8660 // above reads — classes are not linked there, so that walk ended at once.
8661 // Static methods and fields already inherited because `class_static` does
8662 // this same parent walk for `fn_prop`; only accessors had no equivalent:
8663 //
8664 // class Base { static get kind() { return 'base' } }
8665 // class Sub extends Base {}
8666 // Sub.plain() // worked, a fn_prop
8667 // Sub.kind // undefined; node reads 'base'
8668 //
8669 // The caller invokes the getter with the class it was READ off as `this`,
8670 // so a getter reading `this.x` sees the subclass, per 10.2.4.
8671 let mut cls = recv.clone();
8672 while let Some(JsObj::Class(c)) = h.get(&cls) {
8673 let Some(parent) = c.parent.clone() else {
8674 break;
8675 };
8676 if let Some(a) = h.own_accessor(&parent, key) {
8677 return Some(a);
8678 }
8679 cls = parent;
8680 }
8681 None
8682}
8683
8684/// Register a builtin error prototype (for `instanceof Error` etc.).
8685pub fn set_error_proto(name: &str, proto: Value) {
8686 with_host(|h| {
8687 h.error_protos.insert(name.to_string(), proto);
8688 });
8689}
8690pub fn error_proto(name: &str) -> Option<Value> {
8691 with_host(|h| h.error_protos.get(name).cloned())
8692}
8693/// Error prototype lookup with a borrowed host (used inside a `with_host` block).
8694pub fn error_proto_of(h: &JsHost, name: &str) -> Option<Value> {
8695 h.error_protos.get(name).cloned()
8696}
8697
8698impl JsHost {
8699 /// `Error.prototype.toString` for an object whose prototype chain reaches
8700 /// `Error.prototype`: `"Name"` with an empty message, else `"Name: message"`.
8701 /// `None` for anything that is not an error, so the caller keeps its own
8702 /// stringification.
8703 pub fn error_to_string(&self, v: &Value) -> Option<String> {
8704 let base = self.error_protos.get("Error")?;
8705 let mut cur = self.proto_of(v);
8706 let mut is_error = false;
8707 while let Some(p) = cur {
8708 if self.strict_eq(&p, base) {
8709 is_error = true;
8710 break;
8711 }
8712 cur = self.proto_of(&p);
8713 }
8714 if !is_error {
8715 return None;
8716 }
8717 let name = lookup_chain(self, v, "name")
8718 .map(|n| self.str_of(&n))
8719 .unwrap_or_else(|| "Error".into());
8720 let message = lookup_chain(self, v, "message")
8721 .map(|m| self.str_of(&m))
8722 .unwrap_or_default();
8723 // Node's internal coded errors override `toString` as
8724 // `${name} [${code}]: ${message}` (internal/errors.js NodeError). The
8725 // `@@nodeError` tag marks the errors `synth_error` built from a
8726 // `Name [ERR_CODE]: …` string, so a user error that merely has a `.code`
8727 // property still stringifies plainly.
8728 if let Some(JsObj::Object(p)) = self.get(v) {
8729 if p.contains_key("@@nodeError") {
8730 if let Some(code) = p.get("code").map(|c| self.str_of(c)) {
8731 return Some(format!("{name} [{code}]: {message}"));
8732 }
8733 }
8734 }
8735 Some(match (name.is_empty(), message.is_empty()) {
8736 (true, _) => message,
8737 (false, true) => name,
8738 (false, false) => format!("{name}: {message}"),
8739 })
8740 }
8741}
8742
8743/// The set of builtin error constructor names forming the error hierarchy.
8744pub const ERROR_NAMES: &[&str] = &[
8745 "Error",
8746 "TypeError",
8747 "RangeError",
8748 "SyntaxError",
8749 "ReferenceError",
8750 "EvalError",
8751 "URIError",
8752 "AggregateError",
8753 // `assert`'s error class. It is NOT a global (node exposes it only as
8754 // `assert.AssertionError`, and `GLOBAL_FUNCS` is a separate table), but it
8755 // has to be a name `synth_error` recognizes: without it the head
8756 // `AssertionError [ERR_ASSERTION]: …` failed the class check and fell into
8757 // the `Error` branch with the WHOLE head kept as the message, so `e.name`
8758 // was `Error` and `e.message` carried a prefix node keeps out of it.
8759 "AssertionError",
8760 // The WHATWG error class `AbortSignal.reason` carries. Unlike the others its
8761 // `name` comes from the SECOND constructor argument rather than from the
8762 // class, so its prototype keeps the base default and each instance stamps
8763 // its own name into an internal slot.
8764 "DOMException",
8765];
8766
8767impl JsHost {
8768 /// Lazily build the builtin error prototype chain: `Error.prototype →
8769 /// Object.prototype`, and every specific error's prototype → `Error.prototype`.
8770 /// Populated once; instances link to these so `e instanceof TypeError` and
8771 /// `e instanceof Error` both hold.
8772 /// The real `Buffer.prototype` object, building the
8773 /// `Buffer.prototype → Uint8Array.prototype → Object.prototype` chain on
8774 /// first use.
8775 ///
8776 /// A `Buffer` used to be a bare tagged object with no `[[Prototype]]` at
8777 /// all, so `Object.getPrototypeOf(buf) === Buffer.prototype` read false and
8778 /// `instanceof` had to be special-cased around it. Each prototype is a
8779 /// genuine object carrying `@proto:<Ctor>:<method>` thunks for its instance
8780 /// methods, so `Buffer.prototype.slice.call(buf, 1)` still dispatches the
8781 /// way it did when `Buffer.prototype` was a `Builtin` namespace.
8782 pub fn ensure_native_protos(&mut self) {
8783 // The wrapper prototypes share this registry and this guard would skip
8784 // them, so they are built through their own.
8785 self.ensure_wrapper_protos();
8786 self.ensure_function_kind_protos();
8787 if self.native_protos.contains_key("Buffer") {
8788 return;
8789 }
8790 let obj_proto = self.object_proto();
8791 // `Object.prototype` is the one builtin prototype that already existed as
8792 // a real object (it is the chain root). Register it so `Object.prototype`
8793 // reads resolve to THAT object rather than a fresh `Builtin` namespace —
8794 // otherwise `Object.getPrototypeOf(C.prototype) === Object.prototype`
8795 // compares a real object against a thunk and reads false.
8796 self.native_protos
8797 .insert("Object".to_string(), obj_proto.clone());
8798 for m in crate::builtins::OBJECT_PROTO_METHODS {
8799 let thunk = self.alloc(JsObj::Builtin(format!("@proto:Object:{m}")));
8800 if let Some(JsObj::Object(p)) = self.get_mut(&obj_proto) {
8801 p.insert((*m).to_string(), thunk);
8802 }
8803 self.hide_prop(&obj_proto, m);
8804 }
8805 // `Buffer.prototype → Uint8Array.prototype → %TypedArray%.prototype →
8806 // Object.prototype`, which is the chain node v26.7.0 really has. The
8807 // shared iteration methods (`every`, `map`, `filter`, …) live on the
8808 // `%TypedArray%.prototype` intermediate, NOT on `Uint8Array.prototype`:
8809 // measured, `Uint8Array.prototype.hasOwnProperty('every')` is false in
8810 // Node while the intermediate owns it. `%TypedArray%` is not a global,
8811 // so it is reachable only by walking the chain — exactly as in Node.
8812 // Every element kind gets its own prototype hanging off the shared
8813 // intermediate, so `Object.getPrototypeOf(new Int32Array(1))` is
8814 // `Int32Array.prototype` rather than some other kind's. Linking them all
8815 // to `Uint8Array.prototype` would have been the easy version and would
8816 // have made an `Int32Array` claim the wrong prototype.
8817 let mut chain: Vec<(&str, Value)> = vec![("TypedArray", obj_proto)];
8818 for kind in crate::stdlib::typedarray::ELEMENT_KINDS {
8819 chain.push((kind, Value::Undef)); // parent: %TypedArray%.prototype
8820 }
8821 // `Buffer.prototype`'s parent is `Uint8Array.prototype` specifically.
8822 chain.push(("Buffer", Value::Undef));
8823 let mut prev: Option<Value> = None;
8824 for (ctor, parent) in chain.drain(..) {
8825 let proto = self.new_object(IndexMap::new());
8826 // Each kind hangs off the shared intermediate; `Buffer` hangs off
8827 // `Uint8Array.prototype`; the intermediate itself off
8828 // `Object.prototype`.
8829 let parent = match ctor {
8830 "TypedArray" => parent,
8831 "Buffer" => self
8832 .native_protos
8833 .get("Uint8Array")
8834 .cloned()
8835 .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
8836 _ => self
8837 .native_protos
8838 .get("TypedArray")
8839 .cloned()
8840 .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
8841 };
8842 self.set_proto(&proto, parent);
8843 // `%TypedArray%.prototype` has no reachable constructor global, so
8844 // it gets no `constructor` slot (Node's is the anonymous
8845 // `%TypedArray%` intrinsic).
8846 if ctor != "TypedArray" {
8847 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
8848 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8849 p.insert("constructor".into(), ctor_val);
8850 }
8851 self.hide_prop(&proto, "constructor");
8852 }
8853 let methods: &[&str] = match ctor {
8854 "Buffer" => crate::stdlib::buffer::INSTANCE_METHODS,
8855 "TypedArray" => crate::stdlib::typedarray::PROTOTYPE_METHODS,
8856 // `Uint8Array` alone owns the base64/hex pair — no other view
8857 // has them, which is the whole reason they cannot live on the
8858 // shared `%TypedArray%` prototype above.
8859 "Uint8Array" => crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS,
8860 // Every other kind's prototype owns no methods; it inherits them
8861 // from the intermediate above. It does own `BYTES_PER_ELEMENT`,
8862 // which is per-kind and which Node really keeps there (measured:
8863 // `Uint8Array.prototype.hasOwnProperty('BYTES_PER_ELEMENT')`).
8864 _ => &[],
8865 };
8866 if crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor) {
8867 let bpe = Value::Float(crate::stdlib::typedarray::bytes_per_element(ctor) as f64);
8868 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8869 p.insert("BYTES_PER_ELEMENT".into(), bpe);
8870 }
8871 self.hide_prop(&proto, "BYTES_PER_ELEMENT");
8872 }
8873 for m in methods {
8874 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
8875 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8876 p.insert((*m).to_string(), thunk);
8877 }
8878 self.hide_prop(&proto, m);
8879 }
8880 self.native_protos.insert(ctor.to_string(), proto.clone());
8881 prev = Some(proto);
8882 }
8883 }
8884
8885 /// `String.prototype`, `Number.prototype` and `Boolean.prototype` as REAL
8886 /// objects.
8887 ///
8888 /// A wrapper built by `new String("a")` needs a genuine `[[Prototype]]`
8889 /// link: `Builtin("String.prototype")` is a thunk namespace that cannot
8890 /// appear on a prototype chain, so `Object.getPrototypeOf(w) ===
8891 /// String.prototype` and `w instanceof String` both read false while the
8892 /// wrapper's methods still resolved through the string funnel. Registering
8893 /// them here puts them on the same footing as `Buffer.prototype`.
8894 /// `GeneratorFunction.prototype`, `AsyncFunction.prototype` and
8895 /// `AsyncGeneratorFunction.prototype` — the intrinsics a generator or async
8896 /// function's `[[Prototype]]` really points at.
8897 ///
8898 /// None are globals (node exposes them only through
8899 /// `Object.getPrototypeOf(function*(){}).constructor`), so they live here
8900 /// rather than among the wrapper constructors. Each hangs off
8901 /// `Function.prototype` and carries the `Symbol.toStringTag` that names it.
8902 pub fn ensure_function_kind_protos(&mut self) {
8903 if self.native_protos.contains_key("GeneratorFunction") {
8904 return;
8905 }
8906 let base = self
8907 .native_protos
8908 .get("Function")
8909 .cloned()
8910 .unwrap_or_else(|| self.object_proto());
8911 for ctor in [
8912 "GeneratorFunction",
8913 "AsyncFunction",
8914 "AsyncGeneratorFunction",
8915 ] {
8916 let proto = self.new_object(IndexMap::new());
8917 self.set_proto(&proto, base.clone());
8918 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
8919 let tag = self.new_str(ctor);
8920 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8921 p.insert("constructor".into(), ctor_val);
8922 p.insert("@@toStringTag".into(), tag);
8923 }
8924 self.hide_prop(&proto, "constructor");
8925 self.hide_prop(&proto, "@@toStringTag");
8926 self.native_protos.insert(ctor.to_string(), proto);
8927 }
8928 }
8929
8930 pub fn ensure_wrapper_protos(&mut self) {
8931 if self.native_protos.contains_key("String") {
8932 return;
8933 }
8934 let obj_proto = self.object_proto();
8935 for ctor in ["String", "Number", "Boolean", "Symbol", "BigInt"] {
8936 let proto = self.new_object(IndexMap::new());
8937 self.set_proto(&proto, obj_proto.clone());
8938 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
8939 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8940 p.insert("constructor".into(), ctor_val);
8941 }
8942 self.hide_prop(&proto, "constructor");
8943 // The three conversions must be here because `Object.prototype`
8944 // also defines them: without a shadowing entry a wrapper would
8945 // inherit the object forms and `String(new String("a"))` would
8946 // report `[object Object]`.
8947 //
8948 // The REST are here because the prototype is a real object a script
8949 // can read a method OFF of. Only the three were installed, on the
8950 // reasoning that `charAt`/`toFixed`/… reach the primitive through
8951 // `call_method` anyway — true for `s.charAt(0)` and false for the
8952 // generic-borrowing form: `String.prototype.trim` read `undefined`,
8953 // so `String.prototype.trim.call(s)` — and `Number.prototype
8954 // .toFixed.call(n)`, and every `Array.prototype`-style borrow of a
8955 // wrapper method — threw. `Array.prototype`/`Object.prototype`
8956 // already carried their whole method set; these three did not.
8957 let methods: Vec<&str> = ["toString", "valueOf", "toLocaleString"]
8958 .into_iter()
8959 .chain(match ctor {
8960 "String" => crate::builtins::STRING_PROTO_METHODS.iter().copied(),
8961 "Number" => crate::builtins::NUMBER_PROTO_METHODS.iter().copied(),
8962 _ => [].iter().copied(),
8963 })
8964 // The symbol-keyed methods come from the generated intrinsic
8965 // table, so this object advertises exactly the symbol methods
8966 // node defines on it — `String.prototype[Symbol.iterator]` was
8967 // `undefined` because only the string-keyed lists were walked.
8968 .chain(crate::builtins::proto_symbol_methods(ctor))
8969 .collect();
8970 let mut seen: Vec<&str> = Vec::new();
8971 for m in methods {
8972 if seen.contains(&m) {
8973 continue;
8974 }
8975 seen.push(m);
8976 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
8977 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8978 p.insert(m.to_string(), thunk);
8979 }
8980 self.hide_prop(&proto, m);
8981 }
8982 // `Symbol.prototype` and `BigInt.prototype` are the two wrapper
8983 // prototypes that carry a `@@toStringTag`; the other three are
8984 // branded by their internal slot instead, and node reports
8985 // `undefined` for their tag. Without it
8986 // `Object.prototype.toString.call(Symbol.prototype)` read
8987 // `[object Object]` where node says `[object Symbol]`.
8988 if matches!(ctor, "Symbol" | "BigInt") {
8989 let tag = self.new_str(ctor.to_string());
8990 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8991 p.insert("@@toStringTag".into(), tag);
8992 }
8993 // Not `hide_prop`: a well-known `@@toStringTag` is read-only as
8994 // well as non-enumerable (20.4.3.6), and `hide_prop` leaves it
8995 // writable.
8996 self.set_prop_attrs(
8997 &proto,
8998 "@@toStringTag",
8999 PropAttrs {
9000 writable: false,
9001 enumerable: false,
9002 configurable: true,
9003 },
9004 );
9005 }
9006 // `Symbol.prototype[@@toPrimitive]` (20.4.3.5) is what a string or
9007 // numeric conversion of a symbol reaches FIRST. Its absence was
9008 // observable in the failure wording: `String(Symbol.prototype)`
9009 // throws in node because `@@toPrimitive` rejects a non-Symbol
9010 // `this`, and here the conversion fell through to `toString` and
9011 // named that method in the message instead.
9012 if ctor == "Symbol" {
9013 let thunk = self.alloc(JsObj::Builtin("@proto:Symbol:@@toPrimitive".to_string()));
9014 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9015 p.insert("@@toPrimitive".into(), thunk);
9016 }
9017 self.hide_prop(&proto, "@@toPrimitive");
9018 }
9019 self.native_protos.insert(ctor.to_string(), proto);
9020 }
9021 }
9022
9023 /// The cached template object for one tagged-template site, if it has been
9024 /// evaluated before.
9025 pub fn template_object(&self, key: (u64, u64)) -> Option<Value> {
9026 self.template_objects.get(&key).cloned()
9027 }
9028 /// Record the template object for one tagged-template site.
9029 pub fn set_template_object(&mut self, key: (u64, u64), v: Value) {
9030 self.template_objects.insert(key, v);
9031 }
9032
9033 /// The real prototype object for a builtin exotic, if it has one.
9034 pub fn native_proto(&self, ctor: &str) -> Option<Value> {
9035 self.native_protos.get(ctor).cloned()
9036 }
9037
9038 /// The constructor name whose `.prototype` object IS `v`, for a prototype
9039 /// this host built as a real object (`String.prototype`, `TypeError
9040 /// .prototype`, `Buffer.prototype`) rather than as a `Builtin` namespace.
9041 ///
9042 /// A prototype is an ORDINARY object: it carries no instance's internal
9043 /// slot, so `Object.prototype.toString.call(TypeError.prototype)` is
9044 /// `[object Object]` and not `[object Error]`. Nothing distinguished the two
9045 /// before, so the brand fell through to the "does it look like an Error"
9046 /// test and answered for the prototype as if it were an instance.
9047 pub fn intrinsic_proto_ctor(&self, v: &Value) -> Option<&str> {
9048 if !matches!(v, Value::Obj(_)) {
9049 return None;
9050 }
9051 self.native_protos
9052 .iter()
9053 .chain(self.error_protos.iter())
9054 .find(|(_, p)| *p == v)
9055 .map(|(name, _)| name.as_str())
9056 }
9057
9058 /// The real `.prototype` object for a native stdlib constructor (`StringDecoder`,
9059 /// `Hash`, `URLSearchParams`, …), built on first read and cached.
9060 ///
9061 /// `Ctor.prototype` used to read `undefined` for every native class outside the
9062 /// hand-written `is_builtin_ctor` list, which broke the ES5 subclassing pattern
9063 /// that libraries still use. `iconv-lite`'s internal codec — reached from
9064 /// `raw-body` on every `express.json()` request — does exactly this:
9065 ///
9066 /// ```text
9067 /// var StringDecoder = require('string_decoder').StringDecoder;
9068 /// if (!StringDecoder.prototype.end) StringDecoder.prototype.end = function () {};
9069 /// function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
9070 /// InternalDecoder.prototype = StringDecoder.prototype;
9071 /// ```
9072 ///
9073 /// The first line threw `Cannot read properties of undefined (reading 'end')`.
9074 ///
9075 /// Methods come from `stdlib::instance_method_lists`, the same table a method
9076 /// READ consults, so the prototype can never advertise a name the dispatcher
9077 /// does not implement. Each is the `@proto:<Ctor>:<method>` thunk that
9078 /// dispatches against its invoke-time `this`, so a subclass instance whose
9079 /// prototype IS this object gets the native implementation. Returns `None` for
9080 /// a tag with no instance methods, leaving those constructors as they were.
9081 pub fn ensure_ctor_proto(&mut self, ctor: &str) -> Option<Value> {
9082 if let Some(p) = self.native_protos.get(ctor) {
9083 return Some(p.clone());
9084 }
9085 // `Buffer` and the typed-array kinds belong to the chain
9086 // `ensure_native_protos` builds. Building one of them here first hung it
9087 // straight off `Object.prototype` AND registered it, which made that
9088 // chain's own `contains_key("Buffer")` guard skip the build for the rest
9089 // of the process: after `Buffer.from([1])`, `Buffer.prototype instanceof
9090 // Uint8Array` read false.
9091 if ctor == "Buffer"
9092 || ctor == "TypedArray"
9093 || crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor)
9094 {
9095 self.ensure_native_protos();
9096 return self.native_protos.get(ctor).cloned();
9097 }
9098 let (own, emitter) = crate::stdlib::instance_method_lists(ctor);
9099 // A class can carry accessors and no methods at all
9100 // (`AsymmetricKeyObject` is only `asymmetricKeyType` and
9101 // `asymmetricKeyDetails`), so an empty method list does not mean there
9102 // is no prototype to build.
9103 let (accessor_list, _) = crate::stdlib::instance_accessors(ctor);
9104 if own.is_empty() && emitter.is_empty() && accessor_list.is_empty() {
9105 return None;
9106 }
9107 // A native class with a real PARENT hangs off that parent's prototype
9108 // rather than straight off `Object.prototype`. The stream hierarchy is
9109 // `Readable → Stream → EventEmitter`, which is what makes
9110 // `new Readable() instanceof Stream` hold and what an ES5 subclass
9111 // doing `Object.create(Stream.prototype)` inherits from.
9112 let parent_proto = match crate::stdlib::native_parent(ctor) {
9113 Some(p) => self
9114 .ensure_ctor_proto(p)
9115 .unwrap_or_else(|| self.object_proto()),
9116 None => self.object_proto(),
9117 };
9118 let proto = self.new_object(IndexMap::new());
9119 self.set_proto(&proto, parent_proto);
9120 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9121 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9122 p.insert("constructor".into(), ctor_val);
9123 }
9124 self.hide_prop(&proto, "constructor");
9125 // A prototype member is ENUMERABLE in node for every class but the few
9126 // written as ES classes, so `for (const k in url)` walks `href` and the
9127 // rest. Hiding all of them made that loop find nothing.
9128 let visible = crate::stdlib::instance_members_enumerable(ctor);
9129 let symbols = crate::builtins::proto_symbol_methods(ctor);
9130 for m in own.iter().chain(emitter.iter()).chain(symbols.iter()) {
9131 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9132 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9133 p.insert((*m).to_string(), thunk);
9134 }
9135 // A symbol-keyed member never enumerates.
9136 if !visible || m.starts_with("@@") {
9137 self.hide_prop(&proto, m);
9138 }
9139 }
9140 // Accessors and the class's `Symbol.toStringTag`, both of which live on
9141 // the PROTOTYPE in node — an instance owns neither.
9142 let (accessors, tag) = crate::stdlib::instance_accessors(ctor);
9143 for (key, settable) in accessors {
9144 let get = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@get@{key}")));
9145 let set =
9146 settable.then(|| self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@set@{key}"))));
9147 self.set_accessor(&proto, key, Some(get), set);
9148 if !visible {
9149 self.hide_prop(&proto, key);
9150 }
9151 }
9152 for m in crate::stdlib::instance_late_methods(ctor) {
9153 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9154 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9155 p.insert((*m).to_string(), thunk);
9156 }
9157 if !visible {
9158 self.hide_prop(&proto, m);
9159 }
9160 }
9161 if !tag.is_empty() {
9162 let tag = self.new_str(tag.to_string());
9163 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9164 p.insert("@@toStringTag".into(), tag);
9165 }
9166 self.hide_prop(&proto, "@@toStringTag");
9167 }
9168 self.native_protos.insert(ctor.to_string(), proto.clone());
9169 Some(proto)
9170 }
9171
9172 /// `<ErrorClass>.prototype`, once [`JsHost::ensure_error_protos`] has run.
9173 /// The error prototypes live in their own table, so `ensure_ctor_proto` —
9174 /// which answers from `native_protos` — does not find them.
9175 pub fn error_proto(&self, name: &str) -> Option<Value> {
9176 self.error_protos.get(name).cloned()
9177 }
9178
9179 pub fn ensure_error_protos(&mut self) {
9180 if !self.error_protos.is_empty() {
9181 return;
9182 }
9183 let obj_proto = self.object_proto();
9184 // Error.prototype first (the shared base).
9185 let err_proto = self.new_object(IndexMap::new());
9186 self.set_proto(&err_proto, obj_proto);
9187 let nm = self.new_str("Error");
9188 let empty = self.new_str("");
9189 let ctor = self.alloc(JsObj::Builtin("Error".into()));
9190 // `Error.prototype.toString` (20.5.3.4) has to be an OWN property here,
9191 // not a fallback the stringifier applies when nothing else matches: it
9192 // exists precisely to shadow `Object.prototype.toString`. Without it,
9193 // the first read of `Error.prototype` or `Object.prototype` — which
9194 // `x instanceof Error` performs, so ordinary code triggers it —
9195 // materialised `Object.prototype.toString`, the chain lookup started
9196 // finding it, and `String(err)` flipped from `Error: m` to
9197 // `[object Error]` for the REST OF THE PROCESS, including errors
9198 // created before the read.
9199 let to_string = self.alloc(JsObj::Builtin("@proto:Error:toString".into()));
9200 if let Some(JsObj::Object(p)) = self.get_mut(&err_proto) {
9201 p.insert("name".into(), nm);
9202 p.insert("message".into(), empty);
9203 p.insert("constructor".into(), ctor);
9204 p.insert("toString".into(), to_string);
9205 }
9206 // Everything on `Error.prototype` is non-enumerable in V8.
9207 for k in ["name", "message", "constructor", "toString"] {
9208 self.hide_prop(&err_proto, k);
9209 }
9210 self.error_protos.insert("Error".into(), err_proto.clone());
9211 for name in &ERROR_NAMES[1..] {
9212 let p = self.new_object(IndexMap::new());
9213 self.set_proto(&p, err_proto.clone());
9214 let nm = self.new_str(*name);
9215 let ctor = self.alloc(JsObj::Builtin((*name).to_string()));
9216 if let Some(JsObj::Object(o)) = self.get_mut(&p) {
9217 o.insert("name".into(), nm);
9218 o.insert("constructor".into(), ctor);
9219 }
9220 self.hide_prop(&p, "name");
9221 self.hide_prop(&p, "constructor");
9222 self.error_protos.insert((*name).to_string(), p);
9223 }
9224 }
9225}
9226
9227// ── Map/Set element access (used by builtins) ────────────────────────────────
9228
9229impl JsHost {
9230 /// A function's `.length`: the count of leading params before the first one
9231 /// with a default or the rest element.
9232 pub fn func_arity(&self, v: &Value) -> usize {
9233 // 20.2.3.2: a bound function's `length` is the target's, less the
9234 // arguments already bound, floored at 0. Reporting 0 for every bound
9235 // function breaks arity dispatch — express picks error-handling
9236 // middleware with `fn.length === 4`, so a bound handler was never
9237 // recognised as one.
9238 if let Some(JsObj::BoundFunc { target, args, .. }) = self.get(v) {
9239 return self.func_arity(&target.clone()).saturating_sub(args.len());
9240 }
9241 // A builtin's arity is the specified one, so `Math.max.bind(null,1)`
9242 // reports 1 rather than the 0 a target of unknown arity would give.
9243 if let Some(JsObj::Builtin(n)) = self.get(v) {
9244 return crate::builtins::builtin_meta(n)
9245 .map(|(_, len)| len as usize)
9246 .unwrap_or(0);
9247 }
9248 let def_id = match self.get(v) {
9249 Some(JsObj::Func(f)) => Some(f.def_id),
9250 Some(JsObj::Class(c)) => match c.ctor.as_ref().and_then(|cf| self.get(cf)) {
9251 Some(JsObj::Func(f)) => Some(f.def_id),
9252 _ => None,
9253 },
9254 _ => None,
9255 };
9256 match def_id.and_then(|id| self.funcs.get(id)) {
9257 Some(def) => def
9258 .params
9259 .iter()
9260 .take_while(|p| !p.rest && !p.has_default)
9261 .count(),
9262 None => 0,
9263 }
9264 }
9265
9266 pub fn is_map(&self, v: &Value) -> bool {
9267 matches!(self.get(v), Some(JsObj::Map { .. }))
9268 }
9269 pub fn is_set(&self, v: &Value) -> bool {
9270 matches!(self.get(v), Some(JsObj::Set { .. }))
9271 }
9272}
9273
9274// ── promises & the event loop ────────────────────────────────────────────────
9275
9276impl JsHost {
9277 /// Allocate a fresh pending promise, returning its heap value.
9278 pub fn new_promise(&mut self) -> Value {
9279 let id = self.promises.len() as u32;
9280 self.promises.push(PromiseCell {
9281 state: PromiseState::Pending,
9282 value: Value::Undef,
9283 reactions: Vec::new(),
9284 handled: false,
9285 });
9286 self.alloc(JsObj::Promise { id })
9287 }
9288 pub fn promise_id(&self, v: &Value) -> Option<u32> {
9289 match self.get(v) {
9290 Some(JsObj::Promise { id }) => Some(*id),
9291 _ => None,
9292 }
9293 }
9294 pub fn promise_state(&self, id: u32) -> PromiseState {
9295 self.promises[id as usize].state
9296 }
9297 pub fn promise_value(&self, id: u32) -> Value {
9298 self.promises[id as usize].value.clone()
9299 }
9300 pub fn promise_mark_handled(&mut self, id: u32) {
9301 self.promises[id as usize].handled = true;
9302 }
9303 /// Take the pending reactions of a promise (called on settle).
9304 pub fn take_reactions(&mut self, id: u32) -> Vec<PromiseReaction> {
9305 std::mem::take(&mut self.promises[id as usize].reactions)
9306 }
9307 pub fn add_reaction(&mut self, id: u32, r: PromiseReaction) {
9308 self.promises[id as usize].reactions.push(r);
9309 }
9310 pub fn settle_promise(&mut self, id: u32, state: PromiseState, value: Value) {
9311 let c = &mut self.promises[id as usize];
9312 if c.state != PromiseState::Pending {
9313 return; // already settled — resolve/reject are one-shot
9314 }
9315 c.state = state;
9316 c.value = value;
9317 }
9318 pub fn queue_micro(&mut self, cb: Value, args: Vec<Value>) {
9319 self.microtasks.push_back(Task::Js { cb, args });
9320 }
9321 pub fn queue_nexttick(&mut self, cb: Value, args: Vec<Value>) {
9322 self.nextticks.push_back(Task::Js { cb, args });
9323 }
9324 /// Schedule a native (Rust) microtask — used by Promise reactions and async
9325 /// resumption.
9326 pub fn queue_micro_native(&mut self, f: Box<dyn FnOnce() -> Result<(), String>>) {
9327 self.microtasks.push_back(Task::Native(f));
9328 }
9329 /// Schedule a macrotask. `interval` is the repeat period for `setInterval`
9330 /// (`None` for the one-shot `setTimeout`/`setImmediate`). Returns the timer
9331 /// id, which the `Timeout`/`Immediate` handle object carries so `clear*`,
9332 /// `ref`/`unref` and `refresh` can find this entry again.
9333 pub fn add_timer(
9334 &mut self,
9335 delay: f64,
9336 callback: Value,
9337 args: Vec<Value>,
9338 interval: Option<f64>,
9339 ) -> u64 {
9340 let id = self.next_timer;
9341 self.next_timer += 1;
9342 // Real deadline for the real-clock path; `setImmediate` (delay < 0) is
9343 // clamped to "now". Virtual-clock ordering still uses `delay`/`seq`.
9344 let deadline = Instant::now() + Duration::from_millis(delay.max(0.0) as u64);
9345 self.macrotasks.push(Timer {
9346 id,
9347 delay,
9348 seq: id,
9349 callback,
9350 args,
9351 cancelled: false,
9352 interval,
9353 refed: true,
9354 deadline,
9355 });
9356 id
9357 }
9358 /// Re-arm a repeating timer that is about to fire, keeping its id (so a
9359 /// `clearInterval` from *inside* the callback cancels this very entry) and
9360 /// taking a fresh `seq` so same-delay peers still round-robin.
9361 ///
9362 /// Called BEFORE the callback runs: if it were called after, the entry would
9363 /// be absent while the callback executed and a `clearInterval(t)` there would
9364 /// cancel nothing, resurrecting an interval the program had stopped.
9365 fn rearm_timer(&mut self, t: &Timer, period: f64) {
9366 let seq = self.next_timer;
9367 self.next_timer += 1;
9368 let deadline = Instant::now() + Duration::from_millis(period.max(0.0) as u64);
9369 self.macrotasks.push(Timer {
9370 id: t.id,
9371 delay: t.delay,
9372 seq,
9373 callback: t.callback.clone(),
9374 args: t.args.clone(),
9375 cancelled: false,
9376 interval: Some(period),
9377 refed: t.refed,
9378 deadline,
9379 });
9380 }
9381 /// `timeout.ref()` / `timeout.unref()` — set the handle bit on a pending
9382 /// timer. A no-op once the timer has fired or been cleared (Node likewise
9383 /// treats `ref`/`unref` on a dead timer as inert).
9384 pub fn set_timer_refed(&mut self, id: u64, refed: bool) {
9385 for t in &mut self.macrotasks {
9386 if t.id == id && !t.cancelled {
9387 t.refed = refed;
9388 }
9389 }
9390 }
9391 /// `timeout.hasRef()` — whether a still-pending timer holds the loop open.
9392 /// A fired or cleared timer reports `false`, matching Node.
9393 pub fn timer_has_ref(&self, id: u64) -> bool {
9394 self.macrotasks
9395 .iter()
9396 .any(|t| t.id == id && !t.cancelled && t.refed)
9397 }
9398 /// `timeout.refresh()` — restart the countdown from now, as if the timer had
9399 /// just been scheduled.
9400 pub fn refresh_timer(&mut self, id: u64) {
9401 let now = Instant::now();
9402 for t in &mut self.macrotasks {
9403 if t.id == id && !t.cancelled {
9404 t.deadline = now + Duration::from_millis(t.delay.max(0.0) as u64);
9405 }
9406 }
9407 }
9408 /// Clone the I/O sender for a background I/O thread.
9409 pub fn io_sender(&self) -> Sender<IoTask> {
9410 self.io_tx.clone()
9411 }
9412 /// Register a live handle (listener/socket/ref'd resource) keeping the loop
9413 /// alive.
9414 pub fn incr_handle(&mut self) {
9415 self.open_handles += 1;
9416 }
9417 /// Release a handle; the loop exits once this reaches `0` with empty queues.
9418 pub fn decr_handle(&mut self) {
9419 self.open_handles = self.open_handles.saturating_sub(1);
9420 }
9421 pub fn open_handles(&self) -> usize {
9422 self.open_handles
9423 }
9424 /// Pop the earliest timer whose real deadline is at or before `now` (I/O
9425 /// path). Ties break by `seq`.
9426 fn pop_due_timer(&mut self, now: Instant) -> Option<Timer> {
9427 let idx = self
9428 .macrotasks
9429 .iter()
9430 .enumerate()
9431 .filter(|(_, t)| !t.cancelled && t.deadline <= now)
9432 .min_by(|(_, a), (_, b)| a.deadline.cmp(&b.deadline).then(a.seq.cmp(&b.seq)))
9433 .map(|(i, _)| i);
9434 idx.map(|i| self.macrotasks.remove(i))
9435 }
9436 /// Time until the earliest pending timer's deadline (I/O path blocking bound),
9437 /// or `None` if no timers are pending. Clamped to `0` for already-due timers.
9438 fn next_timer_timeout(&self, now: Instant) -> Option<Duration> {
9439 self.macrotasks
9440 .iter()
9441 .filter(|t| !t.cancelled)
9442 .map(|t| t.deadline)
9443 .min()
9444 .map(|d| d.saturating_duration_since(now))
9445 }
9446 pub fn cancel_timer(&mut self, id: u64) {
9447 for t in &mut self.macrotasks {
9448 if t.id == id {
9449 t.cancelled = true;
9450 }
9451 }
9452 }
9453 fn pop_next_timer(&mut self) -> Option<Timer> {
9454 // Earliest (delay, seq) fires first — a deterministic virtual clock.
9455 let idx = self
9456 .macrotasks
9457 .iter()
9458 .enumerate()
9459 .filter(|(_, t)| !t.cancelled)
9460 .min_by(|(_, a), (_, b)| {
9461 a.delay
9462 .partial_cmp(&b.delay)
9463 .unwrap_or(std::cmp::Ordering::Equal)
9464 .then(a.seq.cmp(&b.seq))
9465 })
9466 .map(|(i, _)| i);
9467 idx.map(|i| self.macrotasks.remove(i))
9468 }
9469 fn next_microtask(&mut self) -> Option<Task> {
9470 // Node's `processTicksAndRejections` runs in ROUNDS: drain the nextTick
9471 // queue, then drain the microtask queue in full, then repeat if the
9472 // microtasks queued more ticks. A tick queued from INSIDE a microtask
9473 // therefore waits for the rest of that microtask queue.
9474 //
9475 // Preferring ticks on every step interleaved the two, so
9476 // `Promise.resolve().then(() => process.nextTick(f))` ran `f` before the
9477 // promise callbacks queued behind it — the one ordering difference a
9478 // library scheduling work from a `.then` can actually observe.
9479 if !self.draining_micro {
9480 if let Some(t) = self.nextticks.pop_front() {
9481 return Some(t);
9482 }
9483 }
9484 if let Some(t) = self.microtasks.pop_front() {
9485 // Stay in the microtask phase until this queue is exhausted.
9486 self.draining_micro = !self.microtasks.is_empty();
9487 return Some(t);
9488 }
9489 self.draining_micro = false;
9490 self.nextticks.pop_front()
9491 }
9492 fn has_microtasks(&self) -> bool {
9493 !self.nextticks.is_empty() || !self.microtasks.is_empty()
9494 }
9495 /// Whether any pending timer is *referenced* — the timer half of Node's
9496 /// handle count. Only these keep the loop alive; unref'd timers still fire
9497 /// while something else holds the loop open, but never hold it themselves.
9498 fn has_refed_macrotasks(&self) -> bool {
9499 self.macrotasks.iter().any(|t| !t.cancelled && t.refed)
9500 }
9501 /// Whether any pending timer repeats. A repeating timer cannot run on the
9502 /// virtual clock: virtual time never advances, so the interval would re-arm
9503 /// at the same instant forever, spinning a core and starving every
9504 /// longer-delay timer behind it. Its presence forces the real clock.
9505 fn has_pending_interval(&self) -> bool {
9506 self.macrotasks
9507 .iter()
9508 .any(|t| !t.cancelled && t.interval.is_some())
9509 }
9510}
9511
9512/// Drive the event loop to quiescence.
9513///
9514/// **Liveness** is Node's handle count: the loop runs while a microtask is
9515/// pending, an open handle is registered (a listening server, a live socket, an
9516/// in-flight async op), or a *referenced* timer is still pending. That last term
9517/// is what makes `setInterval(fn, 1000)` hold the process open forever, as it
9518/// does in Node — the interval re-arms itself, so a ref'd timer is always
9519/// pending and the loop never reaches its exit condition.
9520///
9521/// Two **clock regimes**, selected per iteration:
9522///
9523/// - **Virtual clock** (no open handles and no repeating timer): the original
9524/// deterministic path — fire the earliest `(delay, seq)` timer immediately, no
9525/// real waiting. Parity output and test speed for ordinary `setTimeout`
9526/// scripts are unchanged.
9527/// - **Real clock** (an open handle, or any pending interval): fire every timer
9528/// whose wall-clock deadline has passed, then BLOCK on the I/O channel
9529/// (`recv_timeout` bounded by the next deadline, or unbounded `recv` if no
9530/// timers) and run the received `IoTask` on the main thread. The host keeps
9531/// its own `Sender`, so `recv` never disconnects while the process should stay
9532/// alive.
9533///
9534/// A repeating timer *must* take this path: virtual time never advances, so an
9535/// interval on the virtual clock would re-fire at the same instant forever,
9536/// spinning a core and starving every longer-delay timer behind it.
9537///
9538/// Errors thrown by a task/timer/I/O dispatch abort the loop (uncaught → surfaced).
9539pub fn run_event_loop() -> Result<(), String> {
9540 // Own the receiver for the loop's duration (blocking `recv` cannot hold a
9541 // host borrow); restore it afterward so a re-entrant run reuses the channel.
9542 let rx = with_host(|h| h.io_rx.take());
9543 let result = drive_event_loop(rx.as_ref());
9544 with_host(|h| h.io_rx = rx);
9545 result
9546}
9547
9548fn drive_event_loop(rx: Option<&Receiver<IoTask>>) -> Result<(), String> {
9549 loop {
9550 // 1) Exhaust the microtask queue (nextTick before promise reactions),
9551 // then report anything that rejected with nobody watching.
9552 while let Some(task) = with_host(|h| h.next_microtask()) {
9553 task.run()?;
9554 }
9555 check_unhandled_rejections()?;
9556
9557 // 2) Liveness (Node's handle count). Nothing referenced left to do ⇒ the
9558 // process exits, dropping any unref'd timers still pending — which is
9559 // why `setTimeout(fn, 1000).unref()` never fires, while an unref'd
9560 // timer behind a ref'd one does.
9561 let alive =
9562 with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
9563 if !alive {
9564 break;
9565 }
9566
9567 // 3) Pick the clock regime for this turn.
9568 let virtual_clock = with_host(|h| h.open_handles() == 0 && !h.has_pending_interval());
9569 if virtual_clock {
9570 // ── virtual-clock regime (unchanged for one-shot timers) ─────────
9571 match with_host(|h| h.pop_next_timer()) {
9572 Some(t) => fire_timer(t)?,
9573 // Unreachable while `alive` holds (a ref'd timer must exist),
9574 // but exiting is the safe reading of "nothing left to run".
9575 None => break,
9576 }
9577 continue;
9578 }
9579
9580 // ── real-clock / blocking-I/O regime ─────────────────────────────────
9581 let now = Instant::now();
9582 if let Some(t) = with_host(|h| h.pop_due_timer(now)) {
9583 fire_timer(t)?;
9584 continue; // re-drain microtasks, re-check deadlines
9585 }
9586 // Nothing due and no pending microtasks: block for the next I/O event,
9587 // bounded by the soonest timer deadline so due timers still fire on time.
9588 let rx = rx.expect("blocking-I/O regime requires the I/O receiver");
9589 let timeout = with_host(|h| h.next_timer_timeout(now));
9590 let recv = match timeout {
9591 Some(d) => rx.recv_timeout(d),
9592 None => rx
9593 .recv()
9594 .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected),
9595 };
9596 match recv {
9597 Ok(task) => task()?,
9598 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} // a timer is now due
9599 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, // no senders left
9600 }
9601 }
9602 Ok(())
9603}
9604
9605/// Run one due timer's callback, first re-arming it if it repeats.
9606///
9607/// The re-arm happens BEFORE the callback runs so that a `clearInterval(t)`
9608/// issued from inside that callback cancels the next occurrence. Re-arming
9609/// afterwards would leave the interval absent from the queue for the duration of
9610/// its own callback, so the `clear` would match nothing and the freshly pushed
9611/// entry would resurrect an interval the program had just stopped.
9612fn fire_timer(t: Timer) -> Result<(), String> {
9613 if let Some(period) = t.interval {
9614 with_host(|h| h.rearm_timer(&t, period));
9615 }
9616 invoke(&t.callback, t.args, None)?;
9617 Ok(())
9618}
9619
9620// ── async functions & promise resolution (native) ────────────────────────────
9621
9622/// Drive a freshly-built async coroutine and return its result promise.
9623fn run_async(gen: Value) -> Value {
9624 let result = with_host(|h| h.new_promise());
9625 let rid = with_host(|h| h.promise_id(&result).unwrap());
9626 drive_async(gen, rid, Value::Undef);
9627 result
9628}
9629
9630/// Resume an async coroutine one step, wiring `await` continuations to promise
9631/// settlement.
9632fn drive_async(gen: Value, rid: u32, send: Value) {
9633 match gen_resume(&gen, send) {
9634 Ok(GenStep::Yield(awaited)) => {
9635 let ap = promise_of(&awaited);
9636 let aid = with_host(|h| h.promise_id(&ap).unwrap());
9637 let gen2 = gen.clone();
9638 subscribe_native(
9639 aid,
9640 Box::new(move |state, val| {
9641 // Resume the coroutine with a `[tag, value]` packet the AWAIT
9642 // op unwraps (tag 1 ⇒ the awaited promise rejected → throw).
9643 let tag = if state == PromiseState::Rejected {
9644 1.0
9645 } else {
9646 0.0
9647 };
9648 let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9649 drive_async(gen2, rid, packet);
9650 Ok(())
9651 }),
9652 );
9653 }
9654 Ok(GenStep::Done(v)) => resolve_promise_val(rid, v),
9655 Err(e) => {
9656 let ev = take_exc_or_error(&e);
9657 reject_promise_val(rid, ev);
9658 }
9659 }
9660}
9661
9662/// The AWAIT op body (runs inside the async coroutine): suspend, yielding the
9663/// awaited value; on resume, unwrap the settlement packet (throwing on reject).
9664pub fn await_value(awaited: Value) -> Result<Value, String> {
9665 // Inside an `async function*`, `await` and `yield` share one coroutine
9666 // yielder, so an awaited value has to be tagged or the driver would hand it
9667 // to the consumer as if the body had yielded it.
9668 let awaited = match CUR_GEN.with(|c| c.get()) {
9669 Some(id) if with_host(|h| h.generators[id as usize].async_gen) => with_host(|h| {
9670 let mut m = IndexMap::new();
9671 m.insert(AWAIT_MARKER.to_string(), awaited);
9672 h.new_object(m)
9673 }),
9674 _ => awaited,
9675 };
9676 let packet = gen_yield(awaited)?;
9677 let items = with_host(|h| h.iter_vec(&packet)).unwrap_or_default();
9678 let tag = items
9679 .first()
9680 .map(|v| with_host(|h| h.to_number(v)))
9681 .unwrap_or(0.0);
9682 let val = items.get(1).cloned().unwrap_or(Value::Undef);
9683 if tag == 1.0 {
9684 with_host(|h| h.exc = Some(val.clone()));
9685 Err(with_host(|h| crate::builtins::error_string(h, &val)))
9686 } else {
9687 Ok(val)
9688 }
9689}
9690
9691/// Hidden key marking an `await` suspension inside an async generator.
9692const AWAIT_MARKER: &str = "@@await";
9693
9694/// The operand of an `await` suspension, or `None` for a real `yield`.
9695fn await_marker(v: &Value) -> Option<Value> {
9696 with_host(|h| match h.get(v) {
9697 Some(JsObj::Object(props)) if props.len() == 1 => props.get(AWAIT_MARKER).cloned(),
9698 _ => None,
9699 })
9700}
9701
9702/// `AsyncGeneratorEnqueue` — queue one request against an `async function*` and
9703/// hand back the promise its `{value, done}` record (or rejection) will settle.
9704///
9705/// All three of `.next`, `.return` and `.throw` come through here, so a request
9706/// never resumes the body while an earlier one is still suspended on an
9707/// internal `await`.
9708pub fn async_gen_enqueue(gen: &Value, req: GenReq) -> Value {
9709 let step = with_host(|h| h.new_promise());
9710 let sid = with_host(|h| h.promise_id(&step).unwrap());
9711 let id = match with_host(|h| match h.get(gen) {
9712 Some(JsObj::Generator { id }) => Some(*id),
9713 _ => None,
9714 }) {
9715 Some(id) => id,
9716 None => return step,
9717 };
9718 with_host(|h| h.generators[id as usize].queue.push_back((req, sid)));
9719 pump_async_gen(gen.clone(), id);
9720 step
9721}
9722
9723/// One `.next(v)` of an `async function*`.
9724pub fn async_gen_step(gen: &Value, send: Value) -> Value {
9725 async_gen_enqueue(gen, GenReq::Next(send))
9726}
9727
9728/// `AsyncGeneratorResumeNext`: start the oldest queued request, unless one is
9729/// already in flight (the body may only be resumed by one request at a time).
9730fn pump_async_gen(gen: Value, id: u32) {
9731 if with_host(|h| h.generators[id as usize].running) {
9732 return;
9733 }
9734 let Some((req, sid)) = with_host(|h| h.generators[id as usize].queue.pop_front()) else {
9735 return;
9736 };
9737 with_host(|h| h.generators[id as usize].running = true);
9738 start_async_gen_req(gen, sid, req);
9739}
9740
9741/// Begin one queued request: resume the body with the completion it carries,
9742/// then hand the outcome to the shared continuation.
9743///
9744/// A RETURN completion always Awaits its value before the body sees it — via
9745/// `AsyncGeneratorUnwrapYieldResumption` (ECMA-262 27.6.3.7) when the generator
9746/// is suspended at a `yield`, and via `AsyncGeneratorAwaitReturn` (27.6.3.9)
9747/// when it is not yet started or already completed. So a `.return()` settles one
9748/// microtask after a `.next()` or `.throw()` issued in its place would, and the
9749/// `finally` it unwinds through runs a tick later too. Skipping that tick lets a
9750/// `.return()` overtake the reactions of the `.next()` it followed.
9751fn start_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9752 if matches!(req, GenReq::Return(_)) {
9753 with_host(|h| {
9754 h.queue_micro_native(Box::new(move || {
9755 resume_async_gen_req(gen, sid, req);
9756 Ok(())
9757 }))
9758 });
9759 return;
9760 }
9761 resume_async_gen_req(gen, sid, req);
9762}
9763
9764/// Deliver a queued completion to the body and settle its step promise.
9765fn resume_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9766 let step = match req {
9767 GenReq::Next(v) => gen_resume(&gen, v),
9768 GenReq::Return(v) => gen_return(&gen, v),
9769 GenReq::Throw(e) => gen_throw(&gen, e),
9770 };
9771 settle_async_gen_step(gen, sid, step);
9772}
9773
9774/// One request has settled: release the body and start the next queued request.
9775fn finish_async_gen_step(gen: Value, id: u32) {
9776 with_host(|h| h.generators[id as usize].running = false);
9777 pump_async_gen(gen, id);
9778}
9779
9780/// Whether `v` is an `async function*` object (its `.next()` yields promises).
9781pub fn is_async_generator(v: &Value) -> bool {
9782 let id = match with_host(|h| match h.get(v) {
9783 Some(JsObj::Generator { id }) => Some(*id),
9784 _ => None,
9785 }) {
9786 Some(id) => id,
9787 None => return false,
9788 };
9789 with_host(|h| h.generators[id as usize].async_gen)
9790}
9791
9792/// A `{ value, done }` iterator-result object.
9793fn iter_record(value: Value, done: bool) -> Value {
9794 with_host(|h| {
9795 let mut m = IndexMap::new();
9796 m.insert("value".to_string(), value);
9797 m.insert("done".to_string(), Value::Bool(done));
9798 h.new_object(m)
9799 })
9800}
9801
9802/// Resume a request that was suspended on an internal `await` (always a normal
9803/// completion — the awaited promise's outcome rides in `packet`).
9804fn drive_async_gen(gen: Value, sid: u32, packet: Value) {
9805 let step = gen_resume(&gen, packet);
9806 settle_async_gen_step(gen, sid, step);
9807}
9808
9809/// Turn one body resumption into a settled step promise: transparently re-drive
9810/// internal `await` suspensions, and settle on the first REAL yield or on the
9811/// body's completion. Shared by the initial resume of a queued request and by
9812/// every await-resumption of it.
9813fn settle_async_gen_step(gen: Value, sid: u32, step: Result<GenStep, String>) {
9814 let id = match with_host(|h| match h.get(&gen) {
9815 Some(JsObj::Generator { id }) => Some(*id),
9816 _ => None,
9817 }) {
9818 Some(id) => id,
9819 None => return,
9820 };
9821 match step {
9822 Ok(GenStep::Yield(v)) => match await_marker(&v) {
9823 Some(awaited) => {
9824 // An internal `await`: settle it, then resume the body. The
9825 // request stays in flight across the suspension.
9826 let ap = promise_of(&awaited);
9827 let aid = with_host(|h| h.promise_id(&ap).unwrap());
9828 subscribe_native(
9829 aid,
9830 Box::new(move |state, val| {
9831 let tag = if state == PromiseState::Rejected {
9832 1.0
9833 } else {
9834 0.0
9835 };
9836 let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9837 drive_async_gen(gen.clone(), sid, packet);
9838 Ok(())
9839 }),
9840 );
9841 }
9842 // ECMA-262 27.6.3.8 AsyncGeneratorYield step 5: the yielded value is
9843 // AWAITED before the step promise settles, so `yield somePromise`
9844 // hands the consumer the RESOLVED value (and costs its microtask).
9845 None => {
9846 let yp = promise_of(&v);
9847 let yid = with_host(|h| h.promise_id(&yp).unwrap());
9848 subscribe_native(
9849 yid,
9850 Box::new(move |state, val| {
9851 if state == PromiseState::Rejected {
9852 reject_promise_val(sid, val);
9853 } else {
9854 resolve_promise_val(sid, iter_record(val, false));
9855 }
9856 finish_async_gen_step(gen.clone(), id);
9857 Ok(())
9858 }),
9859 );
9860 }
9861 },
9862 Ok(GenStep::Done(v)) => {
9863 resolve_promise_val(sid, iter_record(v, true));
9864 finish_async_gen_step(gen, id);
9865 }
9866 Err(e) => {
9867 let ev = take_exc_or_error(&e);
9868 reject_promise_val(sid, ev);
9869 finish_async_gen_step(gen, id);
9870 }
9871 }
9872}
9873
9874/// A promise for `v`: `v` itself if it is already a promise, else a promise
9875/// resolved with `v`.
9876pub fn promise_of(v: &Value) -> Value {
9877 if with_host(|h| h.promise_id(v)).is_some() {
9878 return v.clone();
9879 }
9880 let p = with_host(|h| h.new_promise());
9881 let id = with_host(|h| h.promise_id(&p).unwrap());
9882 resolve_promise_val(id, v.clone());
9883 p
9884}
9885
9886/// Register a native reaction on promise `id` (schedules immediately if already
9887/// settled).
9888pub fn subscribe_native(id: u32, f: Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>) {
9889 // A native continuation (`await`, promise adoption, `for await`) observes a
9890 // rejection exactly as a `.catch` does, so it is not "unhandled".
9891 with_host(|h| h.promise_mark_handled(id));
9892 let state = with_host(|h| h.promise_state(id));
9893 if state == PromiseState::Pending {
9894 with_host(|h| h.add_reaction(id, PromiseReaction::Native(f)));
9895 } else {
9896 let val = with_host(|h| h.promise_value(id));
9897 with_host(|h| h.queue_micro_native(Box::new(move || f(state, val))));
9898 }
9899}
9900
9901/// The Promise "resolve" operation: adopt `value`'s state if it is a promise,
9902/// else fulfill with it.
9903pub fn resolve_promise_val(id: u32, value: Value) {
9904 if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
9905 return;
9906 }
9907 if let Some(vid) = with_host(|h| h.promise_id(&value)) {
9908 if vid == id {
9909 // Resolving a promise with itself → reject with a TypeError.
9910 let e = with_host(|h| {
9911 crate::builtins::synth_error(h, "TypeError: Chaining cycle detected")
9912 });
9913 reject_promise_val(id, e);
9914 return;
9915 }
9916 // A native promise is still a thenable, so the spec routes it through
9917 // `NewPromiseResolveThenableJob` too — one microtask before the adoption
9918 // is even registered. (`await` does NOT pay this: V8's await optimization
9919 // subscribes to a native promise directly, which `await_value` mirrors.)
9920 with_host(|h| {
9921 h.queue_micro_native(Box::new(move || {
9922 subscribe_native(
9923 vid,
9924 Box::new(move |state, val| {
9925 with_host(|h| h.settle_promise(id, state, val.clone()));
9926 schedule_reactions(id);
9927 Ok(())
9928 }),
9929 );
9930 Ok(())
9931 }))
9932 });
9933 return;
9934 }
9935 // ECMA-262 27.2.1.3.2: any OBJECT carrying a callable `then` is assimilated
9936 // through a dedicated job — the promise adopts what `then` reports, it is
9937 // never fulfilled WITH the thenable itself.
9938 if let Some(then) = thenable_then(&value) {
9939 with_host(|h| {
9940 h.queue_micro_native(Box::new(move || resolve_thenable_job(id, value, then)))
9941 });
9942 return;
9943 }
9944 with_host(|h| h.settle_promise(id, PromiseState::Fulfilled, value));
9945 schedule_reactions(id);
9946}
9947
9948/// `value.then` if `value` is an object with a callable `then` — the test that
9949/// makes a value a *thenable*. Primitives (and objects without one) are `None`.
9950fn thenable_then(value: &Value) -> Option<Value> {
9951 // A PROXY is not a plain object and supplies `then` through its `get` trap,
9952 // so both tests below missed it: `Promise.resolve(proxyThenable)` fulfilled
9953 // WITH the proxy instead of adopting it.
9954 if with_host(|h| h.kind_of(value)) == Some(ObjKind::Proxy) {
9955 return protocol_lookup(value, "then")
9956 .ok()
9957 .flatten()
9958 .filter(|f| with_host(|h| is_callable(h, f)));
9959 }
9960 if !with_host(|h| matches!(h.get(value), Some(JsObj::Object(_)))) {
9961 return None;
9962 }
9963 let then = with_host(|h| lookup_chain(h, value, "then"))?;
9964 with_host(|h| is_callable(h, &then)).then_some(then)
9965}
9966
9967/// `NewPromiseResolveThenableJob`: hand the thenable this promise's own resolve /
9968/// reject continuations and let it settle us. A throw out of `then` rejects.
9969fn resolve_thenable_job(id: u32, thenable: Value, then: Value) -> Result<(), String> {
9970 let res = with_host(|h| h.alloc(JsObj::Builtin(format!("@@presolve:{id}"))));
9971 let rej = with_host(|h| h.alloc(JsObj::Builtin(format!("@@preject:{id}"))));
9972 if let Err(e) = invoke(&then, vec![res, rej], Some(thenable)) {
9973 let ev = take_exc_or_error(&e);
9974 reject_promise_val(id, ev);
9975 }
9976 Ok(())
9977}
9978
9979pub fn reject_promise_val(id: u32, value: Value) {
9980 if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
9981 return;
9982 }
9983 with_host(|h| {
9984 h.settle_promise(id, PromiseState::Rejected, value);
9985 h.pending_rejections.push(id);
9986 });
9987 schedule_reactions(id);
9988}
9989
9990/// Report every promise that settled rejected since the last checkpoint and
9991/// still has no handler. Node's default is `--unhandled-rejections=throw`: the
9992/// rejection becomes an uncaught exception (stderr + exit 1) unless a
9993/// `process.on('unhandledRejection')` listener takes it.
9994fn check_unhandled_rejections() -> Result<(), String> {
9995 loop {
9996 let ids: Vec<u32> = with_host(|h| std::mem::take(&mut h.pending_rejections));
9997 if ids.is_empty() {
9998 return Ok(());
9999 }
10000 for id in ids {
10001 let unhandled = with_host(|h| {
10002 h.promise_state(id) == PromiseState::Rejected && !h.promises[id as usize].handled
10003 });
10004 if !unhandled {
10005 continue;
10006 }
10007 // Report each promise at most once, however many checkpoints pass.
10008 with_host(|h| h.promise_mark_handled(id));
10009 let val = with_host(|h| h.promise_value(id));
10010 let listeners = with_host(|h| h.take_process_listeners("unhandledRejection"));
10011 if listeners.is_empty() {
10012 let msg = with_host(|h| crate::builtins::error_string(h, &val));
10013 with_host(|h| h.exc = Some(val));
10014 return Err(msg);
10015 }
10016 let promise = with_host(|h| h.alloc(JsObj::Promise { id }));
10017 for f in listeners {
10018 invoke(&f, vec![val.clone(), promise.clone()], None)?;
10019 }
10020 }
10021 }
10022}
10023
10024/// Drain a settled promise's reactions into microtasks.
10025fn schedule_reactions(id: u32) {
10026 let reactions = with_host(|h| h.take_reactions(id));
10027 let state = with_host(|h| h.promise_state(id));
10028 let value = with_host(|h| h.promise_value(id));
10029 for r in reactions {
10030 let value = value.clone();
10031 match r {
10032 PromiseReaction::Native(f) => {
10033 with_host(|h| h.queue_micro_native(Box::new(move || f(state, value))));
10034 }
10035 PromiseReaction::Js {
10036 on_ful,
10037 on_rej,
10038 result,
10039 } => {
10040 with_host(|h| {
10041 h.queue_micro_native(Box::new(move || {
10042 run_js_reaction(state, value, on_ful, on_rej, result)
10043 }))
10044 });
10045 }
10046 }
10047 }
10048}
10049
10050/// Run a `.then` reaction: call the appropriate handler and settle the result
10051/// promise with its outcome (or pass through if there is no handler).
10052fn run_js_reaction(
10053 state: PromiseState,
10054 value: Value,
10055 on_ful: Value,
10056 on_rej: Value,
10057 result: Value,
10058) -> Result<(), String> {
10059 let rid = match with_host(|h| h.promise_id(&result)) {
10060 Some(i) => i,
10061 None => return Ok(()),
10062 };
10063 let handler = if state == PromiseState::Rejected {
10064 on_rej
10065 } else {
10066 on_ful
10067 };
10068 if with_host(|h| is_callable(h, &handler)) {
10069 match invoke(&handler, vec![value], None) {
10070 Ok(r) => resolve_promise_val(rid, r),
10071 Err(e) => reject_promise_val(rid, take_exc_or_error(&e)),
10072 }
10073 } else if state == PromiseState::Rejected {
10074 reject_promise_val(rid, value);
10075 } else {
10076 resolve_promise_val(rid, value);
10077 }
10078 Ok(())
10079}
10080
10081/// The JS value of a just-caught error: the live `exc` (a real thrown value) or a
10082/// synthesized `Error` from the internal message.
10083pub fn take_exc_or_error(e: &str) -> Value {
10084 with_host(|h| {
10085 h.error.take();
10086 h.exc
10087 .take()
10088 .unwrap_or_else(|| crate::builtins::synth_error(h, e))
10089 })
10090}
10091
10092/// Register a user `.then` reaction (JS handlers + result promise).
10093pub fn promise_then(p: &Value, on_ful: Value, on_rej: Value) -> Value {
10094 let id = match with_host(|h| h.promise_id(p)) {
10095 Some(i) => i,
10096 None => return Value::Undef,
10097 };
10098 with_host(|h| h.promise_mark_handled(id));
10099 // The result is built with the RECEIVER's species, so a subclass promise
10100 // stays a subclass promise through a `.then` chain.
10101 let result = match crate::builtins::promise_species_from(p) {
10102 Ok(Some(sp)) => sp,
10103 _ => with_host(|h| h.new_promise()),
10104 };
10105 let reaction = PromiseReaction::Js {
10106 on_ful,
10107 on_rej,
10108 result: result.clone(),
10109 };
10110 let state = with_host(|h| h.promise_state(id));
10111 if state == PromiseState::Pending {
10112 with_host(|h| h.add_reaction(id, reaction));
10113 } else {
10114 let value = with_host(|h| h.promise_value(id));
10115 if let PromiseReaction::Js {
10116 on_ful,
10117 on_rej,
10118 result,
10119 } = reaction
10120 {
10121 with_host(|h| {
10122 h.queue_micro_native(Box::new(move || {
10123 run_js_reaction(state, value, on_ful, on_rej, result)
10124 }))
10125 });
10126 }
10127 }
10128 result
10129}
10130
10131/// The ReferenceError for touching `this` in a derived constructor before
10132/// `super()` — or returning from one without calling it.
10133pub fn this_before_super_error() -> String {
10134 "ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor".to_string()
10135}