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 /// The definition's byte range in its script (`(0, 0)`: none), for
367 /// `Function.prototype.toString` (20.2.3.5).
368 #[serde(default)]
369 pub span: (u32, u32),
370 /// The host `scripts` entry `span` indexes, set when the program loads.
371 #[serde(default)]
372 pub script: Option<u32>,
373}
374
375/// One parameter slot. `name` is the simple bound name; a destructuring pattern
376/// is lowered to a synthetic `.arg{i}` name plus body prologue code.
377#[derive(Clone, serde::Serialize, serde::Deserialize)]
378pub struct ParamSlot {
379 pub name: String,
380 /// True for the `...rest` collector.
381 pub rest: bool,
382 /// True if this slot has a default expression (applied in the body prologue).
383 pub has_default: bool,
384}
385
386/// A compiled `try`/`catch`/`finally` block. Bodies are bare chunks run in the
387/// current scope.
388#[derive(Clone, serde::Serialize, serde::Deserialize)]
389pub struct TryDef {
390 pub block: Chunk,
391 /// `(catch_param_name, catch_body)`.
392 pub handler: Option<(Option<String>, Chunk)>,
393 pub finalizer: Option<Chunk>,
394}
395
396/// A live closure value.
397#[derive(Clone)]
398pub struct FuncVal {
399 pub def_id: usize,
400 /// Captured lexical environment (enclosing scope chain), for free vars.
401 pub env: Option<Env>,
402 /// `this` captured at definition time (arrow functions).
403 pub this: Option<Value>,
404 pub is_arrow: bool,
405 /// The owning class name for a method (drives `super` resolution). `None` for
406 /// plain functions/arrows.
407 pub home_class: Option<String>,
408 /// Whether that method is a STATIC one. `super.x` resolves against a
409 /// different object in each case — the parent constructor for a static
410 /// method, the parent's prototype for an instance method — and the class
411 /// name alone cannot tell them apart, since both carry the same one.
412 pub home_static: bool,
413 /// The object literal a shorthand method was defined in, for `super` inside
414 /// it. A class method resolves `super` through `home_class` instead; this
415 /// is the `[[HomeObject]]` an ordinary `{ m() { super.x } }` needs, and
416 /// without it there was nothing to resolve against.
417 pub home_object: Option<Value>,
418}
419
420/// A heap object.
421#[derive(Clone)]
422pub enum JsObj {
423 Str(String),
424 Array(Vec<Value>),
425 Object(IndexMap<String, Value>),
426 Func(FuncVal),
427 /// A first-class reference to a builtin function or namespace
428 /// (`console.log`, `Math`, `parseInt`).
429 Builtin(String),
430 /// A bound method value (`obj.method` captured then called): dispatches
431 /// through `call_method(recv, name, args)` when invoked.
432 BoundMethod {
433 recv: Value,
434 name: String,
435 },
436 /// The single canonical `null`.
437 Null,
438 /// A live iterator over a sequence, with a cursor.
439 Iter {
440 items: Vec<Value>,
441 idx: usize,
442 },
443 /// A bound function (`fn.bind(thisArg, ...preargs)`).
444 BoundFunc {
445 target: Value,
446 this: Value,
447 args: Vec<Value>,
448 },
449 /// A class constructor value: the runtime object produced by a `class`.
450 Class(ClassVal),
451 /// A `Symbol` — a unique property key. `registered` marks a `Symbol.for`
452 /// key (shared) vs a fresh `Symbol()`.
453 Symbol {
454 desc: Option<String>,
455 id: u64,
456 },
457 /// A `Map` (or `WeakMap` when `weak`): insertion-ordered key→value entries.
458 Map {
459 entries: IndexMap<MapKey, (Value, Value)>,
460 weak: bool,
461 },
462 /// A `Set` (or `WeakSet` when `weak`): insertion-ordered unique values.
463 Set {
464 entries: IndexMap<MapKey, Value>,
465 weak: bool,
466 },
467 /// A live generator, backed by a stackful `corosensei` coroutine in
468 /// `JsHost.generators`.
469 Generator {
470 id: u32,
471 },
472 /// A Promise, backed by a `PromiseCell` in `JsHost.promises`.
473 Promise {
474 id: u32,
475 },
476 /// An arbitrary-precision `BigInt` (`typeof === "bigint"`).
477 BigInt(num_bigint::BigInt),
478 /// A compiled regular expression (`/pat/flags` or `new RegExp(...)`).
479 RegExp(Box<RegExpObj>),
480 /// A `Proxy`: every essential internal method is diverted to `handler`'s
481 /// traps (see `crate::proxy`). `revoked` is set by the thunk
482 /// `Proxy.revocable` hands back, after which every operation throws.
483 Proxy {
484 target: Value,
485 handler: Value,
486 revoked: bool,
487 },
488}
489
490/// Which variant a heap object is, carrying none of its contents.
491///
492/// Property access has to pick a branch by variant, but the code inside a branch
493/// re-enters the host (`bound_method`, `lookup_chain`, `invoke`), so it cannot
494/// hold a `&JsObj` borrow across the match. The way out used to be
495/// `h.get(v).cloned()` — which deep-copies the entire backing store (a whole
496/// `Vec<Value>`, `IndexMap`, or `String`) just to read its tag. That made one
497/// property read O(len) and any loop over a collection O(n^2). This type is the
498/// same discriminant with nothing attached, so the probe is O(1) and each branch
499/// re-borrows for only the one field it actually needs.
500/// The well-known symbols node-js actually honors. `Symbol.<name>` is the
501/// interned symbol `@@Symbol.<name>`, and using it as a property key stores
502/// under the sentinel string `@@<name>` (`property_key`) so the internal
503/// lookups (`@@iterator`, `@@toPrimitive`, …) can find it without a symbol
504/// table walk. Symbols V8 defines but node-js does not act on are deliberately
505/// absent: a symbol that reads back while the operator it names ignores it would
506/// be a silent fake. `hasInstance` is listed because `instance_of` consults it.
507pub const WELL_KNOWN_SYMBOLS: &[&str] = &[
508 "iterator",
509 "asyncIterator",
510 "toPrimitive",
511 "toStringTag",
512 "hasInstance",
513 // Nine more the table was missing entirely, so `Symbol.species` and friends
514 // read `undefined` and no protocol keyed on them could be expressed.
515 "species",
516 "isConcatSpreadable",
517 "match",
518 "matchAll",
519 "replace",
520 "search",
521 "split",
522 "unscopables",
523 "dispose",
524 "asyncDispose",
525];
526
527/// Whether the internal key `k` came from a SYMBOL used as a property key
528/// (`@@sym:<id>`, or a well-known `@@iterator`), as opposed to one of node-js's
529/// hidden slots (`@@native`, `@@bytes`, `@@ms`, `@@kind`, …). Only the former
530/// is an observable JavaScript property.
531pub fn is_symbol_key(k: &str) -> bool {
532 match k.strip_prefix("@@") {
533 Some(rest) => rest
534 .strip_prefix("sym:")
535 .map(|i| i.parse::<u64>().is_ok())
536 .unwrap_or_else(|| WELL_KNOWN_SYMBOLS.contains(&rest)),
537 None => false,
538 }
539}
540
541#[derive(Clone, Copy, PartialEq, Eq, Debug)]
542pub enum ObjKind {
543 Str,
544 Array,
545 Object,
546 Func,
547 Builtin,
548 BoundMethod,
549 Null,
550 Iter,
551 BoundFunc,
552 Class,
553 Symbol,
554 Map,
555 Set,
556 Generator,
557 Promise,
558 BigInt,
559 RegExp,
560 Proxy,
561}
562
563impl JsObj {
564 /// This object's variant, without touching its contents.
565 pub fn kind(&self) -> ObjKind {
566 match self {
567 JsObj::Str(_) => ObjKind::Str,
568 JsObj::Array(_) => ObjKind::Array,
569 JsObj::Object(_) => ObjKind::Object,
570 JsObj::Func(_) => ObjKind::Func,
571 JsObj::Builtin(_) => ObjKind::Builtin,
572 JsObj::BoundMethod { .. } => ObjKind::BoundMethod,
573 JsObj::Null => ObjKind::Null,
574 JsObj::Iter { .. } => ObjKind::Iter,
575 JsObj::BoundFunc { .. } => ObjKind::BoundFunc,
576 JsObj::Class(_) => ObjKind::Class,
577 JsObj::Symbol { .. } => ObjKind::Symbol,
578 JsObj::Map { .. } => ObjKind::Map,
579 JsObj::Set { .. } => ObjKind::Set,
580 JsObj::Generator { .. } => ObjKind::Generator,
581 JsObj::Promise { .. } => ObjKind::Promise,
582 JsObj::BigInt(_) => ObjKind::BigInt,
583 JsObj::RegExp(_) => ObjKind::RegExp,
584 JsObj::Proxy { .. } => ObjKind::Proxy,
585 }
586 }
587}
588
589/// A `RegExp` object: the compiled `fancy_regex::Regex` plus the JS-visible
590/// source, flag booleans, and the mutable `lastIndex` cursor (used by `g`/`y`
591/// matching). fancy-regex adds lookaround + backreferences on top of the Rust
592/// `regex` fast path, so the JS grammar node-js can accept is a near-superset.
593#[derive(Clone)]
594pub struct RegExpObj {
595 /// The translated regex. Construction of a pattern fancy-regex still cannot
596 /// express (documented in BUGS.md) throws at `RegExp` build time, so a live
597 /// `RegExpObj` always holds a compiled engine.
598 ///
599 /// Shared (`Rc`) rather than owned, because a regex LITERAL builds a fresh
600 /// `RegExpObj` on every evaluation — it has to, since `lastIndex` is
601 /// per-object mutable state — while the compiled engine behind it is
602 /// immutable and identical every time. See `regexp::compiled`.
603 pub re: std::rc::Rc<fancy_regex::Regex>,
604 pub source: String,
605 pub flags: String,
606 pub global: bool,
607 pub ignore_case: bool,
608 pub multiline: bool,
609 pub dot_all: bool,
610 pub sticky: bool,
611 pub unicode: bool,
612 /// `lastIndex`, in UTF-16 code units; advanced by `exec`/`test` under the
613 /// `g`/`y` flags. The newtype keeps it from being confused with the regex
614 /// engine's byte offsets, which are the same shape and differ off the BMP.
615 pub last_index: crate::utf16::U16Index,
616}
617
618/// A Promise's settled state and pending reactions.
619pub struct PromiseCell {
620 pub state: PromiseState,
621 pub value: Value,
622 /// Reactions registered while still pending; drained (as microtasks) on
623 /// settle.
624 pub reactions: Vec<PromiseReaction>,
625 /// True once a rejection has been observed by a handler (`.then`/`.catch`),
626 /// so the loop doesn't report it as unhandled.
627 pub handled: bool,
628}
629
630/// A pending Promise reaction: a user `.then` (JS handlers + a result promise) or
631/// a native continuation (Promise chaining / async `await` resumption).
632pub enum PromiseReaction {
633 Js {
634 on_ful: Value,
635 on_rej: Value,
636 result: Value,
637 },
638 Native(Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>),
639}
640
641#[derive(Default, Clone, Copy, PartialEq, Eq)]
642pub enum PromiseState {
643 #[default]
644 Pending,
645 Fulfilled,
646 Rejected,
647}
648
649/// A live class constructor. The prototype object (holding instance methods) and
650/// the static-side own properties live on the heap; `parent` is the superclass
651/// constructor value (`None` for a base class).
652#[derive(Clone)]
653pub struct ClassVal {
654 pub name: String,
655 /// The constructor function value (a `JsObj::Func`), or `None` for a class
656 /// with only a synthesized default constructor.
657 pub ctor: Option<Value>,
658 pub parent: Option<Value>,
659 /// `C.prototype` — the object instances delegate to.
660 pub proto: Value,
661 /// Static own properties (static methods/fields), plus `name`/`prototype`.
662 pub statics: IndexMap<String, Value>,
663 /// Instance field initializers: `(name, thunk_fn, name_anon_init)`, run
664 /// per-instance after `super()` (or at construction start for a base class).
665 /// `name_anon_init` records the SYNTACTIC fact that the initializer was an
666 /// anonymous function definition, so 15.7.10 NamedEvaluation applies to its
667 /// result — it cannot be re-derived at run time (a field initialised from an
668 /// already-anonymous function held elsewhere must not be renamed).
669 pub fields: Vec<(String, Value, bool)>,
670 /// The FuncDef holding the class's source span (`String(C)`).
671 pub source_def: Option<usize>,
672}
673
674/// The result of resolving `super.name`: a getter to invoke (accessor property)
675/// or a directly-usable value (method / data property).
676pub enum SuperRef {
677 Getter(Value),
678 Data(Value),
679}
680
681/// A `Map`/`Set` key under SameValueZero: `NaN` collapses to one key, `-0` and
682/// `+0` are the same key, primitives compare by value, objects by heap identity.
683#[derive(Clone, PartialEq, Eq, Hash)]
684pub enum MapKey {
685 Undef,
686 Null,
687 Bool(bool),
688 /// f64 bit pattern with `NaN` canonicalized and `-0` normalized to `+0`.
689 Num(u64),
690 /// A `BigInt` key, by its decimal string (SameValueZero: `1n` is one key).
691 Big(String),
692 Str(String),
693 /// Heap identity (objects, arrays, functions, symbols).
694 Ref(u32),
695 /// A builtin intrinsic, by the name it answers to. Every bare reference
696 /// to `Math` or `parseInt` allocates a fresh handle, so heap identity
697 /// would make `new Set([Math, Math])` two entries; `strict_eq` compares
698 /// these by name too.
699 Intrinsic(String),
700}
701
702// ── environments ─────────────────────────────────────────────────────────────
703
704/// The map behind a scope. Hashing these with `FxHash` instead of the default
705/// was measured SLOWER, not faster — fib went 652ms to 1086ms and a 5M-iteration
706/// counting loop 1894ms to 2381ms on the same machine — so the default stands.
707pub type VarMap = IndexMap<String, Value>;
708
709/// A local-variable environment, shared (by `Rc`) between a frame and any nested
710/// function that captures it.
711pub struct EnvData {
712 pub vars: VarMap,
713 /// The names in `vars` that were declared `const`, so an assignment to one
714 /// throws (16.1.3 / 8.5.2 — an immutable binding rejects SetMutableBinding).
715 ///
716 /// A separate set rather than a flag inside `VarMap`'s value, because
717 /// `set_name` is a hot path — the common case is an env with NO consts,
718 /// where `is_empty()` settles it without hashing the name a second time.
719 pub consts: rustc_hash::FxHashSet<String>,
720 pub parent: Option<Env>,
721}
722pub type Env = Rc<RefCell<EnvData>>;
723
724/// An accessor property: `(getter, setter)`, either optional.
725pub type Accessor = (Option<Value>, Option<Value>);
726
727/// Prefix of the hidden property-map entry that reserves an accessor's slot in
728/// own-key insertion order (see `set_accessor`).
729pub const ORD_MARKER: &str = "@@ord:";
730
731/// The three ECMAScript own-property attributes. `PropAttrs::default()` is the
732/// all-true shape a plain `o.k = v` assignment produces, which is why only
733/// deviations need storing.
734#[derive(Clone, Copy, Debug, PartialEq, Eq)]
735pub struct PropAttrs {
736 pub writable: bool,
737 pub enumerable: bool,
738 pub configurable: bool,
739}
740
741impl Default for PropAttrs {
742 fn default() -> Self {
743 PropAttrs {
744 writable: true,
745 enumerable: true,
746 configurable: true,
747 }
748 }
749}
750
751impl PropAttrs {
752 /// The attribute shape V8 gives an internal-but-inspectable slot such as
753 /// `Error.prototype.message`, `err.stack` or a `Buffer`'s view metadata:
754 /// readable and replaceable, but never enumerated.
755 pub const HIDDEN: PropAttrs = PropAttrs {
756 writable: true,
757 enumerable: false,
758 configurable: true,
759 };
760}
761
762fn new_env(parent: Option<Env>) -> Env {
763 Rc::new(RefCell::new(EnvData {
764 vars: VarMap::default(),
765 consts: rustc_hash::FxHashSet::default(),
766 parent,
767 }))
768}
769
770/// A fresh empty scope chained under `parent`.
771pub fn child_env(parent: Env) -> Env {
772 new_env(Some(parent))
773}
774
775/// One function activation.
776pub struct Frame {
777 pub env: Env,
778 /// The env this activation started in — the FUNCTION scope. `var` and hoisted
779 /// function declarations bind here no matter how many block scopes are open.
780 pub base_env: Env,
781 pub this_obj: Option<Value>,
782 /// `new.target` for this activation (the constructor when invoked via `new`).
783 pub new_target: Option<Value>,
784 /// The class value owning the running method (drives `super`); `None` outside
785 /// a class method/constructor.
786 pub home_class: Option<Value>,
787 /// Whether the running method is a static one — see `FuncVal::home_static`.
788 pub home_static: bool,
789 /// The object literal owning the running method — see
790 /// `FuncVal::home_object`.
791 pub home_object: Option<Value>,
792 /// Whether the code in this activation is strict. A write the object
793 /// refuses is a silent no-op in sloppy mode and a `TypeError` here, so the
794 /// ASSIGNMENT SITE decides — not the object being written to.
795 pub strict: bool,
796 /// Source line the frame is currently executing (updated by the DAP line hook
797 /// under `--dap`; stays 0 on ordinary runs).
798 pub line: u32,
799 /// The function name that owns this frame, for the DAP `stackTrace`; `None`
800 /// for the module frame and anonymous activations.
801 pub owner: Option<String>,
802 /// True ONLY for the program's module frame. A generator/async body runs on a
803 /// coroutine whose swapped-in context holds just ITS OWN frame, so the frame
804 /// COUNT cannot tell "module scope" from "coroutine body scope" — without this
805 /// flag every top-level `let`/`var` in such a body declared a GLOBAL, shared
806 /// across concurrent activations of the same function.
807 pub is_module: bool,
808 /// Whether this activation's `this` is bound yet — see [`ThisState`].
809 pub this_state: ThisState,
810}
811
812/// The `[[ThisBindingStatus]]` of a function environment (9.1.1.3), as far as it
813/// is observable: only a DERIVED class constructor starts with `this`
814/// uninitialized, and only `super()` binds it.
815///
816/// The instance is still allocated up front (`construct_class`), so the
817/// state is what makes it unreachable until then: `this` before `super()`, a
818/// second `super()`, and returning without one are each the error node raises
819/// rather than a silent write to the pre-allocated object.
820#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
821pub enum ThisState {
822 /// Every other activation: `this` is whatever was passed in.
823 #[default]
824 Plain,
825 /// A derived constructor before `super()` has returned.
826 Pending,
827 /// A derived constructor after `super()`.
828 Bound,
829}
830
831/// A non-local control signal. `Break`/`Continue` carry the optional loop label
832/// and are only raised when the target loop lives in an ENCLOSING chunk (a
833/// `break` inside a `try` block, which the host runs as its own chunk); a
834/// same-chunk `break` is a plain compiler-resolved jump.
835#[derive(Clone)]
836pub enum Signal {
837 Return(Value),
838 Break(Option<String>),
839 Continue(Option<String>),
840}
841
842/// The JavaScript runtime.
843pub struct JsHost {
844 heap: Vec<JsObj>,
845 /// Function templates, indexed by def id.
846 pub funcs: Vec<FuncDef>,
847 /// Every script text a loaded program was parsed from; a `FuncDef`'s
848 /// `script` indexes it and its `span` slices it.
849 pub scripts: Vec<std::sync::Arc<str>>,
850 /// try/catch/finally block templates, indexed by try id.
851 pub tries: Vec<TryDef>,
852 /// Module-level (global) names.
853 globals: VarMap,
854 /// The one uninitialized-binding marker, allocated on first use. See
855 /// [`JsHost::tdz_marker`].
856 tdz: Option<Value>,
857 /// Module-top-level names still in their temporal dead zone. Kept out of
858 /// `globals` so the marker is never reachable as `globalThis.<name>`.
859 tdz_globals: rustc_hash::FxHashSet<String>,
860 /// Top-level `const` names (a module frame declares into `globals`), so an
861 /// assignment to one throws the same way a block-scoped `const` does.
862 global_consts: rustc_hash::FxHashSet<String>,
863 /// The frame stack (bottom = module).
864 frames: Vec<Frame>,
865 /// The program's top-level scope — the scope runtime-compiled source runs in
866 /// (`new Function`, indirect `eval`, `vm.runInThisContext`; see
867 /// `run_chunk_in_global_scope`), as opposed to whatever function frame
868 /// happens to be executing when that source is compiled.
869 ///
870 /// Held as its own field rather than read off `frames[0]` because a coroutine
871 /// body runs with `frames` SWAPPED for its own one-frame context
872 /// (`install_gen_ctx`), so the bottom frame is not the top-level frame there.
873 ///
874 /// Note this is node-js's ONE top-level scope. Node distinguishes the global
875 /// scope from a CommonJS module's scope (a module body is a wrapper
876 /// function), so in Node a file's top-level `var` is invisible to dynamic
877 /// code; here the entry file is evaluated with Script semantics, so it stays
878 /// visible. That is the same entry-file-is-a-Script divergence `BUGS.md`
879 /// records for top-level `return`, not a separate one — and `node -e`, which
880 /// really is a Script, matches Node exactly.
881 global_env: Env,
882 pub error: Option<String>,
883 /// The in-flight thrown value, if any (JS `throw`).
884 pub exc: Option<Value>,
885 pub signal: Option<Signal>,
886 /// Promises that settled REJECTED this tick. Drained at each microtask
887 /// checkpoint: any still without a handler is an unhandled rejection.
888 pub pending_rejections: Vec<u32>,
889 /// `process.on(event, fn)` listeners, by event name.
890 pub process_listeners: IndexMap<String, Vec<ProcListener>>,
891 /// The canonical `null` handle (allocated once).
892 null_val: Value,
893 /// `[[Prototype]]` link per heap object, by heap index. Absent = default
894 /// (`Object.prototype` for objects, `null` for the root).
895 protos: HashMap<u32, Value>,
896 /// Heap objects whose `[[Prototype]]` is *explicitly* null — via
897 /// `Object.create(null)` or `Object.setPrototypeOf(o, null)`. Distinct from a
898 /// bare `{}` (absent from `protos` but conceptually `Object.prototype`), which
899 /// is why `Object.create(null) instanceof Object` can read `false`.
900 null_proto_objs: HashSet<u32>,
901 /// Own properties of function objects (functions are objects in JS): a live
902 /// closure's `name`/`prototype`/static-ish members. Keyed by heap index.
903 fn_props: HashMap<u32, IndexMap<String, Value>>,
904 /// Accessor (getter/setter) properties per owning object, by heap index then
905 /// key: `(get, set)`. Class `get x()`/`set x()` install here on the prototype.
906 accessors: HashMap<u32, IndexMap<String, Accessor>>,
907 /// Own-property attributes that deviate from the plain-assignment default
908 /// (`{writable, enumerable, configurable}` all true), by heap index then key.
909 /// Only non-default entries are stored, so an ordinary object costs nothing;
910 /// `prop_attrs` returns the default for any key absent here. This is what
911 /// makes `Object.defineProperty(o, k, {enumerable: false})` invisible to
912 /// `Object.keys`/`for-in`/`JSON.stringify` while `getOwnPropertyNames` still
913 /// reports it, and what hides `Error`'s `message`/`stack` the way V8 does.
914 prop_attrs: HashMap<u32, IndexMap<String, PropAttrs>>,
915 /// Heap objects sealed against new properties by `Object.preventExtensions`,
916 /// `Object.seal` or `Object.freeze`.
917 non_extensible: HashSet<u32>,
918 /// Private names (`#m`) declared as a METHOD or accessor rather than as a
919 /// field, for the brand-check error text: node distinguishes `Receiver must
920 /// be an instance of class C` (a private method or accessor) from `Cannot
921 /// read private member #x …` (a private field). Which class is answered by
922 /// the running method's home class, not by this set, so two classes
923 /// declaring the same private method name stay exact.
924 private_methods: HashSet<String>,
925 /// The ELIDED element positions of each array, by heap index. Absent (the
926 /// overwhelmingly common case) means the array is dense.
927 ///
928 /// A hole is deliberately NOT a `Value` variant. A sentinel value would have
929 /// to be mapped back to `undefined` at every element read in the runtime, and
930 /// a single missed read would leak an un-nameable value into user code — a
931 /// worse failure than storing `undefined` and losing the distinction. Keeping
932 /// the marker OUTSIDE the value domain makes that leak structurally
933 /// impossible: the element vector still holds a perfectly ordinary
934 /// `Value::Undef` at a hole, so any code path that has not been taught about
935 /// holes degrades to exactly the pre-existing behaviour (a visible
936 /// `undefined`) instead of producing something unrepresentable.
937 ///
938 /// Sized like the array it describes in the worst case (`new Array(n)` marks
939 /// every index), which is the same order as the `Vec<Value>` already paid for
940 /// that array — so it cannot turn a working allocation into an OOM.
941 array_holes: HashMap<u32, rustc_hash::FxHashSet<usize>>,
942 /// See `take_super_replacement`.
943 super_replacement: Option<Value>,
944 /// Set by `run_class_ctor` for the one call that follows: the next user
945 /// function activation is a derived constructor and starts `Pending`.
946 derived_ctor_next: bool,
947 /// Whether the entry script's top-level `var`s bind to its own scope rather
948 /// than to the globals map — the CommonJS wrapper Node puts every file in.
949 module_scope: bool,
950 /// User-assigned static properties on a builtin namespace/constructor, keyed
951 /// by namespace name then property (`Error` → `prepareStackTrace`,
952 /// `stackTraceLimit`). Each bare `Error` reference allocates a fresh
953 /// `Builtin` handle, so these cannot live in `fn_props` (which is per-heap-
954 /// index); this stable side table lets `Error.prepareStackTrace = fn` persist.
955 builtin_statics: HashMap<String, IndexMap<String, Value>>,
956 /// The shared well-known `Object.prototype` object (chain root for objects).
957 object_proto: Value,
958 /// Class name of each class `prototype` object, by heap index — lets an
959 /// instance recover its constructor name (for `util.inspect` prefix and
960 /// `obj.constructor.name`).
961 proto_class: HashMap<u32, Value>,
962 /// Class constructor values by name, so a running method's `home_class` name
963 /// resolves to its class value (for `super`).
964 class_registry: HashMap<String, Value>,
965 /// Well-known prototype objects for the builtin error constructors, by name.
966 error_protos: HashMap<String, Value>,
967 /// The template object of each tagged-template SITE, keyed by the chunk that
968 /// holds the site and the site's ordinal within its compilation.
969 ///
970 /// GetTemplateObject (13.2.8.4) caches by Parse Node, so a site evaluated
971 /// twice hands back the SAME object: ``const t = () => tag`x`;`` makes
972 /// `t() === t()` true, and a tag that memoizes on the strings array — the
973 /// documented reason the object is cached, and how `lit-html` and `graphql`
974 /// avoid re-parsing — saw a fresh array every call here. Two sites with
975 /// identical text are still distinct objects, which the chunk hash plus the
976 /// ordinal keep apart.
977 template_objects: HashMap<(u64, u64), Value>,
978 /// Real prototype *objects* for the builtin exotics whose instances need a
979 /// genuine `[[Prototype]]` link (`Buffer`, `Uint8Array`). Most builtin
980 /// prototypes are `Builtin("<Ctor>.prototype")` thunk namespaces, which
981 /// cannot appear on a prototype chain and report `typeof "function"`.
982 native_protos: HashMap<String, Value>,
983 /// `Symbol.for` registry: description → symbol value.
984 symbol_registry: HashMap<String, Value>,
985 /// Monotonic id source for fresh `Symbol()` values.
986 next_symbol: u64,
987 /// Every live symbol by its id, so a `@@sym:<id>` property key can be
988 /// turned back into the symbol VALUE for `Object.getOwnPropertySymbols`.
989 symbols_by_id: HashMap<u64, Value>,
990 /// Well-known symbol ids (`Symbol.iterator` …) to their ECMAScript name.
991 /// Identity is by id, not description, so a user `Symbol("Symbol.iterator")`
992 /// is a distinct key.
993 well_known_ids: HashMap<u64, String>,
994 /// Suspended generator coroutines, indexed by `JsObj::Generator.id`.
995 generators: Vec<GenCell>,
996 /// Promise cells, indexed by `JsObj::Promise.id`.
997 promises: Vec<PromiseCell>,
998 /// Whether the loop is part-way through draining the microtask queue, so a
999 /// `nextTick` queued by one of them waits for the round to finish. See
1000 /// `next_microtask`.
1001 draining_micro: bool,
1002 /// `process.nextTick` callbacks (drained before promise microtasks).
1003 pub nextticks: std::collections::VecDeque<Task>,
1004 /// Promise-reaction / `queueMicrotask` microtasks.
1005 pub microtasks: std::collections::VecDeque<Task>,
1006 /// `setTimeout`/`setInterval`/`setImmediate` macrotasks.
1007 pub macrotasks: Vec<Timer>,
1008 /// Monotonic timer-id source.
1009 next_timer: u64,
1010 /// Cloned by I/O worker threads to post `IoTask`s back to the main-thread
1011 /// event loop. Kept alive for the host's lifetime so the loop's `recv` never
1012 /// sees a spurious `Disconnected` while a server is running.
1013 io_tx: Sender<IoTask>,
1014 /// Owned by the event loop (taken out for the blocking `recv`). Receives the
1015 /// `IoTask`s posted by I/O threads.
1016 io_rx: Option<Receiver<IoTask>>,
1017 /// Ref-count of "things keeping the process alive": open listeners, live
1018 /// sockets, ref'd handles. The loop exits only when this is `0` AND both task
1019 /// queues are empty. A pure script never touches it, so it exits exactly as
1020 /// before.
1021 open_handles: usize,
1022 /// In-process output sink. When `Some`, everything the program writes to
1023 /// stdout/stderr is appended here instead of reaching the process streams —
1024 /// what an embedder (a TUI that owns the terminal) needs so a `console.log`
1025 /// cannot corrupt its display. `None` (the default) is the ordinary
1026 /// standalone `node` behaviour: writes go straight to the real streams.
1027 ///
1028 /// Bytes, not `String`: a program may legitimately write output that is not
1029 /// valid UTF-8 (`process.stdout.write(Buffer.from([0xff]))`), and a `String`
1030 /// buffer can only hold the lossy `U+FFFD` transcription of it.
1031 capture: Option<Vec<u8>>,
1032 /// `process.exitCode`: the code the process exits with when the event loop
1033 /// drains, or `None` while unset. Separate from an explicit
1034 /// `process.exit(n)`, which exits immediately with `n`.
1035 pub exit_code: Option<i32>,
1036 /// Whether the `exit` event has already been emitted, so the `process.exit`
1037 /// path and the end-of-loop path cannot both fire it (Node's `_exiting`).
1038 pub exiting: bool,
1039 /// The one `globalThis` object. It has to be a singleton: `globalThis` is an
1040 /// identity in JS, so `globalThis === globalThis` is `true` and a property
1041 /// written through one read is visible through the next. Minting a fresh
1042 /// object per read made both false.
1043 global_obj: Value,
1044}
1045
1046/// One `process.on`/`process.once` registration. `once` is not decoration: a
1047/// `once` listener must be UNREGISTERED before it runs, so a second `emit` of
1048/// the same event does not reach it. Treating `once` as an alias of `on` made
1049/// `process.once('e', f); process.emit('e'); process.emit('e')` call `f` twice
1050/// and leave it in `process.listeners('e')` — node v26.7.0 calls it once and
1051/// reports zero listeners afterwards.
1052#[derive(Clone)]
1053pub struct ProcListener {
1054 pub f: Value,
1055 pub once: bool,
1056}
1057
1058/// A queued unit of work: either a JS callback invocation (`queueMicrotask`,
1059/// `nextTick`, timer body) or a native step (Promise reaction / async resume).
1060pub enum Task {
1061 Js { cb: Value, args: Vec<Value> },
1062 Native(Box<dyn FnOnce() -> Result<(), String>>),
1063}
1064
1065impl Task {
1066 fn run(self) -> Result<(), String> {
1067 match self {
1068 Task::Js { cb, args } => invoke(&cb, args, None).map(|_| ()),
1069 Task::Native(f) => f(),
1070 }
1071 }
1072}
1073
1074/// A scheduled macrotask (`setTimeout`/`setInterval`/`setImmediate`). Ordering
1075/// is by `(delay, seq)` — a deterministic virtual clock, never wall time.
1076pub struct Timer {
1077 pub id: u64,
1078 pub delay: f64,
1079 pub seq: u64,
1080 pub callback: Value,
1081 pub args: Vec<Value>,
1082 pub cancelled: bool,
1083 /// Repeat period in ms for a `setInterval` timer; `None` for the one-shot
1084 /// `setTimeout`/`setImmediate`. A repeating timer is re-armed with a fresh
1085 /// deadline each time it fires, so it keeps the loop alive indefinitely —
1086 /// exactly like Node, where `setInterval` runs until cleared.
1087 pub interval: Option<f64>,
1088 /// Node's `ref`/`unref` handle bit. Only a *referenced* pending timer keeps
1089 /// the event loop alive; an unref'd one still fires while the loop happens
1090 /// to be alive for another reason, but never holds it open by itself.
1091 pub refed: bool,
1092 /// Real wall-clock deadline (`now + delay`), used only on the real-clock
1093 /// path (an open handle or a pending interval). On the pure virtual clock
1094 /// this is ignored.
1095 pub deadline: Instant,
1096}
1097
1098/// One suspended generator. `coro` is `None` only while actively running (taken
1099/// out across `Coroutine::resume`); `ctx` holds its volatile execution context
1100/// (frames/signal/error/exc) while suspended.
1101struct GenCell {
1102 coro: Option<corosensei::Coroutine<Value, Value, Result<Value, String>>>,
1103 /// Raw pointer to the coroutine body's `Yielder`, published on entry (same
1104 /// thread → valid for the body's life). Read by `yield` to suspend.
1105 yielder: *const (),
1106 ctx: GenContext,
1107 done: bool,
1108 /// True once the body has been resumed at least once (so it is suspended at a
1109 /// `yield`). `.return()`/`.throw()` only unwind a *started* generator.
1110 started: bool,
1111 /// A completion injected by `.return(v)` / `.throw(e)`: consumed by the next
1112 /// `yield` resume so the body unwinds (running any pending `finally`).
1113 inject: Option<GenInject>,
1114 /// True for an `async function*` body, where `await` AND `yield` share one
1115 /// coroutine yielder: `await` wraps its operand in an await marker so the
1116 /// driver can tell an internal suspension from a real yield.
1117 async_gen: bool,
1118 /// `[[AsyncGeneratorQueue]]` — pending requests as
1119 /// `(completion, step promise id)`. ECMA-262 27.6.3.6 keeps this queue so
1120 /// overlapping requests resume the body ONE AT A TIME and settle in request
1121 /// order; without it a second request issued before the first settles races
1122 /// past it and the results arrive swapped. `.next`, `.return` AND `.throw`
1123 /// all enqueue — a `.return()` that skipped the queue would terminate the
1124 /// body while an earlier `.next()` was still suspended on an `await`, and
1125 /// that `.next()` would then wrongly report `{done: true}`.
1126 queue: std::collections::VecDeque<(GenReq, u32)>,
1127 /// True while a queued request is being driven.
1128 running: bool,
1129 /// The [`stack_floor`] that applies while this generator's body is running.
1130 ///
1131 /// A corosensei coroutine executes on its OWN mmap'd stack, so the address
1132 /// range the thread's pthread record describes says nothing about how much
1133 /// room the body has left. Recorded from the coroutine's `Stack::limit()` at
1134 /// construction and swapped in around every resume; without it the guard
1135 /// compared a coroutine stack pointer against the main stack's floor and
1136 /// (depending on where mmap landed) either fired immediately or never.
1137 stack_floor: usize,
1138}
1139
1140/// A forced completion pushed into a suspended generator by `.return()`/`.throw()`.
1141enum GenInject {
1142 Return(Value),
1143 Throw(Value),
1144}
1145
1146/// One queued `[[AsyncGeneratorQueue]]` request. ECMA-262 27.6.3.6
1147/// `AsyncGeneratorEnqueue` records a *completion*, not just a sent value, which
1148/// is why `.return()` and `.throw()` queue behind pending `.next()` calls
1149/// instead of unwinding the body on the spot.
1150#[derive(Clone)]
1151pub enum GenReq {
1152 /// `.next(v)` — resume normally with `v`.
1153 Next(Value),
1154 /// `.return(v)` — resume with a forced return completion.
1155 Return(Value),
1156 /// `.throw(e)` — resume with a forced throw completion.
1157 Throw(Value),
1158}
1159
1160/// The mutable "execution registers" swapped at every generator resume/suspend
1161/// boundary so a suspended generator's half-finished frame/signal state never
1162/// leaks into the resuming caller. The heap, function/class tables and globals
1163/// are shared and never swapped.
1164#[derive(Default)]
1165struct GenContext {
1166 frames: Vec<Frame>,
1167 error: Option<String>,
1168 exc: Option<Value>,
1169 signal: Option<Signal>,
1170}
1171
1172thread_local! {
1173 /// Id of the generator whose body is currently executing, or `None` at the
1174 /// root. `yield` suspends this generator.
1175 static CUR_GEN: std::cell::Cell<Option<u32>> = const { std::cell::Cell::new(None) };
1176}
1177
1178thread_local! {
1179 static HOST: RefCell<JsHost> = RefCell::new(JsHost::new());
1180}
1181
1182/// Run `f` with mutable access to the thread-local host.
1183pub fn with_host<R>(f: impl FnOnce(&mut JsHost) -> R) -> R {
1184 HOST.with(|h| f(&mut h.borrow_mut()))
1185}
1186
1187/// Reset the host to a clean slate (fresh module frame).
1188pub fn reset_host() {
1189 with_host(|h| *h = JsHost::new());
1190 // Drop any cached module handles / factory closure — they index the old heap.
1191 crate::module::reset();
1192}
1193
1194impl Default for JsHost {
1195 fn default() -> Self {
1196 Self::new()
1197 }
1198}
1199
1200impl JsHost {
1201 pub fn new() -> JsHost {
1202 let global_env = new_env(None);
1203 let (io_tx, io_rx) = std::sync::mpsc::channel();
1204 let mut h = JsHost {
1205 tdz: None,
1206 tdz_globals: Default::default(),
1207 heap: Vec::new(),
1208 funcs: Vec::new(),
1209 scripts: Vec::new(),
1210 tries: Vec::new(),
1211 globals: VarMap::default(),
1212 global_consts: rustc_hash::FxHashSet::default(),
1213 frames: vec![Frame {
1214 env: global_env.clone(),
1215 base_env: global_env.clone(),
1216 this_obj: None,
1217 new_target: None,
1218 home_class: None,
1219 home_static: false,
1220 home_object: None,
1221 strict: false,
1222 line: 0,
1223 owner: None,
1224 is_module: true,
1225 this_state: ThisState::Plain,
1226 }],
1227 global_env,
1228 error: None,
1229 exc: None,
1230 signal: None,
1231 pending_rejections: Vec::new(),
1232 process_listeners: IndexMap::new(),
1233 null_val: Value::Undef,
1234 protos: HashMap::new(),
1235 null_proto_objs: HashSet::new(),
1236 fn_props: HashMap::new(),
1237 accessors: HashMap::new(),
1238 prop_attrs: HashMap::new(),
1239 non_extensible: HashSet::new(),
1240 private_methods: HashSet::new(),
1241 array_holes: HashMap::new(),
1242 super_replacement: None,
1243 derived_ctor_next: false,
1244 module_scope: false,
1245 builtin_statics: HashMap::new(),
1246 object_proto: Value::Undef,
1247 proto_class: HashMap::new(),
1248 class_registry: HashMap::new(),
1249 error_protos: HashMap::new(),
1250 template_objects: HashMap::new(),
1251 native_protos: HashMap::new(),
1252 symbol_registry: HashMap::new(),
1253 next_symbol: 1,
1254 symbols_by_id: HashMap::new(),
1255 well_known_ids: HashMap::new(),
1256 generators: Vec::new(),
1257 promises: Vec::new(),
1258 microtasks: std::collections::VecDeque::new(),
1259 draining_micro: false,
1260 nextticks: std::collections::VecDeque::new(),
1261 macrotasks: Vec::new(),
1262 next_timer: 1,
1263 io_tx,
1264 io_rx: Some(io_rx),
1265 open_handles: 0,
1266 capture: None,
1267 exit_code: None,
1268 exiting: false,
1269 global_obj: Value::Undef,
1270 };
1271 h.null_val = h.alloc(JsObj::Null);
1272 // `Object.prototype`: the chain root, its own `[[Prototype]]` is null.
1273 h.object_proto = h.new_object(IndexMap::new());
1274 h.global_obj = h.new_object(IndexMap::new());
1275 h
1276 }
1277
1278 /// Whether `v` IS the one `globalThis` object (not merely an object).
1279 pub fn is_global_object(&self, v: &Value) -> bool {
1280 !matches!(self.global_obj, Value::Undef) && self.global_obj == *v
1281 }
1282
1283 /// The `globalThis` object — one per host, so its identity and its
1284 /// properties both survive across reads.
1285 pub fn global_object(&mut self) -> Value {
1286 if matches!(self.global_obj, Value::Undef) {
1287 self.global_obj = self.new_object(IndexMap::new());
1288 }
1289 self.global_obj.clone()
1290 }
1291
1292 // ── prototype chain ──────────────────────────────────────────────────
1293 /// The `[[Prototype]]` of a heap value, if explicitly linked.
1294 pub fn proto_of(&self, v: &Value) -> Option<Value> {
1295 if let Value::Obj(i) = v {
1296 self.protos.get(i).cloned()
1297 } else {
1298 None
1299 }
1300 }
1301 /// Set `v`'s `[[Prototype]]` to `proto`. Null links the object as an explicit
1302 /// null-prototype object (recorded so `instanceof Object` reads false);
1303 /// undefined just clears any link without the null marker.
1304 pub fn set_proto(&mut self, v: &Value, proto: Value) {
1305 if let Value::Obj(i) = v {
1306 if self.is_null(&proto) {
1307 self.protos.remove(i);
1308 self.null_proto_objs.insert(*i);
1309 } else if matches!(proto, Value::Undef) {
1310 self.protos.remove(i);
1311 } else {
1312 self.protos.insert(*i, proto);
1313 self.null_proto_objs.remove(i);
1314 }
1315 }
1316 }
1317 /// Whether `v`'s `[[Prototype]]` was explicitly set to null.
1318 pub fn has_null_proto(&self, v: &Value) -> bool {
1319 matches!(v, Value::Obj(i) if self.null_proto_objs.contains(i))
1320 }
1321 /// Whether `util.inspect` renders `v` with the `[Object: null prototype]`
1322 /// tag. That is a question about the object's ACTUAL `[[Prototype]]`, which
1323 /// for `Object.prototype` is null even though nothing ever set it so: it is
1324 /// the chain root and was never passed through `set_proto`, so the
1325 /// explicitly-nulled registry does not hold it and `console.log(Object
1326 /// .prototype)` printed a bare `{}` where node prints the tag.
1327 ///
1328 /// Kept apart from [`Self::has_null_proto`], which nine other call sites ask
1329 /// about whether Object.prototype's own methods and `__proto__` accessor are
1330 /// INHERITED. `Object.prototype` inherits nothing and still owns all of them.
1331 pub fn inspects_null_proto(&self, v: &Value) -> bool {
1332 self.has_null_proto(v) || *v == self.object_proto
1333 }
1334 pub fn object_proto(&self) -> Value {
1335 self.object_proto.clone()
1336 }
1337 /// Record that the prototype object `proto` belongs to the class constructor
1338 /// `class_val` (so instances can recover their constructor).
1339 pub fn tag_proto_class(&mut self, proto: &Value, class_val: Value) {
1340 if let Value::Obj(i) = proto {
1341 self.proto_class.insert(*i, class_val);
1342 }
1343 }
1344 /// The class whose `prototype` object IS `v`, if `v` is one.
1345 pub fn class_owning_proto(&self, v: &Value) -> Option<Value> {
1346 match v {
1347 Value::Obj(i) => self.proto_class.get(i).cloned(),
1348 _ => None,
1349 }
1350 }
1351 /// The class constructor value nearest in `obj`'s prototype chain, if any.
1352 pub fn class_of(&self, obj: &Value) -> Option<Value> {
1353 let mut cur = self.proto_of(obj);
1354 while let Some(p) = cur {
1355 if let Value::Obj(i) = &p {
1356 if let Some(c) = self.proto_class.get(i) {
1357 return Some(c.clone());
1358 }
1359 }
1360 cur = self.proto_of(&p);
1361 }
1362 None
1363 }
1364 /// The constructor display name of `obj` for `util.inspect` (empty ⇒ plain
1365 /// object, no prefix).
1366 pub fn ctor_name(&self, obj: &Value) -> String {
1367 if let Some(c) = self.class_of(obj) {
1368 if let Some(JsObj::Class(cv)) = self.get(&c) {
1369 return cv.name.clone();
1370 }
1371 }
1372 // A `function F(){}` constructor is not a `class`, so it has no
1373 // `proto_class` entry. V8's `getConstructorName` walks the prototype
1374 // chain for an own `constructor` that is a named function — which is
1375 // what makes `console.log(new F())` print `F { y: 2 }`.
1376 let mut cur = self.proto_of(obj);
1377 while let Some(p) = cur {
1378 let ctor = match self.get(&p) {
1379 Some(JsObj::Object(props)) => props.get("constructor").cloned(),
1380 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => self.fn_prop(&p, "constructor"),
1381 _ => None,
1382 };
1383 if let Some(f) = ctor {
1384 let n = self.callable_name(&f);
1385 if !n.is_empty() {
1386 return n;
1387 }
1388 }
1389 cur = self.proto_of(&p);
1390 }
1391 String::new()
1392 }
1393
1394 /// Whether a callable owns a `prototype` property. `MakeConstructor`
1395 /// (10.2.5) runs for an ordinary function definition and for every
1396 /// generator; an arrow, a `MethodDefinition`, an async function and a bound
1397 /// function are not constructors and own none.
1398 pub fn owns_prototype(&self, v: &Value) -> bool {
1399 match self.get(v) {
1400 Some(JsObj::Class(_)) => true,
1401 Some(JsObj::Func(f)) => match self.funcs.get(f.def_id) {
1402 Some(d) => d.is_generator || !(d.is_arrow || d.is_async || d.is_method),
1403 None => false,
1404 },
1405 _ => false,
1406 }
1407 }
1408
1409 /// A function's own-property table (created on demand).
1410 pub fn fn_prop(&self, v: &Value, name: &str) -> Option<Value> {
1411 if let Value::Obj(i) = v {
1412 self.fn_props.get(i).and_then(|m| m.get(name).cloned())
1413 } else {
1414 None
1415 }
1416 }
1417
1418 /// A class static member, inherited down the constructor chain: a subclass
1419 /// sees its superclass's `static` methods/fields (`Sub.create` → `Base.create`).
1420 pub fn class_static(&self, class_val: &Value, name: &str) -> Option<Value> {
1421 let mut cur = class_val.clone();
1422 loop {
1423 if let Some(v) = self.fn_prop(&cur, name) {
1424 return Some(v);
1425 }
1426 match self.get(&cur) {
1427 Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1428 _ => return None,
1429 }
1430 }
1431 }
1432
1433 /// The first `extends` ancestor that is NOT a user class — the builtin
1434 /// constructor a class chain bottoms out in (`class D extends Array {}` →
1435 /// the `Array` builtin), or `None` for a chain of user classes only.
1436 ///
1437 /// `class_static` walks `ClassVal.parent` and gives up the moment the parent
1438 /// stops being a `Class`, so a static declared by the BUILTIN half of the
1439 /// chain was unreachable: `D.from` read `undefined` where node inherits
1440 /// `Array.from`. Returning the ancestor lets the caller finish the lookup
1441 /// with an ordinary property read, which is what reaches a builtin's
1442 /// statics.
1443 pub fn class_builtin_ancestor(&self, class_val: &Value) -> Option<Value> {
1444 let mut cur = class_val.clone();
1445 loop {
1446 match self.get(&cur) {
1447 Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1448 _ => return Some(cur),
1449 }
1450 }
1451 }
1452 pub fn set_fn_prop(&mut self, v: &Value, name: &str, val: Value) {
1453 if let Value::Obj(i) = v {
1454 self.fn_props
1455 .entry(*i)
1456 .or_default()
1457 .insert(name.to_string(), val);
1458 }
1459 // `name` and `prototype` are own properties of every function/class, but
1460 // never enumerable ones (SetFunctionName 10.2.9, MakeConstructor
1461 // 10.2.5), so `Object.keys(fn)` and `for (k in fn)` report only what a
1462 // script assigned. An ARRAY receiver reaching the same side table has no
1463 // such exotic keys — `arr.name = 'x'` is an ordinary enumerable property.
1464 if !matches!(self.kind_of(v), Some(ObjKind::Func) | Some(ObjKind::Class)) {
1465 return;
1466 }
1467 let attrs = match name {
1468 "name" => PropAttrs {
1469 writable: false,
1470 enumerable: false,
1471 configurable: true,
1472 },
1473 "prototype" => PropAttrs {
1474 writable: true,
1475 enumerable: false,
1476 configurable: false,
1477 },
1478 _ => return,
1479 };
1480 self.set_prop_attrs(v, name, attrs);
1481 }
1482 /// A user-assigned static on a builtin namespace (`Error.prepareStackTrace`).
1483 pub fn builtin_static(&self, ns: &str, name: &str) -> Option<Value> {
1484 self.builtin_statics
1485 .get(ns)
1486 .and_then(|m| m.get(name).cloned())
1487 }
1488 /// Assign a static on a builtin namespace (persists across fresh `Builtin`
1489 /// handles for the same namespace).
1490 pub fn set_builtin_static(&mut self, ns: &str, name: &str, val: Value) {
1491 self.builtin_statics
1492 .entry(ns.to_string())
1493 .or_default()
1494 .insert(name.to_string(), val);
1495 }
1496 /// `delete <ns>.<name>` for a script-assigned static. Reports whether the
1497 /// key was there — without this, `delete Array.prototype.patch` answered
1498 /// true and left the entry in place, so the patch outlived its own removal.
1499 pub fn remove_builtin_static(&mut self, ns: &str, name: &str) -> bool {
1500 self.builtin_statics
1501 .get_mut(ns)
1502 .is_some_and(|m| m.shift_remove(name).is_some())
1503 }
1504 /// Every namespace a script has assigned a static onto, with that
1505 /// namespace's assigned keys — the source of the user-added half of
1506 /// `Object.getOwnPropertyNames(Array.prototype)`.
1507 pub fn builtin_static_keys(&self, ns: &str) -> Vec<String> {
1508 self.builtin_statics
1509 .get(ns)
1510 .map(|m| m.keys().cloned().collect())
1511 .unwrap_or_default()
1512 }
1513 /// Drop an own property from the side table (`delete arr.foo`,
1514 /// `delete fn.tag`). Reports whether the key was there.
1515 pub fn remove_fn_prop(&mut self, v: &Value, name: &str) -> bool {
1516 match v {
1517 Value::Obj(i) => self
1518 .fn_props
1519 .get_mut(i)
1520 .map(|m| m.shift_remove(name).is_some())
1521 .unwrap_or(false),
1522 _ => false,
1523 }
1524 }
1525 pub fn fn_prop_keys(&self, v: &Value) -> Vec<String> {
1526 if let Value::Obj(i) = v {
1527 self.fn_props
1528 .get(i)
1529 .map(|m| m.keys().cloned().collect())
1530 .unwrap_or_default()
1531 } else {
1532 Vec::new()
1533 }
1534 }
1535
1536 /// Install an accessor `(get, set)` for `key` on the object `owner`.
1537 pub fn set_accessor(
1538 &mut self,
1539 owner: &Value,
1540 key: &str,
1541 get: Option<Value>,
1542 set: Option<Value>,
1543 ) {
1544 if let Value::Obj(i) = owner {
1545 // Accessors live in their own table, but JS reports own keys in a
1546 // single insertion order across data AND accessor properties. Drop an
1547 // ordering marker into the property map so
1548 // `{ a: 1, get b() {}, c: 3 }` enumerates a, b, c — not a, c, b.
1549 // The marker is `@@`-prefixed, so it is invisible to every reader.
1550 let marker = format!("{ORD_MARKER}{key}");
1551 match self.get_mut(owner) {
1552 Some(JsObj::Object(props)) => {
1553 if !props.contains_key(key) && !props.contains_key(&marker) {
1554 props.insert(marker, Value::Undef);
1555 }
1556 }
1557 // A function or class keeps its own properties in the fn-prop
1558 // side table, so its ordering marker belongs there. Without it a
1559 // static accessor enumerated AFTER every static field and method
1560 // regardless of where the class body declared it: node reports
1561 // `class A { static s = 2; static get sv(){} static m(){} }` as
1562 // `['sv', 'm', 's']` — the methods and accessors in source order
1563 // first, then the fields — and this reported `['m', 's', 'sv']`.
1564 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1565 let table = self.fn_props.entry(*i).or_default();
1566 if !table.contains_key(key) && !table.contains_key(&marker) {
1567 table.insert(marker, Value::Undef);
1568 }
1569 }
1570 _ => {}
1571 }
1572 let slot = self
1573 .accessors
1574 .entry(*i)
1575 .or_default()
1576 .entry(key.to_string())
1577 .or_insert((None, None));
1578 if get.is_some() {
1579 slot.0 = get;
1580 }
1581 if set.is_some() {
1582 slot.1 = set;
1583 }
1584 }
1585 }
1586 /// The accessor `(get, set)` for `key` directly on `owner` (no chain walk).
1587 /// Drop an own accessor property entirely, marker and all.
1588 ///
1589 /// `delete obj.accessorProp` used to clear only the property map, and an
1590 /// accessor does not live there — so the delete reported success while the
1591 /// getter kept answering and `in` kept reporting the key.
1592 pub fn remove_accessor(&mut self, owner: &Value, key: &str) {
1593 if let Value::Obj(i) = owner {
1594 if let Some(m) = self.accessors.get_mut(i) {
1595 m.shift_remove(key);
1596 }
1597 }
1598 let marker = format!("{ORD_MARKER}{key}");
1599 match self.get_mut(owner) {
1600 Some(JsObj::Object(props)) => {
1601 props.shift_remove(&marker);
1602 }
1603 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1604 if let Value::Obj(i) = owner {
1605 if let Some(t) = self.fn_props.get_mut(i) {
1606 t.shift_remove(&marker);
1607 }
1608 }
1609 }
1610 _ => {}
1611 }
1612 }
1613
1614 /// Turn an own accessor property into a data property carrying `value`,
1615 /// keeping its place in the own-key order.
1616 ///
1617 /// `set_accessor` records that order with an `@@ord:` marker in the
1618 /// property map rather than a real key, so deleting the accessor and
1619 /// inserting the value would append the key at the end instead. Node
1620 /// reports `{ a: 1, get b() {}, c: 3 }` redefined through
1621 /// `Object.defineProperty(o, 'b', { value })` as `a, b, c`.
1622 pub fn accessor_to_data(&mut self, owner: &Value, key: &str, value: Value) {
1623 if let Value::Obj(i) = owner {
1624 if let Some(m) = self.accessors.get_mut(i) {
1625 m.shift_remove(key);
1626 }
1627 }
1628 let marker = format!("{ORD_MARKER}{key}");
1629 let swap = |map: &mut IndexMap<String, Value>| match map.get_index_of(&marker) {
1630 Some(pos) => {
1631 *map = map
1632 .iter()
1633 .enumerate()
1634 .map(|(n, (k, v))| {
1635 if n == pos {
1636 (key.to_string(), value.clone())
1637 } else {
1638 (k.clone(), v.clone())
1639 }
1640 })
1641 .collect();
1642 }
1643 None => {
1644 map.insert(key.to_string(), value.clone());
1645 }
1646 };
1647 let fn_table = matches!(
1648 self.get(owner),
1649 Some(JsObj::Func(_)) | Some(JsObj::Class(_))
1650 );
1651 if fn_table {
1652 if let Value::Obj(i) = owner {
1653 swap(self.fn_props.entry(*i).or_default());
1654 }
1655 } else if let Some(JsObj::Object(props)) = self.get_mut(owner) {
1656 swap(props);
1657 }
1658 }
1659
1660 /// Move the per-heap-index bookkeeping of `src` onto `dst`.
1661 ///
1662 /// Used when one object becomes another in place (a class extending a
1663 /// builtin exotic). The prototype link is deliberately NOT moved: `dst`
1664 /// already points at the leaf class's prototype, which is the one its
1665 /// methods must resolve through.
1666 pub fn move_index_state(&mut self, src: u32, dst: u32) {
1667 if let Some(holes) = self.array_holes.remove(&src) {
1668 self.array_holes.insert(dst, holes);
1669 }
1670 if let Some(attrs) = self.prop_attrs.remove(&src) {
1671 self.prop_attrs.entry(dst).or_default().extend(attrs);
1672 }
1673 if let Some(props) = self.fn_props.remove(&src) {
1674 self.fn_props.entry(dst).or_default().extend(props);
1675 }
1676 if let Some(acc) = self.accessors.remove(&src) {
1677 self.accessors.entry(dst).or_default().extend(acc);
1678 }
1679 }
1680
1681 pub fn own_accessor(&self, owner: &Value, key: &str) -> Option<(Option<Value>, Option<Value>)> {
1682 if let Value::Obj(i) = owner {
1683 self.accessors.get(i).and_then(|m| m.get(key).cloned())
1684 } else {
1685 None
1686 }
1687 }
1688
1689 /// The own accessor-property keys of `owner`, in installation order.
1690 pub fn own_accessor_keys(&self, owner: &Value) -> Vec<String> {
1691 match owner {
1692 Value::Obj(i) => self
1693 .accessors
1694 .get(i)
1695 .map(|m| m.keys().cloned().collect())
1696 .unwrap_or_default(),
1697 _ => Vec::new(),
1698 }
1699 }
1700
1701 // ── own-property attributes ──────────────────────────────────────────
1702
1703 /// Record non-default attributes for `owner[key]`. Storing the default shape
1704 /// clears the entry so the table only ever holds deviations.
1705 pub fn set_prop_attrs(&mut self, owner: &Value, key: &str, attrs: PropAttrs) {
1706 if let Value::Obj(i) = owner {
1707 if attrs == PropAttrs::default() {
1708 if let Some(m) = self.prop_attrs.get_mut(i) {
1709 m.shift_remove(key);
1710 }
1711 } else {
1712 self.prop_attrs
1713 .entry(*i)
1714 .or_default()
1715 .insert(key.to_string(), attrs);
1716 }
1717 }
1718 }
1719
1720 /// Copy every recorded property attribute from `from` to `to`. A pass that
1721 /// rebuilds an object (`JSON.stringify`'s `toJSON` walk) must carry them
1722 /// across or the copy silently re-exposes non-enumerable slots.
1723 pub fn copy_prop_attrs(&mut self, from: &Value, to: &Value) {
1724 if let (Value::Obj(f), Value::Obj(_)) = (from, to) {
1725 if let Some(m) = self.prop_attrs.get(f).cloned() {
1726 for (k, a) in m {
1727 self.set_prop_attrs(to, &k, a);
1728 }
1729 }
1730 }
1731 }
1732
1733 /// The attributes of own property `owner[key]` (all-true when unrecorded).
1734 pub fn prop_attrs(&self, owner: &Value, key: &str) -> PropAttrs {
1735 // An array's `length` is the array exotic's own property (10.4.2):
1736 // never enumerated and never configurable, and writable until
1737 // `Object.freeze` clears that — which is what stops a `push` from
1738 // extending a frozen array. Reporting it unconditionally writable made
1739 // `Object.isFrozen(Object.freeze([]))` false once the elements started
1740 // being sealed, because `length` was then the one key that never
1741 // followed.
1742 if key == "length" && matches!(self.get(owner), Some(JsObj::Array(_))) {
1743 let writable = match owner {
1744 Value::Obj(i) => self
1745 .prop_attrs
1746 .get(i)
1747 .and_then(|m| m.get(key))
1748 .map(|a| a.writable)
1749 .unwrap_or(true),
1750 _ => true,
1751 };
1752 return PropAttrs {
1753 writable,
1754 enumerable: false,
1755 // An ARGUMENTS object's `length` is an ordinary data property
1756 // (10.4.4.6), so it is configurable where a real array's is
1757 // not. The two share a backing representation here, so the
1758 // exotic's attributes have to be told apart explicitly.
1759 configurable: crate::builtins::is_arguments_h(self, owner),
1760 };
1761 }
1762 match owner {
1763 Value::Obj(i) => self
1764 .prop_attrs
1765 .get(i)
1766 .and_then(|m| m.get(key))
1767 .copied()
1768 .unwrap_or_default(),
1769 _ => PropAttrs::default(),
1770 }
1771 }
1772
1773 /// Whether own property `owner[key]` shows up in `for-in`/`Object.keys`.
1774 /// Internal slots (`@@…`) and private class fields (`#…`) never do.
1775 pub fn is_enumerable(&self, owner: &Value, key: &str) -> bool {
1776 !key.starts_with("@@") && !key.starts_with('#') && self.prop_attrs(owner, key).enumerable
1777 }
1778
1779 /// Mark `owner[key]` non-enumerable, leaving it writable/configurable — the
1780 /// shape of every V8 "hidden but real" own property.
1781 pub fn hide_prop(&mut self, owner: &Value, key: &str) {
1782 self.set_prop_attrs(owner, key, PropAttrs::HIDDEN);
1783 }
1784
1785 /// Whether a plain `owner[key] = v` assignment is allowed to land. A
1786 /// non-writable data property silently ignores the write in sloppy mode,
1787 /// which is the mode every script here runs in; so does adding a *new* key to
1788 /// a non-extensible object.
1789 pub fn can_write_prop(&self, owner: &Value, key: &str) -> bool {
1790 if !self.prop_attrs(owner, key).writable {
1791 return false;
1792 }
1793 // An intrinsic prototype on the chain may define the name NON-WRITABLE,
1794 // and those members own no map entry for the walk below to find:
1795 // `o[Symbol.toStringTag] = 'x'` where `o` inherits from `Map.prototype`
1796 // is refused in node and was creating an own property here, which then
1797 // changed the object's brand.
1798 // The receiver's OWN kind counts too, not only the prototypes an
1799 // explicit link reaches: a plain array inherits `Array.prototype`
1800 // implicitly, with no link for the walk to follow, and
1801 // `a[Symbol.unscopables] = 'x'` is refused there just the same.
1802 //
1803 // Restricted to SYMBOL-keyed members. The string-keyed non-writable
1804 // ones — `Function.prototype.length`/`name`, `String.prototype.length`
1805 // — are also OWN properties of every instance, so the inherited rule
1806 // never decides them; applying it anyway blocked `SetFunctionName`
1807 // itself, and naming the setter in `Object.defineProperty(o, 'v', {set
1808 // (x) {…}})` then threw.
1809 if key.starts_with("@@")
1810 && crate::builtins::own_ctor_name(self, owner)
1811 .into_iter()
1812 .chain(crate::builtins::chain_intrinsic_ctors_h(self, owner))
1813 .any(|c| crate::builtins::is_proto_readonly(c, key))
1814 {
1815 return false;
1816 }
1817 // 10.1.9.2: with no OWN property, the inherited one decides. A
1818 // non-writable data property up the chain blocks the write rather than
1819 // being shadowed — including one on a frozen prototype. Only own
1820 // attributes were consulted, so `Object.create(frozenBase).f = 2`
1821 // quietly created an own property node refuses to create.
1822 //
1823 // An inherited ACCESSOR does not block: its setter runs, and the write
1824 // path checks for one before reaching here.
1825 let has_own = match self.get(owner) {
1826 Some(JsObj::Object(p)) => p.contains_key(key),
1827 _ => true,
1828 };
1829 if !has_own {
1830 let mut cur = self.proto_of(owner);
1831 while let Some(proto) = cur {
1832 if self.own_accessor(&proto, key).is_some() {
1833 break;
1834 }
1835 let present =
1836 matches!(self.get(&proto), Some(JsObj::Object(p)) if p.contains_key(key));
1837 if present {
1838 if !self.prop_attrs(&proto, key).writable {
1839 return false;
1840 }
1841 break;
1842 }
1843 cur = self.proto_of(&proto);
1844 }
1845 }
1846 if self.is_extensible(owner) {
1847 return true;
1848 }
1849 // A non-extensible object refuses a NEW key. Only the plain-object arm
1850 // could name its own keys, so every other shape answered "own" for any
1851 // key at all: `Object.freeze(arr).extra = 1` landed, and so did a write
1852 // to the frozen template object a tagged template hands its tag.
1853 match self.get(owner) {
1854 Some(JsObj::Object(p)) => p.contains_key(key),
1855 Some(JsObj::Array(items)) => {
1856 key == "length"
1857 || key
1858 .parse::<usize>()
1859 .map(|i| i < items.len())
1860 .unwrap_or(false)
1861 || self.fn_prop(owner, key).is_some()
1862 }
1863 // A RegExp's `lastIndex` is an own property, so a merely
1864 // NON-EXTENSIBLE regexp still accepts a write to it.
1865 Some(JsObj::RegExp(_)) => key == "lastIndex" || self.fn_prop(owner, key).is_some(),
1866 // Every other shape keeps its own properties in the fn-prop side
1867 // table (a function's statics, a Map's assigned properties), so
1868 // "does it already own this key" is that table's question. Answering
1869 // a blanket `true` let a NEW key land on a frozen function and a
1870 // frozen Map.
1871 _ => self.fn_prop(owner, key).is_some(),
1872 }
1873 }
1874
1875 /// Mark `v` closed to new properties (`Object.preventExtensions`).
1876 pub fn prevent_extensions(&mut self, v: &Value) {
1877 if let Value::Obj(i) = v {
1878 self.non_extensible.insert(*i);
1879 }
1880 }
1881
1882 pub fn is_extensible(&self, v: &Value) -> bool {
1883 !matches!(v, Value::Obj(i) if self.non_extensible.contains(i))
1884 }
1885
1886 /// Apply `Object.seal` (`freeze == false`) or `Object.freeze` (`true`): close
1887 /// the object and strip `configurable` — and, when freezing, `writable` —
1888 /// from every own property, data and accessor alike.
1889 pub fn seal_object(&mut self, v: &Value, freeze: bool) {
1890 self.prevent_extensions(v);
1891 let mut keys = self.integrity_keys(v);
1892 keys.extend(self.own_accessor_keys(v));
1893 for k in keys {
1894 let mut a = self.prop_attrs(v, &k);
1895 a.configurable = false;
1896 if freeze {
1897 a.writable = false;
1898 }
1899 self.set_prop_attrs(v, &k, a);
1900 }
1901 }
1902
1903 /// The own DATA-property keys SetIntegrityLevel (7.3.15) walks.
1904 ///
1905 /// An array's elements are own properties too, and only the `Object` arm was
1906 /// walked — so `Object.freeze([1, 2])` sealed nothing: `a[0] = 9` wrote
1907 /// through, and the elements still reported `writable: true,
1908 /// configurable: true` while `Object.isFrozen` answered true over an empty
1909 /// key list. `length` is an own property as well, and freezing it is what
1910 /// stops a `push` from extending a frozen array.
1911 fn integrity_keys(&self, v: &Value) -> Vec<String> {
1912 let side_table_keys = |v: &Value| -> Vec<String> {
1913 match v {
1914 Value::Obj(i) => self
1915 .fn_props
1916 .get(i)
1917 .map(|m| m.keys().cloned().collect())
1918 .unwrap_or_default(),
1919 _ => Vec::new(),
1920 }
1921 };
1922 match self.get(v) {
1923 Some(JsObj::Object(p)) => p.keys().cloned().collect(),
1924 // A RegExp's only own property is its `lastIndex` cursor, which
1925 // lives in the `RegExpObj` struct. Without it here `Object.freeze`
1926 // sealed nothing and a frozen regexp's cursor still moved.
1927 Some(JsObj::RegExp(_)) => vec!["lastIndex".to_string()],
1928 // A function's statics and a Map's assigned properties live in the
1929 // fn-prop side table, and freezing has to reach them too.
1930 Some(JsObj::Func(_))
1931 | Some(JsObj::Class(_))
1932 | Some(JsObj::Map { .. })
1933 | Some(JsObj::Set { .. })
1934 | Some(JsObj::Promise { .. }) => side_table_keys(v),
1935 Some(JsObj::Array(items)) => (0..items.len())
1936 .map(|i| i.to_string())
1937 .chain(std::iter::once("length".to_string()))
1938 // A named property stuck on an array (`a.tag = 't'`, a match
1939 // array's `.index`/`.groups`) is an own property too, and
1940 // freezing has to reach it.
1941 .chain(match v {
1942 Value::Obj(i) => self
1943 .fn_props
1944 .get(i)
1945 .map(|m| m.keys().cloned().collect::<Vec<_>>())
1946 .unwrap_or_default(),
1947 _ => Vec::new(),
1948 })
1949 .collect(),
1950 _ => Vec::new(),
1951 }
1952 }
1953
1954 /// `Object.isSealed` (`freeze == false`) / `Object.isFrozen` (`true`).
1955 pub fn is_sealed(&self, v: &Value, freeze: bool) -> bool {
1956 if self.is_extensible(v) {
1957 return false;
1958 }
1959 let mut keys = self.integrity_keys(v);
1960 keys.extend(self.own_accessor_keys(v));
1961 keys.iter().all(|k| {
1962 let a = self.prop_attrs(v, k);
1963 !a.configurable && (!freeze || !a.writable)
1964 })
1965 }
1966
1967 /// A fresh unique `Symbol(desc)` value.
1968 pub fn new_symbol(&mut self, desc: Option<String>) -> Value {
1969 let id = self.next_symbol;
1970 self.next_symbol += 1;
1971 let v = self.alloc(JsObj::Symbol { desc, id });
1972 self.symbols_by_id.insert(id, v.clone());
1973 v
1974 }
1975
1976 /// The symbol VALUE an internal symbol property key (`@@sym:<id>` or a
1977 /// well-known `@@iterator`) came from.
1978 pub fn symbol_of_key(&self, k: &str) -> Option<Value> {
1979 if let Some(id) = k.strip_prefix("@@sym:").and_then(|i| i.parse::<u64>().ok()) {
1980 return self.symbols_by_id.get(&id).cloned();
1981 }
1982 let name = k.strip_prefix("@@")?;
1983 WELL_KNOWN_SYMBOLS
1984 .contains(&name)
1985 .then(|| {
1986 self.symbol_registry
1987 .get(&format!("@@Symbol.{name}"))
1988 .cloned()
1989 })
1990 .flatten()
1991 }
1992
1993 /// The own symbol-keyed property keys of `v` as SYMBOL values —
1994 /// `Object.getOwnPropertySymbols` / the symbol half of `Reflect.ownKeys`.
1995 pub fn own_symbol_keys(&self, v: &Value) -> Vec<Value> {
1996 let keys: Vec<String> = match self.get(v) {
1997 Some(JsObj::Object(p)) => p.keys().cloned().collect(),
1998 // An Array/Function receiver has no property map: its non-index own
1999 // properties — symbol-keyed ones included — live in the fn-prop side
2000 // table, and are just as much own properties as an object's.
2001 Some(_) => self.fn_prop_keys(v),
2002 None => return Vec::new(),
2003 };
2004 keys.iter().filter_map(|k| self.symbol_of_key(k)).collect()
2005 }
2006
2007 /// The own SYMBOL-keyed enumerable `(internal key, value)` pairs of `v` —
2008 /// what `CopyDataProperties` (object spread, `Object.assign`) copies
2009 /// alongside the string keys, and what `Object.keys` / `for-in` /
2010 /// `JSON.stringify` deliberately skip.
2011 pub fn own_symbol_entries(&self, v: &Value) -> Vec<(String, Value)> {
2012 match self.get(v) {
2013 Some(JsObj::Object(p)) => p
2014 .iter()
2015 .filter(|(k, _)| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2016 .map(|(k, val)| (k.clone(), val.clone()))
2017 .collect(),
2018 // Array/Function: the side table (see `own_symbol_keys`).
2019 Some(_) => self
2020 .fn_prop_keys(v)
2021 .into_iter()
2022 .filter(|k| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2023 .map(|k| {
2024 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
2025 (k, val)
2026 })
2027 .collect(),
2028 None => Vec::new(),
2029 }
2030 }
2031 /// The shared `Symbol.for(key)` value (interned by description).
2032 pub fn symbol_for(&mut self, key: &str) -> Value {
2033 if let Some(v) = self.symbol_registry.get(key) {
2034 return v.clone();
2035 }
2036 let s = self.new_symbol(Some(key.to_string()));
2037 self.symbol_registry.insert(key.to_string(), s.clone());
2038 s
2039 }
2040 /// `Symbol.keyFor(sym)`: the registry key `Symbol.for` interned `sym` under,
2041 /// or `undefined` for a symbol that is not in the registry at all.
2042 ///
2043 /// Matched by symbol IDENTITY, not by description — `Symbol.for('k')` and
2044 /// `Symbol('k')` share a description and only the first is registered. The
2045 /// `@@Symbol.*` well-known entries are registry-internal and never a
2046 /// `keyFor` answer, matching node: `Symbol.keyFor(Symbol.iterator)` is
2047 /// `undefined` there.
2048 pub fn symbol_registry_key(&mut self, sym: &Value) -> Value {
2049 let Some(key) = self
2050 .symbol_registry
2051 .iter()
2052 .find(|(k, v)| self.strict_eq(v, sym) && !k.starts_with("@@Symbol."))
2053 .map(|(k, _)| k.clone())
2054 else {
2055 return Value::Undef;
2056 };
2057 self.new_str(key)
2058 }
2059 /// The well-known `Symbol.iterator` (a fixed shared symbol whose internal
2060 /// property key is `@@iterator`).
2061 pub fn well_known_iterator(&mut self) -> Value {
2062 self.symbol_for("@@Symbol.iterator")
2063 }
2064 /// The well-known `Symbol.asyncIterator` (internal key `@@asyncIterator`).
2065 pub fn well_known_async_iterator(&mut self) -> Value {
2066 self.symbol_for("@@Symbol.asyncIterator")
2067 }
2068 /// A well-known symbol by its ECMAScript name (`toPrimitive`,
2069 /// `toStringTag`, …). Its internal property key is `@@<name>` — see
2070 /// [`WELL_KNOWN_SYMBOLS`] and `property_key`.
2071 ///
2072 /// Its DESCRIPTION is `Symbol.<name>`, so `String(Symbol.iterator)` prints
2073 /// `Symbol(Symbol.iterator)` as V8 does, while the registry key keeps the
2074 /// `@@` prefix — `Symbol.for('Symbol.iterator')` therefore stays a
2075 /// different symbol, and identification is by id, so a user-made
2076 /// `Symbol('Symbol.iterator')` is not mistaken for the well-known one.
2077 pub fn well_known_symbol(&mut self, name: &str) -> Value {
2078 let key = format!("@@Symbol.{name}");
2079 if let Some(v) = self.symbol_registry.get(&key) {
2080 return v.clone();
2081 }
2082 let s = self.new_symbol(Some(format!("Symbol.{name}")));
2083 if let Some(JsObj::Symbol { id, .. }) = self.get(&s) {
2084 self.well_known_ids.insert(*id, name.to_string());
2085 }
2086 self.symbol_registry.insert(key, s.clone());
2087 s
2088 }
2089 /// The internal property-key string for a value used as a key. A `Symbol`
2090 /// maps to a stable per-symbol string so symbol-keyed props round-trip;
2091 /// `Symbol.iterator` maps to the sentinel `@@iterator`.
2092 pub fn property_key(&self, v: &Value) -> String {
2093 if let Some(JsObj::Symbol { id, .. }) = self.get(v) {
2094 if let Some(n) = self.well_known_ids.get(id) {
2095 return format!("@@{n}");
2096 }
2097 return format!("@@sym:{id}");
2098 }
2099 self.str_of(v)
2100 }
2101
2102 pub fn null(&self) -> Value {
2103 self.null_val.clone()
2104 }
2105 pub fn is_null(&self, v: &Value) -> bool {
2106 matches!(self.get(v), Some(JsObj::Null))
2107 }
2108
2109 // ── program loading ──────────────────────────────────────────────────
2110 pub fn program_offsets(&self) -> (usize, usize) {
2111 (self.funcs.len(), self.tries.len())
2112 }
2113 pub fn load_program(&mut self, funcs: Vec<FuncDef>, tries: Vec<TryDef>) {
2114 self.funcs.extend(funcs);
2115 self.tries.extend(tries);
2116 }
2117 /// The source text of function `def_id`, when its program kept one.
2118 pub fn func_source(&self, def_id: usize) -> Option<&str> {
2119 let d = self.funcs.get(def_id)?;
2120 let (start, end) = d.span;
2121 if end == 0 {
2122 return None;
2123 }
2124 self.scripts
2125 .get(d.script? as usize)?
2126 .get(start as usize..end as usize)
2127 }
2128 pub fn try_def(&self, id: usize) -> Option<TryDef> {
2129 self.tries.get(id).cloned()
2130 }
2131
2132 /// What `try` statement `id` HAS — `(has handler, catch parameter name, has
2133 /// finalizer)` — without copying its chunks. Running a `try` used to clone
2134 /// the whole `TryDef`, so a `try` inside a loop deep-copied its block, its
2135 /// handler and its finalizer on every iteration just to learn its shape.
2136 pub fn try_shape(&self, id: usize) -> Option<(bool, Option<String>, bool)> {
2137 let t = self.tries.get(id)?;
2138 Some((
2139 t.handler.is_some(),
2140 t.handler.as_ref().and_then(|(bind, _)| bind.clone()),
2141 t.finalizer.is_some(),
2142 ))
2143 }
2144
2145 /// One `try` part's bytecode: 0 = block, 1 = handler body, 2 = finalizer.
2146 /// Reached only when no pooled VM already holds that chunk.
2147 pub fn try_chunk(&self, id: usize, part: u64) -> Option<Chunk> {
2148 let t = self.tries.get(id)?;
2149 match part {
2150 0 => Some(t.block.clone()),
2151 1 => t.handler.as_ref().map(|(_, body)| body.clone()),
2152 _ => t.finalizer.clone(),
2153 }
2154 }
2155
2156 // ── heap allocation / accessors ──────────────────────────────────────
2157 pub fn alloc(&mut self, obj: JsObj) -> Value {
2158 self.heap.push(obj);
2159 Value::Obj((self.heap.len() - 1) as u32)
2160 }
2161 pub fn get(&self, v: &Value) -> Option<&JsObj> {
2162 if let Value::Obj(i) = v {
2163 self.heap.get(*i as usize)
2164 } else {
2165 None
2166 }
2167 }
2168 pub fn get_mut(&mut self, v: &Value) -> Option<&mut JsObj> {
2169 if let Value::Obj(i) = v {
2170 self.heap.get_mut(*i as usize)
2171 } else {
2172 None
2173 }
2174 }
2175 /// Which variant `v` points at, without copying its contents. Use this in
2176 /// place of `get(v).cloned()` whenever only the tag is needed — see
2177 /// [`ObjKind`].
2178 pub fn kind_of(&self, v: &Value) -> Option<ObjKind> {
2179 self.get(v).map(JsObj::kind)
2180 }
2181 pub fn new_str(&mut self, s: impl Into<String>) -> Value {
2182 self.alloc(JsObj::Str(s.into()))
2183 }
2184 pub fn new_array(&mut self, items: Vec<Value>) -> Value {
2185 self.alloc(JsObj::Array(items))
2186 }
2187
2188 /// Record that `name` was declared as a private method or accessor.
2189 pub fn note_private_method(&mut self, name: &str) {
2190 self.private_methods.insert(name.to_string());
2191 }
2192
2193 /// Whether `name` was declared as a private method/accessor by some class,
2194 /// as opposed to a private field.
2195 pub fn is_private_method(&self, name: &str) -> bool {
2196 self.private_methods.contains(name)
2197 }
2198
2199 /// The name of the class whose body the running function belongs to. Only a
2200 /// method of that class can even mention its private names, so this is the
2201 /// class a failed brand check must name.
2202 /// The `super` binding of the frame now running: the owning class name,
2203 /// whether the method is static, and the home object of an object-literal
2204 /// method. An ARROW captures all three at creation, the way it captures
2205 /// `this` — `super` inside an arrow means the enclosing METHOD's `super`.
2206 /// Whether the activation now running is strict code.
2207 /// Whether `v` is a function whose own body is SLOPPY — not an arrow, and
2208 /// with no `'use strict'` of its own or inherited from its script. This is
2209 /// the receiver test the `arguments`/`caller` poison pill keys on: node
2210 /// decides by the FUNCTION, never by the code doing the reading.
2211 pub fn fn_is_sloppy(&self, v: &Value) -> bool {
2212 match self.get(v) {
2213 Some(JsObj::Func(fv)) => {
2214 !fv.is_arrow && !self.funcs.get(fv.def_id).is_some_and(|d| d.strict)
2215 }
2216 _ => false,
2217 }
2218 }
2219 pub fn current_strict(&self) -> bool {
2220 self.frame().strict
2221 }
2222
2223 /// Mark the frame about to run as STRICT — used for a program whose own top
2224 /// level says `'use strict'`, which has no `FuncDef` to carry the flag.
2225 pub fn set_current_strict(&mut self) {
2226 if let Some(f) = self.frames.last_mut() {
2227 f.strict = true;
2228 }
2229 }
2230
2231 pub fn current_home(&self) -> (Option<String>, bool, Option<Value>) {
2232 (
2233 self.current_home_class_name(),
2234 self.frame().home_static,
2235 self.frame().home_object.clone(),
2236 )
2237 }
2238
2239 pub fn current_home_class_name(&self) -> Option<String> {
2240 match self.get(&self.current_home_class()?) {
2241 Some(JsObj::Class(c)) => Some(c.name.clone()),
2242 _ => None,
2243 }
2244 }
2245
2246 /// Whether `recv` — or anything on its prototype chain — carries the private
2247 /// name `key`. A private FIELD is an own property of the instance; a private
2248 /// METHOD lives on the class prototype, one link up.
2249 pub fn has_private(&self, recv: &Value, key: &str) -> bool {
2250 let mut cur = Some(recv.clone());
2251 while let Some(v) = cur {
2252 let owns = match self.get(&v) {
2253 Some(JsObj::Object(p)) => p.contains_key(key),
2254 Some(JsObj::Class(c)) => c.statics.contains_key(key),
2255 _ => false,
2256 };
2257 if owns || self.own_accessor(&v, key).is_some() || self.fn_prop(&v, key).is_some() {
2258 return true;
2259 }
2260 cur = self.proto_of(&v);
2261 }
2262 false
2263 }
2264
2265 // ── array holes ──────────────────────────────────────────────────────
2266 //
2267 // Every read/write of an array's elision set goes through this block. See
2268 // the `array_holes` field for why the marker lives here rather than in
2269 // `Value`.
2270
2271 /// Whether element `i` of array `arr` is an elided element (a "hole"), as
2272 /// opposed to a stored `undefined`. `false` for anything that is not an
2273 /// array, and for every index of a dense one.
2274 pub fn is_hole(&self, arr: &Value, i: usize) -> bool {
2275 match (arr, ()) {
2276 (Value::Obj(idx), ()) => self.array_holes.get(idx).is_some_and(|hs| hs.contains(&i)),
2277 _ => false,
2278 }
2279 }
2280
2281 /// Whether `arr` has any elided element at all — one hash probe, and the
2282 /// guard every hole-aware code path takes before doing anything slower.
2283 pub fn has_holes(&self, arr: &Value) -> bool {
2284 matches!(arr, Value::Obj(i) if self.array_holes.contains_key(i))
2285 }
2286
2287 /// `arr`'s hole positions in ASCENDING order, or an empty vec if dense.
2288 /// Sorted because every consumer (own-key enumeration, `util.inspect`
2289 /// run-grouping) needs index order, and the backing set has none.
2290 pub fn hole_indices(&self, arr: &Value) -> Vec<usize> {
2291 let Value::Obj(i) = arr else {
2292 return Vec::new();
2293 };
2294 let Some(hs) = self.array_holes.get(i) else {
2295 return Vec::new();
2296 };
2297 let mut v: Vec<usize> = hs.iter().copied().collect();
2298 v.sort_unstable();
2299 v
2300 }
2301
2302 /// Record element `i` of `arr` as elided.
2303 pub fn mark_hole(&mut self, arr: &Value, i: usize) {
2304 if let Value::Obj(idx) = arr {
2305 self.array_holes.entry(*idx).or_default().insert(i);
2306 }
2307 }
2308
2309 /// Record `range` of `arr` as elided (a `new Array(n)`, a `length` grow, or
2310 /// the gap a write past the end opens).
2311 pub fn mark_hole_range(&mut self, arr: &Value, range: std::ops::Range<usize>) {
2312 if range.is_empty() {
2313 return;
2314 }
2315 if let Value::Obj(idx) = arr {
2316 self.array_holes.entry(*idx).or_default().extend(range);
2317 }
2318 }
2319
2320 /// Element `i` now holds a real value: it is no longer a hole. Every write
2321 /// to an array index calls this, which is what keeps a stale hole record
2322 /// from outliving the elision it described.
2323 pub fn clear_hole(&mut self, arr: &Value, i: usize) {
2324 let Value::Obj(idx) = arr else { return };
2325 let Some(hs) = self.array_holes.get_mut(idx) else {
2326 return;
2327 };
2328 hs.remove(&i);
2329 if hs.is_empty() {
2330 self.array_holes.remove(idx);
2331 }
2332 }
2333
2334 /// `arr` is dense from here on (`fill` over the whole array, a fresh
2335 /// dense assignment into an existing handle).
2336 pub fn clear_holes(&mut self, arr: &Value) {
2337 if let Value::Obj(idx) = arr {
2338 self.array_holes.remove(idx);
2339 }
2340 }
2341
2342 /// Copy `src`'s elision set onto `dst`, optionally shifting each position by
2343 /// `f`. Used by every method that derives a new array whose holes track the
2344 /// source's (`slice`, `concat`, `map`).
2345 pub fn copy_holes(&mut self, src: &Value, dst: &Value, f: impl Fn(usize) -> Option<usize>) {
2346 if !self.has_holes(src) {
2347 return;
2348 }
2349 let moved: rustc_hash::FxHashSet<usize> =
2350 self.hole_indices(src).into_iter().filter_map(f).collect();
2351 self.install_holes(dst, moved);
2352 }
2353
2354 /// Rewrite `arr`'s own elision set in place: `f(i)` gives the position each
2355 /// existing hole moves to, or `None` if the mutation removed it. This is the
2356 /// one primitive behind every structural array mutation — `shift` is
2357 /// `i.checked_sub(1)`, `unshift(k)` is `i + k`, `reverse` is `len-1-i`, and
2358 /// `splice` is the general case.
2359 pub fn remap_holes(&mut self, arr: &Value, f: impl Fn(usize) -> Option<usize>) {
2360 if !self.has_holes(arr) {
2361 return;
2362 }
2363 let moved: rustc_hash::FxHashSet<usize> =
2364 self.hole_indices(arr).into_iter().filter_map(f).collect();
2365 self.install_holes(arr, moved);
2366 }
2367
2368 /// Replace `arr`'s elision set outright, dropping the record entirely when
2369 /// the new set is empty so `has_holes` stays a single negative probe for the
2370 /// dense case.
2371 pub fn install_holes(&mut self, arr: &Value, holes: rustc_hash::FxHashSet<usize>) {
2372 let Value::Obj(idx) = arr else { return };
2373 if holes.is_empty() {
2374 self.array_holes.remove(idx);
2375 } else {
2376 self.array_holes.insert(*idx, holes);
2377 }
2378 }
2379
2380 /// Forget any hole at or past `len` — what a `pop`, a `length` shrink or a
2381 /// truncating `splice` leaves behind.
2382 pub fn truncate_holes(&mut self, arr: &Value, len: usize) {
2383 self.remap_holes(arr, |i| (i < len).then_some(i));
2384 }
2385
2386 /// `util.inspect`'s `formatSpecialArray`: the element strings of a SPARSE
2387 /// array, where each maximal run of elided positions collapses to a single
2388 /// `<N empty items>` entry. Returns the entries and whether the last of them
2389 /// is the `... N more items` tail (which the grid layout must not size a
2390 /// column to).
2391 ///
2392 /// The `maxArrayLength` cap counts ENTRIES, not indices, so a run costs one
2393 /// slot however long it is — matching node, where `[ ...Array(200) ]`-style
2394 /// sparse arrays print a single `<200 empty items>`.
2395 fn inspect_sparse(
2396 &self,
2397 v: &Value,
2398 items: &[Value],
2399 indent: usize,
2400 st: &mut InspectCycles,
2401 ) -> (Vec<String>, bool) {
2402 let holes: rustc_hash::FxHashSet<usize> = self.hole_indices(v).into_iter().collect();
2403 let empties = |n: usize| {
2404 let unit = if n == 1 { "item" } else { "items" };
2405 format!("<{n} empty {unit}>")
2406 };
2407 let mut out: Vec<String> = Vec::new();
2408 // The first index not yet accounted for by an entry.
2409 let mut index = 0usize;
2410 for (i, it) in items.iter().enumerate() {
2411 if out.len() >= inspect_max_array_length() {
2412 break;
2413 }
2414 if holes.contains(&i) {
2415 continue;
2416 }
2417 if i > index {
2418 out.push(empties(i - index));
2419 index = i;
2420 if out.len() >= inspect_max_array_length() {
2421 break;
2422 }
2423 }
2424 out.push(self.inspect_lvl(it, indent + 2, st));
2425 index = i + 1;
2426 }
2427 let remaining = items.len() - index;
2428 if remaining == 0 {
2429 return (out, false);
2430 }
2431 if out.len() < inspect_max_array_length() {
2432 // Trailing holes are still `<N empty items>`, not a truncation.
2433 out.push(empties(remaining));
2434 (out, false)
2435 } else {
2436 let unit = if remaining == 1 { "item" } else { "items" };
2437 out.push(format!("... {remaining} more {unit}"));
2438 (out, true)
2439 }
2440 }
2441 pub fn new_object(&mut self, mut props: IndexMap<String, Value>) -> Value {
2442 // Integer-index keys enumerate ascending-first regardless of the order
2443 // they were supplied in (object literal, spread, Object.assign result).
2444 canonicalize_own_keys(&mut props);
2445 // A map carrying the hidden `@@native` tag IS an instance of that native
2446 // class, so it hangs off the class prototype rather than
2447 // `Object.prototype`. Eleven classes — `Hash`, `Cipheriv`,
2448 // `StringDecoder`, `Script`, `URLSearchParams`, `Console`,
2449 // `AbortController` among them — built plain objects instead, so
2450 // `x.constructor.name` read `"Object"` and a chain walk found none of
2451 // the class's methods. Linking HERE means a construction site cannot
2452 // forget it; the tag is already in the map at every one of them.
2453 let tag = props.get("@@native").and_then(|v| self.as_str(v));
2454 let obj = self.alloc(JsObj::Object(props));
2455 if let Some(proto) = tag.and_then(|t| self.ensure_ctor_proto(&t)) {
2456 self.set_proto(&obj, proto);
2457 }
2458 obj
2459 }
2460 pub fn as_str(&self, v: &Value) -> Option<String> {
2461 match v {
2462 Value::Str(s) => Some((**s).clone()),
2463 Value::Obj(_) => match self.get(v) {
2464 Some(JsObj::Str(s)) => Some(s.clone()),
2465 _ => None,
2466 },
2467 _ => None,
2468 }
2469 }
2470
2471 // ── scope / names ────────────────────────────────────────────────────
2472 fn frame(&self) -> &Frame {
2473 self.frames.last().unwrap()
2474 }
2475 fn cur_env(&self) -> Env {
2476 self.frame().env.clone()
2477 }
2478
2479 // ── DAP debug introspection (used only under `--dap`) ────────────────────
2480 /// Number of active call frames (the debugger's step-depth reference).
2481 pub fn frame_depth(&self) -> usize {
2482 self.frames.len()
2483 }
2484 /// Record the source line the innermost frame is executing (DAP line hook).
2485 pub fn set_cur_line(&mut self, line: u32) {
2486 if let Some(f) = self.frames.last_mut() {
2487 f.line = line;
2488 }
2489 }
2490 /// The `.stack` tail for an error created right now: one ` at <name>`
2491 /// line per live frame, innermost first, ending at the module frame.
2492 ///
2493 /// These are the REAL user frames — node-js has no `file:line:column` (the
2494 /// per-frame line is only tracked under `--dap`) and no Node-internal
2495 /// module-loader frames, so `.stack` names the call chain but can never be
2496 /// byte-identical to V8's. The names are what makes a thrown error
2497 /// diagnosable; the missing positions are documented in BUGS.md.
2498 /// V8's `Error.stackTraceLimit` — how many frames a captured stack keeps.
2499 ///
2500 /// The default is 10, it is settable, and setting it to 0 is the documented
2501 /// way to make error construction cheap. It did not exist, so the read was
2502 /// `undefined` and every stack carried every frame regardless.
2503 pub fn stack_trace_limit(&self) -> usize {
2504 match self.builtin_static("Error", "stackTraceLimit") {
2505 Some(v) => {
2506 let n = self.to_number(&v);
2507 if n.is_finite() && n > 0.0 {
2508 n as usize
2509 } else if n.is_nan() || n <= 0.0 {
2510 0
2511 } else {
2512 usize::MAX
2513 }
2514 }
2515 None => 10,
2516 }
2517 }
2518
2519 pub fn stack_frames(&self) -> String {
2520 let limit = self.stack_trace_limit();
2521 if limit == 0 {
2522 return String::new();
2523 }
2524 let mut out = String::new();
2525 for (i, f) in self.frames.iter().enumerate().rev().take(limit) {
2526 let name = match (&f.owner, i) {
2527 (Some(n), _) => n.clone(),
2528 (None, 0) => "Object.<anonymous>".to_string(),
2529 (None, _) => "<anonymous>".to_string(),
2530 };
2531 out.push_str("\n at ");
2532 out.push_str(&name);
2533 }
2534 if out.is_empty() && limit > 0 {
2535 out.push_str("\n at <anonymous>");
2536 }
2537 out
2538 }
2539
2540 /// The call stack as (frame name, line) pairs, innermost first — for the DAP
2541 /// `stackTrace`. `owner` carries the function name where known.
2542 pub fn dbg_stack(&self) -> Vec<(String, u32)> {
2543 self.frames
2544 .iter()
2545 .rev()
2546 .map(|f| {
2547 let name = f.owner.clone().unwrap_or_else(|| "<module>".to_string());
2548 (name, f.line)
2549 })
2550 .collect()
2551 }
2552 /// The innermost frame's locals as (name, inspect) pairs — for DAP `variables`.
2553 pub fn dbg_locals(&self) -> Vec<(String, String)> {
2554 let env = self.cur_env();
2555 let names: Vec<String> = env.borrow().vars.keys().cloned().collect();
2556 names
2557 .into_iter()
2558 .map(|n| {
2559 let v = self.read_name(&n).unwrap_or(Value::Undef);
2560 (n, self.inspect(&v))
2561 })
2562 .collect()
2563 }
2564
2565 /// Scope-chain read: local + enclosing chain, then globals.
2566 /// Whether `name` is a module-top-level binding that has not reached its
2567 /// declaration yet. Separate from [`JsHost::is_tdz`], which answers for a
2568 /// block-scoped one by inspecting the value it holds.
2569 pub fn is_tdz_global(&self, name: &str) -> bool {
2570 self.tdz_globals.contains(name)
2571 }
2572
2573 pub fn read_name(&self, name: &str) -> Option<Value> {
2574 let mut env = Some(self.cur_env());
2575 while let Some(e) = env {
2576 if let Some(v) = e.borrow().vars.get(name) {
2577 return Some(v.clone());
2578 }
2579 env = e.borrow().parent.clone();
2580 }
2581 self.globals.get(name).cloned()
2582 }
2583 pub fn read_global(&self, name: &str) -> Option<Value> {
2584 self.globals.get(name).cloned()
2585 }
2586
2587 /// Whether `name` is bound anywhere on the scope chain or in the globals —
2588 /// `read_name(..).is_some()` without cloning the value it finds. The
2589 /// strict-mode assignment path asks this and nothing else.
2590 pub fn has_name(&self, name: &str) -> bool {
2591 let mut env = Some(self.cur_env());
2592 while let Some(e) = env {
2593 if e.borrow().vars.contains_key(name) {
2594 return true;
2595 }
2596 env = e.borrow().parent.clone();
2597 }
2598 self.globals.contains_key(name)
2599 }
2600
2601 /// Assign to an existing binding up the scope chain, else create a global
2602 /// (JS assignment to an undeclared name targets the global object).
2603 /// Assign to an existing binding, or create a global. Returns `false` when
2604 /// the nearest binding is an immutable (`const`) one, which the caller turns
2605 /// into `TypeError: Assignment to constant variable.` — assigning to a
2606 /// `const` used to succeed SILENTLY, so code that node rejects ran on with
2607 /// a mutated constant.
2608 #[must_use]
2609 pub fn set_name(&mut self, name: &str, val: Value) -> bool {
2610 let mut env = Some(self.cur_env());
2611 while let Some(e) = env {
2612 // `get_mut`, not `contains_key` + `insert`: overwriting an existing
2613 // binding hashed the name twice and allocated a fresh `String` key
2614 // for a key that was already there — once per assignment, so once
2615 // per loop iteration in any counting loop.
2616 //
2617 // The const check runs only at the env that OWNS the name, and the
2618 // `is_empty` guard settles the common (no consts here) case without
2619 // hashing the name again.
2620 let mut b = e.borrow_mut();
2621 if b.vars.contains_key(name) {
2622 if !b.consts.is_empty() && b.consts.contains(name) {
2623 return false;
2624 }
2625 if let Some(slot) = b.vars.get_mut(name) {
2626 *slot = val;
2627 }
2628 return true;
2629 }
2630 drop(b);
2631 env = e.borrow().parent.clone();
2632 }
2633 if self.global_consts.contains(name) {
2634 return false;
2635 }
2636 match self.globals.get_mut(name) {
2637 Some(slot) => *slot = val,
2638 None => {
2639 self.globals.insert(name.to_string(), val);
2640 }
2641 }
2642 true
2643 }
2644
2645 /// Declare a `const` binding: the same placement as [`Self::declare_name`],
2646 /// plus recording the name as immutable in whichever scope received it.
2647 pub fn declare_const_name(&mut self, name: &str, val: Value) {
2648 let f = self.frame();
2649 let to_globals = f.is_module && Rc::ptr_eq(&f.env, &f.base_env);
2650 self.declare_name(name, val);
2651 if to_globals {
2652 self.global_consts.insert(name.to_string());
2653 } else {
2654 self.cur_env().borrow_mut().consts.insert(name.to_string());
2655 }
2656 }
2657
2658 /// The value a lexical binding holds between entering its scope and reaching
2659 /// its declaration — its TEMPORAL DEAD ZONE. One heap object for the whole
2660 /// process, so the check is a heap-index comparison and the marker cannot be
2661 /// produced by any JavaScript expression. It never escapes: every path that
2662 /// could read it throws first.
2663 pub fn tdz_marker(&mut self) -> Value {
2664 if let Some(v) = &self.tdz {
2665 return v.clone();
2666 }
2667 let v = self.alloc(JsObj::Builtin("@@tdz".into()));
2668 self.tdz = Some(v.clone());
2669 v
2670 }
2671
2672 /// Whether `v` is the uninitialized-binding marker.
2673 pub fn is_tdz(&self, v: &Value) -> bool {
2674 matches!((&self.tdz, v), (Some(Value::Obj(a)), Value::Obj(b)) if a == b)
2675 }
2676
2677 /// Declare `name` in the CURRENT scope as uninitialized, unless that scope
2678 /// already binds it. Emitted at the top of every scope for each `let`,
2679 /// `const` and `class` declared directly in it, so a read before the
2680 /// declaration throws instead of finding an OUTER binding of the same name —
2681 /// `let x = 1; { x; let x = 2 }` used to read the outer `1`.
2682 pub fn hoist_tdz(&mut self, name: &str) {
2683 let marker = self.tdz_marker();
2684 let f = self.frame();
2685 // At module top level a lexical binding lives in `globals`, which is ALSO
2686 // what backs `globalThis.<name>` — so parking the marker there exposes it
2687 // to JavaScript, and `const crypto = …` made `globalThis.crypto` read
2688 // back as the marker. Top-level dead zones are tracked in a separate set
2689 // that only the name-read path consults.
2690 if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2691 if !self.globals.contains_key(name) {
2692 self.tdz_globals.insert(name.to_string());
2693 }
2694 return;
2695 }
2696 let env = self.cur_env();
2697 let mut e = env.borrow_mut();
2698 if !e.vars.contains_key(name) {
2699 e.vars.insert(name.to_string(), marker);
2700 }
2701 }
2702
2703 /// Declare a new binding in the current scope (`let`/`const`). At the top of
2704 /// the module frame there is no local env, so those names become globals; once
2705 /// a block scope is open the binding belongs to that block.
2706 pub fn declare_name(&mut self, name: &str, val: Value) {
2707 let f = self.frame();
2708 if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2709 self.tdz_globals.remove(name);
2710 self.globals.insert(name.to_string(), val);
2711 } else {
2712 self.cur_env()
2713 .borrow_mut()
2714 .vars
2715 .insert(name.to_string(), val);
2716 }
2717 }
2718
2719 /// Declare a `var` (or a hoisted function declaration): FUNCTION-scoped, so it
2720 /// skips every open block scope and lands in the activation's base env.
2721 /// Create a hoisted `var` binding, initialised to `undefined`, only when the
2722 /// name is not already bound in this activation.
2723 ///
2724 /// `var` bindings come into existence when the scope is entered, not where
2725 /// the declaration is written — `f(){ x; var x = 1 }` reads `undefined`
2726 /// rather than throwing. "If absent" is what keeps a parameter intact: in
2727 /// `function f(a) { var a; }` the `var` names a binding that already exists
2728 /// and must not be reset, which is also why a bare `var x;` emits nothing at
2729 /// its own position.
2730 pub fn hoist_var_name(&mut self, name: &str) {
2731 // The ENTRY script's top level is a CommonJS module body, not global
2732 // scope: node wraps every file in a function, so a top-level `var` is a
2733 // local of that wrapper. Binding it into the globals map made
2734 // `var x = 3` at the top of the entry readable as `globalThis.x`, where
2735 // node says `undefined` — a REQUIRED module already ran inside a real
2736 // frame and behaved correctly, so only the entry file differed.
2737 if self.frame().is_module && !self.module_scope {
2738 self.globals.entry(name.to_string()).or_insert(Value::Undef);
2739 return;
2740 }
2741 let base = self.frame().base_env.clone();
2742 let mut env = base.borrow_mut();
2743 if !env.vars.contains_key(name) {
2744 env.vars.insert(name.to_string(), Value::Undef);
2745 }
2746 }
2747
2748 pub fn declare_var_name(&mut self, name: &str, val: Value) {
2749 if self.frame().is_module && !self.module_scope {
2750 self.globals.insert(name.to_string(), val);
2751 return;
2752 }
2753 let base = self.frame().base_env.clone();
2754 base.borrow_mut().vars.insert(name.to_string(), val);
2755 }
2756
2757 /// Enter a fresh block scope.
2758 pub fn push_scope(&mut self) {
2759 let env = self.cur_env();
2760 self.frames.last_mut().unwrap().env = child_env(env);
2761 }
2762
2763 /// Open a scope that is also the activation's VARIABLE environment, and
2764 /// return the previous one so the caller can restore it.
2765 ///
2766 /// A block scope is not enough for a strict direct `eval`: `var` and a
2767 /// hoisted function declaration bind to `base_env`, so they walked straight
2768 /// past a plain `push_scope` and still landed in the caller's function
2769 /// scope. Only `let`/`const` were contained.
2770 pub fn push_var_scope(&mut self) -> Env {
2771 let env = child_env(self.cur_env());
2772 let f = self.frames.last_mut().unwrap();
2773 let prev = std::mem::replace(&mut f.base_env, env.clone());
2774 f.env = env;
2775 prev
2776 }
2777
2778 /// Restore the variable environment a `push_var_scope` replaced.
2779 pub fn pop_var_scope(&mut self, prev: Env) {
2780 let f = self.frames.last_mut().unwrap();
2781 f.env = prev.clone();
2782 f.base_env = prev;
2783 }
2784
2785 /// Leave the innermost block scope (never pops past the activation's base).
2786 pub fn pop_scope(&mut self) {
2787 let cur = self.cur_env();
2788 if Rc::ptr_eq(&cur, &self.frame().base_env) {
2789 return;
2790 }
2791 let parent = cur.borrow().parent.clone();
2792 if let Some(p) = parent {
2793 self.frames.last_mut().unwrap().env = p;
2794 }
2795 }
2796
2797 /// Replace the innermost block scope with a fresh copy of its bindings — the
2798 /// per-iteration environment a `for (let i …)` loop creates, so a closure made
2799 /// in one iteration keeps that iteration's value.
2800 pub fn copy_scope(&mut self) {
2801 let cur = self.cur_env();
2802 if Rc::ptr_eq(&cur, &self.frame().base_env) {
2803 return;
2804 }
2805 let parent = cur.borrow().parent.clone();
2806 let fresh = new_env(parent);
2807 fresh.borrow_mut().vars = cur.borrow().vars.clone();
2808 self.frames.last_mut().unwrap().env = fresh;
2809 }
2810
2811 /// The current block-scope env, for save/restore across a nested chunk.
2812 pub fn scope_snapshot(&self) -> Env {
2813 self.cur_env()
2814 }
2815 pub fn restore_scope(&mut self, env: Env) {
2816 self.frames.last_mut().unwrap().env = env;
2817 }
2818 pub fn set_global(&mut self, name: &str, val: Value) {
2819 self.globals.insert(name.to_string(), val);
2820 }
2821
2822 // ── output capture ───────────────────────────────────────────────────
2823 //
2824 // Every write a *program* makes — `console.log`, `process.stdout.write`,
2825 // `print` — funnels through `write_out`, so turning capture on redirects all
2826 // of them at once. Diagnostics the runtime itself emits (the REPL banner, a
2827 // crash traceback from `main`) deliberately do not: they belong to the
2828 // process, not to the program.
2829
2830 /// Start capturing program output in-process. Any text already captured is
2831 /// discarded, so each run starts clean.
2832 pub fn begin_capture(&mut self) {
2833 self.capture = Some(Vec::new());
2834 }
2835
2836 /// Stop capturing and take everything written since [`begin_capture`],
2837 /// returning the empty string when capture was not on. The captured bytes
2838 /// are rendered lossily: this API hands back a `String`, so a program that
2839 /// wrote non-UTF-8 gets `U+FFFD` here even though the same write reaches a
2840 /// real stdout byte-exact. Use [`end_capture_bytes`] to keep those bytes.
2841 ///
2842 /// [`begin_capture`]: JsHost::begin_capture
2843 /// [`end_capture_bytes`]: JsHost::end_capture_bytes
2844 pub fn end_capture(&mut self) -> String {
2845 String::from_utf8_lossy(&self.capture.take().unwrap_or_default()).into_owned()
2846 }
2847
2848 /// Stop capturing and take the raw bytes, without the lossy transcription
2849 /// [`end_capture`] applies.
2850 ///
2851 /// [`end_capture`]: JsHost::end_capture
2852 pub fn end_capture_bytes(&mut self) -> Vec<u8> {
2853 self.capture.take().unwrap_or_default()
2854 }
2855
2856 /// Whether output is being captured — the one thing a caller needs to know
2857 /// before asking the real stream a question (`isTTY`, cursor position).
2858 pub fn capturing(&self) -> bool {
2859 self.capture.is_some()
2860 }
2861
2862 /// Write program output: into the capture buffer when capturing, else to the
2863 /// process stream `stderr` selects. `s` is written verbatim — callers add
2864 /// their own line ending, as `console.log` does and `process.stdout.write`
2865 /// does not.
2866 pub fn write_out(&mut self, s: &str, stderr: bool) {
2867 self.write_out_bytes(s.as_bytes(), stderr);
2868 }
2869
2870 /// Write program output as raw BYTES. `process.stdout.write(buf)` hands Node
2871 /// a byte string and Node writes it through untouched, so a `Buffer` holding
2872 /// `ff fe 41` reaches stdout as those three bytes. Routing it through a Rust
2873 /// `String` first replaced every non-UTF-8 byte with `U+FFFD` — three bytes
2874 /// became seven — so the byte path exists separately from [`write_out`].
2875 ///
2876 /// [`write_out`]: JsHost::write_out
2877 pub fn write_out_bytes(&mut self, bytes: &[u8], stderr: bool) {
2878 if let Some(buf) = &mut self.capture {
2879 buf.extend_from_slice(bytes);
2880 return;
2881 }
2882 use std::io::Write as _;
2883 if stderr {
2884 let mut e = std::io::stderr();
2885 let _ = e.write_all(bytes);
2886 let _ = e.flush();
2887 } else {
2888 let mut o = std::io::stdout();
2889 let _ = o.write_all(bytes);
2890 let _ = o.flush();
2891 }
2892 }
2893 pub fn del_name(&mut self, name: &str) {
2894 if self
2895 .cur_env()
2896 .borrow_mut()
2897 .vars
2898 .shift_remove(name)
2899 .is_some()
2900 {
2901 return;
2902 }
2903 self.globals.shift_remove(name);
2904 }
2905
2906 pub fn current_this(&self) -> Option<Value> {
2907 self.frame().this_obj.clone()
2908 }
2909
2910 /// The running activation's [`ThisState`].
2911 pub fn this_state(&self) -> ThisState {
2912 self.frame().this_state
2913 }
2914
2915 /// Mark the next user-function activation as a derived constructor.
2916 pub fn mark_next_call_derived_ctor(&mut self) {
2917 self.derived_ctor_next = true;
2918 }
2919
2920 /// BindThisValue (9.1.1.3.1) for a `super()` that has just returned: the
2921 /// nearest derived-constructor activation becomes `Bound`. That is the top
2922 /// frame, or — for `super()` inside an arrow — the constructor below the
2923 /// arrow's own frame. `false` when it was already bound: the second call.
2924 pub fn bind_super_this(&mut self) -> bool {
2925 let Some(f) = self
2926 .frames
2927 .iter_mut()
2928 .rev()
2929 .find(|f| f.this_state != ThisState::Plain)
2930 else {
2931 return true;
2932 };
2933 if f.this_state == ThisState::Bound {
2934 return false;
2935 }
2936 f.this_state = ThisState::Bound;
2937 true
2938 }
2939
2940 /// The object a `super()` call substituted for the instance, if any.
2941 ///
2942 /// `construct_class` allocates the instance up front, so when a base
2943 /// constructor RETURNS an object the substitution happens deep inside the
2944 /// VM, after that allocation. This carries it back out. Each
2945 /// `construct_class` saves and restores the previous value around its own
2946 /// run, so a `new` inside a constructor body cannot steal it.
2947 pub fn take_super_replacement(&mut self) -> Option<Value> {
2948 self.super_replacement.take()
2949 }
2950
2951 pub fn swap_super_replacement(&mut self, v: Option<Value>) -> Option<Value> {
2952 std::mem::replace(&mut self.super_replacement, v)
2953 }
2954
2955 /// Rebind the running activation's `this`.
2956 ///
2957 /// Only `super()` does this: when the parent constructor RETURNS an object,
2958 /// 15.7.15 makes that object the derived instance, so the rest of the
2959 /// derived constructor has to write to it rather than to the one allocated
2960 /// before the call.
2961 pub fn set_current_this(&mut self, v: Value) {
2962 if let Some(f) = self.frames.last_mut() {
2963 f.this_obj = Some(v.clone());
2964 }
2965 self.super_replacement = Some(v);
2966 }
2967 /// The callbacks to run for `event`, consuming any `once` registration in
2968 /// the same step — so a listener that re-emits the event cannot re-enter a
2969 /// one-shot handler.
2970 pub fn take_process_listeners(&mut self, event: &str) -> Vec<Value> {
2971 let Some(list) = self.process_listeners.get_mut(event) else {
2972 return Vec::new();
2973 };
2974 let fired: Vec<Value> = list.iter().map(|l| l.f.clone()).collect();
2975 list.retain(|l| !l.once);
2976 fired
2977 }
2978
2979 /// Bind the TOP-LEVEL `this` — the value a `this` outside any function sees.
2980 ///
2981 /// Node answers differently per entry point and both answers are objects:
2982 /// `node f.js` runs a CommonJS module, so top-level `this` is
2983 /// `module.exports`; `node -e` and `node -` run a Script, so it is
2984 /// `globalThis`. Verified on node v26.7.0 —
2985 /// `console.log(this === globalThis, this === module.exports)` is
2986 /// `false true` from a file and `true false` from `-e` and from stdin. It
2987 /// was `undefined` at every entry point here, so `this.x = 1` at module
2988 /// scope threw instead of populating the exports object.
2989 ///
2990 /// Only the base frame is touched: a plain function call still gets its own
2991 /// (`undefined`) binding rather than inheriting this one.
2992 pub fn set_top_this(&mut self, v: Value) {
2993 if let Some(f) = self.frames.first_mut() {
2994 f.this_obj = Some(v);
2995 }
2996 }
2997 pub fn current_env_capture(&self) -> Env {
2998 self.frame().env.clone()
2999 }
3000 pub fn current_new_target(&self) -> Option<Value> {
3001 self.frame().new_target.clone()
3002 }
3003 fn current_home_class(&self) -> Option<Value> {
3004 self.frame().home_class.clone()
3005 }
3006
3007 /// The `(parent_ctor, this_class_fields)` for a running constructor's
3008 /// `super(...)`, derived from the frame's home class.
3009 pub fn super_context(&self) -> (Option<Value>, Vec<(String, Value, bool)>) {
3010 match self.current_home_class() {
3011 Some(cv) => match self.get(&cv) {
3012 Some(JsObj::Class(c)) => (c.parent.clone(), c.fields.clone()),
3013 _ => (None, Vec::new()),
3014 },
3015 None => (None, Vec::new()),
3016 }
3017 }
3018
3019 /// Resolve `super.name` to either the parent-prototype getter (to be invoked
3020 /// by the caller, outside any host borrow) or a directly-usable value.
3021 pub fn super_resolve(&self, name: &str) -> SuperRef {
3022 // A shorthand method in an OBJECT LITERAL resolves `super` through its
3023 // home object's prototype; only a class method has a home CLASS. With
3024 // nothing tracked for the literal case, `{ m() { super.x() } }` had no
3025 // parent to look in and reported the method missing.
3026 if let Some(home) = self.frame().home_object.clone() {
3027 let target = self.proto_of(&home).unwrap_or(Value::Undef);
3028 if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3029 return SuperRef::Getter(getter);
3030 }
3031 return SuperRef::Data(lookup_chain(self, &target, name).unwrap_or(Value::Undef));
3032 }
3033 let parent = match self
3034 .current_home_class()
3035 .and_then(|cv| match self.get(&cv) {
3036 Some(JsObj::Class(c)) => c.parent.clone(),
3037 _ => None,
3038 }) {
3039 Some(p) => p,
3040 None => return SuperRef::Data(Value::Undef),
3041 };
3042 // A STATIC method's home object is the constructor, so `super.x` reads
3043 // off the parent CONSTRUCTOR; an instance method's is the prototype
3044 // object, so it reads off the parent's prototype. Always taking the
3045 // prototype meant `static s() { return super.s(); }` found nothing and
3046 // then tried to call it.
3047 let target = if self.frame().home_static {
3048 parent.clone()
3049 } else {
3050 match self.get(&parent) {
3051 Some(JsObj::Class(pc)) => pc.proto.clone(),
3052 _ => self.fn_prop(&parent, "prototype").unwrap_or(Value::Undef),
3053 }
3054 };
3055 if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3056 return SuperRef::Getter(getter);
3057 }
3058 if let Some(v) = lookup_chain(self, &target, name) {
3059 return SuperRef::Data(v);
3060 }
3061 // A static method lives in the fn-prop side table, not the property map.
3062 SuperRef::Data(self.fn_prop(&target, name).unwrap_or(Value::Undef))
3063 }
3064
3065 // ── signals / errors ─────────────────────────────────────────────────
3066 pub fn take_error(&mut self) -> Option<String> {
3067 self.error.take()
3068 }
3069 pub fn raise_str(&mut self, class: &str, msg: &str) -> String {
3070 let s = if msg.is_empty() {
3071 class.to_string()
3072 } else {
3073 format!("{class}: {msg}")
3074 };
3075 self.error = Some(s.clone());
3076 s
3077 }
3078}
3079
3080// ── error constructors ───────────────────────────────────────────────────────
3081
3082pub fn type_error(msg: &str) -> String {
3083 format!("TypeError: {msg}")
3084}
3085pub fn ref_error(name: &str) -> String {
3086 format!("ReferenceError: {name} is not defined")
3087}
3088
3089/// The error a read of a lexical binding still in its TEMPORAL DEAD ZONE
3090/// raises. Distinct from [`ref_error`] on purpose: node says which of the two
3091/// happened, and the difference is how a reader tells a misspelled name from a
3092/// `let` used above its declaration.
3093pub fn tdz_error(name: &str) -> String {
3094 format!("ReferenceError: Cannot access '{name}' before initialization")
3095}
3096pub fn range_error(msg: &str) -> String {
3097 format!("RangeError: {msg}")
3098}
3099
3100/// V8's `String::kMaxLength` on a 64-bit build, in UTF-16 code units — the
3101/// largest string the engine will materialize.
3102///
3103/// Measured on node v26.7.0 (darwin arm64):
3104/// `require('buffer').constants.MAX_STRING_LENGTH` is `536870888`,
3105/// `'a'.repeat(536870888)` succeeds with that length, and
3106/// `'a'.repeat(536870889)` is `RangeError: Invalid string length`.
3107pub const MAX_STRING_LENGTH: usize = 536_870_888;
3108
3109/// The error V8 raises for a string operation whose RESULT would exceed
3110/// [`MAX_STRING_LENGTH`]. It is raised from the length arithmetic, before any
3111/// allocation: `'a'.repeat(2**40)` throws promptly on node where node-js used to
3112/// sit building a 1 TiB `String` until it was killed.
3113pub fn invalid_string_length() -> String {
3114 range_error("Invalid string length")
3115}
3116
3117/// `ToUint32`-validated array length — ECMA-262 10.4.2.2 `ArrayCreate` step 1
3118/// and 10.4.2.4 `ArraySetLength` step 3.
3119///
3120/// A length is legal only if `ToUint32(v)` equals `ToNumber(v)` exactly, so
3121/// `-1`, `1.5`, `NaN`, `Infinity`, `'x'` and `2**32` are all
3122/// `RangeError: Invalid array length` while `'3'` is `3` and `-0` is `0`
3123/// (measured on node v26.7.0: `new Array(-0).length` is `0`, `a.length = '3'`
3124/// leaves `3`, `a.length = 'x'` throws). node-js validated none of them — it
3125/// built `[-1]` from `new Array(-1)`, silently ignored `a.length = -1`, and sat
3126/// materializing four billion elements for `a.length = 2**32`.
3127pub fn to_array_length(v: &Value) -> Result<usize, String> {
3128 // 10.4.2.4 steps 2-3 run TWO conversions: `ToUint32(value)` and then
3129 // `ToNumber(value)`, compared against each other. Both are observable — a
3130 // counting `valueOf` sees two calls in node and saw one here — and the
3131 // second is what makes `arr.length = 1.5` a RangeError rather than 1.
3132 let u32_pass = to_number_value(v)?;
3133 let n = to_number_value(v)?;
3134 let _ = u32_pass;
3135 // `ToUint32`: truncate toward zero, then modulo 2^32.
3136 let u = if n.is_finite() {
3137 (n.trunc() as i64).rem_euclid(1i64 << 32) as u32
3138 } else {
3139 0
3140 };
3141 // `-0` compares equal to `0` here, which is what makes `new Array(-0)` legal.
3142 if (u as f64) != n {
3143 return Err(range_error("Invalid array length"));
3144 }
3145 Ok(u as usize)
3146}
3147
3148/// A Node *coded* error raised from the JS layer: `Name [ERR_CODE]: message`.
3149///
3150/// `builtins::synth_error` parses that head back apart, so the bracketed code
3151/// becomes the enumerable `err.code` that `err.code === 'ERR_INVALID_URL'`-style
3152/// handling reads. Writing the head by hand at each throw site is what left a
3153/// dozen of them with `err.code === undefined` while the message matched.
3154///
3155/// Use this for errors Node raises from `lib/internal/errors.js`, whose `.name`
3156/// is left bracketed while the stack is captured and therefore shows up in both
3157/// `String(err)` and `err.stack` — measured on v26.7.0:
3158///
3159/// ```text
3160/// process.exit(1.5) -> RangeError [ERR_OUT_OF_RANGE]: The value of "code" …
3161/// ```
3162pub fn coded_error(class: &str, code: &str, msg: &str) -> String {
3163 format!("{class} [{code}]: {msg}")
3164}
3165
3166/// The marker `plain_coded_error` hides a code behind, and `synth_error` strips.
3167pub const CODE_MARK: &str = "\u{1}code:";
3168
3169/// Marks an error string as a `DOMException` carrying a WHATWG error NAME
3170/// rather than one of the ECMAScript error classes. WebCrypto and the abort
3171/// APIs reject with these, and the name (`NotSupportedError`) is not a class
3172/// `synth_error` could otherwise recognise.
3173pub const DOM_MARK: &str = "\u{1}dom:";
3174
3175/// A `DOMException` error string: `name` is the WHATWG error name.
3176pub fn dom_error(name: &str, msg: &str) -> String {
3177 format!("{DOM_MARK}{name}\u{1}{msg}")
3178}
3179
3180/// A Node coded error raised from the *native* layer: `.code` is set, but the
3181/// name is never bracketed, so `String(err)` is the plain `Name: message`.
3182///
3183/// The distinction is observable and is not a stylistic choice — on v26.7.0,
3184/// `String(new URL("/x") error)` is `TypeError: Invalid URL` with
3185/// `.code === 'ERR_INVALID_URL'`, while the JS-layer `process.exit(1.5)` error
3186/// brackets its code into the very same two reads. Encoding both through one
3187/// `Name [CODE]:` head would have to pick one and be wrong about the other.
3188///
3189/// The code rides in a marker at the head of the message rather than in the
3190/// error class, because the class text is exactly what must NOT carry it. The
3191/// marker is an internal wire format between a throw site and `synth_error`; it
3192/// never survives into a `.message`.
3193pub fn plain_coded_error(class: &str, code: &str, msg: &str) -> String {
3194 format!("{class}: {CODE_MARK}{code}\u{1}{msg}")
3195}
3196
3197/// Marks the start of the extra string own properties a
3198/// [`plain_coded_error_with`] error carries after its message.
3199pub const FIELDS_MARK: char = '\u{2}';
3200
3201/// [`plain_coded_error`] plus extra enumerable string own properties, set after
3202/// `code` in the order given — `new URL('x', 'nope')` throws with
3203/// `Object.keys(e)` reading `["code","input","base"]`.
3204///
3205/// Each field is `key\u{3}<byte length>\u{3}value`, so a value (a URL input is
3206/// arbitrary user text) may carry any character, the separators included.
3207pub fn plain_coded_error_with(class: &str, code: &str, msg: &str, fields: &[(&str, &str)]) -> String {
3208 let mut s = plain_coded_error(class, code, msg);
3209 s.push(FIELDS_MARK);
3210 for (k, v) in fields {
3211 s.push_str(&format!("{k}\u{3}{}\u{3}{v}", v.len()));
3212 }
3213 s
3214}
3215
3216/// An error string as a person reads it: `TypeError: Invalid URL`, with the
3217/// internal code and field markers of [`plain_coded_error`] /
3218/// [`plain_coded_error_with`] removed. An uncaught native error is printed
3219/// from its string, and printed the wire format (`\u{1}code:ERR_INVALID_URL…`).
3220pub fn plain_error_text(e: &str) -> String {
3221 let Some(i) = e.find(CODE_MARK) else {
3222 return e.to_string();
3223 };
3224 let (head, rest) = e.split_at(i);
3225 match rest[CODE_MARK.len()..].split_once('\u{1}') {
3226 Some((_, m)) => format!("{head}{}", split_error_fields(m).0),
3227 None => e.to_string(),
3228 }
3229}
3230
3231/// Split a [`plain_coded_error_with`] message back into the message and its
3232/// fields. A message with no field mark comes back whole with no fields.
3233pub fn split_error_fields(msg: &str) -> (&str, Vec<(&str, &str)>) {
3234 let Some((head, mut rest)) = msg.split_once(FIELDS_MARK) else {
3235 return (msg, Vec::new());
3236 };
3237 let mut fields = Vec::new();
3238 while let Some((k, tail)) = rest.split_once('\u{3}') {
3239 let Some((len, tail)) = tail.split_once('\u{3}') else { break };
3240 let Ok(len) = len.parse::<usize>() else { break };
3241 let Some(v) = tail.get(..len) else { break };
3242 fields.push((k, v));
3243 rest = &tail[len..];
3244 }
3245 (head, fields)
3246}
3247
3248/// `TypeError [ERR_INVALID_ARG_TYPE]: The "<name>" <kind> must be of type
3249/// <expected>. Received …` — Node's single most common argument rejection.
3250pub fn invalid_arg_type(name: &str, kind: &str, expected: &str, v: &Value) -> String {
3251 coded_error(
3252 "TypeError",
3253 "ERR_INVALID_ARG_TYPE",
3254 &format!(
3255 "The \"{name}\" {kind} must be of type {expected}. Received {}",
3256 crate::stdlib::received_desc(v)
3257 ),
3258 )
3259}
3260
3261// ── the fusevm run plumbing ──────────────────────────────────────────────────
3262
3263thread_local! {
3264 static DEBUG_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3265}
3266
3267/// Enable/disable DAP debug execution (`node --dap`).
3268pub fn set_debug_mode(on: bool) {
3269 DEBUG_MODE.with(|d| d.set(on));
3270}
3271
3272// ── join cycle detection ─────────────────────────────────────────────────────
3273
3274thread_local! {
3275 /// Heap handles whose join is in progress, innermost last — V8's JoinStack.
3276 static JOIN_STACK: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
3277}
3278
3279/// V8's `JoinStackPush`: record that `v` is being joined, or report `false` if
3280/// it already is.
3281///
3282/// `Array.prototype.join` (and `toString`/`toLocaleString`, which route through
3283/// it) is the one place the language walks an object graph with no depth bound,
3284/// so every engine cuts re-entrance here: a receiver already on the stack
3285/// contributes the EMPTY STRING rather than recursing. Measured on node v26.7.0,
3286/// `const a=[1]; a.push(a); a.push(2); a.join('-')` is `"1--2"`, and
3287/// `String(a)`/`` `${a}` `` on `a=[a]` are both `""`. node-js had no such cut and
3288/// recursed until the native stack overflowed, ABORTING the process (exit 134) —
3289/// uncatchable, where node returns a string.
3290///
3291/// Only re-entrance is cut, not repetition: `[a,a].join('|')` still renders `a`
3292/// twice, because the first render pops before the second pushes.
3293///
3294/// A `true` return MUST be paired with [`join_stack_pop`].
3295pub fn join_stack_push(v: &Value) -> bool {
3296 match v {
3297 Value::Obj(i) => JOIN_STACK.with(|s| {
3298 let mut s = s.borrow_mut();
3299 if s.contains(i) {
3300 false
3301 } else {
3302 s.push(*i);
3303 true
3304 }
3305 }),
3306 _ => true,
3307 }
3308}
3309
3310/// Pop the innermost [`join_stack_push`].
3311pub fn join_stack_pop() {
3312 JOIN_STACK.with(|s| {
3313 s.borrow_mut().pop();
3314 });
3315}
3316
3317// ── native stack guard ───────────────────────────────────────────────────────
3318
3319thread_local! {
3320 /// Lowest stack address a nested run may start from, or 0 before the
3321 /// running thread's bounds have been measured. Cached because the pthread
3322 /// query is a syscall-free but non-trivial read and this is on every call.
3323 static STACK_FLOOR: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
3324}
3325
3326/// Stack left unusable below the floor, as a fraction of the whole stack: the
3327/// throw itself still has to unwind, build an `Error`, capture `.stack` and run
3328/// whatever `catch` receives it, all of which needs room *below* the deepest
3329/// call that was allowed.
3330const STACK_RESERVE_DIVISOR: usize = 8;
3331/// Floor of that reserve, for a thread whose stack is small enough that an
3332/// eighth of it would not cover the unwind.
3333const STACK_RESERVE_MIN: usize = 512 * 1024;
3334/// Reserve assumed on a platform whose stack bounds cannot be queried. Deliberately
3335/// large relative to a default 8 MiB stack — over-reserving costs recursion
3336/// depth, under-reserving costs the process.
3337const STACK_RESERVE_FALLBACK: usize = 1024 * 1024;
3338
3339/// The address of a local in the caller's frame — how far down the stack
3340/// execution currently is. `black_box` keeps the probe from being optimized into
3341/// a different frame.
3342fn stack_pointer() -> usize {
3343 let probe = 0u8;
3344 std::hint::black_box(&probe) as *const u8 as usize
3345}
3346
3347/// The running thread's `(lowest address, size)` stack bounds.
3348///
3349/// Asked of pthread rather than assumed, because the three threads that run JS
3350/// have three different stacks: the `node` binary's own (`main.rs` reserves
3351/// [`crate::JS_STACK_SIZE`]), a `worker_threads` thread's, and a `cargo test`
3352/// harness thread's. A fixed byte budget would be wrong on two of the three.
3353fn stack_bounds() -> Option<(usize, usize)> {
3354 #[cfg(target_vendor = "apple")]
3355 {
3356 // SAFETY: both calls are pure reads of the calling thread's own
3357 // pthread record; neither allocates nor can fail.
3358 unsafe {
3359 let me = libc::pthread_self();
3360 let top = libc::pthread_get_stackaddr_np(me) as usize;
3361 let size = libc::pthread_get_stacksize_np(me);
3362 if size == 0 || top < size {
3363 return None;
3364 }
3365 Some((top - size, size))
3366 }
3367 }
3368 #[cfg(target_os = "linux")]
3369 {
3370 // SAFETY: `attr` is initialized by `pthread_getattr_np` before it is
3371 // read, only read on the success path, and destroyed on every path.
3372 unsafe {
3373 let mut attr: libc::pthread_attr_t = std::mem::zeroed();
3374 if libc::pthread_getattr_np(libc::pthread_self(), &mut attr) != 0 {
3375 return None;
3376 }
3377 let mut low: *mut libc::c_void = std::ptr::null_mut();
3378 let mut size: libc::size_t = 0;
3379 let ok = libc::pthread_attr_getstack(&attr, &mut low, &mut size) == 0;
3380 libc::pthread_attr_destroy(&mut attr);
3381 if ok && size != 0 {
3382 return Some((low as usize, size));
3383 }
3384 None
3385 }
3386 }
3387 #[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
3388 {
3389 None
3390 }
3391}
3392
3393/// The stack address below which a further nested VM run must throw instead of
3394/// recursing.
3395///
3396/// Every JS call is a Rust-level recursion — `run_user_func_nt` pushes a
3397/// [`Frame`], then `run_chunk_on` builds a whole new `fusevm::VM` on the stack
3398/// and runs the body, whose own calls land back here. Unbounded JS recursion
3399/// therefore used to exhaust the OS stack and ABORT: `fatal runtime error:
3400/// stack overflow`, exit 134, which no `try`/`catch` can see. V8 throws a
3401/// catchable `RangeError: Maximum call stack size exceeded` instead (measured on
3402/// node v26.7.0: `let d=0; function f(){d++;f()}` reports depth 9901).
3403///
3404/// The floor is derived from the thread's real bounds rather than a frame count
3405/// because a node-js frame has no fixed size — a debug build spends ~98 KiB per
3406/// JS call (measured: `node -e 'function f(n){…f(n-1)}'` survived 83 on an 8 MiB
3407/// stack and no more), a release build far less, and a native builtin recursing
3408/// through a user callback spends a different amount again.
3409fn stack_floor() -> usize {
3410 let cached = STACK_FLOOR.with(|c| c.get());
3411 if cached != 0 {
3412 return cached;
3413 }
3414 let floor = match stack_bounds() {
3415 Some((low, size)) => low + (size / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN),
3416 None => stack_pointer().saturating_sub(STACK_RESERVE_FALLBACK),
3417 };
3418 STACK_FLOOR.with(|c| c.set(floor));
3419 floor
3420}
3421
3422/// Stack given to each generator/async coroutine.
3423///
3424/// corosensei's default is 1 MiB, which at a debug build's ~98 KiB per JS call
3425/// left a `function*` body barely ten frames of recursion before it walked off
3426/// the end. The mapping is `PROT_NONE` reserved and `mprotect`ed, so the cost of
3427/// a larger one is address space, not resident memory — but it IS per live
3428/// generator, so this stays far below the entry thread's
3429/// [`crate::JS_STACK_SIZE`]: a program with thousands of concurrent async calls
3430/// has thousands of these.
3431const CORO_STACK_SIZE: usize = 16 * 1024 * 1024;
3432
3433/// The [`stack_floor`] that applies while a coroutine on `stack` is running.
3434fn coro_stack_floor(stack: &impl corosensei::stack::Stack) -> usize {
3435 stack.limit().get() + (CORO_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN)
3436}
3437
3438/// corosensei's own `DefaultStack::default()` size, used only when the
3439/// [`CORO_STACK_SIZE`] reservation is refused and the coroutine therefore runs
3440/// on a stack whose bounds are not ours to read.
3441const CORO_FALLBACK_STACK_SIZE: usize = 1024 * 1024;
3442
3443/// Give a coroutine whose stack bounds are unknown a floor measured from where
3444/// its body starts. Called once, at body entry, on the coroutine's own stack.
3445fn ensure_coroutine_floor() {
3446 if STACK_FLOOR.with(|c| c.get()) != 0 {
3447 return;
3448 }
3449 let budget = CORO_FALLBACK_STACK_SIZE
3450 - (CORO_FALLBACK_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN);
3451 STACK_FLOOR.with(|c| c.set(stack_pointer().saturating_sub(budget)));
3452}
3453
3454/// Install `floor` as the current stack floor, returning the previous one.
3455///
3456/// Used around a coroutine resume, which switches to a stack the thread's
3457/// pthread record knows nothing about. A floor of 0 means "not known" and makes
3458/// the next [`stack_floor`] measure again, which is the right answer for the
3459/// entry thread and a conservative one for a fallback coroutine stack.
3460fn swap_stack_floor(floor: usize) -> usize {
3461 STACK_FLOOR.with(|c| c.replace(floor))
3462}
3463
3464/// Whether the native stack is too close to its floor for one more nested run.
3465pub fn stack_exhausted() -> bool {
3466 stack_pointer() <= stack_floor()
3467}
3468
3469/// The error V8 raises when the call stack is exhausted. Catchable, and with the
3470/// `RangeError` constructor node uses — not a `panic!`.
3471pub fn stack_overflow_error() -> String {
3472 range_error("Maximum call stack size exceeded")
3473}
3474
3475/// Pool key for the body of user function `def_id`.
3476pub fn func_key(def_id: usize) -> u64 {
3477 1 << 40 | def_id as u64
3478}
3479
3480/// Pool key for one part of `try` statement `try_id`: 0 = the block, 1 = the
3481/// handler, 2 = the finalizer.
3482pub fn try_key(try_id: usize, part: u64) -> u64 {
3483 2 << 40 | (try_id as u64) << 2 | part
3484}
3485
3486thread_local! {
3487 /// VMs that have finished a run, kept for the next one — grouped by the
3488 /// chunk they still hold.
3489 ///
3490 /// Every JS call, every `try` block and every generator step runs its chunk
3491 /// through [`run_chunk_on`], which used to build a `fusevm::VM` from
3492 /// scratch: three `Vec` allocations, 70 `register_builtin` writes, an `Arc`
3493 /// for the numeric hook, and the JIT enable — per call. `fib(27)` makes
3494 /// 400k calls, so it built 400k VMs to run 23 ops each.
3495 ///
3496 /// Worse, the caller had to hand over an OWNED `Chunk`, so every call also
3497 /// deep-copied the function's whole compiled body: six `Vec`s, a `String`,
3498 /// and `sub_chunks` recursively. Keying the pool by chunk means a repeated
3499 /// call takes back the VM that already holds that body and copies nothing:
3500 /// `VM::reset` is handed the chunk the VM was already carrying.
3501 ///
3502 /// `VM::reset` keeps the builtin table, the hooks and the JIT setting, so a
3503 /// recycled VM needs none of that again. Each key holds a stack of VMs, and
3504 /// a nested (or recursive) call takes the next one, so a key grows to the
3505 /// deepest simultaneous entry into that function and no further.
3506 static VM_POOL: RefCell<rustc_hash::FxHashMap<u64, Vec<VM>>> =
3507 RefCell::new(rustc_hash::FxHashMap::default());
3508}
3509
3510/// An idle VM filed under `key`, if any.
3511fn take_pooled(key: u64) -> Option<VM> {
3512 VM_POOL.with(|p| p.borrow_mut().get_mut(&key).and_then(|v| v.pop()))
3513}
3514
3515/// File a finished VM under `key` for the next run to take.
3516fn put_pooled(key: u64, vm: VM) {
3517 VM_POOL.with(|p| p.borrow_mut().entry(key).or_default().push(vm));
3518}
3519
3520/// Take a VM ready to run `chunk` — recycled if one is idle, otherwise built
3521/// and fitted with the builtins and hooks a fresh VM needs.
3522fn acquire_vm(chunk: Chunk) -> VM {
3523 if let Some(mut vm) = take_pooled(0) {
3524 vm.reset(chunk);
3525 return vm;
3526 }
3527 let mut vm = VM::new(chunk);
3528 crate::builtins::install(&mut vm);
3529 vm.set_numeric_hook(std::sync::Arc::new(|op, a, b| {
3530 crate::builtins::numeric_hook(op, a, b)
3531 }));
3532 // Under `--dap` the tracing JIT would compile hot loops and skip the
3533 // per-statement `DBG_LINE` markers, so debug runs stay on the pure
3534 // interpreter. The `DBG_LINE` builtin fires the debugger line hook; the
3535 // extension seam mirrors pythonrs should the marker emission ever switch.
3536 // The mode is fixed before the first chunk runs, so a pooled VM can never
3537 // come back wearing the wrong one.
3538 if DEBUG_MODE.with(|d| d.get()) {
3539 vm.set_extension_handler(Box::new(|vm, id, _| {
3540 crate::dap::on_ext(vm, id);
3541 }));
3542 } else {
3543 vm.enable_tracing_jit();
3544 }
3545 vm
3546}
3547
3548/// Register every node-js builtin + the numeric hook on a VM, then run it.
3549///
3550/// For a chunk that runs once — a module body, an `eval` — there is nothing to
3551/// key a pool by, so this resets a spare VM with the caller's chunk. Anything
3552/// that runs repeatedly (a function body, a `try` block) goes through
3553/// [`run_chunk_keyed`] instead and never copies its chunk twice.
3554pub fn run_chunk_on(chunk: Chunk) -> Result<Value, String> {
3555 // Checked before the `VM` is built: `VM::new` + `install` are themselves
3556 // several KiB of frame, so a check after them could already have overflowed.
3557 if stack_exhausted() {
3558 return Err(stack_overflow_error());
3559 }
3560 finish_run(0, acquire_vm(chunk))
3561}
3562
3563/// Run the chunk filed under `key`, building it with `make` only if no VM is
3564/// already holding it. A recycled VM re-runs the chunk it kept, so a repeated
3565/// call copies no bytecode at all.
3566pub fn run_chunk_keyed(key: u64, make: impl FnOnce() -> Chunk) -> Result<Value, String> {
3567 if stack_exhausted() {
3568 return Err(stack_overflow_error());
3569 }
3570 let vm = match take_pooled(key) {
3571 Some(mut vm) => {
3572 // Hand the VM back the chunk it is already carrying: `reset` takes
3573 // an owned `Chunk`, and this is the one place where the owned chunk
3574 // costs nothing.
3575 let held = std::mem::take(&mut vm.chunk);
3576 vm.reset(held);
3577 vm
3578 }
3579 None => acquire_vm(make()),
3580 };
3581 finish_run(key, vm)
3582}
3583
3584/// Run a prepared VM to completion and file it back under `key`.
3585fn finish_run(key: u64, mut vm: VM) -> Result<Value, String> {
3586 let outcome = vm.run();
3587 let result = match outcome {
3588 _ if with_host(|h| h.error.is_some()) => {
3589 Err(with_host(|h| h.take_error()).expect("just checked"))
3590 }
3591 VMResult::Ok(v) => Ok(v),
3592 VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
3593 VMResult::Error(e) => Err(e),
3594 };
3595 put_pooled(key, vm);
3596 result
3597}
3598
3599/// Run `chunk` in the GLOBAL scope instead of the caller's.
3600///
3601/// `run_chunk_on` executes on whatever frame is current, so a nested run sees —
3602/// and can shadow — the *calling function's* locals. That is right for a direct
3603/// `eval`, and wrong for every other runtime-source construct: a `new Function`
3604/// body, an indirect `eval` and `vm.runInThisContext` are all specified to run
3605/// in the global scope (ECMA-262 19.2.1.1 `PerformEval` with a null
3606/// `strictCaller`/`direct` pair; `FunctionBody` is instantiated with the *global*
3607/// environment, 20.2.1.1.1 step 26). Measured against node v26.7.0,
3608/// `function outer(){ let loc = 42; return vm.runInThisContext('typeof loc'); }`
3609/// is `"undefined"` there and was `"number"` here.
3610///
3611/// A `var` the chunk itself declares lands in the top-level scope and persists,
3612/// so successive `vm.runInThisContext` calls share it.
3613pub fn run_chunk_in_global_scope(chunk: Chunk) -> Result<Value, String> {
3614 // An INDIRECT eval really is global code (19.2.1.1 step 6): its `var`s bind
3615 // to the global object, not to the entry module's wrapper scope. The flag
3616 // that keeps the entry script's own `var`s out of the globals map has to be
3617 // lifted for the duration, or `(0, eval)('var g = 1')` stopped reaching
3618 // `globalThis.g`.
3619 let prev_scope = with_host(|h| std::mem::take(&mut h.module_scope));
3620 let out = run_chunk_in_global_scope_inner(chunk);
3621 with_host(|h| h.module_scope = prev_scope);
3622 out
3623}
3624
3625fn run_chunk_in_global_scope_inner(chunk: Chunk) -> Result<Value, String> {
3626 let global_env = with_host(|h| h.global_env.clone());
3627 with_host(|h| {
3628 h.frames.push(Frame {
3629 env: global_env.clone(),
3630 base_env: global_env,
3631 this_obj: None,
3632 new_target: None,
3633 home_class: None,
3634 home_static: false,
3635 home_object: None,
3636 strict: false,
3637 line: 0,
3638 owner: None,
3639 is_module: true,
3640 this_state: ThisState::Plain,
3641 })
3642 });
3643 let r = run_chunk_on(chunk);
3644 with_host(|h| {
3645 h.frames.pop();
3646 });
3647 r
3648}
3649
3650/// Run the top-level program chunk, then drain the event loop (microtasks +
3651/// timers) until quiescent — matching Node, which keeps the process alive while
3652/// pending async work remains.
3653pub fn run_main(chunk: Chunk) -> Result<Value, String> {
3654 with_host(|h| h.module_scope = true);
3655 let r = run_chunk_on(chunk);
3656 with_host(|h| h.signal = None);
3657 if r.is_ok() {
3658 run_event_loop()?;
3659 finish_process_events()?;
3660 }
3661 r
3662}
3663
3664/// The shutdown sequence Node runs once the loop has drained on its own: fire
3665/// `beforeExit` (which MAY schedule more work, in which case the loop runs
3666/// again and `beforeExit` fires again), then fire `exit` exactly once.
3667///
3668/// Neither event fired at all before this existed, so `process.on('exit', …)`
3669/// was a registration with no delivery — a listener whose body printed was
3670/// silently dropped, and one that set `process.exitCode` could not affect the
3671/// status. Measured on node v26.7.0,
3672/// `process.on('exit', c => console.log('exit', c))` prints `exit 0`.
3673///
3674/// An explicit `process.exit()` never reaches here (it leaves the process from
3675/// inside the builtin), and neither does an uncaught exception — matching
3676/// Node, where `beforeExit` is skipped on both paths.
3677fn finish_process_events() -> Result<(), String> {
3678 // Bounded: a `beforeExit` listener that re-arms work every time would spin
3679 // forever, exactly as it does in Node, but a runaway here would hang a
3680 // parity run with no output, so it is capped and then treated as drained.
3681 for _ in 0..1000 {
3682 let code = with_host(|h| h.exit_code).unwrap_or(0);
3683 if !crate::stdlib::process::emit_before_exit(code)? {
3684 break;
3685 }
3686 let more =
3687 with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
3688 if !more {
3689 break;
3690 }
3691 run_event_loop()?;
3692 }
3693 let code = with_host(|h| h.exit_code).unwrap_or(0);
3694 crate::stdlib::process::emit_exit_event(code)
3695}
3696
3697// ── formatting ───────────────────────────────────────────────────────────────
3698
3699/// Format a JS number exactly as `Number.prototype.toString` does for the common
3700/// range (no exponential-notation threshold handling for very large/small).
3701pub fn fmt_number(f: f64) -> String {
3702 if f.is_nan() {
3703 return "NaN".into();
3704 }
3705 if f.is_infinite() {
3706 return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
3707 }
3708 if f == 0.0 {
3709 // Covers -0.0 too: (-0).toString() === "0".
3710 return "0".into();
3711 }
3712 if f < 0.0 {
3713 return format!("-{}", js_number_repr(-f));
3714 }
3715 js_number_repr(f)
3716}
3717
3718/// If `k` is an array-index property key, return its numeric value. Per
3719/// ECMAScript, a String property key `P` is an array index iff
3720/// `ToString(ToUint32(P)) === P` and `ToUint32(P) !== 2^32 - 1` — i.e. a
3721/// canonical decimal (no leading zeros, no sign) in the range `0..=2^32-2`.
3722pub fn array_index(k: &str) -> Option<u32> {
3723 if k.is_empty() {
3724 return None;
3725 }
3726 if k == "0" {
3727 return Some(0);
3728 }
3729 // A leading '0' (other than the lone "0" above) is non-canonical.
3730 if k.as_bytes()[0] == b'0' {
3731 return None;
3732 }
3733 if !k.bytes().all(|b| b.is_ascii_digit()) {
3734 return None;
3735 }
3736 match k.parse::<u64>() {
3737 // Array index must be < 2^32-1; u32::MAX == 2^32-1 is excluded.
3738 Ok(n) if n < u32::MAX as u64 => Some(n as u32),
3739 _ => None,
3740 }
3741}
3742
3743/// Compare two own-property keys for `OrdinaryOwnPropertyKeys` enumeration order:
3744/// integer-index keys sort ascending-numeric and precede all string keys; two
3745/// non-index keys compare `Equal` so a *stable* sort leaves them in insertion
3746/// order. (Symbols are stored as `@@…`/`#…` string keys and are non-index, so
3747/// they also fall into the stable-insertion-order tail.)
3748pub fn key_order_cmp(a: &str, b: &str) -> std::cmp::Ordering {
3749 use std::cmp::Ordering;
3750 match (array_index(a), array_index(b)) {
3751 (Some(x), Some(y)) => x.cmp(&y),
3752 (Some(_), None) => Ordering::Less,
3753 (None, Some(_)) => Ordering::Greater,
3754 (None, None) => Ordering::Equal,
3755 }
3756}
3757
3758/// Reorder an object's own-property map into `OrdinaryOwnPropertyKeys` order in
3759/// place: array-index keys ascending first, then the remaining keys in their
3760/// existing (insertion) order. A no-op unless at least one index key is present,
3761/// so the overwhelmingly common all-string-key object keeps its exact order and
3762/// pays nothing. `IndexMap::sort_by` is a stable sort.
3763pub fn canonicalize_own_keys(props: &mut IndexMap<String, Value>) {
3764 if props.keys().any(|k| array_index(k).is_some()) {
3765 props.sort_by(|ak, _, bk, _| key_order_cmp(ak, bk));
3766 }
3767}
3768
3769/// ECMAScript `Number::toString` layout for a positive, finite, nonzero value.
3770///
3771/// Rust's `Display`/`LowerExp` give the shortest round-trip decimal digits, but
3772/// NOT JavaScript's exponential-vs-fixed threshold: Rust prints `1e21` as
3773/// `1000000000000000000000` and `1e-7` as `0.0000001`, whereas JS prints `1e+21`
3774/// and `1e-7`. So we take the shortest digits from `{:e}` and re-lay them out per
3775/// the spec (steps 5–10 of Number::toString): `k` significant digits `s` with
3776/// decimal exponent `n` (value = s × 10^(n−k)); exponential form only when
3777/// `n > 21` or `n ≤ -6`.
3778fn js_number_repr(a: f64) -> String {
3779 // `{:e}` yields `d[.ddd]e<exp>` with the mantissa in [1, 10) and shortest
3780 // round-trip digits. Split it into the digit string `s` and exponent `E`.
3781 let sci = format!("{a:e}");
3782 let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
3783 let e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
3784 let s: String = mant.chars().filter(|c| *c != '.').collect();
3785 let k = s.len() as i32; // number of significant digits
3786 let n = e + 1; // value = s × 10^(n−k), 10^(k−1) ≤ s < 10^k
3787
3788 if k <= n && n <= 21 {
3789 // Integer with trailing zeros: all digits, then n−k zeros.
3790 let mut out = s;
3791 out.push_str(&"0".repeat((n - k) as usize));
3792 out
3793 } else if 0 < n && n <= 21 {
3794 // Decimal point inside the digit run: n digits, '.', the rest.
3795 format!("{}.{}", &s[..n as usize], &s[n as usize..])
3796 } else if -6 < n && n <= 0 {
3797 // Leading "0." then (−n) zeros then all digits.
3798 format!("0.{}{}", "0".repeat((-n) as usize), s)
3799 } else {
3800 // Exponential form. Exponent digit is n−1, always signed.
3801 let exp = n - 1;
3802 let sign = if exp >= 0 { '+' } else { '-' };
3803 let mag = exp.abs();
3804 if k == 1 {
3805 format!("{s}e{sign}{mag}")
3806 } else {
3807 format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
3808 }
3809 }
3810}
3811
3812impl JsHost {
3813 /// The `typeof` string for `v`.
3814 pub fn type_of(&self, v: &Value) -> &'static str {
3815 match v {
3816 Value::Undef => "undefined",
3817 Value::Bool(_) => "boolean",
3818 Value::Int(_) | Value::Float(_) => "number",
3819 Value::Str(_) => "string",
3820 Value::Obj(_) => match self.get(v) {
3821 Some(JsObj::Str(_)) => "string",
3822 // 10.5's `[[Call]]` slot exists on a proxy exactly when its
3823 // target is callable, so `typeof` classifies by the target —
3824 // `typeof new Proxy(function(){}, {})` is `'function'`. The walk
3825 // is bounded: a proxy of a proxy defers again.
3826 Some(JsObj::Proxy { target, .. }) => {
3827 let mut cur = target;
3828 for _ in 0..100 {
3829 match self.get(cur) {
3830 Some(JsObj::Proxy { target: t, .. }) => cur = t,
3831 _ => break,
3832 }
3833 }
3834 if is_callable(self, cur) {
3835 "function"
3836 } else {
3837 "object"
3838 }
3839 }
3840 Some(JsObj::Func(_))
3841 | Some(JsObj::BoundMethod { .. })
3842 | Some(JsObj::BoundFunc { .. })
3843 | Some(JsObj::Class(_)) => "function",
3844 // A Builtin is a callable (`Array`, `parseInt`, `Math.floor`) —
3845 // `typeof === "function"` — EXCEPT the non-callable namespace
3846 // objects (`Math`, `JSON`, `require('fs')`, …) which are "object".
3847 Some(JsObj::Builtin(n)) => {
3848 if builtin_is_callable(n) {
3849 "function"
3850 } else {
3851 "object"
3852 }
3853 }
3854 Some(JsObj::Symbol { .. }) => "symbol",
3855 Some(JsObj::BigInt(_)) => "bigint",
3856 _ => "object", // arrays, objects, null, Map/Set, generators
3857 },
3858 _ => "object",
3859 }
3860 }
3861
3862 /// JS truthiness: false / 0 / -0 / NaN / "" / null / undefined are falsy.
3863 pub fn truthy(&self, v: &Value) -> bool {
3864 match v {
3865 Value::Undef => false,
3866 Value::Bool(b) => *b,
3867 Value::Int(n) => *n != 0,
3868 Value::Float(f) => *f != 0.0 && !f.is_nan(),
3869 Value::Str(s) => !s.is_empty(),
3870 Value::Obj(_) => match self.get(v) {
3871 Some(JsObj::Str(s)) => !s.is_empty(),
3872 Some(JsObj::Null) => false,
3873 Some(JsObj::BigInt(b)) => !num_traits::Zero::is_zero(b),
3874 _ => true, // arrays, objects, functions
3875 },
3876 _ => true,
3877 }
3878 }
3879
3880 /// Coerce to a number (`ToNumber`): the arithmetic-context conversion.
3881 pub fn to_number(&self, v: &Value) -> f64 {
3882 match v {
3883 Value::Undef => f64::NAN,
3884 Value::Bool(b) => {
3885 if *b {
3886 1.0
3887 } else {
3888 0.0
3889 }
3890 }
3891 Value::Int(n) => *n as f64,
3892 Value::Float(f) => *f,
3893 Value::Str(s) => str_to_number(s),
3894 Value::Obj(_) => match self.get(v) {
3895 Some(JsObj::Str(s)) => str_to_number(s),
3896 Some(JsObj::Null) => 0.0,
3897 Some(JsObj::BigInt(b)) => bigint_to_f64(b),
3898 Some(JsObj::Array(items)) => {
3899 // [] -> 0, [x] -> ToNumber(x), else NaN.
3900 if items.is_empty() {
3901 0.0
3902 } else if items.len() == 1 {
3903 self.to_number(&items[0])
3904 } else {
3905 f64::NAN
3906 }
3907 }
3908 _ => f64::NAN,
3909 },
3910 _ => f64::NAN,
3911 }
3912 }
3913
3914 /// `String(v)` — the string-coercion form (raw, unquoted).
3915 pub fn str_of(&self, v: &Value) -> String {
3916 match v {
3917 Value::Undef => "undefined".into(),
3918 Value::Bool(b) => if *b { "true" } else { "false" }.into(),
3919 Value::Int(n) => n.to_string(),
3920 Value::Float(f) => fmt_number(*f),
3921 Value::Str(s) => (**s).clone(),
3922 Value::Obj(_) => match self.get(v) {
3923 Some(JsObj::Str(s)) => s.clone(),
3924 Some(JsObj::Null) => "null".into(),
3925 Some(JsObj::BigInt(b)) => b.to_string(),
3926 Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
3927 Some(JsObj::Array(items)) => {
3928 // Array.prototype.toString: comma-join, null/undefined -> "".
3929 // Guarded by the JoinStack (see `join_stack_push`) so a
3930 // self-referential array yields "" instead of recursing until
3931 // the native stack aborts the process.
3932 if !join_stack_push(v) {
3933 return String::new();
3934 }
3935 let parts: Vec<String> = items
3936 .iter()
3937 .map(|x| match x {
3938 Value::Undef => String::new(),
3939 _ if self.is_null(x) => String::new(),
3940 _ => self.str_of(x),
3941 })
3942 .collect();
3943 join_stack_pop();
3944 parts.join(",")
3945 }
3946 Some(JsObj::Object(props)) => {
3947 // A native `Buffer` stringifies to its decoded (utf-8)
3948 // contents, matching `buf.toString()` — needed for `'' + buf`,
3949 // template interpolation, and `data += chunk` (the pattern
3950 // Express/body-parser use to read a request body).
3951 if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Buffer") {
3952 let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
3953 Some(JsObj::Array(items)) => {
3954 items.iter().map(|x| self.to_number(x) as u8).collect()
3955 }
3956 _ => Vec::new(),
3957 };
3958 String::from_utf8_lossy(&bytes).into_owned()
3959 } else if let Some(s) = self.error_to_string(v) {
3960 s
3961 } else {
3962 "[object Object]".into()
3963 }
3964 }
3965 Some(JsObj::Func(f)) => {
3966 // A function built from runtime source (`new Function`,
3967 // `vm.compileFunction`) retains the exact text V8 synthesizes
3968 // for it, so `Function.prototype.toString` reports what Node
3969 // reports. Every other function slices its span out of the
3970 // script it was parsed from; only one whose program kept no
3971 // text (an AOT image, a `rust { }` desugared file) falls back
3972 // to the placeholder.
3973 if let Some(src) = self.fn_prop(v, "@@source") {
3974 return self.str_of(&src);
3975 }
3976 if let Some(text) = self.func_source(f.def_id) {
3977 return text.to_string();
3978 }
3979 let name = self
3980 .funcs
3981 .get(f.def_id)
3982 .map(|d| d.name.clone())
3983 .unwrap_or_default();
3984 format!("function {name}() {{ [code] }}")
3985 }
3986 // The native-code form names the FUNCTION, not its key:
3987 // `String(Math.max)` is `function max() { [native code] }`.
3988 Some(JsObj::Builtin(n)) => {
3989 // The `console` methods are the exception node itself makes:
3990 // each is a wrapper, so `String(console.log)` is the
3991 // ANONYMOUS native-code form even though `console.log.name`
3992 // is `log`. Measured on v26.8.1.
3993 if n.starts_with("console.") {
3994 "function () { [native code] }".into()
3995 } else if let Some(accessor) = crate::builtins::proto_getter_name(n) {
3996 // An accessor half names itself `get size` / `set
3997 // arguments`, which `builtin_name` cannot build because
3998 // it returns a borrowed `&str`.
3999 format!("function {accessor}() {{ [native code] }}")
4000 } else {
4001 format!(
4002 "function {}() {{ [native code] }}",
4003 crate::builtins::builtin_name(n)
4004 )
4005 }
4006 }
4007 // A method read off an instance names itself the same way the
4008 // prototype method it resolves to does: `String([].slice)` is
4009 // `function slice() { [native code] }`.
4010 Some(JsObj::BoundMethod { name, .. }) => {
4011 format!("function {name}() {{ [native code] }}")
4012 }
4013 Some(JsObj::BoundFunc { .. }) => "function () { [native code] }".into(),
4014 // `Function.prototype.toString` refuses to expose a proxy's
4015 // target: V8 reports the native-code form for a proxy of ANY
4016 // callable, so `String(new Proxy(function f(){}, {}))` is
4017 // `function () { [native code] }`, not `f`'s source.
4018 Some(JsObj::Proxy { .. }) if is_callable(self, v) => {
4019 "function () { [native code] }".into()
4020 }
4021 Some(JsObj::Class(c)) => match c.source_def.and_then(|d| self.func_source(d)) {
4022 Some(text) => text.to_string(),
4023 None => format!("class {} {{ }}", c.name),
4024 },
4025 Some(JsObj::Symbol { desc, .. }) => {
4026 // `String(sym)` is allowed (unlike implicit coercion) and yields
4027 // `Symbol(desc)`.
4028 match desc {
4029 Some(d) => format!("Symbol({d})"),
4030 None => "Symbol()".into(),
4031 }
4032 }
4033 _ => "[object Object]".into(),
4034 },
4035 _ => "[object Object]".into(),
4036 }
4037 }
4038
4039 /// The `Symbol.toStringTag` string `util.inspect` renders as a `[Tag]`
4040 /// prefix. V8 suppresses the tag when it is an OWN ENUMERABLE property,
4041 /// because it is then already listed as a `Symbol(Symbol.toStringTag): …`
4042 /// entry and showing it twice would be wrong.
4043 ///
4044 /// Only a DATA property is seen. A tag supplied by a prototype getter
4045 /// (`class C { get [Symbol.toStringTag]() { … } }`) would need a JS call,
4046 /// which cannot run under the host borrow `inspect` holds — such an object
4047 /// prints without the prefix.
4048 /// `[String: 'ab']` / `[Number: 1]` / `[Boolean: false]` — how node renders
4049 /// a primitive wrapper, distinguishing it from the bare primitive.
4050 fn inspect_wrapper(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Option<String> {
4051 let prim = match self.get(v) {
4052 Some(JsObj::Object(p)) => p.get("@@primitive").cloned()?,
4053 _ => return None,
4054 };
4055 let ctor = match &prim {
4056 Value::Bool(_) => "Boolean",
4057 Value::Int(_) | Value::Float(_) => "Number",
4058 // BigInt and Symbol primitives live on the heap; their boxes are
4059 // `[BigInt: 1n]` and `[Symbol: Symbol(s)]`.
4060 _ => match self.get(&prim) {
4061 Some(JsObj::BigInt(_)) => "BigInt",
4062 Some(JsObj::Symbol { .. }) => "Symbol",
4063 _ => "String",
4064 },
4065 };
4066 let head = format!("[{ctor}: {}]", self.inspect_lvl(&prim, indent, st));
4067 // Extra own properties still print, as `[String: 'ab'] { tag: 1 }`. The
4068 // boxed characters are NOT extras — node hides the index properties of
4069 // a String wrapper, showing only what was added to it.
4070 let width = if ctor == "String" {
4071 self.str_of(&prim).chars().count()
4072 } else {
4073 0
4074 };
4075 let extras: Vec<String> = match self.get(v) {
4076 Some(JsObj::Object(p)) => p
4077 .iter()
4078 .filter(|(k, _)| {
4079 !k.starts_with("@@")
4080 && !k.starts_with('#')
4081 && self.prop_attrs(v, k).enumerable
4082 && !k.parse::<usize>().is_ok_and(|i| i < width)
4083 })
4084 .map(|(k, val)| {
4085 format!("{}: {}", fmt_key(k), self.inspect_lvl(val, indent + 2, st))
4086 })
4087 .collect(),
4088 _ => Vec::new(),
4089 };
4090 if extras.is_empty() {
4091 return Some(head);
4092 }
4093 Some(self.render_object(&extras, &format!("{head} "), indent, st))
4094 }
4095
4096 /// The `key: value` parts for own properties a script attached to an exotic
4097 /// whose contents are internal slots — `new Map([['k',1]])` with `m.x = 5`
4098 /// prints `Map(1) { 'k' => 1, x: 5 }`.
4099 fn side_table_parts(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Vec<String> {
4100 self.fn_prop_keys(v)
4101 .into_iter()
4102 .filter(|k| {
4103 !k.starts_with("@@")
4104 && !k.starts_with('#')
4105 && !is_symbol_key(k)
4106 && self.prop_attrs(v, k).enumerable
4107 })
4108 .map(|k| {
4109 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4110 format!(
4111 "{}: {}",
4112 fmt_key(&k),
4113 self.inspect_lvl(&val, indent + 2, st)
4114 )
4115 })
4116 .collect()
4117 }
4118
4119 fn inspect_tag(&self, v: &Value) -> Option<String> {
4120 let own = matches!(self.get(v), Some(JsObj::Object(p)) if p.contains_key("@@toStringTag"));
4121 if own && self.prop_attrs(v, "@@toStringTag").enumerable {
4122 return None;
4123 }
4124 let t = lookup_chain(self, v, "@@toStringTag")?;
4125 self.as_str(&t)
4126 }
4127
4128 /// `console.log`-style rendering of a top-level argument: bare strings print
4129 /// raw; everything else uses `inspect`.
4130 pub fn console_format(&self, v: &Value) -> String {
4131 match v {
4132 Value::Str(_) => self.str_of(v),
4133 Value::Obj(_) if matches!(self.get(v), Some(JsObj::Str(_))) => self.str_of(v),
4134 _ => self.inspect(v),
4135 }
4136 }
4137
4138 /// `util.inspect`-style rendering (nested; strings quoted).
4139 pub fn inspect(&self, v: &Value) -> String {
4140 self.inspect_lvl(v, 0, &mut InspectCycles::default())
4141 }
4142
4143 /// `util.inspect` at a given indentation level, with the cycle guard applied
4144 /// around the object cases.
4145 ///
4146 /// A value already being rendered further up the chain is a CYCLE, and Node
4147 /// marks both ends of it: the back-edge prints `[Circular *N]` and the
4148 /// object it points back at is prefixed `<ref *N>`. Without this the walk
4149 /// only stopped when the depth limit turned the back-edge into `[Object]`,
4150 /// so `const c={a:1}; c.c=c` printed the misleading
4151 /// `{ a: 1, c: { a: 1, c: { a: 1, c: [Object] } } }` instead of
4152 /// `<ref *1> { a: 1, c: [Circular *1] }`.
4153 ///
4154 /// The `*N` id is only assigned when the back-edge is reached, i.e. while
4155 /// the target's own children are being rendered — so the prefix can only be
4156 /// decided after `inspect_value` returns.
4157 /// Whether `v` renders as a LEAF — a finished string produced without
4158 /// recursing into any child.
4159 ///
4160 /// Node assigns `ctx.currentDepth = recurseTimes` in `formatRaw`, but only
4161 /// after the early returns for the shapes that answer immediately: a bare
4162 /// Date is its ISO string, a regex is its literal, an empty container is its
4163 /// braces, and a Buffer is whatever its `[util.inspect.custom]` says. None of
4164 /// those record a depth, so a group containing one is not pushed over the
4165 /// `compact` threshold by it — `util.inspect([new Date(0), null], { compact:
4166 /// 1 })` stays on one line. Charging them a level broke exactly those groups.
4167 fn renders_without_expanding(&self, v: &Value) -> bool {
4168 let plain_props = |p: &IndexMap<String, Value>| {
4169 p.keys().all(|k| k.starts_with("@@") || k.starts_with('#'))
4170 };
4171 match self.get(v) {
4172 // A regex never recurses, with or without its hidden `lastIndex`.
4173 Some(JsObj::RegExp(_)) => true,
4174 Some(JsObj::Map { entries, .. }) => entries.is_empty(),
4175 Some(JsObj::Set { entries, .. }) => entries.is_empty(),
4176 Some(JsObj::Array(items)) => items.is_empty() && self.own_symbol_entries(v).is_empty(),
4177 Some(JsObj::Object(p)) => match p.get("@@native").map(|t| self.str_of(t)).as_deref() {
4178 Some("Buffer") => inspect_custom(),
4179 // Own properties added to a Date DO get expanded after it.
4180 Some("Date") => plain_props(p),
4181 Some(_) => false,
4182 None => plain_props(p) && self.own_symbol_entries(v).is_empty(),
4183 },
4184 _ => false,
4185 }
4186 }
4187
4188 fn inspect_lvl(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4189 if !matches!(v, Value::Obj(_)) {
4190 return self.inspect_value(v, indent, st);
4191 }
4192 if st.seen.iter().any(|p| self.strict_eq(p, v)) {
4193 return format!("[Circular *{}]", st.mark(self, v));
4194 }
4195 st.seen.push(v.clone());
4196 // Node ASSIGNS `ctx.currentDepth = recurseTimes` on entry to each value
4197 // it expands — not a running maximum — so after the children have been
4198 // rendered it holds the depth of the last chain below this group, which
4199 // is what `reduceToSingleString` compares. A value the depth limit
4200 // stubs out as `[Object]` is never expanded and must not count, or an
4201 // object whose deepest level was elided would break where node joins.
4202 // Only a value node actually EXPANDS advances the depth. A string,
4203 // symbol or bigint is a JS primitive that this host happens to store on
4204 // the heap, so it reaches here as `Value::Obj` where an unboxed number
4205 // returns above — and counting it as a level made any group holding one
4206 // look deeper than it was. Under `compact: 1` that is the difference
4207 // between node's `Map(2) { 'k2' => 8, 'j' => 5 }` and breaking the same
4208 // Map across four lines, because its string KEYS were being charged a
4209 // nesting level.
4210 if indent as i64 <= inspect_indent_limit()
4211 && !is_primitive(self, v)
4212 && !self.renders_without_expanding(v)
4213 {
4214 st.deepest = indent;
4215 }
4216 let body = self.inspect_value(v, indent, st);
4217 st.seen.pop();
4218 match st.id_of(self, v) {
4219 Some(id) => format!("<ref *{id}> {body}"),
4220 None => body,
4221 }
4222 }
4223
4224 /// The rendering itself, once `inspect_lvl` has established that `v` is not
4225 /// a back-edge into an object already on the stack.
4226 fn inspect_value(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4227 if let Some(s) = self.inspect_wrapper(v, indent, st) {
4228 return s;
4229 }
4230 match v {
4231 Value::Undef => "undefined".into(),
4232 Value::Bool(b) => if *b { "true" } else { "false" }.into(),
4233 Value::Int(n) => n.to_string(),
4234 // `util.inspect` distinguishes negative zero; `String(-0)` does not.
4235 Value::Float(f) if *f == 0.0 && f.is_sign_negative() => "-0".into(),
4236 Value::Float(f) => fmt_number(*f),
4237 Value::Str(s) => quote_str(s),
4238 Value::Obj(_) => match self.get(v) {
4239 Some(JsObj::Str(s)) => quote_str(s),
4240 Some(JsObj::Null) => "null".into(),
4241 // `util.inspect` renders a bigint with the `n` suffix, a regex bare.
4242 Some(JsObj::BigInt(b)) => format!("{b}n"),
4243 // `lastIndex` is a non-enumerable own property of every regex,
4244 // so `showHidden` (and therefore `%o`) appends it:
4245 // `/x/g { [lastIndex]: 0 }`.
4246 Some(JsObj::RegExp(r)) => {
4247 let body = format!("/{}/{}", r.source, r.flags);
4248 if inspect_show_hidden() {
4249 format!("{body} {{ [lastIndex]: {} }}", r.last_index.get())
4250 } else {
4251 body
4252 }
4253 }
4254 // `util.inspect` on node v26.7.0 renders a proxy as
4255 // `Proxy(<target>)` — the target's own rendering, wrapped. It
4256 // deliberately does NOT run the handler's traps, so this stays a
4257 // pure `&self` read like every other inspect arm.
4258 Some(JsObj::Proxy { target, .. }) => {
4259 format!("Proxy({})", self.inspect_lvl(target, indent, st))
4260 }
4261 // `arguments` is backed by an Array but is an ordinary-shaped
4262 // exotic to util.inspect: node prints its indices as quoted
4263 // keys under the `[Arguments]` tag, `[Arguments] { '0': 1 }`.
4264 Some(JsObj::Array(items)) if crate::builtins::is_arguments_h(self, v) => {
4265 if indent as i64 > inspect_indent_limit() {
4266 return "[Arguments]".into();
4267 }
4268 let mut inner: Vec<String> = items
4269 .iter()
4270 .enumerate()
4271 .map(|(i, x)| format!("'{i}': {}", self.inspect_lvl(x, indent + 2, st)))
4272 .collect();
4273 for k in self.fn_prop_keys(v) {
4274 if k.starts_with("@@") || !self.prop_attrs(v, &k).enumerable {
4275 continue;
4276 }
4277 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4278 inner.push(format!(
4279 "{}: {}",
4280 fmt_key(&k),
4281 self.inspect_lvl(&val, indent + 2, st)
4282 ));
4283 }
4284 if inner.is_empty() {
4285 return "[Arguments] {}".into();
4286 }
4287 self.render_object(&inner, "[Arguments] ", indent, st)
4288 }
4289 Some(JsObj::Array(items)) => {
4290 // Own enumerable non-index string props (e.g. a `str.match(re)`
4291 // result's `index`/`input`/`groups`, or a user-assigned
4292 // `arr.foo`) render after the elements, as `key: value`.
4293 let prop_keys: Vec<String> = self
4294 .fn_prop_keys(v)
4295 .into_iter()
4296 .filter(|k| {
4297 !k.starts_with("@@")
4298 && !k.starts_with('#')
4299 && self.prop_attrs(v, k).enumerable
4300 })
4301 .collect();
4302 // An own enumerable SYMBOL-keyed property renders after the
4303 // string keys as `Symbol(desc): value`, as it does on an
4304 // object receiver.
4305 // An instance of an Array SUBCLASS leads with its
4306 // constructor and length, `Bar(2) [ 1, 2 ]`, as node's
4307 // `getPrefix` does for any non-`Array` constructor.
4308 let sub = match self.proto_of(v) {
4309 Some(_) => self.ctor_name(v),
4310 None => String::new(),
4311 };
4312 let base = if sub.is_empty() || sub == "Array" {
4313 String::new()
4314 } else {
4315 format!("{sub}({}) ", items.len())
4316 };
4317 let sym_entries = self.own_symbol_entries(v);
4318 // Under `showHidden` even an empty array has something to
4319 // show — node prints `[ [length]: 0 ]`, not `[]`.
4320 if items.is_empty()
4321 && prop_keys.is_empty()
4322 && sym_entries.is_empty()
4323 && !inspect_show_hidden()
4324 {
4325 return format!("{base}[]");
4326 }
4327 // Node's default inspect depth is 2 (root = depth 0); deeper
4328 // nesting collapses to `[Array]`. indent grows by 2 per level.
4329 if indent as i64 > inspect_indent_limit() {
4330 return "[Array]".into();
4331 }
4332 // `util.inspect`'s `maxArrayLength` (default 100): only the
4333 // first 100 elements are formatted, and the rest collapse to
4334 // a `... N more items` entry. Without the cap a 120-element
4335 // array printed all 120 — and, because the grid column width
4336 // is computed from what is SHOWN, every column was also one
4337 // character wider than node's.
4338 // A SPARSE array takes node's `formatSpecialArray` path: an
4339 // elided run renders as `<N empty items>` rather than as the
4340 // `undefined` it reads back as.
4341 let (mut inner, has_tail) = if self.has_holes(v) {
4342 self.inspect_sparse(v, items, indent, st)
4343 } else {
4344 let shown = items.len().min(inspect_max_array_length());
4345 let mut inner: Vec<String> = items[..shown]
4346 .iter()
4347 .map(|x| self.inspect_lvl(x, indent + 2, st))
4348 .collect();
4349 let remaining = items.len() - shown;
4350 if remaining > 0 {
4351 let unit = if remaining == 1 { "item" } else { "items" };
4352 inner.push(format!("... {remaining} more {unit}"));
4353 }
4354 (inner, remaining > 0)
4355 };
4356 // `showHidden` exposes the non-enumerable `length`, which an
4357 // array always has. It sorts BEFORE any own property node
4358 // shows (`[ 1, [length]: 1, x: 2 ]`) and, being an entry
4359 // rather than an element, it also turns the column grid off —
4360 // which is why a ten-element array under `showHidden` prints
4361 // on one line rather than as a grid.
4362 let show_hidden = inspect_show_hidden();
4363 if show_hidden {
4364 inner.push(format!("[length]: {}", items.len()));
4365 }
4366 let has_props = show_hidden || !prop_keys.is_empty() || !sym_entries.is_empty();
4367 for k in &prop_keys {
4368 let val = self.fn_prop(v, k).unwrap_or(Value::Undef);
4369 inner.push(format!(
4370 "{}: {}",
4371 fmt_key(k),
4372 self.inspect_lvl(&val, indent + 2, st)
4373 ));
4374 }
4375 for (k, val) in &sym_entries {
4376 let label = match self.symbol_of_key(k) {
4377 Some(s) => self.inspect(&s),
4378 None => continue,
4379 };
4380 inner.push(format!(
4381 "{label}: {}",
4382 self.inspect_lvl(val, indent + 2, st)
4383 ));
4384 }
4385 self.render_array(
4386 &inner,
4387 items,
4388 indent,
4389 ArrayLayout {
4390 has_props,
4391 has_tail,
4392 base: &base,
4393 },
4394 st,
4395 )
4396 }
4397 // `URLSearchParams` renders its pairs, not its slots:
4398 // `URLSearchParams { 'a' => '1', 'b' => '2' }`. Keys repeat,
4399 // which is why it is a pair list rather than a Map rendering.
4400 Some(JsObj::Object(props))
4401 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4402 == Some("URLSearchParams") =>
4403 {
4404 let pairs: Vec<Value> = match props.get("@@pairs").and_then(|a| self.get(a)) {
4405 Some(JsObj::Array(items)) => items.clone(),
4406 _ => Vec::new(),
4407 };
4408 if pairs.is_empty() {
4409 return "URLSearchParams {}".into();
4410 }
4411 let inner: Vec<String> = pairs
4412 .iter()
4413 .filter_map(|kv| match self.get(kv) {
4414 Some(JsObj::Array(p)) if p.len() == 2 => Some(format!(
4415 "{} => {}",
4416 self.inspect_lvl(&p[0], indent + 2, st),
4417 self.inspect_lvl(&p[1], indent + 2, st)
4418 )),
4419 _ => None,
4420 })
4421 .collect();
4422 self.render_object(&inner, "URLSearchParams ", indent, st)
4423 }
4424 // A typed array renders as `Uint8Array(3) [ 1, 2, 3 ]` — its
4425 // constructor and length, then the elements laid out exactly as
4426 // an array's. Without this it fell through to the generic object
4427 // arm and printed the `{ length, byteLength, byteOffset,
4428 // BYTES_PER_ELEMENT }` bookkeeping instead of the CONTENTS,
4429 // which is the whole reason anyone logs one.
4430 Some(JsObj::Object(props))
4431 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4432 == Some("TypedArray") =>
4433 {
4434 let kind = props
4435 .get("@@kind")
4436 .map(|k| self.str_of(k))
4437 .unwrap_or_else(|| "TypedArray".into());
4438 // Rendered as STRINGS: a 64-bit view's elements are BigInts,
4439 // which this shared borrow cannot allocate as values.
4440 let elems = crate::stdlib::typedarray::elems_display(self, v);
4441 // The grid layout sizes its columns from the VALUES; a
4442 // 64-bit view's come back as `undefined` (no allocation is
4443 // possible here), which only affects column padding.
4444 let vals = crate::stdlib::typedarray::elems_with_host(self, v);
4445 let base = format!("{kind}({}) ", elems.len());
4446 if indent as i64 > inspect_indent_limit() {
4447 return format!("[{kind}]");
4448 }
4449 let shown = elems.len().min(inspect_max_array_length());
4450 let mut inner: Vec<String> = elems[..shown].to_vec();
4451 let remaining = elems.len() - shown;
4452 if remaining > 0 {
4453 let unit = if remaining == 1 { "item" } else { "items" };
4454 inner.push(format!("... {remaining} more {unit}"));
4455 }
4456 // A view's whole identity — its element width, its window
4457 // onto the backing store, and the store itself — is
4458 // non-enumerable, so `showHidden` is the only way to see it.
4459 // `util.format('%o', view)` goes through here, since `%o`
4460 // implies `showHidden`.
4461 let show_hidden = inspect_show_hidden();
4462 if show_hidden {
4463 let bpe = crate::stdlib::typedarray::bytes_per_element(&kind);
4464 let byte_offset = props
4465 .get("byteOffset")
4466 .map(|x| self.to_number(x))
4467 .unwrap_or(0.0);
4468 inner.push(format!("[BYTES_PER_ELEMENT]: {bpe}"));
4469 inner.push(format!("[length]: {}", elems.len()));
4470 inner.push(format!("[byteLength]: {}", elems.len() * bpe));
4471 inner.push(format!("[byteOffset]: {}", fmt_number(byte_offset)));
4472 // An ArrayBuffer reached AS a view's backing store is
4473 // rendered by node WITHOUT its contents — just
4474 // `ArrayBuffer { [byteLength]: N }` — even though the
4475 // same buffer inspected on its own leads with
4476 // `[Uint8Contents]`. Recursing through the normal
4477 // ArrayBuffer branch therefore printed the bytes twice,
4478 // once as the view's elements and again as the store's.
4479 let buf_len = props
4480 .get("@@buffer")
4481 .and_then(|b| self.get(b))
4482 .and_then(|o| match o {
4483 JsObj::Object(bp) => bp.get("@@bytes").cloned(),
4484 _ => None,
4485 })
4486 .and_then(|b| {
4487 self.get(&b).map(|o| match o {
4488 JsObj::Array(items) => items.len(),
4489 _ => 0,
4490 })
4491 })
4492 .unwrap_or(0);
4493 inner.push(format!(
4494 "[buffer]: ArrayBuffer {{ [byteLength]: {buf_len} }}"
4495 ));
4496 }
4497 self.render_array(
4498 &inner,
4499 &vals,
4500 indent,
4501 ArrayLayout {
4502 has_props: show_hidden,
4503 has_tail: remaining > 0,
4504 base: &base,
4505 },
4506 st,
4507 )
4508 }
4509 // An `ArrayBuffer` renders its CONTENTS, which is the only way
4510 // to see them — it exposes no indices of its own:
4511 // `ArrayBuffer { [Uint8Contents]: <00 01>, [byteLength]: 2 }`.
4512 Some(JsObj::Object(props))
4513 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4514 == Some("ArrayBuffer") =>
4515 {
4516 let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4517 Some(JsObj::Array(items)) => {
4518 items.iter().map(|x| self.to_number(x) as u8).collect()
4519 }
4520 _ => Vec::new(),
4521 };
4522 let hex: Vec<String> = bytes.iter().map(|b| format!("{b:02x}")).collect();
4523 let mut parts = vec![
4524 format!("[Uint8Contents]: <{}>", hex.join(" ")),
4525 format!("[byteLength]: {}", bytes.len()),
4526 ];
4527 if props.contains_key("@@maxByteLength") {
4528 let max = props
4529 .get("@@maxByteLength")
4530 .map(|m| self.to_number(m))
4531 .unwrap_or(0.0);
4532 parts.insert(1, format!("maxByteLength: {}", fmt_number(max)));
4533 }
4534 self.render_object(&parts, "ArrayBuffer ", indent, st)
4535 }
4536 // A `Date` renders as its ISO-8601 form. Its time value lives in
4537 // the internal `@@ms` slot, which the generic object branch below
4538 // does not show, so without this arm every Date printed as `{}` —
4539 // including through `console.log(d)`, inside arrays, objects and
4540 // Maps, and in an `assert` diff.
4541 Some(JsObj::Object(props))
4542 if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Date") =>
4543 {
4544 let base = crate::stdlib::date::inspect_with_host(self, v);
4545 // Own properties added to a Date follow the date itself, the
4546 // way node appends them: `2020-01-01T00:00:00.000Z { x: 1 }`.
4547 let extra = self.side_table_parts(v, indent, st);
4548 let mut inner: Vec<String> = props
4549 .iter()
4550 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
4551 .map(|(k, val)| format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)))
4552 .collect();
4553 inner.extend(extra);
4554 if inner.is_empty() {
4555 return base;
4556 }
4557 self.render_object(&inner, &format!("{base} "), indent, st)
4558 }
4559 // A `Buffer` renders as `<Buffer 01 02 03>` — hex bytes, capped
4560 // at 50 with a `... N more byte(s)` tail, exactly as
4561 // `util.inspect` does. Without this a `console.log(buf)` (the
4562 // single most common thing anyone does with a Buffer) printed
4563 // the internal `{ length, byteLength, … }` bookkeeping.
4564 Some(JsObj::Object(props))
4565 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4566 == Some("Buffer") =>
4567 {
4568 let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4569 Some(JsObj::Array(items)) => {
4570 items.iter().map(|x| self.to_number(x) as u8).collect()
4571 }
4572 _ => Vec::new(),
4573 };
4574 // `<Buffer …>` is Buffer's `[util.inspect.custom]` hook, not
4575 // the shape of the object. Under `customInspect: false` node
4576 // does not call that hook and falls back to the generic
4577 // byte-view rendering — which is what an `assert` diff shows,
4578 // since assert inspects with the hook disabled so that a
4579 // failure names the differing BYTE rather than two opaque hex
4580 // blobs. The constructor is `Buffer` while the brand is still
4581 // `Uint8Array`, so node prints both.
4582 if !inspect_custom() {
4583 let base = format!("Buffer({}) [Uint8Array] ", bytes.len());
4584 if indent as i64 > inspect_indent_limit() {
4585 return "[Buffer [Uint8Array]]".into();
4586 }
4587 let shown = bytes.len().min(inspect_max_array_length());
4588 let mut inner: Vec<String> =
4589 bytes[..shown].iter().map(|b| b.to_string()).collect();
4590 let vals: Vec<Value> = bytes[..shown]
4591 .iter()
4592 .map(|b| Value::Float(*b as f64))
4593 .collect();
4594 let remaining = bytes.len() - shown;
4595 if remaining > 0 {
4596 let unit = if remaining == 1 { "item" } else { "items" };
4597 inner.push(format!("... {remaining} more {unit}"));
4598 }
4599 return self.render_array(
4600 &inner,
4601 &vals,
4602 indent,
4603 ArrayLayout {
4604 has_props: false,
4605 has_tail: remaining > 0,
4606 base: &base,
4607 },
4608 st,
4609 );
4610 }
4611 const MAX: usize = 50;
4612 let shown: Vec<String> =
4613 bytes.iter().take(MAX).map(|b| format!("{b:02x}")).collect();
4614 let mut out = format!("<Buffer {}", shown.join(" "));
4615 if bytes.len() > MAX {
4616 let more = bytes.len() - MAX;
4617 let unit = if more == 1 { "byte" } else { "bytes" };
4618 out.push_str(&format!(" ... {more} more {unit}"));
4619 }
4620 out.push('>');
4621 out
4622 }
4623 // An Error inspects as its `.stack` — never as an object literal
4624 // exposing the internal `message`/`stack` slots. Any own property
4625 // a script added beyond those follows in braces, as V8 renders
4626 // it: `Error: x\n at … { code: 'C' }`.
4627 Some(JsObj::Object(_)) if self.error_to_string(v).is_some() => {
4628 let mut stack = lookup_chain(self, v, "stack")
4629 .map(|s| self.str_of(&s))
4630 .unwrap_or_else(|| self.error_to_string(v).unwrap_or_default());
4631 // A `DOMException` prints its CLASS and then its name —
4632 // `DOMException [AbortError]: m` — where a plain error
4633 // prints only its stack head.
4634 if let Some(JsObj::Object(p)) = self.get(v) {
4635 if let Some(n) = p.get("@@domName") {
4636 let name = self.str_of(n);
4637 stack = format!(
4638 "DOMException [{name}]{}",
4639 stack.strip_prefix(&name).unwrap_or(&stack)
4640 );
4641 }
4642 }
4643 let extra: Vec<String> = self
4644 .own_enum_key_names(v)
4645 .into_iter()
4646 .filter(|k| k != "name")
4647 .map(|k| {
4648 let val = self.fn_prop(v, &k).unwrap_or_else(|| match self.get(v) {
4649 Some(JsObj::Object(p)) => {
4650 p.get(&k).cloned().unwrap_or(Value::Undef)
4651 }
4652 _ => Value::Undef,
4653 });
4654 format!(
4655 "{}: {}",
4656 fmt_key(&k),
4657 self.inspect_lvl(&val, indent + 2, st)
4658 )
4659 })
4660 .collect();
4661 if extra.is_empty() {
4662 stack
4663 } else {
4664 format!("{stack} {{ {} }}", extra.join(", "))
4665 }
4666 }
4667 Some(JsObj::Object(props)) => {
4668 // Instances print with their constructor name as a prefix
4669 // (`C { x: 1 }`); plain objects have none; a null-prototype
4670 // object (e.g. an `Object.groupBy` result) is tagged
4671 // `[Object: null prototype]`.
4672 let ctor = match self.ctor_name(v) {
4673 n if n.is_empty() => "Object".to_string(),
4674 n => n,
4675 };
4676 let plain_prefix = if ctor == "Object" {
4677 String::new()
4678 } else {
4679 format!("{ctor} ")
4680 };
4681 let prefix = if self.inspects_null_proto(v) {
4682 "[Object: null prototype] ".to_string()
4683 } else {
4684 // An inherited `Symbol.toStringTag` shows as `Ctor [Tag] `.
4685 match self.inspect_tag(v) {
4686 Some(t) if t != ctor => format!("{ctor} [{t}] "),
4687 _ => plain_prefix.clone(),
4688 }
4689 };
4690 // Skip node-js's internal slots (`@@native`, `@@bytes`, …) and
4691 // private class fields; a real symbol-keyed own property is a
4692 // visible one and renders as `Symbol(desc): value`.
4693 // An own ACCESSOR has no value to print: node shows the
4694 // label `[Getter]` / `[Setter]` / `[Getter/Setter]` in its
4695 // place. It is found through the `@@ord:` marker the
4696 // property map holds for it, which is also what puts it in
4697 // declaration order among the data properties. Without this
4698 // an accessor rendered as nothing at all — `{ get z(){} }`
4699 // printed `{}`.
4700 let mut shown: Vec<(String, Result<&Value, &'static str>)> = props
4701 .iter()
4702 .filter_map(|(k, val)| match k.strip_prefix(ORD_MARKER) {
4703 Some(real) => {
4704 let attrs = self.prop_attrs(v, real);
4705 let label = match self.own_accessor(v, real)? {
4706 (Some(_), Some(_)) => "[Getter/Setter]",
4707 (Some(_), None) => "[Getter]",
4708 (None, Some(_)) => "[Setter]",
4709 (None, None) => return None,
4710 };
4711 attrs.enumerable.then(|| (fmt_key(real), Err(label)))
4712 }
4713 // Only an ENUMERABLE own property is shown, as node
4714 // does: a native instance keeps bookkeeping (a
4715 // `URLSearchParams`'s `size`) as a hidden own slot,
4716 // and printing it would report a spec getter as data.
4717 None if !k.starts_with("@@")
4718 && !k.starts_with('#')
4719 && self.prop_attrs(v, k).enumerable =>
4720 {
4721 Some((fmt_key(k), Ok(val)))
4722 }
4723 None => None,
4724 })
4725 .collect();
4726 shown.extend(props.iter().filter_map(|(k, val)| {
4727 let sym = self.symbol_of_key(k)?;
4728 self.prop_attrs(v, k)
4729 .enumerable
4730 .then(|| (self.inspect(&sym), Ok(val)))
4731 }));
4732 if shown.is_empty() {
4733 return format!("{prefix}{{}}");
4734 }
4735 // Depth limit (Node default 2): deeper objects collapse to
4736 // `[Object]` (or `[ClassName]` for a named instance).
4737 if indent as i64 > inspect_indent_limit() {
4738 return if self.inspects_null_proto(v) {
4739 // Already bracketed (`[Object: null prototype]`).
4740 prefix.trim_end().to_string()
4741 } else if plain_prefix.is_empty() {
4742 "[Object]".into()
4743 } else {
4744 format!("[{}]", plain_prefix.trim_end())
4745 };
4746 }
4747 let inner: Vec<String> = shown
4748 .iter()
4749 .map(|(k, val)| match val {
4750 Ok(val) => format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)),
4751 Err(label) => format!("{k}: {label}"),
4752 })
4753 .collect();
4754 self.render_object(&inner, &prefix, indent, st)
4755 }
4756 Some(JsObj::Symbol { desc, .. }) => match desc {
4757 Some(d) => format!("Symbol({d})"),
4758 None => "Symbol()".into(),
4759 },
4760 Some(JsObj::Class(c)) => {
4761 let base = if c.parent.is_some() {
4762 let pname = c
4763 .parent
4764 .as_ref()
4765 .map(|p| self.callable_name(p))
4766 .unwrap_or_default();
4767 format!("[class {} extends {}]", c.name, pname)
4768 } else {
4769 format!("[class {}]", c.name)
4770 };
4771 self.with_callable_props(v, base, indent, st)
4772 }
4773 // A Map/Set renders its members at the NEXT nesting level, and
4774 // collapses to `[Map]`/`[Set]` past the depth limit exactly as an
4775 // array collapses to `[Array]`. Both used to recurse through
4776 // `inspect`, which restarts at indent 0, so the depth gate never
4777 // fired: nesting printed one level too deep at every depth
4778 // (measured on node v26.7.0, four nested Maps print
4779 // `Map(1) { 'a' => Map(1) { 'b' => Map(1) { 'c' => [Map] } } }`),
4780 // and a SELF-referential Map or Set recursed forever and aborted
4781 // the process — `const m=new Map(); m.set('m',m); console.log(m)`
4782 // died with `fatal runtime error: stack overflow`, which no
4783 // `try`/`catch` can see. An empty one still prints in full at any
4784 // depth, as `[]`/`{}` do.
4785 // A WEAK collection never shows its contents: node prints
4786 // `WeakMap { <items unknown> }` whether it holds anything or
4787 // not, because the entries are not enumerable by design.
4788 Some(JsObj::Map { weak: true, .. }) => "WeakMap { <items unknown> }".into(),
4789 Some(JsObj::Set { weak: true, .. }) => "WeakSet { <items unknown> }".into(),
4790 Some(JsObj::Map { entries, .. }) => {
4791 let extra = self.side_table_parts(v, indent, st);
4792 if entries.is_empty() && extra.is_empty() {
4793 return "Map(0) {}".into();
4794 }
4795 if indent as i64 > inspect_indent_limit() {
4796 return "[Map]".into();
4797 }
4798 let mut inner: Vec<String> = entries
4799 .values()
4800 .map(|(k, val)| {
4801 // Sequenced, not nested in one `format!`: both arms
4802 // need the same `&mut` cycle state.
4803 let ks = self.inspect_lvl(k, indent + 2, st);
4804 let vs = self.inspect_lvl(val, indent + 2, st);
4805 format!("{ks} => {vs}")
4806 })
4807 .collect();
4808 inner.extend(extra);
4809 // Laid out by the SAME routine as a plain object, not joined
4810 // onto one line unconditionally. `Map`/`Set` were the only
4811 // containers that never consulted `breakLength` or `compact`,
4812 // so every collection wide enough to wrap printed as one long
4813 // line: node breaks a seven-member Set of ten-character
4814 // strings across seven lines, and `util.inspect(m, {compact:
4815 // false})` — which assert's own diff renderer depends on —
4816 // could not break a Map at all. Node builds these through
4817 // `reduceToSingleString` with `braces[0]` of `Map(n) {`, which
4818 // is this `prefix` (the trailing space is the brace gap).
4819 let prefix = format!("Map({}) ", entries.len());
4820 self.render_object(&inner, &prefix, indent, st)
4821 }
4822 Some(JsObj::Set { entries, .. }) => {
4823 let extra = self.side_table_parts(v, indent, st);
4824 if entries.is_empty() && extra.is_empty() {
4825 return "Set(0) {}".into();
4826 }
4827 if indent as i64 > inspect_indent_limit() {
4828 return "[Set]".into();
4829 }
4830 let mut inner: Vec<String> = entries
4831 .values()
4832 .map(|v| self.inspect_lvl(v, indent + 2, st))
4833 .collect();
4834 inner.extend(extra);
4835 // Same layout routine as a Map (see above). Note node does
4836 // NOT column-group a wide Set the way it grids an array:
4837 // `groupArrayElements` is reached only from the list
4838 // formatter, so a 30-member Set is thirty lines.
4839 let prefix = format!("Set({}) ", entries.len());
4840 self.render_object(&inner, &prefix, indent, st)
4841 }
4842 Some(JsObj::Generator { .. }) => "Object [Generator] {}".into(),
4843 Some(JsObj::Promise { id }) => match self.promises.get(*id as usize) {
4844 Some(c) => match c.state {
4845 PromiseState::Pending => "Promise { <pending> }".into(),
4846 PromiseState::Fulfilled => {
4847 format!("Promise {{ {} }}", self.inspect_lvl(&c.value, 0, st))
4848 }
4849 PromiseState::Rejected => {
4850 format!(
4851 "Promise {{ <rejected> {} }}",
4852 self.inspect_lvl(&c.value, 0, st)
4853 )
4854 }
4855 },
4856 None => "Promise { <pending> }".into(),
4857 },
4858 Some(JsObj::Func(f)) => {
4859 // `callable_name`, not the FuncDef name: an anonymous
4860 // function expression gets its name by inference from the
4861 // binding it initialises (`const f = function(){}`), and
4862 // that lands as an own `name` property.
4863 let name = self.callable_name(v);
4864 // util.inspect labels a function by its kind, the same
4865 // string V8 gives it as `Symbol.toStringTag`:
4866 // `[AsyncFunction: af]`, `[GeneratorFunction: g]`.
4867 let kind = match self.funcs.get(f.def_id) {
4868 Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction",
4869 Some(d) if d.is_generator => "GeneratorFunction",
4870 Some(d) if d.is_async => "AsyncFunction",
4871 _ => "Function",
4872 };
4873 let base = if name.is_empty() {
4874 format!("[{kind} (anonymous)]")
4875 } else {
4876 format!("[{kind}: {name}]")
4877 };
4878 self.with_callable_props(v, base, indent, st)
4879 }
4880 Some(JsObj::Builtin(n)) => {
4881 // A namespace object is not a function and must not be
4882 // printed as one. The three ECMAScript namespaces carry a
4883 // `Symbol.toStringTag` and inspect as `Object [Math] {}`;
4884 // their members are all non-enumerable, so the braces really
4885 // are empty. A `require()`d module namespace has no tag and
4886 // node prints its members, which cannot be rendered here —
4887 // formatting a member means allocating its value, and this
4888 // runs under the host borrow.
4889 if !builtin_is_callable(n) {
4890 match crate::builtins::well_known_tag(self, v) {
4891 Some(tag) => format!("Object [{tag}] {{}}"),
4892 // `Set.prototype` inspects under the CONSTRUCTOR's
4893 // name, not the key: node prints `Object [Set] {}`.
4894 None => {
4895 format!("Object [{}] {{}}", n.trim_end_matches(".prototype"))
4896 }
4897 }
4898 } else {
4899 format!("[Function: {}]", crate::builtins::builtin_name(n))
4900 }
4901 }
4902 // A bound method is not anonymous: it is the prototype method it
4903 // resolves to, so `console.log(new Uint8Array(1).set)` reports
4904 // `[Function: set]`.
4905 Some(JsObj::BoundMethod { name, .. }) => format!("[Function: {name}]"),
4906 Some(JsObj::BoundFunc { target, .. }) => {
4907 let n = self.callable_name(target);
4908 if n.is_empty() {
4909 "[Function: bound ]".into()
4910 } else {
4911 format!("[Function: bound {n}]")
4912 }
4913 }
4914 _ => "undefined".into(),
4915 },
4916 _ => "undefined".into(),
4917 }
4918 }
4919
4920 /// Append a callable's own enumerable properties to its `[Function: f]` /
4921 /// `[class C]` base, the way `util.inspect` does: `[Function: f] { a: 1 }`.
4922 /// A callable with none renders as the bare base.
4923 fn with_callable_props(
4924 &self,
4925 v: &Value,
4926 base: String,
4927 indent: usize,
4928 st: &mut InspectCycles,
4929 ) -> String {
4930 let mut inner: Vec<String> = self
4931 .own_enum_key_names(v)
4932 .into_iter()
4933 .map(|k| {
4934 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4935 format!(
4936 "{}: {}",
4937 fmt_key(&k),
4938 self.inspect_lvl(&val, indent + 2, st)
4939 )
4940 })
4941 .collect();
4942 for (k, val) in self.own_symbol_entries(v) {
4943 if let Some(sym) = self.symbol_of_key(&k) {
4944 inner.push(format!(
4945 "{}: {}",
4946 self.inspect(&sym),
4947 self.inspect_lvl(&val, indent + 2, st)
4948 ));
4949 }
4950 }
4951 if inner.is_empty() {
4952 return base;
4953 }
4954 self.render_object(&inner, &format!("{base} "), indent, st)
4955 }
4956
4957 /// Render a non-empty array's already-formatted element strings, applying
4958 /// Node's `util.inspect` layout: a single line when it fits, else a multi-line
4959 /// grid via `groupArrayElements` (for >6 entries), else one element per line.
4960 /// `values` is the raw element list (drives numeric right-alignment); `indent`
4961 /// is the array's own indentation level.
4962 fn render_array(
4963 &self,
4964 output: &[String],
4965 values: &[Value],
4966 indent: usize,
4967 opts: ArrayLayout<'_>,
4968 st: &InspectCycles,
4969 ) -> String {
4970 let ArrayLayout {
4971 has_props,
4972 has_tail,
4973 base,
4974 } = opts;
4975 // Group array elements together if the array has more than six entries.
4976 // Arrays carrying extra own props (`index`/`input`/… on a match result)
4977 // are never grid-grouped — Node lays those out plainly.
4978 // `compact: false` (held as 0) also turns the GRID off, not just the
4979 // single-line join. Node reaches `groupArrayElements` only under
4980 // `ctx.compact >= 1`, so `util.inspect(arr, { compact: false })` is one
4981 // element per line however many there are; without this gate a 30-element
4982 // array still came back column-aligned in three rows, which is the form
4983 // assert's diff renderer splits on — every array diff would have been
4984 // computed over grid rows instead of elements.
4985 let entries = output.len();
4986 let (lines, grouped) = if entries > 6 && !has_props && inspect_compact() >= 1 {
4987 group_array_elements(self, output, values, indent, has_tail)
4988 } else {
4989 (output.to_vec(), false)
4990 };
4991 // A typed array prints its constructor and length ahead of the brackets
4992 // (`Uint8Array(3) [ 1, 2, 3 ]`); node counts that as `base` in the
4993 // break-length seed, so a long tag wraps the list one entry sooner.
4994 if output.is_empty() {
4995 return format!("{base}[]");
4996 }
4997 // If no grouping happened, try to line everything up on a single line.
4998 if !grouped {
4999 // start = output.length + indentationLvl + braces[0].len(1) + base + 10
5000 let start = output.len() + indent + 1 + base.chars().count() + 10;
5001 if self.may_compact(indent, st) && is_below_break_length(output, start) {
5002 return format!("{base}[ {} ]", output.join(", "));
5003 }
5004 }
5005 // Otherwise: one (grouped or single) entry per line, indented by indent+2.
5006 let pad = " ".repeat(indent);
5007 let sep = format!(",\n{pad} ");
5008 format!("{base}[\n{pad} {}\n{pad}]", lines.join(&sep))
5009 }
5010
5011 /// Render a non-empty object's already-formatted `key: value` strings with
5012 /// Node's `util.inspect` layout: a single line when it fits `breakLength`,
5013 /// else one property per line indented by `indent + 2`. `prefix` is the
5014 /// constructor/`[Object: null prototype]` tag (with trailing space) or empty.
5015 /// Mirrors `render_array`'s break decision, including the `compact` depth
5016 /// gate.
5017 /// Whether a group at `indent` may be joined onto one line.
5018 ///
5019 /// Node's `reduceToSingleString`: only while the subtree below this group is
5020 /// SHALLOWER than `compact` (default 3). `compact: false` is held as 0, so
5021 /// nothing qualifies and every group breaks.
5022 fn may_compact(&self, indent: usize, st: &InspectCycles) -> bool {
5023 let compact = inspect_compact();
5024 if compact < 1 {
5025 return false;
5026 }
5027 // Levels, not columns: the indent advances by two per level.
5028 let depth_below = (st.deepest.saturating_sub(indent)) / 2;
5029 (depth_below as i64) < compact
5030 }
5031
5032 fn render_object(
5033 &self,
5034 output: &[String],
5035 prefix: &str,
5036 indent: usize,
5037 st: &InspectCycles,
5038 ) -> String {
5039 // start = output.length + indentationLvl + braces[0].len + base(0) + 10.
5040 // For a tagged object Node folds the tag into `braces[0]` (e.g.
5041 // `"Point {"`, `"[Object: null prototype] {"`), so its length is the
5042 // prefix (which carries the trailing space) plus the `{`.
5043 // `sorted: true` orders the RENDERED entries, not the keys. Node sorts
5044 // the finished `key: value` strings (`output.sort()` in `formatRaw` for
5045 // the object shape), which is observably different from sorting keys
5046 // whenever a key needs quoting — `'b-b': 1` sorts under `'`, not `b`.
5047 // `assert`'s diff renderer depends on this: without it two objects
5048 // carrying the same properties in a different insertion order diffed as
5049 // a wholesale rewrite of every line instead of as equal.
5050 let sorted_output;
5051 let output = if inspect_sorted() {
5052 let mut v = output.to_vec();
5053 v.sort();
5054 sorted_output = v;
5055 &sorted_output[..]
5056 } else {
5057 output
5058 };
5059 let braces0 = prefix.chars().count() + 1;
5060 let start = output.len() + indent + braces0 + 10;
5061 if self.may_compact(indent, st) && is_below_break_length(output, start) {
5062 return format!("{prefix}{{ {} }}", output.join(", "));
5063 }
5064 let pad = " ".repeat(indent);
5065 let sep = format!(",\n{pad} ");
5066 format!("{prefix}{{\n{pad} {}\n{pad}}}", output.join(&sep))
5067 }
5068
5069 /// The `.name` of any callable (function/class/builtin/bound).
5070 pub fn callable_name(&self, v: &Value) -> String {
5071 // A user-set `.name` own property wins.
5072 if let Some(n) = self.fn_prop(v, "name") {
5073 return self.str_of(&n);
5074 }
5075 match self.get(v) {
5076 Some(JsObj::Func(f)) => self
5077 .funcs
5078 .get(f.def_id)
5079 .map(|d| d.name.clone())
5080 .unwrap_or_default(),
5081 Some(JsObj::Class(c)) => c.name.clone(),
5082 // Not the whole key: a builtin's `.name` is its last segment, and a
5083 // prototype thunk's key is `@proto:<Ctor>:<method>` — which has no
5084 // `.` at all, so this reported the internal spelling verbatim and
5085 // `console.log(Uint8Array.prototype.set)` printed
5086 // `[Function: @proto:TypedArray:set]`.
5087 Some(JsObj::Builtin(n)) => crate::builtins::builtin_name(n).to_string(),
5088 Some(JsObj::BoundFunc { target, .. }) => {
5089 format!("bound {}", self.callable_name(target))
5090 }
5091 Some(JsObj::BoundMethod { name, .. }) => name.clone(),
5092 _ => String::new(),
5093 }
5094 }
5095
5096 // ── equality / comparison / arithmetic (numeric-hook + builtin paths) ──
5097
5098 /// Strict equality (`===`): same type and same value, no coercion.
5099 pub fn strict_eq(&self, a: &Value, b: &Value) -> bool {
5100 match (a, b) {
5101 (Value::Undef, Value::Undef) => true,
5102 (Value::Bool(x), Value::Bool(y)) => x == y,
5103 (Value::Str(x), Value::Str(y)) => x == y,
5104 _ => {
5105 // Numbers (NaN !== NaN, +0 === -0).
5106 let an = matches!(a, Value::Int(_) | Value::Float(_));
5107 let bn = matches!(b, Value::Int(_) | Value::Float(_));
5108 if an && bn {
5109 let x = self.to_number(a);
5110 let y = self.to_number(b);
5111 return x == y;
5112 }
5113 // BigInt === BigInt compares by value (each literal is a distinct
5114 // heap cell, so reference identity would be wrong). BigInt is never
5115 // `===` a Number (different types).
5116 if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5117 return x == y;
5118 }
5119 // Heap values.
5120 if let (Some(sa), Some(sb)) = (self.as_str(a), self.as_str(b)) {
5121 return sa == sb;
5122 }
5123 let na = self.is_null(a);
5124 let nb = self.is_null(b);
5125 if na || nb {
5126 return na && nb;
5127 }
5128 // A builtin namespace/constructor/prototype is a SINGLETON in JS
5129 // (`Math === Math`, `Array.prototype === Array.prototype`), but
5130 // every bare reference here allocates a fresh handle, so compare
5131 // those by name rather than by heap index.
5132 if let (Some(JsObj::Builtin(x)), Some(JsObj::Builtin(y))) =
5133 (self.get(a), self.get(b))
5134 {
5135 return builtin_identity(x) == builtin_identity(y);
5136 }
5137 // Reference identity for arrays/objects/functions.
5138 matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
5139 }
5140 }
5141 }
5142
5143 /// Whether `v` is `null` or `undefined`.
5144 pub fn is_nullish(&self, v: &Value) -> bool {
5145 matches!(v, Value::Undef) || self.is_null(v)
5146 }
5147
5148 /// The ECMAScript "loose type" of `v` for the `==` algorithm: `"number"`,
5149 /// `"string"` (primitive or heap string), `"boolean"`, `"undefined"`,
5150 /// `"null"`, or `"object"` (array / plain object / function).
5151 fn js_type(&self, v: &Value) -> &'static str {
5152 match v {
5153 Value::Undef => "undefined",
5154 Value::Bool(_) => "boolean",
5155 Value::Int(_) | Value::Float(_) => "number",
5156 Value::Str(_) => "string",
5157 Value::Obj(_) => match self.get(v) {
5158 Some(JsObj::Str(_)) => "string",
5159 Some(JsObj::Null) => "null",
5160 Some(JsObj::BigInt(_)) => "bigint",
5161 _ => "object",
5162 },
5163 _ => "object",
5164 }
5165 }
5166
5167 /// Loose equality (`==`) following the ECMAScript Abstract Equality Comparison.
5168 /// Objects reduce via `ToPrimitive` (which for our heap objects is always their
5169 /// string `toString`), so `[0] == "0"` is `true` (string compare of `"0"`) but
5170 /// `[0] == ""` is `false` — never a number coercion of the object.
5171 pub fn loose_eq(&self, a: &Value, b: &Value) -> bool {
5172 // Same type: identical to `===` (number==number, string==string, etc.).
5173 if self.strict_eq(a, b) {
5174 return true;
5175 }
5176 let ta = self.js_type(a);
5177 let tb = self.js_type(b);
5178 // null and undefined are loosely equal only to each other.
5179 if self.is_nullish(a) || self.is_nullish(b) {
5180 return self.is_nullish(a) && self.is_nullish(b);
5181 }
5182 // BigInt ⇄ (Number | String | Boolean | Object): compare mathematical
5183 // values (both-BigInt was already settled by the `strict_eq` above).
5184 if ta == "bigint" || tb == "bigint" {
5185 return self.bigint_loose_eq(a, b);
5186 }
5187 if ta == tb {
5188 // Same type but not strict-equal (and not nullish) ⇒ not equal.
5189 return false;
5190 }
5191 // number ⇄ string: compare as numbers.
5192 if (ta == "number" && tb == "string") || (ta == "string" && tb == "number") {
5193 return self.to_number(a) == self.to_number(b);
5194 }
5195 // boolean side coerces to number, then recompares.
5196 if ta == "boolean" {
5197 return self.loose_eq(&Value::Float(self.to_number(a)), b);
5198 }
5199 if tb == "boolean" {
5200 return self.loose_eq(a, &Value::Float(self.to_number(b)));
5201 }
5202 // object ⇄ (number|string): ToPrimitive the object (→ its string form),
5203 // then recompare as string==string or number==string.
5204 if ta == "object" && (tb == "number" || tb == "string") {
5205 let pa = self.str_of(a);
5206 return if tb == "string" {
5207 pa == self.str_of(b)
5208 } else {
5209 str_to_number(&pa) == self.to_number(b)
5210 };
5211 }
5212 if tb == "object" && (ta == "number" || ta == "string") {
5213 let pb = self.str_of(b);
5214 return if ta == "string" {
5215 self.str_of(a) == pb
5216 } else {
5217 self.to_number(a) == str_to_number(&pb)
5218 };
5219 }
5220 false
5221 }
5222
5223 /// The numeric-hook arithmetic/relational fallback for non-native operands
5224 /// (called by fusevm when at least one operand isn't `Int`/`Float`).
5225 pub fn arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5226 use NumOp::*;
5227 match op {
5228 Add => {
5229 // `+`: if either operand is a string, concatenate string forms;
5230 // otherwise numeric addition.
5231 let a_str = self.prefers_string(a);
5232 let b_str = self.prefers_string(b);
5233 if a_str || b_str {
5234 // String concatenation wins even with a bigint operand
5235 // (`1n + "x"` → `"1x"`).
5236 let s = format!("{}{}", self.str_of(a), self.str_of(b));
5237 Ok(self.new_str(s))
5238 } else if self.is_bigint_val(a) || self.is_bigint_val(b) {
5239 self.bigint_arith(op, a, b)
5240 } else {
5241 Ok(Value::Float(self.to_number(a) + self.to_number(b)))
5242 }
5243 }
5244 Sub | Mul | Div | Mod | Pow if self.is_bigint_val(a) || self.is_bigint_val(b) => {
5245 self.bigint_arith(op, a, b)
5246 }
5247 Sub => Ok(Value::Float(self.to_number(a) - self.to_number(b))),
5248 Mul => Ok(Value::Float(self.to_number(a) * self.to_number(b))),
5249 Div => Ok(Value::Float(self.to_number(a) / self.to_number(b))),
5250 Mod => Ok(Value::Float(js_mod(self.to_number(a), self.to_number(b)))),
5251 Pow => Ok(Value::Float(crate::builtins::js_pow(
5252 self.to_number(a),
5253 self.to_number(b),
5254 ))),
5255 Neg if self.is_bigint_val(a) => self.bigint_arith(op, a, b),
5256 Neg => Ok(Value::Float(-self.to_number(a))),
5257 Lt | Le | Gt | Ge => Ok(Value::Bool(self.relational(op, a, b))),
5258 Eq => Ok(Value::Bool(self.loose_eq(a, b))),
5259 Ne => Ok(Value::Bool(!self.loose_eq(a, b))),
5260 }
5261 }
5262
5263 /// Whether `v`'s primitive (`ToPrimitive` with the default hint) is a string,
5264 /// which drives `+` toward concatenation. Primitive strings qualify, and so
5265 /// do heap objects whose default `ToPrimitive` is their (string) `toString`:
5266 /// arrays (`[1,2,3]+3 → "1,2,33"`), plain objects (`{}+[] → "[object Object]"`),
5267 /// and functions. `null`/`undefined`/`boolean`/`number` do not.
5268 fn prefers_string(&self, v: &Value) -> bool {
5269 match v {
5270 Value::Str(_) => true,
5271 // A BigInt's `ToPrimitive` is the bigint itself (numeric), NOT a string,
5272 // so `1n + 2n` is bigint addition, not concatenation. `null` has no
5273 // string primitive either.
5274 Value::Obj(_) => !matches!(
5275 self.get(v),
5276 Some(JsObj::Null) | Some(JsObj::BigInt(_)) | None
5277 ),
5278 _ => false,
5279 }
5280 }
5281
5282 /// Relational comparison (`< <= > >=`) with JS coercion: string/string is
5283 /// lexicographic, otherwise numeric (NaN yields false).
5284 fn relational(&self, op: NumOp, a: &Value, b: &Value) -> bool {
5285 use std::cmp::Ordering;
5286 let ord = if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5287 // BigInt < BigInt: exact (no f64 precision loss for large magnitudes).
5288 x.cmp(&y)
5289 } else if let (Some(x), Some(y)) = (self.as_str(a), self.as_str(b)) {
5290 // 7.2.13 IsLessThan compares CODE UNITS, which is not Rust's `str`
5291 // order once an astral character meets a BMP one — see `utf16`.
5292 crate::utf16::cmp_units(&x, &y)
5293 } else {
5294 let x = self.to_number(a);
5295 let y = self.to_number(b);
5296 match x.partial_cmp(&y) {
5297 Some(o) => o,
5298 None => return false, // NaN operand
5299 }
5300 };
5301 match op {
5302 NumOp::Lt => ord == Ordering::Less,
5303 NumOp::Le => ord != Ordering::Greater,
5304 NumOp::Gt => ord == Ordering::Greater,
5305 NumOp::Ge => ord != Ordering::Less,
5306 _ => false,
5307 }
5308 }
5309
5310 /// Bitwise/shift ops with JS ToInt32/ToUint32 semantics — or true
5311 /// arbitrary-width BigInt bitwise when both operands are BigInt (mixing a
5312 /// BigInt with a Number throws, matching Node).
5313 pub fn bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5314 if self.is_bigint_val(a) || self.is_bigint_val(b) {
5315 return self.bigint_bitwise(tag, a, b);
5316 }
5317 let x = to_int32(self.to_number(a));
5318 let y = to_int32(self.to_number(b));
5319 let r: i64 = match tag {
5320 binop::BITAND => (x & y) as i64,
5321 binop::BITOR => (x | y) as i64,
5322 binop::BITXOR => (x ^ y) as i64,
5323 binop::SHL => (x.wrapping_shl((y as u32) & 31)) as i64,
5324 binop::SHR => (x >> ((y as u32) & 31)) as i64,
5325 binop::USHR => (to_uint32(self.to_number(a)) >> ((y as u32) & 31)) as i64,
5326 _ => 0,
5327 };
5328 Ok(Value::Float(r as f64))
5329 }
5330
5331 // ── BigInt operations ────────────────────────────────────────────────────
5332 /// Whether `v` is a heap `BigInt`.
5333 pub fn is_bigint_val(&self, v: &Value) -> bool {
5334 matches!(self.get(v), Some(JsObj::BigInt(_)))
5335 }
5336 /// The `BigInt` value of `v` (a heap bigint), else `None`.
5337 pub fn as_bigint(&self, v: &Value) -> Option<num_bigint::BigInt> {
5338 match self.get(v) {
5339 Some(JsObj::BigInt(b)) => Some(b.clone()),
5340 _ => None,
5341 }
5342 }
5343 /// Allocate a heap `BigInt`.
5344 pub fn new_bigint(&mut self, b: num_bigint::BigInt) -> Value {
5345 self.alloc(JsObj::BigInt(b))
5346 }
5347
5348 /// BigInt arithmetic (`+ - * / % **`, unary `-`). Requires BOTH operands to be
5349 /// BigInt for a binary op; mixing a BigInt with a Number throws the exact Node
5350 /// `TypeError` (a string operand is handled as concatenation before we get
5351 /// here). Division/`%` truncate toward zero; `**` needs a non-negative
5352 /// exponent.
5353 fn bigint_arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5354 use num_traits::{Signed, Zero};
5355 use NumOp::*;
5356 if op == Neg {
5357 let x = self.as_bigint(a).expect("bigint_arith Neg on non-bigint");
5358 return Ok(self.new_bigint(-x));
5359 }
5360 let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5361 (Some(x), Some(y)) => (x, y),
5362 // Exactly one side is a BigInt → the other is a Number/Boolean: illegal.
5363 _ => {
5364 return Err(type_error(
5365 "Cannot mix BigInt and other types, use explicit conversions",
5366 ))
5367 }
5368 };
5369 let r = match op {
5370 Add => x + y,
5371 Sub => x - y,
5372 Mul => x * y,
5373 Div => {
5374 if y.is_zero() {
5375 return Err("RangeError: Division by zero".into());
5376 }
5377 x / y // truncates toward zero (matches JS BigInt division)
5378 }
5379 Mod => {
5380 if y.is_zero() {
5381 return Err("RangeError: Division by zero".into());
5382 }
5383 x % y // sign follows the dividend (truncated), like JS
5384 }
5385 Pow => {
5386 if y.is_negative() {
5387 return Err("RangeError: Exponent must be positive".into());
5388 }
5389 let exp = num_traits::ToPrimitive::to_u32(&y)
5390 .ok_or_else(|| "RangeError: Maximum BigInt size exceeded".to_string())?;
5391 num_traits::Pow::pow(x, exp)
5392 }
5393 _ => return Err(type_error("unsupported BigInt operation")),
5394 };
5395 Ok(self.new_bigint(r))
5396 }
5397
5398 /// BigInt bitwise (`& | ^ << >>`); `>>>` has no BigInt form. Both operands must
5399 /// be BigInt (mixing throws).
5400 fn bigint_bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5401 let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5402 (Some(x), Some(y)) => (x, y),
5403 _ => {
5404 return Err(type_error(
5405 "Cannot mix BigInt and other types, use explicit conversions",
5406 ))
5407 }
5408 };
5409 let r = match tag {
5410 binop::BITAND => x & y,
5411 binop::BITOR => x | y,
5412 binop::BITXOR => x ^ y,
5413 binop::SHL => {
5414 let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5415 if n >= 0 {
5416 x << (n as usize)
5417 } else {
5418 x >> ((-n) as usize)
5419 }
5420 }
5421 binop::SHR => {
5422 let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5423 if n >= 0 {
5424 x >> (n as usize)
5425 } else {
5426 x << ((-n) as usize)
5427 }
5428 }
5429 binop::USHR => {
5430 return Err(type_error(
5431 "BigInts have no unsigned right shift, use >> instead",
5432 ))
5433 }
5434 _ => return Err(type_error("unsupported BigInt operation")),
5435 };
5436 Ok(self.new_bigint(r))
5437 }
5438
5439 /// BigInt ⇄ (Number | Boolean | String | Object) loose equality (`==`). Both
5440 /// being BigInt was already handled by `strict_eq`.
5441 fn bigint_loose_eq(&self, a: &Value, b: &Value) -> bool {
5442 // Order so `big` is the BigInt side and `other` the counterpart.
5443 let (big, other) = match (self.as_bigint(a), self.as_bigint(b)) {
5444 (Some(x), _) => (x, b),
5445 (_, Some(y)) => (y, a),
5446 _ => return false,
5447 };
5448 match other {
5449 Value::Bool(bo) => big == num_bigint::BigInt::from(*bo as i64),
5450 Value::Int(n) => big == num_bigint::BigInt::from(*n),
5451 Value::Float(f) => {
5452 // Equal only when the float is an integer with the same value.
5453 if !f.is_finite() || f.fract() != 0.0 {
5454 return false;
5455 }
5456 bigint_to_f64(&big) == *f
5457 }
5458 Value::Str(s) => match parse_bigint_str(s) {
5459 Some(bs) => big == bs,
5460 None => false,
5461 },
5462 Value::Obj(_) => match self.get(other) {
5463 // A heap string parses like a primitive string.
5464 Some(JsObj::Str(s)) => parse_bigint_str(s).map(|bs| big == bs).unwrap_or(false),
5465 _ => {
5466 // Other objects reduce via ToPrimitive (their string form).
5467 let s = self.str_of(other);
5468 parse_bigint_str(&s).map(|bs| big == bs).unwrap_or(false)
5469 }
5470 },
5471 _ => false,
5472 }
5473 }
5474}
5475
5476/// Parse a string to a BigInt under JS `StringToBigInt` rules: trimmed, empty →
5477/// `0n`, decimal or `0x`/`0o`/`0b` prefixed; any junk → `None`.
5478pub fn parse_bigint_str(s: &str) -> Option<num_bigint::BigInt> {
5479 let t = crate::utf16::js_trim(s);
5480 if t.is_empty() {
5481 return Some(num_bigint::BigInt::from(0));
5482 }
5483 let (radix, digits) = if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5484 (16, h)
5485 } else if let Some(o) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5486 (8, o)
5487 } else if let Some(bb) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5488 (2, bb)
5489 } else {
5490 (10, t)
5491 };
5492 num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
5493}
5494
5495/// Coerce a BigInt to `f64` (for `Number(bigint)` and mixed relational compares);
5496/// out-of-range magnitudes become ±Infinity, matching Node.
5497pub fn bigint_to_f64(b: &num_bigint::BigInt) -> f64 {
5498 num_traits::ToPrimitive::to_f64(b).unwrap_or_else(|| {
5499 if num_traits::Signed::is_negative(b) {
5500 f64::NEG_INFINITY
5501 } else {
5502 f64::INFINITY
5503 }
5504 })
5505}
5506
5507/// JS `%` remainder (sign follows the dividend; matches `f64::rem`).
5508fn js_mod(a: f64, b: f64) -> f64 {
5509 a % b
5510}
5511
5512/// Cycle bookkeeping for one `util.inspect` render.
5513///
5514/// `seen` is the chain of objects currently being rendered (an entry appearing
5515/// twice is a back-edge), and `refs` records every object a back-edge pointed
5516/// at, in first-encountered order — its position + 1 is the `*N` id Node prints
5517/// in `[Circular *N]` / `<ref *N>`.
5518/// How an array-shaped group is laid out, beyond its entries themselves.
5519#[derive(Clone, Copy)]
5520struct ArrayLayout<'a> {
5521 /// Extra own properties follow the elements, which suppresses grid grouping.
5522 has_props: bool,
5523 /// `output`'s last entry is the `... N more items` tail rather than a real
5524 /// element, so the grid must not size a column to it.
5525 has_tail: bool,
5526 /// A constructor tag printed before the brackets, with a trailing space
5527 /// (`"Uint8Array(3) "`), or empty for a plain array.
5528 base: &'a str,
5529}
5530
5531#[derive(Default)]
5532struct InspectCycles {
5533 seen: Vec<Value>,
5534 refs: Vec<Value>,
5535 /// The indent level of the value most recently EXPANDED — node's
5536 /// `ctx.currentDepth`. `reduceToSingleString` puts a group on one line only
5537 /// while `currentDepth - thisDepth < compact`, so without it a deeply
5538 /// nested object printed on one line where node breaks the outer levels.
5539 deepest: usize,
5540}
5541
5542impl InspectCycles {
5543 /// Record `v` as a cycle target (idempotent) and return its 1-based id.
5544 fn mark(&mut self, h: &JsHost, v: &Value) -> usize {
5545 if let Some(id) = self.id_of(h, v) {
5546 return id;
5547 }
5548 self.refs.push(v.clone());
5549 self.refs.len()
5550 }
5551
5552 /// The `*N` id already assigned to `v`, if any.
5553 fn id_of(&self, h: &JsHost, v: &Value) -> Option<usize> {
5554 self.refs
5555 .iter()
5556 .position(|p| h.strict_eq(p, v))
5557 .map(|i| i + 1)
5558 }
5559}
5560
5561thread_local! {
5562 /// The active `util.inspect` `depth` (nesting levels shown before collapsing
5563 /// to `[Object]`/`[Array]`). Node's default is 2; `util.inspect(v,{depth:N})`
5564 /// overrides it for one call, `console.log`/`util.format` use the default.
5565 /// Signed, because `util.inspect(v, { depth: -1 })` is legal and means
5566 /// "already past the limit" — everything collapses to `[Object]` at the top
5567 /// level. Held as `usize` it read as an enormous depth and expanded fully.
5568 static INSPECT_MAX_DEPTH: std::cell::Cell<i64> = const { std::cell::Cell::new(2) };
5569
5570 /// `util.inspect`'s `compact` option. Node's default is the NUMBER 3: a
5571 /// group is put on one line only when the subtree below it is shallower
5572 /// than this. `compact: false` is held as 0, which no subtree depth is
5573 /// below, so every group breaks — which is exactly what node does.
5574 static INSPECT_COMPACT: std::cell::Cell<i64> = const { std::cell::Cell::new(DEFAULT_COMPACT) };
5575
5576 /// `util.inspect`'s `breakLength`. Node's default is 128, but `util.inspect`
5577 /// itself passes 80.
5578 static INSPECT_BREAK_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(80) };
5579
5580 /// `util.inspect`'s `sorted` option: emit an object's own keys in code-unit
5581 /// order instead of insertion order. Off by default. `assert`'s diff renderer
5582 /// turns it on so that two objects built with the same keys in a different
5583 /// order diff as equal rather than as a wholesale rewrite.
5584 static INSPECT_SORTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5585
5586 /// `util.inspect`'s `maxArrayLength`: how many entries are formatted before
5587 /// the rest collapse into `... N more items`. Node's default is 100;
5588 /// `Infinity`/`null` means "all", held here as `usize::MAX`.
5589 static INSPECT_MAX_ARRAY_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(DEFAULT_MAX_ARRAY_LENGTH) };
5590
5591 /// `util.inspect`'s `customInspect` option: whether a value's own
5592 /// `[util.inspect.custom]` rendering is used. On by default; `assert` turns
5593 /// it off so a diff shows an object's real structure rather than whatever
5594 /// summary it prefers to print.
5595 static INSPECT_CUSTOM: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
5596
5597 /// `util.inspect`'s `showHidden`: reveal the non-enumerable slots a value
5598 /// carries — an array's `length`, a typed array's element width and window
5599 /// onto its backing store. Off by default; `util.format`'s `%o` turns it on.
5600 static INSPECT_SHOW_HIDDEN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5601}
5602
5603/// Set the `util.inspect` `showHidden` option for the next render.
5604pub fn set_inspect_show_hidden(s: bool) {
5605 INSPECT_SHOW_HIDDEN.with(|x| x.set(s));
5606}
5607
5608pub(crate) fn inspect_show_hidden() -> bool {
5609 INSPECT_SHOW_HIDDEN.with(|x| x.get())
5610}
5611
5612/// Set the `util.inspect` `customInspect` option for the next render.
5613pub fn set_inspect_custom(c: bool) {
5614 INSPECT_CUSTOM.with(|x| x.set(c));
5615}
5616
5617pub(crate) fn inspect_custom() -> bool {
5618 INSPECT_CUSTOM.with(|x| x.get())
5619}
5620
5621/// Set the `util.inspect` `sorted` option for the next render.
5622pub fn set_inspect_sorted(s: bool) {
5623 INSPECT_SORTED.with(|x| x.set(s));
5624}
5625
5626pub(crate) fn inspect_sorted() -> bool {
5627 INSPECT_SORTED.with(|x| x.get())
5628}
5629
5630/// Set the `util.inspect` `maxArrayLength` for the next render.
5631pub fn set_inspect_max_array_length(n: usize) {
5632 INSPECT_MAX_ARRAY_LENGTH.with(|x| x.set(n));
5633}
5634
5635pub(crate) fn inspect_max_array_length() -> usize {
5636 INSPECT_MAX_ARRAY_LENGTH.with(|x| x.get())
5637}
5638
5639/// Set the `util.inspect` `compact` option for the next render (0 for `false`).
5640pub fn set_inspect_compact(c: i64) {
5641 INSPECT_COMPACT.with(|x| x.set(c));
5642}
5643
5644/// Set the `util.inspect` `breakLength` for the next render.
5645pub fn set_inspect_break_length(n: usize) {
5646 INSPECT_BREAK_LENGTH.with(|x| x.set(n));
5647}
5648
5649fn inspect_compact() -> i64 {
5650 INSPECT_COMPACT.with(|x| x.get())
5651}
5652
5653/// Set the `util.inspect` depth for the next render (restore to 2 after).
5654pub fn set_inspect_max_depth(d: i64) {
5655 INSPECT_MAX_DEPTH.with(|c| c.set(d));
5656}
5657/// Twice the configured depth, which is what the inspect walk compares its
5658/// indent against. Saturating, because `util.inspect(x, { depth: null })` and
5659/// `{ depth: Infinity }` both set the depth to `usize::MAX`, and doubling that
5660/// overflowed and panicked the process — an abort no script could catch.
5661fn inspect_indent_limit() -> i64 {
5662 inspect_max_depth().saturating_mul(2)
5663}
5664
5665fn inspect_max_depth() -> i64 {
5666 INSPECT_MAX_DEPTH.with(|c| c.get())
5667}
5668
5669/// ECMA-262 `ToInt32` (7.1.6): truncate toward zero, reduce modulo 2^32, then
5670/// reinterpret as signed.
5671///
5672/// The reduction has to happen in `f64`, not by casting through `i64`. Rust
5673/// saturates an out-of-range float-to-int cast, so `1e300 as i64` is `i64::MAX`
5674/// and `1e300 | 0` came out `-1` where every engine says `0`; the same
5675/// saturation made `1e300 >>> 0` report `4294967295`. `rem_euclid` on a
5676/// power-of-two modulus is exact for every finite double, so this is the whole
5677/// fix — and it is the form `Math.clz32` already used.
5678pub(crate) fn to_int32(f: f64) -> i32 {
5679 to_uint32(f) as i32
5680}
5681pub(crate) fn to_uint32(f: f64) -> u32 {
5682 if !f.is_finite() {
5683 return 0;
5684 }
5685 f.trunc().rem_euclid(4294967296.0) as u32
5686}
5687
5688/// Parse a string in numeric context (`ToNumber`): trimmed, empty -> 0.
5689fn str_to_number(s: &str) -> f64 {
5690 let t = crate::utf16::js_trim(s);
5691 if t.is_empty() {
5692 return 0.0;
5693 }
5694 if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5695 return i64::from_str_radix(hex, 16)
5696 .map(|n| n as f64)
5697 .unwrap_or(f64::NAN);
5698 }
5699 if let Some(oct) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5700 return i64::from_str_radix(oct, 8)
5701 .map(|n| n as f64)
5702 .unwrap_or(f64::NAN);
5703 }
5704 if let Some(bin) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5705 return i64::from_str_radix(bin, 2)
5706 .map(|n| n as f64)
5707 .unwrap_or(f64::NAN);
5708 }
5709 match t {
5710 "Infinity" | "+Infinity" => f64::INFINITY,
5711 "-Infinity" => f64::NEG_INFINITY,
5712 _ => t.parse::<f64>().unwrap_or(f64::NAN),
5713 }
5714}
5715
5716/// `util.inspect` break length (the width past which entries wrap). Node's default.
5717fn break_length() -> usize {
5718 INSPECT_BREAK_LENGTH.with(|x| x.get())
5719}
5720/// Node's default `compact` setting (the `compact * 4` column cap term).
5721/// Node's DEFAULT `compact` setting, and the initial value of
5722/// `INSPECT_COMPACT`. The grid's column cap is `compact * 4`, so it has to be
5723/// read through `inspect_compact()` at render time: under `{ compact: 1 }` node
5724/// lays a byte array out four columns wide, and the hardcoded 3 gave twelve.
5725const DEFAULT_COMPACT: i64 = 3;
5726/// Node's default `maxArrayLength` — the initial value of
5727/// `INSPECT_MAX_ARRAY_LENGTH`, which `util.inspect(v, { maxArrayLength: N })`
5728/// overrides per call. Read it through `inspect_max_array_length()`, never
5729/// directly: as a bare constant the option had no effect and a 120-element array
5730/// was truncated at 100 even under `maxArrayLength: Infinity`.
5731pub(crate) const DEFAULT_MAX_ARRAY_LENGTH: usize = 100;
5732
5733/// Whether `output` fits on a single line — a faithful port of Node's
5734/// `isBelowBreakLength` (no colors, no `base`). `start` is the caller's seed
5735/// length (braces + indentation + slack).
5736fn is_below_break_length(output: &[String], start: usize) -> bool {
5737 let limit = break_length();
5738 let mut total = output.len() + start;
5739 if total + output.len() > limit {
5740 return false;
5741 }
5742 for o in output {
5743 if o.contains('\n') {
5744 return false;
5745 }
5746 total += o.chars().count();
5747 if total > limit {
5748 return false;
5749 }
5750 }
5751 true
5752}
5753
5754/// Faithful port of Node's `util.inspect` `groupArrayElements`: lay out the
5755/// already-formatted element strings into an aligned multi-column grid. Returns
5756/// `(lines, grouped)` — `grouped` is false when Node would leave the output
5757/// ungrouped (so the caller falls back to single-line / one-per-line).
5758fn group_array_elements(
5759 host: &JsHost,
5760 output: &[String],
5761 values: &[Value],
5762 indentation_lvl: usize,
5763 has_tail: bool,
5764) -> (Vec<String>, bool) {
5765 let separator_space = 2usize; // ", " between entries
5766 // A `... N more items` tail is not an element: node drops it from the grid
5767 // (`outputLength--`) so it neither widens a column nor occupies a cell, then
5768 // re-appends it as its own final line.
5769 let output_length = output.len() - usize::from(has_tail);
5770 let data_len: Vec<usize> = output.iter().map(|o| o.chars().count()).collect();
5771 let mut total_length = 0usize;
5772 let mut max_length = 0usize;
5773 for &len in &data_len[..output_length] {
5774 total_length += len + separator_space;
5775 if len > max_length {
5776 max_length = len;
5777 }
5778 }
5779 let actual_max = max_length + separator_space;
5780 // Only group when ≥3 entries fit across AND the entries aren't wildly uneven.
5781 if !(actual_max * 3 + indentation_lvl < break_length()
5782 && (total_length as f64 / actual_max as f64 > 5.0 || max_length <= 6))
5783 {
5784 return (output.to_vec(), false);
5785 }
5786 let approx_char_heights = 2.5f64;
5787 let average_bias = (actual_max as f64 - total_length as f64 / output_length as f64).sqrt();
5788 let biased_max = (actual_max as f64 - 3.0 - average_bias).max(1.0);
5789 // Ideally a square grid; capped by break length, compact*4, and 15 columns.
5790 let columns = [
5791 ((approx_char_heights * biased_max * output_length as f64).sqrt() / biased_max).round()
5792 as i64,
5793 ((break_length() - indentation_lvl) as f64 / actual_max as f64).floor() as i64,
5794 inspect_compact().saturating_mul(4),
5795 15,
5796 ]
5797 .into_iter()
5798 .min()
5799 .unwrap();
5800 if columns <= 1 {
5801 return (output.to_vec(), false);
5802 }
5803 let columns = columns as usize;
5804 // The widest entry (plus separator) in each column.
5805 let mut max_line_length = vec![0usize; columns];
5806 for (i, slot) in max_line_length.iter_mut().enumerate() {
5807 let mut line_length = 0;
5808 let mut j = i;
5809 while j < output_length {
5810 if data_len[j] > line_length {
5811 line_length = data_len[j];
5812 }
5813 j += columns;
5814 }
5815 *slot = line_length + separator_space;
5816 }
5817 // Right-align (padStart) only when every element is a number/bigint.
5818 let pad_start = values.iter().all(|v| {
5819 matches!(v, Value::Int(_) | Value::Float(_))
5820 || matches!(host.get(v), Some(JsObj::BigInt(_)))
5821 });
5822 let mut tmp = Vec::new();
5823 let mut i = 0;
5824 while i < output_length {
5825 let max = (i + columns).min(output_length);
5826 let mut str_line = String::new();
5827 let mut j = i;
5828 while j < max.saturating_sub(1) {
5829 // `output[j]` has no colors here, so padding == max_line_length[col].
5830 let col = j - i;
5831 let cell = format!("{}, ", output[j]);
5832 let target = max_line_length[col];
5833 str_line.push_str(&pad_to(&cell, target, pad_start));
5834 j += 1;
5835 }
5836 // The last cell of the row: right-aligned entries pad without the ", ".
5837 if pad_start {
5838 let col = j - i;
5839 let target = max_line_length[col] - separator_space;
5840 str_line.push_str(&pad_to(&output[j], target, true));
5841 } else {
5842 str_line.push_str(&output[j]);
5843 }
5844 tmp.push(str_line);
5845 i += columns;
5846 }
5847 if has_tail {
5848 tmp.push(output[output_length].clone());
5849 }
5850 (tmp, true)
5851}
5852
5853/// Pad `s` to `width` chars: right-justified when `pad_start`, else left-justified.
5854/// (Padding is measured in chars; already ANSI-free here.)
5855fn pad_to(s: &str, width: usize, pad_start: bool) -> String {
5856 let len = s.chars().count();
5857 if len >= width {
5858 return s.to_string();
5859 }
5860 let fill = " ".repeat(width - len);
5861 if pad_start {
5862 format!("{fill}{s}")
5863 } else {
5864 format!("{s}{fill}")
5865 }
5866}
5867
5868/// Quote a string the way `util.inspect` does — a port of `strEscape` in Node's
5869/// `lib/internal/util/inspect.js`.
5870///
5871/// The quote character is chosen so the contents need as little escaping as
5872/// possible: single quotes normally, double quotes when the string contains a
5873/// `'` but no `"`, and a backtick when it contains both (and neither a backtick
5874/// nor a `${`). Only the ACTIVE quote is backslash-escaped, alongside `\` and
5875/// the C0 controls + DEL, which use Node's `meta` table (`\n`, `\t`, `\b`,
5876/// `\f`, `\r` short forms; `\x0B`, `\x1F`, `\x7F` uppercase-hex otherwise).
5877fn quote_str(s: &str) -> String {
5878 let quote = if !s.contains('\'') {
5879 '\''
5880 } else if !s.contains('"') {
5881 '"'
5882 } else if !s.contains('`') && !s.contains("${") {
5883 '`'
5884 } else {
5885 '\''
5886 };
5887 let mut out = String::with_capacity(s.len() + 2);
5888 out.push(quote);
5889 for c in s.chars() {
5890 match c {
5891 _ if c == quote => {
5892 out.push('\\');
5893 out.push(c);
5894 }
5895 '\\' => out.push_str("\\\\"),
5896 '\u{8}' => out.push_str("\\b"),
5897 '\t' => out.push_str("\\t"),
5898 '\n' => out.push_str("\\n"),
5899 '\u{c}' => out.push_str("\\f"),
5900 '\r' => out.push_str("\\r"),
5901 '\u{0}'..='\u{1f}' | '\u{7f}' => out.push_str(&format!("\\x{:02X}", c as u32)),
5902 _ => out.push(c),
5903 }
5904 }
5905 out.push(quote);
5906 out
5907}
5908
5909/// Render an object key: bare if it is a valid identifier, quoted otherwise.
5910fn fmt_key(k: &str) -> String {
5911 let ok = !k.is_empty()
5912 && k.chars()
5913 .next()
5914 .map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
5915 .unwrap_or(false)
5916 && k.chars()
5917 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
5918 if ok {
5919 k.to_string()
5920 } else {
5921 quote_str(k)
5922 }
5923}
5924
5925// ── iteration ────────────────────────────────────────────────────────────────
5926
5927impl JsHost {
5928 /// Collect an iterable into a vector of values (arrays, strings, Map/Set).
5929 /// Generators and user `Symbol.iterator` objects go through `iter_all`, which
5930 /// holds no host borrow across resumes.
5931 pub fn iter_vec(&mut self, v: &Value) -> Result<Vec<Value>, String> {
5932 match self.get(v) {
5933 Some(JsObj::Array(items)) => Ok(items.clone()),
5934 Some(JsObj::Str(s)) => {
5935 let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
5936 Ok(chars.into_iter().map(|c| self.new_str(c)).collect())
5937 }
5938 Some(JsObj::Iter { items, idx }) => Ok(items[*idx..].to_vec()),
5939 Some(JsObj::Set { entries, .. }) => Ok(entries.values().cloned().collect()),
5940 Some(JsObj::Map { entries, .. }) => {
5941 // Map iterates as `[key, value]` pairs.
5942 let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
5943 Ok(pairs
5944 .into_iter()
5945 .map(|(k, v)| self.new_array(vec![k, v]))
5946 .collect())
5947 }
5948 // A `Buffer` iterates over its BYTES and a typed array over its
5949 // ELEMENTS — both are iterable in Node. Only `@@bytes` was handled
5950 // here, so `[...buf]` worked while `[...new Uint8Array([1])]` threw
5951 // "object is not iterable", which is the same invariant holding at
5952 // one of its two sites.
5953 Some(JsObj::Object(props))
5954 if props.contains_key("@@bytes") || props.contains_key("@@buffer") =>
5955 {
5956 // Iterating a view over a DETACHED buffer throws, naming the
5957 // `values` iterator — reading its elements answers zero length,
5958 // but spreading it is a method call and does not.
5959 if crate::stdlib::typedarray::view_detached_h(self, v) {
5960 return Err(crate::stdlib::typedarray::detached_error(
5961 "%TypedArray%.prototype",
5962 "values",
5963 false,
5964 ));
5965 }
5966 Ok(crate::stdlib::typedarray::elems_mut_host(self, v))
5967 }
5968 // V8 names the VALUE, not its type: `[...5]` is `5 is not iterable`,
5969 // `[...{}]` is `{} is not iterable`. Reporting `typeof` instead
5970 // produced `number is not iterable`, which no engine emits.
5971 _ => {
5972 let shown = self.inspect(v);
5973 Err(type_error(&format!("{shown} is not iterable")))
5974 }
5975 }
5976 }
5977
5978 /// Enumerable string keys of an object/array (for `for-in`). Internal
5979 /// symbol-keyed props (`@@…`) are not enumerable.
5980 /// `for-in` visits own enumerable keys, then every *inherited* enumerable key
5981 /// not already seen, walking the whole prototype chain. Class methods and the
5982 /// builtin prototypes are non-enumerable, so in practice this only surfaces
5983 /// keys a script put on a prototype itself (`F.prototype.y = 2`) — but that
5984 /// is exactly the constructor-function idiom older packages are written in.
5985 pub fn enum_keys(&mut self, v: &Value) -> Vec<Value> {
5986 let mut keys = self.own_enum_key_names(v);
5987 let mut cur = self.proto_of(v);
5988 let mut hops = 0;
5989 while let Some(p) = cur {
5990 // A cyclic or pathologically deep chain must not hang the loop.
5991 hops += 1;
5992 if hops > 100 || matches!(p, Value::Undef) || self.is_null(&p) {
5993 break;
5994 }
5995 for k in self.own_enum_key_names(&p) {
5996 if !keys.contains(&k) {
5997 keys.push(k);
5998 }
5999 }
6000 cur = self.proto_of(&p);
6001 }
6002 keys.into_iter().map(|k| self.new_str(k)).collect()
6003 }
6004
6005 /// The own *enumerable* string keys of `v`, in property order — the single
6006 /// source of truth behind `for-in`, `Object.keys`/`values`/`entries`,
6007 /// object spread, `Object.assign` and `JSON.stringify`. Internal slots
6008 /// (`@@…`), private fields (`#…`) and anything marked non-enumerable via
6009 /// `prop_attrs` are excluded.
6010 pub fn own_enum_key_names(&self, v: &Value) -> Vec<String> {
6011 self.own_key_names(v, true)
6012 }
6013
6014 /// Own string keys of `v` in insertion order. `enum_only` drops the
6015 /// non-enumerable ones (`Object.keys`); otherwise every own key is reported
6016 /// (`getOwnPropertyNames`/`Reflect.ownKeys`).
6017 pub fn own_key_names(&self, v: &Value, enum_only: bool) -> Vec<String> {
6018 let mut keys = self.own_enum_data_keys(v, enum_only);
6019 // A global a SCRIPT created (`x = 1` with no declaration) is an own
6020 // ENUMERABLE property of the global object, but lives in the globals map
6021 // rather than in its property map — so no listing saw it, while
6022 // `globalThis.x` read it back and its descriptor called it enumerable.
6023 if self.is_global_object(v) {
6024 for k in self.globals.keys() {
6025 if !keys.contains(k) {
6026 keys.push(k.clone());
6027 }
6028 }
6029 }
6030 // A RegExp's `lastIndex` is a SYNTHESIZED own property — it lives in the
6031 // `RegExpObj` struct, not a property map — so nothing above can list it.
6032 // Non-enumerable, so only `getOwnPropertyNames` sees it.
6033 if !enum_only && matches!(self.get(v), Some(JsObj::RegExp(_))) {
6034 keys.push("lastIndex".to_string());
6035 }
6036 // An accessor defined before its object had any ordering marker (a class
6037 // prototype accessor, say) still has to appear.
6038 for k in self.own_accessor_keys(v) {
6039 if (!enum_only || self.prop_attrs(v, &k).enumerable) && !keys.contains(&k) {
6040 keys.push(k);
6041 }
6042 }
6043 keys
6044 }
6045
6046 /// The keys that own a slot in the object's property map, in insertion
6047 /// order, resolving accessor ordering markers back to their real key.
6048 /// Every global a SCRIPT created, in creation order — the own enumerable
6049 /// keys of the global object that live in the globals map rather than in
6050 /// its property map. `x = 1` with no declaration makes one, and
6051 /// `Object.keys(globalThis)` reports it in node.
6052 pub fn script_global_names(&self) -> Vec<String> {
6053 self.globals.keys().cloned().collect()
6054 }
6055 /// Drop a global a script created. Reports whether it was there.
6056 pub fn remove_global(&mut self, name: &str) -> bool {
6057 self.globals.shift_remove(name).is_some()
6058 }
6059 fn own_enum_data_keys(&self, v: &Value, enum_only: bool) -> Vec<String> {
6060 match self.get(v) {
6061 // A `Buffer` is an index-keyed exotic: its own enumerable keys are
6062 // `"0".."len-1"` (the bytes live in the hidden `@@bytes` slot), never
6063 // the `length`/`byteLength` view metadata, which V8 keeps on the
6064 // prototype chain or as non-enumerable own slots.
6065 // A `Buffer` and every other typed array are index-keyed exotics:
6066 // their own enumerable keys are `"0".."len-1"` (the elements live in
6067 // a hidden slot), never the `length`/`byteLength` view metadata,
6068 // which V8 keeps on the prototype chain or as non-enumerable own
6069 // slots. Only `Buffer` had this arm, so `Object.keys(u8)` was empty
6070 // and `JSON.stringify(u8)` was `{}` where node gives
6071 // `{"0":10,"1":9}` — `hasOwnProperty(0)` already answered true, so
6072 // the two views of the same question disagreed.
6073 Some(JsObj::Object(props))
6074 if matches!(
6075 props.get("@@native").map(|t| self.str_of(t)).as_deref(),
6076 Some("Buffer") | Some("TypedArray")
6077 ) =>
6078 {
6079 // A view over a DETACHED buffer has no index properties at all:
6080 // its own `length` still holds the old count, so reading that
6081 // back left `Object.keys` listing eight names over no bytes.
6082 if crate::stdlib::typedarray::view_detached_h(self, v) {
6083 return Vec::new();
6084 }
6085 // A Buffer counts its byte store; every other view reports the
6086 // element count of its window onto the ArrayBuffer.
6087 let n = match props.get("@@bytes").and_then(|b| self.get(b)) {
6088 Some(JsObj::Array(items)) => items.len(),
6089 _ => props
6090 .get("length")
6091 .map(|l| self.to_number(l))
6092 .unwrap_or(0.0) as usize,
6093 };
6094 (0..n).map(|i| i.to_string()).collect()
6095 }
6096 Some(JsObj::Object(props)) => props
6097 .keys()
6098 .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6099 Some(real) => Some(real.to_string()),
6100 None if !k.starts_with("@@") && !k.starts_with('#') => Some(k.clone()),
6101 None => None,
6102 })
6103 .filter(|k| !enum_only || self.prop_attrs(v, k).enumerable)
6104 .collect(),
6105 // A STRING is an index-keyed exotic too (10.4.3): its own keys are
6106 // its UTF-16 code-unit indices, plus the non-enumerable `length`.
6107 // Without this arm every whole-object view of a string primitive was
6108 // empty — `for (const k in 'ab')` iterated nothing, `Object.keys`
6109 // and `Object.assign({}, 'ab')` reported `{}` — while `'ab'[0]` and
6110 // `'ab'.length` answered normally, so the two views disagreed. The
6111 // spread form `{...'ab'}` went through a different path and was
6112 // already right, which is what made the gap easy to miss.
6113 Some(JsObj::Str(s)) => {
6114 let mut keys: Vec<String> =
6115 (0..crate::utf16::len(s)).map(|i| i.to_string()).collect();
6116 if !enum_only {
6117 keys.push("length".into());
6118 }
6119 keys
6120 }
6121 // `OrdinaryOwnPropertyKeys` on an array exotic: the integer indices
6122 // ascending, then the exotic non-enumerable `length`, then the
6123 // ordinary string keys in insertion order. Those ordinary keys have
6124 // no property map to live in — a `str.match()` result's
6125 // `index`/`input`/`groups` and any user-assigned `arr.foo` are kept
6126 // in the fn-prop side table — so they are read back from there.
6127 Some(JsObj::Array(items)) => {
6128 // An ELIDED element is not an own property at all, so it
6129 // contributes no key — the difference behind
6130 // `Object.keys([1,,3])` being `['0','2']`.
6131 let mut keys: Vec<String> = (0..items.len())
6132 .filter(|i| !self.is_hole(v, *i))
6133 .map(|i| i.to_string())
6134 .collect();
6135 if !enum_only {
6136 keys.push("length".into());
6137 }
6138 keys.extend(self.fn_prop_keys(v).into_iter().filter(|k| {
6139 !k.starts_with("@@")
6140 && !k.starts_with('#')
6141 && (!enum_only || self.prop_attrs(v, k).enumerable)
6142 }));
6143 keys
6144 }
6145 // A function/class keeps every own property in the side table. Its
6146 // exotic `name`/`length`/`prototype` and its class methods are all
6147 // non-enumerable, so under `enum_only` what is left is exactly what
6148 // a script assigned; `getOwnPropertyNames` reports the exotics too,
6149 // in V8's order (`length`, `name`, `prototype`, then the rest).
6150 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
6151 let mut keys: Vec<String> = Vec::new();
6152 if !enum_only {
6153 keys.push("length".into());
6154 keys.push("name".into());
6155 if self.owns_prototype(v) {
6156 keys.push("prototype".into());
6157 }
6158 }
6159 let rest: Vec<String> = self
6160 .fn_prop_keys(v)
6161 .into_iter()
6162 // An accessor's ordering marker resolves back to its real
6163 // key, so a static getter enumerates where it was declared.
6164 .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6165 Some(real) => Some(real.to_string()),
6166 None if !k.starts_with("@@") && !k.starts_with('#') => Some(k),
6167 None => None,
6168 })
6169 .filter(|k| {
6170 !keys.contains(k) && (!enum_only || self.prop_attrs(v, k).enumerable)
6171 })
6172 .collect();
6173 keys.extend(rest);
6174 keys
6175 }
6176 // A builtin namespace (`require('buffer')`, `Buffer`) enumerates the
6177 // members node-js implements, so a package that copies a namespace
6178 // key-by-key gets the working set instead of an empty object.
6179 Some(JsObj::Builtin(ns)) => crate::stdlib::namespace_keys(&ns.clone()),
6180 // A `Map`/`Set`/`Promise`/`RegExp`/generator holds only its internal
6181 // slots, so what a script assigned lives in the side table — and is
6182 // just as much an own property as an object's.
6183 Some(_) => self
6184 .fn_prop_keys(v)
6185 .into_iter()
6186 .filter(|k| {
6187 !k.starts_with("@@")
6188 && !k.starts_with('#')
6189 && (!enum_only || self.prop_attrs(v, k).enumerable)
6190 })
6191 .collect(),
6192 _ => Vec::new(),
6193 }
6194 }
6195
6196 /// The own enumerable `(key, value)` pairs of `v`. Buffer index keys resolve
6197 /// through the byte store; everything else reads the property map. Own
6198 /// accessor keys come back as `Undef` here — `own_enum_entries_deep` runs
6199 /// their getters, which cannot happen under the host borrow.
6200 pub fn own_enum_entries(&self, v: &Value) -> Vec<(String, Value)> {
6201 self.own_enum_key_names(v)
6202 .into_iter()
6203 .map(|k| {
6204 let val = match self.get(v) {
6205 // A Buffer's index keys read out of the hidden `@@bytes`
6206 // array; resolve inline rather than through
6207 // `buffer::byte_get`, which would re-borrow the host.
6208 Some(JsObj::Object(props)) => props.get(&k).cloned().unwrap_or_else(|| {
6209 // A Buffer's elements live in `@@bytes` and every
6210 // other typed array's in `@@elems`; both are index
6211 // keys with no entry in the property map.
6212 match k.parse::<usize>() {
6213 Ok(i) => crate::stdlib::typedarray::elems_with_host(self, v)
6214 .get(i)
6215 .cloned()
6216 .unwrap_or(Value::Undef),
6217 _ => Value::Undef,
6218 }
6219 }),
6220 // A Map/Set/Promise/RegExp/generator keeps every own
6221 // property in the side table.
6222 Some(
6223 JsObj::Map { .. }
6224 | JsObj::Set { .. }
6225 | JsObj::Promise { .. }
6226 | JsObj::RegExp(_)
6227 | JsObj::Generator { .. }
6228 | JsObj::Symbol { .. }
6229 | JsObj::BigInt(_)
6230 | JsObj::Iter { .. },
6231 ) => self.fn_prop(v, &k).unwrap_or(Value::Undef),
6232 // An index reads the element; any other own key (`foo`,
6233 // a match result's `index`) lives in the side table.
6234 Some(JsObj::Array(items)) => k
6235 .parse::<usize>()
6236 .ok()
6237 .and_then(|i| items.get(i).cloned())
6238 .or_else(|| self.fn_prop(v, &k))
6239 .unwrap_or(Value::Undef),
6240 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
6241 self.fn_prop(v, &k).unwrap_or(Value::Undef)
6242 }
6243 _ => Value::Undef,
6244 };
6245 (k, val)
6246 })
6247 .collect()
6248 }
6249}
6250
6251/// The own enumerable `(key, value)` pairs of `v` with every enumerable own
6252/// accessor's getter invoked — the observable shape `Object.values`,
6253/// `Object.entries`, object spread and `JSON.stringify` all need. Must be called
6254/// outside a `with_host` borrow because a getter re-enters the host.
6255pub fn own_enum_entries_deep(v: &Value) -> Result<Vec<(String, Value)>, String> {
6256 // A Proxy has no property map at all: its own enumerable entries come from
6257 // the `ownKeys` + `getOwnPropertyDescriptor` + `get` traps. A trap that
6258 // throws surfaces as an empty result here because this signature is
6259 // infallible; the callers that MUST propagate a trap throw (`Object.keys`
6260 // and friends) go through `builtins::object_keys`, which does.
6261 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
6262 return crate::proxy::own_enum_entries(v);
6263 }
6264 // A builtin namespace (`require('path')`, `Buffer`) has no property map at
6265 // all: its members are resolved on demand by `namespace_property`, which
6266 // re-enters the host and so cannot run inside `own_enum_entries`'s borrow.
6267 // Without this, spread and `Object.assign` copied the namespace's KEYS with
6268 // `undefined` for every value — measured against node v26.7.0,
6269 // `{...require('path')}.join` was `undefined` here and a function there,
6270 // while `Object.entries(require('path'))` (which resolves through
6271 // `builtins`, not through this borrow) was already correct. Two enumeration
6272 // paths, one of them silently value-less.
6273 if let Some(ns) = with_host(|h| match h.get(v) {
6274 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6275 _ => None,
6276 }) {
6277 return Ok(with_host(|h| h.own_enum_key_names(v))
6278 .into_iter()
6279 .map(|k| {
6280 let val = crate::builtins::namespace_property(&ns, &k);
6281 (k, val)
6282 })
6283 .collect());
6284 }
6285 // A string primitive's own entries are its code units. `own_enum_entries`
6286 // cannot build them: allocating the one-character string for each index
6287 // needs `&mut` host access, and it runs under a shared borrow.
6288 if let Some(sv) = with_host(|h| match h.get(v) {
6289 Some(JsObj::Str(s)) => Some(s.clone()),
6290 _ => None,
6291 }) {
6292 let units = crate::utf16::Units::of(&sv);
6293 return Ok(with_host(|h| {
6294 (0..units.len())
6295 .filter_map(|i| units.unit_str(i).map(|c| (i.to_string(), h.new_str(c))))
6296 .collect()
6297 }));
6298 }
6299 let accessor_keys: Vec<String> = with_host(|h| {
6300 h.own_accessor_keys(v)
6301 .into_iter()
6302 .filter(|k| h.prop_attrs(v, k).enumerable)
6303 .collect()
6304 });
6305 let entries = with_host(|h| h.own_enum_entries(v));
6306 // A getter that THROWS propagates: `Object.entries`, `Object.assign`,
6307 // object spread and `JSON.stringify` all read through here, and every one
6308 // of them swallowed the exception and reported the property as absent (or
6309 // as `null`) instead.
6310 let mut out = Vec::with_capacity(entries.len());
6311 for (k, val) in entries {
6312 if accessor_keys.contains(&k) {
6313 out.push((k.clone(), get_prop_chain(v, &k)?));
6314 } else {
6315 out.push((k, val));
6316 }
6317 }
6318 Ok(out)
6319}
6320
6321// ── function invocation ──────────────────────────────────────────────────────
6322
6323/// Marshal a JS call argument into a native fusevm `Value` for `rust { }` FFI.
6324/// JS strings ride as `Value::Obj(JsObj::Str)` heap handles, which fusevm's
6325/// marshaller cannot read (it calls `Value::to_str`, which returns `"(obj:N)"`
6326/// for a handle); rewrite them to a native `Value::Str`. Numbers are already
6327/// native `Value::Int`/`Value::Float`, so they pass through (fusevm coerces
6328/// Float→i64/f64 per the export signature).
6329fn marshal_ffi_arg(v: &Value) -> Value {
6330 match v {
6331 Value::Obj(_) => match with_host(|h| h.as_str(v)) {
6332 Some(s) => Value::str(s),
6333 None => v.clone(),
6334 },
6335 _ => v.clone(),
6336 }
6337}
6338
6339/// Resolve a bare name and call it (`f(args)`, `parseInt(args)`).
6340pub fn call_named(name: &str, args: Vec<Value>) -> Result<Value, String> {
6341 // Inline Rust FFI: the `rust { ... }` desugar emits `__rust_compile(b64,
6342 // line)`; compile + register the block's exported functions, returning JS
6343 // `undefined` (`Value::Undef`).
6344 if name == "__rust_compile" {
6345 let b64 = args
6346 .first()
6347 .map(|v| with_host(|h| h.str_of(v)))
6348 .unwrap_or_default();
6349 return fusevm::ffi::compile_and_register(&b64).map(|_| Value::Undef);
6350 }
6351 if let Some(v) = with_host(|h| h.read_name(name)) {
6352 return invoke(&v, args, None);
6353 }
6354 // A DIRECT eval — the literal `eval(src)` call form — is the ONLY one that
6355 // evaluates in the CALLER's scope; `(0, eval)(src)`, `const e = eval; e(src)`
6356 // and `[eval][0](src)` all reach the same function value but are INDIRECT
6357 // evals and evaluate in the global scope (ECMA-262 19.2.1.1 `PerformEval`).
6358 // This is the one place the two forms are distinguishable without a compiler
6359 // change: `call_named` is reached only from `ops::CALL`, which the compiler
6360 // emits exclusively for a bare-identifier callee, while every value-call form
6361 // goes through `invoke` → `call_builtin_function`. The `read_name` miss above
6362 // has already established that `eval` is not shadowed by a user binding.
6363 if name == "eval" {
6364 return crate::builtins::eval_source(args.first(), true);
6365 }
6366 if crate::builtins::is_known_builtin(name) {
6367 return crate::builtins::call_builtin_function(name, args);
6368 }
6369 // A `rust { ... }` block's exported functions are callable by bareword.
6370 // Reached only after user names/globals and builtins all miss, so JS code
6371 // always wins; the registry membership check keeps this off the hot path.
6372 if fusevm::ffi::is_registered(name) {
6373 let margs: Vec<Value> = args.iter().map(marshal_ffi_arg).collect();
6374 if let Some(r) = fusevm::ffi::try_call(name, &margs) {
6375 return r;
6376 }
6377 }
6378 Err(ref_error(name))
6379}
6380
6381thread_local! {
6382 /// The constructor a builtin STATIC is currently being invoked on.
6383 ///
6384 /// `A.from(x)` on `class A extends Array` re-dispatches against the `Array`
6385 /// builtin, which is reached by NAME and so cannot see `A`. The species
6386 /// rules need it: `Array.from`, `Array.of` and every `Promise` static build
6387 /// their result with `this`, so on a subclass they must construct through
6388 /// it. A stack, since one static can call another.
6389 static STATIC_THIS: std::cell::RefCell<Vec<Value>> =
6390 const { std::cell::RefCell::new(Vec::new()) };
6391}
6392
6393/// Run `f` with `recv` recorded as the receiver of a builtin static call.
6394pub fn with_static_this<R>(recv: &Value, f: impl FnOnce() -> R) -> R {
6395 STATIC_THIS.with(|s| s.borrow_mut().push(recv.clone()));
6396 let out = f();
6397 STATIC_THIS.with(|s| {
6398 s.borrow_mut().pop();
6399 });
6400 out
6401}
6402
6403/// The constructor the running builtin static was called on, if it was reached
6404/// through a subclass rather than directly.
6405pub fn current_static_this() -> Option<Value> {
6406 STATIC_THIS.with(|s| s.borrow().last().cloned())
6407}
6408
6409/// `recv.name(args)`.
6410pub fn call_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
6411 // `undefined.foo()` is a `[[Get]]` and THEN a call (13.3.6 EvaluateCall), so
6412 // the failure is the property read, not the call: node reports
6413 // `Cannot read properties of undefined (reading 'foo')`. node-js ran the
6414 // whole method dispatch against the nullish receiver, found nothing, and
6415 // reported `undefined.foo is not a function` — the wrong error class of
6416 // message for the single most common runtime fault in JS, and one that
6417 // points at the callee instead of at the base that was nullish.
6418 if with_host(|h| h.is_nullish(recv)) {
6419 return Err(type_error(&format!(
6420 "Cannot read properties of {} (reading '{name}')",
6421 with_host(|h| h.str_of(recv))
6422 )));
6423 }
6424 // `this.#m(…)` is a `[[PrivateGet]]` followed by a call, so the brand check
6425 // comes first: an unbranded receiver throws here rather than reporting the
6426 // method missing. Only a `#`-prefixed name pays the extra probe.
6427 if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
6428 return Err(crate::builtins::private_brand_message(name, false));
6429 }
6430 // `proxy.m(…)` is 13.3.6 `EvaluateCall`: `Get(proxy, "m")` — through the
6431 // `get` trap — then a call with the PROXY as `this`. The `lookup_*` shortcuts
6432 // below all read a property map a proxy does not have.
6433 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
6434 let f = crate::builtins::get_property(recv, name)?;
6435 if !with_host(|h| is_callable(h, &f)) {
6436 return Err(type_error(&format!("{name} is not a function")));
6437 }
6438 // `Function.prototype.call`/`apply`/`bind`/`toString` and the REFLECTIVE
6439 // `Object.prototype` methods are generic over `this`. node-js models each
6440 // as a thunk BOUND to the object it was read off — through a proxy, that
6441 // is the target — so invoking the thunk answers for the target and skips
6442 // the traps entirely: `pf.call(1, 2)` never reached the `apply` trap and
6443 // `p.hasOwnProperty(k)` never reached the descriptor trap. Re-dispatch
6444 // those against the PROXY, which is the `this` the real method receives.
6445 //
6446 // `toString`/`valueOf`/`toLocaleString` are deliberately NOT re-dispatched
6447 // for a non-callable proxy: they resolve by the TARGET's kind (a proxy of
6448 // an array stringifies `1,2` through `Array.prototype.toString`, not
6449 // `[object Object]`), which the bound thunk already gets right.
6450 if with_host(|h| matches!(h.get(&f), Some(JsObj::BoundMethod { .. }))) {
6451 if with_host(|h| is_callable(h, recv)) {
6452 if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6453 return Ok(r);
6454 }
6455 }
6456 if matches!(
6457 name,
6458 "hasOwnProperty" | "propertyIsEnumerable" | "isPrototypeOf"
6459 ) {
6460 return crate::builtins::object_builtin_method(recv, name, args);
6461 }
6462 // The three above resolve by the TARGET's kind, and the thunk is
6463 // already bound to the target — so it must be invoked WITHOUT a
6464 // receiver override. Passing the proxy as `this` made the
6465 // `BoundMethod` arm of `invoke` prefer it over its own receiver and
6466 // call straight back into this branch, so `String(new Proxy({}, {}))`
6467 // recursed until the stack overflowed and the process aborted.
6468 if matches!(name, "toString" | "valueOf" | "toLocaleString") {
6469 return invoke(&f, args, None);
6470 }
6471 }
6472 return invoke(&f, args, Some(recv.clone()));
6473 }
6474 // Namespace builtins (`console`, `Math`, `JSON`, ...): dispatch by qualified
6475 // name.
6476 if let Some(ns) = with_host(|h| match h.get(recv) {
6477 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6478 _ => None,
6479 }) {
6480 let qualified = format!("{ns}.{name}");
6481 if crate::builtins::is_known_builtin(&qualified) {
6482 return crate::builtins::call_builtin_function(&qualified, args);
6483 }
6484 }
6485 // Object / instance: an accessor getter that yields a function, an own or
6486 // inherited method (class methods live on the prototype chain), then an
6487 // Object.prototype builtin (hasOwnProperty …). Resolve via `lookup_*`
6488 // directly — NOT get_property — so the Object.prototype-builtin fallback
6489 // never routes back through a BoundMethod and recurses.
6490 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
6491 // A native stdlib instance (`Buffer`/crypto `Hash`/`EventEmitter`/`URL`/
6492 // fs `Stats`/http `ServerResponse`…) carries a hidden `@@native` tag.
6493 // A user-added or reparented-prototype method takes precedence over the
6494 // native dispatcher — matching JS resolution order (own → prototype
6495 // chain). This is what lets Express work: it does
6496 // `Object.setPrototypeOf(res, app.response)` and calls `res.send(...)`,
6497 // where `send` is a plain function on the reparented prototype. Native
6498 // instance methods (`res.end`/`write`/…) are NOT stored as plain
6499 // function properties, so `lookup_chain` misses them and we fall through
6500 // to `instance_call` for the real native behavior.
6501 if let Some(tag) = crate::stdlib::native_tag(recv) {
6502 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6503 if with_host(|h| is_callable(h, &f)) {
6504 return invoke(&f, args, Some(recv.clone()));
6505 }
6506 }
6507 // `Object.prototype` methods reach a native instance too — a Buffer
6508 // inherits `hasOwnProperty`/`isPrototypeOf` through its prototype
6509 // chain, and the native dispatcher has no entry for them.
6510 if crate::builtins::is_object_builtin_method(name)
6511 && !crate::stdlib::instance_has_method(&tag, name)
6512 {
6513 return crate::builtins::object_builtin_method(recv, name, args);
6514 }
6515 return crate::stdlib::instance_call(&tag, recv, name, args);
6516 }
6517 // A primitive wrapper forwards to the primitive's method table, the
6518 // same way a native instance forwards to its tag's. A user method on
6519 // the wrapper or anywhere on its chain still wins first.
6520 if let Some(prim) = crate::builtins::wrapped_primitive(recv) {
6521 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6522 if with_host(|h| is_callable(h, &f)) {
6523 return invoke(&f, args, Some(recv.clone()));
6524 }
6525 }
6526 // The reflective `Object.prototype` methods answer for the WRAPPER
6527 // — `w.hasOwnProperty("0")` asks about the wrapper's own index
6528 // properties, not about the string.
6529 if crate::builtins::is_object_builtin_method(name) {
6530 return crate::builtins::object_builtin_method(recv, name, args);
6531 }
6532 return call_method(&prim, name, args);
6533 }
6534 if let Some((Some(getter), _)) = with_host(|h| lookup_accessor(h, recv, name)) {
6535 let f = invoke(&getter, Vec::new(), Some(recv.clone()))?;
6536 if with_host(|h| is_callable(h, &f)) {
6537 return invoke(&f, args, Some(recv.clone()));
6538 }
6539 }
6540 // A Proxy in the prototype chain serves the method through its `get`
6541 // trap. `lookup_chain` below reads property maps, which a proxy has none
6542 // of, so without this `child.m()` on `Object.create(proxy)` reported
6543 // "m is not a function" even though `child.m` already read correctly.
6544 if crate::builtins::proxy_proto_link(recv, name).is_some() {
6545 let f = crate::builtins::get_property(recv, name)?;
6546 if !with_host(|h| is_callable(h, &f)) {
6547 return Err(type_error(&format!("{name} is not a function")));
6548 }
6549 return invoke(&f, args, Some(recv.clone()));
6550 }
6551 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6552 if with_host(|h| is_callable(h, &f)) {
6553 return invoke(&f, args, Some(recv.clone()));
6554 }
6555 return Err(type_error(&format!("{name} is not a function")));
6556 }
6557 // A method patched onto `Object.prototype`. `lookup_chain` cannot find
6558 // it: a plain object is not LINKED to the intrinsic prototype object,
6559 // its `Object.prototype` members are synthesized instead. So
6560 // `Object.prototype.tap = f; ({}).tap()` reported "is not a function"
6561 // while `({}).tap` already read back as `f`.
6562 if let Some(f) = crate::builtins::inherited_builtin_static(recv, name) {
6563 if with_host(|h| is_callable(h, &f)) {
6564 return invoke(&f, args, Some(recv.clone()));
6565 }
6566 }
6567 // A method from an intrinsic prototype this object's CHAIN passes
6568 // through — `Object.create(Array.prototype).push(1)`. The read already
6569 // resolves it through the same owner oracle; dispatch reported "is not
6570 // a function", the read and the call disagreeing once more.
6571 if let Some(owner) = crate::builtins::inherited_method_owner_pub(recv, name) {
6572 if owner != "Object" {
6573 return crate::builtins::proto_method(recv, &format!("{owner}:{name}"), args);
6574 }
6575 }
6576 if crate::builtins::is_object_builtin_method(name) {
6577 return crate::builtins::object_builtin_method(recv, name, args);
6578 }
6579 if name == "constructor" {
6580 if let Some(r) = call_default_ctor(recv, &args) {
6581 return r;
6582 }
6583 }
6584 return Err(type_error(&format!("{name} is not a function")));
6585 }
6586 // Function value methods: call / apply / bind, then any static method stored
6587 // on the function object.
6588 if matches!(
6589 with_host(|h| h.kind_of(recv)),
6590 Some(ObjKind::Func)
6591 | Some(ObjKind::Class)
6592 | Some(ObjKind::BoundFunc)
6593 | Some(ObjKind::BoundMethod)
6594 | Some(ObjKind::Builtin)
6595 ) {
6596 if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6597 return Ok(r);
6598 }
6599 // A static method (own or inherited): `this` is the constructor (`recv`).
6600 let stat = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6601 with_host(|h| h.class_static(recv, name))
6602 } else {
6603 with_host(|h| h.fn_prop(recv, name))
6604 };
6605 if let Some(f) = stat {
6606 if with_host(|h| is_callable(h, &f)) {
6607 return invoke(&f, args, Some(recv.clone()));
6608 }
6609 }
6610 // `class_static` only walks user-class `extends` links, so a chain that
6611 // bottoms out in a BUILTIN constructor (`class D extends Array {}`)
6612 // could not reach that builtin's statics: `D.from([1,2])` threw
6613 // "from is not a function" even though `typeof D.from` said `function`.
6614 // Re-dispatch the call against that ancestor, which is what reaches a
6615 // builtin namespace's methods.
6616 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6617 if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
6618 if with_host(|h| h.kind_of(&anc)) == Some(ObjKind::Builtin) {
6619 // The subclass is recorded so a species-aware static
6620 // (`Array.from`, `Promise.resolve`, …) builds its result
6621 // through it rather than through the builtin.
6622 return with_static_this(recv, || call_method(&anc, name, args));
6623 }
6624 }
6625 }
6626 // A method inherited via the function's [[Prototype]] chain (set with
6627 // `Object.setPrototypeOf(fn, proto)`) — the `router` package's router
6628 // functions inherit `route`/`use`/`get`/… from `Router.prototype`.
6629 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6630 if with_host(|h| is_callable(h, &f)) {
6631 return invoke(&f, args, Some(recv.clone()));
6632 }
6633 }
6634 // An `Object.prototype` method invoked with a builtin namespace/prototype
6635 // as `this` (`hasOwnProperty.call(Map.prototype, 'get')`, the get-intrinsic
6636 // ownership probe) — dispatch it against the builtin receiver.
6637 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin)
6638 && crate::builtins::is_object_builtin_method(name)
6639 {
6640 return crate::builtins::object_builtin_method(recv, name, args);
6641 }
6642 }
6643 if name == "constructor" {
6644 if let Some(r) = call_default_ctor(recv, &args) {
6645 return r;
6646 }
6647 }
6648 // Type methods (array/string/number, Map/Set/Symbol/generator methods).
6649 crate::builtins::call_type_method(recv, name, args)
6650}
6651
6652/// `x.constructor(...)` invoked as a CALL when nothing on `x`'s prototype chain
6653/// owns a `constructor` slot.
6654///
6655/// Reading the property already resolves a builtin instance's native constructor
6656/// (the `constructor` arm of `builtins::get_property`), but the CALL path only
6657/// consulted the prototype chain, so the two disagreed:
6658/// `(function(){}).constructor === Function` read `true` while
6659/// `(function(){}).constructor('return 9')` threw
6660/// `TypeError: constructor is not a function`. That call form is exactly how
6661/// `get-intrinsic` — a transitive dependency of express — reaches the `Function`
6662/// constructor. Resolved here through the same one definition the read uses, so
6663/// the two can no longer drift apart. `None` means "not resolvable/callable",
6664/// leaving the caller's original error in place.
6665fn call_default_ctor(recv: &Value, args: &[Value]) -> Option<Result<Value, String>> {
6666 let ctor = crate::builtins::get_property(recv, "constructor").ok()?;
6667 with_host(|h| is_callable(h, &ctor)).then(|| invoke(&ctor, args.to_vec(), None))
6668}
6669
6670/// Call any callable value.
6671pub fn invoke(callable: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6672 // `[[Call]]` on a Proxy runs the `apply` trap (or forwards to the target).
6673 // Probed by kind first so the ordinary call path never clones its arguments.
6674 if with_host(|h| h.kind_of(callable)) == Some(ObjKind::Proxy) {
6675 return crate::proxy::apply(callable, args, this).map(|r| r.expect("kind_of said Proxy"));
6676 }
6677 let obj = with_host(|h| h.get(callable).cloned());
6678 match obj {
6679 // A builtin-prototype method thunk (`Object.prototype.toString`): dispatch
6680 // against the invoke-time `this` (supplied by `.call`/`.apply`).
6681 Some(JsObj::Builtin(name)) if name.starts_with("@proto:") => {
6682 let recv = this.unwrap_or(Value::Undef);
6683 crate::builtins::proto_method(&recv, &name["@proto:".len()..], args)
6684 }
6685 // An intrinsic prototype's GETTER, borrowed off its descriptor — the
6686 // form a library uses to read a slot from an arbitrary receiver
6687 // (`Object.getOwnPropertyDescriptor(Map.prototype, 'size').get
6688 // .call(m)`). It brand-checks `this` and reads, or throws naming
6689 // itself.
6690 // The setter half of the `arguments`/`caller` poison pill — the only
6691 // intrinsic accessor here that has one, and it throws like its getter.
6692 Some(JsObj::Builtin(name)) if name.starts_with("@protoset:") => {
6693 let _ = &name;
6694 let recv = this.unwrap_or(Value::Undef);
6695 // The setter half accepts silently for the same receivers the
6696 // getter answers for, and throws for the rest.
6697 if with_host(|h| h.fn_is_sloppy(&recv)) {
6698 Ok(Value::Undef)
6699 } else {
6700 Err(type_error(crate::builtins::POISON_PILL))
6701 }
6702 }
6703 Some(JsObj::Builtin(name)) if name.starts_with("@protoget:") => {
6704 let recv = this.unwrap_or(Value::Undef);
6705 let rest = &name["@protoget:".len()..];
6706 let (ctor, key) = rest.split_once(':').unwrap_or((rest, ""));
6707 crate::builtins::proto_getter_call(ctor, key, &recv)
6708 }
6709 // `NativeCtor.call(obj, …)` — ES5 "constructor stealing", still shipped by
6710 // libraries that predate `class`. `iconv-lite`'s internal codec is exactly
6711 // this:
6712 //
6713 // function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
6714 // InternalDecoder.prototype = StringDecoder.prototype;
6715 //
6716 // A native constructor builds a fresh tagged object, so initializing the
6717 // SUPPLIED object means building one and moving its slots across.
6718 //
6719 // The guard is deliberately narrow: `obj` must already inherit from THIS
6720 // constructor's prototype, i.e. the subclass really did adopt it. Without
6721 // that, `Date.call(x)` and `Buffer.call(x)` — which in JS ignore `this` and
6722 // return a string / a buffer — would start mutating `x` instead.
6723 Some(JsObj::Builtin(ref name)) if steals_ctor(name, this.as_ref()) => {
6724 let target = this.expect("guard checked");
6725 let built = crate::stdlib::construct(name, &args)
6726 .expect("guard checked a native constructor")?;
6727 adopt_native_slots(&target, &built);
6728 Ok(Value::Undef)
6729 }
6730 Some(JsObj::Builtin(name)) => crate::builtins::call_builtin_function(&name, args),
6731 Some(JsObj::Func(fv)) => run_user_func_of(&fv, args, this, Some(callable.clone())),
6732 // A method read off an object is modelled as a thunk BOUND to it, but an
6733 // explicit `.call`/`.apply` receiver still wins — `Function.prototype.call`
6734 // rebinds `this`, and every `Array.prototype` method is generic over it, so
6735 // `[].slice.call(arrayLike)` must run against the ARGUMENT. Dropping the
6736 // override made that read back as the empty array the thunk was read off.
6737 // A nullish override is ignored: it carries no receiver to dispatch on.
6738 Some(JsObj::BoundMethod { recv, name }) => {
6739 let target = match &this {
6740 Some(t) if !matches!(t, Value::Undef) && !with_host(|h| h.is_null(t)) => t,
6741 _ => &recv,
6742 };
6743 // A thunk read off an ARRAY carries an `Array.prototype` method, and
6744 // those are generic over `this` — route the rebound call through
6745 // `proto_method` so an array-LIKE receiver takes the generic path
6746 // instead of being told the method does not exist.
6747 if with_host(|h| h.kind_of(&recv)) == Some(ObjKind::Array) {
6748 return crate::builtins::proto_method(target, &format!("Array:{name}"), args);
6749 }
6750 call_method(target, &name, args)
6751 }
6752 Some(JsObj::BoundFunc {
6753 target,
6754 this: bthis,
6755 args: pre,
6756 }) => {
6757 let mut all = pre;
6758 all.extend(args);
6759 invoke(&target, all, Some(bthis))
6760 }
6761 Some(JsObj::Class(c)) => Err(type_error(&format!(
6762 "Class constructor {} cannot be invoked without 'new'",
6763 c.name
6764 ))),
6765 _ => Err(type_error(&format!(
6766 "{} is not a function",
6767 with_host(|h| h.str_of(callable))
6768 ))),
6769 }
6770}
6771
6772/// Whether calling the native constructor `name` with `this` is the ES5
6773/// constructor-stealing pattern rather than an ordinary call.
6774///
6775/// True only when `name` really is a native stdlib constructor AND `this` is a
6776/// plain object that already inherits from that constructor's prototype — the
6777/// signature of `Sub.prototype = Native.prototype; Native.call(this, …)`. An
6778/// object that merely happens to be passed as `this` does not qualify, so
6779/// `Date.call(x)` / `Buffer.call(x)` keep their JS meaning (ignore `this`).
6780fn steals_ctor(name: &str, this: Option<&Value>) -> bool {
6781 let Some(target) = this else { return false };
6782 if !with_host(|h| matches!(h.get(target), Some(JsObj::Object(_)))) {
6783 return false;
6784 }
6785 // Already initialized (e.g. a re-entrant call) — nothing to steal.
6786 if crate::stdlib::native_tag(target).is_some() {
6787 return false;
6788 }
6789 let Some(proto) = with_host(|h| h.ensure_ctor_proto(name)) else {
6790 return false;
6791 };
6792 let mut cur = with_host(|h| h.proto_of(target));
6793 while let Some(p) = cur {
6794 if p == proto {
6795 return true;
6796 }
6797 cur = with_host(|h| h.proto_of(&p));
6798 }
6799 false
6800}
6801
6802/// Move a freshly-constructed native instance's state onto `target`, so an
6803/// object built by a subclass constructor becomes a working instance of the
6804/// native class. Copies every own key the native constructor set — the hidden
6805/// `@@`-prefixed slots that carry the state AND the plain ones it exposes
6806/// (`StringDecoder`'s `encoding`) — without disturbing keys `target` already has.
6807fn adopt_native_slots(target: &Value, built: &Value) {
6808 let slots: Vec<(String, Value)> = with_host(|h| match h.get(built) {
6809 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
6810 _ => Vec::new(),
6811 });
6812 with_host(|h| {
6813 if let Some(JsObj::Object(p)) = h.get_mut(target) {
6814 for (k, v) in slots {
6815 p.insert(k, v);
6816 }
6817 }
6818 });
6819}
6820
6821/// Execute a user function/closure body on a fresh frame.
6822pub fn run_user_func(fv: &FuncVal, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6823 run_user_func_of(fv, args, this, None)
6824}
6825
6826/// [`run_user_func`] with the function VALUE the call came through, which the
6827/// `arguments` object needs for its `callee`.
6828pub fn run_user_func_of(
6829 fv: &FuncVal,
6830 args: Vec<Value>,
6831 this: Option<Value>,
6832 callee: Option<Value>,
6833) -> Result<Value, String> {
6834 run_user_func_full(fv, args, this, None, callee)
6835}
6836
6837/// As `run_user_func`, but with an explicit `new.target` (set by `new`).
6838pub fn run_user_func_nt(
6839 fv: &FuncVal,
6840 args: Vec<Value>,
6841 this: Option<Value>,
6842 new_target: Option<Value>,
6843) -> Result<Value, String> {
6844 run_user_func_full(fv, args, this, new_target, None)
6845}
6846
6847fn run_user_func_full(
6848 fv: &FuncVal,
6849 args: Vec<Value>,
6850 this: Option<Value>,
6851 new_target: Option<Value>,
6852 callee: Option<Value>,
6853) -> Result<Value, String> {
6854 // Consumed first, before anything here can start another call.
6855 let derived_ctor = with_host(|h| std::mem::take(&mut h.derived_ctor_next));
6856 // Only the light fields: cloning the whole `FuncDef` cloned its `Chunk` —
6857 // the entire compiled body, `sub_chunks` and all — on every single call.
6858 // The chunk is now reached once per pooled VM, in the two arms below.
6859 let (params, is_generator, is_async, is_arrow_def, def_name) = with_host(|h| {
6860 let d = &h.funcs[fv.def_id];
6861 (
6862 d.params.clone(),
6863 d.is_generator,
6864 d.is_async,
6865 d.is_arrow,
6866 d.name.clone(),
6867 )
6868 });
6869 let env = new_env(fv.env.clone());
6870 // Bind the simple/rest arg slots; destructuring + defaults run in the body
6871 // prologue (compiled ahead of the user statements).
6872 let fn_is_sloppy = with_host(|h| !h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
6873 bind_params(
6874 &env,
6875 ¶ms,
6876 args,
6877 is_arrow_def,
6878 callee.as_ref(),
6879 fn_is_sloppy && !is_arrow_def,
6880 );
6881 // Arrow functions capture `this` lexically; regular functions receive it.
6882 let mut this_val = if fv.is_arrow { fv.this.clone() } else { this };
6883 // 10.2.1.2 OrdinaryCallBindThis: in SLOPPY mode an absent or nullish `this`
6884 // becomes the global object. Only a strict function keeps `undefined`, and
6885 // an arrow has no `this` of its own to substitute. Leaving it undefined
6886 // meant a plain `f()`, a detached method, a callback and `f.call(null)` all
6887 // saw `undefined` where node sees `globalThis`.
6888 let sloppy_this = !fv.is_arrow
6889 && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict))
6890 && match &this_val {
6891 None => true,
6892 Some(v) => matches!(v, Value::Undef) || with_host(|h| h.is_null(v)),
6893 };
6894 if sloppy_this {
6895 this_val = Some(with_host(|h| h.global_object()));
6896 } else if !fv.is_arrow && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)) {
6897 // The other half of OrdinaryCallBindThis: a SLOPPY function boxes a
6898 // primitive `this` with `ToObject`, so `f.call(5)` sees a `Number`
6899 // wrapper rather than the number. Only strict mode passes it through.
6900 if let Some(t) = this_val.clone() {
6901 let boxed = crate::builtins::to_object(&t);
6902 this_val = Some(boxed);
6903 }
6904 }
6905 // A generator function does not run its body on call — it returns a suspended
6906 // generator over the already-bound frame.
6907 if is_generator {
6908 let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
6909 let gen = make_generator(
6910 chunk,
6911 env,
6912 this_val,
6913 fv.home_class.clone(),
6914 fv.home_static,
6915 fv.home_object.clone(),
6916 with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
6917 );
6918 if is_async {
6919 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(&gen).cloned()) {
6920 with_host(|h| h.generators[id as usize].async_gen = true);
6921 }
6922 }
6923 return Ok(gen);
6924 }
6925 // An async function runs on a coroutine and returns a Promise: it executes
6926 // synchronously up to the first `await`, then continues via microtasks.
6927 if is_async {
6928 let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
6929 let gen = make_generator(
6930 chunk,
6931 env,
6932 this_val,
6933 fv.home_class.clone(),
6934 fv.home_static,
6935 fv.home_object.clone(),
6936 with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
6937 );
6938 return Ok(run_async(gen));
6939 }
6940 let home = fv
6941 .home_class
6942 .as_ref()
6943 .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
6944 // Resolved BEFORE the borrow below: reading the function table re-enters
6945 // the host, and doing it inside the frame-push closure double-borrows.
6946 let fn_strict = with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
6947 with_host(|h| {
6948 h.frames.push(Frame {
6949 base_env: env.clone(),
6950 env,
6951 this_obj: this_val,
6952 new_target,
6953 home_class: home,
6954 home_static: fv.home_static,
6955 home_object: fv.home_object.clone(),
6956 strict: fn_strict,
6957 line: 0,
6958 owner: Some(def_name),
6959 is_module: false,
6960 this_state: if derived_ctor {
6961 ThisState::Pending
6962 } else {
6963 ThisState::Plain
6964 },
6965 })
6966 });
6967 let r = run_chunk_keyed(func_key(fv.def_id), || {
6968 with_host(|h| h.funcs[fv.def_id].chunk.clone())
6969 });
6970 let (sig, this_state) = with_host(|h| {
6971 let frame = h.frames.pop();
6972 (h.signal.take(), frame.map(|f| f.this_state))
6973 });
6974 let ret = match r {
6975 Err(e) => return Err(e),
6976 Ok(_) => match sig {
6977 Some(Signal::Return(v)) => v,
6978 _ => Value::Undef,
6979 },
6980 };
6981 // 10.2.2 [[Construct]] steps 10-12 for a derived constructor: an object
6982 // return wins; any other non-undefined return is a TypeError; and falling
6983 // off the end (or `return;`) needs `this` to have been bound by `super()`.
6984 if derived_ctor && !returns_object(&ret) {
6985 if !matches!(ret, Value::Undef) {
6986 return Err(type_error(
6987 "Derived constructors may only return object or undefined",
6988 ));
6989 }
6990 if this_state == Some(ThisState::Pending) {
6991 return Err(this_before_super_error());
6992 }
6993 }
6994 Ok(ret)
6995}
6996
6997/// Bind positional args into a fresh call environment. The compiler emits the
6998/// param names in `def.params`; a `...rest` slot collects the tail as an array.
6999fn bind_params(
7000 env: &Env,
7001 params: &[ParamSlot],
7002 args: Vec<Value>,
7003 is_arrow: bool,
7004 callee: Option<&Value>,
7005 sloppy: bool,
7006) {
7007 let mut vars = VarMap::default();
7008 let mut i = 0;
7009 for slot in params {
7010 if slot.rest {
7011 let rest: Vec<Value> = args.get(i..).map(|s| s.to_vec()).unwrap_or_default();
7012 let arr = with_host(|h| h.new_array(rest));
7013 vars.insert(slot.name.clone(), arr);
7014 } else {
7015 let v = args.get(i).cloned().unwrap_or(Value::Undef);
7016 vars.insert(slot.name.clone(), v);
7017 i += 1;
7018 }
7019 }
7020 // `arguments` array (simple approximation — see BUGS.md: it is a real
7021 // Array, not an Arguments exotic). An ARROW function never gets one:
7022 // `FunctionDeclarationInstantiation` (10.2.11) creates the binding only for
7023 // a non-arrow, so `arguments` inside an arrow resolves lexically to the
7024 // enclosing function's. Binding an empty one here made
7025 // `function f(){ const g = () => [...arguments]; }` see zero args.
7026 if !is_arrow {
7027 let args_arr = with_host(|h| {
7028 let a = h.new_array(args);
7029 // Marked so it can be told apart from an ordinary array: node's
7030 // `arguments` is an exotic, and without the mark
7031 // `Array.isArray(arguments)` was true, the brand was
7032 // `[object Array]` and `util.types.isArgumentsObject` was false.
7033 // The backing representation stays an Array, which is what keeps
7034 // indices, `length`, spread and `for-of` working.
7035 h.set_fn_prop(&a, "@@arguments", Value::Bool(true));
7036 // `callee` is the function itself in SLOPPY code (it is a poison
7037 // pill only in strict, which the read path handles). It read back
7038 // `undefined`, so the pre-`class` self-reference idiom
7039 // `(function(){ arguments.callee })` found nothing.
7040 if let Some(f) = callee {
7041 if sloppy {
7042 h.set_fn_prop(&a, "@@callee", f.clone());
7043 }
7044 }
7045 a
7046 });
7047 vars.entry("arguments".to_string()).or_insert(args_arr);
7048 }
7049 env.borrow_mut().vars = vars;
7050}
7051
7052/// Construct an instance with `new` — creates a fresh object, binds it as
7053/// `this`, runs the constructor, and returns the object (unless the constructor
7054/// returns its own object).
7055pub fn construct(ctor: &Value, args: Vec<Value>) -> Result<Value, String> {
7056 construct_nt(ctor, args, ctor.clone())
7057}
7058
7059/// `new` with an explicit `new.target` (differs from `ctor` when a derived class
7060/// calls `super(...)` — the target stays the originally-`new`ed class).
7061pub fn construct_nt(ctor: &Value, args: Vec<Value>, new_target: Value) -> Result<Value, String> {
7062 // `new proxy(…)` runs the `construct` trap (or forwards to the target).
7063 if with_host(|h| h.kind_of(ctor)) == Some(ObjKind::Proxy) {
7064 return crate::proxy::construct(ctor, args, &new_target)
7065 .map(|r| r.expect("kind_of said Proxy"));
7066 }
7067 let obj = with_host(|h| h.get(ctor).cloned());
7068 match obj {
7069 Some(JsObj::Class(_)) => construct_class(ctor, args, new_target),
7070 Some(JsObj::Func(fv)) => {
7071 // Only an ORDINARY function has a `[[Construct]]` slot. An arrow, a
7072 // `function*` and an `async function` are callable but not
7073 // constructable (10.2.2 is installed only for the ordinary case), so
7074 // `new` on one is a TypeError — node-js instead ran the body and
7075 // handed back a half-built instance (for a generator, an object whose
7076 // constructor had returned a suspended generator).
7077 let non_ctor = with_host(|h| {
7078 h.funcs
7079 .get(fv.def_id)
7080 // A MethodDefinition is in the same boat: `new ({m(){}}).m()`
7081 // is `TypeError: o.m is not a constructor` on node v26.7.0,
7082 // which is also why a method owns no `prototype`.
7083 .map(|d| d.is_generator || d.is_async || d.is_method)
7084 .unwrap_or(false)
7085 });
7086 if fv.is_arrow || non_ctor {
7087 return Err(not_a_constructor(ctor));
7088 }
7089 // A plain constructor function: instance delegates to `fn.prototype`
7090 // (auto-created with a `.constructor` back-link if not yet accessed).
7091 let inst = with_host(|h| {
7092 let o = h.new_object(IndexMap::new());
7093 let proto = h.fn_prop(ctor, "prototype").unwrap_or_else(|| {
7094 let p = h.new_object(IndexMap::new());
7095 if let Some(JsObj::Object(pp)) = h.get_mut(&p) {
7096 pp.insert("constructor".to_string(), ctor.clone());
7097 }
7098 // `F.prototype.constructor` is non-enumerable in JS.
7099 h.hide_prop(&p, "constructor");
7100 h.set_fn_prop(ctor, "prototype", p.clone());
7101 p
7102 });
7103 h.set_proto(&o, proto);
7104 o
7105 });
7106 let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target))?;
7107 if returns_object(&r) {
7108 Ok(r)
7109 } else {
7110 Ok(inst)
7111 }
7112 }
7113 Some(JsObj::Builtin(name)) => crate::builtins::construct_builtin(&name, args),
7114 Some(JsObj::BoundFunc {
7115 target, args: pre, ..
7116 }) => {
7117 let mut all = pre;
7118 all.extend(args);
7119 construct_nt(&target, all, new_target)
7120 }
7121 _ => Err(not_a_constructor(ctor)),
7122 }
7123}
7124
7125/// `TypeError: <callee> is not a constructor`.
7126///
7127/// V8 names the callee by its SOURCE TEXT (`new g()` reports `g`, `new o.m()`
7128/// reports `o.m`); node-js keeps no spans, so a named callable is reported by
7129/// its name — the same string in the common case — and anything else by its
7130/// value.
7131fn not_a_constructor(ctor: &Value) -> String {
7132 let name = with_host(|h| match h.callable_name(ctor) {
7133 n if n.is_empty() => h.str_of(ctor),
7134 n => n,
7135 });
7136 type_error(&format!("{name} is not a constructor"))
7137}
7138
7139/// Whether a constructor's return value is an object (so `new` yields it instead
7140/// of the fresh instance). In JS "object" includes functions — the `router`
7141/// package's constructor `return router` (a function) must be honored, or the
7142/// returned router loses its callable identity.
7143fn returns_object(r: &Value) -> bool {
7144 matches!(
7145 with_host(|h| h.get(r).cloned()),
7146 Some(JsObj::Object(_))
7147 | Some(JsObj::Array(_))
7148 | Some(JsObj::Map { .. })
7149 | Some(JsObj::Set { .. })
7150 | Some(JsObj::Func(_))
7151 | Some(JsObj::Class(_))
7152 | Some(JsObj::BoundFunc { .. })
7153 | Some(JsObj::BoundMethod { .. })
7154 | Some(JsObj::RegExp(_))
7155 )
7156}
7157
7158/// Construct a `class` instance: allocate the object linked to `C.prototype`,
7159/// run field initializers + the constructor (which may call `super(...)`).
7160fn construct_class(
7161 class_val: &Value,
7162 args: Vec<Value>,
7163 new_target: Value,
7164) -> Result<Value, String> {
7165 let cv = match with_host(|h| h.get(class_val).cloned()) {
7166 Some(JsObj::Class(c)) => c,
7167 _ => return Err(type_error("not a class")),
7168 };
7169 // Resolve the prototype of the *most-derived* class being `new`ed, so an
7170 // instance created through a `super()` chain still delegates to the leaf
7171 // prototype (correct method resolution).
7172 let leaf_proto = match with_host(|h| h.get(&new_target).cloned()) {
7173 Some(JsObj::Class(c)) => c.proto.clone(),
7174 _ => cv.proto.clone(),
7175 };
7176 let inst = with_host(|h| {
7177 let o = h.new_object(IndexMap::new());
7178 h.set_proto(&o, leaf_proto.clone());
7179 o
7180 });
7181 // A `super()` deeper in may substitute the instance; the previous value is
7182 // restored so a `new` inside a constructor body cannot be mistaken for one.
7183 let saved = with_host(|h| h.swap_super_replacement(None));
7184 let ran = run_class_ctor(&cv, &inst, args, &new_target);
7185 let substituted = with_host(|h| {
7186 let s = h.take_super_replacement();
7187 h.swap_super_replacement(saved);
7188 s
7189 });
7190 // A constructor that returns an object replaces the instance (`new`
7191 // semantics); failing that, whatever `super()` substituted for it.
7192 match ran? {
7193 Some(obj) if returns_object(&obj) => Ok(obj),
7194 _ => Ok(substituted.unwrap_or(inst)),
7195 }
7196}
7197
7198/// Run one class's field initializers then its constructor on an existing
7199/// instance. Returns the constructor's explicit object return (if any). For a
7200/// base class this is the whole init; for a derived class the constructor body
7201/// reaches `super(...)` which recurses into the parent.
7202fn run_class_ctor(
7203 cv: &ClassVal,
7204 inst: &Value,
7205 args: Vec<Value>,
7206 new_target: &Value,
7207) -> Result<Option<Value>, String> {
7208 // A derived class must run its fields AFTER super() returns; SUPER_CALL does
7209 // that. A base class initializes fields before the constructor body.
7210 if cv.parent.is_none() {
7211 init_fields(cv, inst)?;
7212 }
7213 match &cv.ctor {
7214 Some(ctor_fn) => {
7215 let fv = match with_host(|h| h.get(ctor_fn).cloned()) {
7216 Some(JsObj::Func(f)) => f,
7217 _ => return Err(type_error("class constructor is not a function")),
7218 };
7219 if cv.parent.is_some() {
7220 with_host(|h| h.mark_next_call_derived_ctor());
7221 }
7222 let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7223 return Ok(Some(r));
7224 }
7225 None => {
7226 // Default constructor: `constructor(...a){ super(...a); }` for a
7227 // derived class, empty for a base class.
7228 if let Some(parent) = &cv.parent {
7229 // A base constructor's returned object becomes the instance, so
7230 // the implicit `constructor(...a){ super(...a) }` hands it on.
7231 if let Some(replacement) = super_construct(parent, args, inst, new_target)? {
7232 init_fields(cv, &replacement)?;
7233 return Ok(Some(replacement));
7234 }
7235 init_fields(cv, inst)?;
7236 }
7237 }
7238 }
7239 Ok(None)
7240}
7241
7242/// Evaluate and assign a class's instance-field initializers on `inst`.
7243fn init_fields(cv: &ClassVal, inst: &Value) -> Result<(), String> {
7244 for (name, thunk, name_anon) in &cv.fields {
7245 init_one_field(inst, name, thunk, *name_anon)?;
7246 }
7247 Ok(())
7248}
7249
7250/// Evaluate ONE instance-field initializer thunk and install the result on
7251/// `inst`.
7252///
7253/// Shared by the base-class path (`init_fields`) and the derived-class path
7254/// that runs after `super(...)`; the two used to be separate loops, and only the
7255/// first canonicalized an array-index key.
7256///
7257/// `name_anon` carries 15.7.10's NamedEvaluation: `class C { f = function(){} }`
7258/// gives the function the name `f`. It is decided by the compiler from the
7259/// syntax, never from the value.
7260pub fn init_one_field(
7261 inst: &Value,
7262 name: &str,
7263 thunk: &Value,
7264 name_anon: bool,
7265) -> Result<(), String> {
7266 // The thunk is an arrow capturing the class scope; run it with `this`=inst
7267 // so `this.other`-referencing initializers work.
7268 let val = invoke(thunk, Vec::new(), Some(inst.clone()))?;
7269 with_host(|h| {
7270 if name_anon {
7271 let s = h.new_str(name.to_string());
7272 h.set_fn_prop(&val, "name", s);
7273 }
7274 if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7275 let is_new = !props.contains_key(name);
7276 props.insert(name.to_string(), val);
7277 if is_new && array_index(name).is_some() {
7278 canonicalize_own_keys(props);
7279 }
7280 }
7281 });
7282 Ok(())
7283}
7284
7285/// Run a parent constructor as part of `super(...)`: dispatch on the parent's
7286/// kind (class vs plain function vs builtin) using the existing instance.
7287/// Run the parent constructor against `inst`.
7288///
7289/// Returns the object the parent's `[[Construct]]` produced when that is NOT
7290/// `inst` — a base constructor is allowed to `return` one, and 15.7.15 makes it
7291/// the derived instance too. The caller rebinds `this` to it, so the rest of the
7292/// derived constructor writes to the object `new` will hand back.
7293pub fn super_construct(
7294 parent: &Value,
7295 args: Vec<Value>,
7296 inst: &Value,
7297 new_target: &Value,
7298) -> Result<Option<Value>, String> {
7299 match with_host(|h| h.get(parent).cloned()) {
7300 Some(JsObj::Class(pcv)) => Ok(run_class_ctor(&pcv, inst, args, new_target)?
7301 .filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst)))),
7302 Some(JsObj::Func(fv)) => {
7303 let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7304 Ok(Some(r).filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst))))
7305 }
7306 Some(JsObj::Builtin(name)) => {
7307 let built = crate::builtins::construct_builtin(&name, args)?;
7308 // An EXOTIC parent (`class A extends Array`) keeps its behaviour in
7309 // the heap variant, not in a property map, so copying own props
7310 // cannot carry it: the instance has to BECOME the built object.
7311 // Without this `new (class extends Array {})().push` was not a
7312 // function, and the same for Map, Set, RegExp, Promise and
7313 // Function — subclassing a builtin produced a plain object.
7314 if !become_exotic(inst, &built) {
7315 // An `Error` subclass is ordinary: its state IS own properties.
7316 adopt_own_props(inst, &built);
7317 }
7318 Ok(None)
7319 }
7320 // A Proxy parent (`class D extends new Proxy(B, {})`): `super(…)` is
7321 // `[[Construct]]` on the proxy, so the `construct` trap runs (or forwards
7322 // to the target). node-js initializes an ALREADY-allocated `inst` rather
7323 // than adopting the constructor's return value, so what the proxy built
7324 // is moved across — the same move the builtin arm makes.
7325 Some(JsObj::Proxy { .. }) => {
7326 let built = construct_nt(parent, args, new_target.clone())?;
7327 if !become_exotic(inst, &built) {
7328 adopt_own_props(inst, &built);
7329 }
7330 Ok(None)
7331 }
7332 _ => Err(type_error("super is not a constructor")),
7333 }
7334}
7335
7336/// Move `built`'s own properties (and their attributes) onto `inst`. Used where
7337/// a parent constructor produces a fresh object but node-js's class model has
7338/// already allocated the instance `this` is bound to.
7339/// Replace `inst`'s heap object with `built`'s, so an instance whose class
7340/// extends a builtin EXOTIC really is one.
7341///
7342/// `inst` keeps its identity and its prototype link — the leaf class's
7343/// prototype, which is what method resolution and `instanceof` walk — while its
7344/// contents become the exotic the parent constructor produced. The side tables
7345/// keyed by heap index (array holes, property attributes, the fn-prop table)
7346/// move across with it.
7347///
7348/// Returns false for a variant whose state is ordinary own properties
7349/// (`Error`), which the caller copies instead.
7350fn become_exotic(inst: &Value, built: &Value) -> bool {
7351 let exotic = matches!(
7352 with_host(|h| h.get(built).cloned()),
7353 Some(JsObj::Array(_))
7354 | Some(JsObj::Map { .. })
7355 | Some(JsObj::Set { .. })
7356 | Some(JsObj::RegExp(_))
7357 | Some(JsObj::Promise { .. })
7358 | Some(JsObj::Func(_))
7359 | Some(JsObj::Str(_))
7360 | Some(JsObj::BigInt(_))
7361 | Some(JsObj::Symbol { .. })
7362 );
7363 if !exotic {
7364 return false;
7365 }
7366 let (Value::Obj(dst), Value::Obj(src)) = (inst, built) else {
7367 return false;
7368 };
7369 let (dst, src) = (*dst, *src);
7370 with_host(|h| {
7371 if let Some(obj) = h.get(built).cloned() {
7372 if let Some(slot) = h.get_mut(inst) {
7373 *slot = obj;
7374 }
7375 }
7376 h.move_index_state(src, dst);
7377 });
7378 true
7379}
7380
7381fn adopt_own_props(inst: &Value, built: &Value) {
7382 let entries: Vec<(String, Value)> = with_host(|h| match h.get(built) {
7383 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
7384 _ => Vec::new(),
7385 });
7386 with_host(|h| {
7387 let keys: Vec<String> = entries.iter().map(|(k, _)| k.clone()).collect();
7388 if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7389 for (k, v) in entries {
7390 props.insert(k, v);
7391 }
7392 canonicalize_own_keys(props);
7393 }
7394 // The copied slots keep the attributes the source gave them, so
7395 // `class E extends Error` instances hide `message`/`stack` too.
7396 for k in keys {
7397 let a = h.prop_attrs(built, &k);
7398 h.set_prop_attrs(inst, &k, a);
7399 }
7400 });
7401}
7402
7403// ── class construction (runtime) ─────────────────────────────────────────────
7404
7405/// Build a class constructor value from its parts. The compiler emits (via
7406/// `MKCLASS`) the evaluated parent (or undefined) and the constructor closure (or
7407/// undefined for a default constructor); methods/getters/setters/statics/fields
7408/// are installed afterward by `DEF_MEMBER`/`DEF_FIELD`.
7409pub fn build_class(name: &str, parent: Value, ctor: Value, source_def: Option<usize>) -> Value {
7410 // A Proxy parent (`class D extends new Proxy(B, {})`): `D.prototype`'s
7411 // `[[Prototype]]` is `Get(parent, "prototype")` — a read that runs the `get`
7412 // trap and so re-enters the host, which the borrow below cannot allow.
7413 // Without it the link fell back to `Object.prototype` and every inherited
7414 // method went missing.
7415 let proxy_parent_proto = (with_host(|h| h.kind_of(&parent)) == Some(ObjKind::Proxy))
7416 .then(|| crate::builtins::get_property(&parent, "prototype").ok())
7417 .flatten();
7418 with_host(|h| {
7419 let parent_opt = if matches!(parent, Value::Undef) {
7420 None
7421 } else {
7422 Some(parent.clone())
7423 };
7424 // The class prototype delegates to the parent's prototype (or
7425 // Object.prototype for a base class). Extending a builtin error links to
7426 // that error's prototype so `instanceof Error` holds for the subclass.
7427 let parent_proto = match &parent_opt {
7428 Some(_) if proxy_parent_proto.is_some() => {
7429 proxy_parent_proto.clone().expect("checked is_some")
7430 }
7431 Some(p) => match h.get(p).cloned() {
7432 Some(JsObj::Class(pc)) => pc.proto.clone(),
7433 Some(JsObj::Builtin(bn)) => {
7434 h.ensure_error_protos();
7435 h.ensure_native_protos();
7436 // `class S extends String {}` links to the REAL
7437 // `String.prototype`, the same way an error subclass links
7438 // to its error prototype. Without it `S.prototype`'s
7439 // `[[Prototype]]` fell back to `Object.prototype`, so
7440 // `new S("hi") instanceof String` read false and
7441 // `String(new S("hi"))` reported `[object String]` instead
7442 // of `hi`.
7443 error_proto_of(h, &bn)
7444 .or_else(|| h.native_proto(&bn))
7445 .or_else(|| h.fn_prop(p, "prototype"))
7446 .unwrap_or_else(|| h.object_proto())
7447 }
7448 _ => h
7449 .fn_prop(p, "prototype")
7450 .unwrap_or_else(|| h.object_proto()),
7451 },
7452 None => h.object_proto(),
7453 };
7454 let proto = h.new_object(IndexMap::new());
7455 h.set_proto(&proto, parent_proto);
7456 let ctor_opt = if matches!(ctor, Value::Undef) {
7457 None
7458 } else {
7459 Some(ctor.clone())
7460 };
7461 // Give the constructor closure its home class (for `super.method()`), and
7462 // record its `.name`.
7463 if let Some(cf) = &ctor_opt {
7464 if let Some(JsObj::Func(f)) = h.get_mut(cf) {
7465 f.home_class = Some(name.to_string());
7466 }
7467 }
7468 let cval = ClassVal {
7469 name: name.to_string(),
7470 ctor: ctor_opt,
7471 parent: parent_opt,
7472 proto: proto.clone(),
7473 statics: IndexMap::new(),
7474 fields: Vec::new(),
7475 source_def,
7476 };
7477 let class_val = h.alloc(JsObj::Class(cval));
7478 h.class_registry.insert(name.to_string(), class_val.clone());
7479 // Link prototype → class (for instance display + `constructor`), and give
7480 // the class its own `prototype` fn-prop so `C.prototype` reads work.
7481 h.tag_proto_class(&proto, class_val.clone());
7482 h.set_fn_prop(&class_val, "prototype", proto.clone());
7483 // `Class.prototype.constructor === Class`.
7484 if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
7485 p.insert("constructor".to_string(), class_val.clone());
7486 }
7487 h.hide_prop(&proto, "constructor");
7488 class_val
7489 })
7490}
7491
7492/// Install a method / getter / setter on a class (`DEF_MEMBER`). `kind` is a
7493/// `member::*` tag; `is_static` targets the constructor side.
7494pub fn define_member(class_val: &Value, name: &str, kind: i64, is_static: bool, func: Value) {
7495 with_host(|h| {
7496 let cname = match h.get(class_val) {
7497 Some(JsObj::Class(c)) => c.name.clone(),
7498 _ => String::new(),
7499 };
7500 // A private method/accessor: remember which class declared it, so a
7501 // brand-check failure can name the class the way node does. A static
7502 // FIELD is data, not a method, so it keeps the field wording.
7503 if name.starts_with('#') && kind != member::STATIC_FIELD {
7504 h.note_private_method(name);
7505 }
7506 // Give the method its home class for `super.x()`, and record whether it
7507 // is static — `super` resolves against a different object either way.
7508 if let Some(JsObj::Func(f)) = h.get_mut(&func) {
7509 f.home_class = Some(cname);
7510 f.home_static = is_static;
7511 }
7512 // Static members live on the constructor (fn-props / static accessors);
7513 // instance members on the prototype.
7514 let target = if is_static {
7515 class_val.clone()
7516 } else {
7517 match h.get(class_val) {
7518 Some(JsObj::Class(c)) => c.proto.clone(),
7519 _ => return,
7520 }
7521 };
7522 match kind {
7523 member::GET => h.set_accessor(&target, name, Some(func), None),
7524 member::SET => h.set_accessor(&target, name, None, Some(func)),
7525 _ => {
7526 // A static field is enumerable (`Object.keys(C)` lists it) unlike
7527 // a method, so it must not reach the `hide_prop` below.
7528 if kind == member::STATIC_FIELD {
7529 if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7530 c.statics.insert(name.to_string(), func.clone());
7531 }
7532 h.set_fn_prop(class_val, name, func);
7533 return;
7534 }
7535 if is_static {
7536 if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7537 c.statics.insert(name.to_string(), func.clone());
7538 }
7539 h.set_fn_prop(class_val, name, func);
7540 } else if let Some(JsObj::Object(p)) = h.get_mut(&target) {
7541 p.insert(name.to_string(), func);
7542 }
7543 }
7544 }
7545 // Class methods and accessors are non-enumerable (ES2015 ClassDefinition-
7546 // Evaluation), so `for (k in instance)` walking the prototype chain never
7547 // yields them and `Object.keys(C.prototype)` is empty.
7548 h.hide_prop(&target, name);
7549 });
7550}
7551
7552/// Register an instance-field initializer thunk on a class (`DEF_FIELD`).
7553pub fn define_field(class_val: &Value, name: &str, thunk: Value, name_anon: bool) {
7554 with_host(|h| {
7555 if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7556 c.fields.push((name.to_string(), thunk, name_anon));
7557 }
7558 });
7559}
7560
7561/// The `[[Prototype]]` object a constructor value hands to its instances
7562/// (`Ctor.prototype`), for `instanceof`.
7563fn ctor_prototype(h: &JsHost, ctor: &Value) -> Option<Value> {
7564 match h.get(ctor) {
7565 Some(JsObj::Class(c)) => Some(c.proto.clone()),
7566 Some(JsObj::Func(_)) => h.fn_prop(ctor, "prototype"),
7567 // A builtin's prototype lives in one of two registries: the error
7568 // prototypes, or the native exotic prototypes (`Buffer.prototype`,
7569 // `Uint8Array.prototype`). Consulting only the first made `instanceof`
7570 // blind to the real `Buffer.prototype → Uint8Array.prototype` chain, so
7571 // `Buffer.prototype instanceof Uint8Array` read false even though the
7572 // link was there — the instance case only passed via a native-tag
7573 // special case, which a prototype object does not carry.
7574 Some(JsObj::Builtin(name)) => h
7575 .error_protos
7576 .get(name)
7577 .or_else(|| h.native_protos.get(name))
7578 .cloned(),
7579 Some(JsObj::BoundFunc { target, .. }) => ctor_prototype(h, &target.clone()),
7580 _ => None,
7581 }
7582}
7583
7584/// `ctor.prototype` in the SAME representation `builtins::prototype_of` yields,
7585/// so a chain walk driven by that function can compare the two with `strict_eq`.
7586///
7587/// `ctor_prototype` answers only for the constructors whose prototype object
7588/// really exists on the heap (classes, user functions, the error and native
7589/// exotics). A bare builtin like `Object`/`Array` has none there — its instances
7590/// report `h.object_proto()` / a `Builtin("<C>.prototype")` handle — so this
7591/// mirrors that fallback rather than reporting "no prototype" and failing every
7592/// comparison.
7593fn walk_target_prototype(ctor: &Value) -> Option<Value> {
7594 if let Some(p) = with_host(|h| ctor_prototype(h, ctor)) {
7595 return Some(p);
7596 }
7597 let name = with_host(|h| match h.get(ctor) {
7598 Some(JsObj::Builtin(n)) => Some(n.clone()),
7599 _ => None,
7600 })?;
7601 if name == "Object" {
7602 return Some(with_host(|h| h.object_proto()));
7603 }
7604 Some(with_host(|h| {
7605 h.alloc(JsObj::Builtin(format!("{name}.prototype")))
7606 }))
7607}
7608
7609/// V8's "not a function" wording for a value that was expected to be callable.
7610/// A number/string/boolean is named WITH its value (`number 1 is not a
7611/// function`, `string "s" is not a function`); every other type is named by type
7612/// alone (`object is not a function`, `symbol is not a function`).
7613pub fn not_a_function_message(v: &Value) -> String {
7614 with_host(|h| match v {
7615 Value::Undef => "undefined is not a function".into(),
7616 Value::Bool(b) => format!("boolean {b} is not a function"),
7617 Value::Int(_) | Value::Float(_) => format!("number {} is not a function", h.str_of(v)),
7618 Value::Str(s) => format!("string \"{s}\" is not a function"),
7619 Value::Obj(_) => match h.get(v) {
7620 Some(JsObj::Str(s)) => format!("string \"{s}\" is not a function"),
7621 Some(JsObj::Symbol { .. }) => "symbol is not a function".into(),
7622 Some(JsObj::BigInt(_)) => "bigint is not a function".into(),
7623 _ => "object is not a function".into(),
7624 },
7625 _ => "object is not a function".into(),
7626 })
7627}
7628
7629/// `obj instanceof ctor` — walk `obj`'s prototype chain looking for
7630/// `ctor.prototype`.
7631pub fn instance_of(obj: &Value, ctor: &Value) -> Result<bool, String> {
7632 // 13.10.2 InstanceofOperator step 3: a `Symbol.hasInstance` method on the
7633 // right-hand side REPLACES the prototype-chain walk entirely, and it is
7634 // consulted before the callability check — which is why a plain (uncallable)
7635 // object that defines it is a legal `instanceof` right-hand side.
7636 if matches!(ctor, Value::Obj(_)) {
7637 // `class C { static [Symbol.hasInstance](){} }` and a method defined on a
7638 // plain function both land in the fn-prop side table (which
7639 // `class_static` reads, following the `extends` chain), NOT in an object
7640 // property map — so consulting only `lookup_chain` would find the object
7641 // literal form and silently miss the two forms V8 users actually write.
7642 let handler = match with_host(|h| h.class_static(ctor, "@@hasInstance")) {
7643 Some(f) => Some(f),
7644 None => protocol_lookup(ctor, "@@hasInstance")?,
7645 };
7646 // GetMethod (7.3.11) treats only `undefined`/`null` as "absent"; anything
7647 // else that is not callable is a TypeError, so a data property here does
7648 // NOT fall back to the prototype walk.
7649 match handler {
7650 Some(f) if with_host(|h| is_callable(h, &f)) => {
7651 let r = invoke(&f, vec![obj.clone()], Some(ctor.clone()))?;
7652 return Ok(with_host(|h| h.truthy(&r)));
7653 }
7654 Some(f)
7655 if !matches!(f, Value::Undef)
7656 && !with_host(|h| matches!(h.get(&f), Some(JsObj::Null))) =>
7657 {
7658 return Err(type_error(¬_a_function_message(&f)));
7659 }
7660 _ => {}
7661 }
7662 }
7663 // 13.10.2 InstanceofOperator validates the RIGHT-hand side FIRST, so
7664 // `1 instanceof 3` throws even though the left side could never match.
7665 // Returning early on the left side skipped that check entirely.
7666 let ctor_callable = with_host(|h| {
7667 matches!(
7668 h.get(ctor),
7669 Some(JsObj::Func(_))
7670 | Some(JsObj::Class(_))
7671 | Some(JsObj::Builtin(_))
7672 | Some(JsObj::BoundFunc { .. })
7673 )
7674 });
7675 if !ctor_callable {
7676 // V8 has TWO messages here and they are not interchangeable: a primitive
7677 // right-hand side is "not an object", an object that is merely not
7678 // callable is "not callable". Only the second was implemented, so
7679 // `1 instanceof 3` reported nothing at all.
7680 return Err(type_error(if with_host(|h| !is_primitive(h, ctor)) {
7681 "Right-hand side of 'instanceof' is not callable"
7682 } else {
7683 "Right-hand side of 'instanceof' is not an object"
7684 }));
7685 }
7686 // A non-object left-hand side is never an instance — but only after the
7687 // right-hand side has been validated above.
7688 if !matches!(obj, Value::Obj(_)) {
7689 return Ok(false);
7690 }
7691 // A Proxy shares no heap variant with its target, so the structural arms
7692 // below would misclassify it. 10.5.3 says `OrdinaryHasInstance` walks
7693 // `[[GetPrototypeOf]]`, i.e. the handler's `getPrototypeOf` trap — run that
7694 // walk here, which also gives a custom trap the final say.
7695 if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Proxy) {
7696 with_host(|h| {
7697 h.ensure_error_protos();
7698 h.ensure_native_protos();
7699 });
7700 let Some(target) = walk_target_prototype(ctor) else {
7701 return Ok(false);
7702 };
7703 let mut cur = crate::proxy::get_prototype_of(obj)?.unwrap_or(Value::Undef);
7704 for _ in 0..100 {
7705 if matches!(cur, Value::Undef) || with_host(|h| h.is_null(&cur)) {
7706 return Ok(false);
7707 }
7708 if with_host(|h| h.strict_eq(&cur, &target)) {
7709 return Ok(true);
7710 }
7711 cur = crate::builtins::prototype_of(&cur);
7712 }
7713 return Ok(false);
7714 }
7715 // Builtin constructors whose instances aren't prototype-linked in our model
7716 // (arrays/plain objects/functions) get a structural instanceof.
7717 if let Some(JsObj::Builtin(name)) = with_host(|h| h.get(ctor).cloned()) {
7718 // …but an object whose chain PASSES THROUGH the intrinsic prototype is
7719 // an instance regardless of its own kind, which is the whole of the ES5
7720 // subclassing pattern: `F.prototype = Object.create(Array.prototype)`
7721 // makes `new F() instanceof Array` true. A structural test alone said
7722 // false.
7723 if crate::builtins::chain_intrinsic_ctors_pub(obj).contains(&name.as_str()) {
7724 return Ok(true);
7725 }
7726 let kind = with_host(|h| h.get(obj).cloned());
7727 match name.as_str() {
7728 "Array" => return Ok(matches!(kind, Some(JsObj::Array(_)))),
7729 "Function" => return Ok(with_host(|h| is_callable(h, obj))),
7730 // Map/Set/Promise instances are distinct heap variants, not
7731 // prototype-linked, so match them structurally (a WeakMap/WeakSet is a
7732 // Map/Set with `weak: true`, so `weakMap instanceof Map` is false).
7733 "Map" => return Ok(matches!(kind, Some(JsObj::Map { weak: false, .. }))),
7734 "WeakMap" => return Ok(matches!(kind, Some(JsObj::Map { weak: true, .. }))),
7735 "Set" => return Ok(matches!(kind, Some(JsObj::Set { weak: false, .. }))),
7736 "WeakSet" => return Ok(matches!(kind, Some(JsObj::Set { weak: true, .. }))),
7737 "Promise" => return Ok(matches!(kind, Some(JsObj::Promise { .. }))),
7738 // A RegExp is its own heap variant too, not a prototype-linked object.
7739 "RegExp" => return Ok(matches!(kind, Some(JsObj::RegExp(_)))),
7740 "Object" => {
7741 // Everything object-typed except a null-prototype object is an
7742 // Object instance.
7743 let is_obj = matches!(
7744 kind,
7745 Some(JsObj::Object(_))
7746 | Some(JsObj::Array(_))
7747 // A namespace object and a builtin function are both
7748 // `instanceof Object`: `Math instanceof Object` is true.
7749 | Some(JsObj::Builtin(_))
7750 | Some(JsObj::Func(_))
7751 | Some(JsObj::Class(_))
7752 | Some(JsObj::Map { .. })
7753 | Some(JsObj::Set { .. })
7754 | Some(JsObj::Promise { .. })
7755 | Some(JsObj::Generator { .. })
7756 | Some(JsObj::RegExp(_))
7757 );
7758 if is_obj {
7759 // A null-prototype object (Object.create(null) or
7760 // setPrototypeOf(o, null)) is NOT an Object instance.
7761 if with_host(|h| h.has_null_proto(obj)) {
7762 return Ok(false);
7763 }
7764 return Ok(true);
7765 }
7766 return Ok(false);
7767 }
7768 // A Node `Buffer` IS a `Uint8Array` subclass instance.
7769 "Uint8Array" if crate::stdlib::native_tag(obj).as_deref() == Some("Buffer") => {
7770 return Ok(true);
7771 }
7772 // Every typed array carries the same `TypedArray` tag; the constructor
7773 // it is an instance of is its ELEMENT KIND.
7774 k if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") => {
7775 return Ok(crate::stdlib::typedarray::kind_of(obj) == k);
7776 }
7777 // A native-tagged instance (`WeakRef`, `FinalizationRegistry`,
7778 // `TextEncoder`, …) is an instance of the builtin whose name matches
7779 // its hidden `@@native` tag.
7780 other => {
7781 if crate::stdlib::native_tag(obj).as_deref() == Some(other) {
7782 return Ok(true);
7783 }
7784 }
7785 }
7786 }
7787 with_host(|h| h.ensure_error_protos());
7788 // The native exotic prototypes are built lazily; `instanceof` may be the
7789 // first thing to ask for them, so materialise them before the chain walk.
7790 with_host(|h| h.ensure_native_protos());
7791 let target = match with_host(|h| ctor_prototype(h, ctor)) {
7792 Some(p) => p,
7793 None => return Ok(false),
7794 };
7795 let mut cur = with_host(|h| h.proto_of(obj));
7796 while let Some(p) = cur {
7797 if with_host(|h| h.strict_eq(&p, &target)) {
7798 return Ok(true);
7799 }
7800 cur = with_host(|h| h.proto_of(&p));
7801 }
7802 Ok(false)
7803}
7804
7805// ── generators (stackful coroutines, same-thread via corosensei) ─────────────
7806
7807impl JsHost {
7808 /// Swap the volatile execution context in one shot, returning the previous
7809 /// one — installs a generator's context on resume, pulls it back on suspend.
7810 fn install_gen_ctx(&mut self, mut c: GenContext) -> GenContext {
7811 std::mem::swap(&mut self.frames, &mut c.frames);
7812 std::mem::swap(&mut self.error, &mut c.error);
7813 std::mem::swap(&mut self.exc, &mut c.exc);
7814 std::mem::swap(&mut self.signal, &mut c.signal);
7815 c
7816 }
7817 pub fn is_generator_val(&self, v: &Value) -> bool {
7818 matches!(self.get(v), Some(JsObj::Generator { .. }))
7819 }
7820 /// Whether `v` is an ASYNC generator object — the borrow-free form of
7821 /// [`is_async_generator`], usable from code already holding the host.
7822 pub fn is_async_gen_val(&self, v: &Value) -> bool {
7823 match self.get(v) {
7824 Some(JsObj::Generator { id }) => self
7825 .generators
7826 .get(*id as usize)
7827 .map(|g| g.async_gen)
7828 .unwrap_or(false),
7829 _ => false,
7830 }
7831 }
7832 pub fn gen_done(&self, id: u32) -> bool {
7833 self.generators
7834 .get(id as usize)
7835 .map(|g| g.done)
7836 .unwrap_or(true)
7837 }
7838 fn gen_started(&self, id: u32) -> bool {
7839 self.generators
7840 .get(id as usize)
7841 .map(|g| g.started)
7842 .unwrap_or(false)
7843 }
7844}
7845
7846/// Build a suspended generator whose body is `chunk`, run in a frame with the
7847/// already-bound `env`. Nothing executes until the first `gen_resume`.
7848fn make_generator(
7849 chunk: Chunk,
7850 env: Env,
7851 this_val: Option<Value>,
7852 home_class: Option<String>,
7853 home_static: bool,
7854 home_object: Option<Value>,
7855 strict: bool,
7856) -> Value {
7857 let home = home_class
7858 .as_ref()
7859 .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
7860 let frame = Frame {
7861 base_env: env.clone(),
7862 env,
7863 this_obj: this_val,
7864 new_target: None,
7865 home_class: home,
7866 home_static,
7867 home_object,
7868 strict,
7869 line: 0,
7870 owner: None,
7871 is_module: false,
7872 this_state: ThisState::Plain,
7873 };
7874 let id = with_host(|h| {
7875 let id = h.generators.len() as u32;
7876 h.generators.push(GenCell {
7877 coro: None,
7878 yielder: std::ptr::null(),
7879 ctx: GenContext {
7880 frames: vec![frame],
7881 ..GenContext::default()
7882 },
7883 done: false,
7884 started: false,
7885 inject: None,
7886 async_gen: false,
7887 queue: std::collections::VecDeque::new(),
7888 running: false,
7889 stack_floor: 0,
7890 });
7891 id
7892 });
7893 let body = move |yielder: &corosensei::Yielder<Value, Value>, _first: Value| {
7894 ensure_coroutine_floor();
7895 // Same thread → publish the yielder so `yield` (deep in the body's VM)
7896 // can reach it. Valid for the whole body lifetime.
7897 with_host(|h| h.generators[id as usize].yielder = yielder as *const _ as *const ());
7898 let r = run_chunk_on(chunk);
7899 // A `return` inside the body leaves a Return signal carrying the final
7900 // value; capture it so `.next()` reports it as the completion value.
7901 let ret = with_host(|h| match h.signal.take() {
7902 Some(Signal::Return(v)) => v,
7903 _ => Value::Undef,
7904 });
7905 r.map(|_| ret)
7906 };
7907 // The body's stack is allocated here rather than left to `Coroutine::new` so
7908 // that its size is ours to choose and, above all, so its `limit()` is known:
7909 // that address is what `stack_exhausted` must compare against while the body
7910 // runs, since a coroutine does NOT run on the thread stack pthread reports.
7911 // A refused reservation still yields a working generator on corosensei's own
7912 // 1 MiB default, with a floor derived on entry instead.
7913 let (coro, floor) = match corosensei::stack::DefaultStack::new(CORO_STACK_SIZE) {
7914 Ok(stack) => {
7915 let floor = coro_stack_floor(&stack);
7916 (corosensei::Coroutine::with_stack(stack, body), floor)
7917 }
7918 Err(_) => (corosensei::Coroutine::new(body), 0),
7919 };
7920 with_host(|h| {
7921 h.generators[id as usize].coro = Some(coro);
7922 h.generators[id as usize].stack_floor = floor;
7923 });
7924 with_host(|h| h.alloc(JsObj::Generator { id }))
7925}
7926
7927/// `yield v` — suspend the running generator, handing `v` to the resumer; returns
7928/// the value the next `gen_resume(x)` supplies (a `.next(x)` argument).
7929pub fn gen_yield(v: Value) -> Result<Value, String> {
7930 let id = match CUR_GEN.with(|c| c.get()) {
7931 Some(id) => id,
7932 None => return Err(type_error("yield outside a generator")),
7933 };
7934 let yp = with_host(|h| h.generators[id as usize].yielder);
7935 // SAFETY: same-thread coroutine; the yielder lives for the whole body, and we
7936 // only reach here from inside that body (its stack is live).
7937 let yielder = unsafe { &*(yp as *const corosensei::Yielder<Value, Value>) };
7938 let sent = yielder.suspend(v);
7939 // On resume, a `.return(v)`/`.throw(e)` may have queued a forced completion:
7940 // convert it into a Return signal / thrown value so the body unwinds and any
7941 // `finally` runs, exactly as a source-level `return`/`throw` would.
7942 if let Some(inj) = with_host(|h| h.generators[id as usize].inject.take()) {
7943 match inj {
7944 GenInject::Return(rv) => {
7945 with_host(|h| h.signal = Some(Signal::Return(rv)));
7946 return Ok(Value::Undef);
7947 }
7948 GenInject::Throw(ev) => {
7949 let msg = with_host(|h| crate::builtins::error_string(h, &ev));
7950 with_host(|h| h.exc = Some(ev));
7951 return Err(msg);
7952 }
7953 }
7954 }
7955 Ok(sent)
7956}
7957
7958/// `generator.return(v)`: force the generator to complete, running any pending
7959/// `finally`. If it is already done (or never started) it just reports
7960/// `{value:v, done:true}` without executing the body.
7961pub fn gen_return(gen: &Value, v: Value) -> Result<GenStep, String> {
7962 let id = match with_host(|h| h.get(gen).cloned()) {
7963 Some(JsObj::Generator { id }) => id,
7964 _ => return Err(type_error("not a generator")),
7965 };
7966 // Not started yet (coro present, ctx never resumed) OR already done → no body
7967 // to unwind: complete immediately with the supplied value.
7968 let started = with_host(|h| h.gen_started(id));
7969 if with_host(|h| h.generators[id as usize].done) || !started {
7970 with_host(|h| h.generators[id as usize].done = true);
7971 return Ok(GenStep::Done(v));
7972 }
7973 with_host(|h| h.generators[id as usize].inject = Some(GenInject::Return(v)));
7974 gen_resume(gen, Value::Undef)
7975}
7976
7977/// `generator.throw(e)`: inject a throw at the suspension point, running any
7978/// pending `finally` and letting an enclosing `try/catch` in the body handle it.
7979pub fn gen_throw(gen: &Value, e: Value) -> Result<GenStep, String> {
7980 let id = match with_host(|h| h.get(gen).cloned()) {
7981 Some(JsObj::Generator { id }) => id,
7982 _ => return Err(type_error("not a generator")),
7983 };
7984 let started = with_host(|h| h.gen_started(id));
7985 if with_host(|h| h.generators[id as usize].done) || !started {
7986 // A throw into a done/unstarted generator propagates to the caller.
7987 with_host(|h| h.generators[id as usize].done = true);
7988 let msg = with_host(|h| crate::builtins::error_string(h, &e));
7989 with_host(|h| h.exc = Some(e));
7990 return Err(msg);
7991 }
7992 with_host(|h| h.generators[id as usize].inject = Some(GenInject::Throw(e)));
7993 gen_resume(gen, Value::Undef)
7994}
7995
7996/// Outcome of resuming a generator: a yielded value (not done), or the final
7997/// completion value (done).
7998pub enum GenStep {
7999 Yield(Value),
8000 Done(Value),
8001}
8002
8003/// Resume a generator until its next `yield` or its body returns. Preserves the
8004/// shared host: the coroutine is taken out so the body re-enters `with_host`
8005/// freely, and the volatile context is swapped so the caller's frames/signal
8006/// survive the switch.
8007pub fn gen_resume(gen: &Value, send: Value) -> Result<GenStep, String> {
8008 let id = match with_host(|h| h.get(gen).cloned()) {
8009 Some(JsObj::Generator { id }) => id,
8010 _ => return Err(type_error("not a generator")),
8011 };
8012 if with_host(|h| h.generators[id as usize].done) {
8013 return Ok(GenStep::Done(Value::Undef));
8014 }
8015 let mut coro = match with_host(|h| h.generators[id as usize].coro.take()) {
8016 Some(c) => c,
8017 None => return Err("TypeError: generator already executing".into()),
8018 };
8019 with_host(|h| h.generators[id as usize].started = true);
8020 let gen_ctx = with_host(|h| std::mem::take(&mut h.generators[id as usize].ctx));
8021 let caller_ctx = with_host(|h| h.install_gen_ctx(gen_ctx));
8022 let prev = CUR_GEN.with(|c| c.replace(Some(id)));
8023 // The body runs on the coroutine's OWN stack, so the guard's floor has to
8024 // move with it and move back on suspend — generators nest, and a resume from
8025 // inside another generator must restore that one's floor, not the thread's.
8026 let coro_floor = with_host(|h| h.generators[id as usize].stack_floor);
8027 let caller_floor = swap_stack_floor(coro_floor);
8028
8029 let out = coro.resume(send); // no host borrow held; body drives its own VM
8030
8031 let measured = swap_stack_floor(caller_floor);
8032 // A coroutine on corosensei's default stack has no known bounds, so the
8033 // floor it measured for itself on first entry is kept for later resumes.
8034 if coro_floor == 0 && measured != 0 {
8035 with_host(|h| h.generators[id as usize].stack_floor = measured);
8036 }
8037 CUR_GEN.with(|c| c.set(prev));
8038 let mut gen_ctx = with_host(|h| h.install_gen_ctx(caller_ctx));
8039 // A `throw` inside the body left the thrown VALUE in the generator's context,
8040 // which the swap above just stashed away. Hand it to the caller so the
8041 // rejection/catch keeps the original error object instead of a string rebuild.
8042 let thrown = gen_ctx.exc.take();
8043 with_host(|h| {
8044 if let Some(v) = thrown {
8045 h.exc = Some(v);
8046 }
8047 h.generators[id as usize].ctx = gen_ctx;
8048 h.generators[id as usize].coro = Some(coro);
8049 });
8050
8051 match out {
8052 corosensei::CoroutineResult::Yield(y) => Ok(GenStep::Yield(y)),
8053 corosensei::CoroutineResult::Return(r) => {
8054 // Release the coroutine — and with it the mmap'd stack it owns —
8055 // the moment the body completes. `h.generators` only ever grows (an
8056 // id is never reused), so a program that awaits in a loop otherwise
8057 // accumulates one whole [`CORO_STACK_SIZE`] reservation per call for
8058 // the life of the process. A finished generator is never resumed:
8059 // `gen_resume` returns `Done` on the `done` flag before it looks.
8060 with_host(|h| {
8061 let g = &mut h.generators[id as usize];
8062 g.done = true;
8063 g.coro = None;
8064 });
8065 match r {
8066 Ok(v) => Ok(GenStep::Done(v)),
8067 Err(e) => Err(e),
8068 }
8069 }
8070 }
8071}
8072
8073/// Force a generator to completion (used by `.return()` and abandoned loops):
8074/// marks it done without running further.
8075pub fn gen_close(gen: &Value) {
8076 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(gen).cloned()) {
8077 with_host(|h| h.generators[id as usize].done = true);
8078 }
8079}
8080
8081// ── iteration protocol (arrays, strings, Map/Set, generators, Symbol.iterator) ─
8082
8083/// Convert a Map/Set key value into a `MapKey` under SameValueZero.
8084pub fn map_key(h: &JsHost, v: &Value) -> MapKey {
8085 match v {
8086 Value::Undef => MapKey::Undef,
8087 Value::Bool(b) => MapKey::Bool(*b),
8088 Value::Int(n) => MapKey::Num(norm_num_bits(*n as f64)),
8089 Value::Float(f) => MapKey::Num(norm_num_bits(*f)),
8090 Value::Str(s) => MapKey::Str((**s).clone()),
8091 Value::Obj(i) => match h.get(v) {
8092 Some(JsObj::Str(s)) => MapKey::Str(s.clone()),
8093 Some(JsObj::Null) => MapKey::Null,
8094 Some(JsObj::BigInt(b)) => MapKey::Big(b.to_string()),
8095 Some(JsObj::Builtin(n)) => MapKey::Intrinsic(builtin_identity(n).to_string()),
8096 _ => MapKey::Ref(*i),
8097 },
8098 _ => MapKey::Undef,
8099 }
8100}
8101
8102/// Canonical bit pattern for a Map/Set numeric key: `NaN` → one value, `-0` → `+0`.
8103fn norm_num_bits(f: f64) -> u64 {
8104 if f.is_nan() {
8105 return f64::NAN.to_bits();
8106 }
8107 if f == 0.0 {
8108 return 0.0f64.to_bits(); // fold -0 into +0
8109 }
8110 f.to_bits()
8111}
8112
8113/// Fully materialize any iterable into a vector of values.
8114/// Pull at most `n` values, then close the iterator — 8.6.2
8115/// IteratorBindingInitialization, which is what an array destructuring pattern
8116/// without a `...rest` element performs.
8117///
8118/// The distinction from [`iter_all`] is not an optimization. A pattern names a
8119/// fixed number of targets, so the spec pulls exactly that many and calls
8120/// IteratorClose on whatever is left; draining instead made
8121///
8122/// ```text
8123/// const [first] = infiniteGenerator();
8124/// ```
8125///
8126/// run forever. It is also observable on any finite iterator, as the count of
8127/// `next()` calls and whether `return()` ever ran.
8128///
8129/// A `...rest` element genuinely consumes the remainder, so those patterns keep
8130/// using `iter_all` and an unbounded source hangs there in node too.
8131pub fn iter_take(v: &Value, n: usize) -> Result<Vec<Value>, String> {
8132 // A Proxy iterates through its traps, which materialize eagerly; there is
8133 // no step-wise form to bound, so this keeps the draining behaviour.
8134 if let Some(items) = crate::proxy::iterate(v)? {
8135 return Ok(items.into_iter().take(n).collect());
8136 }
8137 if with_host(|h| h.is_generator_val(v)) {
8138 let mut out = Vec::new();
8139 while out.len() < n {
8140 match gen_resume(v, Value::Undef)? {
8141 GenStep::Yield(x) => out.push(x),
8142 _ => return Ok(out), // ran out on its own; nothing left to close
8143 }
8144 }
8145 // Stopped early: `.return()` resumes it at the yield so `finally` runs.
8146 let _ = gen_return(v, Value::Undef);
8147 return Ok(out);
8148 }
8149 if let Some(iter_fn) = user_iterator_fn(v) {
8150 let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8151 let mut out = Vec::new();
8152 while out.len() < n {
8153 let step = call_method(&iterator, "next", Vec::new())?;
8154 // Read first: resolving the property re-enters the host, so doing
8155 // it inside the `with_host` closure double-borrows and aborts.
8156 let done = get_prop_chain(&step, "done")?;
8157 if with_host(|h| h.truthy(&done)) {
8158 return Ok(out);
8159 }
8160 out.push(get_prop_chain(&step, "value")?);
8161 }
8162 // IteratorClose: `return` is optional on the protocol, and a throw from
8163 // it is swallowed here the way a normal (non-abrupt) completion does.
8164 if let Ok(ret) = get_prop_chain(&iterator, "return") {
8165 if with_host(|h| is_callable(h, &ret)) {
8166 let _ = invoke(&ret, Vec::new(), Some(iterator.clone()));
8167 }
8168 }
8169 return Ok(out);
8170 }
8171 // Arrays, strings, Map/Set: already materialized, and their built-in
8172 // iterators carry no `return`, so there is nothing to close. The same
8173 // reachability rule as `iter_all` applies — this is the DESTRUCTURING
8174 // entry point, and `const [x] = a` bound 1 from an array whose prototype no
8175 // longer carried `Symbol.iterator`.
8176 if !crate::builtins::own_intrinsic_reachable_pub(v) {
8177 let shown = with_host(|h| h.inspect(v));
8178 return Err(type_error(&format!("{shown} is not iterable")));
8179 }
8180 with_host(|h| h.iter_vec(v)).map(|items| items.into_iter().take(n).collect())
8181}
8182
8183pub fn iter_all(v: &Value) -> Result<Vec<Value>, String> {
8184 // A Proxy iterates through its traps (see `crate::proxy::iterate`); it has
8185 // no heap variant `iter_vec` could recognise.
8186 if let Some(items) = crate::proxy::iterate(v)? {
8187 return Ok(items);
8188 }
8189 // Generators / user iterators must resume without a live host borrow.
8190 if with_host(|h| h.is_generator_val(v)) {
8191 let mut out = Vec::new();
8192 while let GenStep::Yield(x) = gen_resume(v, Value::Undef)? {
8193 out.push(x);
8194 }
8195 return Ok(out);
8196 }
8197 // Object with a user-defined Symbol.iterator: drive its iterator protocol.
8198 // Checked BEFORE the reachability guard below, since an own `Symbol
8199 // .iterator` makes a value iterable no matter what its prototype is.
8200 if let Some(iter_fn) = user_iterator_fn(v) {
8201 let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8202 return drain_iterator(&iterator);
8203 }
8204 // The fast paths below read a builtin's backing storage directly, which is
8205 // only legitimate while that builtin's `Symbol.iterator` is still
8206 // reachable: replacing the prototype takes it away, and node then reports
8207 // the value as not iterable. Spread, destructuring and `Array.from`'s
8208 // iterable branch all funnel through here.
8209 if !crate::builtins::own_intrinsic_reachable_pub(v) {
8210 let shown = with_host(|h| h.inspect(v));
8211 return Err(type_error(&format!("{shown} is not iterable")));
8212 }
8213 // A String wrapper iterates its code POINTS, exactly as the primitive does
8214 // (22.1.3.34) — `[...new String("ab")]` is `["a","b"]`, not a TypeError.
8215 if let Some(prim) = crate::builtins::wrapped_primitive(v) {
8216 if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) {
8217 return iter_all(&prim);
8218 }
8219 }
8220 // An array's index ACCESSORS are not in its backing vector, so iterating one
8221 // (spread, `for-of`, `Array.from`) has to resolve them the way the
8222 // `Array.prototype` methods do.
8223 let mut items = with_host(|h| h.iter_vec(v))?;
8224 if with_host(|h| matches!(h.get(v), Some(JsObj::Array(_)))) {
8225 crate::builtins::resolve_index_accessors_pub(v, &mut items);
8226 }
8227 Ok(items)
8228}
8229
8230// ── async iteration (`for await (… of …)`) ───────────────────────────────────
8231
8232/// Obtain an async iterator for `for await`. If `src` has a `Symbol.asyncIterator`
8233/// method, use it (its `.next()` returns a promise of `{value, done}`); otherwise
8234/// fall back to the sync iterable, materialized into a `JsObj::Iter` whose values
8235/// are awaited one at a time by `async_step`.
8236pub fn get_async_iterator(src: &Value) -> Result<Value, String> {
8237 if let Some(f) = user_async_iterator_fn(src) {
8238 return invoke(&f, Vec::new(), Some(src.clone()));
8239 }
8240 // An `async function*` object IS its own async iterator; draining it into a
8241 // list here would run the whole body (and any `finally`) before the consumer
8242 // sees the first value.
8243 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(src).cloned()) {
8244 if with_host(|h| h.generators[id as usize].async_gen) {
8245 return Ok(src.clone());
8246 }
8247 }
8248 let items = iter_all(src)?;
8249 Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
8250}
8251
8252/// If `v` has an own/inherited `Symbol.asyncIterator` method, return it.
8253fn user_async_iterator_fn(v: &Value) -> Option<Value> {
8254 // A PROXY supplies the protocol through its `get` trap and is not a plain
8255 // object, so the shape test below rejects it outright.
8256 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8257 return protocol_lookup(v, "@@asyncIterator")
8258 .ok()
8259 .flatten()
8260 .filter(|f| with_host(|h| is_callable(h, f)));
8261 }
8262 let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8263 if !is_plain {
8264 return None;
8265 }
8266 // Full property resolution, not a stored-property lookup — the same reason
8267 // `user_iterator_fn` does it for the SYNC protocol. A NATIVE-tagged object
8268 // dispatches its methods through the stdlib method table rather than a
8269 // property map, so `lookup_chain` reported no `Symbol.asyncIterator` for one
8270 // even though reading it gives a function: `for await (const v of
8271 // timersPromises.setInterval(…))` said the iterator "is not iterable".
8272 let f = crate::builtins::get_property(v, "@@asyncIterator").ok()?;
8273 with_host(|h| is_callable(h, &f)).then_some(f)
8274}
8275
8276/// One step of a `for await` loop: return a Promise that settles to a
8277/// `{value, done}` record. For a native async iterator this is `iter.next()`
8278/// (already a promise of the record). For the sync fallback it pops the next raw
8279/// value, awaits it, and packages `{value: resolved, done:false}` (or
8280/// `{done:true}` at exhaustion).
8281pub fn async_step(iterator: &Value) -> Result<Value, String> {
8282 // An `async function*` object: resume it through the await-aware driver.
8283 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(iterator).cloned()) {
8284 if with_host(|h| h.generators[id as usize].async_gen) {
8285 return Ok(async_gen_step(iterator, Value::Undef));
8286 }
8287 }
8288 // Sync-fallback iterator: drive it here, awaiting each yielded value.
8289 if let Some(JsObj::Iter { items, idx }) = with_host(|h| h.get(iterator).cloned()) {
8290 if idx >= items.len() {
8291 // `AsyncFromSyncIteratorContinuation` resolves the record THROUGH a
8292 // promise even at exhaustion, so the `done: true` step costs the same
8293 // two microtask ticks a value step does.
8294 let step = with_host(|h| h.new_promise());
8295 let sid = with_host(|h| h.promise_id(&step).unwrap());
8296 with_host(|h| {
8297 h.queue_micro_native(Box::new(move || {
8298 resolve_promise_val(sid, iter_record(Value::Undef, true));
8299 Ok(())
8300 }))
8301 });
8302 return Ok(step);
8303 }
8304 let raw = items[idx].clone();
8305 with_host(|h| {
8306 if let Some(JsObj::Iter { idx, .. }) = h.get_mut(iterator) {
8307 *idx += 1;
8308 }
8309 });
8310 // Await the raw value (adopts a promise's resolution), then wrap.
8311 let step = with_host(|h| h.new_promise());
8312 let sid = with_host(|h| h.promise_id(&step).unwrap());
8313 let raw_p = promise_of(&raw);
8314 let raw_id = with_host(|h| h.promise_id(&raw_p).unwrap());
8315 subscribe_native(
8316 raw_id,
8317 Box::new(move |state, val| {
8318 if state == PromiseState::Rejected {
8319 reject_promise_val(sid, val);
8320 } else {
8321 resolve_promise_val(sid, iter_record(val, false));
8322 }
8323 Ok(())
8324 }),
8325 );
8326 return Ok(step);
8327 }
8328 // Native async iterator: `iter.next()` returns the {value,done} promise.
8329 let r = call_method(iterator, "next", Vec::new())?;
8330 Ok(promise_of(&r))
8331}
8332
8333/// If `v` has an own/inherited `Symbol.iterator` method (internal key
8334/// `@@iterator`), return it. Arrays/strings use the native fast path instead.
8335pub fn user_iterator_fn(v: &Value) -> Option<Value> {
8336 let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8337 if !is_plain {
8338 return None;
8339 }
8340 // Full property resolution, not a stored-property lookup: a NATIVE-tagged
8341 // object (`URLSearchParams`, `Headers`) dispatches its methods through the
8342 // stdlib method table rather than a property map, so `lookup_chain` reported
8343 // no `Symbol.iterator` for one even though reading it gave a function —
8344 // `[...new URLSearchParams('a=1')]` threw `{} is not iterable`.
8345 let f = crate::builtins::get_property(v, "@@iterator").ok()?;
8346 with_host(|h| is_callable(h, &f)).then_some(f)
8347}
8348
8349/// Drive an iterator object (one with a `.next()` returning `{value, done}`) to
8350/// exhaustion.
8351/// Step `src`'s iterator, handing each value to `f`, and CLOSE the iterator if
8352/// `f` exits abruptly (7.4.9 IteratorClose).
8353///
8354/// The difference from `iter_all` + a loop is that this never materializes the
8355/// whole sequence: `Array.from(infinite, mapFn)` where `mapFn` throws has to
8356/// stop at the first call, and draining first means it never gets there at all.
8357pub fn iter_for_each(
8358 src: &Value,
8359 mut f: impl FnMut(Value, usize) -> Result<(), String>,
8360) -> Result<(), String> {
8361 // Only a USER iterator can be infinite or observe its own close; every
8362 // other shape is already a finite materialized sequence.
8363 let Some(iter_fn) = user_iterator_fn(src) else {
8364 for (i, v) in iter_all(src)?.into_iter().enumerate() {
8365 f(v, i)?;
8366 }
8367 return Ok(());
8368 };
8369 let iterator = invoke(&iter_fn, Vec::new(), Some(src.clone()))?;
8370 let mut i = 0usize;
8371 loop {
8372 let step = call_method(&iterator, "next", Vec::new())?;
8373 let done = get_prop_chain(&step, "done")?;
8374 if with_host(|h| h.truthy(&done)) {
8375 return Ok(());
8376 }
8377 let value = get_prop_chain(&step, "value")?;
8378 if let Err(e) = f(value, i) {
8379 // The callback's error wins over anything `return()` raises, so a
8380 // throwing `return` is swallowed here (7.4.9 step 6).
8381 let _ = close_iterator(&iterator);
8382 return Err(e);
8383 }
8384 i += 1;
8385 }
8386}
8387
8388/// Call `iterator.return()` if it has one, as IteratorClose does.
8389pub fn close_iterator(iterator: &Value) -> Result<(), String> {
8390 let has = crate::builtins::get_property(iterator, "return")?;
8391 if with_host(|h| is_callable(h, &has)) {
8392 call_method(iterator, "return", Vec::new())?;
8393 }
8394 Ok(())
8395}
8396
8397pub(crate) fn drain_iterator(iterator: &Value) -> Result<Vec<Value>, String> {
8398 let mut out = Vec::new();
8399 loop {
8400 let step = call_method(iterator, "next", Vec::new())?;
8401 let done = get_prop_chain(&step, "done")?;
8402 if with_host(|h| h.truthy(&done)) {
8403 break;
8404 }
8405 out.push(get_prop_chain(&step, "value")?);
8406 }
8407 Ok(out)
8408}
8409
8410/// Property read that walks the prototype chain (used by iteration helpers).
8411/// Whether the builtin named `n` is CALLABLE. Most are (`Array`, `parseInt`,
8412/// `Math.floor`); the exceptions are the namespace objects a script can only
8413/// read properties off (`Math`, `JSON`, every `require()`d core module), which
8414/// report `typeof === "object"` and carry no `name`/`length`.
8415pub fn builtin_is_callable(n: &str) -> bool {
8416 // A `<Ctor>.prototype` handle is a namespace of methods, not a function:
8417 // `typeof Set.prototype` is `"object"`, and treating it as callable made it
8418 // brand `[object Function]`, inspect as `[Function: prototype]`, and answer
8419 // `true` to `instanceof Function`. `Function.prototype` is the one that
8420 // really IS callable (10.2.4: it is an anonymous built-in that returns
8421 // undefined), which is why it is not stripped here.
8422 if n != "Function.prototype" && n.ends_with(".prototype") {
8423 return false;
8424 }
8425 // A `match` rather than a slice scan: this runs on every callability test,
8426 // which is every call and every `ToPrimitive`, and a `contains` over the
8427 // list below compares against all 56 entries before answering "callable" —
8428 // the common case. The compiler turns the arms into a length-then-bytes
8429 // decision tree instead.
8430 !matches!(
8431 n,
8432 "Math"
8433 | "JSON"
8434 | "console"
8435 | "Reflect"
8436 | "process"
8437 | "Atomics"
8438 | "performance"
8439 | "fs"
8440 | "path"
8441 | "os"
8442 | "util"
8443 | "crypto"
8444 | "webcrypto"
8445 | "SubtleCrypto"
8446 | "querystring"
8447 | "events"
8448 | "timers"
8449 | "perf_hooks"
8450 | "async_hooks"
8451 | "diagnostics_channel"
8452 | "v8"
8453 | "dns"
8454 | "punycode"
8455 | "child_process"
8456 | "tty"
8457 | "url"
8458 | "zlib"
8459 | "string_decoder"
8460 | "http"
8461 | "net"
8462 | "buffer"
8463 | "function"
8464 | "path/win32"
8465 | "fs/promises"
8466 | "stream/promises"
8467 | "stream/consumers"
8468 | "stream/web"
8469 | "timers/promises"
8470 | "dns/promises"
8471 | "https"
8472 | "http2"
8473 | "tls"
8474 | "dgram"
8475 | "cluster"
8476 | "worker_threads"
8477 | "readline"
8478 | "readline/promises"
8479 | "repl"
8480 | "vm"
8481 | "domain"
8482 | "trace_events"
8483 | "wasi"
8484 | "inspector"
8485 | "object"
8486 )
8487 // The live `require.cache` view is a plain object to a script, not
8488 // something it can call.
8489 && n != crate::builtins::REQUIRE_CACHE
8490}
8491
8492pub fn get_prop_chain(recv: &Value, name: &str) -> Result<Value, String> {
8493 crate::builtins::get_property(recv, name)
8494}
8495
8496/// Whether `v` is an ECMAScript primitive, i.e. `ToPrimitive` is the identity
8497/// on it. `undefined`, `null`, booleans, numbers, strings, symbols and bigints
8498/// qualify; every other heap cell (objects, arrays, functions, `Map`/`Set`,
8499/// native-tagged instances) is an object and must be converted.
8500pub fn is_primitive(h: &JsHost, v: &Value) -> bool {
8501 match v {
8502 Value::Obj(_) => matches!(
8503 h.get(v),
8504 None | Some(JsObj::Null)
8505 | Some(JsObj::Str(_))
8506 | Some(JsObj::Symbol { .. })
8507 | Some(JsObj::BigInt(_))
8508 ),
8509 _ => true,
8510 }
8511}
8512
8513/// `ToPrimitive(v, hint)` — ECMA-262 7.1.1. `hint` is `"default"`, `"number"`
8514/// or `"string"`.
8515///
8516/// An object carrying a `Symbol.toPrimitive` method (internal key
8517/// `@@toPrimitive`) has it called with the hint and must return a primitive.
8518/// Otherwise `OrdinaryToPrimitive` (7.1.1.1) tries `valueOf` then `toString` —
8519/// the order reversed for the string hint — and takes the FIRST call whose
8520/// result is a primitive. An object that yields no primitive (a null-prototype
8521/// object has neither method) throws V8's
8522/// `TypeError: Cannot convert object to primitive value`.
8523///
8524/// This is the conversion behind `+`, `-`/`*`/`/`/`%`/`**`, the relational
8525/// operators, `==` against a primitive, and `ToPropertyKey` — all of which used
8526/// to read `str_of` directly and so never invoked a user `valueOf`.
8527pub fn to_primitive(v: &Value, hint: &str) -> Result<Value, String> {
8528 if with_host(|h| is_primitive(h, v)) {
8529 return Ok(v.clone());
8530 }
8531 if let Some(f) = protocol_lookup(v, "@@toPrimitive")? {
8532 if with_host(|h| is_callable(h, &f)) {
8533 let hv = with_host(|h| h.new_str(hint.to_string()));
8534 let r = invoke(&f, vec![hv], Some(v.clone()))?;
8535 if with_host(|h| is_primitive(h, &r)) {
8536 return Ok(r);
8537 }
8538 return Err(type_error("Cannot convert object to primitive value"));
8539 }
8540 }
8541 // `Date.prototype[@@toPrimitive]` (21.4.4.45) treats the DEFAULT hint as
8542 // `"string"`, which is why `new Date() + 1` concatenates while
8543 // `new Date() - 1` is arithmetic.
8544 let hint = if hint == "default" && crate::stdlib::native_tag(v).as_deref() == Some("Date") {
8545 "string"
8546 } else {
8547 hint
8548 };
8549 let order = if hint == "string" {
8550 ["toString", "valueOf"]
8551 } else {
8552 ["valueOf", "toString"]
8553 };
8554 // Whether either candidate was actually CALLED. The `[object Tag]` fallback
8555 // below is for exotics whose property funnel exposes no callable
8556 // `toString`, not for an object whose own methods ran and returned
8557 // non-primitives — that case is the spec's TypeError, and branding it
8558 // instead meant `({ valueOf: () => ({}), toString: () => ({}) }) + 1`
8559 // quietly produced `"[object Object]1"`.
8560 let mut called_any = false;
8561 for m in order {
8562 let f = crate::builtins::get_property(v, m).unwrap_or(Value::Undef);
8563 if !with_host(|h| is_callable(h, &f)) {
8564 continue;
8565 }
8566 called_any = true;
8567 // On a Proxy the resolved method is a thunk bound to the TARGET, so
8568 // invoking it directly would stringify the target — `String(new
8569 // Proxy(function f(){}, {}))` reported `f`'s source where V8 reports the
8570 // native-code form. `call_method` re-dispatches the generic
8571 // `Function.prototype`/`Object.prototype` methods against the proxy.
8572 let r = if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8573 call_method(v, m, Vec::new())?
8574 } else {
8575 invoke(&f, Vec::new(), Some(v.clone()))?
8576 };
8577 if with_host(|h| is_primitive(h, &r)) {
8578 return Ok(r);
8579 }
8580 }
8581 // Every object except a null-prototype one inherits `Object.prototype
8582 // .toString`, which always returns a string — so the exhausted-methods
8583 // TypeError is reachable only there. The exotics whose property funnel has
8584 // no `toString` entry of its own (`Map`, `Set`, `Promise`, …) land here and
8585 // get the same `[object Tag]` brand V8 gives them.
8586 // A proxy WITH a `get` trap is not one of those exotics: the trap answered
8587 // for both method names, and if what came back was not callable there is
8588 // nothing left to call — `String(new Proxy({}, { get: () => undefined }))`
8589 // is a TypeError on node, where branding it `[object Object]` invented a
8590 // conversion the trap explicitly refused. A TRAPLESS proxy is different: its
8591 // read forwarded to the target, so `String(new Proxy(new Map(), {}))` gets
8592 // the target's `[object Map]` brand exactly as the bare `Map` does.
8593 if !called_any && !with_host(|h| h.has_null_proto(v)) && !crate::proxy::has_trap(v, "get") {
8594 return crate::builtins::proto_method(v, "Object:toString", Vec::new());
8595 }
8596 Err(type_error("Cannot convert object to primitive value"))
8597}
8598
8599/// `ToString(v)` with `ToPrimitive` method dispatch: an object is converted
8600/// with the string hint (so a user `toString` — or `valueOf`, if `toString`
8601/// is absent or returns an object — is invoked), then rendered by `str_of`.
8602/// Returns a heap string value.
8603pub fn to_string_value(v: &Value) -> Result<Value, String> {
8604 let p = to_primitive(v, "string")?;
8605 // `ToString(symbol)` throws (7.1.17 step 2) — the ONLY conversion a symbol
8606 // refuses. `String(sym)` is the documented exception and is handled at that
8607 // call site, not here, so every implicit coercion (`sym + ''`, `` `${sym}` ``,
8608 // `[sym].join()`) rejects the way node does instead of silently rendering
8609 // `Symbol(desc)`.
8610 if with_host(|h| matches!(h.get(&p), Some(JsObj::Symbol { .. }))) {
8611 return Err(type_error("Cannot convert a Symbol value to a string"));
8612 }
8613 Ok(with_host(|h| {
8614 let s = h.str_of(&p);
8615 h.new_str(s)
8616 }))
8617}
8618
8619/// `String(v)` — 22.1.1.1. Identical to [`to_string_value`] except that a
8620/// SYMBOL argument is allowed and renders as `Symbol(desc)` (step 2a).
8621pub fn string_ctor_value(v: &Value) -> Result<Value, String> {
8622 if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8623 return Ok(with_host(|h| {
8624 let s = h.str_of(v);
8625 h.new_str(s)
8626 }));
8627 }
8628 to_string_value(v)
8629}
8630
8631/// `ToNumber(v)` — ECMA-262 7.1.4 — with the object case going through
8632/// `ToPrimitive(v, number)` first, so `+{ valueOf() { return 7 } }` is `7` and
8633/// `+new Date(0)` is `0`. `JsHost::to_number` alone cannot do this: it runs
8634/// under the host borrow and so can never invoke a JS `valueOf`.
8635pub fn to_number_value(v: &Value) -> Result<f64, String> {
8636 // `ToNumber(symbol)` throws (7.1.4 step 2). It is primitive, so without this
8637 // it fell into `to_number` and quietly produced `NaN` — `Number(Symbol())`
8638 // and `+Symbol()` are both `TypeError` on node v26.7.0.
8639 if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8640 return Err(type_error("Cannot convert a Symbol value to a number"));
8641 }
8642 if let Some(n) = with_host(|h| is_primitive(h, v).then(|| h.to_number(v))) {
8643 return Ok(n);
8644 }
8645 let p = to_primitive(v, "number")?;
8646 // Steps 2-3 apply to the ToPrimitive RESULT, not only to the argument. Only
8647 // the argument was checked, so an object whose conversion yields a symbol or
8648 // a BigInt slipped past: `+Object(9n)` answered 9 and
8649 // `+{ [Symbol.toPrimitive]() { return Symbol('s') } }` answered NaN, where
8650 // both are TypeErrors. `Number(x)` is ToNumeric and keeps its own path,
8651 // which is why `Number(Object(9n))` is still 9.
8652 match with_host(|h| h.get(&p).cloned()) {
8653 Some(JsObj::Symbol { .. }) => Err(type_error("Cannot convert a Symbol value to a number")),
8654 Some(JsObj::BigInt(_)) => Err(type_error("Cannot convert a BigInt value to a number")),
8655 _ => Ok(with_host(|h| h.to_number(&p))),
8656 }
8657}
8658
8659/// `ToPropertyKey(v)` — ECMA-262 7.1.19. A symbol keeps its stable internal
8660/// key; anything else is `ToPrimitive(v, string)` then `ToString`, so
8661/// `obj[{ toString() { return 'k' } }]` really reads `obj.k`.
8662pub fn to_property_key(v: &Value) -> Result<String, String> {
8663 // One borrow for the overwhelmingly common primitive key (`a[i]`, `o[s]`,
8664 // `o[sym]`); only an object key pays for the conversion.
8665 if let Some(k) = with_host(|h| is_primitive(h, v).then(|| h.property_key(v))) {
8666 return Ok(k);
8667 }
8668 let p = to_primitive(v, "string")?;
8669 Ok(with_host(|h| h.str_of(&p)))
8670}
8671
8672/// Whether `h.get(v)` is any callable kind. A Proxy is callable exactly when its
8673/// target is (10.5: the `[[Call]]` slot is installed only for a callable
8674/// target), so `typeof` and every `is_callable` guard agree on one answer.
8675pub fn is_callable(h: &JsHost, v: &Value) -> bool {
8676 match h.get(v) {
8677 // Not every builtin is a function: the namespace objects (`Math`,
8678 // `require('fs')`) and the `<Ctor>.prototype` handles are data, and
8679 // calling one is a `TypeError` in node exactly as `typeof` says.
8680 Some(JsObj::Builtin(n)) => builtin_is_callable(n),
8681 Some(JsObj::Func(_))
8682 | Some(JsObj::BoundMethod { .. })
8683 | Some(JsObj::BoundFunc { .. })
8684 | Some(JsObj::Class(_)) => true,
8685 Some(JsObj::Proxy { target, .. }) => is_callable(h, target),
8686 _ => false,
8687 }
8688}
8689
8690/// Walk `recv`'s own props then its prototype chain for `key`, returning the
8691/// stored value (methods, inherited data props). Does NOT invoke accessors.
8692/// A PROTOCOL lookup — `Symbol.toPrimitive`, `Symbol.hasInstance`, `toJSON`,
8693/// `then` and the rest — which the spec performs with `[[Get]]`.
8694///
8695/// That distinction only shows on a PROXY: `lookup_chain` walks the property
8696/// map and never asks the handler, so a proxy supplying a protocol method
8697/// through its `get` trap was invisible and the operation fell back to the
8698/// default. Everything else takes the cheap chain walk.
8699pub fn protocol_lookup(v: &Value, key: &str) -> Result<Option<Value>, String> {
8700 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8701 let got = crate::builtins::get_property(v, key)?;
8702 return Ok((!matches!(got, Value::Undef)).then_some(got));
8703 }
8704 Ok(with_host(|h| lookup_chain(h, v, key)))
8705}
8706
8707pub fn lookup_chain(h: &JsHost, recv: &Value, key: &str) -> Option<Value> {
8708 if let Some(JsObj::Object(p)) = h.get(recv) {
8709 if let Some(v) = p.get(key) {
8710 return Some(v.clone());
8711 }
8712 }
8713 let mut cur = h.proto_of(recv);
8714 while let Some(p) = cur {
8715 // A chain link may be a plain object OR a function/class (the `router`
8716 // package sets `Router.prototype = function(){}` and hangs its methods off
8717 // that function, so the methods live in the fn-prop side table).
8718 match h.get(&p) {
8719 Some(JsObj::Object(props)) => {
8720 if let Some(v) = props.get(key) {
8721 return Some(v.clone());
8722 }
8723 }
8724 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
8725 if let Some(v) = h.fn_prop(&p, key) {
8726 return Some(v);
8727 }
8728 }
8729 _ => {}
8730 }
8731 cur = h.proto_of(&p);
8732 }
8733 None
8734}
8735
8736/// Find a getter/setter accessor for `key` on `recv` or up its prototype chain.
8737pub fn lookup_accessor(
8738 h: &JsHost,
8739 recv: &Value,
8740 key: &str,
8741) -> Option<(Option<Value>, Option<Value>)> {
8742 if let Some(a) = h.own_accessor(recv, key) {
8743 return Some(a);
8744 }
8745 let mut cur = h.proto_of(recv);
8746 while let Some(p) = cur {
8747 if let Some(a) = h.own_accessor(&p, key) {
8748 return Some(a);
8749 }
8750 cur = h.proto_of(&p);
8751 }
8752 // A STATIC accessor declared by an ancestor class. A subclass reaches its
8753 // parent's statics through `ClassVal.parent`, not the `protos` map the walk
8754 // above reads — classes are not linked there, so that walk ended at once.
8755 // Static methods and fields already inherited because `class_static` does
8756 // this same parent walk for `fn_prop`; only accessors had no equivalent:
8757 //
8758 // class Base { static get kind() { return 'base' } }
8759 // class Sub extends Base {}
8760 // Sub.plain() // worked, a fn_prop
8761 // Sub.kind // undefined; node reads 'base'
8762 //
8763 // The caller invokes the getter with the class it was READ off as `this`,
8764 // so a getter reading `this.x` sees the subclass, per 10.2.4.
8765 let mut cls = recv.clone();
8766 while let Some(JsObj::Class(c)) = h.get(&cls) {
8767 let Some(parent) = c.parent.clone() else {
8768 break;
8769 };
8770 if let Some(a) = h.own_accessor(&parent, key) {
8771 return Some(a);
8772 }
8773 cls = parent;
8774 }
8775 None
8776}
8777
8778/// Register a builtin error prototype (for `instanceof Error` etc.).
8779pub fn set_error_proto(name: &str, proto: Value) {
8780 with_host(|h| {
8781 h.error_protos.insert(name.to_string(), proto);
8782 });
8783}
8784pub fn error_proto(name: &str) -> Option<Value> {
8785 with_host(|h| h.error_protos.get(name).cloned())
8786}
8787/// Error prototype lookup with a borrowed host (used inside a `with_host` block).
8788pub fn error_proto_of(h: &JsHost, name: &str) -> Option<Value> {
8789 h.error_protos.get(name).cloned()
8790}
8791
8792impl JsHost {
8793 /// `Error.prototype.toString` for an object whose prototype chain reaches
8794 /// `Error.prototype`: `"Name"` with an empty message, else `"Name: message"`.
8795 /// `None` for anything that is not an error, so the caller keeps its own
8796 /// stringification.
8797 pub fn error_to_string(&self, v: &Value) -> Option<String> {
8798 let base = self.error_protos.get("Error")?;
8799 let mut cur = self.proto_of(v);
8800 let mut is_error = false;
8801 while let Some(p) = cur {
8802 if self.strict_eq(&p, base) {
8803 is_error = true;
8804 break;
8805 }
8806 cur = self.proto_of(&p);
8807 }
8808 if !is_error {
8809 return None;
8810 }
8811 let name = lookup_chain(self, v, "name")
8812 .map(|n| self.str_of(&n))
8813 .unwrap_or_else(|| "Error".into());
8814 let message = lookup_chain(self, v, "message")
8815 .map(|m| self.str_of(&m))
8816 .unwrap_or_default();
8817 // Node's internal coded errors override `toString` as
8818 // `${name} [${code}]: ${message}` (internal/errors.js NodeError). The
8819 // `@@nodeError` tag marks the errors `synth_error` built from a
8820 // `Name [ERR_CODE]: …` string, so a user error that merely has a `.code`
8821 // property still stringifies plainly.
8822 if let Some(JsObj::Object(p)) = self.get(v) {
8823 if p.contains_key("@@nodeError") {
8824 if let Some(code) = p.get("code").map(|c| self.str_of(c)) {
8825 return Some(format!("{name} [{code}]: {message}"));
8826 }
8827 }
8828 }
8829 Some(match (name.is_empty(), message.is_empty()) {
8830 (true, _) => message,
8831 (false, true) => name,
8832 (false, false) => format!("{name}: {message}"),
8833 })
8834 }
8835}
8836
8837/// The set of builtin error constructor names forming the error hierarchy.
8838pub const ERROR_NAMES: &[&str] = &[
8839 "Error",
8840 "TypeError",
8841 "RangeError",
8842 "SyntaxError",
8843 "ReferenceError",
8844 "EvalError",
8845 "URIError",
8846 "AggregateError",
8847 // `assert`'s error class. It is NOT a global (node exposes it only as
8848 // `assert.AssertionError`, and `GLOBAL_FUNCS` is a separate table), but it
8849 // has to be a name `synth_error` recognizes: without it the head
8850 // `AssertionError [ERR_ASSERTION]: …` failed the class check and fell into
8851 // the `Error` branch with the WHOLE head kept as the message, so `e.name`
8852 // was `Error` and `e.message` carried a prefix node keeps out of it.
8853 "AssertionError",
8854 // The WHATWG error class `AbortSignal.reason` carries. Unlike the others its
8855 // `name` comes from the SECOND constructor argument rather than from the
8856 // class, so its prototype keeps the base default and each instance stamps
8857 // its own name into an internal slot.
8858 "DOMException",
8859];
8860
8861impl JsHost {
8862 /// Lazily build the builtin error prototype chain: `Error.prototype →
8863 /// Object.prototype`, and every specific error's prototype → `Error.prototype`.
8864 /// Populated once; instances link to these so `e instanceof TypeError` and
8865 /// `e instanceof Error` both hold.
8866 /// The real `Buffer.prototype` object, building the
8867 /// `Buffer.prototype → Uint8Array.prototype → Object.prototype` chain on
8868 /// first use.
8869 ///
8870 /// A `Buffer` used to be a bare tagged object with no `[[Prototype]]` at
8871 /// all, so `Object.getPrototypeOf(buf) === Buffer.prototype` read false and
8872 /// `instanceof` had to be special-cased around it. Each prototype is a
8873 /// genuine object carrying `@proto:<Ctor>:<method>` thunks for its instance
8874 /// methods, so `Buffer.prototype.slice.call(buf, 1)` still dispatches the
8875 /// way it did when `Buffer.prototype` was a `Builtin` namespace.
8876 pub fn ensure_native_protos(&mut self) {
8877 // The wrapper prototypes share this registry and this guard would skip
8878 // them, so they are built through their own.
8879 self.ensure_wrapper_protos();
8880 self.ensure_function_kind_protos();
8881 if self.native_protos.contains_key("Buffer") {
8882 return;
8883 }
8884 let obj_proto = self.object_proto();
8885 // `Object.prototype` is the one builtin prototype that already existed as
8886 // a real object (it is the chain root). Register it so `Object.prototype`
8887 // reads resolve to THAT object rather than a fresh `Builtin` namespace —
8888 // otherwise `Object.getPrototypeOf(C.prototype) === Object.prototype`
8889 // compares a real object against a thunk and reads false.
8890 self.native_protos
8891 .insert("Object".to_string(), obj_proto.clone());
8892 for m in crate::builtins::OBJECT_PROTO_METHODS {
8893 let thunk = self.alloc(JsObj::Builtin(format!("@proto:Object:{m}")));
8894 if let Some(JsObj::Object(p)) = self.get_mut(&obj_proto) {
8895 p.insert((*m).to_string(), thunk);
8896 }
8897 self.hide_prop(&obj_proto, m);
8898 }
8899 // `Buffer.prototype → Uint8Array.prototype → %TypedArray%.prototype →
8900 // Object.prototype`, which is the chain node v26.7.0 really has. The
8901 // shared iteration methods (`every`, `map`, `filter`, …) live on the
8902 // `%TypedArray%.prototype` intermediate, NOT on `Uint8Array.prototype`:
8903 // measured, `Uint8Array.prototype.hasOwnProperty('every')` is false in
8904 // Node while the intermediate owns it. `%TypedArray%` is not a global,
8905 // so it is reachable only by walking the chain — exactly as in Node.
8906 // Every element kind gets its own prototype hanging off the shared
8907 // intermediate, so `Object.getPrototypeOf(new Int32Array(1))` is
8908 // `Int32Array.prototype` rather than some other kind's. Linking them all
8909 // to `Uint8Array.prototype` would have been the easy version and would
8910 // have made an `Int32Array` claim the wrong prototype.
8911 let mut chain: Vec<(&str, Value)> = vec![("TypedArray", obj_proto)];
8912 for kind in crate::stdlib::typedarray::ELEMENT_KINDS {
8913 chain.push((kind, Value::Undef)); // parent: %TypedArray%.prototype
8914 }
8915 // `Buffer.prototype`'s parent is `Uint8Array.prototype` specifically.
8916 chain.push(("Buffer", Value::Undef));
8917 let mut prev: Option<Value> = None;
8918 for (ctor, parent) in chain.drain(..) {
8919 let proto = self.new_object(IndexMap::new());
8920 // Each kind hangs off the shared intermediate; `Buffer` hangs off
8921 // `Uint8Array.prototype`; the intermediate itself off
8922 // `Object.prototype`.
8923 let parent = match ctor {
8924 "TypedArray" => parent,
8925 "Buffer" => self
8926 .native_protos
8927 .get("Uint8Array")
8928 .cloned()
8929 .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
8930 _ => self
8931 .native_protos
8932 .get("TypedArray")
8933 .cloned()
8934 .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
8935 };
8936 self.set_proto(&proto, parent);
8937 // `%TypedArray%.prototype` has no reachable constructor global, so
8938 // it gets no `constructor` slot (Node's is the anonymous
8939 // `%TypedArray%` intrinsic).
8940 if ctor != "TypedArray" {
8941 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
8942 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8943 p.insert("constructor".into(), ctor_val);
8944 }
8945 self.hide_prop(&proto, "constructor");
8946 }
8947 let methods: &[&str] = match ctor {
8948 "Buffer" => crate::stdlib::buffer::INSTANCE_METHODS,
8949 "TypedArray" => crate::stdlib::typedarray::PROTOTYPE_METHODS,
8950 // `Uint8Array` alone owns the base64/hex pair — no other view
8951 // has them, which is the whole reason they cannot live on the
8952 // shared `%TypedArray%` prototype above.
8953 "Uint8Array" => crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS,
8954 // Every other kind's prototype owns no methods; it inherits them
8955 // from the intermediate above. It does own `BYTES_PER_ELEMENT`,
8956 // which is per-kind and which Node really keeps there (measured:
8957 // `Uint8Array.prototype.hasOwnProperty('BYTES_PER_ELEMENT')`).
8958 _ => &[],
8959 };
8960 if crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor) {
8961 let bpe = Value::Float(crate::stdlib::typedarray::bytes_per_element(ctor) as f64);
8962 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8963 p.insert("BYTES_PER_ELEMENT".into(), bpe);
8964 }
8965 self.hide_prop(&proto, "BYTES_PER_ELEMENT");
8966 }
8967 for m in methods {
8968 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
8969 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
8970 p.insert((*m).to_string(), thunk);
8971 }
8972 self.hide_prop(&proto, m);
8973 }
8974 self.native_protos.insert(ctor.to_string(), proto.clone());
8975 prev = Some(proto);
8976 }
8977 }
8978
8979 /// `String.prototype`, `Number.prototype` and `Boolean.prototype` as REAL
8980 /// objects.
8981 ///
8982 /// A wrapper built by `new String("a")` needs a genuine `[[Prototype]]`
8983 /// link: `Builtin("String.prototype")` is a thunk namespace that cannot
8984 /// appear on a prototype chain, so `Object.getPrototypeOf(w) ===
8985 /// String.prototype` and `w instanceof String` both read false while the
8986 /// wrapper's methods still resolved through the string funnel. Registering
8987 /// them here puts them on the same footing as `Buffer.prototype`.
8988 /// `GeneratorFunction.prototype`, `AsyncFunction.prototype` and
8989 /// `AsyncGeneratorFunction.prototype` — the intrinsics a generator or async
8990 /// function's `[[Prototype]]` really points at.
8991 ///
8992 /// None are globals (node exposes them only through
8993 /// `Object.getPrototypeOf(function*(){}).constructor`), so they live here
8994 /// rather than among the wrapper constructors. Each hangs off
8995 /// `Function.prototype` and carries the `Symbol.toStringTag` that names it.
8996 pub fn ensure_function_kind_protos(&mut self) {
8997 if self.native_protos.contains_key("GeneratorFunction") {
8998 return;
8999 }
9000 let base = self
9001 .native_protos
9002 .get("Function")
9003 .cloned()
9004 .unwrap_or_else(|| self.object_proto());
9005 for ctor in [
9006 "GeneratorFunction",
9007 "AsyncFunction",
9008 "AsyncGeneratorFunction",
9009 ] {
9010 let proto = self.new_object(IndexMap::new());
9011 self.set_proto(&proto, base.clone());
9012 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9013 let tag = self.new_str(ctor);
9014 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9015 p.insert("constructor".into(), ctor_val);
9016 p.insert("@@toStringTag".into(), tag);
9017 }
9018 self.hide_prop(&proto, "constructor");
9019 self.hide_prop(&proto, "@@toStringTag");
9020 self.native_protos.insert(ctor.to_string(), proto);
9021 }
9022 }
9023
9024 pub fn ensure_wrapper_protos(&mut self) {
9025 if self.native_protos.contains_key("String") {
9026 return;
9027 }
9028 let obj_proto = self.object_proto();
9029 for ctor in ["String", "Number", "Boolean", "Symbol", "BigInt"] {
9030 let proto = self.new_object(IndexMap::new());
9031 self.set_proto(&proto, obj_proto.clone());
9032 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9033 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9034 p.insert("constructor".into(), ctor_val);
9035 }
9036 self.hide_prop(&proto, "constructor");
9037 // The three conversions must be here because `Object.prototype`
9038 // also defines them: without a shadowing entry a wrapper would
9039 // inherit the object forms and `String(new String("a"))` would
9040 // report `[object Object]`.
9041 //
9042 // The REST are here because the prototype is a real object a script
9043 // can read a method OFF of. Only the three were installed, on the
9044 // reasoning that `charAt`/`toFixed`/… reach the primitive through
9045 // `call_method` anyway — true for `s.charAt(0)` and false for the
9046 // generic-borrowing form: `String.prototype.trim` read `undefined`,
9047 // so `String.prototype.trim.call(s)` — and `Number.prototype
9048 // .toFixed.call(n)`, and every `Array.prototype`-style borrow of a
9049 // wrapper method — threw. `Array.prototype`/`Object.prototype`
9050 // already carried their whole method set; these three did not.
9051 let methods: Vec<&str> = ["toString", "valueOf", "toLocaleString"]
9052 .into_iter()
9053 .chain(match ctor {
9054 "String" => crate::builtins::STRING_PROTO_METHODS.iter().copied(),
9055 "Number" => crate::builtins::NUMBER_PROTO_METHODS.iter().copied(),
9056 _ => [].iter().copied(),
9057 })
9058 // The symbol-keyed methods come from the generated intrinsic
9059 // table, so this object advertises exactly the symbol methods
9060 // node defines on it — `String.prototype[Symbol.iterator]` was
9061 // `undefined` because only the string-keyed lists were walked.
9062 .chain(crate::builtins::proto_symbol_methods(ctor))
9063 .collect();
9064 let mut seen: Vec<&str> = Vec::new();
9065 for m in methods {
9066 if seen.contains(&m) {
9067 continue;
9068 }
9069 seen.push(m);
9070 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9071 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9072 p.insert(m.to_string(), thunk);
9073 }
9074 self.hide_prop(&proto, m);
9075 }
9076 // `Symbol.prototype` and `BigInt.prototype` are the two wrapper
9077 // prototypes that carry a `@@toStringTag`; the other three are
9078 // branded by their internal slot instead, and node reports
9079 // `undefined` for their tag. Without it
9080 // `Object.prototype.toString.call(Symbol.prototype)` read
9081 // `[object Object]` where node says `[object Symbol]`.
9082 if matches!(ctor, "Symbol" | "BigInt") {
9083 let tag = self.new_str(ctor.to_string());
9084 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9085 p.insert("@@toStringTag".into(), tag);
9086 }
9087 // Not `hide_prop`: a well-known `@@toStringTag` is read-only as
9088 // well as non-enumerable (20.4.3.6), and `hide_prop` leaves it
9089 // writable.
9090 self.set_prop_attrs(
9091 &proto,
9092 "@@toStringTag",
9093 PropAttrs {
9094 writable: false,
9095 enumerable: false,
9096 configurable: true,
9097 },
9098 );
9099 }
9100 // `Symbol.prototype[@@toPrimitive]` (20.4.3.5) is what a string or
9101 // numeric conversion of a symbol reaches FIRST. Its absence was
9102 // observable in the failure wording: `String(Symbol.prototype)`
9103 // throws in node because `@@toPrimitive` rejects a non-Symbol
9104 // `this`, and here the conversion fell through to `toString` and
9105 // named that method in the message instead.
9106 if ctor == "Symbol" {
9107 let thunk = self.alloc(JsObj::Builtin("@proto:Symbol:@@toPrimitive".to_string()));
9108 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9109 p.insert("@@toPrimitive".into(), thunk);
9110 }
9111 self.hide_prop(&proto, "@@toPrimitive");
9112 }
9113 self.native_protos.insert(ctor.to_string(), proto);
9114 }
9115 }
9116
9117 /// The cached template object for one tagged-template site, if it has been
9118 /// evaluated before.
9119 pub fn template_object(&self, key: (u64, u64)) -> Option<Value> {
9120 self.template_objects.get(&key).cloned()
9121 }
9122 /// Record the template object for one tagged-template site.
9123 pub fn set_template_object(&mut self, key: (u64, u64), v: Value) {
9124 self.template_objects.insert(key, v);
9125 }
9126
9127 /// The real prototype object for a builtin exotic, if it has one.
9128 pub fn native_proto(&self, ctor: &str) -> Option<Value> {
9129 self.native_protos.get(ctor).cloned()
9130 }
9131
9132 /// The constructor name whose `.prototype` object IS `v`, for a prototype
9133 /// this host built as a real object (`String.prototype`, `TypeError
9134 /// .prototype`, `Buffer.prototype`) rather than as a `Builtin` namespace.
9135 ///
9136 /// A prototype is an ORDINARY object: it carries no instance's internal
9137 /// slot, so `Object.prototype.toString.call(TypeError.prototype)` is
9138 /// `[object Object]` and not `[object Error]`. Nothing distinguished the two
9139 /// before, so the brand fell through to the "does it look like an Error"
9140 /// test and answered for the prototype as if it were an instance.
9141 pub fn intrinsic_proto_ctor(&self, v: &Value) -> Option<&str> {
9142 if !matches!(v, Value::Obj(_)) {
9143 return None;
9144 }
9145 self.native_protos
9146 .iter()
9147 .chain(self.error_protos.iter())
9148 .find(|(_, p)| *p == v)
9149 .map(|(name, _)| name.as_str())
9150 }
9151
9152 /// The real `.prototype` object for a native stdlib constructor (`StringDecoder`,
9153 /// `Hash`, `URLSearchParams`, …), built on first read and cached.
9154 ///
9155 /// `Ctor.prototype` used to read `undefined` for every native class outside the
9156 /// hand-written `is_builtin_ctor` list, which broke the ES5 subclassing pattern
9157 /// that libraries still use. `iconv-lite`'s internal codec — reached from
9158 /// `raw-body` on every `express.json()` request — does exactly this:
9159 ///
9160 /// ```text
9161 /// var StringDecoder = require('string_decoder').StringDecoder;
9162 /// if (!StringDecoder.prototype.end) StringDecoder.prototype.end = function () {};
9163 /// function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
9164 /// InternalDecoder.prototype = StringDecoder.prototype;
9165 /// ```
9166 ///
9167 /// The first line threw `Cannot read properties of undefined (reading 'end')`.
9168 ///
9169 /// Methods come from `stdlib::instance_method_lists`, the same table a method
9170 /// READ consults, so the prototype can never advertise a name the dispatcher
9171 /// does not implement. Each is the `@proto:<Ctor>:<method>` thunk that
9172 /// dispatches against its invoke-time `this`, so a subclass instance whose
9173 /// prototype IS this object gets the native implementation. Returns `None` for
9174 /// a tag with no instance methods, leaving those constructors as they were.
9175 pub fn ensure_ctor_proto(&mut self, ctor: &str) -> Option<Value> {
9176 if let Some(p) = self.native_protos.get(ctor) {
9177 return Some(p.clone());
9178 }
9179 // `Buffer` and the typed-array kinds belong to the chain
9180 // `ensure_native_protos` builds. Building one of them here first hung it
9181 // straight off `Object.prototype` AND registered it, which made that
9182 // chain's own `contains_key("Buffer")` guard skip the build for the rest
9183 // of the process: after `Buffer.from([1])`, `Buffer.prototype instanceof
9184 // Uint8Array` read false.
9185 if ctor == "Buffer"
9186 || ctor == "TypedArray"
9187 || crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor)
9188 {
9189 self.ensure_native_protos();
9190 return self.native_protos.get(ctor).cloned();
9191 }
9192 let (own, emitter) = crate::stdlib::instance_method_lists(ctor);
9193 // A class can carry accessors and no methods at all
9194 // (`AsymmetricKeyObject` is only `asymmetricKeyType` and
9195 // `asymmetricKeyDetails`), so an empty method list does not mean there
9196 // is no prototype to build.
9197 let (accessor_list, _) = crate::stdlib::instance_accessors(ctor);
9198 if own.is_empty() && emitter.is_empty() && accessor_list.is_empty() {
9199 return None;
9200 }
9201 // A native class with a real PARENT hangs off that parent's prototype
9202 // rather than straight off `Object.prototype`. The stream hierarchy is
9203 // `Readable → Stream → EventEmitter`, which is what makes
9204 // `new Readable() instanceof Stream` hold and what an ES5 subclass
9205 // doing `Object.create(Stream.prototype)` inherits from.
9206 let parent_proto = match crate::stdlib::native_parent(ctor) {
9207 Some(p) => self
9208 .ensure_ctor_proto(p)
9209 .unwrap_or_else(|| self.object_proto()),
9210 None => self.object_proto(),
9211 };
9212 let proto = self.new_object(IndexMap::new());
9213 self.set_proto(&proto, parent_proto);
9214 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9215 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9216 p.insert("constructor".into(), ctor_val);
9217 }
9218 self.hide_prop(&proto, "constructor");
9219 // A prototype member is ENUMERABLE in node for every class but the few
9220 // written as ES classes, so `for (const k in url)` walks `href` and the
9221 // rest. Hiding all of them made that loop find nothing.
9222 let visible = crate::stdlib::instance_members_enumerable(ctor);
9223 let symbols = crate::builtins::proto_symbol_methods(ctor);
9224 for m in own.iter().chain(emitter.iter()).chain(symbols.iter()) {
9225 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9226 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9227 p.insert((*m).to_string(), thunk);
9228 }
9229 // A symbol-keyed member never enumerates.
9230 if !visible || m.starts_with("@@") {
9231 self.hide_prop(&proto, m);
9232 }
9233 }
9234 // Accessors and the class's `Symbol.toStringTag`, both of which live on
9235 // the PROTOTYPE in node — an instance owns neither.
9236 let (accessors, tag) = crate::stdlib::instance_accessors(ctor);
9237 for (key, settable) in accessors {
9238 let get = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@get@{key}")));
9239 let set =
9240 settable.then(|| self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@set@{key}"))));
9241 self.set_accessor(&proto, key, Some(get), set);
9242 if !visible {
9243 self.hide_prop(&proto, key);
9244 }
9245 }
9246 for m in crate::stdlib::instance_late_methods(ctor) {
9247 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9248 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9249 p.insert((*m).to_string(), thunk);
9250 }
9251 if !visible {
9252 self.hide_prop(&proto, m);
9253 }
9254 }
9255 if !tag.is_empty() {
9256 let tag = self.new_str(tag.to_string());
9257 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9258 p.insert("@@toStringTag".into(), tag);
9259 }
9260 self.hide_prop(&proto, "@@toStringTag");
9261 }
9262 self.native_protos.insert(ctor.to_string(), proto.clone());
9263 Some(proto)
9264 }
9265
9266 /// `<ErrorClass>.prototype`, once [`JsHost::ensure_error_protos`] has run.
9267 /// The error prototypes live in their own table, so `ensure_ctor_proto` —
9268 /// which answers from `native_protos` — does not find them.
9269 pub fn error_proto(&self, name: &str) -> Option<Value> {
9270 self.error_protos.get(name).cloned()
9271 }
9272
9273 pub fn ensure_error_protos(&mut self) {
9274 if !self.error_protos.is_empty() {
9275 return;
9276 }
9277 let obj_proto = self.object_proto();
9278 // Error.prototype first (the shared base).
9279 let err_proto = self.new_object(IndexMap::new());
9280 self.set_proto(&err_proto, obj_proto);
9281 let nm = self.new_str("Error");
9282 let empty = self.new_str("");
9283 let ctor = self.alloc(JsObj::Builtin("Error".into()));
9284 // `Error.prototype.toString` (20.5.3.4) has to be an OWN property here,
9285 // not a fallback the stringifier applies when nothing else matches: it
9286 // exists precisely to shadow `Object.prototype.toString`. Without it,
9287 // the first read of `Error.prototype` or `Object.prototype` — which
9288 // `x instanceof Error` performs, so ordinary code triggers it —
9289 // materialised `Object.prototype.toString`, the chain lookup started
9290 // finding it, and `String(err)` flipped from `Error: m` to
9291 // `[object Error]` for the REST OF THE PROCESS, including errors
9292 // created before the read.
9293 let to_string = self.alloc(JsObj::Builtin("@proto:Error:toString".into()));
9294 if let Some(JsObj::Object(p)) = self.get_mut(&err_proto) {
9295 p.insert("name".into(), nm);
9296 p.insert("message".into(), empty);
9297 p.insert("constructor".into(), ctor);
9298 p.insert("toString".into(), to_string);
9299 }
9300 // Everything on `Error.prototype` is non-enumerable in V8.
9301 for k in ["name", "message", "constructor", "toString"] {
9302 self.hide_prop(&err_proto, k);
9303 }
9304 self.error_protos.insert("Error".into(), err_proto.clone());
9305 for name in &ERROR_NAMES[1..] {
9306 let p = self.new_object(IndexMap::new());
9307 self.set_proto(&p, err_proto.clone());
9308 let nm = self.new_str(*name);
9309 let ctor = self.alloc(JsObj::Builtin((*name).to_string()));
9310 if let Some(JsObj::Object(o)) = self.get_mut(&p) {
9311 o.insert("name".into(), nm);
9312 o.insert("constructor".into(), ctor);
9313 }
9314 self.hide_prop(&p, "name");
9315 self.hide_prop(&p, "constructor");
9316 self.error_protos.insert((*name).to_string(), p);
9317 }
9318 }
9319}
9320
9321// ── Map/Set element access (used by builtins) ────────────────────────────────
9322
9323impl JsHost {
9324 /// A function's `.length`: the count of leading params before the first one
9325 /// with a default or the rest element.
9326 pub fn func_arity(&self, v: &Value) -> usize {
9327 // 20.2.3.2: a bound function's `length` is the target's, less the
9328 // arguments already bound, floored at 0. Reporting 0 for every bound
9329 // function breaks arity dispatch — express picks error-handling
9330 // middleware with `fn.length === 4`, so a bound handler was never
9331 // recognised as one.
9332 if let Some(JsObj::BoundFunc { target, args, .. }) = self.get(v) {
9333 return self.func_arity(&target.clone()).saturating_sub(args.len());
9334 }
9335 // A builtin's arity is the specified one, so `Math.max.bind(null,1)`
9336 // reports 1 rather than the 0 a target of unknown arity would give.
9337 if let Some(JsObj::Builtin(n)) = self.get(v) {
9338 return crate::builtins::builtin_meta(n)
9339 .map(|(_, len)| len as usize)
9340 .unwrap_or(0);
9341 }
9342 let def_id = match self.get(v) {
9343 Some(JsObj::Func(f)) => Some(f.def_id),
9344 Some(JsObj::Class(c)) => match c.ctor.as_ref().and_then(|cf| self.get(cf)) {
9345 Some(JsObj::Func(f)) => Some(f.def_id),
9346 _ => None,
9347 },
9348 _ => None,
9349 };
9350 match def_id.and_then(|id| self.funcs.get(id)) {
9351 Some(def) => def
9352 .params
9353 .iter()
9354 .take_while(|p| !p.rest && !p.has_default)
9355 .count(),
9356 None => 0,
9357 }
9358 }
9359
9360 pub fn is_map(&self, v: &Value) -> bool {
9361 matches!(self.get(v), Some(JsObj::Map { .. }))
9362 }
9363 pub fn is_set(&self, v: &Value) -> bool {
9364 matches!(self.get(v), Some(JsObj::Set { .. }))
9365 }
9366}
9367
9368// ── promises & the event loop ────────────────────────────────────────────────
9369
9370impl JsHost {
9371 /// Allocate a fresh pending promise, returning its heap value.
9372 pub fn new_promise(&mut self) -> Value {
9373 let id = self.promises.len() as u32;
9374 self.promises.push(PromiseCell {
9375 state: PromiseState::Pending,
9376 value: Value::Undef,
9377 reactions: Vec::new(),
9378 handled: false,
9379 });
9380 self.alloc(JsObj::Promise { id })
9381 }
9382 pub fn promise_id(&self, v: &Value) -> Option<u32> {
9383 match self.get(v) {
9384 Some(JsObj::Promise { id }) => Some(*id),
9385 _ => None,
9386 }
9387 }
9388 pub fn promise_state(&self, id: u32) -> PromiseState {
9389 self.promises[id as usize].state
9390 }
9391 pub fn promise_value(&self, id: u32) -> Value {
9392 self.promises[id as usize].value.clone()
9393 }
9394 pub fn promise_mark_handled(&mut self, id: u32) {
9395 self.promises[id as usize].handled = true;
9396 }
9397 /// Take the pending reactions of a promise (called on settle).
9398 pub fn take_reactions(&mut self, id: u32) -> Vec<PromiseReaction> {
9399 std::mem::take(&mut self.promises[id as usize].reactions)
9400 }
9401 pub fn add_reaction(&mut self, id: u32, r: PromiseReaction) {
9402 self.promises[id as usize].reactions.push(r);
9403 }
9404 pub fn settle_promise(&mut self, id: u32, state: PromiseState, value: Value) {
9405 let c = &mut self.promises[id as usize];
9406 if c.state != PromiseState::Pending {
9407 return; // already settled — resolve/reject are one-shot
9408 }
9409 c.state = state;
9410 c.value = value;
9411 }
9412 pub fn queue_micro(&mut self, cb: Value, args: Vec<Value>) {
9413 self.microtasks.push_back(Task::Js { cb, args });
9414 }
9415 pub fn queue_nexttick(&mut self, cb: Value, args: Vec<Value>) {
9416 self.nextticks.push_back(Task::Js { cb, args });
9417 }
9418 /// Schedule a native (Rust) microtask — used by Promise reactions and async
9419 /// resumption.
9420 pub fn queue_micro_native(&mut self, f: Box<dyn FnOnce() -> Result<(), String>>) {
9421 self.microtasks.push_back(Task::Native(f));
9422 }
9423 /// Schedule a macrotask. `interval` is the repeat period for `setInterval`
9424 /// (`None` for the one-shot `setTimeout`/`setImmediate`). Returns the timer
9425 /// id, which the `Timeout`/`Immediate` handle object carries so `clear*`,
9426 /// `ref`/`unref` and `refresh` can find this entry again.
9427 pub fn add_timer(
9428 &mut self,
9429 delay: f64,
9430 callback: Value,
9431 args: Vec<Value>,
9432 interval: Option<f64>,
9433 ) -> u64 {
9434 let id = self.next_timer;
9435 self.next_timer += 1;
9436 // Real deadline for the real-clock path; `setImmediate` (delay < 0) is
9437 // clamped to "now". Virtual-clock ordering still uses `delay`/`seq`.
9438 let deadline = Instant::now() + Duration::from_millis(delay.max(0.0) as u64);
9439 self.macrotasks.push(Timer {
9440 id,
9441 delay,
9442 seq: id,
9443 callback,
9444 args,
9445 cancelled: false,
9446 interval,
9447 refed: true,
9448 deadline,
9449 });
9450 id
9451 }
9452 /// Re-arm a repeating timer that is about to fire, keeping its id (so a
9453 /// `clearInterval` from *inside* the callback cancels this very entry) and
9454 /// taking a fresh `seq` so same-delay peers still round-robin.
9455 ///
9456 /// Called BEFORE the callback runs: if it were called after, the entry would
9457 /// be absent while the callback executed and a `clearInterval(t)` there would
9458 /// cancel nothing, resurrecting an interval the program had stopped.
9459 fn rearm_timer(&mut self, t: &Timer, period: f64) {
9460 let seq = self.next_timer;
9461 self.next_timer += 1;
9462 let deadline = Instant::now() + Duration::from_millis(period.max(0.0) as u64);
9463 self.macrotasks.push(Timer {
9464 id: t.id,
9465 delay: t.delay,
9466 seq,
9467 callback: t.callback.clone(),
9468 args: t.args.clone(),
9469 cancelled: false,
9470 interval: Some(period),
9471 refed: t.refed,
9472 deadline,
9473 });
9474 }
9475 /// `timeout.ref()` / `timeout.unref()` — set the handle bit on a pending
9476 /// timer. A no-op once the timer has fired or been cleared (Node likewise
9477 /// treats `ref`/`unref` on a dead timer as inert).
9478 pub fn set_timer_refed(&mut self, id: u64, refed: bool) {
9479 for t in &mut self.macrotasks {
9480 if t.id == id && !t.cancelled {
9481 t.refed = refed;
9482 }
9483 }
9484 }
9485 /// `timeout.hasRef()` — whether a still-pending timer holds the loop open.
9486 /// A fired or cleared timer reports `false`, matching Node.
9487 pub fn timer_has_ref(&self, id: u64) -> bool {
9488 self.macrotasks
9489 .iter()
9490 .any(|t| t.id == id && !t.cancelled && t.refed)
9491 }
9492 /// `timeout.refresh()` — restart the countdown from now, as if the timer had
9493 /// just been scheduled.
9494 pub fn refresh_timer(&mut self, id: u64) {
9495 let now = Instant::now();
9496 for t in &mut self.macrotasks {
9497 if t.id == id && !t.cancelled {
9498 t.deadline = now + Duration::from_millis(t.delay.max(0.0) as u64);
9499 }
9500 }
9501 }
9502 /// Clone the I/O sender for a background I/O thread.
9503 pub fn io_sender(&self) -> Sender<IoTask> {
9504 self.io_tx.clone()
9505 }
9506 /// Register a live handle (listener/socket/ref'd resource) keeping the loop
9507 /// alive.
9508 pub fn incr_handle(&mut self) {
9509 self.open_handles += 1;
9510 }
9511 /// Release a handle; the loop exits once this reaches `0` with empty queues.
9512 pub fn decr_handle(&mut self) {
9513 self.open_handles = self.open_handles.saturating_sub(1);
9514 }
9515 pub fn open_handles(&self) -> usize {
9516 self.open_handles
9517 }
9518 /// Pop the earliest timer whose real deadline is at or before `now` (I/O
9519 /// path). Ties break by `seq`.
9520 fn pop_due_timer(&mut self, now: Instant) -> Option<Timer> {
9521 let idx = self
9522 .macrotasks
9523 .iter()
9524 .enumerate()
9525 .filter(|(_, t)| !t.cancelled && t.deadline <= now)
9526 .min_by(|(_, a), (_, b)| a.deadline.cmp(&b.deadline).then(a.seq.cmp(&b.seq)))
9527 .map(|(i, _)| i);
9528 idx.map(|i| self.macrotasks.remove(i))
9529 }
9530 /// Time until the earliest pending timer's deadline (I/O path blocking bound),
9531 /// or `None` if no timers are pending. Clamped to `0` for already-due timers.
9532 fn next_timer_timeout(&self, now: Instant) -> Option<Duration> {
9533 self.macrotasks
9534 .iter()
9535 .filter(|t| !t.cancelled)
9536 .map(|t| t.deadline)
9537 .min()
9538 .map(|d| d.saturating_duration_since(now))
9539 }
9540 pub fn cancel_timer(&mut self, id: u64) {
9541 for t in &mut self.macrotasks {
9542 if t.id == id {
9543 t.cancelled = true;
9544 }
9545 }
9546 }
9547 fn pop_next_timer(&mut self) -> Option<Timer> {
9548 // Earliest (delay, seq) fires first — a deterministic virtual clock.
9549 let idx = self
9550 .macrotasks
9551 .iter()
9552 .enumerate()
9553 .filter(|(_, t)| !t.cancelled)
9554 .min_by(|(_, a), (_, b)| {
9555 a.delay
9556 .partial_cmp(&b.delay)
9557 .unwrap_or(std::cmp::Ordering::Equal)
9558 .then(a.seq.cmp(&b.seq))
9559 })
9560 .map(|(i, _)| i);
9561 idx.map(|i| self.macrotasks.remove(i))
9562 }
9563 fn next_microtask(&mut self) -> Option<Task> {
9564 // Node's `processTicksAndRejections` runs in ROUNDS: drain the nextTick
9565 // queue, then drain the microtask queue in full, then repeat if the
9566 // microtasks queued more ticks. A tick queued from INSIDE a microtask
9567 // therefore waits for the rest of that microtask queue.
9568 //
9569 // Preferring ticks on every step interleaved the two, so
9570 // `Promise.resolve().then(() => process.nextTick(f))` ran `f` before the
9571 // promise callbacks queued behind it — the one ordering difference a
9572 // library scheduling work from a `.then` can actually observe.
9573 if !self.draining_micro {
9574 if let Some(t) = self.nextticks.pop_front() {
9575 return Some(t);
9576 }
9577 }
9578 if let Some(t) = self.microtasks.pop_front() {
9579 // Stay in the microtask phase until this queue is exhausted.
9580 self.draining_micro = !self.microtasks.is_empty();
9581 return Some(t);
9582 }
9583 self.draining_micro = false;
9584 self.nextticks.pop_front()
9585 }
9586 fn has_microtasks(&self) -> bool {
9587 !self.nextticks.is_empty() || !self.microtasks.is_empty()
9588 }
9589 /// Whether any pending timer is *referenced* — the timer half of Node's
9590 /// handle count. Only these keep the loop alive; unref'd timers still fire
9591 /// while something else holds the loop open, but never hold it themselves.
9592 fn has_refed_macrotasks(&self) -> bool {
9593 self.macrotasks.iter().any(|t| !t.cancelled && t.refed)
9594 }
9595 /// Whether any pending timer repeats. A repeating timer cannot run on the
9596 /// virtual clock: virtual time never advances, so the interval would re-arm
9597 /// at the same instant forever, spinning a core and starving every
9598 /// longer-delay timer behind it. Its presence forces the real clock.
9599 fn has_pending_interval(&self) -> bool {
9600 self.macrotasks
9601 .iter()
9602 .any(|t| !t.cancelled && t.interval.is_some())
9603 }
9604}
9605
9606/// Drive the event loop to quiescence.
9607///
9608/// **Liveness** is Node's handle count: the loop runs while a microtask is
9609/// pending, an open handle is registered (a listening server, a live socket, an
9610/// in-flight async op), or a *referenced* timer is still pending. That last term
9611/// is what makes `setInterval(fn, 1000)` hold the process open forever, as it
9612/// does in Node — the interval re-arms itself, so a ref'd timer is always
9613/// pending and the loop never reaches its exit condition.
9614///
9615/// Two **clock regimes**, selected per iteration:
9616///
9617/// - **Virtual clock** (no open handles and no repeating timer): the original
9618/// deterministic path — fire the earliest `(delay, seq)` timer immediately, no
9619/// real waiting. Parity output and test speed for ordinary `setTimeout`
9620/// scripts are unchanged.
9621/// - **Real clock** (an open handle, or any pending interval): fire every timer
9622/// whose wall-clock deadline has passed, then BLOCK on the I/O channel
9623/// (`recv_timeout` bounded by the next deadline, or unbounded `recv` if no
9624/// timers) and run the received `IoTask` on the main thread. The host keeps
9625/// its own `Sender`, so `recv` never disconnects while the process should stay
9626/// alive.
9627///
9628/// A repeating timer *must* take this path: virtual time never advances, so an
9629/// interval on the virtual clock would re-fire at the same instant forever,
9630/// spinning a core and starving every longer-delay timer behind it.
9631///
9632/// Errors thrown by a task/timer/I/O dispatch abort the loop (uncaught → surfaced).
9633pub fn run_event_loop() -> Result<(), String> {
9634 // Own the receiver for the loop's duration (blocking `recv` cannot hold a
9635 // host borrow); restore it afterward so a re-entrant run reuses the channel.
9636 let rx = with_host(|h| h.io_rx.take());
9637 let result = drive_event_loop(rx.as_ref());
9638 with_host(|h| h.io_rx = rx);
9639 result
9640}
9641
9642fn drive_event_loop(rx: Option<&Receiver<IoTask>>) -> Result<(), String> {
9643 loop {
9644 // 1) Exhaust the microtask queue (nextTick before promise reactions),
9645 // then report anything that rejected with nobody watching.
9646 while let Some(task) = with_host(|h| h.next_microtask()) {
9647 task.run()?;
9648 }
9649 check_unhandled_rejections()?;
9650
9651 // 2) Liveness (Node's handle count). Nothing referenced left to do ⇒ the
9652 // process exits, dropping any unref'd timers still pending — which is
9653 // why `setTimeout(fn, 1000).unref()` never fires, while an unref'd
9654 // timer behind a ref'd one does.
9655 let alive =
9656 with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
9657 if !alive {
9658 break;
9659 }
9660
9661 // 3) Pick the clock regime for this turn.
9662 let virtual_clock = with_host(|h| h.open_handles() == 0 && !h.has_pending_interval());
9663 if virtual_clock {
9664 // ── virtual-clock regime (unchanged for one-shot timers) ─────────
9665 match with_host(|h| h.pop_next_timer()) {
9666 Some(t) => fire_timer(t)?,
9667 // Unreachable while `alive` holds (a ref'd timer must exist),
9668 // but exiting is the safe reading of "nothing left to run".
9669 None => break,
9670 }
9671 continue;
9672 }
9673
9674 // ── real-clock / blocking-I/O regime ─────────────────────────────────
9675 let now = Instant::now();
9676 if let Some(t) = with_host(|h| h.pop_due_timer(now)) {
9677 fire_timer(t)?;
9678 continue; // re-drain microtasks, re-check deadlines
9679 }
9680 // Nothing due and no pending microtasks: block for the next I/O event,
9681 // bounded by the soonest timer deadline so due timers still fire on time.
9682 let rx = rx.expect("blocking-I/O regime requires the I/O receiver");
9683 let timeout = with_host(|h| h.next_timer_timeout(now));
9684 let recv = match timeout {
9685 Some(d) => rx.recv_timeout(d),
9686 None => rx
9687 .recv()
9688 .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected),
9689 };
9690 match recv {
9691 Ok(task) => task()?,
9692 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} // a timer is now due
9693 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, // no senders left
9694 }
9695 }
9696 Ok(())
9697}
9698
9699/// Run one due timer's callback, first re-arming it if it repeats.
9700///
9701/// The re-arm happens BEFORE the callback runs so that a `clearInterval(t)`
9702/// issued from inside that callback cancels the next occurrence. Re-arming
9703/// afterwards would leave the interval absent from the queue for the duration of
9704/// its own callback, so the `clear` would match nothing and the freshly pushed
9705/// entry would resurrect an interval the program had just stopped.
9706fn fire_timer(t: Timer) -> Result<(), String> {
9707 if let Some(period) = t.interval {
9708 with_host(|h| h.rearm_timer(&t, period));
9709 }
9710 invoke(&t.callback, t.args, None)?;
9711 Ok(())
9712}
9713
9714// ── async functions & promise resolution (native) ────────────────────────────
9715
9716/// Drive a freshly-built async coroutine and return its result promise.
9717fn run_async(gen: Value) -> Value {
9718 let result = with_host(|h| h.new_promise());
9719 let rid = with_host(|h| h.promise_id(&result).unwrap());
9720 drive_async(gen, rid, Value::Undef);
9721 result
9722}
9723
9724/// Resume an async coroutine one step, wiring `await` continuations to promise
9725/// settlement.
9726fn drive_async(gen: Value, rid: u32, send: Value) {
9727 match gen_resume(&gen, send) {
9728 Ok(GenStep::Yield(awaited)) => {
9729 let ap = promise_of(&awaited);
9730 let aid = with_host(|h| h.promise_id(&ap).unwrap());
9731 let gen2 = gen.clone();
9732 subscribe_native(
9733 aid,
9734 Box::new(move |state, val| {
9735 // Resume the coroutine with a `[tag, value]` packet the AWAIT
9736 // op unwraps (tag 1 ⇒ the awaited promise rejected → throw).
9737 let tag = if state == PromiseState::Rejected {
9738 1.0
9739 } else {
9740 0.0
9741 };
9742 let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9743 drive_async(gen2, rid, packet);
9744 Ok(())
9745 }),
9746 );
9747 }
9748 Ok(GenStep::Done(v)) => resolve_promise_val(rid, v),
9749 Err(e) => {
9750 let ev = take_exc_or_error(&e);
9751 reject_promise_val(rid, ev);
9752 }
9753 }
9754}
9755
9756/// The AWAIT op body (runs inside the async coroutine): suspend, yielding the
9757/// awaited value; on resume, unwrap the settlement packet (throwing on reject).
9758pub fn await_value(awaited: Value) -> Result<Value, String> {
9759 // Inside an `async function*`, `await` and `yield` share one coroutine
9760 // yielder, so an awaited value has to be tagged or the driver would hand it
9761 // to the consumer as if the body had yielded it.
9762 let awaited = match CUR_GEN.with(|c| c.get()) {
9763 Some(id) if with_host(|h| h.generators[id as usize].async_gen) => with_host(|h| {
9764 let mut m = IndexMap::new();
9765 m.insert(AWAIT_MARKER.to_string(), awaited);
9766 h.new_object(m)
9767 }),
9768 _ => awaited,
9769 };
9770 let packet = gen_yield(awaited)?;
9771 let items = with_host(|h| h.iter_vec(&packet)).unwrap_or_default();
9772 let tag = items
9773 .first()
9774 .map(|v| with_host(|h| h.to_number(v)))
9775 .unwrap_or(0.0);
9776 let val = items.get(1).cloned().unwrap_or(Value::Undef);
9777 if tag == 1.0 {
9778 with_host(|h| h.exc = Some(val.clone()));
9779 Err(with_host(|h| crate::builtins::error_string(h, &val)))
9780 } else {
9781 Ok(val)
9782 }
9783}
9784
9785/// Hidden key marking an `await` suspension inside an async generator.
9786const AWAIT_MARKER: &str = "@@await";
9787
9788/// The operand of an `await` suspension, or `None` for a real `yield`.
9789fn await_marker(v: &Value) -> Option<Value> {
9790 with_host(|h| match h.get(v) {
9791 Some(JsObj::Object(props)) if props.len() == 1 => props.get(AWAIT_MARKER).cloned(),
9792 _ => None,
9793 })
9794}
9795
9796/// `AsyncGeneratorEnqueue` — queue one request against an `async function*` and
9797/// hand back the promise its `{value, done}` record (or rejection) will settle.
9798///
9799/// All three of `.next`, `.return` and `.throw` come through here, so a request
9800/// never resumes the body while an earlier one is still suspended on an
9801/// internal `await`.
9802pub fn async_gen_enqueue(gen: &Value, req: GenReq) -> Value {
9803 let step = with_host(|h| h.new_promise());
9804 let sid = with_host(|h| h.promise_id(&step).unwrap());
9805 let id = match with_host(|h| match h.get(gen) {
9806 Some(JsObj::Generator { id }) => Some(*id),
9807 _ => None,
9808 }) {
9809 Some(id) => id,
9810 None => return step,
9811 };
9812 with_host(|h| h.generators[id as usize].queue.push_back((req, sid)));
9813 pump_async_gen(gen.clone(), id);
9814 step
9815}
9816
9817/// One `.next(v)` of an `async function*`.
9818pub fn async_gen_step(gen: &Value, send: Value) -> Value {
9819 async_gen_enqueue(gen, GenReq::Next(send))
9820}
9821
9822/// `AsyncGeneratorResumeNext`: start the oldest queued request, unless one is
9823/// already in flight (the body may only be resumed by one request at a time).
9824fn pump_async_gen(gen: Value, id: u32) {
9825 if with_host(|h| h.generators[id as usize].running) {
9826 return;
9827 }
9828 let Some((req, sid)) = with_host(|h| h.generators[id as usize].queue.pop_front()) else {
9829 return;
9830 };
9831 with_host(|h| h.generators[id as usize].running = true);
9832 start_async_gen_req(gen, sid, req);
9833}
9834
9835/// Begin one queued request: resume the body with the completion it carries,
9836/// then hand the outcome to the shared continuation.
9837///
9838/// A RETURN completion always Awaits its value before the body sees it — via
9839/// `AsyncGeneratorUnwrapYieldResumption` (ECMA-262 27.6.3.7) when the generator
9840/// is suspended at a `yield`, and via `AsyncGeneratorAwaitReturn` (27.6.3.9)
9841/// when it is not yet started or already completed. So a `.return()` settles one
9842/// microtask after a `.next()` or `.throw()` issued in its place would, and the
9843/// `finally` it unwinds through runs a tick later too. Skipping that tick lets a
9844/// `.return()` overtake the reactions of the `.next()` it followed.
9845fn start_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9846 if matches!(req, GenReq::Return(_)) {
9847 with_host(|h| {
9848 h.queue_micro_native(Box::new(move || {
9849 resume_async_gen_req(gen, sid, req);
9850 Ok(())
9851 }))
9852 });
9853 return;
9854 }
9855 resume_async_gen_req(gen, sid, req);
9856}
9857
9858/// Deliver a queued completion to the body and settle its step promise.
9859fn resume_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9860 let step = match req {
9861 GenReq::Next(v) => gen_resume(&gen, v),
9862 GenReq::Return(v) => gen_return(&gen, v),
9863 GenReq::Throw(e) => gen_throw(&gen, e),
9864 };
9865 settle_async_gen_step(gen, sid, step);
9866}
9867
9868/// One request has settled: release the body and start the next queued request.
9869fn finish_async_gen_step(gen: Value, id: u32) {
9870 with_host(|h| h.generators[id as usize].running = false);
9871 pump_async_gen(gen, id);
9872}
9873
9874/// Whether `v` is an `async function*` object (its `.next()` yields promises).
9875pub fn is_async_generator(v: &Value) -> bool {
9876 let id = match with_host(|h| match h.get(v) {
9877 Some(JsObj::Generator { id }) => Some(*id),
9878 _ => None,
9879 }) {
9880 Some(id) => id,
9881 None => return false,
9882 };
9883 with_host(|h| h.generators[id as usize].async_gen)
9884}
9885
9886/// A `{ value, done }` iterator-result object.
9887fn iter_record(value: Value, done: bool) -> Value {
9888 with_host(|h| {
9889 let mut m = IndexMap::new();
9890 m.insert("value".to_string(), value);
9891 m.insert("done".to_string(), Value::Bool(done));
9892 h.new_object(m)
9893 })
9894}
9895
9896/// Resume a request that was suspended on an internal `await` (always a normal
9897/// completion — the awaited promise's outcome rides in `packet`).
9898fn drive_async_gen(gen: Value, sid: u32, packet: Value) {
9899 let step = gen_resume(&gen, packet);
9900 settle_async_gen_step(gen, sid, step);
9901}
9902
9903/// Turn one body resumption into a settled step promise: transparently re-drive
9904/// internal `await` suspensions, and settle on the first REAL yield or on the
9905/// body's completion. Shared by the initial resume of a queued request and by
9906/// every await-resumption of it.
9907fn settle_async_gen_step(gen: Value, sid: u32, step: Result<GenStep, String>) {
9908 let id = match with_host(|h| match h.get(&gen) {
9909 Some(JsObj::Generator { id }) => Some(*id),
9910 _ => None,
9911 }) {
9912 Some(id) => id,
9913 None => return,
9914 };
9915 match step {
9916 Ok(GenStep::Yield(v)) => match await_marker(&v) {
9917 Some(awaited) => {
9918 // An internal `await`: settle it, then resume the body. The
9919 // request stays in flight across the suspension.
9920 let ap = promise_of(&awaited);
9921 let aid = with_host(|h| h.promise_id(&ap).unwrap());
9922 subscribe_native(
9923 aid,
9924 Box::new(move |state, val| {
9925 let tag = if state == PromiseState::Rejected {
9926 1.0
9927 } else {
9928 0.0
9929 };
9930 let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9931 drive_async_gen(gen.clone(), sid, packet);
9932 Ok(())
9933 }),
9934 );
9935 }
9936 // ECMA-262 27.6.3.8 AsyncGeneratorYield step 5: the yielded value is
9937 // AWAITED before the step promise settles, so `yield somePromise`
9938 // hands the consumer the RESOLVED value (and costs its microtask).
9939 None => {
9940 let yp = promise_of(&v);
9941 let yid = with_host(|h| h.promise_id(&yp).unwrap());
9942 subscribe_native(
9943 yid,
9944 Box::new(move |state, val| {
9945 if state == PromiseState::Rejected {
9946 reject_promise_val(sid, val);
9947 } else {
9948 resolve_promise_val(sid, iter_record(val, false));
9949 }
9950 finish_async_gen_step(gen.clone(), id);
9951 Ok(())
9952 }),
9953 );
9954 }
9955 },
9956 Ok(GenStep::Done(v)) => {
9957 resolve_promise_val(sid, iter_record(v, true));
9958 finish_async_gen_step(gen, id);
9959 }
9960 Err(e) => {
9961 let ev = take_exc_or_error(&e);
9962 reject_promise_val(sid, ev);
9963 finish_async_gen_step(gen, id);
9964 }
9965 }
9966}
9967
9968/// A promise for `v`: `v` itself if it is already a promise, else a promise
9969/// resolved with `v`.
9970pub fn promise_of(v: &Value) -> Value {
9971 if with_host(|h| h.promise_id(v)).is_some() {
9972 return v.clone();
9973 }
9974 let p = with_host(|h| h.new_promise());
9975 let id = with_host(|h| h.promise_id(&p).unwrap());
9976 resolve_promise_val(id, v.clone());
9977 p
9978}
9979
9980/// Register a native reaction on promise `id` (schedules immediately if already
9981/// settled).
9982pub fn subscribe_native(id: u32, f: Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>) {
9983 // A native continuation (`await`, promise adoption, `for await`) observes a
9984 // rejection exactly as a `.catch` does, so it is not "unhandled".
9985 with_host(|h| h.promise_mark_handled(id));
9986 let state = with_host(|h| h.promise_state(id));
9987 if state == PromiseState::Pending {
9988 with_host(|h| h.add_reaction(id, PromiseReaction::Native(f)));
9989 } else {
9990 let val = with_host(|h| h.promise_value(id));
9991 with_host(|h| h.queue_micro_native(Box::new(move || f(state, val))));
9992 }
9993}
9994
9995/// The Promise "resolve" operation: adopt `value`'s state if it is a promise,
9996/// else fulfill with it.
9997pub fn resolve_promise_val(id: u32, value: Value) {
9998 if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
9999 return;
10000 }
10001 if let Some(vid) = with_host(|h| h.promise_id(&value)) {
10002 if vid == id {
10003 // Resolving a promise with itself → reject with a TypeError.
10004 let e = with_host(|h| {
10005 crate::builtins::synth_error(h, "TypeError: Chaining cycle detected")
10006 });
10007 reject_promise_val(id, e);
10008 return;
10009 }
10010 // A native promise is still a thenable, so the spec routes it through
10011 // `NewPromiseResolveThenableJob` too — one microtask before the adoption
10012 // is even registered. (`await` does NOT pay this: V8's await optimization
10013 // subscribes to a native promise directly, which `await_value` mirrors.)
10014 with_host(|h| {
10015 h.queue_micro_native(Box::new(move || {
10016 subscribe_native(
10017 vid,
10018 Box::new(move |state, val| {
10019 with_host(|h| h.settle_promise(id, state, val.clone()));
10020 schedule_reactions(id);
10021 Ok(())
10022 }),
10023 );
10024 Ok(())
10025 }))
10026 });
10027 return;
10028 }
10029 // ECMA-262 27.2.1.3.2: any OBJECT carrying a callable `then` is assimilated
10030 // through a dedicated job — the promise adopts what `then` reports, it is
10031 // never fulfilled WITH the thenable itself.
10032 if let Some(then) = thenable_then(&value) {
10033 with_host(|h| {
10034 h.queue_micro_native(Box::new(move || resolve_thenable_job(id, value, then)))
10035 });
10036 return;
10037 }
10038 with_host(|h| h.settle_promise(id, PromiseState::Fulfilled, value));
10039 schedule_reactions(id);
10040}
10041
10042/// `value.then` if `value` is an object with a callable `then` — the test that
10043/// makes a value a *thenable*. Primitives (and objects without one) are `None`.
10044fn thenable_then(value: &Value) -> Option<Value> {
10045 // A PROXY is not a plain object and supplies `then` through its `get` trap,
10046 // so both tests below missed it: `Promise.resolve(proxyThenable)` fulfilled
10047 // WITH the proxy instead of adopting it.
10048 if with_host(|h| h.kind_of(value)) == Some(ObjKind::Proxy) {
10049 return protocol_lookup(value, "then")
10050 .ok()
10051 .flatten()
10052 .filter(|f| with_host(|h| is_callable(h, f)));
10053 }
10054 if !with_host(|h| matches!(h.get(value), Some(JsObj::Object(_)))) {
10055 return None;
10056 }
10057 let then = with_host(|h| lookup_chain(h, value, "then"))?;
10058 with_host(|h| is_callable(h, &then)).then_some(then)
10059}
10060
10061/// `NewPromiseResolveThenableJob`: hand the thenable this promise's own resolve /
10062/// reject continuations and let it settle us. A throw out of `then` rejects.
10063fn resolve_thenable_job(id: u32, thenable: Value, then: Value) -> Result<(), String> {
10064 let res = with_host(|h| h.alloc(JsObj::Builtin(format!("@@presolve:{id}"))));
10065 let rej = with_host(|h| h.alloc(JsObj::Builtin(format!("@@preject:{id}"))));
10066 if let Err(e) = invoke(&then, vec![res, rej], Some(thenable)) {
10067 let ev = take_exc_or_error(&e);
10068 reject_promise_val(id, ev);
10069 }
10070 Ok(())
10071}
10072
10073pub fn reject_promise_val(id: u32, value: Value) {
10074 if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
10075 return;
10076 }
10077 with_host(|h| {
10078 h.settle_promise(id, PromiseState::Rejected, value);
10079 h.pending_rejections.push(id);
10080 });
10081 schedule_reactions(id);
10082}
10083
10084/// Report every promise that settled rejected since the last checkpoint and
10085/// still has no handler. Node's default is `--unhandled-rejections=throw`: the
10086/// rejection becomes an uncaught exception (stderr + exit 1) unless a
10087/// `process.on('unhandledRejection')` listener takes it.
10088fn check_unhandled_rejections() -> Result<(), String> {
10089 loop {
10090 let ids: Vec<u32> = with_host(|h| std::mem::take(&mut h.pending_rejections));
10091 if ids.is_empty() {
10092 return Ok(());
10093 }
10094 for id in ids {
10095 let unhandled = with_host(|h| {
10096 h.promise_state(id) == PromiseState::Rejected && !h.promises[id as usize].handled
10097 });
10098 if !unhandled {
10099 continue;
10100 }
10101 // Report each promise at most once, however many checkpoints pass.
10102 with_host(|h| h.promise_mark_handled(id));
10103 let val = with_host(|h| h.promise_value(id));
10104 let listeners = with_host(|h| h.take_process_listeners("unhandledRejection"));
10105 if listeners.is_empty() {
10106 let msg = with_host(|h| crate::builtins::error_string(h, &val));
10107 with_host(|h| h.exc = Some(val));
10108 return Err(msg);
10109 }
10110 let promise = with_host(|h| h.alloc(JsObj::Promise { id }));
10111 for f in listeners {
10112 invoke(&f, vec![val.clone(), promise.clone()], None)?;
10113 }
10114 }
10115 }
10116}
10117
10118/// Drain a settled promise's reactions into microtasks.
10119fn schedule_reactions(id: u32) {
10120 let reactions = with_host(|h| h.take_reactions(id));
10121 let state = with_host(|h| h.promise_state(id));
10122 let value = with_host(|h| h.promise_value(id));
10123 for r in reactions {
10124 let value = value.clone();
10125 match r {
10126 PromiseReaction::Native(f) => {
10127 with_host(|h| h.queue_micro_native(Box::new(move || f(state, value))));
10128 }
10129 PromiseReaction::Js {
10130 on_ful,
10131 on_rej,
10132 result,
10133 } => {
10134 with_host(|h| {
10135 h.queue_micro_native(Box::new(move || {
10136 run_js_reaction(state, value, on_ful, on_rej, result)
10137 }))
10138 });
10139 }
10140 }
10141 }
10142}
10143
10144/// Run a `.then` reaction: call the appropriate handler and settle the result
10145/// promise with its outcome (or pass through if there is no handler).
10146fn run_js_reaction(
10147 state: PromiseState,
10148 value: Value,
10149 on_ful: Value,
10150 on_rej: Value,
10151 result: Value,
10152) -> Result<(), String> {
10153 let rid = match with_host(|h| h.promise_id(&result)) {
10154 Some(i) => i,
10155 None => return Ok(()),
10156 };
10157 let handler = if state == PromiseState::Rejected {
10158 on_rej
10159 } else {
10160 on_ful
10161 };
10162 if with_host(|h| is_callable(h, &handler)) {
10163 match invoke(&handler, vec![value], None) {
10164 Ok(r) => resolve_promise_val(rid, r),
10165 Err(e) => reject_promise_val(rid, take_exc_or_error(&e)),
10166 }
10167 } else if state == PromiseState::Rejected {
10168 reject_promise_val(rid, value);
10169 } else {
10170 resolve_promise_val(rid, value);
10171 }
10172 Ok(())
10173}
10174
10175/// The JS value of a just-caught error: the live `exc` (a real thrown value) or a
10176/// synthesized `Error` from the internal message.
10177pub fn take_exc_or_error(e: &str) -> Value {
10178 with_host(|h| {
10179 h.error.take();
10180 h.exc
10181 .take()
10182 .unwrap_or_else(|| crate::builtins::synth_error(h, e))
10183 })
10184}
10185
10186/// Register a user `.then` reaction (JS handlers + result promise).
10187pub fn promise_then(p: &Value, on_ful: Value, on_rej: Value) -> Value {
10188 let id = match with_host(|h| h.promise_id(p)) {
10189 Some(i) => i,
10190 None => return Value::Undef,
10191 };
10192 with_host(|h| h.promise_mark_handled(id));
10193 // The result is built with the RECEIVER's species, so a subclass promise
10194 // stays a subclass promise through a `.then` chain.
10195 let result = match crate::builtins::promise_species_from(p) {
10196 Ok(Some(sp)) => sp,
10197 _ => with_host(|h| h.new_promise()),
10198 };
10199 let reaction = PromiseReaction::Js {
10200 on_ful,
10201 on_rej,
10202 result: result.clone(),
10203 };
10204 let state = with_host(|h| h.promise_state(id));
10205 if state == PromiseState::Pending {
10206 with_host(|h| h.add_reaction(id, reaction));
10207 } else {
10208 let value = with_host(|h| h.promise_value(id));
10209 if let PromiseReaction::Js {
10210 on_ful,
10211 on_rej,
10212 result,
10213 } = reaction
10214 {
10215 with_host(|h| {
10216 h.queue_micro_native(Box::new(move || {
10217 run_js_reaction(state, value, on_ful, on_rej, result)
10218 }))
10219 });
10220 }
10221 }
10222 result
10223}
10224
10225/// The ReferenceError for touching `this` in a derived constructor before
10226/// `super()` — or returning from one without calling it.
10227pub fn this_before_super_error() -> String {
10228 "ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor".to_string()
10229}
10230
10231/// The intrinsic a builtin name denotes. Two property paths that the spec
10232/// defines as the SAME function object compare `===`: `Number.parseInt` is
10233/// `%parseInt%` (21.1.2.13) and `Number.parseFloat` is `%parseFloat%`
10234/// (21.1.2.12), so `Number.parseInt === parseInt` is `true`.
10235fn builtin_identity(name: &str) -> &str {
10236 match name {
10237 "Number.parseInt" => "parseInt",
10238 "Number.parseFloat" => "parseFloat",
10239 _ => name,
10240 }
10241}