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/// What an array iterator yields at each index: `keys()`, `values()` (and
421/// `Symbol.iterator`), or `entries()`.
422#[derive(Clone, Copy, PartialEq, Eq)]
423pub enum ArrayIterKind {
424 Keys,
425 Values,
426 Entries,
427}
428
429/// A heap object.
430#[derive(Clone)]
431pub enum JsObj {
432 Str(String),
433 Array(Vec<Value>),
434 Object(IndexMap<String, Value>),
435 Func(FuncVal),
436 /// A first-class reference to a builtin function or namespace
437 /// (`console.log`, `Math`, `parseInt`).
438 Builtin(String),
439 /// A bound method value (`obj.method` captured then called): dispatches
440 /// through `call_method(recv, name, args)` when invoked.
441 BoundMethod {
442 recv: Value,
443 name: String,
444 },
445 /// The single canonical `null`.
446 Null,
447 /// An iterator over a sequence, with a cursor.
448 ///
449 /// `items` is a snapshot, taken when the iterator was made. An ARRAY
450 /// iterator is not one: `array` names the array and what each step yields,
451 /// and every step reads the array as it is then (23.1.5.1), so a `for-of`
452 /// sees an element pushed or written during the loop and stops at a length
453 /// that shrank. `items` is empty for those.
454 Iter {
455 items: Vec<Value>,
456 idx: usize,
457 array: Option<(Value, ArrayIterKind)>,
458 },
459 /// A bound function (`fn.bind(thisArg, ...preargs)`).
460 BoundFunc {
461 target: Value,
462 this: Value,
463 args: Vec<Value>,
464 },
465 /// A class constructor value: the runtime object produced by a `class`.
466 Class(ClassVal),
467 /// A `Symbol` — a unique property key. `registered` marks a `Symbol.for`
468 /// key (shared) vs a fresh `Symbol()`.
469 Symbol {
470 desc: Option<String>,
471 id: u64,
472 },
473 /// A `Map` (or `WeakMap` when `weak`): insertion-ordered key→value entries.
474 Map {
475 entries: IndexMap<MapKey, (Value, Value)>,
476 weak: bool,
477 },
478 /// A `Set` (or `WeakSet` when `weak`): insertion-ordered unique values.
479 Set {
480 entries: IndexMap<MapKey, Value>,
481 weak: bool,
482 },
483 /// A live generator, backed by a stackful `corosensei` coroutine in
484 /// `JsHost.generators`.
485 Generator {
486 id: u32,
487 },
488 /// A Promise, backed by a `PromiseCell` in `JsHost.promises`.
489 Promise {
490 id: u32,
491 },
492 /// An arbitrary-precision `BigInt` (`typeof === "bigint"`).
493 BigInt(num_bigint::BigInt),
494 /// A compiled regular expression (`/pat/flags` or `new RegExp(...)`).
495 RegExp(Box<RegExpObj>),
496 /// A `Proxy`: every essential internal method is diverted to `handler`'s
497 /// traps (see `crate::proxy`). `revoked` is set by the thunk
498 /// `Proxy.revocable` hands back, after which every operation throws.
499 Proxy {
500 target: Value,
501 handler: Value,
502 revoked: bool,
503 },
504}
505
506/// Which variant a heap object is, carrying none of its contents.
507///
508/// Property access has to pick a branch by variant, but the code inside a branch
509/// re-enters the host (`bound_method`, `lookup_chain`, `invoke`), so it cannot
510/// hold a `&JsObj` borrow across the match. The way out used to be
511/// `h.get(v).cloned()` — which deep-copies the entire backing store (a whole
512/// `Vec<Value>`, `IndexMap`, or `String`) just to read its tag. That made one
513/// property read O(len) and any loop over a collection O(n^2). This type is the
514/// same discriminant with nothing attached, so the probe is O(1) and each branch
515/// re-borrows for only the one field it actually needs.
516/// The well-known symbols node-js actually honors. `Symbol.<name>` is the
517/// interned symbol `@@Symbol.<name>`, and using it as a property key stores
518/// under the sentinel string `@@<name>` (`property_key`) so the internal
519/// lookups (`@@iterator`, `@@toPrimitive`, …) can find it without a symbol
520/// table walk. Symbols V8 defines but node-js does not act on are deliberately
521/// absent: a symbol that reads back while the operator it names ignores it would
522/// be a silent fake. `hasInstance` is listed because `instance_of` consults it.
523pub const WELL_KNOWN_SYMBOLS: &[&str] = &[
524 "iterator",
525 "asyncIterator",
526 "toPrimitive",
527 "toStringTag",
528 "hasInstance",
529 // Nine more the table was missing entirely, so `Symbol.species` and friends
530 // read `undefined` and no protocol keyed on them could be expressed.
531 "species",
532 "isConcatSpreadable",
533 "match",
534 "matchAll",
535 "replace",
536 "search",
537 "split",
538 "unscopables",
539 "dispose",
540 "asyncDispose",
541];
542
543/// Whether the internal key `k` came from a SYMBOL used as a property key
544/// (`@@sym:<id>`, or a well-known `@@iterator`), as opposed to one of node-js's
545/// hidden slots (`@@native`, `@@bytes`, `@@ms`, `@@kind`, …). Only the former
546/// is an observable JavaScript property.
547pub fn is_symbol_key(k: &str) -> bool {
548 match k.strip_prefix("@@") {
549 Some(rest) => rest
550 .strip_prefix("sym:")
551 .map(|i| i.parse::<u64>().is_ok())
552 .unwrap_or_else(|| WELL_KNOWN_SYMBOLS.contains(&rest)),
553 None => false,
554 }
555}
556
557#[derive(Clone, Copy, PartialEq, Eq, Debug)]
558pub enum ObjKind {
559 Str,
560 Array,
561 Object,
562 Func,
563 Builtin,
564 BoundMethod,
565 Null,
566 Iter,
567 BoundFunc,
568 Class,
569 Symbol,
570 Map,
571 Set,
572 Generator,
573 Promise,
574 BigInt,
575 RegExp,
576 Proxy,
577}
578
579impl JsObj {
580 /// This object's variant, without touching its contents.
581 pub fn kind(&self) -> ObjKind {
582 match self {
583 JsObj::Str(_) => ObjKind::Str,
584 JsObj::Array(_) => ObjKind::Array,
585 JsObj::Object(_) => ObjKind::Object,
586 JsObj::Func(_) => ObjKind::Func,
587 JsObj::Builtin(_) => ObjKind::Builtin,
588 JsObj::BoundMethod { .. } => ObjKind::BoundMethod,
589 JsObj::Null => ObjKind::Null,
590 JsObj::Iter { .. } => ObjKind::Iter,
591 JsObj::BoundFunc { .. } => ObjKind::BoundFunc,
592 JsObj::Class(_) => ObjKind::Class,
593 JsObj::Symbol { .. } => ObjKind::Symbol,
594 JsObj::Map { .. } => ObjKind::Map,
595 JsObj::Set { .. } => ObjKind::Set,
596 JsObj::Generator { .. } => ObjKind::Generator,
597 JsObj::Promise { .. } => ObjKind::Promise,
598 JsObj::BigInt(_) => ObjKind::BigInt,
599 JsObj::RegExp(_) => ObjKind::RegExp,
600 JsObj::Proxy { .. } => ObjKind::Proxy,
601 }
602 }
603}
604
605/// A `RegExp` object: the compiled `fancy_regex::Regex` plus the JS-visible
606/// source, flag booleans, and the mutable `lastIndex` cursor (used by `g`/`y`
607/// matching). fancy-regex adds lookaround + backreferences on top of the Rust
608/// `regex` fast path, so the JS grammar node-js can accept is a near-superset.
609#[derive(Clone)]
610pub struct RegExpObj {
611 /// The translated regex. Construction of a pattern fancy-regex still cannot
612 /// express (documented in BUGS.md) throws at `RegExp` build time, so a live
613 /// `RegExpObj` always holds a compiled engine.
614 ///
615 /// Shared (`Rc`) rather than owned, because a regex LITERAL builds a fresh
616 /// `RegExpObj` on every evaluation — it has to, since `lastIndex` is
617 /// per-object mutable state — while the compiled engine behind it is
618 /// immutable and identical every time. See `regexp::compiled`.
619 pub re: std::rc::Rc<fancy_regex::Regex>,
620 pub source: String,
621 pub flags: String,
622 pub global: bool,
623 pub ignore_case: bool,
624 pub multiline: bool,
625 pub dot_all: bool,
626 pub sticky: bool,
627 pub unicode: bool,
628 /// `lastIndex`, in UTF-16 code units; advanced by `exec`/`test` under the
629 /// `g`/`y` flags. The newtype keeps it from being confused with the regex
630 /// engine's byte offsets, which are the same shape and differ off the BMP.
631 pub last_index: crate::utf16::U16Index,
632}
633
634/// A Promise's settled state and pending reactions.
635pub struct PromiseCell {
636 pub state: PromiseState,
637 pub value: Value,
638 /// Reactions registered while still pending; drained (as microtasks) on
639 /// settle.
640 pub reactions: Vec<PromiseReaction>,
641 /// True once a rejection has been observed by a handler (`.then`/`.catch`),
642 /// so the loop doesn't report it as unhandled.
643 pub handled: bool,
644}
645
646/// A pending Promise reaction: a user `.then` (JS handlers + a result promise) or
647/// a native continuation (Promise chaining / async `await` resumption).
648pub enum PromiseReaction {
649 Js {
650 on_ful: Value,
651 on_rej: Value,
652 result: Value,
653 },
654 Native(Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>),
655}
656
657#[derive(Default, Clone, Copy, PartialEq, Eq)]
658pub enum PromiseState {
659 #[default]
660 Pending,
661 Fulfilled,
662 Rejected,
663}
664
665/// A live class constructor. The prototype object (holding instance methods) and
666/// the static-side own properties live on the heap; `parent` is the superclass
667/// constructor value (`None` for a base class).
668#[derive(Clone)]
669pub struct ClassVal {
670 pub name: String,
671 /// The constructor function value (a `JsObj::Func`), or `None` for a class
672 /// with only a synthesized default constructor.
673 pub ctor: Option<Value>,
674 pub parent: Option<Value>,
675 /// `C.prototype` — the object instances delegate to.
676 pub proto: Value,
677 /// Static own properties (static methods/fields), plus `name`/`prototype`.
678 pub statics: IndexMap<String, Value>,
679 /// Instance field initializers: `(name, thunk_fn, name_anon_init)`, run
680 /// per-instance after `super()` (or at construction start for a base class).
681 /// `name_anon_init` records the SYNTACTIC fact that the initializer was an
682 /// anonymous function definition, so 15.7.10 NamedEvaluation applies to its
683 /// result — it cannot be re-derived at run time (a field initialised from an
684 /// already-anonymous function held elsewhere must not be renamed).
685 pub fields: Vec<(String, Value, bool)>,
686 /// The FuncDef holding the class's source span (`String(C)`).
687 pub source_def: Option<usize>,
688}
689
690/// The result of resolving `super.name`: a getter to invoke (accessor property)
691/// or a directly-usable value (method / data property).
692pub enum SuperRef {
693 Getter(Value),
694 Data(Value),
695}
696
697/// A `Map`/`Set` key under SameValueZero: `NaN` collapses to one key, `-0` and
698/// `+0` are the same key, primitives compare by value, objects by heap identity.
699#[derive(Clone, PartialEq, Eq, Hash)]
700pub enum MapKey {
701 Undef,
702 Null,
703 Bool(bool),
704 /// f64 bit pattern with `NaN` canonicalized and `-0` normalized to `+0`.
705 Num(u64),
706 /// A `BigInt` key, by its decimal string (SameValueZero: `1n` is one key).
707 Big(String),
708 Str(String),
709 /// Heap identity (objects, arrays, functions, symbols).
710 Ref(u32),
711 /// A builtin intrinsic, by the name it answers to. Every bare reference
712 /// to `Math` or `parseInt` allocates a fresh handle, so heap identity
713 /// would make `new Set([Math, Math])` two entries; `strict_eq` compares
714 /// these by name too.
715 Intrinsic(String),
716}
717
718// ── environments ─────────────────────────────────────────────────────────────
719
720/// The map behind a scope. Hashing these with `FxHash` instead of the default
721/// was measured SLOWER, not faster — fib went 652ms to 1086ms and a 5M-iteration
722/// counting loop 1894ms to 2381ms on the same machine — so the default stands.
723pub type VarMap = IndexMap<String, Value>;
724
725/// A local-variable environment, shared (by `Rc`) between a frame and any nested
726/// function that captures it.
727pub struct EnvData {
728 pub vars: VarMap,
729 /// The names in `vars` that were declared `const`, so an assignment to one
730 /// throws (16.1.3 / 8.5.2 — an immutable binding rejects SetMutableBinding).
731 ///
732 /// A separate set rather than a flag inside `VarMap`'s value, because
733 /// `set_name` is a hot path — the common case is an env with NO consts,
734 /// where `is_empty()` settles it without hashing the name a second time.
735 pub consts: rustc_hash::FxHashSet<String>,
736 pub parent: Option<Env>,
737}
738pub type Env = Rc<RefCell<EnvData>>;
739
740/// An accessor property: `(getter, setter)`, either optional.
741pub type Accessor = (Option<Value>, Option<Value>);
742
743/// Prefix of the hidden property-map entry that reserves an accessor's slot in
744/// own-key insertion order (see `set_accessor`).
745pub const ORD_MARKER: &str = "@@ord:";
746
747/// The three ECMAScript own-property attributes. `PropAttrs::default()` is the
748/// all-true shape a plain `o.k = v` assignment produces, which is why only
749/// deviations need storing.
750#[derive(Clone, Copy, Debug, PartialEq, Eq)]
751pub struct PropAttrs {
752 pub writable: bool,
753 pub enumerable: bool,
754 pub configurable: bool,
755}
756
757impl Default for PropAttrs {
758 fn default() -> Self {
759 PropAttrs {
760 writable: true,
761 enumerable: true,
762 configurable: true,
763 }
764 }
765}
766
767impl PropAttrs {
768 /// The attribute shape V8 gives an internal-but-inspectable slot such as
769 /// `Error.prototype.message`, `err.stack` or a `Buffer`'s view metadata:
770 /// readable and replaceable, but never enumerated.
771 pub const HIDDEN: PropAttrs = PropAttrs {
772 writable: true,
773 enumerable: false,
774 configurable: true,
775 };
776}
777
778fn new_env(parent: Option<Env>) -> Env {
779 Rc::new(RefCell::new(EnvData {
780 vars: VarMap::default(),
781 consts: rustc_hash::FxHashSet::default(),
782 parent,
783 }))
784}
785
786/// A fresh empty scope chained under `parent`.
787pub fn child_env(parent: Env) -> Env {
788 new_env(Some(parent))
789}
790
791/// One function activation.
792pub struct Frame {
793 pub env: Env,
794 /// The env this activation started in — the FUNCTION scope. `var` and hoisted
795 /// function declarations bind here no matter how many block scopes are open.
796 pub base_env: Env,
797 pub this_obj: Option<Value>,
798 /// `new.target` for this activation (the constructor when invoked via `new`).
799 pub new_target: Option<Value>,
800 /// The class value owning the running method (drives `super`); `None` outside
801 /// a class method/constructor.
802 pub home_class: Option<Value>,
803 /// Whether the running method is a static one — see `FuncVal::home_static`.
804 pub home_static: bool,
805 /// The object literal owning the running method — see
806 /// `FuncVal::home_object`.
807 pub home_object: Option<Value>,
808 /// Whether the code in this activation is strict. A write the object
809 /// refuses is a silent no-op in sloppy mode and a `TypeError` here, so the
810 /// ASSIGNMENT SITE decides — not the object being written to.
811 pub strict: bool,
812 /// Source line the frame is currently executing (updated by the DAP line hook
813 /// under `--dap`; stays 0 on ordinary runs).
814 pub line: u32,
815 /// The function name that owns this frame, for the DAP `stackTrace`; `None`
816 /// for the module frame and anonymous activations.
817 pub owner: Option<String>,
818 /// True ONLY for the program's module frame. A generator/async body runs on a
819 /// coroutine whose swapped-in context holds just ITS OWN frame, so the frame
820 /// COUNT cannot tell "module scope" from "coroutine body scope" — without this
821 /// flag every top-level `let`/`var` in such a body declared a GLOBAL, shared
822 /// across concurrent activations of the same function.
823 pub is_module: bool,
824 /// Whether this activation's `this` is bound yet — see [`ThisState`].
825 pub this_state: ThisState,
826}
827
828/// The `[[ThisBindingStatus]]` of a function environment (9.1.1.3), as far as it
829/// is observable: only a DERIVED class constructor starts with `this`
830/// uninitialized, and only `super()` binds it.
831///
832/// The instance is still allocated up front (`construct_class`), so the
833/// state is what makes it unreachable until then: `this` before `super()`, a
834/// second `super()`, and returning without one are each the error node raises
835/// rather than a silent write to the pre-allocated object.
836#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
837pub enum ThisState {
838 /// Every other activation: `this` is whatever was passed in.
839 #[default]
840 Plain,
841 /// A derived constructor before `super()` has returned.
842 Pending,
843 /// A derived constructor after `super()`.
844 Bound,
845}
846
847/// A non-local control signal. `Break`/`Continue` carry the optional loop label
848/// and are only raised when the target loop lives in an ENCLOSING chunk (a
849/// `break` inside a `try` block, which the host runs as its own chunk); a
850/// same-chunk `break` is a plain compiler-resolved jump.
851#[derive(Clone)]
852pub enum Signal {
853 Return(Value),
854 Break(Option<String>),
855 Continue(Option<String>),
856}
857
858/// The JavaScript runtime.
859pub struct JsHost {
860 heap: Vec<JsObj>,
861 /// Function templates, indexed by def id.
862 pub funcs: Vec<FuncDef>,
863 /// Every script text a loaded program was parsed from; a `FuncDef`'s
864 /// `script` indexes it and its `span` slices it.
865 pub scripts: Vec<std::sync::Arc<str>>,
866 /// try/catch/finally block templates, indexed by try id.
867 pub tries: Vec<TryDef>,
868 /// Module-level (global) names.
869 globals: VarMap,
870 /// The one uninitialized-binding marker, allocated on first use. See
871 /// [`JsHost::tdz_marker`].
872 tdz: Option<Value>,
873 /// Module-top-level names still in their temporal dead zone. Kept out of
874 /// `globals` so the marker is never reachable as `globalThis.<name>`.
875 tdz_globals: rustc_hash::FxHashSet<String>,
876 /// Top-level `const` names (a module frame declares into `globals`), so an
877 /// assignment to one throws the same way a block-scoped `const` does.
878 global_consts: rustc_hash::FxHashSet<String>,
879 /// The frame stack (bottom = module).
880 frames: Vec<Frame>,
881 /// The program's top-level scope — the scope runtime-compiled source runs in
882 /// (`new Function`, indirect `eval`, `vm.runInThisContext`; see
883 /// `run_chunk_in_global_scope`), as opposed to whatever function frame
884 /// happens to be executing when that source is compiled.
885 ///
886 /// Held as its own field rather than read off `frames[0]` because a coroutine
887 /// body runs with `frames` SWAPPED for its own one-frame context
888 /// (`install_gen_ctx`), so the bottom frame is not the top-level frame there.
889 ///
890 /// Note this is node-js's ONE top-level scope. Node distinguishes the global
891 /// scope from a CommonJS module's scope (a module body is a wrapper
892 /// function), so in Node a file's top-level `var` is invisible to dynamic
893 /// code; here the entry file is evaluated with Script semantics, so it stays
894 /// visible. That is the same entry-file-is-a-Script divergence `BUGS.md`
895 /// records for top-level `return`, not a separate one — and `node -e`, which
896 /// really is a Script, matches Node exactly.
897 global_env: Env,
898 pub error: Option<String>,
899 /// The in-flight thrown value, if any (JS `throw`).
900 pub exc: Option<Value>,
901 pub signal: Option<Signal>,
902 /// Promises that settled REJECTED this tick. Drained at each microtask
903 /// checkpoint: any still without a handler is an unhandled rejection.
904 pub pending_rejections: Vec<u32>,
905 /// `process.on(event, fn)` listeners, by event name.
906 pub process_listeners: IndexMap<String, Vec<ProcListener>>,
907 /// The canonical `null` handle (allocated once).
908 null_val: Value,
909 /// `[[Prototype]]` link per heap object, by heap index. Absent = default
910 /// (`Object.prototype` for objects, `null` for the root).
911 protos: HashMap<u32, Value>,
912 /// Heap objects whose `[[Prototype]]` is *explicitly* null — via
913 /// `Object.create(null)` or `Object.setPrototypeOf(o, null)`. Distinct from a
914 /// bare `{}` (absent from `protos` but conceptually `Object.prototype`), which
915 /// is why `Object.create(null) instanceof Object` can read `false`.
916 null_proto_objs: HashSet<u32>,
917 /// Own properties of function objects (functions are objects in JS): a live
918 /// closure's `name`/`prototype`/static-ish members. Keyed by heap index.
919 fn_props: HashMap<u32, IndexMap<String, Value>>,
920 /// Accessor (getter/setter) properties per owning object, by heap index then
921 /// key: `(get, set)`. Class `get x()`/`set x()` install here on the prototype.
922 accessors: HashMap<u32, IndexMap<String, Accessor>>,
923 /// Own-property attributes that deviate from the plain-assignment default
924 /// (`{writable, enumerable, configurable}` all true), by heap index then key.
925 /// Only non-default entries are stored, so an ordinary object costs nothing;
926 /// `prop_attrs` returns the default for any key absent here. This is what
927 /// makes `Object.defineProperty(o, k, {enumerable: false})` invisible to
928 /// `Object.keys`/`for-in`/`JSON.stringify` while `getOwnPropertyNames` still
929 /// reports it, and what hides `Error`'s `message`/`stack` the way V8 does.
930 prop_attrs: HashMap<u32, IndexMap<String, PropAttrs>>,
931 /// Heap objects sealed against new properties by `Object.preventExtensions`,
932 /// `Object.seal` or `Object.freeze`.
933 non_extensible: HashSet<u32>,
934 /// Private names (`#m`) declared as a METHOD or accessor rather than as a
935 /// field, for the brand-check error text: node distinguishes `Receiver must
936 /// be an instance of class C` (a private method or accessor) from `Cannot
937 /// read private member #x …` (a private field). Which class is answered by
938 /// the running method's home class, not by this set, so two classes
939 /// declaring the same private method name stay exact.
940 private_methods: HashSet<String>,
941 /// The ELIDED element positions of each array, by heap index. Absent (the
942 /// overwhelmingly common case) means the array is dense.
943 ///
944 /// A hole is deliberately NOT a `Value` variant. A sentinel value would have
945 /// to be mapped back to `undefined` at every element read in the runtime, and
946 /// a single missed read would leak an un-nameable value into user code — a
947 /// worse failure than storing `undefined` and losing the distinction. Keeping
948 /// the marker OUTSIDE the value domain makes that leak structurally
949 /// impossible: the element vector still holds a perfectly ordinary
950 /// `Value::Undef` at a hole, so any code path that has not been taught about
951 /// holes degrades to exactly the pre-existing behaviour (a visible
952 /// `undefined`) instead of producing something unrepresentable.
953 ///
954 /// Sized like the array it describes in the worst case (`new Array(n)` marks
955 /// every index), which is the same order as the `Vec<Value>` already paid for
956 /// that array — so it cannot turn a working allocation into an OOM.
957 array_holes: HashMap<u32, rustc_hash::FxHashSet<usize>>,
958 /// See `take_super_replacement`.
959 super_replacement: Option<Value>,
960 /// Set by `run_class_ctor` for the one call that follows: the next user
961 /// function activation is a derived constructor and starts `Pending`.
962 derived_ctor_next: bool,
963 /// Whether the entry script's top-level `var`s bind to its own scope rather
964 /// than to the globals map — the CommonJS wrapper Node puts every file in.
965 module_scope: bool,
966 /// User-assigned static properties on a builtin namespace/constructor, keyed
967 /// by namespace name then property (`Error` → `prepareStackTrace`,
968 /// `stackTraceLimit`). Each bare `Error` reference allocates a fresh
969 /// `Builtin` handle, so these cannot live in `fn_props` (which is per-heap-
970 /// index); this stable side table lets `Error.prepareStackTrace = fn` persist.
971 builtin_statics: HashMap<String, IndexMap<String, Value>>,
972 /// The shared well-known `Object.prototype` object (chain root for objects).
973 object_proto: Value,
974 /// Class name of each class `prototype` object, by heap index — lets an
975 /// instance recover its constructor name (for `util.inspect` prefix and
976 /// `obj.constructor.name`).
977 proto_class: HashMap<u32, Value>,
978 /// Class constructor values by name, so a running method's `home_class` name
979 /// resolves to its class value (for `super`).
980 class_registry: HashMap<String, Value>,
981 /// Well-known prototype objects for the builtin error constructors, by name.
982 error_protos: HashMap<String, Value>,
983 /// The template object of each tagged-template SITE, keyed by the chunk that
984 /// holds the site and the site's ordinal within its compilation.
985 ///
986 /// GetTemplateObject (13.2.8.4) caches by Parse Node, so a site evaluated
987 /// twice hands back the SAME object: ``const t = () => tag`x`;`` makes
988 /// `t() === t()` true, and a tag that memoizes on the strings array — the
989 /// documented reason the object is cached, and how `lit-html` and `graphql`
990 /// avoid re-parsing — saw a fresh array every call here. Two sites with
991 /// identical text are still distinct objects, which the chunk hash plus the
992 /// ordinal keep apart.
993 template_objects: HashMap<(u64, u64), Value>,
994 /// Real prototype *objects* for the builtin exotics whose instances need a
995 /// genuine `[[Prototype]]` link (`Buffer`, `Uint8Array`). Most builtin
996 /// prototypes are `Builtin("<Ctor>.prototype")` thunk namespaces, which
997 /// cannot appear on a prototype chain and report `typeof "function"`.
998 native_protos: HashMap<String, Value>,
999 /// `Symbol.for` registry: description → symbol value.
1000 symbol_registry: HashMap<String, Value>,
1001 /// Monotonic id source for fresh `Symbol()` values.
1002 next_symbol: u64,
1003 /// Every live symbol by its id, so a `@@sym:<id>` property key can be
1004 /// turned back into the symbol VALUE for `Object.getOwnPropertySymbols`.
1005 symbols_by_id: HashMap<u64, Value>,
1006 /// Well-known symbol ids (`Symbol.iterator` …) to their ECMAScript name.
1007 /// Identity is by id, not description, so a user `Symbol("Symbol.iterator")`
1008 /// is a distinct key.
1009 well_known_ids: HashMap<u64, String>,
1010 /// Suspended generator coroutines, indexed by `JsObj::Generator.id`.
1011 generators: Vec<GenCell>,
1012 /// Promise cells, indexed by `JsObj::Promise.id`.
1013 promises: Vec<PromiseCell>,
1014 /// Whether the loop is part-way through draining the microtask queue, so a
1015 /// `nextTick` queued by one of them waits for the round to finish. See
1016 /// `next_microtask`.
1017 draining_micro: bool,
1018 /// `process.nextTick` callbacks (drained before promise microtasks).
1019 pub nextticks: std::collections::VecDeque<Task>,
1020 /// Promise-reaction / `queueMicrotask` microtasks.
1021 pub microtasks: std::collections::VecDeque<Task>,
1022 /// `setTimeout`/`setInterval`/`setImmediate` macrotasks.
1023 pub macrotasks: Vec<Timer>,
1024 /// Monotonic timer-id source.
1025 next_timer: u64,
1026 /// Cloned by I/O worker threads to post `IoTask`s back to the main-thread
1027 /// event loop. Kept alive for the host's lifetime so the loop's `recv` never
1028 /// sees a spurious `Disconnected` while a server is running.
1029 io_tx: Sender<IoTask>,
1030 /// Owned by the event loop (taken out for the blocking `recv`). Receives the
1031 /// `IoTask`s posted by I/O threads.
1032 io_rx: Option<Receiver<IoTask>>,
1033 /// Ref-count of "things keeping the process alive": open listeners, live
1034 /// sockets, ref'd handles. The loop exits only when this is `0` AND both task
1035 /// queues are empty. A pure script never touches it, so it exits exactly as
1036 /// before.
1037 open_handles: usize,
1038 /// In-process output sink. When `Some`, everything the program writes to
1039 /// stdout/stderr is appended here instead of reaching the process streams —
1040 /// what an embedder (a TUI that owns the terminal) needs so a `console.log`
1041 /// cannot corrupt its display. `None` (the default) is the ordinary
1042 /// standalone `node` behaviour: writes go straight to the real streams.
1043 ///
1044 /// Bytes, not `String`: a program may legitimately write output that is not
1045 /// valid UTF-8 (`process.stdout.write(Buffer.from([0xff]))`), and a `String`
1046 /// buffer can only hold the lossy `U+FFFD` transcription of it.
1047 capture: Option<Vec<u8>>,
1048 /// `process.exitCode`: the code the process exits with when the event loop
1049 /// drains, or `None` while unset. Separate from an explicit
1050 /// `process.exit(n)`, which exits immediately with `n`.
1051 pub exit_code: Option<i32>,
1052 /// Whether the `exit` event has already been emitted, so the `process.exit`
1053 /// path and the end-of-loop path cannot both fire it (Node's `_exiting`).
1054 pub exiting: bool,
1055 /// The one `globalThis` object. It has to be a singleton: `globalThis` is an
1056 /// identity in JS, so `globalThis === globalThis` is `true` and a property
1057 /// written through one read is visible through the next. Minting a fresh
1058 /// object per read made both false.
1059 global_obj: Value,
1060}
1061
1062/// One `process.on`/`process.once` registration. `once` is not decoration: a
1063/// `once` listener must be UNREGISTERED before it runs, so a second `emit` of
1064/// the same event does not reach it. Treating `once` as an alias of `on` made
1065/// `process.once('e', f); process.emit('e'); process.emit('e')` call `f` twice
1066/// and leave it in `process.listeners('e')` — node v26.7.0 calls it once and
1067/// reports zero listeners afterwards.
1068#[derive(Clone)]
1069pub struct ProcListener {
1070 pub f: Value,
1071 pub once: bool,
1072}
1073
1074/// A queued unit of work: either a JS callback invocation (`queueMicrotask`,
1075/// `nextTick`, timer body) or a native step (Promise reaction / async resume).
1076pub enum Task {
1077 Js { cb: Value, args: Vec<Value> },
1078 Native(Box<dyn FnOnce() -> Result<(), String>>),
1079}
1080
1081impl Task {
1082 fn run(self) -> Result<(), String> {
1083 match self {
1084 Task::Js { cb, args } => invoke(&cb, args, None).map(|_| ()),
1085 Task::Native(f) => f(),
1086 }
1087 }
1088}
1089
1090/// A scheduled macrotask (`setTimeout`/`setInterval`/`setImmediate`). Ordering
1091/// is by `(delay, seq)` — a deterministic virtual clock, never wall time.
1092pub struct Timer {
1093 pub id: u64,
1094 pub delay: f64,
1095 pub seq: u64,
1096 pub callback: Value,
1097 pub args: Vec<Value>,
1098 pub cancelled: bool,
1099 /// Repeat period in ms for a `setInterval` timer; `None` for the one-shot
1100 /// `setTimeout`/`setImmediate`. A repeating timer is re-armed with a fresh
1101 /// deadline each time it fires, so it keeps the loop alive indefinitely —
1102 /// exactly like Node, where `setInterval` runs until cleared.
1103 pub interval: Option<f64>,
1104 /// Node's `ref`/`unref` handle bit. Only a *referenced* pending timer keeps
1105 /// the event loop alive; an unref'd one still fires while the loop happens
1106 /// to be alive for another reason, but never holds it open by itself.
1107 pub refed: bool,
1108 /// Real wall-clock deadline (`now + delay`), used only on the real-clock
1109 /// path (an open handle or a pending interval). On the pure virtual clock
1110 /// this is ignored.
1111 pub deadline: Instant,
1112}
1113
1114/// One suspended generator. `coro` is `None` only while actively running (taken
1115/// out across `Coroutine::resume`); `ctx` holds its volatile execution context
1116/// (frames/signal/error/exc) while suspended.
1117struct GenCell {
1118 coro: Option<corosensei::Coroutine<Value, Value, Result<Value, String>>>,
1119 /// Raw pointer to the coroutine body's `Yielder`, published on entry (same
1120 /// thread → valid for the body's life). Read by `yield` to suspend.
1121 yielder: *const (),
1122 ctx: GenContext,
1123 done: bool,
1124 /// True once the body has been resumed at least once (so it is suspended at a
1125 /// `yield`). `.return()`/`.throw()` only unwind a *started* generator.
1126 started: bool,
1127 /// A completion injected by `.return(v)` / `.throw(e)`: consumed by the next
1128 /// `yield` resume so the body unwinds (running any pending `finally`).
1129 inject: Option<GenInject>,
1130 /// True for an `async function*` body, where `await` AND `yield` share one
1131 /// coroutine yielder: `await` wraps its operand in an await marker so the
1132 /// driver can tell an internal suspension from a real yield.
1133 async_gen: bool,
1134 /// `[[AsyncGeneratorQueue]]` — pending requests as
1135 /// `(completion, step promise id)`. ECMA-262 27.6.3.6 keeps this queue so
1136 /// overlapping requests resume the body ONE AT A TIME and settle in request
1137 /// order; without it a second request issued before the first settles races
1138 /// past it and the results arrive swapped. `.next`, `.return` AND `.throw`
1139 /// all enqueue — a `.return()` that skipped the queue would terminate the
1140 /// body while an earlier `.next()` was still suspended on an `await`, and
1141 /// that `.next()` would then wrongly report `{done: true}`.
1142 queue: std::collections::VecDeque<(GenReq, u32)>,
1143 /// True while a queued request is being driven.
1144 running: bool,
1145 /// The [`stack_floor`] that applies while this generator's body is running.
1146 ///
1147 /// A corosensei coroutine executes on its OWN mmap'd stack, so the address
1148 /// range the thread's pthread record describes says nothing about how much
1149 /// room the body has left. Recorded from the coroutine's `Stack::limit()` at
1150 /// construction and swapped in around every resume; without it the guard
1151 /// compared a coroutine stack pointer against the main stack's floor and
1152 /// (depending on where mmap landed) either fired immediately or never.
1153 stack_floor: usize,
1154}
1155
1156/// A forced completion pushed into a suspended generator by `.return()`/`.throw()`.
1157enum GenInject {
1158 Return(Value),
1159 Throw(Value),
1160}
1161
1162/// One queued `[[AsyncGeneratorQueue]]` request. ECMA-262 27.6.3.6
1163/// `AsyncGeneratorEnqueue` records a *completion*, not just a sent value, which
1164/// is why `.return()` and `.throw()` queue behind pending `.next()` calls
1165/// instead of unwinding the body on the spot.
1166#[derive(Clone)]
1167pub enum GenReq {
1168 /// `.next(v)` — resume normally with `v`.
1169 Next(Value),
1170 /// `.return(v)` — resume with a forced return completion.
1171 Return(Value),
1172 /// `.throw(e)` — resume with a forced throw completion.
1173 Throw(Value),
1174}
1175
1176/// The mutable "execution registers" swapped at every generator resume/suspend
1177/// boundary so a suspended generator's half-finished frame/signal state never
1178/// leaks into the resuming caller. The heap, function/class tables and globals
1179/// are shared and never swapped.
1180#[derive(Default)]
1181struct GenContext {
1182 frames: Vec<Frame>,
1183 error: Option<String>,
1184 exc: Option<Value>,
1185 signal: Option<Signal>,
1186}
1187
1188thread_local! {
1189 /// Id of the generator whose body is currently executing, or `None` at the
1190 /// root. `yield` suspends this generator.
1191 static CUR_GEN: std::cell::Cell<Option<u32>> = const { std::cell::Cell::new(None) };
1192}
1193
1194thread_local! {
1195 static HOST: RefCell<JsHost> = RefCell::new(JsHost::new());
1196}
1197
1198/// Run `f` with mutable access to the thread-local host.
1199pub fn with_host<R>(f: impl FnOnce(&mut JsHost) -> R) -> R {
1200 HOST.with(|h| f(&mut h.borrow_mut()))
1201}
1202
1203/// Reset the host to a clean slate (fresh module frame).
1204pub fn reset_host() {
1205 with_host(|h| *h = JsHost::new());
1206 // Drop any cached module handles / factory closure — they index the old heap.
1207 crate::module::reset();
1208}
1209
1210impl Default for JsHost {
1211 fn default() -> Self {
1212 Self::new()
1213 }
1214}
1215
1216impl JsHost {
1217 pub fn new() -> JsHost {
1218 let global_env = new_env(None);
1219 let (io_tx, io_rx) = std::sync::mpsc::channel();
1220 let mut h = JsHost {
1221 tdz: None,
1222 tdz_globals: Default::default(),
1223 heap: Vec::new(),
1224 funcs: Vec::new(),
1225 scripts: Vec::new(),
1226 tries: Vec::new(),
1227 globals: VarMap::default(),
1228 global_consts: rustc_hash::FxHashSet::default(),
1229 frames: vec![Frame {
1230 env: global_env.clone(),
1231 base_env: global_env.clone(),
1232 this_obj: None,
1233 new_target: None,
1234 home_class: None,
1235 home_static: false,
1236 home_object: None,
1237 strict: false,
1238 line: 0,
1239 owner: None,
1240 is_module: true,
1241 this_state: ThisState::Plain,
1242 }],
1243 global_env,
1244 error: None,
1245 exc: None,
1246 signal: None,
1247 pending_rejections: Vec::new(),
1248 process_listeners: IndexMap::new(),
1249 null_val: Value::Undef,
1250 protos: HashMap::new(),
1251 null_proto_objs: HashSet::new(),
1252 fn_props: HashMap::new(),
1253 accessors: HashMap::new(),
1254 prop_attrs: HashMap::new(),
1255 non_extensible: HashSet::new(),
1256 private_methods: HashSet::new(),
1257 array_holes: HashMap::new(),
1258 super_replacement: None,
1259 derived_ctor_next: false,
1260 module_scope: false,
1261 builtin_statics: HashMap::new(),
1262 object_proto: Value::Undef,
1263 proto_class: HashMap::new(),
1264 class_registry: HashMap::new(),
1265 error_protos: HashMap::new(),
1266 template_objects: HashMap::new(),
1267 native_protos: HashMap::new(),
1268 symbol_registry: HashMap::new(),
1269 next_symbol: 1,
1270 symbols_by_id: HashMap::new(),
1271 well_known_ids: HashMap::new(),
1272 generators: Vec::new(),
1273 promises: Vec::new(),
1274 microtasks: std::collections::VecDeque::new(),
1275 draining_micro: false,
1276 nextticks: std::collections::VecDeque::new(),
1277 macrotasks: Vec::new(),
1278 next_timer: 1,
1279 io_tx,
1280 io_rx: Some(io_rx),
1281 open_handles: 0,
1282 capture: None,
1283 exit_code: None,
1284 exiting: false,
1285 global_obj: Value::Undef,
1286 };
1287 h.null_val = h.alloc(JsObj::Null);
1288 // `Object.prototype`: the chain root, its own `[[Prototype]]` is null.
1289 h.object_proto = h.new_object(IndexMap::new());
1290 h.global_obj = h.new_object(IndexMap::new());
1291 h
1292 }
1293
1294 /// Whether `v` IS the one `globalThis` object (not merely an object).
1295 pub fn is_global_object(&self, v: &Value) -> bool {
1296 !matches!(self.global_obj, Value::Undef) && self.global_obj == *v
1297 }
1298
1299 /// The `globalThis` object — one per host, so its identity and its
1300 /// properties both survive across reads.
1301 pub fn global_object(&mut self) -> Value {
1302 if matches!(self.global_obj, Value::Undef) {
1303 self.global_obj = self.new_object(IndexMap::new());
1304 }
1305 self.global_obj.clone()
1306 }
1307
1308 // ── prototype chain ──────────────────────────────────────────────────
1309 /// The `[[Prototype]]` of a heap value, if explicitly linked.
1310 pub fn proto_of(&self, v: &Value) -> Option<Value> {
1311 if let Value::Obj(i) = v {
1312 self.protos.get(i).cloned()
1313 } else {
1314 None
1315 }
1316 }
1317 /// Set `v`'s `[[Prototype]]` to `proto`. Null links the object as an explicit
1318 /// null-prototype object (recorded so `instanceof Object` reads false);
1319 /// undefined just clears any link without the null marker.
1320 pub fn set_proto(&mut self, v: &Value, proto: Value) {
1321 if let Value::Obj(i) = v {
1322 if self.is_null(&proto) {
1323 self.protos.remove(i);
1324 self.null_proto_objs.insert(*i);
1325 } else if matches!(proto, Value::Undef) {
1326 self.protos.remove(i);
1327 } else {
1328 self.protos.insert(*i, proto);
1329 self.null_proto_objs.remove(i);
1330 }
1331 }
1332 }
1333 /// Whether `v`'s `[[Prototype]]` was explicitly set to null.
1334 pub fn has_null_proto(&self, v: &Value) -> bool {
1335 matches!(v, Value::Obj(i) if self.null_proto_objs.contains(i))
1336 }
1337 /// Whether `util.inspect` renders `v` with the `[Object: null prototype]`
1338 /// tag. That is a question about the object's ACTUAL `[[Prototype]]`, which
1339 /// for `Object.prototype` is null even though nothing ever set it so: it is
1340 /// the chain root and was never passed through `set_proto`, so the
1341 /// explicitly-nulled registry does not hold it and `console.log(Object
1342 /// .prototype)` printed a bare `{}` where node prints the tag.
1343 ///
1344 /// Kept apart from [`Self::has_null_proto`], which nine other call sites ask
1345 /// about whether Object.prototype's own methods and `__proto__` accessor are
1346 /// INHERITED. `Object.prototype` inherits nothing and still owns all of them.
1347 pub fn inspects_null_proto(&self, v: &Value) -> bool {
1348 self.has_null_proto(v) || *v == self.object_proto
1349 }
1350 pub fn object_proto(&self) -> Value {
1351 self.object_proto.clone()
1352 }
1353 /// Record that the prototype object `proto` belongs to the class constructor
1354 /// `class_val` (so instances can recover their constructor).
1355 pub fn tag_proto_class(&mut self, proto: &Value, class_val: Value) {
1356 if let Value::Obj(i) = proto {
1357 self.proto_class.insert(*i, class_val);
1358 }
1359 }
1360 /// The class whose `prototype` object IS `v`, if `v` is one.
1361 pub fn class_owning_proto(&self, v: &Value) -> Option<Value> {
1362 match v {
1363 Value::Obj(i) => self.proto_class.get(i).cloned(),
1364 _ => None,
1365 }
1366 }
1367 /// The class constructor value nearest in `obj`'s prototype chain, if any.
1368 pub fn class_of(&self, obj: &Value) -> Option<Value> {
1369 let mut cur = self.proto_of(obj);
1370 while let Some(p) = cur {
1371 if let Value::Obj(i) = &p {
1372 if let Some(c) = self.proto_class.get(i) {
1373 return Some(c.clone());
1374 }
1375 }
1376 cur = self.proto_of(&p);
1377 }
1378 None
1379 }
1380 /// The constructor display name of `obj` for `util.inspect` (empty ⇒ plain
1381 /// object, no prefix).
1382 pub fn ctor_name(&self, obj: &Value) -> String {
1383 if let Some(c) = self.class_of(obj) {
1384 if let Some(JsObj::Class(cv)) = self.get(&c) {
1385 return cv.name.clone();
1386 }
1387 }
1388 // A `function F(){}` constructor is not a `class`, so it has no
1389 // `proto_class` entry. V8's `getConstructorName` walks the prototype
1390 // chain for an own `constructor` that is a named function — which is
1391 // what makes `console.log(new F())` print `F { y: 2 }`.
1392 let mut cur = self.proto_of(obj);
1393 while let Some(p) = cur {
1394 let ctor = match self.get(&p) {
1395 Some(JsObj::Object(props)) => props.get("constructor").cloned(),
1396 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => self.fn_prop(&p, "constructor"),
1397 _ => None,
1398 };
1399 if let Some(f) = ctor {
1400 let n = self.callable_name(&f);
1401 if !n.is_empty() {
1402 return n;
1403 }
1404 }
1405 cur = self.proto_of(&p);
1406 }
1407 String::new()
1408 }
1409
1410 /// Whether a callable owns a `prototype` property. `MakeConstructor`
1411 /// (10.2.5) runs for an ordinary function definition and for every
1412 /// generator; an arrow, a `MethodDefinition`, an async function and a bound
1413 /// function are not constructors and own none.
1414 pub fn owns_prototype(&self, v: &Value) -> bool {
1415 match self.get(v) {
1416 Some(JsObj::Class(_)) => true,
1417 Some(JsObj::Func(f)) => match self.funcs.get(f.def_id) {
1418 Some(d) => d.is_generator || !(d.is_arrow || d.is_async || d.is_method),
1419 None => false,
1420 },
1421 _ => false,
1422 }
1423 }
1424
1425 /// A function's own-property table (created on demand).
1426 pub fn fn_prop(&self, v: &Value, name: &str) -> Option<Value> {
1427 if let Value::Obj(i) = v {
1428 self.fn_props.get(i).and_then(|m| m.get(name).cloned())
1429 } else {
1430 None
1431 }
1432 }
1433
1434 /// A class static member, inherited down the constructor chain: a subclass
1435 /// sees its superclass's `static` methods/fields (`Sub.create` → `Base.create`).
1436 pub fn class_static(&self, class_val: &Value, name: &str) -> Option<Value> {
1437 let mut cur = class_val.clone();
1438 loop {
1439 if let Some(v) = self.fn_prop(&cur, name) {
1440 return Some(v);
1441 }
1442 match self.get(&cur) {
1443 Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1444 _ => return None,
1445 }
1446 }
1447 }
1448
1449 /// The first `extends` ancestor that is NOT a user class — the builtin
1450 /// constructor a class chain bottoms out in (`class D extends Array {}` →
1451 /// the `Array` builtin), or `None` for a chain of user classes only.
1452 ///
1453 /// `class_static` walks `ClassVal.parent` and gives up the moment the parent
1454 /// stops being a `Class`, so a static declared by the BUILTIN half of the
1455 /// chain was unreachable: `D.from` read `undefined` where node inherits
1456 /// `Array.from`. Returning the ancestor lets the caller finish the lookup
1457 /// with an ordinary property read, which is what reaches a builtin's
1458 /// statics.
1459 pub fn class_builtin_ancestor(&self, class_val: &Value) -> Option<Value> {
1460 let mut cur = class_val.clone();
1461 loop {
1462 match self.get(&cur) {
1463 Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1464 _ => return Some(cur),
1465 }
1466 }
1467 }
1468 pub fn set_fn_prop(&mut self, v: &Value, name: &str, val: Value) {
1469 if let Value::Obj(i) = v {
1470 self.fn_props
1471 .entry(*i)
1472 .or_default()
1473 .insert(name.to_string(), val);
1474 }
1475 // `name` and `prototype` are own properties of every function/class, but
1476 // never enumerable ones (SetFunctionName 10.2.9, MakeConstructor
1477 // 10.2.5), so `Object.keys(fn)` and `for (k in fn)` report only what a
1478 // script assigned. An ARRAY receiver reaching the same side table has no
1479 // such exotic keys — `arr.name = 'x'` is an ordinary enumerable property.
1480 if !matches!(self.kind_of(v), Some(ObjKind::Func) | Some(ObjKind::Class)) {
1481 return;
1482 }
1483 let attrs = match name {
1484 "name" => PropAttrs {
1485 writable: false,
1486 enumerable: false,
1487 configurable: true,
1488 },
1489 "prototype" => PropAttrs {
1490 writable: true,
1491 enumerable: false,
1492 configurable: false,
1493 },
1494 _ => return,
1495 };
1496 self.set_prop_attrs(v, name, attrs);
1497 }
1498 /// A user-assigned static on a builtin namespace (`Error.prepareStackTrace`).
1499 pub fn builtin_static(&self, ns: &str, name: &str) -> Option<Value> {
1500 self.builtin_statics
1501 .get(ns)
1502 .and_then(|m| m.get(name).cloned())
1503 }
1504 /// Assign a static on a builtin namespace (persists across fresh `Builtin`
1505 /// handles for the same namespace).
1506 pub fn set_builtin_static(&mut self, ns: &str, name: &str, val: Value) {
1507 self.builtin_statics
1508 .entry(ns.to_string())
1509 .or_default()
1510 .insert(name.to_string(), val);
1511 }
1512 /// `delete <ns>.<name>` for a script-assigned static. Reports whether the
1513 /// key was there — without this, `delete Array.prototype.patch` answered
1514 /// true and left the entry in place, so the patch outlived its own removal.
1515 pub fn remove_builtin_static(&mut self, ns: &str, name: &str) -> bool {
1516 self.builtin_statics
1517 .get_mut(ns)
1518 .is_some_and(|m| m.shift_remove(name).is_some())
1519 }
1520 /// Every namespace a script has assigned a static onto, with that
1521 /// namespace's assigned keys — the source of the user-added half of
1522 /// `Object.getOwnPropertyNames(Array.prototype)`.
1523 pub fn builtin_static_keys(&self, ns: &str) -> Vec<String> {
1524 self.builtin_statics
1525 .get(ns)
1526 .map(|m| m.keys().cloned().collect())
1527 .unwrap_or_default()
1528 }
1529 /// Drop an own property from the side table (`delete arr.foo`,
1530 /// `delete fn.tag`). Reports whether the key was there.
1531 pub fn remove_fn_prop(&mut self, v: &Value, name: &str) -> bool {
1532 match v {
1533 Value::Obj(i) => self
1534 .fn_props
1535 .get_mut(i)
1536 .map(|m| m.shift_remove(name).is_some())
1537 .unwrap_or(false),
1538 _ => false,
1539 }
1540 }
1541 pub fn fn_prop_keys(&self, v: &Value) -> Vec<String> {
1542 if let Value::Obj(i) = v {
1543 self.fn_props
1544 .get(i)
1545 .map(|m| m.keys().cloned().collect())
1546 .unwrap_or_default()
1547 } else {
1548 Vec::new()
1549 }
1550 }
1551
1552 /// Install an accessor `(get, set)` for `key` on the object `owner`.
1553 pub fn set_accessor(
1554 &mut self,
1555 owner: &Value,
1556 key: &str,
1557 get: Option<Value>,
1558 set: Option<Value>,
1559 ) {
1560 if let Value::Obj(i) = owner {
1561 // Accessors live in their own table, but JS reports own keys in a
1562 // single insertion order across data AND accessor properties. Drop an
1563 // ordering marker into the property map so
1564 // `{ a: 1, get b() {}, c: 3 }` enumerates a, b, c — not a, c, b.
1565 // The marker is `@@`-prefixed, so it is invisible to every reader.
1566 let marker = format!("{ORD_MARKER}{key}");
1567 match self.get_mut(owner) {
1568 Some(JsObj::Object(props)) => {
1569 if !props.contains_key(key) && !props.contains_key(&marker) {
1570 props.insert(marker, Value::Undef);
1571 }
1572 }
1573 // A function or class keeps its own properties in the fn-prop
1574 // side table, so its ordering marker belongs there. Without it a
1575 // static accessor enumerated AFTER every static field and method
1576 // regardless of where the class body declared it: node reports
1577 // `class A { static s = 2; static get sv(){} static m(){} }` as
1578 // `['sv', 'm', 's']` — the methods and accessors in source order
1579 // first, then the fields — and this reported `['m', 's', 'sv']`.
1580 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1581 let table = self.fn_props.entry(*i).or_default();
1582 if !table.contains_key(key) && !table.contains_key(&marker) {
1583 table.insert(marker, Value::Undef);
1584 }
1585 }
1586 _ => {}
1587 }
1588 let slot = self
1589 .accessors
1590 .entry(*i)
1591 .or_default()
1592 .entry(key.to_string())
1593 .or_insert((None, None));
1594 if get.is_some() {
1595 slot.0 = get;
1596 }
1597 if set.is_some() {
1598 slot.1 = set;
1599 }
1600 }
1601 }
1602 /// The accessor `(get, set)` for `key` directly on `owner` (no chain walk).
1603 /// Drop an own accessor property entirely, marker and all.
1604 ///
1605 /// `delete obj.accessorProp` used to clear only the property map, and an
1606 /// accessor does not live there — so the delete reported success while the
1607 /// getter kept answering and `in` kept reporting the key.
1608 pub fn remove_accessor(&mut self, owner: &Value, key: &str) {
1609 if let Value::Obj(i) = owner {
1610 if let Some(m) = self.accessors.get_mut(i) {
1611 m.shift_remove(key);
1612 }
1613 }
1614 let marker = format!("{ORD_MARKER}{key}");
1615 match self.get_mut(owner) {
1616 Some(JsObj::Object(props)) => {
1617 props.shift_remove(&marker);
1618 }
1619 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1620 if let Value::Obj(i) = owner {
1621 if let Some(t) = self.fn_props.get_mut(i) {
1622 t.shift_remove(&marker);
1623 }
1624 }
1625 }
1626 _ => {}
1627 }
1628 }
1629
1630 /// Turn an own accessor property into a data property carrying `value`,
1631 /// keeping its place in the own-key order.
1632 ///
1633 /// `set_accessor` records that order with an `@@ord:` marker in the
1634 /// property map rather than a real key, so deleting the accessor and
1635 /// inserting the value would append the key at the end instead. Node
1636 /// reports `{ a: 1, get b() {}, c: 3 }` redefined through
1637 /// `Object.defineProperty(o, 'b', { value })` as `a, b, c`.
1638 pub fn accessor_to_data(&mut self, owner: &Value, key: &str, value: Value) {
1639 if let Value::Obj(i) = owner {
1640 if let Some(m) = self.accessors.get_mut(i) {
1641 m.shift_remove(key);
1642 }
1643 }
1644 let marker = format!("{ORD_MARKER}{key}");
1645 let swap = |map: &mut IndexMap<String, Value>| match map.get_index_of(&marker) {
1646 Some(pos) => {
1647 *map = map
1648 .iter()
1649 .enumerate()
1650 .map(|(n, (k, v))| {
1651 if n == pos {
1652 (key.to_string(), value.clone())
1653 } else {
1654 (k.clone(), v.clone())
1655 }
1656 })
1657 .collect();
1658 }
1659 None => {
1660 map.insert(key.to_string(), value.clone());
1661 }
1662 };
1663 let fn_table = matches!(
1664 self.get(owner),
1665 Some(JsObj::Func(_)) | Some(JsObj::Class(_))
1666 );
1667 if fn_table {
1668 if let Value::Obj(i) = owner {
1669 swap(self.fn_props.entry(*i).or_default());
1670 }
1671 } else if let Some(JsObj::Object(props)) = self.get_mut(owner) {
1672 swap(props);
1673 }
1674 }
1675
1676 /// Move the per-heap-index bookkeeping of `src` onto `dst`.
1677 ///
1678 /// Used when one object becomes another in place (a class extending a
1679 /// builtin exotic). The prototype link is deliberately NOT moved: `dst`
1680 /// already points at the leaf class's prototype, which is the one its
1681 /// methods must resolve through.
1682 pub fn move_index_state(&mut self, src: u32, dst: u32) {
1683 if let Some(holes) = self.array_holes.remove(&src) {
1684 self.array_holes.insert(dst, holes);
1685 }
1686 if let Some(attrs) = self.prop_attrs.remove(&src) {
1687 self.prop_attrs.entry(dst).or_default().extend(attrs);
1688 }
1689 if let Some(props) = self.fn_props.remove(&src) {
1690 self.fn_props.entry(dst).or_default().extend(props);
1691 }
1692 if let Some(acc) = self.accessors.remove(&src) {
1693 self.accessors.entry(dst).or_default().extend(acc);
1694 }
1695 }
1696
1697 pub fn own_accessor(&self, owner: &Value, key: &str) -> Option<(Option<Value>, Option<Value>)> {
1698 if let Value::Obj(i) = owner {
1699 self.accessors.get(i).and_then(|m| m.get(key).cloned())
1700 } else {
1701 None
1702 }
1703 }
1704
1705 /// The own accessor-property keys of `owner`, in installation order.
1706 pub fn own_accessor_keys(&self, owner: &Value) -> Vec<String> {
1707 match owner {
1708 Value::Obj(i) => self
1709 .accessors
1710 .get(i)
1711 .map(|m| m.keys().cloned().collect())
1712 .unwrap_or_default(),
1713 _ => Vec::new(),
1714 }
1715 }
1716
1717 // ── own-property attributes ──────────────────────────────────────────
1718
1719 /// Record non-default attributes for `owner[key]`. Storing the default shape
1720 /// clears the entry so the table only ever holds deviations.
1721 pub fn set_prop_attrs(&mut self, owner: &Value, key: &str, attrs: PropAttrs) {
1722 if let Value::Obj(i) = owner {
1723 if attrs == PropAttrs::default() {
1724 if let Some(m) = self.prop_attrs.get_mut(i) {
1725 m.shift_remove(key);
1726 }
1727 } else {
1728 self.prop_attrs
1729 .entry(*i)
1730 .or_default()
1731 .insert(key.to_string(), attrs);
1732 }
1733 }
1734 }
1735
1736 /// Copy every recorded property attribute from `from` to `to`. A pass that
1737 /// rebuilds an object (`JSON.stringify`'s `toJSON` walk) must carry them
1738 /// across or the copy silently re-exposes non-enumerable slots.
1739 pub fn copy_prop_attrs(&mut self, from: &Value, to: &Value) {
1740 if let (Value::Obj(f), Value::Obj(_)) = (from, to) {
1741 if let Some(m) = self.prop_attrs.get(f).cloned() {
1742 for (k, a) in m {
1743 self.set_prop_attrs(to, &k, a);
1744 }
1745 }
1746 }
1747 }
1748
1749 /// The attributes of own property `owner[key]` (all-true when unrecorded).
1750 pub fn prop_attrs(&self, owner: &Value, key: &str) -> PropAttrs {
1751 // An array's `length` is the array exotic's own property (10.4.2):
1752 // never enumerated and never configurable, and writable until
1753 // `Object.freeze` clears that — which is what stops a `push` from
1754 // extending a frozen array. Reporting it unconditionally writable made
1755 // `Object.isFrozen(Object.freeze([]))` false once the elements started
1756 // being sealed, because `length` was then the one key that never
1757 // followed.
1758 if key == "length" && matches!(self.get(owner), Some(JsObj::Array(_))) {
1759 let writable = match owner {
1760 Value::Obj(i) => self
1761 .prop_attrs
1762 .get(i)
1763 .and_then(|m| m.get(key))
1764 .map(|a| a.writable)
1765 .unwrap_or(true),
1766 _ => true,
1767 };
1768 return PropAttrs {
1769 writable,
1770 enumerable: false,
1771 // An ARGUMENTS object's `length` is an ordinary data property
1772 // (10.4.4.6), so it is configurable where a real array's is
1773 // not. The two share a backing representation here, so the
1774 // exotic's attributes have to be told apart explicitly.
1775 configurable: crate::builtins::is_arguments_h(self, owner),
1776 };
1777 }
1778 match owner {
1779 Value::Obj(i) => self
1780 .prop_attrs
1781 .get(i)
1782 .and_then(|m| m.get(key))
1783 .copied()
1784 .unwrap_or_default(),
1785 _ => PropAttrs::default(),
1786 }
1787 }
1788
1789 /// Whether own property `owner[key]` shows up in `for-in`/`Object.keys`.
1790 /// Internal slots (`@@…`) and private class fields (`#…`) never do.
1791 pub fn is_enumerable(&self, owner: &Value, key: &str) -> bool {
1792 !key.starts_with("@@") && !key.starts_with('#') && self.prop_attrs(owner, key).enumerable
1793 }
1794
1795 /// Mark `owner[key]` non-enumerable, leaving it writable/configurable — the
1796 /// shape of every V8 "hidden but real" own property.
1797 pub fn hide_prop(&mut self, owner: &Value, key: &str) {
1798 self.set_prop_attrs(owner, key, PropAttrs::HIDDEN);
1799 }
1800
1801 /// Whether a plain `owner[key] = v` assignment is allowed to land. A
1802 /// non-writable data property silently ignores the write in sloppy mode,
1803 /// which is the mode every script here runs in; so does adding a *new* key to
1804 /// a non-extensible object.
1805 pub fn can_write_prop(&self, owner: &Value, key: &str) -> bool {
1806 if !self.prop_attrs(owner, key).writable {
1807 return false;
1808 }
1809 // An intrinsic prototype on the chain may define the name NON-WRITABLE,
1810 // and those members own no map entry for the walk below to find:
1811 // `o[Symbol.toStringTag] = 'x'` where `o` inherits from `Map.prototype`
1812 // is refused in node and was creating an own property here, which then
1813 // changed the object's brand.
1814 // The receiver's OWN kind counts too, not only the prototypes an
1815 // explicit link reaches: a plain array inherits `Array.prototype`
1816 // implicitly, with no link for the walk to follow, and
1817 // `a[Symbol.unscopables] = 'x'` is refused there just the same.
1818 //
1819 // Restricted to SYMBOL-keyed members. The string-keyed non-writable
1820 // ones — `Function.prototype.length`/`name`, `String.prototype.length`
1821 // — are also OWN properties of every instance, so the inherited rule
1822 // never decides them; applying it anyway blocked `SetFunctionName`
1823 // itself, and naming the setter in `Object.defineProperty(o, 'v', {set
1824 // (x) {…}})` then threw.
1825 if key.starts_with("@@")
1826 && crate::builtins::own_ctor_name(self, owner)
1827 .into_iter()
1828 .chain(crate::builtins::chain_intrinsic_ctors_h(self, owner))
1829 .any(|c| crate::builtins::is_proto_readonly(c, key))
1830 {
1831 return false;
1832 }
1833 // 10.1.9.2: with no OWN property, the inherited one decides. A
1834 // non-writable data property up the chain blocks the write rather than
1835 // being shadowed — including one on a frozen prototype. Only own
1836 // attributes were consulted, so `Object.create(frozenBase).f = 2`
1837 // quietly created an own property node refuses to create.
1838 //
1839 // An inherited ACCESSOR does not block: its setter runs, and the write
1840 // path checks for one before reaching here.
1841 let has_own = match self.get(owner) {
1842 Some(JsObj::Object(p)) => p.contains_key(key),
1843 _ => true,
1844 };
1845 if !has_own {
1846 let mut cur = self.proto_of(owner);
1847 while let Some(proto) = cur {
1848 if self.own_accessor(&proto, key).is_some() {
1849 break;
1850 }
1851 let present =
1852 matches!(self.get(&proto), Some(JsObj::Object(p)) if p.contains_key(key));
1853 if present {
1854 if !self.prop_attrs(&proto, key).writable {
1855 return false;
1856 }
1857 break;
1858 }
1859 cur = self.proto_of(&proto);
1860 }
1861 }
1862 if self.is_extensible(owner) {
1863 return true;
1864 }
1865 // A non-extensible object refuses a NEW key. Only the plain-object arm
1866 // could name its own keys, so every other shape answered "own" for any
1867 // key at all: `Object.freeze(arr).extra = 1` landed, and so did a write
1868 // to the frozen template object a tagged template hands its tag.
1869 match self.get(owner) {
1870 Some(JsObj::Object(p)) => p.contains_key(key),
1871 Some(JsObj::Array(items)) => {
1872 key == "length"
1873 || key
1874 .parse::<usize>()
1875 .map(|i| i < items.len())
1876 .unwrap_or(false)
1877 || self.fn_prop(owner, key).is_some()
1878 }
1879 // A RegExp's `lastIndex` is an own property, so a merely
1880 // NON-EXTENSIBLE regexp still accepts a write to it.
1881 Some(JsObj::RegExp(_)) => key == "lastIndex" || self.fn_prop(owner, key).is_some(),
1882 // Every other shape keeps its own properties in the fn-prop side
1883 // table (a function's statics, a Map's assigned properties), so
1884 // "does it already own this key" is that table's question. Answering
1885 // a blanket `true` let a NEW key land on a frozen function and a
1886 // frozen Map.
1887 _ => self.fn_prop(owner, key).is_some(),
1888 }
1889 }
1890
1891 /// Mark `v` closed to new properties (`Object.preventExtensions`).
1892 pub fn prevent_extensions(&mut self, v: &Value) {
1893 if let Value::Obj(i) = v {
1894 self.non_extensible.insert(*i);
1895 }
1896 }
1897
1898 pub fn is_extensible(&self, v: &Value) -> bool {
1899 !matches!(v, Value::Obj(i) if self.non_extensible.contains(i))
1900 }
1901
1902 /// Apply `Object.seal` (`freeze == false`) or `Object.freeze` (`true`): close
1903 /// the object and strip `configurable` — and, when freezing, `writable` —
1904 /// from every own property, data and accessor alike.
1905 pub fn seal_object(&mut self, v: &Value, freeze: bool) {
1906 self.prevent_extensions(v);
1907 let mut keys = self.integrity_keys(v);
1908 keys.extend(self.own_accessor_keys(v));
1909 for k in keys {
1910 let mut a = self.prop_attrs(v, &k);
1911 a.configurable = false;
1912 if freeze {
1913 a.writable = false;
1914 }
1915 self.set_prop_attrs(v, &k, a);
1916 }
1917 }
1918
1919 /// The own DATA-property keys SetIntegrityLevel (7.3.15) walks.
1920 ///
1921 /// An array's elements are own properties too, and only the `Object` arm was
1922 /// walked — so `Object.freeze([1, 2])` sealed nothing: `a[0] = 9` wrote
1923 /// through, and the elements still reported `writable: true,
1924 /// configurable: true` while `Object.isFrozen` answered true over an empty
1925 /// key list. `length` is an own property as well, and freezing it is what
1926 /// stops a `push` from extending a frozen array.
1927 fn integrity_keys(&self, v: &Value) -> Vec<String> {
1928 let side_table_keys = |v: &Value| -> Vec<String> {
1929 match v {
1930 Value::Obj(i) => self
1931 .fn_props
1932 .get(i)
1933 .map(|m| m.keys().cloned().collect())
1934 .unwrap_or_default(),
1935 _ => Vec::new(),
1936 }
1937 };
1938 match self.get(v) {
1939 Some(JsObj::Object(p)) => p.keys().cloned().collect(),
1940 // A RegExp's only own property is its `lastIndex` cursor, which
1941 // lives in the `RegExpObj` struct. Without it here `Object.freeze`
1942 // sealed nothing and a frozen regexp's cursor still moved.
1943 Some(JsObj::RegExp(_)) => vec!["lastIndex".to_string()],
1944 // A function's statics and a Map's assigned properties live in the
1945 // fn-prop side table, and freezing has to reach them too.
1946 Some(JsObj::Func(_))
1947 | Some(JsObj::Class(_))
1948 | Some(JsObj::Map { .. })
1949 | Some(JsObj::Set { .. })
1950 | Some(JsObj::Promise { .. }) => side_table_keys(v),
1951 Some(JsObj::Array(items)) => (0..items.len())
1952 .map(|i| i.to_string())
1953 .chain(std::iter::once("length".to_string()))
1954 // A named property stuck on an array (`a.tag = 't'`, a match
1955 // array's `.index`/`.groups`) is an own property too, and
1956 // freezing has to reach it.
1957 .chain(match v {
1958 Value::Obj(i) => self
1959 .fn_props
1960 .get(i)
1961 .map(|m| m.keys().cloned().collect::<Vec<_>>())
1962 .unwrap_or_default(),
1963 _ => Vec::new(),
1964 })
1965 .collect(),
1966 _ => Vec::new(),
1967 }
1968 }
1969
1970 /// `Object.isSealed` (`freeze == false`) / `Object.isFrozen` (`true`).
1971 pub fn is_sealed(&self, v: &Value, freeze: bool) -> bool {
1972 if self.is_extensible(v) {
1973 return false;
1974 }
1975 let mut keys = self.integrity_keys(v);
1976 keys.extend(self.own_accessor_keys(v));
1977 keys.iter().all(|k| {
1978 let a = self.prop_attrs(v, k);
1979 !a.configurable && (!freeze || !a.writable)
1980 })
1981 }
1982
1983 /// A fresh unique `Symbol(desc)` value.
1984 pub fn new_symbol(&mut self, desc: Option<String>) -> Value {
1985 let id = self.next_symbol;
1986 self.next_symbol += 1;
1987 let v = self.alloc(JsObj::Symbol { desc, id });
1988 self.symbols_by_id.insert(id, v.clone());
1989 v
1990 }
1991
1992 /// The symbol VALUE an internal symbol property key (`@@sym:<id>` or a
1993 /// well-known `@@iterator`) came from.
1994 pub fn symbol_of_key(&self, k: &str) -> Option<Value> {
1995 if let Some(id) = k.strip_prefix("@@sym:").and_then(|i| i.parse::<u64>().ok()) {
1996 return self.symbols_by_id.get(&id).cloned();
1997 }
1998 let name = k.strip_prefix("@@")?;
1999 WELL_KNOWN_SYMBOLS
2000 .contains(&name)
2001 .then(|| {
2002 self.symbol_registry
2003 .get(&format!("@@Symbol.{name}"))
2004 .cloned()
2005 })
2006 .flatten()
2007 }
2008
2009 /// The own symbol-keyed property keys of `v` as SYMBOL values —
2010 /// `Object.getOwnPropertySymbols` / the symbol half of `Reflect.ownKeys`.
2011 pub fn own_symbol_keys(&self, v: &Value) -> Vec<Value> {
2012 let keys: Vec<String> = match self.get(v) {
2013 Some(JsObj::Object(p)) => p.keys().cloned().collect(),
2014 // An Array/Function receiver has no property map: its non-index own
2015 // properties — symbol-keyed ones included — live in the fn-prop side
2016 // table, and are just as much own properties as an object's.
2017 Some(_) => self.fn_prop_keys(v),
2018 None => return Vec::new(),
2019 };
2020 keys.iter().filter_map(|k| self.symbol_of_key(k)).collect()
2021 }
2022
2023 /// The own SYMBOL-keyed enumerable `(internal key, value)` pairs of `v` —
2024 /// what `CopyDataProperties` (object spread, `Object.assign`) copies
2025 /// alongside the string keys, and what `Object.keys` / `for-in` /
2026 /// `JSON.stringify` deliberately skip.
2027 pub fn own_symbol_entries(&self, v: &Value) -> Vec<(String, Value)> {
2028 match self.get(v) {
2029 Some(JsObj::Object(p)) => p
2030 .iter()
2031 .filter(|(k, _)| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2032 .map(|(k, val)| (k.clone(), val.clone()))
2033 .collect(),
2034 // Array/Function: the side table (see `own_symbol_keys`).
2035 Some(_) => self
2036 .fn_prop_keys(v)
2037 .into_iter()
2038 .filter(|k| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2039 .map(|k| {
2040 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
2041 (k, val)
2042 })
2043 .collect(),
2044 None => Vec::new(),
2045 }
2046 }
2047 /// The shared `Symbol.for(key)` value (interned by description).
2048 pub fn symbol_for(&mut self, key: &str) -> Value {
2049 if let Some(v) = self.symbol_registry.get(key) {
2050 return v.clone();
2051 }
2052 let s = self.new_symbol(Some(key.to_string()));
2053 self.symbol_registry.insert(key.to_string(), s.clone());
2054 s
2055 }
2056 /// `Symbol.keyFor(sym)`: the registry key `Symbol.for` interned `sym` under,
2057 /// or `undefined` for a symbol that is not in the registry at all.
2058 ///
2059 /// Matched by symbol IDENTITY, not by description — `Symbol.for('k')` and
2060 /// `Symbol('k')` share a description and only the first is registered. The
2061 /// `@@Symbol.*` well-known entries are registry-internal and never a
2062 /// `keyFor` answer, matching node: `Symbol.keyFor(Symbol.iterator)` is
2063 /// `undefined` there.
2064 pub fn symbol_registry_key(&mut self, sym: &Value) -> Value {
2065 let Some(key) = self
2066 .symbol_registry
2067 .iter()
2068 .find(|(k, v)| self.strict_eq(v, sym) && !k.starts_with("@@Symbol."))
2069 .map(|(k, _)| k.clone())
2070 else {
2071 return Value::Undef;
2072 };
2073 self.new_str(key)
2074 }
2075 /// The well-known `Symbol.iterator` (a fixed shared symbol whose internal
2076 /// property key is `@@iterator`).
2077 pub fn well_known_iterator(&mut self) -> Value {
2078 self.symbol_for("@@Symbol.iterator")
2079 }
2080 /// The well-known `Symbol.asyncIterator` (internal key `@@asyncIterator`).
2081 pub fn well_known_async_iterator(&mut self) -> Value {
2082 self.symbol_for("@@Symbol.asyncIterator")
2083 }
2084 /// A well-known symbol by its ECMAScript name (`toPrimitive`,
2085 /// `toStringTag`, …). Its internal property key is `@@<name>` — see
2086 /// [`WELL_KNOWN_SYMBOLS`] and `property_key`.
2087 ///
2088 /// Its DESCRIPTION is `Symbol.<name>`, so `String(Symbol.iterator)` prints
2089 /// `Symbol(Symbol.iterator)` as V8 does, while the registry key keeps the
2090 /// `@@` prefix — `Symbol.for('Symbol.iterator')` therefore stays a
2091 /// different symbol, and identification is by id, so a user-made
2092 /// `Symbol('Symbol.iterator')` is not mistaken for the well-known one.
2093 pub fn well_known_symbol(&mut self, name: &str) -> Value {
2094 let key = format!("@@Symbol.{name}");
2095 if let Some(v) = self.symbol_registry.get(&key) {
2096 return v.clone();
2097 }
2098 let s = self.new_symbol(Some(format!("Symbol.{name}")));
2099 if let Some(JsObj::Symbol { id, .. }) = self.get(&s) {
2100 self.well_known_ids.insert(*id, name.to_string());
2101 }
2102 self.symbol_registry.insert(key, s.clone());
2103 s
2104 }
2105 /// The internal property-key string for a value used as a key. A `Symbol`
2106 /// maps to a stable per-symbol string so symbol-keyed props round-trip;
2107 /// `Symbol.iterator` maps to the sentinel `@@iterator`.
2108 pub fn property_key(&self, v: &Value) -> String {
2109 if let Some(JsObj::Symbol { id, .. }) = self.get(v) {
2110 if let Some(n) = self.well_known_ids.get(id) {
2111 return format!("@@{n}");
2112 }
2113 return format!("@@sym:{id}");
2114 }
2115 self.str_of(v)
2116 }
2117
2118 pub fn null(&self) -> Value {
2119 self.null_val.clone()
2120 }
2121 pub fn is_null(&self, v: &Value) -> bool {
2122 matches!(self.get(v), Some(JsObj::Null))
2123 }
2124
2125 // ── program loading ──────────────────────────────────────────────────
2126 pub fn program_offsets(&self) -> (usize, usize) {
2127 (self.funcs.len(), self.tries.len())
2128 }
2129 pub fn load_program(&mut self, funcs: Vec<FuncDef>, tries: Vec<TryDef>) {
2130 self.funcs.extend(funcs);
2131 self.tries.extend(tries);
2132 }
2133 /// The source text of function `def_id`, when its program kept one.
2134 pub fn func_source(&self, def_id: usize) -> Option<&str> {
2135 let d = self.funcs.get(def_id)?;
2136 let (start, end) = d.span;
2137 if end == 0 {
2138 return None;
2139 }
2140 self.scripts
2141 .get(d.script? as usize)?
2142 .get(start as usize..end as usize)
2143 }
2144 pub fn try_def(&self, id: usize) -> Option<TryDef> {
2145 self.tries.get(id).cloned()
2146 }
2147
2148 /// What `try` statement `id` HAS — `(has handler, catch parameter name, has
2149 /// finalizer)` — without copying its chunks. Running a `try` used to clone
2150 /// the whole `TryDef`, so a `try` inside a loop deep-copied its block, its
2151 /// handler and its finalizer on every iteration just to learn its shape.
2152 pub fn try_shape(&self, id: usize) -> Option<(bool, Option<String>, bool)> {
2153 let t = self.tries.get(id)?;
2154 Some((
2155 t.handler.is_some(),
2156 t.handler.as_ref().and_then(|(bind, _)| bind.clone()),
2157 t.finalizer.is_some(),
2158 ))
2159 }
2160
2161 /// One `try` part's bytecode: 0 = block, 1 = handler body, 2 = finalizer.
2162 /// Reached only when no pooled VM already holds that chunk.
2163 pub fn try_chunk(&self, id: usize, part: u64) -> Option<Chunk> {
2164 let t = self.tries.get(id)?;
2165 match part {
2166 0 => Some(t.block.clone()),
2167 1 => t.handler.as_ref().map(|(_, body)| body.clone()),
2168 _ => t.finalizer.clone(),
2169 }
2170 }
2171
2172 // ── heap allocation / accessors ──────────────────────────────────────
2173 pub fn alloc(&mut self, obj: JsObj) -> Value {
2174 self.heap.push(obj);
2175 Value::Obj((self.heap.len() - 1) as u32)
2176 }
2177 pub fn get(&self, v: &Value) -> Option<&JsObj> {
2178 if let Value::Obj(i) = v {
2179 self.heap.get(*i as usize)
2180 } else {
2181 None
2182 }
2183 }
2184 pub fn get_mut(&mut self, v: &Value) -> Option<&mut JsObj> {
2185 if let Value::Obj(i) = v {
2186 self.heap.get_mut(*i as usize)
2187 } else {
2188 None
2189 }
2190 }
2191 /// Which variant `v` points at, without copying its contents. Use this in
2192 /// place of `get(v).cloned()` whenever only the tag is needed — see
2193 /// [`ObjKind`].
2194 pub fn kind_of(&self, v: &Value) -> Option<ObjKind> {
2195 self.get(v).map(JsObj::kind)
2196 }
2197 pub fn new_str(&mut self, s: impl Into<String>) -> Value {
2198 self.alloc(JsObj::Str(s.into()))
2199 }
2200 pub fn new_array(&mut self, items: Vec<Value>) -> Value {
2201 self.alloc(JsObj::Array(items))
2202 }
2203
2204 /// Record that `name` was declared as a private method or accessor.
2205 pub fn note_private_method(&mut self, name: &str) {
2206 self.private_methods.insert(name.to_string());
2207 }
2208
2209 /// Whether `name` was declared as a private method/accessor by some class,
2210 /// as opposed to a private field.
2211 pub fn is_private_method(&self, name: &str) -> bool {
2212 self.private_methods.contains(name)
2213 }
2214
2215 /// The name of the class whose body the running function belongs to. Only a
2216 /// method of that class can even mention its private names, so this is the
2217 /// class a failed brand check must name.
2218 /// The `super` binding of the frame now running: the owning class name,
2219 /// whether the method is static, and the home object of an object-literal
2220 /// method. An ARROW captures all three at creation, the way it captures
2221 /// `this` — `super` inside an arrow means the enclosing METHOD's `super`.
2222 /// Whether the activation now running is strict code.
2223 /// Whether `v` is a function whose own body is SLOPPY — not an arrow, and
2224 /// with no `'use strict'` of its own or inherited from its script. This is
2225 /// the receiver test the `arguments`/`caller` poison pill keys on: node
2226 /// decides by the FUNCTION, never by the code doing the reading.
2227 pub fn fn_is_sloppy(&self, v: &Value) -> bool {
2228 match self.get(v) {
2229 Some(JsObj::Func(fv)) => {
2230 !fv.is_arrow && !self.funcs.get(fv.def_id).is_some_and(|d| d.strict)
2231 }
2232 _ => false,
2233 }
2234 }
2235 pub fn current_strict(&self) -> bool {
2236 self.frame().strict
2237 }
2238
2239 /// Mark the frame about to run as STRICT — used for a program whose own top
2240 /// level says `'use strict'`, which has no `FuncDef` to carry the flag.
2241 pub fn set_current_strict(&mut self) {
2242 if let Some(f) = self.frames.last_mut() {
2243 f.strict = true;
2244 }
2245 }
2246
2247 pub fn current_home(&self) -> (Option<String>, bool, Option<Value>) {
2248 (
2249 self.current_home_class_name(),
2250 self.frame().home_static,
2251 self.frame().home_object.clone(),
2252 )
2253 }
2254
2255 pub fn current_home_class_name(&self) -> Option<String> {
2256 match self.get(&self.current_home_class()?) {
2257 Some(JsObj::Class(c)) => Some(c.name.clone()),
2258 _ => None,
2259 }
2260 }
2261
2262 /// Whether `recv` — or anything on its prototype chain — carries the private
2263 /// name `key`. A private FIELD is an own property of the instance; a private
2264 /// METHOD lives on the class prototype, one link up.
2265 pub fn has_private(&self, recv: &Value, key: &str) -> bool {
2266 let mut cur = Some(recv.clone());
2267 while let Some(v) = cur {
2268 let owns = match self.get(&v) {
2269 Some(JsObj::Object(p)) => p.contains_key(key),
2270 Some(JsObj::Class(c)) => c.statics.contains_key(key),
2271 _ => false,
2272 };
2273 if owns || self.own_accessor(&v, key).is_some() || self.fn_prop(&v, key).is_some() {
2274 return true;
2275 }
2276 cur = self.proto_of(&v);
2277 }
2278 false
2279 }
2280
2281 // ── array holes ──────────────────────────────────────────────────────
2282 //
2283 // Every read/write of an array's elision set goes through this block. See
2284 // the `array_holes` field for why the marker lives here rather than in
2285 // `Value`.
2286
2287 /// Whether element `i` of array `arr` is an elided element (a "hole"), as
2288 /// opposed to a stored `undefined`. `false` for anything that is not an
2289 /// array, and for every index of a dense one.
2290 pub fn is_hole(&self, arr: &Value, i: usize) -> bool {
2291 match (arr, ()) {
2292 (Value::Obj(idx), ()) => self.array_holes.get(idx).is_some_and(|hs| hs.contains(&i)),
2293 _ => false,
2294 }
2295 }
2296
2297 /// Whether `arr` has any elided element at all — one hash probe, and the
2298 /// guard every hole-aware code path takes before doing anything slower.
2299 pub fn has_holes(&self, arr: &Value) -> bool {
2300 matches!(arr, Value::Obj(i) if self.array_holes.contains_key(i))
2301 }
2302
2303 /// `arr`'s hole positions in ASCENDING order, or an empty vec if dense.
2304 /// Sorted because every consumer (own-key enumeration, `util.inspect`
2305 /// run-grouping) needs index order, and the backing set has none.
2306 pub fn hole_indices(&self, arr: &Value) -> Vec<usize> {
2307 let Value::Obj(i) = arr else {
2308 return Vec::new();
2309 };
2310 let Some(hs) = self.array_holes.get(i) else {
2311 return Vec::new();
2312 };
2313 let mut v: Vec<usize> = hs.iter().copied().collect();
2314 v.sort_unstable();
2315 v
2316 }
2317
2318 /// Record element `i` of `arr` as elided.
2319 pub fn mark_hole(&mut self, arr: &Value, i: usize) {
2320 if let Value::Obj(idx) = arr {
2321 self.array_holes.entry(*idx).or_default().insert(i);
2322 }
2323 }
2324
2325 /// Record `range` of `arr` as elided (a `new Array(n)`, a `length` grow, or
2326 /// the gap a write past the end opens).
2327 pub fn mark_hole_range(&mut self, arr: &Value, range: std::ops::Range<usize>) {
2328 if range.is_empty() {
2329 return;
2330 }
2331 if let Value::Obj(idx) = arr {
2332 self.array_holes.entry(*idx).or_default().extend(range);
2333 }
2334 }
2335
2336 /// Element `i` now holds a real value: it is no longer a hole. Every write
2337 /// to an array index calls this, which is what keeps a stale hole record
2338 /// from outliving the elision it described.
2339 pub fn clear_hole(&mut self, arr: &Value, i: usize) {
2340 let Value::Obj(idx) = arr else { return };
2341 let Some(hs) = self.array_holes.get_mut(idx) else {
2342 return;
2343 };
2344 hs.remove(&i);
2345 if hs.is_empty() {
2346 self.array_holes.remove(idx);
2347 }
2348 }
2349
2350 /// `arr` is dense from here on (`fill` over the whole array, a fresh
2351 /// dense assignment into an existing handle).
2352 pub fn clear_holes(&mut self, arr: &Value) {
2353 if let Value::Obj(idx) = arr {
2354 self.array_holes.remove(idx);
2355 }
2356 }
2357
2358 /// Copy `src`'s elision set onto `dst`, optionally shifting each position by
2359 /// `f`. Used by every method that derives a new array whose holes track the
2360 /// source's (`slice`, `concat`, `map`).
2361 pub fn copy_holes(&mut self, src: &Value, dst: &Value, f: impl Fn(usize) -> Option<usize>) {
2362 if !self.has_holes(src) {
2363 return;
2364 }
2365 let moved: rustc_hash::FxHashSet<usize> =
2366 self.hole_indices(src).into_iter().filter_map(f).collect();
2367 self.install_holes(dst, moved);
2368 }
2369
2370 /// Rewrite `arr`'s own elision set in place: `f(i)` gives the position each
2371 /// existing hole moves to, or `None` if the mutation removed it. This is the
2372 /// one primitive behind every structural array mutation — `shift` is
2373 /// `i.checked_sub(1)`, `unshift(k)` is `i + k`, `reverse` is `len-1-i`, and
2374 /// `splice` is the general case.
2375 pub fn remap_holes(&mut self, arr: &Value, f: impl Fn(usize) -> Option<usize>) {
2376 if !self.has_holes(arr) {
2377 return;
2378 }
2379 let moved: rustc_hash::FxHashSet<usize> =
2380 self.hole_indices(arr).into_iter().filter_map(f).collect();
2381 self.install_holes(arr, moved);
2382 }
2383
2384 /// Replace `arr`'s elision set outright, dropping the record entirely when
2385 /// the new set is empty so `has_holes` stays a single negative probe for the
2386 /// dense case.
2387 pub fn install_holes(&mut self, arr: &Value, holes: rustc_hash::FxHashSet<usize>) {
2388 let Value::Obj(idx) = arr else { return };
2389 if holes.is_empty() {
2390 self.array_holes.remove(idx);
2391 } else {
2392 self.array_holes.insert(*idx, holes);
2393 }
2394 }
2395
2396 /// Forget any hole at or past `len` — what a `pop`, a `length` shrink or a
2397 /// truncating `splice` leaves behind.
2398 pub fn truncate_holes(&mut self, arr: &Value, len: usize) {
2399 self.remap_holes(arr, |i| (i < len).then_some(i));
2400 }
2401
2402 /// `util.inspect`'s `formatSpecialArray`: the element strings of a SPARSE
2403 /// array, where each maximal run of elided positions collapses to a single
2404 /// `<N empty items>` entry. Returns the entries and whether the last of them
2405 /// is the `... N more items` tail (which the grid layout must not size a
2406 /// column to).
2407 ///
2408 /// The `maxArrayLength` cap counts ENTRIES, not indices, so a run costs one
2409 /// slot however long it is — matching node, where `[ ...Array(200) ]`-style
2410 /// sparse arrays print a single `<200 empty items>`.
2411 fn inspect_sparse(
2412 &self,
2413 v: &Value,
2414 items: &[Value],
2415 indent: usize,
2416 st: &mut InspectCycles,
2417 ) -> (Vec<String>, bool) {
2418 let holes: rustc_hash::FxHashSet<usize> = self.hole_indices(v).into_iter().collect();
2419 let empties = |n: usize| {
2420 let unit = if n == 1 { "item" } else { "items" };
2421 format!("<{n} empty {unit}>")
2422 };
2423 let mut out: Vec<String> = Vec::new();
2424 // The first index not yet accounted for by an entry.
2425 let mut index = 0usize;
2426 for (i, it) in items.iter().enumerate() {
2427 if out.len() >= inspect_max_array_length() {
2428 break;
2429 }
2430 if holes.contains(&i) {
2431 continue;
2432 }
2433 if i > index {
2434 out.push(empties(i - index));
2435 index = i;
2436 if out.len() >= inspect_max_array_length() {
2437 break;
2438 }
2439 }
2440 out.push(self.inspect_lvl(it, indent + 2, st));
2441 index = i + 1;
2442 }
2443 let remaining = items.len() - index;
2444 if remaining == 0 {
2445 return (out, false);
2446 }
2447 if out.len() < inspect_max_array_length() {
2448 // Trailing holes are still `<N empty items>`, not a truncation.
2449 out.push(empties(remaining));
2450 (out, false)
2451 } else {
2452 let unit = if remaining == 1 { "item" } else { "items" };
2453 out.push(format!("... {remaining} more {unit}"));
2454 (out, true)
2455 }
2456 }
2457 pub fn new_object(&mut self, mut props: IndexMap<String, Value>) -> Value {
2458 // Integer-index keys enumerate ascending-first regardless of the order
2459 // they were supplied in (object literal, spread, Object.assign result).
2460 canonicalize_own_keys(&mut props);
2461 // A map carrying the hidden `@@native` tag IS an instance of that native
2462 // class, so it hangs off the class prototype rather than
2463 // `Object.prototype`. Eleven classes — `Hash`, `Cipheriv`,
2464 // `StringDecoder`, `Script`, `URLSearchParams`, `Console`,
2465 // `AbortController` among them — built plain objects instead, so
2466 // `x.constructor.name` read `"Object"` and a chain walk found none of
2467 // the class's methods. Linking HERE means a construction site cannot
2468 // forget it; the tag is already in the map at every one of them.
2469 let tag = props.get("@@native").and_then(|v| self.as_str(v));
2470 let obj = self.alloc(JsObj::Object(props));
2471 if let Some(proto) = tag.and_then(|t| self.ensure_ctor_proto(&t)) {
2472 self.set_proto(&obj, proto);
2473 }
2474 obj
2475 }
2476 pub fn as_str(&self, v: &Value) -> Option<String> {
2477 match v {
2478 Value::Str(s) => Some((**s).clone()),
2479 Value::Obj(_) => match self.get(v) {
2480 Some(JsObj::Str(s)) => Some(s.clone()),
2481 _ => None,
2482 },
2483 _ => None,
2484 }
2485 }
2486
2487 // ── scope / names ────────────────────────────────────────────────────
2488 fn frame(&self) -> &Frame {
2489 self.frames.last().unwrap()
2490 }
2491 fn cur_env(&self) -> Env {
2492 self.frame().env.clone()
2493 }
2494
2495 // ── DAP debug introspection (used only under `--dap`) ────────────────────
2496 /// Number of active call frames (the debugger's step-depth reference).
2497 pub fn frame_depth(&self) -> usize {
2498 self.frames.len()
2499 }
2500 /// Record the source line the innermost frame is executing (DAP line hook).
2501 pub fn set_cur_line(&mut self, line: u32) {
2502 if let Some(f) = self.frames.last_mut() {
2503 f.line = line;
2504 }
2505 }
2506 /// The `.stack` tail for an error created right now: one ` at <name>`
2507 /// line per live frame, innermost first, ending at the module frame.
2508 ///
2509 /// These are the REAL user frames — node-js has no `file:line:column` (the
2510 /// per-frame line is only tracked under `--dap`) and no Node-internal
2511 /// module-loader frames, so `.stack` names the call chain but can never be
2512 /// byte-identical to V8's. The names are what makes a thrown error
2513 /// diagnosable; the missing positions are documented in BUGS.md.
2514 /// V8's `Error.stackTraceLimit` — how many frames a captured stack keeps.
2515 ///
2516 /// The default is 10, it is settable, and setting it to 0 is the documented
2517 /// way to make error construction cheap. It did not exist, so the read was
2518 /// `undefined` and every stack carried every frame regardless.
2519 pub fn stack_trace_limit(&self) -> usize {
2520 match self.builtin_static("Error", "stackTraceLimit") {
2521 Some(v) => {
2522 let n = self.to_number(&v);
2523 if n.is_finite() && n > 0.0 {
2524 n as usize
2525 } else if n.is_nan() || n <= 0.0 {
2526 0
2527 } else {
2528 usize::MAX
2529 }
2530 }
2531 None => 10,
2532 }
2533 }
2534
2535 pub fn stack_frames(&self) -> String {
2536 let limit = self.stack_trace_limit();
2537 if limit == 0 {
2538 return String::new();
2539 }
2540 let mut out = String::new();
2541 for (i, f) in self.frames.iter().enumerate().rev().take(limit) {
2542 let name = match (&f.owner, i) {
2543 (Some(n), _) => n.clone(),
2544 (None, 0) => "Object.<anonymous>".to_string(),
2545 (None, _) => "<anonymous>".to_string(),
2546 };
2547 out.push_str("\n at ");
2548 out.push_str(&name);
2549 }
2550 if out.is_empty() && limit > 0 {
2551 out.push_str("\n at <anonymous>");
2552 }
2553 out
2554 }
2555
2556 /// The call stack as (frame name, line) pairs, innermost first — for the DAP
2557 /// `stackTrace`. `owner` carries the function name where known.
2558 pub fn dbg_stack(&self) -> Vec<(String, u32)> {
2559 self.frames
2560 .iter()
2561 .rev()
2562 .map(|f| {
2563 let name = f.owner.clone().unwrap_or_else(|| "<module>".to_string());
2564 (name, f.line)
2565 })
2566 .collect()
2567 }
2568 /// The innermost frame's locals as (name, inspect) pairs — for DAP `variables`.
2569 pub fn dbg_locals(&self) -> Vec<(String, String)> {
2570 let env = self.cur_env();
2571 let names: Vec<String> = env.borrow().vars.keys().cloned().collect();
2572 names
2573 .into_iter()
2574 .map(|n| {
2575 let v = self.read_name(&n).unwrap_or(Value::Undef);
2576 (n, self.inspect(&v))
2577 })
2578 .collect()
2579 }
2580
2581 /// Scope-chain read: local + enclosing chain, then globals.
2582 /// Whether `name` is a module-top-level binding that has not reached its
2583 /// declaration yet. Separate from [`JsHost::is_tdz`], which answers for a
2584 /// block-scoped one by inspecting the value it holds.
2585 pub fn is_tdz_global(&self, name: &str) -> bool {
2586 self.tdz_globals.contains(name)
2587 }
2588
2589 pub fn read_name(&self, name: &str) -> Option<Value> {
2590 let mut env = Some(self.cur_env());
2591 while let Some(e) = env {
2592 if let Some(v) = e.borrow().vars.get(name) {
2593 return Some(v.clone());
2594 }
2595 env = e.borrow().parent.clone();
2596 }
2597 self.globals.get(name).cloned()
2598 }
2599 pub fn read_global(&self, name: &str) -> Option<Value> {
2600 self.globals.get(name).cloned()
2601 }
2602
2603 /// Whether `name` is bound anywhere on the scope chain or in the globals —
2604 /// `read_name(..).is_some()` without cloning the value it finds. The
2605 /// strict-mode assignment path asks this and nothing else.
2606 pub fn has_name(&self, name: &str) -> bool {
2607 let mut env = Some(self.cur_env());
2608 while let Some(e) = env {
2609 if e.borrow().vars.contains_key(name) {
2610 return true;
2611 }
2612 env = e.borrow().parent.clone();
2613 }
2614 self.globals.contains_key(name)
2615 }
2616
2617 /// Assign to an existing binding up the scope chain, else create a global
2618 /// (JS assignment to an undeclared name targets the global object).
2619 /// Assign to an existing binding, or create a global. Returns `false` when
2620 /// the nearest binding is an immutable (`const`) one, which the caller turns
2621 /// into `TypeError: Assignment to constant variable.` — assigning to a
2622 /// `const` used to succeed SILENTLY, so code that node rejects ran on with
2623 /// a mutated constant.
2624 #[must_use]
2625 pub fn set_name(&mut self, name: &str, val: Value) -> bool {
2626 let mut env = Some(self.cur_env());
2627 while let Some(e) = env {
2628 // `get_mut`, not `contains_key` + `insert`: overwriting an existing
2629 // binding hashed the name twice and allocated a fresh `String` key
2630 // for a key that was already there — once per assignment, so once
2631 // per loop iteration in any counting loop.
2632 //
2633 // The const check runs only at the env that OWNS the name, and the
2634 // `is_empty` guard settles the common (no consts here) case without
2635 // hashing the name again.
2636 let mut b = e.borrow_mut();
2637 if b.vars.contains_key(name) {
2638 if !b.consts.is_empty() && b.consts.contains(name) {
2639 return false;
2640 }
2641 if let Some(slot) = b.vars.get_mut(name) {
2642 *slot = val;
2643 }
2644 return true;
2645 }
2646 drop(b);
2647 env = e.borrow().parent.clone();
2648 }
2649 if self.global_consts.contains(name) {
2650 return false;
2651 }
2652 match self.globals.get_mut(name) {
2653 Some(slot) => *slot = val,
2654 None => {
2655 self.globals.insert(name.to_string(), val);
2656 }
2657 }
2658 true
2659 }
2660
2661 /// Declare a `const` binding: the same placement as [`Self::declare_name`],
2662 /// plus recording the name as immutable in whichever scope received it.
2663 pub fn declare_const_name(&mut self, name: &str, val: Value) {
2664 let f = self.frame();
2665 let to_globals = f.is_module && Rc::ptr_eq(&f.env, &f.base_env);
2666 self.declare_name(name, val);
2667 if to_globals {
2668 self.global_consts.insert(name.to_string());
2669 } else {
2670 self.cur_env().borrow_mut().consts.insert(name.to_string());
2671 }
2672 }
2673
2674 /// The value a lexical binding holds between entering its scope and reaching
2675 /// its declaration — its TEMPORAL DEAD ZONE. One heap object for the whole
2676 /// process, so the check is a heap-index comparison and the marker cannot be
2677 /// produced by any JavaScript expression. It never escapes: every path that
2678 /// could read it throws first.
2679 pub fn tdz_marker(&mut self) -> Value {
2680 if let Some(v) = &self.tdz {
2681 return v.clone();
2682 }
2683 let v = self.alloc(JsObj::Builtin("@@tdz".into()));
2684 self.tdz = Some(v.clone());
2685 v
2686 }
2687
2688 /// Whether `v` is the uninitialized-binding marker.
2689 pub fn is_tdz(&self, v: &Value) -> bool {
2690 matches!((&self.tdz, v), (Some(Value::Obj(a)), Value::Obj(b)) if a == b)
2691 }
2692
2693 /// Declare `name` in the CURRENT scope as uninitialized, unless that scope
2694 /// already binds it. Emitted at the top of every scope for each `let`,
2695 /// `const` and `class` declared directly in it, so a read before the
2696 /// declaration throws instead of finding an OUTER binding of the same name —
2697 /// `let x = 1; { x; let x = 2 }` used to read the outer `1`.
2698 pub fn hoist_tdz(&mut self, name: &str) {
2699 let marker = self.tdz_marker();
2700 let f = self.frame();
2701 // At module top level a lexical binding lives in `globals`, which is ALSO
2702 // what backs `globalThis.<name>` — so parking the marker there exposes it
2703 // to JavaScript, and `const crypto = …` made `globalThis.crypto` read
2704 // back as the marker. Top-level dead zones are tracked in a separate set
2705 // that only the name-read path consults.
2706 if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2707 if !self.globals.contains_key(name) {
2708 self.tdz_globals.insert(name.to_string());
2709 }
2710 return;
2711 }
2712 let env = self.cur_env();
2713 let mut e = env.borrow_mut();
2714 if !e.vars.contains_key(name) {
2715 e.vars.insert(name.to_string(), marker);
2716 }
2717 }
2718
2719 /// Declare a new binding in the current scope (`let`/`const`). At the top of
2720 /// the module frame there is no local env, so those names become globals; once
2721 /// a block scope is open the binding belongs to that block.
2722 pub fn declare_name(&mut self, name: &str, val: Value) {
2723 let f = self.frame();
2724 if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2725 self.tdz_globals.remove(name);
2726 self.globals.insert(name.to_string(), val);
2727 } else {
2728 self.cur_env()
2729 .borrow_mut()
2730 .vars
2731 .insert(name.to_string(), val);
2732 }
2733 }
2734
2735 /// Declare a `var` (or a hoisted function declaration): FUNCTION-scoped, so it
2736 /// skips every open block scope and lands in the activation's base env.
2737 /// Create a hoisted `var` binding, initialised to `undefined`, only when the
2738 /// name is not already bound in this activation.
2739 ///
2740 /// `var` bindings come into existence when the scope is entered, not where
2741 /// the declaration is written — `f(){ x; var x = 1 }` reads `undefined`
2742 /// rather than throwing. "If absent" is what keeps a parameter intact: in
2743 /// `function f(a) { var a; }` the `var` names a binding that already exists
2744 /// and must not be reset, which is also why a bare `var x;` emits nothing at
2745 /// its own position.
2746 pub fn hoist_var_name(&mut self, name: &str) {
2747 // The ENTRY script's top level is a CommonJS module body, not global
2748 // scope: node wraps every file in a function, so a top-level `var` is a
2749 // local of that wrapper. Binding it into the globals map made
2750 // `var x = 3` at the top of the entry readable as `globalThis.x`, where
2751 // node says `undefined` — a REQUIRED module already ran inside a real
2752 // frame and behaved correctly, so only the entry file differed.
2753 if self.frame().is_module && !self.module_scope {
2754 self.globals.entry(name.to_string()).or_insert(Value::Undef);
2755 return;
2756 }
2757 let base = self.frame().base_env.clone();
2758 let mut env = base.borrow_mut();
2759 if !env.vars.contains_key(name) {
2760 env.vars.insert(name.to_string(), Value::Undef);
2761 }
2762 }
2763
2764 pub fn declare_var_name(&mut self, name: &str, val: Value) {
2765 if self.frame().is_module && !self.module_scope {
2766 self.globals.insert(name.to_string(), val);
2767 return;
2768 }
2769 let base = self.frame().base_env.clone();
2770 base.borrow_mut().vars.insert(name.to_string(), val);
2771 }
2772
2773 /// Enter a fresh block scope.
2774 pub fn push_scope(&mut self) {
2775 let env = self.cur_env();
2776 self.frames.last_mut().unwrap().env = child_env(env);
2777 }
2778
2779 /// Open a scope that is also the activation's VARIABLE environment, and
2780 /// return the previous one so the caller can restore it.
2781 ///
2782 /// A block scope is not enough for a strict direct `eval`: `var` and a
2783 /// hoisted function declaration bind to `base_env`, so they walked straight
2784 /// past a plain `push_scope` and still landed in the caller's function
2785 /// scope. Only `let`/`const` were contained.
2786 pub fn push_var_scope(&mut self) -> Env {
2787 let env = child_env(self.cur_env());
2788 let f = self.frames.last_mut().unwrap();
2789 let prev = std::mem::replace(&mut f.base_env, env.clone());
2790 f.env = env;
2791 prev
2792 }
2793
2794 /// Restore the variable environment a `push_var_scope` replaced.
2795 pub fn pop_var_scope(&mut self, prev: Env) {
2796 let f = self.frames.last_mut().unwrap();
2797 f.env = prev.clone();
2798 f.base_env = prev;
2799 }
2800
2801 /// Leave the innermost block scope (never pops past the activation's base).
2802 pub fn pop_scope(&mut self) {
2803 let cur = self.cur_env();
2804 if Rc::ptr_eq(&cur, &self.frame().base_env) {
2805 return;
2806 }
2807 let parent = cur.borrow().parent.clone();
2808 if let Some(p) = parent {
2809 self.frames.last_mut().unwrap().env = p;
2810 }
2811 }
2812
2813 /// Replace the innermost block scope with a fresh copy of its bindings — the
2814 /// per-iteration environment a `for (let i …)` loop creates, so a closure made
2815 /// in one iteration keeps that iteration's value.
2816 pub fn copy_scope(&mut self) {
2817 let cur = self.cur_env();
2818 if Rc::ptr_eq(&cur, &self.frame().base_env) {
2819 return;
2820 }
2821 let parent = cur.borrow().parent.clone();
2822 let fresh = new_env(parent);
2823 fresh.borrow_mut().vars = cur.borrow().vars.clone();
2824 self.frames.last_mut().unwrap().env = fresh;
2825 }
2826
2827 /// The current block-scope env, for save/restore across a nested chunk.
2828 pub fn scope_snapshot(&self) -> Env {
2829 self.cur_env()
2830 }
2831 pub fn restore_scope(&mut self, env: Env) {
2832 self.frames.last_mut().unwrap().env = env;
2833 }
2834 pub fn set_global(&mut self, name: &str, val: Value) {
2835 self.globals.insert(name.to_string(), val);
2836 }
2837
2838 // ── output capture ───────────────────────────────────────────────────
2839 //
2840 // Every write a *program* makes — `console.log`, `process.stdout.write`,
2841 // `print` — funnels through `write_out`, so turning capture on redirects all
2842 // of them at once. Diagnostics the runtime itself emits (the REPL banner, a
2843 // crash traceback from `main`) deliberately do not: they belong to the
2844 // process, not to the program.
2845
2846 /// Start capturing program output in-process. Any text already captured is
2847 /// discarded, so each run starts clean.
2848 pub fn begin_capture(&mut self) {
2849 self.capture = Some(Vec::new());
2850 }
2851
2852 /// Stop capturing and take everything written since [`begin_capture`],
2853 /// returning the empty string when capture was not on. The captured bytes
2854 /// are rendered lossily: this API hands back a `String`, so a program that
2855 /// wrote non-UTF-8 gets `U+FFFD` here even though the same write reaches a
2856 /// real stdout byte-exact. Use [`end_capture_bytes`] to keep those bytes.
2857 ///
2858 /// [`begin_capture`]: JsHost::begin_capture
2859 /// [`end_capture_bytes`]: JsHost::end_capture_bytes
2860 pub fn end_capture(&mut self) -> String {
2861 String::from_utf8_lossy(&self.capture.take().unwrap_or_default()).into_owned()
2862 }
2863
2864 /// Stop capturing and take the raw bytes, without the lossy transcription
2865 /// [`end_capture`] applies.
2866 ///
2867 /// [`end_capture`]: JsHost::end_capture
2868 pub fn end_capture_bytes(&mut self) -> Vec<u8> {
2869 self.capture.take().unwrap_or_default()
2870 }
2871
2872 /// Whether output is being captured — the one thing a caller needs to know
2873 /// before asking the real stream a question (`isTTY`, cursor position).
2874 pub fn capturing(&self) -> bool {
2875 self.capture.is_some()
2876 }
2877
2878 /// Write program output: into the capture buffer when capturing, else to the
2879 /// process stream `stderr` selects. `s` is written verbatim — callers add
2880 /// their own line ending, as `console.log` does and `process.stdout.write`
2881 /// does not.
2882 pub fn write_out(&mut self, s: &str, stderr: bool) {
2883 self.write_out_bytes(s.as_bytes(), stderr);
2884 }
2885
2886 /// Write program output as raw BYTES. `process.stdout.write(buf)` hands Node
2887 /// a byte string and Node writes it through untouched, so a `Buffer` holding
2888 /// `ff fe 41` reaches stdout as those three bytes. Routing it through a Rust
2889 /// `String` first replaced every non-UTF-8 byte with `U+FFFD` — three bytes
2890 /// became seven — so the byte path exists separately from [`write_out`].
2891 ///
2892 /// [`write_out`]: JsHost::write_out
2893 pub fn write_out_bytes(&mut self, bytes: &[u8], stderr: bool) {
2894 if let Some(buf) = &mut self.capture {
2895 buf.extend_from_slice(bytes);
2896 return;
2897 }
2898 use std::io::Write as _;
2899 if stderr {
2900 let mut e = std::io::stderr();
2901 let _ = e.write_all(bytes);
2902 let _ = e.flush();
2903 } else {
2904 let mut o = std::io::stdout();
2905 let _ = o.write_all(bytes);
2906 let _ = o.flush();
2907 }
2908 }
2909 pub fn del_name(&mut self, name: &str) {
2910 if self
2911 .cur_env()
2912 .borrow_mut()
2913 .vars
2914 .shift_remove(name)
2915 .is_some()
2916 {
2917 return;
2918 }
2919 self.globals.shift_remove(name);
2920 }
2921
2922 pub fn current_this(&self) -> Option<Value> {
2923 self.frame().this_obj.clone()
2924 }
2925
2926 /// The running activation's [`ThisState`].
2927 pub fn this_state(&self) -> ThisState {
2928 self.frame().this_state
2929 }
2930
2931 /// Mark the next user-function activation as a derived constructor.
2932 pub fn mark_next_call_derived_ctor(&mut self) {
2933 self.derived_ctor_next = true;
2934 }
2935
2936 /// BindThisValue (9.1.1.3.1) for a `super()` that has just returned: the
2937 /// nearest derived-constructor activation becomes `Bound`. That is the top
2938 /// frame, or — for `super()` inside an arrow — the constructor below the
2939 /// arrow's own frame. `false` when it was already bound: the second call.
2940 pub fn bind_super_this(&mut self) -> bool {
2941 let Some(f) = self
2942 .frames
2943 .iter_mut()
2944 .rev()
2945 .find(|f| f.this_state != ThisState::Plain)
2946 else {
2947 return true;
2948 };
2949 if f.this_state == ThisState::Bound {
2950 return false;
2951 }
2952 f.this_state = ThisState::Bound;
2953 true
2954 }
2955
2956 /// The object a `super()` call substituted for the instance, if any.
2957 ///
2958 /// `construct_class` allocates the instance up front, so when a base
2959 /// constructor RETURNS an object the substitution happens deep inside the
2960 /// VM, after that allocation. This carries it back out. Each
2961 /// `construct_class` saves and restores the previous value around its own
2962 /// run, so a `new` inside a constructor body cannot steal it.
2963 pub fn take_super_replacement(&mut self) -> Option<Value> {
2964 self.super_replacement.take()
2965 }
2966
2967 pub fn swap_super_replacement(&mut self, v: Option<Value>) -> Option<Value> {
2968 std::mem::replace(&mut self.super_replacement, v)
2969 }
2970
2971 /// Rebind the running activation's `this`.
2972 ///
2973 /// Only `super()` does this: when the parent constructor RETURNS an object,
2974 /// 15.7.15 makes that object the derived instance, so the rest of the
2975 /// derived constructor has to write to it rather than to the one allocated
2976 /// before the call.
2977 pub fn set_current_this(&mut self, v: Value) {
2978 if let Some(f) = self.frames.last_mut() {
2979 f.this_obj = Some(v.clone());
2980 }
2981 self.super_replacement = Some(v);
2982 }
2983 /// The callbacks to run for `event`, consuming any `once` registration in
2984 /// the same step — so a listener that re-emits the event cannot re-enter a
2985 /// one-shot handler.
2986 pub fn take_process_listeners(&mut self, event: &str) -> Vec<Value> {
2987 let Some(list) = self.process_listeners.get_mut(event) else {
2988 return Vec::new();
2989 };
2990 let fired: Vec<Value> = list.iter().map(|l| l.f.clone()).collect();
2991 list.retain(|l| !l.once);
2992 fired
2993 }
2994
2995 /// Bind the TOP-LEVEL `this` — the value a `this` outside any function sees.
2996 ///
2997 /// Node answers differently per entry point and both answers are objects:
2998 /// `node f.js` runs a CommonJS module, so top-level `this` is
2999 /// `module.exports`; `node -e` and `node -` run a Script, so it is
3000 /// `globalThis`. Verified on node v26.7.0 —
3001 /// `console.log(this === globalThis, this === module.exports)` is
3002 /// `false true` from a file and `true false` from `-e` and from stdin. It
3003 /// was `undefined` at every entry point here, so `this.x = 1` at module
3004 /// scope threw instead of populating the exports object.
3005 ///
3006 /// Only the base frame is touched: a plain function call still gets its own
3007 /// (`undefined`) binding rather than inheriting this one.
3008 pub fn set_top_this(&mut self, v: Value) {
3009 if let Some(f) = self.frames.first_mut() {
3010 f.this_obj = Some(v);
3011 }
3012 }
3013 pub fn current_env_capture(&self) -> Env {
3014 self.frame().env.clone()
3015 }
3016 pub fn current_new_target(&self) -> Option<Value> {
3017 self.frame().new_target.clone()
3018 }
3019 fn current_home_class(&self) -> Option<Value> {
3020 self.frame().home_class.clone()
3021 }
3022
3023 /// The `(parent_ctor, this_class_fields)` for a running constructor's
3024 /// `super(...)`, derived from the frame's home class.
3025 pub fn super_context(&self) -> (Option<Value>, Vec<(String, Value, bool)>) {
3026 match self.current_home_class() {
3027 Some(cv) => match self.get(&cv) {
3028 Some(JsObj::Class(c)) => (c.parent.clone(), c.fields.clone()),
3029 _ => (None, Vec::new()),
3030 },
3031 None => (None, Vec::new()),
3032 }
3033 }
3034
3035 /// Resolve `super.name` to either the parent-prototype getter (to be invoked
3036 /// by the caller, outside any host borrow) or a directly-usable value.
3037 pub fn super_resolve(&self, name: &str) -> SuperRef {
3038 // A shorthand method in an OBJECT LITERAL resolves `super` through its
3039 // home object's prototype; only a class method has a home CLASS. With
3040 // nothing tracked for the literal case, `{ m() { super.x() } }` had no
3041 // parent to look in and reported the method missing.
3042 if let Some(home) = self.frame().home_object.clone() {
3043 let target = self.proto_of(&home).unwrap_or(Value::Undef);
3044 if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3045 return SuperRef::Getter(getter);
3046 }
3047 return SuperRef::Data(lookup_chain(self, &target, name).unwrap_or(Value::Undef));
3048 }
3049 let parent = match self
3050 .current_home_class()
3051 .and_then(|cv| match self.get(&cv) {
3052 Some(JsObj::Class(c)) => c.parent.clone(),
3053 _ => None,
3054 }) {
3055 Some(p) => p,
3056 None => return SuperRef::Data(Value::Undef),
3057 };
3058 // A STATIC method's home object is the constructor, so `super.x` reads
3059 // off the parent CONSTRUCTOR; an instance method's is the prototype
3060 // object, so it reads off the parent's prototype. Always taking the
3061 // prototype meant `static s() { return super.s(); }` found nothing and
3062 // then tried to call it.
3063 let target = if self.frame().home_static {
3064 parent.clone()
3065 } else {
3066 match self.get(&parent) {
3067 Some(JsObj::Class(pc)) => pc.proto.clone(),
3068 _ => self.fn_prop(&parent, "prototype").unwrap_or(Value::Undef),
3069 }
3070 };
3071 if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3072 return SuperRef::Getter(getter);
3073 }
3074 if let Some(v) = lookup_chain(self, &target, name) {
3075 return SuperRef::Data(v);
3076 }
3077 // A static method lives in the fn-prop side table, not the property map.
3078 SuperRef::Data(self.fn_prop(&target, name).unwrap_or(Value::Undef))
3079 }
3080
3081 // ── signals / errors ─────────────────────────────────────────────────
3082 pub fn take_error(&mut self) -> Option<String> {
3083 self.error.take()
3084 }
3085 pub fn raise_str(&mut self, class: &str, msg: &str) -> String {
3086 let s = if msg.is_empty() {
3087 class.to_string()
3088 } else {
3089 format!("{class}: {msg}")
3090 };
3091 self.error = Some(s.clone());
3092 s
3093 }
3094}
3095
3096// ── error constructors ───────────────────────────────────────────────────────
3097
3098pub fn type_error(msg: &str) -> String {
3099 format!("TypeError: {msg}")
3100}
3101pub fn ref_error(name: &str) -> String {
3102 format!("ReferenceError: {name} is not defined")
3103}
3104
3105/// The error a read of a lexical binding still in its TEMPORAL DEAD ZONE
3106/// raises. Distinct from [`ref_error`] on purpose: node says which of the two
3107/// happened, and the difference is how a reader tells a misspelled name from a
3108/// `let` used above its declaration.
3109pub fn tdz_error(name: &str) -> String {
3110 format!("ReferenceError: Cannot access '{name}' before initialization")
3111}
3112pub fn range_error(msg: &str) -> String {
3113 format!("RangeError: {msg}")
3114}
3115
3116/// V8's `String::kMaxLength` on a 64-bit build, in UTF-16 code units — the
3117/// largest string the engine will materialize.
3118///
3119/// Measured on node v26.7.0 (darwin arm64):
3120/// `require('buffer').constants.MAX_STRING_LENGTH` is `536870888`,
3121/// `'a'.repeat(536870888)` succeeds with that length, and
3122/// `'a'.repeat(536870889)` is `RangeError: Invalid string length`.
3123pub const MAX_STRING_LENGTH: usize = 536_870_888;
3124
3125/// The error V8 raises for a string operation whose RESULT would exceed
3126/// [`MAX_STRING_LENGTH`]. It is raised from the length arithmetic, before any
3127/// allocation: `'a'.repeat(2**40)` throws promptly on node where node-js used to
3128/// sit building a 1 TiB `String` until it was killed.
3129pub fn invalid_string_length() -> String {
3130 range_error("Invalid string length")
3131}
3132
3133/// `ToUint32`-validated array length — ECMA-262 10.4.2.2 `ArrayCreate` step 1
3134/// and 10.4.2.4 `ArraySetLength` step 3.
3135///
3136/// A length is legal only if `ToUint32(v)` equals `ToNumber(v)` exactly, so
3137/// `-1`, `1.5`, `NaN`, `Infinity`, `'x'` and `2**32` are all
3138/// `RangeError: Invalid array length` while `'3'` is `3` and `-0` is `0`
3139/// (measured on node v26.7.0: `new Array(-0).length` is `0`, `a.length = '3'`
3140/// leaves `3`, `a.length = 'x'` throws). node-js validated none of them — it
3141/// built `[-1]` from `new Array(-1)`, silently ignored `a.length = -1`, and sat
3142/// materializing four billion elements for `a.length = 2**32`.
3143pub fn to_array_length(v: &Value) -> Result<usize, String> {
3144 // 10.4.2.4 steps 2-3 run TWO conversions: `ToUint32(value)` and then
3145 // `ToNumber(value)`, compared against each other. Both are observable — a
3146 // counting `valueOf` sees two calls in node and saw one here — and the
3147 // second is what makes `arr.length = 1.5` a RangeError rather than 1.
3148 let u32_pass = to_number_value(v)?;
3149 let n = to_number_value(v)?;
3150 let _ = u32_pass;
3151 // `ToUint32`: truncate toward zero, then modulo 2^32.
3152 let u = if n.is_finite() {
3153 (n.trunc() as i64).rem_euclid(1i64 << 32) as u32
3154 } else {
3155 0
3156 };
3157 // `-0` compares equal to `0` here, which is what makes `new Array(-0)` legal.
3158 if (u as f64) != n {
3159 return Err(range_error("Invalid array length"));
3160 }
3161 Ok(u as usize)
3162}
3163
3164/// A Node *coded* error raised from the JS layer: `Name [ERR_CODE]: message`.
3165///
3166/// `builtins::synth_error` parses that head back apart, so the bracketed code
3167/// becomes the enumerable `err.code` that `err.code === 'ERR_INVALID_URL'`-style
3168/// handling reads. Writing the head by hand at each throw site is what left a
3169/// dozen of them with `err.code === undefined` while the message matched.
3170///
3171/// Use this for errors Node raises from `lib/internal/errors.js`, whose `.name`
3172/// is left bracketed while the stack is captured and therefore shows up in both
3173/// `String(err)` and `err.stack` — measured on v26.7.0:
3174///
3175/// ```text
3176/// process.exit(1.5) -> RangeError [ERR_OUT_OF_RANGE]: The value of "code" …
3177/// ```
3178pub fn coded_error(class: &str, code: &str, msg: &str) -> String {
3179 format!("{class} [{code}]: {msg}")
3180}
3181
3182/// The marker `plain_coded_error` hides a code behind, and `synth_error` strips.
3183pub const CODE_MARK: &str = "\u{1}code:";
3184
3185/// Marks an error string as a `DOMException` carrying a WHATWG error NAME
3186/// rather than one of the ECMAScript error classes. WebCrypto and the abort
3187/// APIs reject with these, and the name (`NotSupportedError`) is not a class
3188/// `synth_error` could otherwise recognise.
3189pub const DOM_MARK: &str = "\u{1}dom:";
3190
3191/// A `DOMException` error string: `name` is the WHATWG error name.
3192pub fn dom_error(name: &str, msg: &str) -> String {
3193 format!("{DOM_MARK}{name}\u{1}{msg}")
3194}
3195
3196/// A Node coded error raised from the *native* layer: `.code` is set, but the
3197/// name is never bracketed, so `String(err)` is the plain `Name: message`.
3198///
3199/// The distinction is observable and is not a stylistic choice — on v26.7.0,
3200/// `String(new URL("/x") error)` is `TypeError: Invalid URL` with
3201/// `.code === 'ERR_INVALID_URL'`, while the JS-layer `process.exit(1.5)` error
3202/// brackets its code into the very same two reads. Encoding both through one
3203/// `Name [CODE]:` head would have to pick one and be wrong about the other.
3204///
3205/// The code rides in a marker at the head of the message rather than in the
3206/// error class, because the class text is exactly what must NOT carry it. The
3207/// marker is an internal wire format between a throw site and `synth_error`; it
3208/// never survives into a `.message`.
3209pub fn plain_coded_error(class: &str, code: &str, msg: &str) -> String {
3210 format!("{class}: {CODE_MARK}{code}\u{1}{msg}")
3211}
3212
3213/// Marks the start of the extra string own properties a
3214/// [`plain_coded_error_with`] error carries after its message.
3215pub const FIELDS_MARK: char = '\u{2}';
3216
3217/// [`plain_coded_error`] plus extra enumerable string own properties, set after
3218/// `code` in the order given — `new URL('x', 'nope')` throws with
3219/// `Object.keys(e)` reading `["code","input","base"]`.
3220///
3221/// Each field is `key\u{3}<byte length>\u{3}value`, so a value (a URL input is
3222/// arbitrary user text) may carry any character, the separators included.
3223pub fn plain_coded_error_with(
3224 class: &str,
3225 code: &str,
3226 msg: &str,
3227 fields: &[(&str, &str)],
3228) -> String {
3229 let mut s = plain_coded_error(class, code, msg);
3230 s.push(FIELDS_MARK);
3231 for (k, v) in fields {
3232 s.push_str(&format!("{k}\u{3}{}\u{3}{v}", v.len()));
3233 }
3234 s
3235}
3236
3237/// An error string as a person reads it: `TypeError: Invalid URL`, with the
3238/// internal code and field markers of [`plain_coded_error`] /
3239/// [`plain_coded_error_with`] removed. An uncaught native error is printed
3240/// from its string, and printed the wire format (`\u{1}code:ERR_INVALID_URL…`).
3241pub fn plain_error_text(e: &str) -> String {
3242 let Some(i) = e.find(CODE_MARK) else {
3243 return e.to_string();
3244 };
3245 let (head, rest) = e.split_at(i);
3246 match rest[CODE_MARK.len()..].split_once('\u{1}') {
3247 Some((_, m)) => format!("{head}{}", split_error_fields(m).0),
3248 None => e.to_string(),
3249 }
3250}
3251
3252/// Split a [`plain_coded_error_with`] message back into the message and its
3253/// fields. A message with no field mark comes back whole with no fields.
3254pub fn split_error_fields(msg: &str) -> (&str, Vec<(&str, &str)>) {
3255 let Some((head, mut rest)) = msg.split_once(FIELDS_MARK) else {
3256 return (msg, Vec::new());
3257 };
3258 let mut fields = Vec::new();
3259 while let Some((k, tail)) = rest.split_once('\u{3}') {
3260 let Some((len, tail)) = tail.split_once('\u{3}') else {
3261 break;
3262 };
3263 let Ok(len) = len.parse::<usize>() else { break };
3264 let Some(v) = tail.get(..len) else { break };
3265 fields.push((k, v));
3266 rest = &tail[len..];
3267 }
3268 (head, fields)
3269}
3270
3271/// `TypeError [ERR_INVALID_ARG_TYPE]: The "<name>" <kind> must be of type
3272/// <expected>. Received …` — Node's single most common argument rejection.
3273pub fn invalid_arg_type(name: &str, kind: &str, expected: &str, v: &Value) -> String {
3274 coded_error(
3275 "TypeError",
3276 "ERR_INVALID_ARG_TYPE",
3277 &format!(
3278 "The \"{name}\" {kind} must be of type {expected}. Received {}",
3279 crate::stdlib::received_desc(v)
3280 ),
3281 )
3282}
3283
3284// ── the fusevm run plumbing ──────────────────────────────────────────────────
3285
3286thread_local! {
3287 static DEBUG_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3288}
3289
3290/// Enable/disable DAP debug execution (`node --dap`).
3291pub fn set_debug_mode(on: bool) {
3292 DEBUG_MODE.with(|d| d.set(on));
3293}
3294
3295// ── join cycle detection ─────────────────────────────────────────────────────
3296
3297thread_local! {
3298 /// Heap handles whose join is in progress, innermost last — V8's JoinStack.
3299 static JOIN_STACK: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
3300}
3301
3302/// V8's `JoinStackPush`: record that `v` is being joined, or report `false` if
3303/// it already is.
3304///
3305/// `Array.prototype.join` (and `toString`/`toLocaleString`, which route through
3306/// it) is the one place the language walks an object graph with no depth bound,
3307/// so every engine cuts re-entrance here: a receiver already on the stack
3308/// contributes the EMPTY STRING rather than recursing. Measured on node v26.7.0,
3309/// `const a=[1]; a.push(a); a.push(2); a.join('-')` is `"1--2"`, and
3310/// `String(a)`/`` `${a}` `` on `a=[a]` are both `""`. node-js had no such cut and
3311/// recursed until the native stack overflowed, ABORTING the process (exit 134) —
3312/// uncatchable, where node returns a string.
3313///
3314/// Only re-entrance is cut, not repetition: `[a,a].join('|')` still renders `a`
3315/// twice, because the first render pops before the second pushes.
3316///
3317/// A `true` return MUST be paired with [`join_stack_pop`].
3318pub fn join_stack_push(v: &Value) -> bool {
3319 match v {
3320 Value::Obj(i) => JOIN_STACK.with(|s| {
3321 let mut s = s.borrow_mut();
3322 if s.contains(i) {
3323 false
3324 } else {
3325 s.push(*i);
3326 true
3327 }
3328 }),
3329 _ => true,
3330 }
3331}
3332
3333/// Pop the innermost [`join_stack_push`].
3334pub fn join_stack_pop() {
3335 JOIN_STACK.with(|s| {
3336 s.borrow_mut().pop();
3337 });
3338}
3339
3340// ── native stack guard ───────────────────────────────────────────────────────
3341
3342thread_local! {
3343 /// Lowest stack address a nested run may start from, or 0 before the
3344 /// running thread's bounds have been measured. Cached because the pthread
3345 /// query is a syscall-free but non-trivial read and this is on every call.
3346 static STACK_FLOOR: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
3347}
3348
3349/// Stack left unusable below the floor, as a fraction of the whole stack: the
3350/// throw itself still has to unwind, build an `Error`, capture `.stack` and run
3351/// whatever `catch` receives it, all of which needs room *below* the deepest
3352/// call that was allowed.
3353const STACK_RESERVE_DIVISOR: usize = 8;
3354/// Floor of that reserve, for a thread whose stack is small enough that an
3355/// eighth of it would not cover the unwind.
3356const STACK_RESERVE_MIN: usize = 512 * 1024;
3357/// Reserve assumed on a platform whose stack bounds cannot be queried. Deliberately
3358/// large relative to a default 8 MiB stack — over-reserving costs recursion
3359/// depth, under-reserving costs the process.
3360const STACK_RESERVE_FALLBACK: usize = 1024 * 1024;
3361
3362/// The address of a local in the caller's frame — how far down the stack
3363/// execution currently is. `black_box` keeps the probe from being optimized into
3364/// a different frame.
3365fn stack_pointer() -> usize {
3366 let probe = 0u8;
3367 std::hint::black_box(&probe) as *const u8 as usize
3368}
3369
3370/// The running thread's `(lowest address, size)` stack bounds.
3371///
3372/// Asked of pthread rather than assumed, because the three threads that run JS
3373/// have three different stacks: the `node` binary's own (`main.rs` reserves
3374/// [`crate::JS_STACK_SIZE`]), a `worker_threads` thread's, and a `cargo test`
3375/// harness thread's. A fixed byte budget would be wrong on two of the three.
3376fn stack_bounds() -> Option<(usize, usize)> {
3377 #[cfg(target_vendor = "apple")]
3378 {
3379 // SAFETY: both calls are pure reads of the calling thread's own
3380 // pthread record; neither allocates nor can fail.
3381 unsafe {
3382 let me = libc::pthread_self();
3383 let top = libc::pthread_get_stackaddr_np(me) as usize;
3384 let size = libc::pthread_get_stacksize_np(me);
3385 if size == 0 || top < size {
3386 return None;
3387 }
3388 Some((top - size, size))
3389 }
3390 }
3391 #[cfg(target_os = "linux")]
3392 {
3393 // SAFETY: `attr` is initialized by `pthread_getattr_np` before it is
3394 // read, only read on the success path, and destroyed on every path.
3395 unsafe {
3396 let mut attr: libc::pthread_attr_t = std::mem::zeroed();
3397 if libc::pthread_getattr_np(libc::pthread_self(), &mut attr) != 0 {
3398 return None;
3399 }
3400 let mut low: *mut libc::c_void = std::ptr::null_mut();
3401 let mut size: libc::size_t = 0;
3402 let ok = libc::pthread_attr_getstack(&attr, &mut low, &mut size) == 0;
3403 libc::pthread_attr_destroy(&mut attr);
3404 if ok && size != 0 {
3405 return Some((low as usize, size));
3406 }
3407 None
3408 }
3409 }
3410 #[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
3411 {
3412 None
3413 }
3414}
3415
3416/// The stack address below which a further nested VM run must throw instead of
3417/// recursing.
3418///
3419/// Every JS call is a Rust-level recursion — `run_user_func_nt` pushes a
3420/// [`Frame`], then `run_chunk_on` builds a whole new `fusevm::VM` on the stack
3421/// and runs the body, whose own calls land back here. Unbounded JS recursion
3422/// therefore used to exhaust the OS stack and ABORT: `fatal runtime error:
3423/// stack overflow`, exit 134, which no `try`/`catch` can see. V8 throws a
3424/// catchable `RangeError: Maximum call stack size exceeded` instead (measured on
3425/// node v26.7.0: `let d=0; function f(){d++;f()}` reports depth 9901).
3426///
3427/// The floor is derived from the thread's real bounds rather than a frame count
3428/// because a node-js frame has no fixed size — a debug build spends ~98 KiB per
3429/// JS call (measured: `node -e 'function f(n){…f(n-1)}'` survived 83 on an 8 MiB
3430/// stack and no more), a release build far less, and a native builtin recursing
3431/// through a user callback spends a different amount again.
3432fn stack_floor() -> usize {
3433 let cached = STACK_FLOOR.with(|c| c.get());
3434 if cached != 0 {
3435 return cached;
3436 }
3437 let floor = match stack_bounds() {
3438 Some((low, size)) => low + (size / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN),
3439 None => stack_pointer().saturating_sub(STACK_RESERVE_FALLBACK),
3440 };
3441 STACK_FLOOR.with(|c| c.set(floor));
3442 floor
3443}
3444
3445/// Stack given to each generator/async coroutine.
3446///
3447/// corosensei's default is 1 MiB, which at a debug build's ~98 KiB per JS call
3448/// left a `function*` body barely ten frames of recursion before it walked off
3449/// the end. The mapping is `PROT_NONE` reserved and `mprotect`ed, so the cost of
3450/// a larger one is address space, not resident memory — but it IS per live
3451/// generator, so this stays far below the entry thread's
3452/// [`crate::JS_STACK_SIZE`]: a program with thousands of concurrent async calls
3453/// has thousands of these.
3454const CORO_STACK_SIZE: usize = 16 * 1024 * 1024;
3455
3456/// The [`stack_floor`] that applies while a coroutine on `stack` is running.
3457fn coro_stack_floor(stack: &impl corosensei::stack::Stack) -> usize {
3458 stack.limit().get() + (CORO_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN)
3459}
3460
3461/// corosensei's own `DefaultStack::default()` size, used only when the
3462/// [`CORO_STACK_SIZE`] reservation is refused and the coroutine therefore runs
3463/// on a stack whose bounds are not ours to read.
3464const CORO_FALLBACK_STACK_SIZE: usize = 1024 * 1024;
3465
3466/// Give a coroutine whose stack bounds are unknown a floor measured from where
3467/// its body starts. Called once, at body entry, on the coroutine's own stack.
3468fn ensure_coroutine_floor() {
3469 if STACK_FLOOR.with(|c| c.get()) != 0 {
3470 return;
3471 }
3472 let budget = CORO_FALLBACK_STACK_SIZE
3473 - (CORO_FALLBACK_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN);
3474 STACK_FLOOR.with(|c| c.set(stack_pointer().saturating_sub(budget)));
3475}
3476
3477/// Install `floor` as the current stack floor, returning the previous one.
3478///
3479/// Used around a coroutine resume, which switches to a stack the thread's
3480/// pthread record knows nothing about. A floor of 0 means "not known" and makes
3481/// the next [`stack_floor`] measure again, which is the right answer for the
3482/// entry thread and a conservative one for a fallback coroutine stack.
3483fn swap_stack_floor(floor: usize) -> usize {
3484 STACK_FLOOR.with(|c| c.replace(floor))
3485}
3486
3487/// Whether the native stack is too close to its floor for one more nested run.
3488pub fn stack_exhausted() -> bool {
3489 stack_pointer() <= stack_floor()
3490}
3491
3492/// The error V8 raises when the call stack is exhausted. Catchable, and with the
3493/// `RangeError` constructor node uses — not a `panic!`.
3494pub fn stack_overflow_error() -> String {
3495 range_error("Maximum call stack size exceeded")
3496}
3497
3498/// Pool key for the body of user function `def_id`.
3499pub fn func_key(def_id: usize) -> u64 {
3500 1 << 40 | def_id as u64
3501}
3502
3503/// Pool key for one part of `try` statement `try_id`: 0 = the block, 1 = the
3504/// handler, 2 = the finalizer.
3505pub fn try_key(try_id: usize, part: u64) -> u64 {
3506 2 << 40 | (try_id as u64) << 2 | part
3507}
3508
3509thread_local! {
3510 /// VMs that have finished a run, kept for the next one — grouped by the
3511 /// chunk they still hold.
3512 ///
3513 /// Every JS call, every `try` block and every generator step runs its chunk
3514 /// through [`run_chunk_on`], which used to build a `fusevm::VM` from
3515 /// scratch: three `Vec` allocations, 70 `register_builtin` writes, an `Arc`
3516 /// for the numeric hook, and the JIT enable — per call. `fib(27)` makes
3517 /// 400k calls, so it built 400k VMs to run 23 ops each.
3518 ///
3519 /// Worse, the caller had to hand over an OWNED `Chunk`, so every call also
3520 /// deep-copied the function's whole compiled body: six `Vec`s, a `String`,
3521 /// and `sub_chunks` recursively. Keying the pool by chunk means a repeated
3522 /// call takes back the VM that already holds that body and copies nothing:
3523 /// `VM::reset` is handed the chunk the VM was already carrying.
3524 ///
3525 /// `VM::reset` keeps the builtin table, the hooks and the JIT setting, so a
3526 /// recycled VM needs none of that again. Each key holds a stack of VMs, and
3527 /// a nested (or recursive) call takes the next one, so a key grows to the
3528 /// deepest simultaneous entry into that function and no further.
3529 static VM_POOL: RefCell<rustc_hash::FxHashMap<u64, Vec<VM>>> =
3530 RefCell::new(rustc_hash::FxHashMap::default());
3531}
3532
3533/// An idle VM filed under `key`, if any.
3534fn take_pooled(key: u64) -> Option<VM> {
3535 VM_POOL.with(|p| p.borrow_mut().get_mut(&key).and_then(|v| v.pop()))
3536}
3537
3538/// File a finished VM under `key` for the next run to take.
3539fn put_pooled(key: u64, vm: VM) {
3540 VM_POOL.with(|p| p.borrow_mut().entry(key).or_default().push(vm));
3541}
3542
3543/// Take a VM ready to run `chunk` — recycled if one is idle, otherwise built
3544/// and fitted with the builtins and hooks a fresh VM needs.
3545fn acquire_vm(chunk: Chunk) -> VM {
3546 if let Some(mut vm) = take_pooled(0) {
3547 vm.reset(chunk);
3548 return vm;
3549 }
3550 let mut vm = VM::new(chunk);
3551 crate::builtins::install(&mut vm);
3552 vm.set_numeric_hook(std::sync::Arc::new(|op, a, b| {
3553 crate::builtins::numeric_hook(op, a, b)
3554 }));
3555 // Under `--dap` the tracing JIT would compile hot loops and skip the
3556 // per-statement `DBG_LINE` markers, so debug runs stay on the pure
3557 // interpreter. The `DBG_LINE` builtin fires the debugger line hook; the
3558 // extension seam mirrors pythonrs should the marker emission ever switch.
3559 // The mode is fixed before the first chunk runs, so a pooled VM can never
3560 // come back wearing the wrong one.
3561 if DEBUG_MODE.with(|d| d.get()) {
3562 vm.set_extension_handler(Box::new(|vm, id, _| {
3563 crate::dap::on_ext(vm, id);
3564 }));
3565 } else {
3566 vm.enable_tracing_jit();
3567 }
3568 vm
3569}
3570
3571/// Register every node-js builtin + the numeric hook on a VM, then run it.
3572///
3573/// For a chunk that runs once — a module body, an `eval` — there is nothing to
3574/// key a pool by, so this resets a spare VM with the caller's chunk. Anything
3575/// that runs repeatedly (a function body, a `try` block) goes through
3576/// [`run_chunk_keyed`] instead and never copies its chunk twice.
3577pub fn run_chunk_on(chunk: Chunk) -> Result<Value, String> {
3578 // Checked before the `VM` is built: `VM::new` + `install` are themselves
3579 // several KiB of frame, so a check after them could already have overflowed.
3580 if stack_exhausted() {
3581 return Err(stack_overflow_error());
3582 }
3583 finish_run(0, acquire_vm(chunk))
3584}
3585
3586/// Run the chunk filed under `key`, building it with `make` only if no VM is
3587/// already holding it. A recycled VM re-runs the chunk it kept, so a repeated
3588/// call copies no bytecode at all.
3589pub fn run_chunk_keyed(key: u64, make: impl FnOnce() -> Chunk) -> Result<Value, String> {
3590 if stack_exhausted() {
3591 return Err(stack_overflow_error());
3592 }
3593 let vm = match take_pooled(key) {
3594 Some(mut vm) => {
3595 // Hand the VM back the chunk it is already carrying: `reset` takes
3596 // an owned `Chunk`, and this is the one place where the owned chunk
3597 // costs nothing.
3598 let held = std::mem::take(&mut vm.chunk);
3599 vm.reset(held);
3600 vm
3601 }
3602 None => acquire_vm(make()),
3603 };
3604 finish_run(key, vm)
3605}
3606
3607/// Run a prepared VM to completion and file it back under `key`.
3608fn finish_run(key: u64, mut vm: VM) -> Result<Value, String> {
3609 let outcome = vm.run();
3610 let result = match outcome {
3611 _ if with_host(|h| h.error.is_some()) => {
3612 Err(with_host(|h| h.take_error()).expect("just checked"))
3613 }
3614 VMResult::Ok(v) => Ok(v),
3615 VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
3616 VMResult::Error(e) => Err(e),
3617 };
3618 put_pooled(key, vm);
3619 result
3620}
3621
3622/// Run `chunk` in the GLOBAL scope instead of the caller's.
3623///
3624/// `run_chunk_on` executes on whatever frame is current, so a nested run sees —
3625/// and can shadow — the *calling function's* locals. That is right for a direct
3626/// `eval`, and wrong for every other runtime-source construct: a `new Function`
3627/// body, an indirect `eval` and `vm.runInThisContext` are all specified to run
3628/// in the global scope (ECMA-262 19.2.1.1 `PerformEval` with a null
3629/// `strictCaller`/`direct` pair; `FunctionBody` is instantiated with the *global*
3630/// environment, 20.2.1.1.1 step 26). Measured against node v26.7.0,
3631/// `function outer(){ let loc = 42; return vm.runInThisContext('typeof loc'); }`
3632/// is `"undefined"` there and was `"number"` here.
3633///
3634/// A `var` the chunk itself declares lands in the top-level scope and persists,
3635/// so successive `vm.runInThisContext` calls share it.
3636pub fn run_chunk_in_global_scope(chunk: Chunk) -> Result<Value, String> {
3637 // An INDIRECT eval really is global code (19.2.1.1 step 6): its `var`s bind
3638 // to the global object, not to the entry module's wrapper scope. The flag
3639 // that keeps the entry script's own `var`s out of the globals map has to be
3640 // lifted for the duration, or `(0, eval)('var g = 1')` stopped reaching
3641 // `globalThis.g`.
3642 let prev_scope = with_host(|h| std::mem::take(&mut h.module_scope));
3643 let out = run_chunk_in_global_scope_inner(chunk);
3644 with_host(|h| h.module_scope = prev_scope);
3645 out
3646}
3647
3648fn run_chunk_in_global_scope_inner(chunk: Chunk) -> Result<Value, String> {
3649 let global_env = with_host(|h| h.global_env.clone());
3650 with_host(|h| {
3651 h.frames.push(Frame {
3652 env: global_env.clone(),
3653 base_env: global_env,
3654 this_obj: None,
3655 new_target: None,
3656 home_class: None,
3657 home_static: false,
3658 home_object: None,
3659 strict: false,
3660 line: 0,
3661 owner: None,
3662 is_module: true,
3663 this_state: ThisState::Plain,
3664 })
3665 });
3666 let r = run_chunk_on(chunk);
3667 with_host(|h| {
3668 h.frames.pop();
3669 });
3670 r
3671}
3672
3673/// Run the top-level program chunk, then drain the event loop (microtasks +
3674/// timers) until quiescent — matching Node, which keeps the process alive while
3675/// pending async work remains.
3676pub fn run_main(chunk: Chunk) -> Result<Value, String> {
3677 with_host(|h| h.module_scope = true);
3678 let r = run_chunk_on(chunk);
3679 with_host(|h| h.signal = None);
3680 if r.is_ok() {
3681 run_event_loop()?;
3682 finish_process_events()?;
3683 }
3684 r
3685}
3686
3687/// The shutdown sequence Node runs once the loop has drained on its own: fire
3688/// `beforeExit` (which MAY schedule more work, in which case the loop runs
3689/// again and `beforeExit` fires again), then fire `exit` exactly once.
3690///
3691/// Neither event fired at all before this existed, so `process.on('exit', …)`
3692/// was a registration with no delivery — a listener whose body printed was
3693/// silently dropped, and one that set `process.exitCode` could not affect the
3694/// status. Measured on node v26.7.0,
3695/// `process.on('exit', c => console.log('exit', c))` prints `exit 0`.
3696///
3697/// An explicit `process.exit()` never reaches here (it leaves the process from
3698/// inside the builtin), and neither does an uncaught exception — matching
3699/// Node, where `beforeExit` is skipped on both paths.
3700fn finish_process_events() -> Result<(), String> {
3701 // Bounded: a `beforeExit` listener that re-arms work every time would spin
3702 // forever, exactly as it does in Node, but a runaway here would hang a
3703 // parity run with no output, so it is capped and then treated as drained.
3704 for _ in 0..1000 {
3705 let code = with_host(|h| h.exit_code).unwrap_or(0);
3706 if !crate::stdlib::process::emit_before_exit(code)? {
3707 break;
3708 }
3709 let more =
3710 with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
3711 if !more {
3712 break;
3713 }
3714 run_event_loop()?;
3715 }
3716 let code = with_host(|h| h.exit_code).unwrap_or(0);
3717 crate::stdlib::process::emit_exit_event(code)
3718}
3719
3720// ── formatting ───────────────────────────────────────────────────────────────
3721
3722/// Format a JS number exactly as `Number.prototype.toString` does for the common
3723/// range (no exponential-notation threshold handling for very large/small).
3724pub fn fmt_number(f: f64) -> String {
3725 if f.is_nan() {
3726 return "NaN".into();
3727 }
3728 if f.is_infinite() {
3729 return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
3730 }
3731 if f == 0.0 {
3732 // Covers -0.0 too: (-0).toString() === "0".
3733 return "0".into();
3734 }
3735 if f < 0.0 {
3736 return format!("-{}", js_number_repr(-f));
3737 }
3738 js_number_repr(f)
3739}
3740
3741/// If `k` is an array-index property key, return its numeric value. Per
3742/// ECMAScript, a String property key `P` is an array index iff
3743/// `ToString(ToUint32(P)) === P` and `ToUint32(P) !== 2^32 - 1` — i.e. a
3744/// canonical decimal (no leading zeros, no sign) in the range `0..=2^32-2`.
3745pub fn array_index(k: &str) -> Option<u32> {
3746 if k.is_empty() {
3747 return None;
3748 }
3749 if k == "0" {
3750 return Some(0);
3751 }
3752 // A leading '0' (other than the lone "0" above) is non-canonical.
3753 if k.as_bytes()[0] == b'0' {
3754 return None;
3755 }
3756 if !k.bytes().all(|b| b.is_ascii_digit()) {
3757 return None;
3758 }
3759 match k.parse::<u64>() {
3760 // Array index must be < 2^32-1; u32::MAX == 2^32-1 is excluded.
3761 Ok(n) if n < u32::MAX as u64 => Some(n as u32),
3762 _ => None,
3763 }
3764}
3765
3766/// Compare two own-property keys for `OrdinaryOwnPropertyKeys` enumeration order:
3767/// integer-index keys sort ascending-numeric and precede all string keys; two
3768/// non-index keys compare `Equal` so a *stable* sort leaves them in insertion
3769/// order. (Symbols are stored as `@@…`/`#…` string keys and are non-index, so
3770/// they also fall into the stable-insertion-order tail.)
3771pub fn key_order_cmp(a: &str, b: &str) -> std::cmp::Ordering {
3772 use std::cmp::Ordering;
3773 match (array_index(a), array_index(b)) {
3774 (Some(x), Some(y)) => x.cmp(&y),
3775 (Some(_), None) => Ordering::Less,
3776 (None, Some(_)) => Ordering::Greater,
3777 (None, None) => Ordering::Equal,
3778 }
3779}
3780
3781/// Reorder an object's own-property map into `OrdinaryOwnPropertyKeys` order in
3782/// place: array-index keys ascending first, then the remaining keys in their
3783/// existing (insertion) order. A no-op unless at least one index key is present,
3784/// so the overwhelmingly common all-string-key object keeps its exact order and
3785/// pays nothing. `IndexMap::sort_by` is a stable sort.
3786pub fn canonicalize_own_keys(props: &mut IndexMap<String, Value>) {
3787 if props.keys().any(|k| array_index(k).is_some()) {
3788 props.sort_by(|ak, _, bk, _| key_order_cmp(ak, bk));
3789 }
3790}
3791
3792/// ECMAScript `Number::toString` layout for a positive, finite, nonzero value.
3793///
3794/// Rust's `Display`/`LowerExp` give the shortest round-trip decimal digits, but
3795/// NOT JavaScript's exponential-vs-fixed threshold: Rust prints `1e21` as
3796/// `1000000000000000000000` and `1e-7` as `0.0000001`, whereas JS prints `1e+21`
3797/// and `1e-7`. So we take the shortest digits from `{:e}` and re-lay them out per
3798/// the spec (steps 5–10 of Number::toString): `k` significant digits `s` with
3799/// decimal exponent `n` (value = s × 10^(n−k)); exponential form only when
3800/// `n > 21` or `n ≤ -6`.
3801fn js_number_repr(a: f64) -> String {
3802 // `{:e}` yields `d[.ddd]e<exp>` with the mantissa in [1, 10) and shortest
3803 // round-trip digits. Split it into the digit string `s` and exponent `E`.
3804 let sci = format!("{a:e}");
3805 let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
3806 let e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
3807 let s: String = mant.chars().filter(|c| *c != '.').collect();
3808 let k = s.len() as i32; // number of significant digits
3809 let n = e + 1; // value = s × 10^(n−k), 10^(k−1) ≤ s < 10^k
3810
3811 if k <= n && n <= 21 {
3812 // Integer with trailing zeros: all digits, then n−k zeros.
3813 let mut out = s;
3814 out.push_str(&"0".repeat((n - k) as usize));
3815 out
3816 } else if 0 < n && n <= 21 {
3817 // Decimal point inside the digit run: n digits, '.', the rest.
3818 format!("{}.{}", &s[..n as usize], &s[n as usize..])
3819 } else if -6 < n && n <= 0 {
3820 // Leading "0." then (−n) zeros then all digits.
3821 format!("0.{}{}", "0".repeat((-n) as usize), s)
3822 } else {
3823 // Exponential form. Exponent digit is n−1, always signed.
3824 let exp = n - 1;
3825 let sign = if exp >= 0 { '+' } else { '-' };
3826 let mag = exp.abs();
3827 if k == 1 {
3828 format!("{s}e{sign}{mag}")
3829 } else {
3830 format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
3831 }
3832 }
3833}
3834
3835impl JsHost {
3836 /// The `typeof` string for `v`.
3837 pub fn type_of(&self, v: &Value) -> &'static str {
3838 match v {
3839 Value::Undef => "undefined",
3840 Value::Bool(_) => "boolean",
3841 Value::Int(_) | Value::Float(_) => "number",
3842 Value::Str(_) => "string",
3843 Value::Obj(_) => match self.get(v) {
3844 Some(JsObj::Str(_)) => "string",
3845 // 10.5's `[[Call]]` slot exists on a proxy exactly when its
3846 // target is callable, so `typeof` classifies by the target —
3847 // `typeof new Proxy(function(){}, {})` is `'function'`. The walk
3848 // is bounded: a proxy of a proxy defers again.
3849 Some(JsObj::Proxy { target, .. }) => {
3850 let mut cur = target;
3851 for _ in 0..100 {
3852 match self.get(cur) {
3853 Some(JsObj::Proxy { target: t, .. }) => cur = t,
3854 _ => break,
3855 }
3856 }
3857 if is_callable(self, cur) {
3858 "function"
3859 } else {
3860 "object"
3861 }
3862 }
3863 Some(JsObj::Func(_))
3864 | Some(JsObj::BoundMethod { .. })
3865 | Some(JsObj::BoundFunc { .. })
3866 | Some(JsObj::Class(_)) => "function",
3867 // A Builtin is a callable (`Array`, `parseInt`, `Math.floor`) —
3868 // `typeof === "function"` — EXCEPT the non-callable namespace
3869 // objects (`Math`, `JSON`, `require('fs')`, …) which are "object".
3870 Some(JsObj::Builtin(n)) => {
3871 if builtin_is_callable(n) {
3872 "function"
3873 } else {
3874 "object"
3875 }
3876 }
3877 Some(JsObj::Symbol { .. }) => "symbol",
3878 Some(JsObj::BigInt(_)) => "bigint",
3879 _ => "object", // arrays, objects, null, Map/Set, generators
3880 },
3881 _ => "object",
3882 }
3883 }
3884
3885 /// JS truthiness: false / 0 / -0 / NaN / "" / null / undefined are falsy.
3886 pub fn truthy(&self, v: &Value) -> bool {
3887 match v {
3888 Value::Undef => false,
3889 Value::Bool(b) => *b,
3890 Value::Int(n) => *n != 0,
3891 Value::Float(f) => *f != 0.0 && !f.is_nan(),
3892 Value::Str(s) => !s.is_empty(),
3893 Value::Obj(_) => match self.get(v) {
3894 Some(JsObj::Str(s)) => !s.is_empty(),
3895 Some(JsObj::Null) => false,
3896 Some(JsObj::BigInt(b)) => !num_traits::Zero::is_zero(b),
3897 _ => true, // arrays, objects, functions
3898 },
3899 _ => true,
3900 }
3901 }
3902
3903 /// Coerce to a number (`ToNumber`): the arithmetic-context conversion.
3904 pub fn to_number(&self, v: &Value) -> f64 {
3905 match v {
3906 Value::Undef => f64::NAN,
3907 Value::Bool(b) => {
3908 if *b {
3909 1.0
3910 } else {
3911 0.0
3912 }
3913 }
3914 Value::Int(n) => *n as f64,
3915 Value::Float(f) => *f,
3916 Value::Str(s) => str_to_number(s),
3917 Value::Obj(_) => match self.get(v) {
3918 Some(JsObj::Str(s)) => str_to_number(s),
3919 Some(JsObj::Null) => 0.0,
3920 Some(JsObj::BigInt(b)) => bigint_to_f64(b),
3921 Some(JsObj::Array(items)) => {
3922 // [] -> 0, [x] -> ToNumber(x), else NaN.
3923 if items.is_empty() {
3924 0.0
3925 } else if items.len() == 1 {
3926 self.to_number(&items[0])
3927 } else {
3928 f64::NAN
3929 }
3930 }
3931 _ => f64::NAN,
3932 },
3933 _ => f64::NAN,
3934 }
3935 }
3936
3937 /// `String(v)` — the string-coercion form (raw, unquoted).
3938 pub fn str_of(&self, v: &Value) -> String {
3939 match v {
3940 Value::Undef => "undefined".into(),
3941 Value::Bool(b) => if *b { "true" } else { "false" }.into(),
3942 Value::Int(n) => n.to_string(),
3943 Value::Float(f) => fmt_number(*f),
3944 Value::Str(s) => (**s).clone(),
3945 Value::Obj(_) => match self.get(v) {
3946 Some(JsObj::Str(s)) => s.clone(),
3947 Some(JsObj::Null) => "null".into(),
3948 Some(JsObj::BigInt(b)) => b.to_string(),
3949 Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
3950 Some(JsObj::Array(items)) => {
3951 // Array.prototype.toString: comma-join, null/undefined -> "".
3952 // Guarded by the JoinStack (see `join_stack_push`) so a
3953 // self-referential array yields "" instead of recursing until
3954 // the native stack aborts the process.
3955 if !join_stack_push(v) {
3956 return String::new();
3957 }
3958 let parts: Vec<String> = items
3959 .iter()
3960 .map(|x| match x {
3961 Value::Undef => String::new(),
3962 _ if self.is_null(x) => String::new(),
3963 _ => self.str_of(x),
3964 })
3965 .collect();
3966 join_stack_pop();
3967 parts.join(",")
3968 }
3969 Some(JsObj::Object(props)) => {
3970 // A native `Buffer` stringifies to its decoded (utf-8)
3971 // contents, matching `buf.toString()` — needed for `'' + buf`,
3972 // template interpolation, and `data += chunk` (the pattern
3973 // Express/body-parser use to read a request body).
3974 if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Buffer") {
3975 let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
3976 Some(JsObj::Array(items)) => {
3977 items.iter().map(|x| self.to_number(x) as u8).collect()
3978 }
3979 _ => Vec::new(),
3980 };
3981 String::from_utf8_lossy(&bytes).into_owned()
3982 } else if let Some(s) = self.error_to_string(v) {
3983 s
3984 } else {
3985 "[object Object]".into()
3986 }
3987 }
3988 Some(JsObj::Func(f)) => {
3989 // A function built from runtime source (`new Function`,
3990 // `vm.compileFunction`) retains the exact text V8 synthesizes
3991 // for it, so `Function.prototype.toString` reports what Node
3992 // reports. Every other function slices its span out of the
3993 // script it was parsed from; only one whose program kept no
3994 // text (an AOT image, a `rust { }` desugared file) falls back
3995 // to the placeholder.
3996 if let Some(src) = self.fn_prop(v, "@@source") {
3997 return self.str_of(&src);
3998 }
3999 if let Some(text) = self.func_source(f.def_id) {
4000 return text.to_string();
4001 }
4002 let name = self
4003 .funcs
4004 .get(f.def_id)
4005 .map(|d| d.name.clone())
4006 .unwrap_or_default();
4007 format!("function {name}() {{ [code] }}")
4008 }
4009 // The native-code form names the FUNCTION, not its key:
4010 // `String(Math.max)` is `function max() { [native code] }`.
4011 Some(JsObj::Builtin(n)) => {
4012 // The `console` methods are the exception node itself makes:
4013 // each is a wrapper, so `String(console.log)` is the
4014 // ANONYMOUS native-code form even though `console.log.name`
4015 // is `log`. Measured on v26.8.1.
4016 if n.starts_with("console.") {
4017 "function () { [native code] }".into()
4018 } else if let Some(accessor) = crate::builtins::proto_getter_name(n) {
4019 // An accessor half names itself `get size` / `set
4020 // arguments`, which `builtin_name` cannot build because
4021 // it returns a borrowed `&str`.
4022 format!("function {accessor}() {{ [native code] }}")
4023 } else {
4024 format!(
4025 "function {}() {{ [native code] }}",
4026 crate::builtins::builtin_name(n)
4027 )
4028 }
4029 }
4030 // A method read off an instance names itself the same way the
4031 // prototype method it resolves to does: `String([].slice)` is
4032 // `function slice() { [native code] }`.
4033 Some(JsObj::BoundMethod { name, .. }) => {
4034 format!("function {name}() {{ [native code] }}")
4035 }
4036 Some(JsObj::BoundFunc { .. }) => "function () { [native code] }".into(),
4037 // `Function.prototype.toString` refuses to expose a proxy's
4038 // target: V8 reports the native-code form for a proxy of ANY
4039 // callable, so `String(new Proxy(function f(){}, {}))` is
4040 // `function () { [native code] }`, not `f`'s source.
4041 Some(JsObj::Proxy { .. }) if is_callable(self, v) => {
4042 "function () { [native code] }".into()
4043 }
4044 Some(JsObj::Class(c)) => match c.source_def.and_then(|d| self.func_source(d)) {
4045 Some(text) => text.to_string(),
4046 None => format!("class {} {{ }}", c.name),
4047 },
4048 Some(JsObj::Symbol { desc, .. }) => {
4049 // `String(sym)` is allowed (unlike implicit coercion) and yields
4050 // `Symbol(desc)`.
4051 match desc {
4052 Some(d) => format!("Symbol({d})"),
4053 None => "Symbol()".into(),
4054 }
4055 }
4056 _ => "[object Object]".into(),
4057 },
4058 _ => "[object Object]".into(),
4059 }
4060 }
4061
4062 /// The `Symbol.toStringTag` string `util.inspect` renders as a `[Tag]`
4063 /// prefix. V8 suppresses the tag when it is an OWN ENUMERABLE property,
4064 /// because it is then already listed as a `Symbol(Symbol.toStringTag): …`
4065 /// entry and showing it twice would be wrong.
4066 ///
4067 /// Only a DATA property is seen. A tag supplied by a prototype getter
4068 /// (`class C { get [Symbol.toStringTag]() { … } }`) would need a JS call,
4069 /// which cannot run under the host borrow `inspect` holds — such an object
4070 /// prints without the prefix.
4071 /// `[String: 'ab']` / `[Number: 1]` / `[Boolean: false]` — how node renders
4072 /// a primitive wrapper, distinguishing it from the bare primitive.
4073 fn inspect_wrapper(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Option<String> {
4074 let prim = match self.get(v) {
4075 Some(JsObj::Object(p)) => p.get("@@primitive").cloned()?,
4076 _ => return None,
4077 };
4078 let ctor = match &prim {
4079 Value::Bool(_) => "Boolean",
4080 Value::Int(_) | Value::Float(_) => "Number",
4081 // BigInt and Symbol primitives live on the heap; their boxes are
4082 // `[BigInt: 1n]` and `[Symbol: Symbol(s)]`.
4083 _ => match self.get(&prim) {
4084 Some(JsObj::BigInt(_)) => "BigInt",
4085 Some(JsObj::Symbol { .. }) => "Symbol",
4086 _ => "String",
4087 },
4088 };
4089 let head = format!("[{ctor}: {}]", self.inspect_lvl(&prim, indent, st));
4090 // Extra own properties still print, as `[String: 'ab'] { tag: 1 }`. The
4091 // boxed characters are NOT extras — node hides the index properties of
4092 // a String wrapper, showing only what was added to it.
4093 let width = if ctor == "String" {
4094 self.str_of(&prim).chars().count()
4095 } else {
4096 0
4097 };
4098 let extras: Vec<String> = match self.get(v) {
4099 Some(JsObj::Object(p)) => p
4100 .iter()
4101 .filter(|(k, _)| {
4102 !k.starts_with("@@")
4103 && !k.starts_with('#')
4104 && self.prop_attrs(v, k).enumerable
4105 && !k.parse::<usize>().is_ok_and(|i| i < width)
4106 })
4107 .map(|(k, val)| {
4108 format!("{}: {}", fmt_key(k), self.inspect_lvl(val, indent + 2, st))
4109 })
4110 .collect(),
4111 _ => Vec::new(),
4112 };
4113 if extras.is_empty() {
4114 return Some(head);
4115 }
4116 Some(self.render_object(&extras, &format!("{head} "), indent, st))
4117 }
4118
4119 /// The `key: value` parts for own properties a script attached to an exotic
4120 /// whose contents are internal slots — `new Map([['k',1]])` with `m.x = 5`
4121 /// prints `Map(1) { 'k' => 1, x: 5 }`.
4122 fn side_table_parts(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Vec<String> {
4123 self.fn_prop_keys(v)
4124 .into_iter()
4125 .filter(|k| {
4126 !k.starts_with("@@")
4127 && !k.starts_with('#')
4128 && !is_symbol_key(k)
4129 && self.prop_attrs(v, k).enumerable
4130 })
4131 .map(|k| {
4132 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4133 format!(
4134 "{}: {}",
4135 fmt_key(&k),
4136 self.inspect_lvl(&val, indent + 2, st)
4137 )
4138 })
4139 .collect()
4140 }
4141
4142 fn inspect_tag(&self, v: &Value) -> Option<String> {
4143 let own = matches!(self.get(v), Some(JsObj::Object(p)) if p.contains_key("@@toStringTag"));
4144 if own && self.prop_attrs(v, "@@toStringTag").enumerable {
4145 return None;
4146 }
4147 let t = lookup_chain(self, v, "@@toStringTag")?;
4148 self.as_str(&t)
4149 }
4150
4151 /// `console.log`-style rendering of a top-level argument: bare strings print
4152 /// raw; everything else uses `inspect`.
4153 pub fn console_format(&self, v: &Value) -> String {
4154 match v {
4155 Value::Str(_) => self.str_of(v),
4156 Value::Obj(_) if matches!(self.get(v), Some(JsObj::Str(_))) => self.str_of(v),
4157 _ => self.inspect(v),
4158 }
4159 }
4160
4161 /// `util.inspect`-style rendering (nested; strings quoted).
4162 pub fn inspect(&self, v: &Value) -> String {
4163 self.inspect_lvl(v, 0, &mut InspectCycles::default())
4164 }
4165
4166 /// `util.inspect` at a given indentation level, with the cycle guard applied
4167 /// around the object cases.
4168 ///
4169 /// A value already being rendered further up the chain is a CYCLE, and Node
4170 /// marks both ends of it: the back-edge prints `[Circular *N]` and the
4171 /// object it points back at is prefixed `<ref *N>`. Without this the walk
4172 /// only stopped when the depth limit turned the back-edge into `[Object]`,
4173 /// so `const c={a:1}; c.c=c` printed the misleading
4174 /// `{ a: 1, c: { a: 1, c: { a: 1, c: [Object] } } }` instead of
4175 /// `<ref *1> { a: 1, c: [Circular *1] }`.
4176 ///
4177 /// The `*N` id is only assigned when the back-edge is reached, i.e. while
4178 /// the target's own children are being rendered — so the prefix can only be
4179 /// decided after `inspect_value` returns.
4180 /// Whether `v` renders as a LEAF — a finished string produced without
4181 /// recursing into any child.
4182 ///
4183 /// Node assigns `ctx.currentDepth = recurseTimes` in `formatRaw`, but only
4184 /// after the early returns for the shapes that answer immediately: a bare
4185 /// Date is its ISO string, a regex is its literal, an empty container is its
4186 /// braces, and a Buffer is whatever its `[util.inspect.custom]` says. None of
4187 /// those record a depth, so a group containing one is not pushed over the
4188 /// `compact` threshold by it — `util.inspect([new Date(0), null], { compact:
4189 /// 1 })` stays on one line. Charging them a level broke exactly those groups.
4190 fn renders_without_expanding(&self, v: &Value) -> bool {
4191 let plain_props = |p: &IndexMap<String, Value>| {
4192 p.keys().all(|k| k.starts_with("@@") || k.starts_with('#'))
4193 };
4194 match self.get(v) {
4195 // A regex never recurses, with or without its hidden `lastIndex`.
4196 Some(JsObj::RegExp(_)) => true,
4197 Some(JsObj::Map { entries, .. }) => entries.is_empty(),
4198 Some(JsObj::Set { entries, .. }) => entries.is_empty(),
4199 Some(JsObj::Array(items)) => items.is_empty() && self.own_symbol_entries(v).is_empty(),
4200 Some(JsObj::Object(p)) => match p.get("@@native").map(|t| self.str_of(t)).as_deref() {
4201 Some("Buffer") => inspect_custom(),
4202 // Own properties added to a Date DO get expanded after it.
4203 Some("Date") => plain_props(p),
4204 Some(_) => false,
4205 None => plain_props(p) && self.own_symbol_entries(v).is_empty(),
4206 },
4207 _ => false,
4208 }
4209 }
4210
4211 fn inspect_lvl(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4212 if !matches!(v, Value::Obj(_)) {
4213 return self.inspect_value(v, indent, st);
4214 }
4215 if st.seen.iter().any(|p| self.strict_eq(p, v)) {
4216 return format!("[Circular *{}]", st.mark(self, v));
4217 }
4218 st.seen.push(v.clone());
4219 // Node ASSIGNS `ctx.currentDepth = recurseTimes` on entry to each value
4220 // it expands — not a running maximum — so after the children have been
4221 // rendered it holds the depth of the last chain below this group, which
4222 // is what `reduceToSingleString` compares. A value the depth limit
4223 // stubs out as `[Object]` is never expanded and must not count, or an
4224 // object whose deepest level was elided would break where node joins.
4225 // Only a value node actually EXPANDS advances the depth. A string,
4226 // symbol or bigint is a JS primitive that this host happens to store on
4227 // the heap, so it reaches here as `Value::Obj` where an unboxed number
4228 // returns above — and counting it as a level made any group holding one
4229 // look deeper than it was. Under `compact: 1` that is the difference
4230 // between node's `Map(2) { 'k2' => 8, 'j' => 5 }` and breaking the same
4231 // Map across four lines, because its string KEYS were being charged a
4232 // nesting level.
4233 if indent as i64 <= inspect_indent_limit()
4234 && !is_primitive(self, v)
4235 && !self.renders_without_expanding(v)
4236 {
4237 st.deepest = indent;
4238 }
4239 let body = self.inspect_value(v, indent, st);
4240 st.seen.pop();
4241 match st.id_of(self, v) {
4242 Some(id) => format!("<ref *{id}> {body}"),
4243 None => body,
4244 }
4245 }
4246
4247 /// The rendering itself, once `inspect_lvl` has established that `v` is not
4248 /// a back-edge into an object already on the stack.
4249 fn inspect_value(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4250 if let Some(s) = self.inspect_wrapper(v, indent, st) {
4251 return s;
4252 }
4253 match v {
4254 Value::Undef => "undefined".into(),
4255 Value::Bool(b) => if *b { "true" } else { "false" }.into(),
4256 Value::Int(n) => n.to_string(),
4257 // `util.inspect` distinguishes negative zero; `String(-0)` does not.
4258 Value::Float(f) if *f == 0.0 && f.is_sign_negative() => "-0".into(),
4259 Value::Float(f) => fmt_number(*f),
4260 Value::Str(s) => quote_str(s),
4261 Value::Obj(_) => match self.get(v) {
4262 Some(JsObj::Str(s)) => quote_str(s),
4263 Some(JsObj::Null) => "null".into(),
4264 // `util.inspect` renders a bigint with the `n` suffix, a regex bare.
4265 Some(JsObj::BigInt(b)) => format!("{b}n"),
4266 // `lastIndex` is a non-enumerable own property of every regex,
4267 // so `showHidden` (and therefore `%o`) appends it:
4268 // `/x/g { [lastIndex]: 0 }`.
4269 Some(JsObj::RegExp(r)) => {
4270 let body = format!("/{}/{}", r.source, r.flags);
4271 if inspect_show_hidden() {
4272 format!("{body} {{ [lastIndex]: {} }}", r.last_index.get())
4273 } else {
4274 body
4275 }
4276 }
4277 // `util.inspect` on node v26.7.0 renders a proxy as
4278 // `Proxy(<target>)` — the target's own rendering, wrapped. It
4279 // deliberately does NOT run the handler's traps, so this stays a
4280 // pure `&self` read like every other inspect arm.
4281 Some(JsObj::Proxy { target, .. }) => {
4282 format!("Proxy({})", self.inspect_lvl(target, indent, st))
4283 }
4284 // `arguments` is backed by an Array but is an ordinary-shaped
4285 // exotic to util.inspect: node prints its indices as quoted
4286 // keys under the `[Arguments]` tag, `[Arguments] { '0': 1 }`.
4287 Some(JsObj::Array(items)) if crate::builtins::is_arguments_h(self, v) => {
4288 if indent as i64 > inspect_indent_limit() {
4289 return "[Arguments]".into();
4290 }
4291 let mut inner: Vec<String> = items
4292 .iter()
4293 .enumerate()
4294 .map(|(i, x)| format!("'{i}': {}", self.inspect_lvl(x, indent + 2, st)))
4295 .collect();
4296 for k in self.fn_prop_keys(v) {
4297 if k.starts_with("@@") || !self.prop_attrs(v, &k).enumerable {
4298 continue;
4299 }
4300 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4301 inner.push(format!(
4302 "{}: {}",
4303 fmt_key(&k),
4304 self.inspect_lvl(&val, indent + 2, st)
4305 ));
4306 }
4307 if inner.is_empty() {
4308 return "[Arguments] {}".into();
4309 }
4310 self.render_object(&inner, "[Arguments] ", indent, st)
4311 }
4312 Some(JsObj::Array(items)) => {
4313 // Own enumerable non-index string props (e.g. a `str.match(re)`
4314 // result's `index`/`input`/`groups`, or a user-assigned
4315 // `arr.foo`) render after the elements, as `key: value`.
4316 let prop_keys: Vec<String> = self
4317 .fn_prop_keys(v)
4318 .into_iter()
4319 .filter(|k| {
4320 !k.starts_with("@@")
4321 && !k.starts_with('#')
4322 && self.prop_attrs(v, k).enumerable
4323 })
4324 .collect();
4325 // An own enumerable SYMBOL-keyed property renders after the
4326 // string keys as `Symbol(desc): value`, as it does on an
4327 // object receiver.
4328 // An instance of an Array SUBCLASS leads with its
4329 // constructor and length, `Bar(2) [ 1, 2 ]`, as node's
4330 // `getPrefix` does for any non-`Array` constructor.
4331 let sub = match self.proto_of(v) {
4332 Some(_) => self.ctor_name(v),
4333 None => String::new(),
4334 };
4335 let base = if sub.is_empty() || sub == "Array" {
4336 String::new()
4337 } else {
4338 format!("{sub}({}) ", items.len())
4339 };
4340 let sym_entries = self.own_symbol_entries(v);
4341 // Under `showHidden` even an empty array has something to
4342 // show — node prints `[ [length]: 0 ]`, not `[]`.
4343 if items.is_empty()
4344 && prop_keys.is_empty()
4345 && sym_entries.is_empty()
4346 && !inspect_show_hidden()
4347 {
4348 return format!("{base}[]");
4349 }
4350 // Node's default inspect depth is 2 (root = depth 0); deeper
4351 // nesting collapses to `[Array]`. indent grows by 2 per level.
4352 if indent as i64 > inspect_indent_limit() {
4353 return "[Array]".into();
4354 }
4355 // `util.inspect`'s `maxArrayLength` (default 100): only the
4356 // first 100 elements are formatted, and the rest collapse to
4357 // a `... N more items` entry. Without the cap a 120-element
4358 // array printed all 120 — and, because the grid column width
4359 // is computed from what is SHOWN, every column was also one
4360 // character wider than node's.
4361 // A SPARSE array takes node's `formatSpecialArray` path: an
4362 // elided run renders as `<N empty items>` rather than as the
4363 // `undefined` it reads back as.
4364 let (mut inner, has_tail) = if self.has_holes(v) {
4365 self.inspect_sparse(v, items, indent, st)
4366 } else {
4367 let shown = items.len().min(inspect_max_array_length());
4368 let mut inner: Vec<String> = items[..shown]
4369 .iter()
4370 .map(|x| self.inspect_lvl(x, indent + 2, st))
4371 .collect();
4372 let remaining = items.len() - shown;
4373 if remaining > 0 {
4374 let unit = if remaining == 1 { "item" } else { "items" };
4375 inner.push(format!("... {remaining} more {unit}"));
4376 }
4377 (inner, remaining > 0)
4378 };
4379 // `showHidden` exposes the non-enumerable `length`, which an
4380 // array always has. It sorts BEFORE any own property node
4381 // shows (`[ 1, [length]: 1, x: 2 ]`) and, being an entry
4382 // rather than an element, it also turns the column grid off —
4383 // which is why a ten-element array under `showHidden` prints
4384 // on one line rather than as a grid.
4385 let show_hidden = inspect_show_hidden();
4386 if show_hidden {
4387 inner.push(format!("[length]: {}", items.len()));
4388 }
4389 let has_props = show_hidden || !prop_keys.is_empty() || !sym_entries.is_empty();
4390 for k in &prop_keys {
4391 let val = self.fn_prop(v, k).unwrap_or(Value::Undef);
4392 inner.push(format!(
4393 "{}: {}",
4394 fmt_key(k),
4395 self.inspect_lvl(&val, indent + 2, st)
4396 ));
4397 }
4398 for (k, val) in &sym_entries {
4399 let label = match self.symbol_of_key(k) {
4400 Some(s) => self.inspect(&s),
4401 None => continue,
4402 };
4403 inner.push(format!(
4404 "{label}: {}",
4405 self.inspect_lvl(val, indent + 2, st)
4406 ));
4407 }
4408 self.render_array(
4409 &inner,
4410 items,
4411 indent,
4412 ArrayLayout {
4413 has_props,
4414 has_tail,
4415 base: &base,
4416 },
4417 st,
4418 )
4419 }
4420 // `URLSearchParams` renders its pairs, not its slots:
4421 // `URLSearchParams { 'a' => '1', 'b' => '2' }`. Keys repeat,
4422 // which is why it is a pair list rather than a Map rendering.
4423 Some(JsObj::Object(props))
4424 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4425 == Some("URLSearchParams") =>
4426 {
4427 let pairs: Vec<Value> = match props.get("@@pairs").and_then(|a| self.get(a)) {
4428 Some(JsObj::Array(items)) => items.clone(),
4429 _ => Vec::new(),
4430 };
4431 if pairs.is_empty() {
4432 return "URLSearchParams {}".into();
4433 }
4434 let inner: Vec<String> = pairs
4435 .iter()
4436 .filter_map(|kv| match self.get(kv) {
4437 Some(JsObj::Array(p)) if p.len() == 2 => Some(format!(
4438 "{} => {}",
4439 self.inspect_lvl(&p[0], indent + 2, st),
4440 self.inspect_lvl(&p[1], indent + 2, st)
4441 )),
4442 _ => None,
4443 })
4444 .collect();
4445 self.render_object(&inner, "URLSearchParams ", indent, st)
4446 }
4447 // A typed array renders as `Uint8Array(3) [ 1, 2, 3 ]` — its
4448 // constructor and length, then the elements laid out exactly as
4449 // an array's. Without this it fell through to the generic object
4450 // arm and printed the `{ length, byteLength, byteOffset,
4451 // BYTES_PER_ELEMENT }` bookkeeping instead of the CONTENTS,
4452 // which is the whole reason anyone logs one.
4453 Some(JsObj::Object(props))
4454 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4455 == Some("TypedArray") =>
4456 {
4457 let kind = props
4458 .get("@@kind")
4459 .map(|k| self.str_of(k))
4460 .unwrap_or_else(|| "TypedArray".into());
4461 // Rendered as STRINGS: a 64-bit view's elements are BigInts,
4462 // which this shared borrow cannot allocate as values.
4463 let elems = crate::stdlib::typedarray::elems_display(self, v);
4464 // The grid layout sizes its columns from the VALUES; a
4465 // 64-bit view's come back as `undefined` (no allocation is
4466 // possible here), which only affects column padding.
4467 let vals = crate::stdlib::typedarray::elems_with_host(self, v);
4468 let base = format!("{kind}({}) ", elems.len());
4469 if indent as i64 > inspect_indent_limit() {
4470 return format!("[{kind}]");
4471 }
4472 let shown = elems.len().min(inspect_max_array_length());
4473 let mut inner: Vec<String> = elems[..shown].to_vec();
4474 let remaining = elems.len() - shown;
4475 if remaining > 0 {
4476 let unit = if remaining == 1 { "item" } else { "items" };
4477 inner.push(format!("... {remaining} more {unit}"));
4478 }
4479 // A view's whole identity — its element width, its window
4480 // onto the backing store, and the store itself — is
4481 // non-enumerable, so `showHidden` is the only way to see it.
4482 // `util.format('%o', view)` goes through here, since `%o`
4483 // implies `showHidden`.
4484 let show_hidden = inspect_show_hidden();
4485 if show_hidden {
4486 let bpe = crate::stdlib::typedarray::bytes_per_element(&kind);
4487 let byte_offset = props
4488 .get("byteOffset")
4489 .map(|x| self.to_number(x))
4490 .unwrap_or(0.0);
4491 inner.push(format!("[BYTES_PER_ELEMENT]: {bpe}"));
4492 inner.push(format!("[length]: {}", elems.len()));
4493 inner.push(format!("[byteLength]: {}", elems.len() * bpe));
4494 inner.push(format!("[byteOffset]: {}", fmt_number(byte_offset)));
4495 // An ArrayBuffer reached AS a view's backing store is
4496 // rendered by node WITHOUT its contents — just
4497 // `ArrayBuffer { [byteLength]: N }` — even though the
4498 // same buffer inspected on its own leads with
4499 // `[Uint8Contents]`. Recursing through the normal
4500 // ArrayBuffer branch therefore printed the bytes twice,
4501 // once as the view's elements and again as the store's.
4502 let buf_len = props
4503 .get("@@buffer")
4504 .and_then(|b| self.get(b))
4505 .and_then(|o| match o {
4506 JsObj::Object(bp) => bp.get("@@bytes").cloned(),
4507 _ => None,
4508 })
4509 .and_then(|b| {
4510 self.get(&b).map(|o| match o {
4511 JsObj::Array(items) => items.len(),
4512 _ => 0,
4513 })
4514 })
4515 .unwrap_or(0);
4516 inner.push(format!(
4517 "[buffer]: ArrayBuffer {{ [byteLength]: {buf_len} }}"
4518 ));
4519 }
4520 self.render_array(
4521 &inner,
4522 &vals,
4523 indent,
4524 ArrayLayout {
4525 has_props: show_hidden,
4526 has_tail: remaining > 0,
4527 base: &base,
4528 },
4529 st,
4530 )
4531 }
4532 // An `ArrayBuffer` renders its CONTENTS, which is the only way
4533 // to see them — it exposes no indices of its own:
4534 // `ArrayBuffer { [Uint8Contents]: <00 01>, [byteLength]: 2 }`.
4535 Some(JsObj::Object(props))
4536 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4537 == Some("ArrayBuffer") =>
4538 {
4539 let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4540 Some(JsObj::Array(items)) => {
4541 items.iter().map(|x| self.to_number(x) as u8).collect()
4542 }
4543 _ => Vec::new(),
4544 };
4545 let hex: Vec<String> = bytes.iter().map(|b| format!("{b:02x}")).collect();
4546 let mut parts = vec![
4547 format!("[Uint8Contents]: <{}>", hex.join(" ")),
4548 format!("[byteLength]: {}", bytes.len()),
4549 ];
4550 if props.contains_key("@@maxByteLength") {
4551 let max = props
4552 .get("@@maxByteLength")
4553 .map(|m| self.to_number(m))
4554 .unwrap_or(0.0);
4555 parts.insert(1, format!("maxByteLength: {}", fmt_number(max)));
4556 }
4557 self.render_object(&parts, "ArrayBuffer ", indent, st)
4558 }
4559 // A `Date` renders as its ISO-8601 form. Its time value lives in
4560 // the internal `@@ms` slot, which the generic object branch below
4561 // does not show, so without this arm every Date printed as `{}` —
4562 // including through `console.log(d)`, inside arrays, objects and
4563 // Maps, and in an `assert` diff.
4564 Some(JsObj::Object(props))
4565 if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Date") =>
4566 {
4567 let base = crate::stdlib::date::inspect_with_host(self, v);
4568 // Own properties added to a Date follow the date itself, the
4569 // way node appends them: `2020-01-01T00:00:00.000Z { x: 1 }`.
4570 let extra = self.side_table_parts(v, indent, st);
4571 let mut inner: Vec<String> = props
4572 .iter()
4573 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
4574 .map(|(k, val)| format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)))
4575 .collect();
4576 inner.extend(extra);
4577 if inner.is_empty() {
4578 return base;
4579 }
4580 self.render_object(&inner, &format!("{base} "), indent, st)
4581 }
4582 // A `Buffer` renders as `<Buffer 01 02 03>` — hex bytes, capped
4583 // at 50 with a `... N more byte(s)` tail, exactly as
4584 // `util.inspect` does. Without this a `console.log(buf)` (the
4585 // single most common thing anyone does with a Buffer) printed
4586 // the internal `{ length, byteLength, … }` bookkeeping.
4587 Some(JsObj::Object(props))
4588 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4589 == Some("Buffer") =>
4590 {
4591 let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4592 Some(JsObj::Array(items)) => {
4593 items.iter().map(|x| self.to_number(x) as u8).collect()
4594 }
4595 _ => Vec::new(),
4596 };
4597 // `<Buffer …>` is Buffer's `[util.inspect.custom]` hook, not
4598 // the shape of the object. Under `customInspect: false` node
4599 // does not call that hook and falls back to the generic
4600 // byte-view rendering — which is what an `assert` diff shows,
4601 // since assert inspects with the hook disabled so that a
4602 // failure names the differing BYTE rather than two opaque hex
4603 // blobs. The constructor is `Buffer` while the brand is still
4604 // `Uint8Array`, so node prints both.
4605 if !inspect_custom() {
4606 let base = format!("Buffer({}) [Uint8Array] ", bytes.len());
4607 if indent as i64 > inspect_indent_limit() {
4608 return "[Buffer [Uint8Array]]".into();
4609 }
4610 let shown = bytes.len().min(inspect_max_array_length());
4611 let mut inner: Vec<String> =
4612 bytes[..shown].iter().map(|b| b.to_string()).collect();
4613 let vals: Vec<Value> = bytes[..shown]
4614 .iter()
4615 .map(|b| Value::Float(*b as f64))
4616 .collect();
4617 let remaining = bytes.len() - shown;
4618 if remaining > 0 {
4619 let unit = if remaining == 1 { "item" } else { "items" };
4620 inner.push(format!("... {remaining} more {unit}"));
4621 }
4622 return self.render_array(
4623 &inner,
4624 &vals,
4625 indent,
4626 ArrayLayout {
4627 has_props: false,
4628 has_tail: remaining > 0,
4629 base: &base,
4630 },
4631 st,
4632 );
4633 }
4634 const MAX: usize = 50;
4635 let shown: Vec<String> =
4636 bytes.iter().take(MAX).map(|b| format!("{b:02x}")).collect();
4637 let mut out = format!("<Buffer {}", shown.join(" "));
4638 if bytes.len() > MAX {
4639 let more = bytes.len() - MAX;
4640 let unit = if more == 1 { "byte" } else { "bytes" };
4641 out.push_str(&format!(" ... {more} more {unit}"));
4642 }
4643 out.push('>');
4644 out
4645 }
4646 // An Error inspects as its `.stack` — never as an object literal
4647 // exposing the internal `message`/`stack` slots. Any own property
4648 // a script added beyond those follows in braces, as V8 renders
4649 // it: `Error: x\n at … { code: 'C' }`.
4650 Some(JsObj::Object(_)) if self.error_to_string(v).is_some() => {
4651 let mut stack = lookup_chain(self, v, "stack")
4652 .map(|s| self.str_of(&s))
4653 .unwrap_or_else(|| self.error_to_string(v).unwrap_or_default());
4654 // A `DOMException` prints its CLASS and then its name —
4655 // `DOMException [AbortError]: m` — where a plain error
4656 // prints only its stack head.
4657 if let Some(JsObj::Object(p)) = self.get(v) {
4658 if let Some(n) = p.get("@@domName") {
4659 let name = self.str_of(n);
4660 stack = format!(
4661 "DOMException [{name}]{}",
4662 stack.strip_prefix(&name).unwrap_or(&stack)
4663 );
4664 }
4665 }
4666 let extra: Vec<String> = self
4667 .own_enum_key_names(v)
4668 .into_iter()
4669 .filter(|k| k != "name")
4670 .map(|k| {
4671 let val = self.fn_prop(v, &k).unwrap_or_else(|| match self.get(v) {
4672 Some(JsObj::Object(p)) => {
4673 p.get(&k).cloned().unwrap_or(Value::Undef)
4674 }
4675 _ => Value::Undef,
4676 });
4677 format!(
4678 "{}: {}",
4679 fmt_key(&k),
4680 self.inspect_lvl(&val, indent + 2, st)
4681 )
4682 })
4683 .collect();
4684 if extra.is_empty() {
4685 stack
4686 } else {
4687 format!("{stack} {{ {} }}", extra.join(", "))
4688 }
4689 }
4690 Some(JsObj::Object(props)) => {
4691 // Instances print with their constructor name as a prefix
4692 // (`C { x: 1 }`); plain objects have none; a null-prototype
4693 // object (e.g. an `Object.groupBy` result) is tagged
4694 // `[Object: null prototype]`.
4695 let ctor = match self.ctor_name(v) {
4696 n if n.is_empty() => "Object".to_string(),
4697 n => n,
4698 };
4699 let plain_prefix = if ctor == "Object" {
4700 String::new()
4701 } else {
4702 format!("{ctor} ")
4703 };
4704 let prefix = if self.inspects_null_proto(v) {
4705 "[Object: null prototype] ".to_string()
4706 } else {
4707 // An inherited `Symbol.toStringTag` shows as `Ctor [Tag] `.
4708 match self.inspect_tag(v) {
4709 Some(t) if t != ctor => format!("{ctor} [{t}] "),
4710 _ => plain_prefix.clone(),
4711 }
4712 };
4713 // Skip node-js's internal slots (`@@native`, `@@bytes`, …) and
4714 // private class fields; a real symbol-keyed own property is a
4715 // visible one and renders as `Symbol(desc): value`.
4716 // An own ACCESSOR has no value to print: node shows the
4717 // label `[Getter]` / `[Setter]` / `[Getter/Setter]` in its
4718 // place. It is found through the `@@ord:` marker the
4719 // property map holds for it, which is also what puts it in
4720 // declaration order among the data properties. Without this
4721 // an accessor rendered as nothing at all — `{ get z(){} }`
4722 // printed `{}`.
4723 let mut shown: Vec<(String, Result<&Value, &'static str>)> = props
4724 .iter()
4725 .filter_map(|(k, val)| match k.strip_prefix(ORD_MARKER) {
4726 Some(real) => {
4727 let attrs = self.prop_attrs(v, real);
4728 let label = match self.own_accessor(v, real)? {
4729 (Some(_), Some(_)) => "[Getter/Setter]",
4730 (Some(_), None) => "[Getter]",
4731 (None, Some(_)) => "[Setter]",
4732 (None, None) => return None,
4733 };
4734 attrs.enumerable.then(|| (fmt_key(real), Err(label)))
4735 }
4736 // Only an ENUMERABLE own property is shown, as node
4737 // does: a native instance keeps bookkeeping (a
4738 // `URLSearchParams`'s `size`) as a hidden own slot,
4739 // and printing it would report a spec getter as data.
4740 None if !k.starts_with("@@")
4741 && !k.starts_with('#')
4742 && self.prop_attrs(v, k).enumerable =>
4743 {
4744 Some((fmt_key(k), Ok(val)))
4745 }
4746 None => None,
4747 })
4748 .collect();
4749 shown.extend(props.iter().filter_map(|(k, val)| {
4750 let sym = self.symbol_of_key(k)?;
4751 self.prop_attrs(v, k)
4752 .enumerable
4753 .then(|| (self.inspect(&sym), Ok(val)))
4754 }));
4755 if shown.is_empty() {
4756 return format!("{prefix}{{}}");
4757 }
4758 // Depth limit (Node default 2): deeper objects collapse to
4759 // `[Object]` (or `[ClassName]` for a named instance).
4760 if indent as i64 > inspect_indent_limit() {
4761 return if self.inspects_null_proto(v) {
4762 // Already bracketed (`[Object: null prototype]`).
4763 prefix.trim_end().to_string()
4764 } else if plain_prefix.is_empty() {
4765 "[Object]".into()
4766 } else {
4767 format!("[{}]", plain_prefix.trim_end())
4768 };
4769 }
4770 let inner: Vec<String> = shown
4771 .iter()
4772 .map(|(k, val)| match val {
4773 Ok(val) => format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)),
4774 Err(label) => format!("{k}: {label}"),
4775 })
4776 .collect();
4777 self.render_object(&inner, &prefix, indent, st)
4778 }
4779 Some(JsObj::Symbol { desc, .. }) => match desc {
4780 Some(d) => format!("Symbol({d})"),
4781 None => "Symbol()".into(),
4782 },
4783 Some(JsObj::Class(c)) => {
4784 let base = if c.parent.is_some() {
4785 let pname = c
4786 .parent
4787 .as_ref()
4788 .map(|p| self.callable_name(p))
4789 .unwrap_or_default();
4790 format!("[class {} extends {}]", c.name, pname)
4791 } else {
4792 format!("[class {}]", c.name)
4793 };
4794 self.with_callable_props(v, base, indent, st)
4795 }
4796 // A Map/Set renders its members at the NEXT nesting level, and
4797 // collapses to `[Map]`/`[Set]` past the depth limit exactly as an
4798 // array collapses to `[Array]`. Both used to recurse through
4799 // `inspect`, which restarts at indent 0, so the depth gate never
4800 // fired: nesting printed one level too deep at every depth
4801 // (measured on node v26.7.0, four nested Maps print
4802 // `Map(1) { 'a' => Map(1) { 'b' => Map(1) { 'c' => [Map] } } }`),
4803 // and a SELF-referential Map or Set recursed forever and aborted
4804 // the process — `const m=new Map(); m.set('m',m); console.log(m)`
4805 // died with `fatal runtime error: stack overflow`, which no
4806 // `try`/`catch` can see. An empty one still prints in full at any
4807 // depth, as `[]`/`{}` do.
4808 // A WEAK collection never shows its contents: node prints
4809 // `WeakMap { <items unknown> }` whether it holds anything or
4810 // not, because the entries are not enumerable by design.
4811 Some(JsObj::Map { weak: true, .. }) => "WeakMap { <items unknown> }".into(),
4812 Some(JsObj::Set { weak: true, .. }) => "WeakSet { <items unknown> }".into(),
4813 Some(JsObj::Map { entries, .. }) => {
4814 let extra = self.side_table_parts(v, indent, st);
4815 if entries.is_empty() && extra.is_empty() {
4816 return "Map(0) {}".into();
4817 }
4818 if indent as i64 > inspect_indent_limit() {
4819 return "[Map]".into();
4820 }
4821 let mut inner: Vec<String> = entries
4822 .values()
4823 .map(|(k, val)| {
4824 // Sequenced, not nested in one `format!`: both arms
4825 // need the same `&mut` cycle state.
4826 let ks = self.inspect_lvl(k, indent + 2, st);
4827 let vs = self.inspect_lvl(val, indent + 2, st);
4828 format!("{ks} => {vs}")
4829 })
4830 .collect();
4831 inner.extend(extra);
4832 // Laid out by the SAME routine as a plain object, not joined
4833 // onto one line unconditionally. `Map`/`Set` were the only
4834 // containers that never consulted `breakLength` or `compact`,
4835 // so every collection wide enough to wrap printed as one long
4836 // line: node breaks a seven-member Set of ten-character
4837 // strings across seven lines, and `util.inspect(m, {compact:
4838 // false})` — which assert's own diff renderer depends on —
4839 // could not break a Map at all. Node builds these through
4840 // `reduceToSingleString` with `braces[0]` of `Map(n) {`, which
4841 // is this `prefix` (the trailing space is the brace gap).
4842 let prefix = format!("Map({}) ", entries.len());
4843 self.render_object(&inner, &prefix, indent, st)
4844 }
4845 Some(JsObj::Set { entries, .. }) => {
4846 let extra = self.side_table_parts(v, indent, st);
4847 if entries.is_empty() && extra.is_empty() {
4848 return "Set(0) {}".into();
4849 }
4850 if indent as i64 > inspect_indent_limit() {
4851 return "[Set]".into();
4852 }
4853 let mut inner: Vec<String> = entries
4854 .values()
4855 .map(|v| self.inspect_lvl(v, indent + 2, st))
4856 .collect();
4857 inner.extend(extra);
4858 // Same layout routine as a Map (see above). Note node does
4859 // NOT column-group a wide Set the way it grids an array:
4860 // `groupArrayElements` is reached only from the list
4861 // formatter, so a 30-member Set is thirty lines.
4862 let prefix = format!("Set({}) ", entries.len());
4863 self.render_object(&inner, &prefix, indent, st)
4864 }
4865 Some(JsObj::Generator { .. }) => "Object [Generator] {}".into(),
4866 Some(JsObj::Promise { id }) => match self.promises.get(*id as usize) {
4867 Some(c) => match c.state {
4868 PromiseState::Pending => "Promise { <pending> }".into(),
4869 PromiseState::Fulfilled => {
4870 format!("Promise {{ {} }}", self.inspect_lvl(&c.value, 0, st))
4871 }
4872 PromiseState::Rejected => {
4873 format!(
4874 "Promise {{ <rejected> {} }}",
4875 self.inspect_lvl(&c.value, 0, st)
4876 )
4877 }
4878 },
4879 None => "Promise { <pending> }".into(),
4880 },
4881 Some(JsObj::Func(f)) => {
4882 // `callable_name`, not the FuncDef name: an anonymous
4883 // function expression gets its name by inference from the
4884 // binding it initialises (`const f = function(){}`), and
4885 // that lands as an own `name` property.
4886 let name = self.callable_name(v);
4887 // util.inspect labels a function by its kind, the same
4888 // string V8 gives it as `Symbol.toStringTag`:
4889 // `[AsyncFunction: af]`, `[GeneratorFunction: g]`.
4890 let kind = match self.funcs.get(f.def_id) {
4891 Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction",
4892 Some(d) if d.is_generator => "GeneratorFunction",
4893 Some(d) if d.is_async => "AsyncFunction",
4894 _ => "Function",
4895 };
4896 let base = if name.is_empty() {
4897 format!("[{kind} (anonymous)]")
4898 } else {
4899 format!("[{kind}: {name}]")
4900 };
4901 self.with_callable_props(v, base, indent, st)
4902 }
4903 Some(JsObj::Builtin(n)) => {
4904 // A namespace object is not a function and must not be
4905 // printed as one. The three ECMAScript namespaces carry a
4906 // `Symbol.toStringTag` and inspect as `Object [Math] {}`;
4907 // their members are all non-enumerable, so the braces really
4908 // are empty. A `require()`d module namespace has no tag and
4909 // node prints its members, which cannot be rendered here —
4910 // formatting a member means allocating its value, and this
4911 // runs under the host borrow.
4912 if !builtin_is_callable(n) {
4913 match crate::builtins::well_known_tag(self, v) {
4914 Some(tag) => format!("Object [{tag}] {{}}"),
4915 // `Set.prototype` inspects under the CONSTRUCTOR's
4916 // name, not the key: node prints `Object [Set] {}`.
4917 None => {
4918 format!("Object [{}] {{}}", n.trim_end_matches(".prototype"))
4919 }
4920 }
4921 } else {
4922 format!("[Function: {}]", crate::builtins::builtin_name(n))
4923 }
4924 }
4925 // A bound method is not anonymous: it is the prototype method it
4926 // resolves to, so `console.log(new Uint8Array(1).set)` reports
4927 // `[Function: set]`.
4928 Some(JsObj::BoundMethod { name, .. }) => format!("[Function: {name}]"),
4929 Some(JsObj::BoundFunc { target, .. }) => {
4930 let n = self.callable_name(target);
4931 if n.is_empty() {
4932 "[Function: bound ]".into()
4933 } else {
4934 format!("[Function: bound {n}]")
4935 }
4936 }
4937 _ => "undefined".into(),
4938 },
4939 _ => "undefined".into(),
4940 }
4941 }
4942
4943 /// Append a callable's own enumerable properties to its `[Function: f]` /
4944 /// `[class C]` base, the way `util.inspect` does: `[Function: f] { a: 1 }`.
4945 /// A callable with none renders as the bare base.
4946 fn with_callable_props(
4947 &self,
4948 v: &Value,
4949 base: String,
4950 indent: usize,
4951 st: &mut InspectCycles,
4952 ) -> String {
4953 let mut inner: Vec<String> = self
4954 .own_enum_key_names(v)
4955 .into_iter()
4956 .map(|k| {
4957 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4958 format!(
4959 "{}: {}",
4960 fmt_key(&k),
4961 self.inspect_lvl(&val, indent + 2, st)
4962 )
4963 })
4964 .collect();
4965 for (k, val) in self.own_symbol_entries(v) {
4966 if let Some(sym) = self.symbol_of_key(&k) {
4967 inner.push(format!(
4968 "{}: {}",
4969 self.inspect(&sym),
4970 self.inspect_lvl(&val, indent + 2, st)
4971 ));
4972 }
4973 }
4974 if inner.is_empty() {
4975 return base;
4976 }
4977 self.render_object(&inner, &format!("{base} "), indent, st)
4978 }
4979
4980 /// Render a non-empty array's already-formatted element strings, applying
4981 /// Node's `util.inspect` layout: a single line when it fits, else a multi-line
4982 /// grid via `groupArrayElements` (for >6 entries), else one element per line.
4983 /// `values` is the raw element list (drives numeric right-alignment); `indent`
4984 /// is the array's own indentation level.
4985 fn render_array(
4986 &self,
4987 output: &[String],
4988 values: &[Value],
4989 indent: usize,
4990 opts: ArrayLayout<'_>,
4991 st: &InspectCycles,
4992 ) -> String {
4993 let ArrayLayout {
4994 has_props,
4995 has_tail,
4996 base,
4997 } = opts;
4998 // Group array elements together if the array has more than six entries.
4999 // Arrays carrying extra own props (`index`/`input`/… on a match result)
5000 // are never grid-grouped — Node lays those out plainly.
5001 // `compact: false` (held as 0) also turns the GRID off, not just the
5002 // single-line join. Node reaches `groupArrayElements` only under
5003 // `ctx.compact >= 1`, so `util.inspect(arr, { compact: false })` is one
5004 // element per line however many there are; without this gate a 30-element
5005 // array still came back column-aligned in three rows, which is the form
5006 // assert's diff renderer splits on — every array diff would have been
5007 // computed over grid rows instead of elements.
5008 let entries = output.len();
5009 let (lines, grouped) = if entries > 6 && !has_props && inspect_compact() >= 1 {
5010 group_array_elements(self, output, values, indent, has_tail)
5011 } else {
5012 (output.to_vec(), false)
5013 };
5014 // A typed array prints its constructor and length ahead of the brackets
5015 // (`Uint8Array(3) [ 1, 2, 3 ]`); node counts that as `base` in the
5016 // break-length seed, so a long tag wraps the list one entry sooner.
5017 if output.is_empty() {
5018 return format!("{base}[]");
5019 }
5020 // If no grouping happened, try to line everything up on a single line.
5021 if !grouped {
5022 // start = output.length + indentationLvl + braces[0].len(1) + base + 10
5023 let start = output.len() + indent + 1 + base.chars().count() + 10;
5024 if self.may_compact(indent, st) && is_below_break_length(output, start) {
5025 return format!("{base}[ {} ]", output.join(", "));
5026 }
5027 }
5028 // Otherwise: one (grouped or single) entry per line, indented by indent+2.
5029 let pad = " ".repeat(indent);
5030 let sep = format!(",\n{pad} ");
5031 format!("{base}[\n{pad} {}\n{pad}]", lines.join(&sep))
5032 }
5033
5034 /// Render a non-empty object's already-formatted `key: value` strings with
5035 /// Node's `util.inspect` layout: a single line when it fits `breakLength`,
5036 /// else one property per line indented by `indent + 2`. `prefix` is the
5037 /// constructor/`[Object: null prototype]` tag (with trailing space) or empty.
5038 /// Mirrors `render_array`'s break decision, including the `compact` depth
5039 /// gate.
5040 /// Whether a group at `indent` may be joined onto one line.
5041 ///
5042 /// Node's `reduceToSingleString`: only while the subtree below this group is
5043 /// SHALLOWER than `compact` (default 3). `compact: false` is held as 0, so
5044 /// nothing qualifies and every group breaks.
5045 fn may_compact(&self, indent: usize, st: &InspectCycles) -> bool {
5046 let compact = inspect_compact();
5047 if compact < 1 {
5048 return false;
5049 }
5050 // Levels, not columns: the indent advances by two per level.
5051 let depth_below = (st.deepest.saturating_sub(indent)) / 2;
5052 (depth_below as i64) < compact
5053 }
5054
5055 fn render_object(
5056 &self,
5057 output: &[String],
5058 prefix: &str,
5059 indent: usize,
5060 st: &InspectCycles,
5061 ) -> String {
5062 // start = output.length + indentationLvl + braces[0].len + base(0) + 10.
5063 // For a tagged object Node folds the tag into `braces[0]` (e.g.
5064 // `"Point {"`, `"[Object: null prototype] {"`), so its length is the
5065 // prefix (which carries the trailing space) plus the `{`.
5066 // `sorted: true` orders the RENDERED entries, not the keys. Node sorts
5067 // the finished `key: value` strings (`output.sort()` in `formatRaw` for
5068 // the object shape), which is observably different from sorting keys
5069 // whenever a key needs quoting — `'b-b': 1` sorts under `'`, not `b`.
5070 // `assert`'s diff renderer depends on this: without it two objects
5071 // carrying the same properties in a different insertion order diffed as
5072 // a wholesale rewrite of every line instead of as equal.
5073 let sorted_output;
5074 let output = if inspect_sorted() {
5075 let mut v = output.to_vec();
5076 v.sort();
5077 sorted_output = v;
5078 &sorted_output[..]
5079 } else {
5080 output
5081 };
5082 let braces0 = prefix.chars().count() + 1;
5083 let start = output.len() + indent + braces0 + 10;
5084 if self.may_compact(indent, st) && is_below_break_length(output, start) {
5085 return format!("{prefix}{{ {} }}", output.join(", "));
5086 }
5087 let pad = " ".repeat(indent);
5088 let sep = format!(",\n{pad} ");
5089 format!("{prefix}{{\n{pad} {}\n{pad}}}", output.join(&sep))
5090 }
5091
5092 /// The `.name` of any callable (function/class/builtin/bound).
5093 pub fn callable_name(&self, v: &Value) -> String {
5094 // A user-set `.name` own property wins.
5095 if let Some(n) = self.fn_prop(v, "name") {
5096 return self.str_of(&n);
5097 }
5098 match self.get(v) {
5099 Some(JsObj::Func(f)) => self
5100 .funcs
5101 .get(f.def_id)
5102 .map(|d| d.name.clone())
5103 .unwrap_or_default(),
5104 Some(JsObj::Class(c)) => c.name.clone(),
5105 // Not the whole key: a builtin's `.name` is its last segment, and a
5106 // prototype thunk's key is `@proto:<Ctor>:<method>` — which has no
5107 // `.` at all, so this reported the internal spelling verbatim and
5108 // `console.log(Uint8Array.prototype.set)` printed
5109 // `[Function: @proto:TypedArray:set]`.
5110 Some(JsObj::Builtin(n)) => crate::builtins::builtin_name(n).to_string(),
5111 Some(JsObj::BoundFunc { target, .. }) => {
5112 format!("bound {}", self.callable_name(target))
5113 }
5114 Some(JsObj::BoundMethod { name, .. }) => name.clone(),
5115 _ => String::new(),
5116 }
5117 }
5118
5119 // ── equality / comparison / arithmetic (numeric-hook + builtin paths) ──
5120
5121 /// Strict equality (`===`): same type and same value, no coercion.
5122 pub fn strict_eq(&self, a: &Value, b: &Value) -> bool {
5123 match (a, b) {
5124 (Value::Undef, Value::Undef) => true,
5125 (Value::Bool(x), Value::Bool(y)) => x == y,
5126 (Value::Str(x), Value::Str(y)) => x == y,
5127 _ => {
5128 // Numbers (NaN !== NaN, +0 === -0).
5129 let an = matches!(a, Value::Int(_) | Value::Float(_));
5130 let bn = matches!(b, Value::Int(_) | Value::Float(_));
5131 if an && bn {
5132 let x = self.to_number(a);
5133 let y = self.to_number(b);
5134 return x == y;
5135 }
5136 // BigInt === BigInt compares by value (each literal is a distinct
5137 // heap cell, so reference identity would be wrong). BigInt is never
5138 // `===` a Number (different types).
5139 if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5140 return x == y;
5141 }
5142 // Heap values.
5143 if let (Some(sa), Some(sb)) = (self.as_str(a), self.as_str(b)) {
5144 return sa == sb;
5145 }
5146 let na = self.is_null(a);
5147 let nb = self.is_null(b);
5148 if na || nb {
5149 return na && nb;
5150 }
5151 // A builtin namespace/constructor/prototype is a SINGLETON in JS
5152 // (`Math === Math`, `Array.prototype === Array.prototype`), but
5153 // every bare reference here allocates a fresh handle, so compare
5154 // those by name rather than by heap index.
5155 if let (Some(JsObj::Builtin(x)), Some(JsObj::Builtin(y))) =
5156 (self.get(a), self.get(b))
5157 {
5158 return builtin_identity(x) == builtin_identity(y);
5159 }
5160 // Reference identity for arrays/objects/functions.
5161 matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
5162 }
5163 }
5164 }
5165
5166 /// Whether `v` is `null` or `undefined`.
5167 pub fn is_nullish(&self, v: &Value) -> bool {
5168 matches!(v, Value::Undef) || self.is_null(v)
5169 }
5170
5171 /// The ECMAScript "loose type" of `v` for the `==` algorithm: `"number"`,
5172 /// `"string"` (primitive or heap string), `"boolean"`, `"undefined"`,
5173 /// `"null"`, or `"object"` (array / plain object / function).
5174 fn js_type(&self, v: &Value) -> &'static str {
5175 match v {
5176 Value::Undef => "undefined",
5177 Value::Bool(_) => "boolean",
5178 Value::Int(_) | Value::Float(_) => "number",
5179 Value::Str(_) => "string",
5180 Value::Obj(_) => match self.get(v) {
5181 Some(JsObj::Str(_)) => "string",
5182 Some(JsObj::Null) => "null",
5183 Some(JsObj::BigInt(_)) => "bigint",
5184 _ => "object",
5185 },
5186 _ => "object",
5187 }
5188 }
5189
5190 /// Loose equality (`==`) following the ECMAScript Abstract Equality Comparison.
5191 /// Objects reduce via `ToPrimitive` (which for our heap objects is always their
5192 /// string `toString`), so `[0] == "0"` is `true` (string compare of `"0"`) but
5193 /// `[0] == ""` is `false` — never a number coercion of the object.
5194 pub fn loose_eq(&self, a: &Value, b: &Value) -> bool {
5195 // Same type: identical to `===` (number==number, string==string, etc.).
5196 if self.strict_eq(a, b) {
5197 return true;
5198 }
5199 let ta = self.js_type(a);
5200 let tb = self.js_type(b);
5201 // null and undefined are loosely equal only to each other.
5202 if self.is_nullish(a) || self.is_nullish(b) {
5203 return self.is_nullish(a) && self.is_nullish(b);
5204 }
5205 // BigInt ⇄ (Number | String | Boolean | Object): compare mathematical
5206 // values (both-BigInt was already settled by the `strict_eq` above).
5207 if ta == "bigint" || tb == "bigint" {
5208 return self.bigint_loose_eq(a, b);
5209 }
5210 if ta == tb {
5211 // Same type but not strict-equal (and not nullish) ⇒ not equal.
5212 return false;
5213 }
5214 // number ⇄ string: compare as numbers.
5215 if (ta == "number" && tb == "string") || (ta == "string" && tb == "number") {
5216 return self.to_number(a) == self.to_number(b);
5217 }
5218 // boolean side coerces to number, then recompares.
5219 if ta == "boolean" {
5220 return self.loose_eq(&Value::Float(self.to_number(a)), b);
5221 }
5222 if tb == "boolean" {
5223 return self.loose_eq(a, &Value::Float(self.to_number(b)));
5224 }
5225 // object ⇄ (number|string): ToPrimitive the object (→ its string form),
5226 // then recompare as string==string or number==string.
5227 if ta == "object" && (tb == "number" || tb == "string") {
5228 let pa = self.str_of(a);
5229 return if tb == "string" {
5230 pa == self.str_of(b)
5231 } else {
5232 str_to_number(&pa) == self.to_number(b)
5233 };
5234 }
5235 if tb == "object" && (ta == "number" || ta == "string") {
5236 let pb = self.str_of(b);
5237 return if ta == "string" {
5238 self.str_of(a) == pb
5239 } else {
5240 self.to_number(a) == str_to_number(&pb)
5241 };
5242 }
5243 false
5244 }
5245
5246 /// The numeric-hook arithmetic/relational fallback for non-native operands
5247 /// (called by fusevm when at least one operand isn't `Int`/`Float`).
5248 pub fn arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5249 use NumOp::*;
5250 match op {
5251 Add => {
5252 // `+`: if either operand is a string, concatenate string forms;
5253 // otherwise numeric addition.
5254 let a_str = self.prefers_string(a);
5255 let b_str = self.prefers_string(b);
5256 if a_str || b_str {
5257 // String concatenation wins even with a bigint operand
5258 // (`1n + "x"` → `"1x"`).
5259 let s = format!("{}{}", self.str_of(a), self.str_of(b));
5260 Ok(self.new_str(s))
5261 } else if self.is_bigint_val(a) || self.is_bigint_val(b) {
5262 self.bigint_arith(op, a, b)
5263 } else {
5264 Ok(Value::Float(self.to_number(a) + self.to_number(b)))
5265 }
5266 }
5267 Sub | Mul | Div | Mod | Pow if self.is_bigint_val(a) || self.is_bigint_val(b) => {
5268 self.bigint_arith(op, a, b)
5269 }
5270 Sub => Ok(Value::Float(self.to_number(a) - self.to_number(b))),
5271 Mul => Ok(Value::Float(self.to_number(a) * self.to_number(b))),
5272 Div => Ok(Value::Float(self.to_number(a) / self.to_number(b))),
5273 Mod => Ok(Value::Float(js_mod(self.to_number(a), self.to_number(b)))),
5274 Pow => Ok(Value::Float(crate::builtins::js_pow(
5275 self.to_number(a),
5276 self.to_number(b),
5277 ))),
5278 Neg if self.is_bigint_val(a) => self.bigint_arith(op, a, b),
5279 Neg => Ok(Value::Float(-self.to_number(a))),
5280 Lt | Le | Gt | Ge => Ok(Value::Bool(self.relational(op, a, b))),
5281 Eq => Ok(Value::Bool(self.loose_eq(a, b))),
5282 Ne => Ok(Value::Bool(!self.loose_eq(a, b))),
5283 }
5284 }
5285
5286 /// Whether `v`'s primitive (`ToPrimitive` with the default hint) is a string,
5287 /// which drives `+` toward concatenation. Primitive strings qualify, and so
5288 /// do heap objects whose default `ToPrimitive` is their (string) `toString`:
5289 /// arrays (`[1,2,3]+3 → "1,2,33"`), plain objects (`{}+[] → "[object Object]"`),
5290 /// and functions. `null`/`undefined`/`boolean`/`number` do not.
5291 fn prefers_string(&self, v: &Value) -> bool {
5292 match v {
5293 Value::Str(_) => true,
5294 // A BigInt's `ToPrimitive` is the bigint itself (numeric), NOT a string,
5295 // so `1n + 2n` is bigint addition, not concatenation. `null` has no
5296 // string primitive either.
5297 Value::Obj(_) => !matches!(
5298 self.get(v),
5299 Some(JsObj::Null) | Some(JsObj::BigInt(_)) | None
5300 ),
5301 _ => false,
5302 }
5303 }
5304
5305 /// Relational comparison (`< <= > >=`) with JS coercion: string/string is
5306 /// lexicographic, otherwise numeric (NaN yields false).
5307 fn relational(&self, op: NumOp, a: &Value, b: &Value) -> bool {
5308 use std::cmp::Ordering;
5309 let ord = if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5310 // BigInt < BigInt: exact (no f64 precision loss for large magnitudes).
5311 x.cmp(&y)
5312 } else if let (Some(x), Some(y)) = (self.as_str(a), self.as_str(b)) {
5313 // 7.2.13 IsLessThan compares CODE UNITS, which is not Rust's `str`
5314 // order once an astral character meets a BMP one — see `utf16`.
5315 crate::utf16::cmp_units(&x, &y)
5316 } else {
5317 let x = self.to_number(a);
5318 let y = self.to_number(b);
5319 match x.partial_cmp(&y) {
5320 Some(o) => o,
5321 None => return false, // NaN operand
5322 }
5323 };
5324 match op {
5325 NumOp::Lt => ord == Ordering::Less,
5326 NumOp::Le => ord != Ordering::Greater,
5327 NumOp::Gt => ord == Ordering::Greater,
5328 NumOp::Ge => ord != Ordering::Less,
5329 _ => false,
5330 }
5331 }
5332
5333 /// Bitwise/shift ops with JS ToInt32/ToUint32 semantics — or true
5334 /// arbitrary-width BigInt bitwise when both operands are BigInt (mixing a
5335 /// BigInt with a Number throws, matching Node).
5336 pub fn bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5337 if self.is_bigint_val(a) || self.is_bigint_val(b) {
5338 return self.bigint_bitwise(tag, a, b);
5339 }
5340 let x = to_int32(self.to_number(a));
5341 let y = to_int32(self.to_number(b));
5342 let r: i64 = match tag {
5343 binop::BITAND => (x & y) as i64,
5344 binop::BITOR => (x | y) as i64,
5345 binop::BITXOR => (x ^ y) as i64,
5346 binop::SHL => (x.wrapping_shl((y as u32) & 31)) as i64,
5347 binop::SHR => (x >> ((y as u32) & 31)) as i64,
5348 binop::USHR => (to_uint32(self.to_number(a)) >> ((y as u32) & 31)) as i64,
5349 _ => 0,
5350 };
5351 Ok(Value::Float(r as f64))
5352 }
5353
5354 // ── BigInt operations ────────────────────────────────────────────────────
5355 /// Whether `v` is a heap `BigInt`.
5356 pub fn is_bigint_val(&self, v: &Value) -> bool {
5357 matches!(self.get(v), Some(JsObj::BigInt(_)))
5358 }
5359 /// The `BigInt` value of `v` (a heap bigint), else `None`.
5360 pub fn as_bigint(&self, v: &Value) -> Option<num_bigint::BigInt> {
5361 match self.get(v) {
5362 Some(JsObj::BigInt(b)) => Some(b.clone()),
5363 _ => None,
5364 }
5365 }
5366 /// Allocate a heap `BigInt`.
5367 pub fn new_bigint(&mut self, b: num_bigint::BigInt) -> Value {
5368 self.alloc(JsObj::BigInt(b))
5369 }
5370
5371 /// BigInt arithmetic (`+ - * / % **`, unary `-`). Requires BOTH operands to be
5372 /// BigInt for a binary op; mixing a BigInt with a Number throws the exact Node
5373 /// `TypeError` (a string operand is handled as concatenation before we get
5374 /// here). Division/`%` truncate toward zero; `**` needs a non-negative
5375 /// exponent.
5376 fn bigint_arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5377 use num_traits::{Signed, Zero};
5378 use NumOp::*;
5379 if op == Neg {
5380 let x = self.as_bigint(a).expect("bigint_arith Neg on non-bigint");
5381 return Ok(self.new_bigint(-x));
5382 }
5383 let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5384 (Some(x), Some(y)) => (x, y),
5385 // Exactly one side is a BigInt → the other is a Number/Boolean: illegal.
5386 _ => {
5387 return Err(type_error(
5388 "Cannot mix BigInt and other types, use explicit conversions",
5389 ))
5390 }
5391 };
5392 let r = match op {
5393 Add => x + y,
5394 Sub => x - y,
5395 Mul => x * y,
5396 Div => {
5397 if y.is_zero() {
5398 return Err("RangeError: Division by zero".into());
5399 }
5400 x / y // truncates toward zero (matches JS BigInt division)
5401 }
5402 Mod => {
5403 if y.is_zero() {
5404 return Err("RangeError: Division by zero".into());
5405 }
5406 x % y // sign follows the dividend (truncated), like JS
5407 }
5408 Pow => {
5409 if y.is_negative() {
5410 return Err("RangeError: Exponent must be positive".into());
5411 }
5412 let exp = num_traits::ToPrimitive::to_u32(&y)
5413 .ok_or_else(|| "RangeError: Maximum BigInt size exceeded".to_string())?;
5414 num_traits::Pow::pow(x, exp)
5415 }
5416 _ => return Err(type_error("unsupported BigInt operation")),
5417 };
5418 Ok(self.new_bigint(r))
5419 }
5420
5421 /// BigInt bitwise (`& | ^ << >>`); `>>>` has no BigInt form. Both operands must
5422 /// be BigInt (mixing throws).
5423 fn bigint_bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5424 let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5425 (Some(x), Some(y)) => (x, y),
5426 _ => {
5427 return Err(type_error(
5428 "Cannot mix BigInt and other types, use explicit conversions",
5429 ))
5430 }
5431 };
5432 let r = match tag {
5433 binop::BITAND => x & y,
5434 binop::BITOR => x | y,
5435 binop::BITXOR => x ^ y,
5436 binop::SHL => {
5437 let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5438 if n >= 0 {
5439 x << (n as usize)
5440 } else {
5441 x >> ((-n) as usize)
5442 }
5443 }
5444 binop::SHR => {
5445 let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5446 if n >= 0 {
5447 x >> (n as usize)
5448 } else {
5449 x << ((-n) as usize)
5450 }
5451 }
5452 binop::USHR => {
5453 return Err(type_error(
5454 "BigInts have no unsigned right shift, use >> instead",
5455 ))
5456 }
5457 _ => return Err(type_error("unsupported BigInt operation")),
5458 };
5459 Ok(self.new_bigint(r))
5460 }
5461
5462 /// BigInt ⇄ (Number | Boolean | String | Object) loose equality (`==`). Both
5463 /// being BigInt was already handled by `strict_eq`.
5464 fn bigint_loose_eq(&self, a: &Value, b: &Value) -> bool {
5465 // Order so `big` is the BigInt side and `other` the counterpart.
5466 let (big, other) = match (self.as_bigint(a), self.as_bigint(b)) {
5467 (Some(x), _) => (x, b),
5468 (_, Some(y)) => (y, a),
5469 _ => return false,
5470 };
5471 match other {
5472 Value::Bool(bo) => big == num_bigint::BigInt::from(*bo as i64),
5473 Value::Int(n) => big == num_bigint::BigInt::from(*n),
5474 Value::Float(f) => {
5475 // Equal only when the float is an integer with the same value.
5476 if !f.is_finite() || f.fract() != 0.0 {
5477 return false;
5478 }
5479 bigint_to_f64(&big) == *f
5480 }
5481 Value::Str(s) => match parse_bigint_str(s) {
5482 Some(bs) => big == bs,
5483 None => false,
5484 },
5485 Value::Obj(_) => match self.get(other) {
5486 // A heap string parses like a primitive string.
5487 Some(JsObj::Str(s)) => parse_bigint_str(s).map(|bs| big == bs).unwrap_or(false),
5488 _ => {
5489 // Other objects reduce via ToPrimitive (their string form).
5490 let s = self.str_of(other);
5491 parse_bigint_str(&s).map(|bs| big == bs).unwrap_or(false)
5492 }
5493 },
5494 _ => false,
5495 }
5496 }
5497}
5498
5499/// Parse a string to a BigInt under JS `StringToBigInt` rules: trimmed, empty →
5500/// `0n`, decimal or `0x`/`0o`/`0b` prefixed; any junk → `None`.
5501pub fn parse_bigint_str(s: &str) -> Option<num_bigint::BigInt> {
5502 let t = crate::utf16::js_trim(s);
5503 if t.is_empty() {
5504 return Some(num_bigint::BigInt::from(0));
5505 }
5506 let (radix, digits) = if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5507 (16, h)
5508 } else if let Some(o) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5509 (8, o)
5510 } else if let Some(bb) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5511 (2, bb)
5512 } else {
5513 (10, t)
5514 };
5515 num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
5516}
5517
5518/// Coerce a BigInt to `f64` (for `Number(bigint)` and mixed relational compares);
5519/// out-of-range magnitudes become ±Infinity, matching Node.
5520pub fn bigint_to_f64(b: &num_bigint::BigInt) -> f64 {
5521 num_traits::ToPrimitive::to_f64(b).unwrap_or_else(|| {
5522 if num_traits::Signed::is_negative(b) {
5523 f64::NEG_INFINITY
5524 } else {
5525 f64::INFINITY
5526 }
5527 })
5528}
5529
5530/// JS `%` remainder (sign follows the dividend; matches `f64::rem`).
5531fn js_mod(a: f64, b: f64) -> f64 {
5532 a % b
5533}
5534
5535/// Cycle bookkeeping for one `util.inspect` render.
5536///
5537/// `seen` is the chain of objects currently being rendered (an entry appearing
5538/// twice is a back-edge), and `refs` records every object a back-edge pointed
5539/// at, in first-encountered order — its position + 1 is the `*N` id Node prints
5540/// in `[Circular *N]` / `<ref *N>`.
5541/// How an array-shaped group is laid out, beyond its entries themselves.
5542#[derive(Clone, Copy)]
5543struct ArrayLayout<'a> {
5544 /// Extra own properties follow the elements, which suppresses grid grouping.
5545 has_props: bool,
5546 /// `output`'s last entry is the `... N more items` tail rather than a real
5547 /// element, so the grid must not size a column to it.
5548 has_tail: bool,
5549 /// A constructor tag printed before the brackets, with a trailing space
5550 /// (`"Uint8Array(3) "`), or empty for a plain array.
5551 base: &'a str,
5552}
5553
5554#[derive(Default)]
5555struct InspectCycles {
5556 seen: Vec<Value>,
5557 refs: Vec<Value>,
5558 /// The indent level of the value most recently EXPANDED — node's
5559 /// `ctx.currentDepth`. `reduceToSingleString` puts a group on one line only
5560 /// while `currentDepth - thisDepth < compact`, so without it a deeply
5561 /// nested object printed on one line where node breaks the outer levels.
5562 deepest: usize,
5563}
5564
5565impl InspectCycles {
5566 /// Record `v` as a cycle target (idempotent) and return its 1-based id.
5567 fn mark(&mut self, h: &JsHost, v: &Value) -> usize {
5568 if let Some(id) = self.id_of(h, v) {
5569 return id;
5570 }
5571 self.refs.push(v.clone());
5572 self.refs.len()
5573 }
5574
5575 /// The `*N` id already assigned to `v`, if any.
5576 fn id_of(&self, h: &JsHost, v: &Value) -> Option<usize> {
5577 self.refs
5578 .iter()
5579 .position(|p| h.strict_eq(p, v))
5580 .map(|i| i + 1)
5581 }
5582}
5583
5584thread_local! {
5585 /// The active `util.inspect` `depth` (nesting levels shown before collapsing
5586 /// to `[Object]`/`[Array]`). Node's default is 2; `util.inspect(v,{depth:N})`
5587 /// overrides it for one call, `console.log`/`util.format` use the default.
5588 /// Signed, because `util.inspect(v, { depth: -1 })` is legal and means
5589 /// "already past the limit" — everything collapses to `[Object]` at the top
5590 /// level. Held as `usize` it read as an enormous depth and expanded fully.
5591 static INSPECT_MAX_DEPTH: std::cell::Cell<i64> = const { std::cell::Cell::new(2) };
5592
5593 /// `util.inspect`'s `compact` option. Node's default is the NUMBER 3: a
5594 /// group is put on one line only when the subtree below it is shallower
5595 /// than this. `compact: false` is held as 0, which no subtree depth is
5596 /// below, so every group breaks — which is exactly what node does.
5597 static INSPECT_COMPACT: std::cell::Cell<i64> = const { std::cell::Cell::new(DEFAULT_COMPACT) };
5598
5599 /// `util.inspect`'s `breakLength`. Node's default is 128, but `util.inspect`
5600 /// itself passes 80.
5601 static INSPECT_BREAK_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(80) };
5602
5603 /// `util.inspect`'s `sorted` option: emit an object's own keys in code-unit
5604 /// order instead of insertion order. Off by default. `assert`'s diff renderer
5605 /// turns it on so that two objects built with the same keys in a different
5606 /// order diff as equal rather than as a wholesale rewrite.
5607 static INSPECT_SORTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5608
5609 /// `util.inspect`'s `maxArrayLength`: how many entries are formatted before
5610 /// the rest collapse into `... N more items`. Node's default is 100;
5611 /// `Infinity`/`null` means "all", held here as `usize::MAX`.
5612 static INSPECT_MAX_ARRAY_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(DEFAULT_MAX_ARRAY_LENGTH) };
5613
5614 /// `util.inspect`'s `customInspect` option: whether a value's own
5615 /// `[util.inspect.custom]` rendering is used. On by default; `assert` turns
5616 /// it off so a diff shows an object's real structure rather than whatever
5617 /// summary it prefers to print.
5618 static INSPECT_CUSTOM: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
5619
5620 /// `util.inspect`'s `showHidden`: reveal the non-enumerable slots a value
5621 /// carries — an array's `length`, a typed array's element width and window
5622 /// onto its backing store. Off by default; `util.format`'s `%o` turns it on.
5623 static INSPECT_SHOW_HIDDEN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5624}
5625
5626/// Set the `util.inspect` `showHidden` option for the next render.
5627pub fn set_inspect_show_hidden(s: bool) {
5628 INSPECT_SHOW_HIDDEN.with(|x| x.set(s));
5629}
5630
5631pub(crate) fn inspect_show_hidden() -> bool {
5632 INSPECT_SHOW_HIDDEN.with(|x| x.get())
5633}
5634
5635/// Set the `util.inspect` `customInspect` option for the next render.
5636pub fn set_inspect_custom(c: bool) {
5637 INSPECT_CUSTOM.with(|x| x.set(c));
5638}
5639
5640pub(crate) fn inspect_custom() -> bool {
5641 INSPECT_CUSTOM.with(|x| x.get())
5642}
5643
5644/// Set the `util.inspect` `sorted` option for the next render.
5645pub fn set_inspect_sorted(s: bool) {
5646 INSPECT_SORTED.with(|x| x.set(s));
5647}
5648
5649pub(crate) fn inspect_sorted() -> bool {
5650 INSPECT_SORTED.with(|x| x.get())
5651}
5652
5653/// Set the `util.inspect` `maxArrayLength` for the next render.
5654pub fn set_inspect_max_array_length(n: usize) {
5655 INSPECT_MAX_ARRAY_LENGTH.with(|x| x.set(n));
5656}
5657
5658pub(crate) fn inspect_max_array_length() -> usize {
5659 INSPECT_MAX_ARRAY_LENGTH.with(|x| x.get())
5660}
5661
5662/// Set the `util.inspect` `compact` option for the next render (0 for `false`).
5663pub fn set_inspect_compact(c: i64) {
5664 INSPECT_COMPACT.with(|x| x.set(c));
5665}
5666
5667/// Set the `util.inspect` `breakLength` for the next render.
5668pub fn set_inspect_break_length(n: usize) {
5669 INSPECT_BREAK_LENGTH.with(|x| x.set(n));
5670}
5671
5672fn inspect_compact() -> i64 {
5673 INSPECT_COMPACT.with(|x| x.get())
5674}
5675
5676/// Set the `util.inspect` depth for the next render (restore to 2 after).
5677pub fn set_inspect_max_depth(d: i64) {
5678 INSPECT_MAX_DEPTH.with(|c| c.set(d));
5679}
5680/// Twice the configured depth, which is what the inspect walk compares its
5681/// indent against. Saturating, because `util.inspect(x, { depth: null })` and
5682/// `{ depth: Infinity }` both set the depth to `usize::MAX`, and doubling that
5683/// overflowed and panicked the process — an abort no script could catch.
5684fn inspect_indent_limit() -> i64 {
5685 inspect_max_depth().saturating_mul(2)
5686}
5687
5688fn inspect_max_depth() -> i64 {
5689 INSPECT_MAX_DEPTH.with(|c| c.get())
5690}
5691
5692/// ECMA-262 `ToInt32` (7.1.6): truncate toward zero, reduce modulo 2^32, then
5693/// reinterpret as signed.
5694///
5695/// The reduction has to happen in `f64`, not by casting through `i64`. Rust
5696/// saturates an out-of-range float-to-int cast, so `1e300 as i64` is `i64::MAX`
5697/// and `1e300 | 0` came out `-1` where every engine says `0`; the same
5698/// saturation made `1e300 >>> 0` report `4294967295`. `rem_euclid` on a
5699/// power-of-two modulus is exact for every finite double, so this is the whole
5700/// fix — and it is the form `Math.clz32` already used.
5701pub(crate) fn to_int32(f: f64) -> i32 {
5702 to_uint32(f) as i32
5703}
5704pub(crate) fn to_uint32(f: f64) -> u32 {
5705 if !f.is_finite() {
5706 return 0;
5707 }
5708 f.trunc().rem_euclid(4294967296.0) as u32
5709}
5710
5711/// Parse a string in numeric context (`ToNumber`): trimmed, empty -> 0.
5712fn str_to_number(s: &str) -> f64 {
5713 let t = crate::utf16::js_trim(s);
5714 if t.is_empty() {
5715 return 0.0;
5716 }
5717 if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5718 return i64::from_str_radix(hex, 16)
5719 .map(|n| n as f64)
5720 .unwrap_or(f64::NAN);
5721 }
5722 if let Some(oct) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5723 return i64::from_str_radix(oct, 8)
5724 .map(|n| n as f64)
5725 .unwrap_or(f64::NAN);
5726 }
5727 if let Some(bin) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5728 return i64::from_str_radix(bin, 2)
5729 .map(|n| n as f64)
5730 .unwrap_or(f64::NAN);
5731 }
5732 match t {
5733 "Infinity" | "+Infinity" => f64::INFINITY,
5734 "-Infinity" => f64::NEG_INFINITY,
5735 _ => t.parse::<f64>().unwrap_or(f64::NAN),
5736 }
5737}
5738
5739/// `util.inspect` break length (the width past which entries wrap). Node's default.
5740fn break_length() -> usize {
5741 INSPECT_BREAK_LENGTH.with(|x| x.get())
5742}
5743/// Node's default `compact` setting (the `compact * 4` column cap term).
5744/// Node's DEFAULT `compact` setting, and the initial value of
5745/// `INSPECT_COMPACT`. The grid's column cap is `compact * 4`, so it has to be
5746/// read through `inspect_compact()` at render time: under `{ compact: 1 }` node
5747/// lays a byte array out four columns wide, and the hardcoded 3 gave twelve.
5748const DEFAULT_COMPACT: i64 = 3;
5749/// Node's default `maxArrayLength` — the initial value of
5750/// `INSPECT_MAX_ARRAY_LENGTH`, which `util.inspect(v, { maxArrayLength: N })`
5751/// overrides per call. Read it through `inspect_max_array_length()`, never
5752/// directly: as a bare constant the option had no effect and a 120-element array
5753/// was truncated at 100 even under `maxArrayLength: Infinity`.
5754pub(crate) const DEFAULT_MAX_ARRAY_LENGTH: usize = 100;
5755
5756/// Whether `output` fits on a single line — a faithful port of Node's
5757/// `isBelowBreakLength` (no colors, no `base`). `start` is the caller's seed
5758/// length (braces + indentation + slack).
5759fn is_below_break_length(output: &[String], start: usize) -> bool {
5760 let limit = break_length();
5761 let mut total = output.len() + start;
5762 if total + output.len() > limit {
5763 return false;
5764 }
5765 for o in output {
5766 if o.contains('\n') {
5767 return false;
5768 }
5769 total += o.chars().count();
5770 if total > limit {
5771 return false;
5772 }
5773 }
5774 true
5775}
5776
5777/// Faithful port of Node's `util.inspect` `groupArrayElements`: lay out the
5778/// already-formatted element strings into an aligned multi-column grid. Returns
5779/// `(lines, grouped)` — `grouped` is false when Node would leave the output
5780/// ungrouped (so the caller falls back to single-line / one-per-line).
5781fn group_array_elements(
5782 host: &JsHost,
5783 output: &[String],
5784 values: &[Value],
5785 indentation_lvl: usize,
5786 has_tail: bool,
5787) -> (Vec<String>, bool) {
5788 let separator_space = 2usize; // ", " between entries
5789 // A `... N more items` tail is not an element: node drops it from the grid
5790 // (`outputLength--`) so it neither widens a column nor occupies a cell, then
5791 // re-appends it as its own final line.
5792 let output_length = output.len() - usize::from(has_tail);
5793 let data_len: Vec<usize> = output.iter().map(|o| o.chars().count()).collect();
5794 let mut total_length = 0usize;
5795 let mut max_length = 0usize;
5796 for &len in &data_len[..output_length] {
5797 total_length += len + separator_space;
5798 if len > max_length {
5799 max_length = len;
5800 }
5801 }
5802 let actual_max = max_length + separator_space;
5803 // Only group when ≥3 entries fit across AND the entries aren't wildly uneven.
5804 if !(actual_max * 3 + indentation_lvl < break_length()
5805 && (total_length as f64 / actual_max as f64 > 5.0 || max_length <= 6))
5806 {
5807 return (output.to_vec(), false);
5808 }
5809 let approx_char_heights = 2.5f64;
5810 let average_bias = (actual_max as f64 - total_length as f64 / output_length as f64).sqrt();
5811 let biased_max = (actual_max as f64 - 3.0 - average_bias).max(1.0);
5812 // Ideally a square grid; capped by break length, compact*4, and 15 columns.
5813 let columns = [
5814 ((approx_char_heights * biased_max * output_length as f64).sqrt() / biased_max).round()
5815 as i64,
5816 ((break_length() - indentation_lvl) as f64 / actual_max as f64).floor() as i64,
5817 inspect_compact().saturating_mul(4),
5818 15,
5819 ]
5820 .into_iter()
5821 .min()
5822 .unwrap();
5823 if columns <= 1 {
5824 return (output.to_vec(), false);
5825 }
5826 let columns = columns as usize;
5827 // The widest entry (plus separator) in each column.
5828 let mut max_line_length = vec![0usize; columns];
5829 for (i, slot) in max_line_length.iter_mut().enumerate() {
5830 let mut line_length = 0;
5831 let mut j = i;
5832 while j < output_length {
5833 if data_len[j] > line_length {
5834 line_length = data_len[j];
5835 }
5836 j += columns;
5837 }
5838 *slot = line_length + separator_space;
5839 }
5840 // Right-align (padStart) only when every element is a number/bigint.
5841 let pad_start = values.iter().all(|v| {
5842 matches!(v, Value::Int(_) | Value::Float(_))
5843 || matches!(host.get(v), Some(JsObj::BigInt(_)))
5844 });
5845 let mut tmp = Vec::new();
5846 let mut i = 0;
5847 while i < output_length {
5848 let max = (i + columns).min(output_length);
5849 let mut str_line = String::new();
5850 let mut j = i;
5851 while j < max.saturating_sub(1) {
5852 // `output[j]` has no colors here, so padding == max_line_length[col].
5853 let col = j - i;
5854 let cell = format!("{}, ", output[j]);
5855 let target = max_line_length[col];
5856 str_line.push_str(&pad_to(&cell, target, pad_start));
5857 j += 1;
5858 }
5859 // The last cell of the row: right-aligned entries pad without the ", ".
5860 if pad_start {
5861 let col = j - i;
5862 let target = max_line_length[col] - separator_space;
5863 str_line.push_str(&pad_to(&output[j], target, true));
5864 } else {
5865 str_line.push_str(&output[j]);
5866 }
5867 tmp.push(str_line);
5868 i += columns;
5869 }
5870 if has_tail {
5871 tmp.push(output[output_length].clone());
5872 }
5873 (tmp, true)
5874}
5875
5876/// Pad `s` to `width` chars: right-justified when `pad_start`, else left-justified.
5877/// (Padding is measured in chars; already ANSI-free here.)
5878fn pad_to(s: &str, width: usize, pad_start: bool) -> String {
5879 let len = s.chars().count();
5880 if len >= width {
5881 return s.to_string();
5882 }
5883 let fill = " ".repeat(width - len);
5884 if pad_start {
5885 format!("{fill}{s}")
5886 } else {
5887 format!("{s}{fill}")
5888 }
5889}
5890
5891/// Quote a string the way `util.inspect` does — a port of `strEscape` in Node's
5892/// `lib/internal/util/inspect.js`.
5893///
5894/// The quote character is chosen so the contents need as little escaping as
5895/// possible: single quotes normally, double quotes when the string contains a
5896/// `'` but no `"`, and a backtick when it contains both (and neither a backtick
5897/// nor a `${`). Only the ACTIVE quote is backslash-escaped, alongside `\` and
5898/// the C0 controls + DEL, which use Node's `meta` table (`\n`, `\t`, `\b`,
5899/// `\f`, `\r` short forms; `\x0B`, `\x1F`, `\x7F` uppercase-hex otherwise).
5900fn quote_str(s: &str) -> String {
5901 let quote = if !s.contains('\'') {
5902 '\''
5903 } else if !s.contains('"') {
5904 '"'
5905 } else if !s.contains('`') && !s.contains("${") {
5906 '`'
5907 } else {
5908 '\''
5909 };
5910 let mut out = String::with_capacity(s.len() + 2);
5911 out.push(quote);
5912 for c in s.chars() {
5913 match c {
5914 _ if c == quote => {
5915 out.push('\\');
5916 out.push(c);
5917 }
5918 '\\' => out.push_str("\\\\"),
5919 '\u{8}' => out.push_str("\\b"),
5920 '\t' => out.push_str("\\t"),
5921 '\n' => out.push_str("\\n"),
5922 '\u{c}' => out.push_str("\\f"),
5923 '\r' => out.push_str("\\r"),
5924 '\u{0}'..='\u{1f}' | '\u{7f}' => out.push_str(&format!("\\x{:02X}", c as u32)),
5925 _ => out.push(c),
5926 }
5927 }
5928 out.push(quote);
5929 out
5930}
5931
5932/// Render an object key: bare if it is a valid identifier, quoted otherwise.
5933fn fmt_key(k: &str) -> String {
5934 let ok = !k.is_empty()
5935 && k.chars()
5936 .next()
5937 .map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
5938 .unwrap_or(false)
5939 && k.chars()
5940 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
5941 if ok {
5942 k.to_string()
5943 } else {
5944 quote_str(k)
5945 }
5946}
5947
5948// ── iteration ────────────────────────────────────────────────────────────────
5949
5950impl JsHost {
5951 /// Collect an iterable into a vector of values (arrays, strings, Map/Set).
5952 /// Generators and user `Symbol.iterator` objects go through `iter_all`, which
5953 /// holds no host borrow across resumes.
5954 pub fn iter_vec(&mut self, v: &Value) -> Result<Vec<Value>, String> {
5955 match self.get(v) {
5956 Some(JsObj::Array(items)) => Ok(items.clone()),
5957 Some(JsObj::Str(s)) => {
5958 let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
5959 Ok(chars.into_iter().map(|c| self.new_str(c)).collect())
5960 }
5961 // A live array iterator, read from where its cursor stands. The
5962 // backing slots only: `iter_all` steps one through `[[Get]]` instead.
5963 Some(JsObj::Iter {
5964 idx,
5965 array: Some((arr, kind)),
5966 ..
5967 }) => {
5968 let (idx, kind) = (*idx, *kind);
5969 let items = match self.get(arr) {
5970 Some(JsObj::Array(items)) if idx < items.len() => items[idx..].to_vec(),
5971 _ => Vec::new(),
5972 };
5973 Ok(items
5974 .into_iter()
5975 .enumerate()
5976 .map(|(n, v)| {
5977 let key = Value::Float((idx + n) as f64);
5978 match kind {
5979 ArrayIterKind::Keys => key,
5980 ArrayIterKind::Values => v,
5981 ArrayIterKind::Entries => self.new_array(vec![key, v]),
5982 }
5983 })
5984 .collect())
5985 }
5986 Some(JsObj::Iter { items, idx, .. }) => Ok(items[*idx..].to_vec()),
5987 Some(JsObj::Set { entries, .. }) => Ok(entries.values().cloned().collect()),
5988 Some(JsObj::Map { entries, .. }) => {
5989 // Map iterates as `[key, value]` pairs.
5990 let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
5991 Ok(pairs
5992 .into_iter()
5993 .map(|(k, v)| self.new_array(vec![k, v]))
5994 .collect())
5995 }
5996 // A `Buffer` iterates over its BYTES and a typed array over its
5997 // ELEMENTS — both are iterable in Node. Only `@@bytes` was handled
5998 // here, so `[...buf]` worked while `[...new Uint8Array([1])]` threw
5999 // "object is not iterable", which is the same invariant holding at
6000 // one of its two sites.
6001 Some(JsObj::Object(props))
6002 if props.contains_key("@@bytes") || props.contains_key("@@buffer") =>
6003 {
6004 // Iterating a view over a DETACHED buffer throws, naming the
6005 // `values` iterator — reading its elements answers zero length,
6006 // but spreading it is a method call and does not.
6007 if crate::stdlib::typedarray::view_detached_h(self, v) {
6008 return Err(crate::stdlib::typedarray::detached_error(
6009 "%TypedArray%.prototype",
6010 "values",
6011 false,
6012 ));
6013 }
6014 Ok(crate::stdlib::typedarray::elems_mut_host(self, v))
6015 }
6016 // V8 names the VALUE, not its type: `[...5]` is `5 is not iterable`,
6017 // `[...{}]` is `{} is not iterable`. Reporting `typeof` instead
6018 // produced `number is not iterable`, which no engine emits.
6019 _ => {
6020 let shown = self.inspect(v);
6021 Err(type_error(&format!("{shown} is not iterable")))
6022 }
6023 }
6024 }
6025
6026 /// Enumerable string keys of an object/array (for `for-in`). Internal
6027 /// symbol-keyed props (`@@…`) are not enumerable.
6028 /// `for-in` visits own enumerable keys, then every *inherited* enumerable key
6029 /// not already seen, walking the whole prototype chain. Class methods and the
6030 /// builtin prototypes are non-enumerable, so in practice this only surfaces
6031 /// keys a script put on a prototype itself (`F.prototype.y = 2`) — but that
6032 /// is exactly the constructor-function idiom older packages are written in.
6033 pub fn enum_keys(&mut self, v: &Value) -> Vec<Value> {
6034 let mut keys = self.own_enum_key_names(v);
6035 let mut cur = self.proto_of(v);
6036 let mut hops = 0;
6037 while let Some(p) = cur {
6038 // A cyclic or pathologically deep chain must not hang the loop.
6039 hops += 1;
6040 if hops > 100 || matches!(p, Value::Undef) || self.is_null(&p) {
6041 break;
6042 }
6043 for k in self.own_enum_key_names(&p) {
6044 if !keys.contains(&k) {
6045 keys.push(k);
6046 }
6047 }
6048 cur = self.proto_of(&p);
6049 }
6050 keys.into_iter().map(|k| self.new_str(k)).collect()
6051 }
6052
6053 /// The own *enumerable* string keys of `v`, in property order — the single
6054 /// source of truth behind `for-in`, `Object.keys`/`values`/`entries`,
6055 /// object spread, `Object.assign` and `JSON.stringify`. Internal slots
6056 /// (`@@…`), private fields (`#…`) and anything marked non-enumerable via
6057 /// `prop_attrs` are excluded.
6058 pub fn own_enum_key_names(&self, v: &Value) -> Vec<String> {
6059 self.own_key_names(v, true)
6060 }
6061
6062 /// Own string keys of `v` in insertion order. `enum_only` drops the
6063 /// non-enumerable ones (`Object.keys`); otherwise every own key is reported
6064 /// (`getOwnPropertyNames`/`Reflect.ownKeys`).
6065 pub fn own_key_names(&self, v: &Value, enum_only: bool) -> Vec<String> {
6066 let mut keys = self.own_enum_data_keys(v, enum_only);
6067 // A global a SCRIPT created (`x = 1` with no declaration) is an own
6068 // ENUMERABLE property of the global object, but lives in the globals map
6069 // rather than in its property map — so no listing saw it, while
6070 // `globalThis.x` read it back and its descriptor called it enumerable.
6071 if self.is_global_object(v) {
6072 for k in self.globals.keys() {
6073 if !keys.contains(k) {
6074 keys.push(k.clone());
6075 }
6076 }
6077 }
6078 // A RegExp's `lastIndex` is a SYNTHESIZED own property — it lives in the
6079 // `RegExpObj` struct, not a property map — so nothing above can list it.
6080 // Non-enumerable, so only `getOwnPropertyNames` sees it.
6081 if !enum_only && matches!(self.get(v), Some(JsObj::RegExp(_))) {
6082 keys.push("lastIndex".to_string());
6083 }
6084 // An accessor defined before its object had any ordering marker (a class
6085 // prototype accessor, say) still has to appear.
6086 for k in self.own_accessor_keys(v) {
6087 if (!enum_only || self.prop_attrs(v, &k).enumerable) && !keys.contains(&k) {
6088 keys.push(k);
6089 }
6090 }
6091 keys
6092 }
6093
6094 /// The keys that own a slot in the object's property map, in insertion
6095 /// order, resolving accessor ordering markers back to their real key.
6096 /// Every global a SCRIPT created, in creation order — the own enumerable
6097 /// keys of the global object that live in the globals map rather than in
6098 /// its property map. `x = 1` with no declaration makes one, and
6099 /// `Object.keys(globalThis)` reports it in node.
6100 pub fn script_global_names(&self) -> Vec<String> {
6101 self.globals.keys().cloned().collect()
6102 }
6103 /// Drop a global a script created. Reports whether it was there.
6104 pub fn remove_global(&mut self, name: &str) -> bool {
6105 self.globals.shift_remove(name).is_some()
6106 }
6107 fn own_enum_data_keys(&self, v: &Value, enum_only: bool) -> Vec<String> {
6108 match self.get(v) {
6109 // A `Buffer` is an index-keyed exotic: its own enumerable keys are
6110 // `"0".."len-1"` (the bytes live in the hidden `@@bytes` slot), never
6111 // the `length`/`byteLength` view metadata, which V8 keeps on the
6112 // prototype chain or as non-enumerable own slots.
6113 // A `Buffer` and every other typed array are index-keyed exotics:
6114 // their own enumerable keys are `"0".."len-1"` (the elements live in
6115 // a hidden slot), never the `length`/`byteLength` view metadata,
6116 // which V8 keeps on the prototype chain or as non-enumerable own
6117 // slots. Only `Buffer` had this arm, so `Object.keys(u8)` was empty
6118 // and `JSON.stringify(u8)` was `{}` where node gives
6119 // `{"0":10,"1":9}` — `hasOwnProperty(0)` already answered true, so
6120 // the two views of the same question disagreed.
6121 Some(JsObj::Object(props))
6122 if matches!(
6123 props.get("@@native").map(|t| self.str_of(t)).as_deref(),
6124 Some("Buffer") | Some("TypedArray")
6125 ) =>
6126 {
6127 // A view over a DETACHED buffer has no index properties at all:
6128 // its own `length` still holds the old count, so reading that
6129 // back left `Object.keys` listing eight names over no bytes.
6130 if crate::stdlib::typedarray::view_detached_h(self, v) {
6131 return Vec::new();
6132 }
6133 // A Buffer counts its byte store; every other view reports the
6134 // element count of its window onto the ArrayBuffer.
6135 let n = match props.get("@@bytes").and_then(|b| self.get(b)) {
6136 Some(JsObj::Array(items)) => items.len(),
6137 _ => props
6138 .get("length")
6139 .map(|l| self.to_number(l))
6140 .unwrap_or(0.0) as usize,
6141 };
6142 (0..n).map(|i| i.to_string()).collect()
6143 }
6144 Some(JsObj::Object(props)) => props
6145 .keys()
6146 .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6147 Some(real) => Some(real.to_string()),
6148 None if !k.starts_with("@@") && !k.starts_with('#') => Some(k.clone()),
6149 None => None,
6150 })
6151 .filter(|k| !enum_only || self.prop_attrs(v, k).enumerable)
6152 .collect(),
6153 // A STRING is an index-keyed exotic too (10.4.3): its own keys are
6154 // its UTF-16 code-unit indices, plus the non-enumerable `length`.
6155 // Without this arm every whole-object view of a string primitive was
6156 // empty — `for (const k in 'ab')` iterated nothing, `Object.keys`
6157 // and `Object.assign({}, 'ab')` reported `{}` — while `'ab'[0]` and
6158 // `'ab'.length` answered normally, so the two views disagreed. The
6159 // spread form `{...'ab'}` went through a different path and was
6160 // already right, which is what made the gap easy to miss.
6161 Some(JsObj::Str(s)) => {
6162 let mut keys: Vec<String> =
6163 (0..crate::utf16::len(s)).map(|i| i.to_string()).collect();
6164 if !enum_only {
6165 keys.push("length".into());
6166 }
6167 keys
6168 }
6169 // `OrdinaryOwnPropertyKeys` on an array exotic: the integer indices
6170 // ascending, then the exotic non-enumerable `length`, then the
6171 // ordinary string keys in insertion order. Those ordinary keys have
6172 // no property map to live in — a `str.match()` result's
6173 // `index`/`input`/`groups` and any user-assigned `arr.foo` are kept
6174 // in the fn-prop side table — so they are read back from there.
6175 Some(JsObj::Array(items)) => {
6176 // An ELIDED element is not an own property at all, so it
6177 // contributes no key — the difference behind
6178 // `Object.keys([1,,3])` being `['0','2']`.
6179 let mut keys: Vec<String> = (0..items.len())
6180 .filter(|i| !self.is_hole(v, *i))
6181 .map(|i| i.to_string())
6182 .collect();
6183 if !enum_only {
6184 keys.push("length".into());
6185 }
6186 keys.extend(self.fn_prop_keys(v).into_iter().filter(|k| {
6187 !k.starts_with("@@")
6188 && !k.starts_with('#')
6189 && (!enum_only || self.prop_attrs(v, k).enumerable)
6190 }));
6191 keys
6192 }
6193 // A function/class keeps every own property in the side table. Its
6194 // exotic `name`/`length`/`prototype` and its class methods are all
6195 // non-enumerable, so under `enum_only` what is left is exactly what
6196 // a script assigned; `getOwnPropertyNames` reports the exotics too,
6197 // in V8's order (`length`, `name`, `prototype`, then the rest).
6198 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
6199 let mut keys: Vec<String> = Vec::new();
6200 if !enum_only {
6201 keys.push("length".into());
6202 keys.push("name".into());
6203 if self.owns_prototype(v) {
6204 keys.push("prototype".into());
6205 }
6206 }
6207 let rest: Vec<String> = self
6208 .fn_prop_keys(v)
6209 .into_iter()
6210 // An accessor's ordering marker resolves back to its real
6211 // key, so a static getter enumerates where it was declared.
6212 .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6213 Some(real) => Some(real.to_string()),
6214 None if !k.starts_with("@@") && !k.starts_with('#') => Some(k),
6215 None => None,
6216 })
6217 .filter(|k| {
6218 !keys.contains(k) && (!enum_only || self.prop_attrs(v, k).enumerable)
6219 })
6220 .collect();
6221 keys.extend(rest);
6222 keys
6223 }
6224 // A builtin namespace (`require('buffer')`, `Buffer`) enumerates the
6225 // members node-js implements, so a package that copies a namespace
6226 // key-by-key gets the working set instead of an empty object.
6227 Some(JsObj::Builtin(ns)) => crate::stdlib::namespace_keys(&ns.clone()),
6228 // A `Map`/`Set`/`Promise`/`RegExp`/generator holds only its internal
6229 // slots, so what a script assigned lives in the side table — and is
6230 // just as much an own property as an object's.
6231 Some(_) => self
6232 .fn_prop_keys(v)
6233 .into_iter()
6234 .filter(|k| {
6235 !k.starts_with("@@")
6236 && !k.starts_with('#')
6237 && (!enum_only || self.prop_attrs(v, k).enumerable)
6238 })
6239 .collect(),
6240 _ => Vec::new(),
6241 }
6242 }
6243
6244 /// The own enumerable `(key, value)` pairs of `v`. Buffer index keys resolve
6245 /// through the byte store; everything else reads the property map. Own
6246 /// accessor keys come back as `Undef` here — `own_enum_entries_deep` runs
6247 /// their getters, which cannot happen under the host borrow.
6248 pub fn own_enum_entries(&self, v: &Value) -> Vec<(String, Value)> {
6249 self.own_enum_key_names(v)
6250 .into_iter()
6251 .map(|k| {
6252 let val = match self.get(v) {
6253 // A Buffer's index keys read out of the hidden `@@bytes`
6254 // array; resolve inline rather than through
6255 // `buffer::byte_get`, which would re-borrow the host.
6256 Some(JsObj::Object(props)) => props.get(&k).cloned().unwrap_or_else(|| {
6257 // A Buffer's elements live in `@@bytes` and every
6258 // other typed array's in `@@elems`; both are index
6259 // keys with no entry in the property map.
6260 match k.parse::<usize>() {
6261 Ok(i) => crate::stdlib::typedarray::elems_with_host(self, v)
6262 .get(i)
6263 .cloned()
6264 .unwrap_or(Value::Undef),
6265 _ => Value::Undef,
6266 }
6267 }),
6268 // A Map/Set/Promise/RegExp/generator keeps every own
6269 // property in the side table.
6270 Some(
6271 JsObj::Map { .. }
6272 | JsObj::Set { .. }
6273 | JsObj::Promise { .. }
6274 | JsObj::RegExp(_)
6275 | JsObj::Generator { .. }
6276 | JsObj::Symbol { .. }
6277 | JsObj::BigInt(_)
6278 | JsObj::Iter { .. },
6279 ) => self.fn_prop(v, &k).unwrap_or(Value::Undef),
6280 // An index reads the element; any other own key (`foo`,
6281 // a match result's `index`) lives in the side table.
6282 Some(JsObj::Array(items)) => k
6283 .parse::<usize>()
6284 .ok()
6285 .and_then(|i| items.get(i).cloned())
6286 .or_else(|| self.fn_prop(v, &k))
6287 .unwrap_or(Value::Undef),
6288 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
6289 self.fn_prop(v, &k).unwrap_or(Value::Undef)
6290 }
6291 _ => Value::Undef,
6292 };
6293 (k, val)
6294 })
6295 .collect()
6296 }
6297}
6298
6299/// The own enumerable `(key, value)` pairs of `v` with every enumerable own
6300/// accessor's getter invoked — the observable shape `Object.values`,
6301/// `Object.entries`, object spread and `JSON.stringify` all need. Must be called
6302/// outside a `with_host` borrow because a getter re-enters the host.
6303pub fn own_enum_entries_deep(v: &Value) -> Result<Vec<(String, Value)>, String> {
6304 // A Proxy has no property map at all: its own enumerable entries come from
6305 // the `ownKeys` + `getOwnPropertyDescriptor` + `get` traps. A trap that
6306 // throws surfaces as an empty result here because this signature is
6307 // infallible; the callers that MUST propagate a trap throw (`Object.keys`
6308 // and friends) go through `builtins::object_keys`, which does.
6309 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
6310 return crate::proxy::own_enum_entries(v);
6311 }
6312 // A builtin namespace (`require('path')`, `Buffer`) has no property map at
6313 // all: its members are resolved on demand by `namespace_property`, which
6314 // re-enters the host and so cannot run inside `own_enum_entries`'s borrow.
6315 // Without this, spread and `Object.assign` copied the namespace's KEYS with
6316 // `undefined` for every value — measured against node v26.7.0,
6317 // `{...require('path')}.join` was `undefined` here and a function there,
6318 // while `Object.entries(require('path'))` (which resolves through
6319 // `builtins`, not through this borrow) was already correct. Two enumeration
6320 // paths, one of them silently value-less.
6321 if let Some(ns) = with_host(|h| match h.get(v) {
6322 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6323 _ => None,
6324 }) {
6325 return Ok(with_host(|h| h.own_enum_key_names(v))
6326 .into_iter()
6327 .map(|k| {
6328 let val = crate::builtins::namespace_property(&ns, &k);
6329 (k, val)
6330 })
6331 .collect());
6332 }
6333 // A string primitive's own entries are its code units. `own_enum_entries`
6334 // cannot build them: allocating the one-character string for each index
6335 // needs `&mut` host access, and it runs under a shared borrow.
6336 if let Some(sv) = with_host(|h| match h.get(v) {
6337 Some(JsObj::Str(s)) => Some(s.clone()),
6338 _ => None,
6339 }) {
6340 let units = crate::utf16::Units::of(&sv);
6341 return Ok(with_host(|h| {
6342 (0..units.len())
6343 .filter_map(|i| units.unit_str(i).map(|c| (i.to_string(), h.new_str(c))))
6344 .collect()
6345 }));
6346 }
6347 let accessor_keys: Vec<String> = with_host(|h| {
6348 h.own_accessor_keys(v)
6349 .into_iter()
6350 .filter(|k| h.prop_attrs(v, k).enumerable)
6351 .collect()
6352 });
6353 let entries = with_host(|h| h.own_enum_entries(v));
6354 // A getter that THROWS propagates: `Object.entries`, `Object.assign`,
6355 // object spread and `JSON.stringify` all read through here, and every one
6356 // of them swallowed the exception and reported the property as absent (or
6357 // as `null`) instead.
6358 let mut out = Vec::with_capacity(entries.len());
6359 for (k, val) in entries {
6360 if accessor_keys.contains(&k) {
6361 out.push((k.clone(), get_prop_chain(v, &k)?));
6362 } else {
6363 out.push((k, val));
6364 }
6365 }
6366 Ok(out)
6367}
6368
6369// ── function invocation ──────────────────────────────────────────────────────
6370
6371/// Marshal a JS call argument into a native fusevm `Value` for `rust { }` FFI.
6372/// JS strings ride as `Value::Obj(JsObj::Str)` heap handles, which fusevm's
6373/// marshaller cannot read (it calls `Value::to_str`, which returns `"(obj:N)"`
6374/// for a handle); rewrite them to a native `Value::Str`. Numbers are already
6375/// native `Value::Int`/`Value::Float`, so they pass through (fusevm coerces
6376/// Float→i64/f64 per the export signature).
6377fn marshal_ffi_arg(v: &Value) -> Value {
6378 match v {
6379 Value::Obj(_) => match with_host(|h| h.as_str(v)) {
6380 Some(s) => Value::str(s),
6381 None => v.clone(),
6382 },
6383 _ => v.clone(),
6384 }
6385}
6386
6387/// Resolve a bare name and call it (`f(args)`, `parseInt(args)`).
6388pub fn call_named(name: &str, args: Vec<Value>) -> Result<Value, String> {
6389 // Inline Rust FFI: the `rust { ... }` desugar emits `__rust_compile(b64,
6390 // line)`; compile + register the block's exported functions, returning JS
6391 // `undefined` (`Value::Undef`).
6392 if name == "__rust_compile" {
6393 let b64 = args
6394 .first()
6395 .map(|v| with_host(|h| h.str_of(v)))
6396 .unwrap_or_default();
6397 return fusevm::ffi::compile_and_register(&b64).map(|_| Value::Undef);
6398 }
6399 if let Some(v) = with_host(|h| h.read_name(name)) {
6400 return invoke(&v, args, None);
6401 }
6402 // A DIRECT eval — the literal `eval(src)` call form — is the ONLY one that
6403 // evaluates in the CALLER's scope; `(0, eval)(src)`, `const e = eval; e(src)`
6404 // and `[eval][0](src)` all reach the same function value but are INDIRECT
6405 // evals and evaluate in the global scope (ECMA-262 19.2.1.1 `PerformEval`).
6406 // This is the one place the two forms are distinguishable without a compiler
6407 // change: `call_named` is reached only from `ops::CALL`, which the compiler
6408 // emits exclusively for a bare-identifier callee, while every value-call form
6409 // goes through `invoke` → `call_builtin_function`. The `read_name` miss above
6410 // has already established that `eval` is not shadowed by a user binding.
6411 if name == "eval" {
6412 return crate::builtins::eval_source(args.first(), true);
6413 }
6414 if crate::builtins::is_known_builtin(name) {
6415 return crate::builtins::call_builtin_function(name, args);
6416 }
6417 // A `rust { ... }` block's exported functions are callable by bareword.
6418 // Reached only after user names/globals and builtins all miss, so JS code
6419 // always wins; the registry membership check keeps this off the hot path.
6420 if fusevm::ffi::is_registered(name) {
6421 let margs: Vec<Value> = args.iter().map(marshal_ffi_arg).collect();
6422 if let Some(r) = fusevm::ffi::try_call(name, &margs) {
6423 return r;
6424 }
6425 }
6426 Err(ref_error(name))
6427}
6428
6429thread_local! {
6430 /// The constructor a builtin STATIC is currently being invoked on.
6431 ///
6432 /// `A.from(x)` on `class A extends Array` re-dispatches against the `Array`
6433 /// builtin, which is reached by NAME and so cannot see `A`. The species
6434 /// rules need it: `Array.from`, `Array.of` and every `Promise` static build
6435 /// their result with `this`, so on a subclass they must construct through
6436 /// it. A stack, since one static can call another.
6437 static STATIC_THIS: std::cell::RefCell<Vec<Value>> =
6438 const { std::cell::RefCell::new(Vec::new()) };
6439}
6440
6441/// Run `f` with `recv` recorded as the receiver of a builtin static call.
6442pub fn with_static_this<R>(recv: &Value, f: impl FnOnce() -> R) -> R {
6443 STATIC_THIS.with(|s| s.borrow_mut().push(recv.clone()));
6444 let out = f();
6445 STATIC_THIS.with(|s| {
6446 s.borrow_mut().pop();
6447 });
6448 out
6449}
6450
6451/// The constructor the running builtin static was called on, if it was reached
6452/// through a subclass rather than directly.
6453pub fn current_static_this() -> Option<Value> {
6454 STATIC_THIS.with(|s| s.borrow().last().cloned())
6455}
6456
6457/// `recv.name(args)`.
6458pub fn call_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
6459 // `undefined.foo()` is a `[[Get]]` and THEN a call (13.3.6 EvaluateCall), so
6460 // the failure is the property read, not the call: node reports
6461 // `Cannot read properties of undefined (reading 'foo')`. node-js ran the
6462 // whole method dispatch against the nullish receiver, found nothing, and
6463 // reported `undefined.foo is not a function` — the wrong error class of
6464 // message for the single most common runtime fault in JS, and one that
6465 // points at the callee instead of at the base that was nullish.
6466 if with_host(|h| h.is_nullish(recv)) {
6467 return Err(type_error(&format!(
6468 "Cannot read properties of {} (reading '{name}')",
6469 with_host(|h| h.str_of(recv))
6470 )));
6471 }
6472 // `this.#m(…)` is a `[[PrivateGet]]` followed by a call, so the brand check
6473 // comes first: an unbranded receiver throws here rather than reporting the
6474 // method missing. Only a `#`-prefixed name pays the extra probe.
6475 if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
6476 return Err(crate::builtins::private_brand_message(name, false));
6477 }
6478 // `proxy.m(…)` is 13.3.6 `EvaluateCall`: `Get(proxy, "m")` — through the
6479 // `get` trap — then a call with the PROXY as `this`. The `lookup_*` shortcuts
6480 // below all read a property map a proxy does not have.
6481 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
6482 let f = crate::builtins::get_property(recv, name)?;
6483 if !with_host(|h| is_callable(h, &f)) {
6484 return Err(type_error(&format!("{name} is not a function")));
6485 }
6486 // `Function.prototype.call`/`apply`/`bind`/`toString` and the REFLECTIVE
6487 // `Object.prototype` methods are generic over `this`. node-js models each
6488 // as a thunk BOUND to the object it was read off — through a proxy, that
6489 // is the target — so invoking the thunk answers for the target and skips
6490 // the traps entirely: `pf.call(1, 2)` never reached the `apply` trap and
6491 // `p.hasOwnProperty(k)` never reached the descriptor trap. Re-dispatch
6492 // those against the PROXY, which is the `this` the real method receives.
6493 //
6494 // `toString`/`valueOf`/`toLocaleString` are deliberately NOT re-dispatched
6495 // for a non-callable proxy: they resolve by the TARGET's kind (a proxy of
6496 // an array stringifies `1,2` through `Array.prototype.toString`, not
6497 // `[object Object]`), which the bound thunk already gets right.
6498 if with_host(|h| matches!(h.get(&f), Some(JsObj::BoundMethod { .. }))) {
6499 if with_host(|h| is_callable(h, recv)) {
6500 if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6501 return Ok(r);
6502 }
6503 }
6504 if matches!(
6505 name,
6506 "hasOwnProperty" | "propertyIsEnumerable" | "isPrototypeOf"
6507 ) {
6508 return crate::builtins::object_builtin_method(recv, name, args);
6509 }
6510 // The three above resolve by the TARGET's kind, and the thunk is
6511 // already bound to the target — so it must be invoked WITHOUT a
6512 // receiver override. Passing the proxy as `this` made the
6513 // `BoundMethod` arm of `invoke` prefer it over its own receiver and
6514 // call straight back into this branch, so `String(new Proxy({}, {}))`
6515 // recursed until the stack overflowed and the process aborted.
6516 if matches!(name, "toString" | "valueOf" | "toLocaleString") {
6517 return invoke(&f, args, None);
6518 }
6519 }
6520 return invoke(&f, args, Some(recv.clone()));
6521 }
6522 // Namespace builtins (`console`, `Math`, `JSON`, ...): dispatch by qualified
6523 // name.
6524 if let Some(ns) = with_host(|h| match h.get(recv) {
6525 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6526 _ => None,
6527 }) {
6528 let qualified = format!("{ns}.{name}");
6529 if crate::builtins::is_known_builtin(&qualified) {
6530 return crate::builtins::call_builtin_function(&qualified, args);
6531 }
6532 }
6533 // Object / instance: an accessor getter that yields a function, an own or
6534 // inherited method (class methods live on the prototype chain), then an
6535 // Object.prototype builtin (hasOwnProperty …). Resolve via `lookup_*`
6536 // directly — NOT get_property — so the Object.prototype-builtin fallback
6537 // never routes back through a BoundMethod and recurses.
6538 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
6539 // A native stdlib instance (`Buffer`/crypto `Hash`/`EventEmitter`/`URL`/
6540 // fs `Stats`/http `ServerResponse`…) carries a hidden `@@native` tag.
6541 // A user-added or reparented-prototype method takes precedence over the
6542 // native dispatcher — matching JS resolution order (own → prototype
6543 // chain). This is what lets Express work: it does
6544 // `Object.setPrototypeOf(res, app.response)` and calls `res.send(...)`,
6545 // where `send` is a plain function on the reparented prototype. Native
6546 // instance methods (`res.end`/`write`/…) are NOT stored as plain
6547 // function properties, so `lookup_chain` misses them and we fall through
6548 // to `instance_call` for the real native behavior.
6549 if let Some(tag) = crate::stdlib::native_tag(recv) {
6550 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6551 if with_host(|h| is_callable(h, &f)) {
6552 return invoke(&f, args, Some(recv.clone()));
6553 }
6554 }
6555 // `Object.prototype` methods reach a native instance too — a Buffer
6556 // inherits `hasOwnProperty`/`isPrototypeOf` through its prototype
6557 // chain, and the native dispatcher has no entry for them.
6558 if crate::builtins::is_object_builtin_method(name)
6559 && !crate::stdlib::instance_has_method(&tag, name)
6560 {
6561 return crate::builtins::object_builtin_method(recv, name, args);
6562 }
6563 return crate::stdlib::instance_call(&tag, recv, name, args);
6564 }
6565 // A primitive wrapper forwards to the primitive's method table, the
6566 // same way a native instance forwards to its tag's. A user method on
6567 // the wrapper or anywhere on its chain still wins first.
6568 if let Some(prim) = crate::builtins::wrapped_primitive(recv) {
6569 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6570 if with_host(|h| is_callable(h, &f)) {
6571 return invoke(&f, args, Some(recv.clone()));
6572 }
6573 }
6574 // The reflective `Object.prototype` methods answer for the WRAPPER
6575 // — `w.hasOwnProperty("0")` asks about the wrapper's own index
6576 // properties, not about the string.
6577 if crate::builtins::is_object_builtin_method(name) {
6578 return crate::builtins::object_builtin_method(recv, name, args);
6579 }
6580 return call_method(&prim, name, args);
6581 }
6582 if let Some((Some(getter), _)) = with_host(|h| lookup_accessor(h, recv, name)) {
6583 let f = invoke(&getter, Vec::new(), Some(recv.clone()))?;
6584 if with_host(|h| is_callable(h, &f)) {
6585 return invoke(&f, args, Some(recv.clone()));
6586 }
6587 }
6588 // A Proxy in the prototype chain serves the method through its `get`
6589 // trap. `lookup_chain` below reads property maps, which a proxy has none
6590 // of, so without this `child.m()` on `Object.create(proxy)` reported
6591 // "m is not a function" even though `child.m` already read correctly.
6592 if crate::builtins::proxy_proto_link(recv, name).is_some() {
6593 let f = crate::builtins::get_property(recv, name)?;
6594 if !with_host(|h| is_callable(h, &f)) {
6595 return Err(type_error(&format!("{name} is not a function")));
6596 }
6597 return invoke(&f, args, Some(recv.clone()));
6598 }
6599 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6600 if with_host(|h| is_callable(h, &f)) {
6601 return invoke(&f, args, Some(recv.clone()));
6602 }
6603 return Err(type_error(&format!("{name} is not a function")));
6604 }
6605 // A method patched onto `Object.prototype`. `lookup_chain` cannot find
6606 // it: a plain object is not LINKED to the intrinsic prototype object,
6607 // its `Object.prototype` members are synthesized instead. So
6608 // `Object.prototype.tap = f; ({}).tap()` reported "is not a function"
6609 // while `({}).tap` already read back as `f`.
6610 if let Some(f) = crate::builtins::inherited_builtin_static(recv, name) {
6611 if with_host(|h| is_callable(h, &f)) {
6612 return invoke(&f, args, Some(recv.clone()));
6613 }
6614 }
6615 // A method from an intrinsic prototype this object's CHAIN passes
6616 // through — `Object.create(Array.prototype).push(1)`. The read already
6617 // resolves it through the same owner oracle; dispatch reported "is not
6618 // a function", the read and the call disagreeing once more.
6619 if let Some(owner) = crate::builtins::inherited_method_owner_pub(recv, name) {
6620 if owner != "Object" {
6621 return crate::builtins::proto_method(recv, &format!("{owner}:{name}"), args);
6622 }
6623 }
6624 if crate::builtins::is_object_builtin_method(name) {
6625 return crate::builtins::object_builtin_method(recv, name, args);
6626 }
6627 if name == "constructor" {
6628 if let Some(r) = call_default_ctor(recv, &args) {
6629 return r;
6630 }
6631 }
6632 return Err(type_error(&format!("{name} is not a function")));
6633 }
6634 // Function value methods: call / apply / bind, then any static method stored
6635 // on the function object.
6636 if matches!(
6637 with_host(|h| h.kind_of(recv)),
6638 Some(ObjKind::Func)
6639 | Some(ObjKind::Class)
6640 | Some(ObjKind::BoundFunc)
6641 | Some(ObjKind::BoundMethod)
6642 | Some(ObjKind::Builtin)
6643 ) {
6644 if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6645 return Ok(r);
6646 }
6647 // A static method (own or inherited): `this` is the constructor (`recv`).
6648 let stat = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6649 with_host(|h| h.class_static(recv, name))
6650 } else {
6651 with_host(|h| h.fn_prop(recv, name))
6652 };
6653 if let Some(f) = stat {
6654 if with_host(|h| is_callable(h, &f)) {
6655 return invoke(&f, args, Some(recv.clone()));
6656 }
6657 }
6658 // `class_static` only walks user-class `extends` links, so a chain that
6659 // bottoms out in a BUILTIN constructor (`class D extends Array {}`)
6660 // could not reach that builtin's statics: `D.from([1,2])` threw
6661 // "from is not a function" even though `typeof D.from` said `function`.
6662 // Re-dispatch the call against that ancestor, which is what reaches a
6663 // builtin namespace's methods.
6664 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6665 if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
6666 if with_host(|h| h.kind_of(&anc)) == Some(ObjKind::Builtin) {
6667 // The subclass is recorded so a species-aware static
6668 // (`Array.from`, `Promise.resolve`, …) builds its result
6669 // through it rather than through the builtin.
6670 return with_static_this(recv, || call_method(&anc, name, args));
6671 }
6672 }
6673 }
6674 // A method inherited via the function's [[Prototype]] chain (set with
6675 // `Object.setPrototypeOf(fn, proto)`) — the `router` package's router
6676 // functions inherit `route`/`use`/`get`/… from `Router.prototype`.
6677 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6678 if with_host(|h| is_callable(h, &f)) {
6679 return invoke(&f, args, Some(recv.clone()));
6680 }
6681 }
6682 // An `Object.prototype` method invoked with a builtin namespace/prototype
6683 // as `this` (`hasOwnProperty.call(Map.prototype, 'get')`, the get-intrinsic
6684 // ownership probe) — dispatch it against the builtin receiver.
6685 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin)
6686 && crate::builtins::is_object_builtin_method(name)
6687 {
6688 return crate::builtins::object_builtin_method(recv, name, args);
6689 }
6690 }
6691 if name == "constructor" {
6692 if let Some(r) = call_default_ctor(recv, &args) {
6693 return r;
6694 }
6695 }
6696 // Type methods (array/string/number, Map/Set/Symbol/generator methods).
6697 crate::builtins::call_type_method(recv, name, args)
6698}
6699
6700/// `x.constructor(...)` invoked as a CALL when nothing on `x`'s prototype chain
6701/// owns a `constructor` slot.
6702///
6703/// Reading the property already resolves a builtin instance's native constructor
6704/// (the `constructor` arm of `builtins::get_property`), but the CALL path only
6705/// consulted the prototype chain, so the two disagreed:
6706/// `(function(){}).constructor === Function` read `true` while
6707/// `(function(){}).constructor('return 9')` threw
6708/// `TypeError: constructor is not a function`. That call form is exactly how
6709/// `get-intrinsic` — a transitive dependency of express — reaches the `Function`
6710/// constructor. Resolved here through the same one definition the read uses, so
6711/// the two can no longer drift apart. `None` means "not resolvable/callable",
6712/// leaving the caller's original error in place.
6713fn call_default_ctor(recv: &Value, args: &[Value]) -> Option<Result<Value, String>> {
6714 let ctor = crate::builtins::get_property(recv, "constructor").ok()?;
6715 with_host(|h| is_callable(h, &ctor)).then(|| invoke(&ctor, args.to_vec(), None))
6716}
6717
6718/// Call any callable value.
6719pub fn invoke(callable: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6720 // `[[Call]]` on a Proxy runs the `apply` trap (or forwards to the target).
6721 // Probed by kind first so the ordinary call path never clones its arguments.
6722 if with_host(|h| h.kind_of(callable)) == Some(ObjKind::Proxy) {
6723 return crate::proxy::apply(callable, args, this).map(|r| r.expect("kind_of said Proxy"));
6724 }
6725 let obj = with_host(|h| h.get(callable).cloned());
6726 match obj {
6727 // A builtin-prototype method thunk (`Object.prototype.toString`): dispatch
6728 // against the invoke-time `this` (supplied by `.call`/`.apply`).
6729 Some(JsObj::Builtin(name)) if name.starts_with("@proto:") => {
6730 let recv = this.unwrap_or(Value::Undef);
6731 crate::builtins::proto_method(&recv, &name["@proto:".len()..], args)
6732 }
6733 // An intrinsic prototype's GETTER, borrowed off its descriptor — the
6734 // form a library uses to read a slot from an arbitrary receiver
6735 // (`Object.getOwnPropertyDescriptor(Map.prototype, 'size').get
6736 // .call(m)`). It brand-checks `this` and reads, or throws naming
6737 // itself.
6738 // The setter half of the `arguments`/`caller` poison pill — the only
6739 // intrinsic accessor here that has one, and it throws like its getter.
6740 Some(JsObj::Builtin(name)) if name.starts_with("@protoset:") => {
6741 let _ = &name;
6742 let recv = this.unwrap_or(Value::Undef);
6743 // The setter half accepts silently for the same receivers the
6744 // getter answers for, and throws for the rest.
6745 if with_host(|h| h.fn_is_sloppy(&recv)) {
6746 Ok(Value::Undef)
6747 } else {
6748 Err(type_error(crate::builtins::POISON_PILL))
6749 }
6750 }
6751 Some(JsObj::Builtin(name)) if name.starts_with("@protoget:") => {
6752 let recv = this.unwrap_or(Value::Undef);
6753 let rest = &name["@protoget:".len()..];
6754 let (ctor, key) = rest.split_once(':').unwrap_or((rest, ""));
6755 crate::builtins::proto_getter_call(ctor, key, &recv)
6756 }
6757 // `NativeCtor.call(obj, …)` — ES5 "constructor stealing", still shipped by
6758 // libraries that predate `class`. `iconv-lite`'s internal codec is exactly
6759 // this:
6760 //
6761 // function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
6762 // InternalDecoder.prototype = StringDecoder.prototype;
6763 //
6764 // A native constructor builds a fresh tagged object, so initializing the
6765 // SUPPLIED object means building one and moving its slots across.
6766 //
6767 // The guard is deliberately narrow: `obj` must already inherit from THIS
6768 // constructor's prototype, i.e. the subclass really did adopt it. Without
6769 // that, `Date.call(x)` and `Buffer.call(x)` — which in JS ignore `this` and
6770 // return a string / a buffer — would start mutating `x` instead.
6771 Some(JsObj::Builtin(ref name)) if steals_ctor(name, this.as_ref()) => {
6772 let target = this.expect("guard checked");
6773 let built = crate::stdlib::construct(name, &args)
6774 .expect("guard checked a native constructor")?;
6775 adopt_native_slots(&target, &built);
6776 Ok(Value::Undef)
6777 }
6778 Some(JsObj::Builtin(name)) => crate::builtins::call_builtin_function(&name, args),
6779 Some(JsObj::Func(fv)) => run_user_func_of(&fv, args, this, Some(callable.clone())),
6780 // A method read off an object is modelled as a thunk BOUND to it, but an
6781 // explicit `.call`/`.apply` receiver still wins — `Function.prototype.call`
6782 // rebinds `this`, and every `Array.prototype` method is generic over it, so
6783 // `[].slice.call(arrayLike)` must run against the ARGUMENT. Dropping the
6784 // override made that read back as the empty array the thunk was read off.
6785 // A nullish override is ignored: it carries no receiver to dispatch on.
6786 Some(JsObj::BoundMethod { recv, name }) => {
6787 let target = match &this {
6788 Some(t) if !matches!(t, Value::Undef) && !with_host(|h| h.is_null(t)) => t,
6789 _ => &recv,
6790 };
6791 // A thunk read off an ARRAY carries an `Array.prototype` method, and
6792 // those are generic over `this` — route the rebound call through
6793 // `proto_method` so an array-LIKE receiver takes the generic path
6794 // instead of being told the method does not exist.
6795 if with_host(|h| h.kind_of(&recv)) == Some(ObjKind::Array) {
6796 return crate::builtins::proto_method(target, &format!("Array:{name}"), args);
6797 }
6798 call_method(target, &name, args)
6799 }
6800 Some(JsObj::BoundFunc {
6801 target,
6802 this: bthis,
6803 args: pre,
6804 }) => {
6805 let mut all = pre;
6806 all.extend(args);
6807 invoke(&target, all, Some(bthis))
6808 }
6809 Some(JsObj::Class(c)) => Err(type_error(&format!(
6810 "Class constructor {} cannot be invoked without 'new'",
6811 c.name
6812 ))),
6813 _ => Err(type_error(&format!(
6814 "{} is not a function",
6815 with_host(|h| h.str_of(callable))
6816 ))),
6817 }
6818}
6819
6820/// Whether calling the native constructor `name` with `this` is the ES5
6821/// constructor-stealing pattern rather than an ordinary call.
6822///
6823/// True only when `name` really is a native stdlib constructor AND `this` is a
6824/// plain object that already inherits from that constructor's prototype — the
6825/// signature of `Sub.prototype = Native.prototype; Native.call(this, …)`. An
6826/// object that merely happens to be passed as `this` does not qualify, so
6827/// `Date.call(x)` / `Buffer.call(x)` keep their JS meaning (ignore `this`).
6828fn steals_ctor(name: &str, this: Option<&Value>) -> bool {
6829 let Some(target) = this else { return false };
6830 if !with_host(|h| matches!(h.get(target), Some(JsObj::Object(_)))) {
6831 return false;
6832 }
6833 // Already initialized (e.g. a re-entrant call) — nothing to steal.
6834 if crate::stdlib::native_tag(target).is_some() {
6835 return false;
6836 }
6837 let Some(proto) = with_host(|h| h.ensure_ctor_proto(name)) else {
6838 return false;
6839 };
6840 let mut cur = with_host(|h| h.proto_of(target));
6841 while let Some(p) = cur {
6842 if p == proto {
6843 return true;
6844 }
6845 cur = with_host(|h| h.proto_of(&p));
6846 }
6847 false
6848}
6849
6850/// Move a freshly-constructed native instance's state onto `target`, so an
6851/// object built by a subclass constructor becomes a working instance of the
6852/// native class. Copies every own key the native constructor set — the hidden
6853/// `@@`-prefixed slots that carry the state AND the plain ones it exposes
6854/// (`StringDecoder`'s `encoding`) — without disturbing keys `target` already has.
6855fn adopt_native_slots(target: &Value, built: &Value) {
6856 let slots: Vec<(String, Value)> = with_host(|h| match h.get(built) {
6857 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
6858 _ => Vec::new(),
6859 });
6860 with_host(|h| {
6861 if let Some(JsObj::Object(p)) = h.get_mut(target) {
6862 for (k, v) in slots {
6863 p.insert(k, v);
6864 }
6865 }
6866 });
6867}
6868
6869/// Execute a user function/closure body on a fresh frame.
6870pub fn run_user_func(fv: &FuncVal, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6871 run_user_func_of(fv, args, this, None)
6872}
6873
6874/// [`run_user_func`] with the function VALUE the call came through, which the
6875/// `arguments` object needs for its `callee`.
6876pub fn run_user_func_of(
6877 fv: &FuncVal,
6878 args: Vec<Value>,
6879 this: Option<Value>,
6880 callee: Option<Value>,
6881) -> Result<Value, String> {
6882 run_user_func_full(fv, args, this, None, callee)
6883}
6884
6885/// As `run_user_func`, but with an explicit `new.target` (set by `new`).
6886pub fn run_user_func_nt(
6887 fv: &FuncVal,
6888 args: Vec<Value>,
6889 this: Option<Value>,
6890 new_target: Option<Value>,
6891) -> Result<Value, String> {
6892 run_user_func_full(fv, args, this, new_target, None)
6893}
6894
6895fn run_user_func_full(
6896 fv: &FuncVal,
6897 args: Vec<Value>,
6898 this: Option<Value>,
6899 new_target: Option<Value>,
6900 callee: Option<Value>,
6901) -> Result<Value, String> {
6902 // Consumed first, before anything here can start another call.
6903 let derived_ctor = with_host(|h| std::mem::take(&mut h.derived_ctor_next));
6904 // Only the light fields: cloning the whole `FuncDef` cloned its `Chunk` —
6905 // the entire compiled body, `sub_chunks` and all — on every single call.
6906 // The chunk is now reached once per pooled VM, in the two arms below.
6907 let (params, is_generator, is_async, is_arrow_def, def_name) = with_host(|h| {
6908 let d = &h.funcs[fv.def_id];
6909 (
6910 d.params.clone(),
6911 d.is_generator,
6912 d.is_async,
6913 d.is_arrow,
6914 d.name.clone(),
6915 )
6916 });
6917 let env = new_env(fv.env.clone());
6918 // Bind the simple/rest arg slots; destructuring + defaults run in the body
6919 // prologue (compiled ahead of the user statements).
6920 let fn_is_sloppy = with_host(|h| !h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
6921 bind_params(
6922 &env,
6923 ¶ms,
6924 args,
6925 is_arrow_def,
6926 callee.as_ref(),
6927 fn_is_sloppy && !is_arrow_def,
6928 );
6929 // Arrow functions capture `this` lexically; regular functions receive it.
6930 let mut this_val = if fv.is_arrow { fv.this.clone() } else { this };
6931 // 10.2.1.2 OrdinaryCallBindThis: in SLOPPY mode an absent or nullish `this`
6932 // becomes the global object. Only a strict function keeps `undefined`, and
6933 // an arrow has no `this` of its own to substitute. Leaving it undefined
6934 // meant a plain `f()`, a detached method, a callback and `f.call(null)` all
6935 // saw `undefined` where node sees `globalThis`.
6936 let sloppy_this = !fv.is_arrow
6937 && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict))
6938 && match &this_val {
6939 None => true,
6940 Some(v) => matches!(v, Value::Undef) || with_host(|h| h.is_null(v)),
6941 };
6942 if sloppy_this {
6943 this_val = Some(with_host(|h| h.global_object()));
6944 } else if !fv.is_arrow && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)) {
6945 // The other half of OrdinaryCallBindThis: a SLOPPY function boxes a
6946 // primitive `this` with `ToObject`, so `f.call(5)` sees a `Number`
6947 // wrapper rather than the number. Only strict mode passes it through.
6948 if let Some(t) = this_val.clone() {
6949 let boxed = crate::builtins::to_object(&t);
6950 this_val = Some(boxed);
6951 }
6952 }
6953 // A generator function does not run its body on call — it returns a suspended
6954 // generator over the already-bound frame.
6955 if is_generator {
6956 let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
6957 let gen = make_generator(
6958 chunk,
6959 env,
6960 this_val,
6961 fv.home_class.clone(),
6962 fv.home_static,
6963 fv.home_object.clone(),
6964 with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
6965 );
6966 if is_async {
6967 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(&gen).cloned()) {
6968 with_host(|h| h.generators[id as usize].async_gen = true);
6969 }
6970 }
6971 return Ok(gen);
6972 }
6973 // An async function runs on a coroutine and returns a Promise: it executes
6974 // synchronously up to the first `await`, then continues via microtasks.
6975 if is_async {
6976 let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
6977 let gen = make_generator(
6978 chunk,
6979 env,
6980 this_val,
6981 fv.home_class.clone(),
6982 fv.home_static,
6983 fv.home_object.clone(),
6984 with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
6985 );
6986 return Ok(run_async(gen));
6987 }
6988 let home = fv
6989 .home_class
6990 .as_ref()
6991 .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
6992 // Resolved BEFORE the borrow below: reading the function table re-enters
6993 // the host, and doing it inside the frame-push closure double-borrows.
6994 let fn_strict = with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
6995 with_host(|h| {
6996 h.frames.push(Frame {
6997 base_env: env.clone(),
6998 env,
6999 this_obj: this_val,
7000 new_target,
7001 home_class: home,
7002 home_static: fv.home_static,
7003 home_object: fv.home_object.clone(),
7004 strict: fn_strict,
7005 line: 0,
7006 owner: Some(def_name),
7007 is_module: false,
7008 this_state: if derived_ctor {
7009 ThisState::Pending
7010 } else {
7011 ThisState::Plain
7012 },
7013 })
7014 });
7015 let r = run_chunk_keyed(func_key(fv.def_id), || {
7016 with_host(|h| h.funcs[fv.def_id].chunk.clone())
7017 });
7018 let (sig, this_state) = with_host(|h| {
7019 let frame = h.frames.pop();
7020 (h.signal.take(), frame.map(|f| f.this_state))
7021 });
7022 let ret = match r {
7023 Err(e) => return Err(e),
7024 Ok(_) => match sig {
7025 Some(Signal::Return(v)) => v,
7026 _ => Value::Undef,
7027 },
7028 };
7029 // 10.2.2 [[Construct]] steps 10-12 for a derived constructor: an object
7030 // return wins; any other non-undefined return is a TypeError; and falling
7031 // off the end (or `return;`) needs `this` to have been bound by `super()`.
7032 if derived_ctor && !returns_object(&ret) {
7033 if !matches!(ret, Value::Undef) {
7034 return Err(type_error(
7035 "Derived constructors may only return object or undefined",
7036 ));
7037 }
7038 if this_state == Some(ThisState::Pending) {
7039 return Err(this_before_super_error());
7040 }
7041 }
7042 Ok(ret)
7043}
7044
7045/// Bind positional args into a fresh call environment. The compiler emits the
7046/// param names in `def.params`; a `...rest` slot collects the tail as an array.
7047fn bind_params(
7048 env: &Env,
7049 params: &[ParamSlot],
7050 args: Vec<Value>,
7051 is_arrow: bool,
7052 callee: Option<&Value>,
7053 sloppy: bool,
7054) {
7055 let mut vars = VarMap::default();
7056 let mut i = 0;
7057 for slot in params {
7058 if slot.rest {
7059 let rest: Vec<Value> = args.get(i..).map(|s| s.to_vec()).unwrap_or_default();
7060 let arr = with_host(|h| h.new_array(rest));
7061 vars.insert(slot.name.clone(), arr);
7062 } else {
7063 let v = args.get(i).cloned().unwrap_or(Value::Undef);
7064 vars.insert(slot.name.clone(), v);
7065 i += 1;
7066 }
7067 }
7068 // `arguments` array (simple approximation — see BUGS.md: it is a real
7069 // Array, not an Arguments exotic). An ARROW function never gets one:
7070 // `FunctionDeclarationInstantiation` (10.2.11) creates the binding only for
7071 // a non-arrow, so `arguments` inside an arrow resolves lexically to the
7072 // enclosing function's. Binding an empty one here made
7073 // `function f(){ const g = () => [...arguments]; }` see zero args.
7074 if !is_arrow {
7075 let args_arr = with_host(|h| {
7076 let a = h.new_array(args);
7077 // Marked so it can be told apart from an ordinary array: node's
7078 // `arguments` is an exotic, and without the mark
7079 // `Array.isArray(arguments)` was true, the brand was
7080 // `[object Array]` and `util.types.isArgumentsObject` was false.
7081 // The backing representation stays an Array, which is what keeps
7082 // indices, `length`, spread and `for-of` working.
7083 h.set_fn_prop(&a, "@@arguments", Value::Bool(true));
7084 // `callee` is the function itself in SLOPPY code (it is a poison
7085 // pill only in strict, which the read path handles). It read back
7086 // `undefined`, so the pre-`class` self-reference idiom
7087 // `(function(){ arguments.callee })` found nothing.
7088 if let Some(f) = callee {
7089 if sloppy {
7090 h.set_fn_prop(&a, "@@callee", f.clone());
7091 }
7092 }
7093 a
7094 });
7095 vars.entry("arguments".to_string()).or_insert(args_arr);
7096 }
7097 env.borrow_mut().vars = vars;
7098}
7099
7100/// Construct an instance with `new` — creates a fresh object, binds it as
7101/// `this`, runs the constructor, and returns the object (unless the constructor
7102/// returns its own object).
7103pub fn construct(ctor: &Value, args: Vec<Value>) -> Result<Value, String> {
7104 construct_nt(ctor, args, ctor.clone())
7105}
7106
7107/// `new` with an explicit `new.target` (differs from `ctor` when a derived class
7108/// calls `super(...)` — the target stays the originally-`new`ed class).
7109pub fn construct_nt(ctor: &Value, args: Vec<Value>, new_target: Value) -> Result<Value, String> {
7110 // `new proxy(…)` runs the `construct` trap (or forwards to the target).
7111 if with_host(|h| h.kind_of(ctor)) == Some(ObjKind::Proxy) {
7112 return crate::proxy::construct(ctor, args, &new_target)
7113 .map(|r| r.expect("kind_of said Proxy"));
7114 }
7115 let obj = with_host(|h| h.get(ctor).cloned());
7116 match obj {
7117 Some(JsObj::Class(_)) => construct_class(ctor, args, new_target),
7118 Some(JsObj::Func(fv)) => {
7119 // Only an ORDINARY function has a `[[Construct]]` slot. An arrow, a
7120 // `function*` and an `async function` are callable but not
7121 // constructable (10.2.2 is installed only for the ordinary case), so
7122 // `new` on one is a TypeError — node-js instead ran the body and
7123 // handed back a half-built instance (for a generator, an object whose
7124 // constructor had returned a suspended generator).
7125 let non_ctor = with_host(|h| {
7126 h.funcs
7127 .get(fv.def_id)
7128 // A MethodDefinition is in the same boat: `new ({m(){}}).m()`
7129 // is `TypeError: o.m is not a constructor` on node v26.7.0,
7130 // which is also why a method owns no `prototype`.
7131 .map(|d| d.is_generator || d.is_async || d.is_method)
7132 .unwrap_or(false)
7133 });
7134 if fv.is_arrow || non_ctor {
7135 return Err(not_a_constructor(ctor));
7136 }
7137 // A plain constructor function: instance delegates to `fn.prototype`
7138 // (auto-created with a `.constructor` back-link if not yet accessed).
7139 let inst = with_host(|h| {
7140 let o = h.new_object(IndexMap::new());
7141 let proto = h.fn_prop(ctor, "prototype").unwrap_or_else(|| {
7142 let p = h.new_object(IndexMap::new());
7143 if let Some(JsObj::Object(pp)) = h.get_mut(&p) {
7144 pp.insert("constructor".to_string(), ctor.clone());
7145 }
7146 // `F.prototype.constructor` is non-enumerable in JS.
7147 h.hide_prop(&p, "constructor");
7148 h.set_fn_prop(ctor, "prototype", p.clone());
7149 p
7150 });
7151 h.set_proto(&o, proto);
7152 o
7153 });
7154 let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target))?;
7155 if returns_object(&r) {
7156 Ok(r)
7157 } else {
7158 Ok(inst)
7159 }
7160 }
7161 Some(JsObj::Builtin(name)) => crate::builtins::construct_builtin(&name, args),
7162 Some(JsObj::BoundFunc {
7163 target, args: pre, ..
7164 }) => {
7165 let mut all = pre;
7166 all.extend(args);
7167 construct_nt(&target, all, new_target)
7168 }
7169 _ => Err(not_a_constructor(ctor)),
7170 }
7171}
7172
7173/// `TypeError: <callee> is not a constructor`.
7174///
7175/// V8 names the callee by its SOURCE TEXT (`new g()` reports `g`, `new o.m()`
7176/// reports `o.m`); node-js keeps no spans, so a named callable is reported by
7177/// its name — the same string in the common case — and anything else by its
7178/// value.
7179fn not_a_constructor(ctor: &Value) -> String {
7180 let name = with_host(|h| match h.callable_name(ctor) {
7181 n if n.is_empty() => h.str_of(ctor),
7182 n => n,
7183 });
7184 type_error(&format!("{name} is not a constructor"))
7185}
7186
7187/// Whether a constructor's return value is an object (so `new` yields it instead
7188/// of the fresh instance). In JS "object" includes functions — the `router`
7189/// package's constructor `return router` (a function) must be honored, or the
7190/// returned router loses its callable identity.
7191fn returns_object(r: &Value) -> bool {
7192 matches!(
7193 with_host(|h| h.get(r).cloned()),
7194 Some(JsObj::Object(_))
7195 | Some(JsObj::Array(_))
7196 | Some(JsObj::Map { .. })
7197 | Some(JsObj::Set { .. })
7198 | Some(JsObj::Func(_))
7199 | Some(JsObj::Class(_))
7200 | Some(JsObj::BoundFunc { .. })
7201 | Some(JsObj::BoundMethod { .. })
7202 | Some(JsObj::RegExp(_))
7203 )
7204}
7205
7206/// Construct a `class` instance: allocate the object linked to `C.prototype`,
7207/// run field initializers + the constructor (which may call `super(...)`).
7208fn construct_class(
7209 class_val: &Value,
7210 args: Vec<Value>,
7211 new_target: Value,
7212) -> Result<Value, String> {
7213 let cv = match with_host(|h| h.get(class_val).cloned()) {
7214 Some(JsObj::Class(c)) => c,
7215 _ => return Err(type_error("not a class")),
7216 };
7217 // Resolve the prototype of the *most-derived* class being `new`ed, so an
7218 // instance created through a `super()` chain still delegates to the leaf
7219 // prototype (correct method resolution).
7220 let leaf_proto = match with_host(|h| h.get(&new_target).cloned()) {
7221 Some(JsObj::Class(c)) => c.proto.clone(),
7222 _ => cv.proto.clone(),
7223 };
7224 let inst = with_host(|h| {
7225 let o = h.new_object(IndexMap::new());
7226 h.set_proto(&o, leaf_proto.clone());
7227 o
7228 });
7229 // A `super()` deeper in may substitute the instance; the previous value is
7230 // restored so a `new` inside a constructor body cannot be mistaken for one.
7231 let saved = with_host(|h| h.swap_super_replacement(None));
7232 let ran = run_class_ctor(&cv, &inst, args, &new_target);
7233 let substituted = with_host(|h| {
7234 let s = h.take_super_replacement();
7235 h.swap_super_replacement(saved);
7236 s
7237 });
7238 // A constructor that returns an object replaces the instance (`new`
7239 // semantics); failing that, whatever `super()` substituted for it.
7240 match ran? {
7241 Some(obj) if returns_object(&obj) => Ok(obj),
7242 _ => Ok(substituted.unwrap_or(inst)),
7243 }
7244}
7245
7246/// Run one class's field initializers then its constructor on an existing
7247/// instance. Returns the constructor's explicit object return (if any). For a
7248/// base class this is the whole init; for a derived class the constructor body
7249/// reaches `super(...)` which recurses into the parent.
7250fn run_class_ctor(
7251 cv: &ClassVal,
7252 inst: &Value,
7253 args: Vec<Value>,
7254 new_target: &Value,
7255) -> Result<Option<Value>, String> {
7256 // A derived class must run its fields AFTER super() returns; SUPER_CALL does
7257 // that. A base class initializes fields before the constructor body.
7258 if cv.parent.is_none() {
7259 init_fields(cv, inst)?;
7260 }
7261 match &cv.ctor {
7262 Some(ctor_fn) => {
7263 let fv = match with_host(|h| h.get(ctor_fn).cloned()) {
7264 Some(JsObj::Func(f)) => f,
7265 _ => return Err(type_error("class constructor is not a function")),
7266 };
7267 if cv.parent.is_some() {
7268 with_host(|h| h.mark_next_call_derived_ctor());
7269 }
7270 let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7271 return Ok(Some(r));
7272 }
7273 None => {
7274 // Default constructor: `constructor(...a){ super(...a); }` for a
7275 // derived class, empty for a base class.
7276 if let Some(parent) = &cv.parent {
7277 // A base constructor's returned object becomes the instance, so
7278 // the implicit `constructor(...a){ super(...a) }` hands it on.
7279 if let Some(replacement) = super_construct(parent, args, inst, new_target)? {
7280 init_fields(cv, &replacement)?;
7281 return Ok(Some(replacement));
7282 }
7283 init_fields(cv, inst)?;
7284 }
7285 }
7286 }
7287 Ok(None)
7288}
7289
7290/// Evaluate and assign a class's instance-field initializers on `inst`.
7291fn init_fields(cv: &ClassVal, inst: &Value) -> Result<(), String> {
7292 for (name, thunk, name_anon) in &cv.fields {
7293 init_one_field(inst, name, thunk, *name_anon)?;
7294 }
7295 Ok(())
7296}
7297
7298/// Evaluate ONE instance-field initializer thunk and install the result on
7299/// `inst`.
7300///
7301/// Shared by the base-class path (`init_fields`) and the derived-class path
7302/// that runs after `super(...)`; the two used to be separate loops, and only the
7303/// first canonicalized an array-index key.
7304///
7305/// `name_anon` carries 15.7.10's NamedEvaluation: `class C { f = function(){} }`
7306/// gives the function the name `f`. It is decided by the compiler from the
7307/// syntax, never from the value.
7308pub fn init_one_field(
7309 inst: &Value,
7310 name: &str,
7311 thunk: &Value,
7312 name_anon: bool,
7313) -> Result<(), String> {
7314 // The thunk is an arrow capturing the class scope; run it with `this`=inst
7315 // so `this.other`-referencing initializers work.
7316 let val = invoke(thunk, Vec::new(), Some(inst.clone()))?;
7317 with_host(|h| {
7318 if name_anon {
7319 let s = h.new_str(name.to_string());
7320 h.set_fn_prop(&val, "name", s);
7321 }
7322 if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7323 let is_new = !props.contains_key(name);
7324 props.insert(name.to_string(), val);
7325 if is_new && array_index(name).is_some() {
7326 canonicalize_own_keys(props);
7327 }
7328 }
7329 });
7330 Ok(())
7331}
7332
7333/// Run a parent constructor as part of `super(...)`: dispatch on the parent's
7334/// kind (class vs plain function vs builtin) using the existing instance.
7335/// Run the parent constructor against `inst`.
7336///
7337/// Returns the object the parent's `[[Construct]]` produced when that is NOT
7338/// `inst` — a base constructor is allowed to `return` one, and 15.7.15 makes it
7339/// the derived instance too. The caller rebinds `this` to it, so the rest of the
7340/// derived constructor writes to the object `new` will hand back.
7341pub fn super_construct(
7342 parent: &Value,
7343 args: Vec<Value>,
7344 inst: &Value,
7345 new_target: &Value,
7346) -> Result<Option<Value>, String> {
7347 match with_host(|h| h.get(parent).cloned()) {
7348 Some(JsObj::Class(pcv)) => Ok(run_class_ctor(&pcv, inst, args, new_target)?
7349 .filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst)))),
7350 Some(JsObj::Func(fv)) => {
7351 let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7352 Ok(Some(r).filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst))))
7353 }
7354 Some(JsObj::Builtin(name)) => {
7355 let built = crate::builtins::construct_builtin(&name, args)?;
7356 // An EXOTIC parent (`class A extends Array`) keeps its behaviour in
7357 // the heap variant, not in a property map, so copying own props
7358 // cannot carry it: the instance has to BECOME the built object.
7359 // Without this `new (class extends Array {})().push` was not a
7360 // function, and the same for Map, Set, RegExp, Promise and
7361 // Function — subclassing a builtin produced a plain object.
7362 if !become_exotic(inst, &built) {
7363 // An `Error` subclass is ordinary: its state IS own properties.
7364 adopt_own_props(inst, &built);
7365 }
7366 Ok(None)
7367 }
7368 // A Proxy parent (`class D extends new Proxy(B, {})`): `super(…)` is
7369 // `[[Construct]]` on the proxy, so the `construct` trap runs (or forwards
7370 // to the target). node-js initializes an ALREADY-allocated `inst` rather
7371 // than adopting the constructor's return value, so what the proxy built
7372 // is moved across — the same move the builtin arm makes.
7373 Some(JsObj::Proxy { .. }) => {
7374 let built = construct_nt(parent, args, new_target.clone())?;
7375 if !become_exotic(inst, &built) {
7376 adopt_own_props(inst, &built);
7377 }
7378 Ok(None)
7379 }
7380 _ => Err(type_error("super is not a constructor")),
7381 }
7382}
7383
7384/// Move `built`'s own properties (and their attributes) onto `inst`. Used where
7385/// a parent constructor produces a fresh object but node-js's class model has
7386/// already allocated the instance `this` is bound to.
7387/// Replace `inst`'s heap object with `built`'s, so an instance whose class
7388/// extends a builtin EXOTIC really is one.
7389///
7390/// `inst` keeps its identity and its prototype link — the leaf class's
7391/// prototype, which is what method resolution and `instanceof` walk — while its
7392/// contents become the exotic the parent constructor produced. The side tables
7393/// keyed by heap index (array holes, property attributes, the fn-prop table)
7394/// move across with it.
7395///
7396/// Returns false for a variant whose state is ordinary own properties
7397/// (`Error`), which the caller copies instead.
7398fn become_exotic(inst: &Value, built: &Value) -> bool {
7399 let exotic = matches!(
7400 with_host(|h| h.get(built).cloned()),
7401 Some(JsObj::Array(_))
7402 | Some(JsObj::Map { .. })
7403 | Some(JsObj::Set { .. })
7404 | Some(JsObj::RegExp(_))
7405 | Some(JsObj::Promise { .. })
7406 | Some(JsObj::Func(_))
7407 | Some(JsObj::Str(_))
7408 | Some(JsObj::BigInt(_))
7409 | Some(JsObj::Symbol { .. })
7410 );
7411 if !exotic {
7412 return false;
7413 }
7414 let (Value::Obj(dst), Value::Obj(src)) = (inst, built) else {
7415 return false;
7416 };
7417 let (dst, src) = (*dst, *src);
7418 with_host(|h| {
7419 if let Some(obj) = h.get(built).cloned() {
7420 if let Some(slot) = h.get_mut(inst) {
7421 *slot = obj;
7422 }
7423 }
7424 h.move_index_state(src, dst);
7425 });
7426 true
7427}
7428
7429fn adopt_own_props(inst: &Value, built: &Value) {
7430 let entries: Vec<(String, Value)> = with_host(|h| match h.get(built) {
7431 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
7432 _ => Vec::new(),
7433 });
7434 with_host(|h| {
7435 let keys: Vec<String> = entries.iter().map(|(k, _)| k.clone()).collect();
7436 if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7437 for (k, v) in entries {
7438 props.insert(k, v);
7439 }
7440 canonicalize_own_keys(props);
7441 }
7442 // The copied slots keep the attributes the source gave them, so
7443 // `class E extends Error` instances hide `message`/`stack` too.
7444 for k in keys {
7445 let a = h.prop_attrs(built, &k);
7446 h.set_prop_attrs(inst, &k, a);
7447 }
7448 });
7449}
7450
7451// ── class construction (runtime) ─────────────────────────────────────────────
7452
7453/// Build a class constructor value from its parts. The compiler emits (via
7454/// `MKCLASS`) the evaluated parent (or undefined) and the constructor closure (or
7455/// undefined for a default constructor); methods/getters/setters/statics/fields
7456/// are installed afterward by `DEF_MEMBER`/`DEF_FIELD`.
7457pub fn build_class(name: &str, parent: Value, ctor: Value, source_def: Option<usize>) -> Value {
7458 // A Proxy parent (`class D extends new Proxy(B, {})`): `D.prototype`'s
7459 // `[[Prototype]]` is `Get(parent, "prototype")` — a read that runs the `get`
7460 // trap and so re-enters the host, which the borrow below cannot allow.
7461 // Without it the link fell back to `Object.prototype` and every inherited
7462 // method went missing.
7463 let proxy_parent_proto = (with_host(|h| h.kind_of(&parent)) == Some(ObjKind::Proxy))
7464 .then(|| crate::builtins::get_property(&parent, "prototype").ok())
7465 .flatten();
7466 with_host(|h| {
7467 let parent_opt = if matches!(parent, Value::Undef) {
7468 None
7469 } else {
7470 Some(parent.clone())
7471 };
7472 // The class prototype delegates to the parent's prototype (or
7473 // Object.prototype for a base class). Extending a builtin error links to
7474 // that error's prototype so `instanceof Error` holds for the subclass.
7475 let parent_proto = match &parent_opt {
7476 Some(_) if proxy_parent_proto.is_some() => {
7477 proxy_parent_proto.clone().expect("checked is_some")
7478 }
7479 Some(p) => match h.get(p).cloned() {
7480 Some(JsObj::Class(pc)) => pc.proto.clone(),
7481 Some(JsObj::Builtin(bn)) => {
7482 h.ensure_error_protos();
7483 h.ensure_native_protos();
7484 // `class S extends String {}` links to the REAL
7485 // `String.prototype`, the same way an error subclass links
7486 // to its error prototype. Without it `S.prototype`'s
7487 // `[[Prototype]]` fell back to `Object.prototype`, so
7488 // `new S("hi") instanceof String` read false and
7489 // `String(new S("hi"))` reported `[object String]` instead
7490 // of `hi`.
7491 error_proto_of(h, &bn)
7492 .or_else(|| h.native_proto(&bn))
7493 .or_else(|| h.fn_prop(p, "prototype"))
7494 .unwrap_or_else(|| h.object_proto())
7495 }
7496 _ => h
7497 .fn_prop(p, "prototype")
7498 .unwrap_or_else(|| h.object_proto()),
7499 },
7500 None => h.object_proto(),
7501 };
7502 let proto = h.new_object(IndexMap::new());
7503 h.set_proto(&proto, parent_proto);
7504 let ctor_opt = if matches!(ctor, Value::Undef) {
7505 None
7506 } else {
7507 Some(ctor.clone())
7508 };
7509 // Give the constructor closure its home class (for `super.method()`), and
7510 // record its `.name`.
7511 if let Some(cf) = &ctor_opt {
7512 if let Some(JsObj::Func(f)) = h.get_mut(cf) {
7513 f.home_class = Some(name.to_string());
7514 }
7515 }
7516 let cval = ClassVal {
7517 name: name.to_string(),
7518 ctor: ctor_opt,
7519 parent: parent_opt,
7520 proto: proto.clone(),
7521 statics: IndexMap::new(),
7522 fields: Vec::new(),
7523 source_def,
7524 };
7525 let class_val = h.alloc(JsObj::Class(cval));
7526 h.class_registry.insert(name.to_string(), class_val.clone());
7527 // Link prototype → class (for instance display + `constructor`), and give
7528 // the class its own `prototype` fn-prop so `C.prototype` reads work.
7529 h.tag_proto_class(&proto, class_val.clone());
7530 h.set_fn_prop(&class_val, "prototype", proto.clone());
7531 // `Class.prototype.constructor === Class`.
7532 if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
7533 p.insert("constructor".to_string(), class_val.clone());
7534 }
7535 h.hide_prop(&proto, "constructor");
7536 class_val
7537 })
7538}
7539
7540/// Install a method / getter / setter on a class (`DEF_MEMBER`). `kind` is a
7541/// `member::*` tag; `is_static` targets the constructor side.
7542pub fn define_member(class_val: &Value, name: &str, kind: i64, is_static: bool, func: Value) {
7543 with_host(|h| {
7544 let cname = match h.get(class_val) {
7545 Some(JsObj::Class(c)) => c.name.clone(),
7546 _ => String::new(),
7547 };
7548 // A private method/accessor: remember which class declared it, so a
7549 // brand-check failure can name the class the way node does. A static
7550 // FIELD is data, not a method, so it keeps the field wording.
7551 if name.starts_with('#') && kind != member::STATIC_FIELD {
7552 h.note_private_method(name);
7553 }
7554 // Give the method its home class for `super.x()`, and record whether it
7555 // is static — `super` resolves against a different object either way.
7556 if let Some(JsObj::Func(f)) = h.get_mut(&func) {
7557 f.home_class = Some(cname);
7558 f.home_static = is_static;
7559 }
7560 // Static members live on the constructor (fn-props / static accessors);
7561 // instance members on the prototype.
7562 let target = if is_static {
7563 class_val.clone()
7564 } else {
7565 match h.get(class_val) {
7566 Some(JsObj::Class(c)) => c.proto.clone(),
7567 _ => return,
7568 }
7569 };
7570 match kind {
7571 member::GET => h.set_accessor(&target, name, Some(func), None),
7572 member::SET => h.set_accessor(&target, name, None, Some(func)),
7573 _ => {
7574 // A static field is enumerable (`Object.keys(C)` lists it) unlike
7575 // a method, so it must not reach the `hide_prop` below.
7576 if kind == member::STATIC_FIELD {
7577 if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7578 c.statics.insert(name.to_string(), func.clone());
7579 }
7580 h.set_fn_prop(class_val, name, func);
7581 return;
7582 }
7583 if is_static {
7584 if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7585 c.statics.insert(name.to_string(), func.clone());
7586 }
7587 h.set_fn_prop(class_val, name, func);
7588 } else if let Some(JsObj::Object(p)) = h.get_mut(&target) {
7589 p.insert(name.to_string(), func);
7590 }
7591 }
7592 }
7593 // Class methods and accessors are non-enumerable (ES2015 ClassDefinition-
7594 // Evaluation), so `for (k in instance)` walking the prototype chain never
7595 // yields them and `Object.keys(C.prototype)` is empty.
7596 h.hide_prop(&target, name);
7597 });
7598}
7599
7600/// Register an instance-field initializer thunk on a class (`DEF_FIELD`).
7601pub fn define_field(class_val: &Value, name: &str, thunk: Value, name_anon: bool) {
7602 with_host(|h| {
7603 if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7604 c.fields.push((name.to_string(), thunk, name_anon));
7605 }
7606 });
7607}
7608
7609/// The `[[Prototype]]` object a constructor value hands to its instances
7610/// (`Ctor.prototype`), for `instanceof`.
7611fn ctor_prototype(h: &JsHost, ctor: &Value) -> Option<Value> {
7612 match h.get(ctor) {
7613 Some(JsObj::Class(c)) => Some(c.proto.clone()),
7614 Some(JsObj::Func(_)) => h.fn_prop(ctor, "prototype"),
7615 // A builtin's prototype lives in one of two registries: the error
7616 // prototypes, or the native exotic prototypes (`Buffer.prototype`,
7617 // `Uint8Array.prototype`). Consulting only the first made `instanceof`
7618 // blind to the real `Buffer.prototype → Uint8Array.prototype` chain, so
7619 // `Buffer.prototype instanceof Uint8Array` read false even though the
7620 // link was there — the instance case only passed via a native-tag
7621 // special case, which a prototype object does not carry.
7622 Some(JsObj::Builtin(name)) => h
7623 .error_protos
7624 .get(name)
7625 .or_else(|| h.native_protos.get(name))
7626 .cloned(),
7627 Some(JsObj::BoundFunc { target, .. }) => ctor_prototype(h, &target.clone()),
7628 _ => None,
7629 }
7630}
7631
7632/// `ctor.prototype` in the SAME representation `builtins::prototype_of` yields,
7633/// so a chain walk driven by that function can compare the two with `strict_eq`.
7634///
7635/// `ctor_prototype` answers only for the constructors whose prototype object
7636/// really exists on the heap (classes, user functions, the error and native
7637/// exotics). A bare builtin like `Object`/`Array` has none there — its instances
7638/// report `h.object_proto()` / a `Builtin("<C>.prototype")` handle — so this
7639/// mirrors that fallback rather than reporting "no prototype" and failing every
7640/// comparison.
7641fn walk_target_prototype(ctor: &Value) -> Option<Value> {
7642 if let Some(p) = with_host(|h| ctor_prototype(h, ctor)) {
7643 return Some(p);
7644 }
7645 let name = with_host(|h| match h.get(ctor) {
7646 Some(JsObj::Builtin(n)) => Some(n.clone()),
7647 _ => None,
7648 })?;
7649 if name == "Object" {
7650 return Some(with_host(|h| h.object_proto()));
7651 }
7652 Some(with_host(|h| {
7653 h.alloc(JsObj::Builtin(format!("{name}.prototype")))
7654 }))
7655}
7656
7657/// V8's "not a function" wording for a value that was expected to be callable.
7658/// A number/string/boolean is named WITH its value (`number 1 is not a
7659/// function`, `string "s" is not a function`); every other type is named by type
7660/// alone (`object is not a function`, `symbol is not a function`).
7661pub fn not_a_function_message(v: &Value) -> String {
7662 with_host(|h| match v {
7663 Value::Undef => "undefined is not a function".into(),
7664 Value::Bool(b) => format!("boolean {b} is not a function"),
7665 Value::Int(_) | Value::Float(_) => format!("number {} is not a function", h.str_of(v)),
7666 Value::Str(s) => format!("string \"{s}\" is not a function"),
7667 Value::Obj(_) => match h.get(v) {
7668 Some(JsObj::Str(s)) => format!("string \"{s}\" is not a function"),
7669 Some(JsObj::Symbol { .. }) => "symbol is not a function".into(),
7670 Some(JsObj::BigInt(_)) => "bigint is not a function".into(),
7671 _ => "object is not a function".into(),
7672 },
7673 _ => "object is not a function".into(),
7674 })
7675}
7676
7677/// `obj instanceof ctor` — walk `obj`'s prototype chain looking for
7678/// `ctor.prototype`.
7679pub fn instance_of(obj: &Value, ctor: &Value) -> Result<bool, String> {
7680 // 13.10.2 InstanceofOperator step 3: a `Symbol.hasInstance` method on the
7681 // right-hand side REPLACES the prototype-chain walk entirely, and it is
7682 // consulted before the callability check — which is why a plain (uncallable)
7683 // object that defines it is a legal `instanceof` right-hand side.
7684 if matches!(ctor, Value::Obj(_)) {
7685 // `class C { static [Symbol.hasInstance](){} }` and a method defined on a
7686 // plain function both land in the fn-prop side table (which
7687 // `class_static` reads, following the `extends` chain), NOT in an object
7688 // property map — so consulting only `lookup_chain` would find the object
7689 // literal form and silently miss the two forms V8 users actually write.
7690 let handler = match with_host(|h| h.class_static(ctor, "@@hasInstance")) {
7691 Some(f) => Some(f),
7692 None => protocol_lookup(ctor, "@@hasInstance")?,
7693 };
7694 // GetMethod (7.3.11) treats only `undefined`/`null` as "absent"; anything
7695 // else that is not callable is a TypeError, so a data property here does
7696 // NOT fall back to the prototype walk.
7697 match handler {
7698 Some(f) if with_host(|h| is_callable(h, &f)) => {
7699 let r = invoke(&f, vec![obj.clone()], Some(ctor.clone()))?;
7700 return Ok(with_host(|h| h.truthy(&r)));
7701 }
7702 Some(f)
7703 if !matches!(f, Value::Undef)
7704 && !with_host(|h| matches!(h.get(&f), Some(JsObj::Null))) =>
7705 {
7706 return Err(type_error(¬_a_function_message(&f)));
7707 }
7708 _ => {}
7709 }
7710 }
7711 // 13.10.2 InstanceofOperator validates the RIGHT-hand side FIRST, so
7712 // `1 instanceof 3` throws even though the left side could never match.
7713 // Returning early on the left side skipped that check entirely.
7714 let ctor_callable = with_host(|h| {
7715 matches!(
7716 h.get(ctor),
7717 Some(JsObj::Func(_))
7718 | Some(JsObj::Class(_))
7719 | Some(JsObj::Builtin(_))
7720 | Some(JsObj::BoundFunc { .. })
7721 )
7722 });
7723 if !ctor_callable {
7724 // V8 has TWO messages here and they are not interchangeable: a primitive
7725 // right-hand side is "not an object", an object that is merely not
7726 // callable is "not callable". Only the second was implemented, so
7727 // `1 instanceof 3` reported nothing at all.
7728 return Err(type_error(if with_host(|h| !is_primitive(h, ctor)) {
7729 "Right-hand side of 'instanceof' is not callable"
7730 } else {
7731 "Right-hand side of 'instanceof' is not an object"
7732 }));
7733 }
7734 // A non-object left-hand side is never an instance — but only after the
7735 // right-hand side has been validated above.
7736 if !matches!(obj, Value::Obj(_)) {
7737 return Ok(false);
7738 }
7739 // A Proxy shares no heap variant with its target, so the structural arms
7740 // below would misclassify it. 10.5.3 says `OrdinaryHasInstance` walks
7741 // `[[GetPrototypeOf]]`, i.e. the handler's `getPrototypeOf` trap — run that
7742 // walk here, which also gives a custom trap the final say.
7743 if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Proxy) {
7744 with_host(|h| {
7745 h.ensure_error_protos();
7746 h.ensure_native_protos();
7747 });
7748 let Some(target) = walk_target_prototype(ctor) else {
7749 return Ok(false);
7750 };
7751 let mut cur = crate::proxy::get_prototype_of(obj)?.unwrap_or(Value::Undef);
7752 for _ in 0..100 {
7753 if matches!(cur, Value::Undef) || with_host(|h| h.is_null(&cur)) {
7754 return Ok(false);
7755 }
7756 if with_host(|h| h.strict_eq(&cur, &target)) {
7757 return Ok(true);
7758 }
7759 cur = crate::builtins::prototype_of(&cur);
7760 }
7761 return Ok(false);
7762 }
7763 // Builtin constructors whose instances aren't prototype-linked in our model
7764 // (arrays/plain objects/functions) get a structural instanceof.
7765 if let Some(JsObj::Builtin(name)) = with_host(|h| h.get(ctor).cloned()) {
7766 // …but an object whose chain PASSES THROUGH the intrinsic prototype is
7767 // an instance regardless of its own kind, which is the whole of the ES5
7768 // subclassing pattern: `F.prototype = Object.create(Array.prototype)`
7769 // makes `new F() instanceof Array` true. A structural test alone said
7770 // false.
7771 if crate::builtins::chain_intrinsic_ctors_pub(obj).contains(&name.as_str()) {
7772 return Ok(true);
7773 }
7774 let kind = with_host(|h| h.get(obj).cloned());
7775 match name.as_str() {
7776 "Array" => return Ok(matches!(kind, Some(JsObj::Array(_)))),
7777 "Function" => return Ok(with_host(|h| is_callable(h, obj))),
7778 // Map/Set/Promise instances are distinct heap variants, not
7779 // prototype-linked, so match them structurally (a WeakMap/WeakSet is a
7780 // Map/Set with `weak: true`, so `weakMap instanceof Map` is false).
7781 "Map" => return Ok(matches!(kind, Some(JsObj::Map { weak: false, .. }))),
7782 "WeakMap" => return Ok(matches!(kind, Some(JsObj::Map { weak: true, .. }))),
7783 "Set" => return Ok(matches!(kind, Some(JsObj::Set { weak: false, .. }))),
7784 "WeakSet" => return Ok(matches!(kind, Some(JsObj::Set { weak: true, .. }))),
7785 "Promise" => return Ok(matches!(kind, Some(JsObj::Promise { .. }))),
7786 // A RegExp is its own heap variant too, not a prototype-linked object.
7787 "RegExp" => return Ok(matches!(kind, Some(JsObj::RegExp(_)))),
7788 "Object" => {
7789 // Everything object-typed except a null-prototype object is an
7790 // Object instance.
7791 let is_obj = matches!(
7792 kind,
7793 Some(JsObj::Object(_))
7794 | Some(JsObj::Array(_))
7795 // A namespace object and a builtin function are both
7796 // `instanceof Object`: `Math instanceof Object` is true.
7797 | Some(JsObj::Builtin(_))
7798 | Some(JsObj::Func(_))
7799 | Some(JsObj::Class(_))
7800 | Some(JsObj::Map { .. })
7801 | Some(JsObj::Set { .. })
7802 | Some(JsObj::Promise { .. })
7803 | Some(JsObj::Generator { .. })
7804 | Some(JsObj::RegExp(_))
7805 );
7806 if is_obj {
7807 // A null-prototype object (Object.create(null) or
7808 // setPrototypeOf(o, null)) is NOT an Object instance.
7809 if with_host(|h| h.has_null_proto(obj)) {
7810 return Ok(false);
7811 }
7812 return Ok(true);
7813 }
7814 return Ok(false);
7815 }
7816 // A Node `Buffer` IS a `Uint8Array` subclass instance.
7817 "Uint8Array" if crate::stdlib::native_tag(obj).as_deref() == Some("Buffer") => {
7818 return Ok(true);
7819 }
7820 // Every typed array carries the same `TypedArray` tag; the constructor
7821 // it is an instance of is its ELEMENT KIND.
7822 k if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") => {
7823 return Ok(crate::stdlib::typedarray::kind_of(obj) == k);
7824 }
7825 // A native-tagged instance (`WeakRef`, `FinalizationRegistry`,
7826 // `TextEncoder`, …) is an instance of the builtin whose name matches
7827 // its hidden `@@native` tag.
7828 other => {
7829 if crate::stdlib::native_tag(obj).as_deref() == Some(other) {
7830 return Ok(true);
7831 }
7832 }
7833 }
7834 }
7835 with_host(|h| h.ensure_error_protos());
7836 // The native exotic prototypes are built lazily; `instanceof` may be the
7837 // first thing to ask for them, so materialise them before the chain walk.
7838 with_host(|h| h.ensure_native_protos());
7839 let target = match with_host(|h| ctor_prototype(h, ctor)) {
7840 Some(p) => p,
7841 None => return Ok(false),
7842 };
7843 let mut cur = with_host(|h| h.proto_of(obj));
7844 while let Some(p) = cur {
7845 if with_host(|h| h.strict_eq(&p, &target)) {
7846 return Ok(true);
7847 }
7848 cur = with_host(|h| h.proto_of(&p));
7849 }
7850 Ok(false)
7851}
7852
7853// ── generators (stackful coroutines, same-thread via corosensei) ─────────────
7854
7855impl JsHost {
7856 /// Swap the volatile execution context in one shot, returning the previous
7857 /// one — installs a generator's context on resume, pulls it back on suspend.
7858 fn install_gen_ctx(&mut self, mut c: GenContext) -> GenContext {
7859 std::mem::swap(&mut self.frames, &mut c.frames);
7860 std::mem::swap(&mut self.error, &mut c.error);
7861 std::mem::swap(&mut self.exc, &mut c.exc);
7862 std::mem::swap(&mut self.signal, &mut c.signal);
7863 c
7864 }
7865 pub fn is_generator_val(&self, v: &Value) -> bool {
7866 matches!(self.get(v), Some(JsObj::Generator { .. }))
7867 }
7868 /// Whether `v` is an ASYNC generator object — the borrow-free form of
7869 /// [`is_async_generator`], usable from code already holding the host.
7870 pub fn is_async_gen_val(&self, v: &Value) -> bool {
7871 match self.get(v) {
7872 Some(JsObj::Generator { id }) => self
7873 .generators
7874 .get(*id as usize)
7875 .map(|g| g.async_gen)
7876 .unwrap_or(false),
7877 _ => false,
7878 }
7879 }
7880 pub fn gen_done(&self, id: u32) -> bool {
7881 self.generators
7882 .get(id as usize)
7883 .map(|g| g.done)
7884 .unwrap_or(true)
7885 }
7886 fn gen_started(&self, id: u32) -> bool {
7887 self.generators
7888 .get(id as usize)
7889 .map(|g| g.started)
7890 .unwrap_or(false)
7891 }
7892}
7893
7894/// Build a suspended generator whose body is `chunk`, run in a frame with the
7895/// already-bound `env`. Nothing executes until the first `gen_resume`.
7896fn make_generator(
7897 chunk: Chunk,
7898 env: Env,
7899 this_val: Option<Value>,
7900 home_class: Option<String>,
7901 home_static: bool,
7902 home_object: Option<Value>,
7903 strict: bool,
7904) -> Value {
7905 let home = home_class
7906 .as_ref()
7907 .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
7908 let frame = Frame {
7909 base_env: env.clone(),
7910 env,
7911 this_obj: this_val,
7912 new_target: None,
7913 home_class: home,
7914 home_static,
7915 home_object,
7916 strict,
7917 line: 0,
7918 owner: None,
7919 is_module: false,
7920 this_state: ThisState::Plain,
7921 };
7922 let id = with_host(|h| {
7923 let id = h.generators.len() as u32;
7924 h.generators.push(GenCell {
7925 coro: None,
7926 yielder: std::ptr::null(),
7927 ctx: GenContext {
7928 frames: vec![frame],
7929 ..GenContext::default()
7930 },
7931 done: false,
7932 started: false,
7933 inject: None,
7934 async_gen: false,
7935 queue: std::collections::VecDeque::new(),
7936 running: false,
7937 stack_floor: 0,
7938 });
7939 id
7940 });
7941 let body = move |yielder: &corosensei::Yielder<Value, Value>, _first: Value| {
7942 ensure_coroutine_floor();
7943 // Same thread → publish the yielder so `yield` (deep in the body's VM)
7944 // can reach it. Valid for the whole body lifetime.
7945 with_host(|h| h.generators[id as usize].yielder = yielder as *const _ as *const ());
7946 let r = run_chunk_on(chunk);
7947 // A `return` inside the body leaves a Return signal carrying the final
7948 // value; capture it so `.next()` reports it as the completion value.
7949 let ret = with_host(|h| match h.signal.take() {
7950 Some(Signal::Return(v)) => v,
7951 _ => Value::Undef,
7952 });
7953 r.map(|_| ret)
7954 };
7955 // The body's stack is allocated here rather than left to `Coroutine::new` so
7956 // that its size is ours to choose and, above all, so its `limit()` is known:
7957 // that address is what `stack_exhausted` must compare against while the body
7958 // runs, since a coroutine does NOT run on the thread stack pthread reports.
7959 // A refused reservation still yields a working generator on corosensei's own
7960 // 1 MiB default, with a floor derived on entry instead.
7961 let (coro, floor) = match corosensei::stack::DefaultStack::new(CORO_STACK_SIZE) {
7962 Ok(stack) => {
7963 let floor = coro_stack_floor(&stack);
7964 (corosensei::Coroutine::with_stack(stack, body), floor)
7965 }
7966 Err(_) => (corosensei::Coroutine::new(body), 0),
7967 };
7968 with_host(|h| {
7969 h.generators[id as usize].coro = Some(coro);
7970 h.generators[id as usize].stack_floor = floor;
7971 });
7972 with_host(|h| h.alloc(JsObj::Generator { id }))
7973}
7974
7975/// `yield v` — suspend the running generator, handing `v` to the resumer; returns
7976/// the value the next `gen_resume(x)` supplies (a `.next(x)` argument).
7977pub fn gen_yield(v: Value) -> Result<Value, String> {
7978 let id = match CUR_GEN.with(|c| c.get()) {
7979 Some(id) => id,
7980 None => return Err(type_error("yield outside a generator")),
7981 };
7982 let yp = with_host(|h| h.generators[id as usize].yielder);
7983 // SAFETY: same-thread coroutine; the yielder lives for the whole body, and we
7984 // only reach here from inside that body (its stack is live).
7985 let yielder = unsafe { &*(yp as *const corosensei::Yielder<Value, Value>) };
7986 let sent = yielder.suspend(v);
7987 // On resume, a `.return(v)`/`.throw(e)` may have queued a forced completion:
7988 // convert it into a Return signal / thrown value so the body unwinds and any
7989 // `finally` runs, exactly as a source-level `return`/`throw` would.
7990 if let Some(inj) = with_host(|h| h.generators[id as usize].inject.take()) {
7991 match inj {
7992 GenInject::Return(rv) => {
7993 with_host(|h| h.signal = Some(Signal::Return(rv)));
7994 return Ok(Value::Undef);
7995 }
7996 GenInject::Throw(ev) => {
7997 let msg = with_host(|h| crate::builtins::error_string(h, &ev));
7998 with_host(|h| h.exc = Some(ev));
7999 return Err(msg);
8000 }
8001 }
8002 }
8003 Ok(sent)
8004}
8005
8006/// `generator.return(v)`: force the generator to complete, running any pending
8007/// `finally`. If it is already done (or never started) it just reports
8008/// `{value:v, done:true}` without executing the body.
8009pub fn gen_return(gen: &Value, v: Value) -> Result<GenStep, String> {
8010 let id = match with_host(|h| h.get(gen).cloned()) {
8011 Some(JsObj::Generator { id }) => id,
8012 _ => return Err(type_error("not a generator")),
8013 };
8014 // Not started yet (coro present, ctx never resumed) OR already done → no body
8015 // to unwind: complete immediately with the supplied value.
8016 let started = with_host(|h| h.gen_started(id));
8017 if with_host(|h| h.generators[id as usize].done) || !started {
8018 with_host(|h| h.generators[id as usize].done = true);
8019 return Ok(GenStep::Done(v));
8020 }
8021 with_host(|h| h.generators[id as usize].inject = Some(GenInject::Return(v)));
8022 gen_resume(gen, Value::Undef)
8023}
8024
8025/// `generator.throw(e)`: inject a throw at the suspension point, running any
8026/// pending `finally` and letting an enclosing `try/catch` in the body handle it.
8027pub fn gen_throw(gen: &Value, e: Value) -> Result<GenStep, String> {
8028 let id = match with_host(|h| h.get(gen).cloned()) {
8029 Some(JsObj::Generator { id }) => id,
8030 _ => return Err(type_error("not a generator")),
8031 };
8032 let started = with_host(|h| h.gen_started(id));
8033 if with_host(|h| h.generators[id as usize].done) || !started {
8034 // A throw into a done/unstarted generator propagates to the caller.
8035 with_host(|h| h.generators[id as usize].done = true);
8036 let msg = with_host(|h| crate::builtins::error_string(h, &e));
8037 with_host(|h| h.exc = Some(e));
8038 return Err(msg);
8039 }
8040 with_host(|h| h.generators[id as usize].inject = Some(GenInject::Throw(e)));
8041 gen_resume(gen, Value::Undef)
8042}
8043
8044/// Outcome of resuming a generator: a yielded value (not done), or the final
8045/// completion value (done).
8046pub enum GenStep {
8047 Yield(Value),
8048 Done(Value),
8049}
8050
8051/// Resume a generator until its next `yield` or its body returns. Preserves the
8052/// shared host: the coroutine is taken out so the body re-enters `with_host`
8053/// freely, and the volatile context is swapped so the caller's frames/signal
8054/// survive the switch.
8055pub fn gen_resume(gen: &Value, send: Value) -> Result<GenStep, String> {
8056 let id = match with_host(|h| h.get(gen).cloned()) {
8057 Some(JsObj::Generator { id }) => id,
8058 _ => return Err(type_error("not a generator")),
8059 };
8060 if with_host(|h| h.generators[id as usize].done) {
8061 return Ok(GenStep::Done(Value::Undef));
8062 }
8063 let mut coro = match with_host(|h| h.generators[id as usize].coro.take()) {
8064 Some(c) => c,
8065 None => return Err("TypeError: generator already executing".into()),
8066 };
8067 with_host(|h| h.generators[id as usize].started = true);
8068 let gen_ctx = with_host(|h| std::mem::take(&mut h.generators[id as usize].ctx));
8069 let caller_ctx = with_host(|h| h.install_gen_ctx(gen_ctx));
8070 let prev = CUR_GEN.with(|c| c.replace(Some(id)));
8071 // The body runs on the coroutine's OWN stack, so the guard's floor has to
8072 // move with it and move back on suspend — generators nest, and a resume from
8073 // inside another generator must restore that one's floor, not the thread's.
8074 let coro_floor = with_host(|h| h.generators[id as usize].stack_floor);
8075 let caller_floor = swap_stack_floor(coro_floor);
8076
8077 let out = coro.resume(send); // no host borrow held; body drives its own VM
8078
8079 let measured = swap_stack_floor(caller_floor);
8080 // A coroutine on corosensei's default stack has no known bounds, so the
8081 // floor it measured for itself on first entry is kept for later resumes.
8082 if coro_floor == 0 && measured != 0 {
8083 with_host(|h| h.generators[id as usize].stack_floor = measured);
8084 }
8085 CUR_GEN.with(|c| c.set(prev));
8086 let mut gen_ctx = with_host(|h| h.install_gen_ctx(caller_ctx));
8087 // A `throw` inside the body left the thrown VALUE in the generator's context,
8088 // which the swap above just stashed away. Hand it to the caller so the
8089 // rejection/catch keeps the original error object instead of a string rebuild.
8090 let thrown = gen_ctx.exc.take();
8091 with_host(|h| {
8092 if let Some(v) = thrown {
8093 h.exc = Some(v);
8094 }
8095 h.generators[id as usize].ctx = gen_ctx;
8096 h.generators[id as usize].coro = Some(coro);
8097 });
8098
8099 match out {
8100 corosensei::CoroutineResult::Yield(y) => Ok(GenStep::Yield(y)),
8101 corosensei::CoroutineResult::Return(r) => {
8102 // Release the coroutine — and with it the mmap'd stack it owns —
8103 // the moment the body completes. `h.generators` only ever grows (an
8104 // id is never reused), so a program that awaits in a loop otherwise
8105 // accumulates one whole [`CORO_STACK_SIZE`] reservation per call for
8106 // the life of the process. A finished generator is never resumed:
8107 // `gen_resume` returns `Done` on the `done` flag before it looks.
8108 with_host(|h| {
8109 let g = &mut h.generators[id as usize];
8110 g.done = true;
8111 g.coro = None;
8112 });
8113 match r {
8114 Ok(v) => Ok(GenStep::Done(v)),
8115 Err(e) => Err(e),
8116 }
8117 }
8118 }
8119}
8120
8121/// Force a generator to completion (used by `.return()` and abandoned loops):
8122/// marks it done without running further.
8123pub fn gen_close(gen: &Value) {
8124 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(gen).cloned()) {
8125 with_host(|h| h.generators[id as usize].done = true);
8126 }
8127}
8128
8129// ── iteration protocol (arrays, strings, Map/Set, generators, Symbol.iterator) ─
8130
8131/// Convert a Map/Set key value into a `MapKey` under SameValueZero.
8132pub fn map_key(h: &JsHost, v: &Value) -> MapKey {
8133 match v {
8134 Value::Undef => MapKey::Undef,
8135 Value::Bool(b) => MapKey::Bool(*b),
8136 Value::Int(n) => MapKey::Num(norm_num_bits(*n as f64)),
8137 Value::Float(f) => MapKey::Num(norm_num_bits(*f)),
8138 Value::Str(s) => MapKey::Str((**s).clone()),
8139 Value::Obj(i) => match h.get(v) {
8140 Some(JsObj::Str(s)) => MapKey::Str(s.clone()),
8141 Some(JsObj::Null) => MapKey::Null,
8142 Some(JsObj::BigInt(b)) => MapKey::Big(b.to_string()),
8143 Some(JsObj::Builtin(n)) => MapKey::Intrinsic(builtin_identity(n).to_string()),
8144 _ => MapKey::Ref(*i),
8145 },
8146 _ => MapKey::Undef,
8147 }
8148}
8149
8150/// Canonical bit pattern for a Map/Set numeric key: `NaN` → one value, `-0` → `+0`.
8151fn norm_num_bits(f: f64) -> u64 {
8152 if f.is_nan() {
8153 return f64::NAN.to_bits();
8154 }
8155 if f == 0.0 {
8156 return 0.0f64.to_bits(); // fold -0 into +0
8157 }
8158 f.to_bits()
8159}
8160
8161/// Fully materialize any iterable into a vector of values.
8162/// Pull at most `n` values, then close the iterator — 8.6.2
8163/// IteratorBindingInitialization, which is what an array destructuring pattern
8164/// without a `...rest` element performs.
8165///
8166/// The distinction from [`iter_all`] is not an optimization. A pattern names a
8167/// fixed number of targets, so the spec pulls exactly that many and calls
8168/// IteratorClose on whatever is left; draining instead made
8169///
8170/// ```text
8171/// const [first] = infiniteGenerator();
8172/// ```
8173///
8174/// run forever. It is also observable on any finite iterator, as the count of
8175/// `next()` calls and whether `return()` ever ran.
8176///
8177/// A `...rest` element genuinely consumes the remainder, so those patterns keep
8178/// using `iter_all` and an unbounded source hangs there in node too.
8179pub fn iter_take(v: &Value, n: usize) -> Result<Vec<Value>, String> {
8180 // A Proxy iterates through its traps, which materialize eagerly; there is
8181 // no step-wise form to bound, so this keeps the draining behaviour.
8182 if let Some(items) = crate::proxy::iterate(v)? {
8183 return Ok(items.into_iter().take(n).collect());
8184 }
8185 if with_host(|h| h.is_generator_val(v)) {
8186 let mut out = Vec::new();
8187 while out.len() < n {
8188 match gen_resume(v, Value::Undef)? {
8189 GenStep::Yield(x) => out.push(x),
8190 _ => return Ok(out), // ran out on its own; nothing left to close
8191 }
8192 }
8193 // Stopped early: `.return()` resumes it at the yield so `finally` runs.
8194 let _ = gen_return(v, Value::Undef);
8195 return Ok(out);
8196 }
8197 if let Some(iter_fn) = user_iterator_fn(v) {
8198 let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8199 let mut out = Vec::new();
8200 while out.len() < n {
8201 let step = call_method(&iterator, "next", Vec::new())?;
8202 // Read first: resolving the property re-enters the host, so doing
8203 // it inside the `with_host` closure double-borrows and aborts.
8204 let done = get_prop_chain(&step, "done")?;
8205 if with_host(|h| h.truthy(&done)) {
8206 return Ok(out);
8207 }
8208 out.push(get_prop_chain(&step, "value")?);
8209 }
8210 // IteratorClose: `return` is optional on the protocol, and a throw from
8211 // it is swallowed here the way a normal (non-abrupt) completion does.
8212 if let Ok(ret) = get_prop_chain(&iterator, "return") {
8213 if with_host(|h| is_callable(h, &ret)) {
8214 let _ = invoke(&ret, Vec::new(), Some(iterator.clone()));
8215 }
8216 }
8217 return Ok(out);
8218 }
8219 // Arrays, strings, Map/Set: already materialized, and their built-in
8220 // iterators carry no `return`, so there is nothing to close. The same
8221 // reachability rule as `iter_all` applies — this is the DESTRUCTURING
8222 // entry point, and `const [x] = a` bound 1 from an array whose prototype no
8223 // longer carried `Symbol.iterator`.
8224 if !crate::builtins::own_intrinsic_reachable_pub(v) {
8225 let shown = with_host(|h| h.inspect(v));
8226 return Err(type_error(&format!("{shown} is not iterable")));
8227 }
8228 with_host(|h| h.iter_vec(v)).map(|items| items.into_iter().take(n).collect())
8229}
8230
8231pub fn iter_all(v: &Value) -> Result<Vec<Value>, String> {
8232 // A Proxy iterates through its traps (see `crate::proxy::iterate`); it has
8233 // no heap variant `iter_vec` could recognise.
8234 if let Some(items) = crate::proxy::iterate(v)? {
8235 return Ok(items);
8236 }
8237 // Generators / user iterators must resume without a live host borrow.
8238 if with_host(|h| h.is_generator_val(v)) {
8239 let mut out = Vec::new();
8240 while let GenStep::Yield(x) = gen_resume(v, Value::Undef)? {
8241 out.push(x);
8242 }
8243 return Ok(out);
8244 }
8245 // A live array iterator is stepped to the end, so an accessor or a hole
8246 // reads as `a[i]` does and the iterator is left exhausted, as spreading it
8247 // leaves it in node.
8248 if with_host(|h| matches!(h.get(v), Some(JsObj::Iter { array: Some(_), .. }))) {
8249 let mut out = Vec::new();
8250 while let Some(Some(x)) = crate::builtins::iter_step(v) {
8251 out.push(x);
8252 }
8253 return Ok(out);
8254 }
8255 // Object with a user-defined Symbol.iterator: drive its iterator protocol.
8256 // Checked BEFORE the reachability guard below, since an own `Symbol
8257 // .iterator` makes a value iterable no matter what its prototype is.
8258 if let Some(iter_fn) = user_iterator_fn(v) {
8259 let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8260 return drain_iterator(&iterator);
8261 }
8262 // The fast paths below read a builtin's backing storage directly, which is
8263 // only legitimate while that builtin's `Symbol.iterator` is still
8264 // reachable: replacing the prototype takes it away, and node then reports
8265 // the value as not iterable. Spread, destructuring and `Array.from`'s
8266 // iterable branch all funnel through here.
8267 if !crate::builtins::own_intrinsic_reachable_pub(v) {
8268 let shown = with_host(|h| h.inspect(v));
8269 return Err(type_error(&format!("{shown} is not iterable")));
8270 }
8271 // A String wrapper iterates its code POINTS, exactly as the primitive does
8272 // (22.1.3.34) — `[...new String("ab")]` is `["a","b"]`, not a TypeError.
8273 if let Some(prim) = crate::builtins::wrapped_primitive(v) {
8274 if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) {
8275 return iter_all(&prim);
8276 }
8277 }
8278 // An array's index ACCESSORS are not in its backing vector, so iterating one
8279 // (spread, `for-of`, `Array.from`) has to resolve them the way the
8280 // `Array.prototype` methods do.
8281 let mut items = with_host(|h| h.iter_vec(v))?;
8282 if with_host(|h| matches!(h.get(v), Some(JsObj::Array(_)))) {
8283 crate::builtins::resolve_index_accessors_pub(v, &mut items);
8284 }
8285 Ok(items)
8286}
8287
8288// ── async iteration (`for await (… of …)`) ───────────────────────────────────
8289
8290/// Obtain an async iterator for `for await`. If `src` has a `Symbol.asyncIterator`
8291/// method, use it (its `.next()` returns a promise of `{value, done}`); otherwise
8292/// fall back to the sync iterable, materialized into a `JsObj::Iter` whose values
8293/// are awaited one at a time by `async_step`.
8294pub fn get_async_iterator(src: &Value) -> Result<Value, String> {
8295 if let Some(f) = user_async_iterator_fn(src) {
8296 return invoke(&f, Vec::new(), Some(src.clone()));
8297 }
8298 // An `async function*` object IS its own async iterator; draining it into a
8299 // list here would run the whole body (and any `finally`) before the consumer
8300 // sees the first value.
8301 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(src).cloned()) {
8302 if with_host(|h| h.generators[id as usize].async_gen) {
8303 return Ok(src.clone());
8304 }
8305 }
8306 let items = iter_all(src)?;
8307 Ok(with_host(|h| {
8308 h.alloc(JsObj::Iter {
8309 items,
8310 idx: 0,
8311 array: None,
8312 })
8313 }))
8314}
8315
8316/// If `v` has an own/inherited `Symbol.asyncIterator` method, return it.
8317fn user_async_iterator_fn(v: &Value) -> Option<Value> {
8318 // A PROXY supplies the protocol through its `get` trap and is not a plain
8319 // object, so the shape test below rejects it outright.
8320 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8321 return protocol_lookup(v, "@@asyncIterator")
8322 .ok()
8323 .flatten()
8324 .filter(|f| with_host(|h| is_callable(h, f)));
8325 }
8326 let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8327 if !is_plain {
8328 return None;
8329 }
8330 // Full property resolution, not a stored-property lookup — the same reason
8331 // `user_iterator_fn` does it for the SYNC protocol. A NATIVE-tagged object
8332 // dispatches its methods through the stdlib method table rather than a
8333 // property map, so `lookup_chain` reported no `Symbol.asyncIterator` for one
8334 // even though reading it gives a function: `for await (const v of
8335 // timersPromises.setInterval(…))` said the iterator "is not iterable".
8336 let f = crate::builtins::get_property(v, "@@asyncIterator").ok()?;
8337 with_host(|h| is_callable(h, &f)).then_some(f)
8338}
8339
8340/// One step of a `for await` loop: return a Promise that settles to a
8341/// `{value, done}` record. For a native async iterator this is `iter.next()`
8342/// (already a promise of the record). For the sync fallback it pops the next raw
8343/// value, awaits it, and packages `{value: resolved, done:false}` (or
8344/// `{done:true}` at exhaustion).
8345pub fn async_step(iterator: &Value) -> Result<Value, String> {
8346 // An `async function*` object: resume it through the await-aware driver.
8347 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(iterator).cloned()) {
8348 if with_host(|h| h.generators[id as usize].async_gen) {
8349 return Ok(async_gen_step(iterator, Value::Undef));
8350 }
8351 }
8352 // Sync-fallback iterator: drive it here, awaiting each yielded value.
8353 if let Some(JsObj::Iter { items, idx, .. }) = with_host(|h| h.get(iterator).cloned()) {
8354 if idx >= items.len() {
8355 // `AsyncFromSyncIteratorContinuation` resolves the record THROUGH a
8356 // promise even at exhaustion, so the `done: true` step costs the same
8357 // two microtask ticks a value step does.
8358 let step = with_host(|h| h.new_promise());
8359 let sid = with_host(|h| h.promise_id(&step).unwrap());
8360 with_host(|h| {
8361 h.queue_micro_native(Box::new(move || {
8362 resolve_promise_val(sid, iter_record(Value::Undef, true));
8363 Ok(())
8364 }))
8365 });
8366 return Ok(step);
8367 }
8368 let raw = items[idx].clone();
8369 with_host(|h| {
8370 if let Some(JsObj::Iter { idx, .. }) = h.get_mut(iterator) {
8371 *idx += 1;
8372 }
8373 });
8374 // Await the raw value (adopts a promise's resolution), then wrap.
8375 let step = with_host(|h| h.new_promise());
8376 let sid = with_host(|h| h.promise_id(&step).unwrap());
8377 let raw_p = promise_of(&raw);
8378 let raw_id = with_host(|h| h.promise_id(&raw_p).unwrap());
8379 subscribe_native(
8380 raw_id,
8381 Box::new(move |state, val| {
8382 if state == PromiseState::Rejected {
8383 reject_promise_val(sid, val);
8384 } else {
8385 resolve_promise_val(sid, iter_record(val, false));
8386 }
8387 Ok(())
8388 }),
8389 );
8390 return Ok(step);
8391 }
8392 // Native async iterator: `iter.next()` returns the {value,done} promise.
8393 let r = call_method(iterator, "next", Vec::new())?;
8394 Ok(promise_of(&r))
8395}
8396
8397/// If `v` has an own/inherited `Symbol.iterator` method (internal key
8398/// `@@iterator`), return it. Arrays/strings use the native fast path instead.
8399pub fn user_iterator_fn(v: &Value) -> Option<Value> {
8400 let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8401 if !is_plain {
8402 return None;
8403 }
8404 // Full property resolution, not a stored-property lookup: a NATIVE-tagged
8405 // object (`URLSearchParams`, `Headers`) dispatches its methods through the
8406 // stdlib method table rather than a property map, so `lookup_chain` reported
8407 // no `Symbol.iterator` for one even though reading it gave a function —
8408 // `[...new URLSearchParams('a=1')]` threw `{} is not iterable`.
8409 let f = crate::builtins::get_property(v, "@@iterator").ok()?;
8410 with_host(|h| is_callable(h, &f)).then_some(f)
8411}
8412
8413/// Drive an iterator object (one with a `.next()` returning `{value, done}`) to
8414/// exhaustion.
8415/// Step `src`'s iterator, handing each value to `f`, and CLOSE the iterator if
8416/// `f` exits abruptly (7.4.9 IteratorClose).
8417///
8418/// The difference from `iter_all` + a loop is that this never materializes the
8419/// whole sequence: `Array.from(infinite, mapFn)` where `mapFn` throws has to
8420/// stop at the first call, and draining first means it never gets there at all.
8421pub fn iter_for_each(
8422 src: &Value,
8423 mut f: impl FnMut(Value, usize) -> Result<(), String>,
8424) -> Result<(), String> {
8425 // Only a USER iterator can be infinite or observe its own close; every
8426 // other shape is already a finite materialized sequence.
8427 let Some(iter_fn) = user_iterator_fn(src) else {
8428 for (i, v) in iter_all(src)?.into_iter().enumerate() {
8429 f(v, i)?;
8430 }
8431 return Ok(());
8432 };
8433 let iterator = invoke(&iter_fn, Vec::new(), Some(src.clone()))?;
8434 let mut i = 0usize;
8435 loop {
8436 let step = call_method(&iterator, "next", Vec::new())?;
8437 let done = get_prop_chain(&step, "done")?;
8438 if with_host(|h| h.truthy(&done)) {
8439 return Ok(());
8440 }
8441 let value = get_prop_chain(&step, "value")?;
8442 if let Err(e) = f(value, i) {
8443 // The callback's error wins over anything `return()` raises, so a
8444 // throwing `return` is swallowed here (7.4.9 step 6).
8445 let _ = close_iterator(&iterator);
8446 return Err(e);
8447 }
8448 i += 1;
8449 }
8450}
8451
8452/// Call `iterator.return()` if it has one, as IteratorClose does.
8453pub fn close_iterator(iterator: &Value) -> Result<(), String> {
8454 let has = crate::builtins::get_property(iterator, "return")?;
8455 if with_host(|h| is_callable(h, &has)) {
8456 call_method(iterator, "return", Vec::new())?;
8457 }
8458 Ok(())
8459}
8460
8461pub(crate) fn drain_iterator(iterator: &Value) -> Result<Vec<Value>, String> {
8462 let mut out = Vec::new();
8463 loop {
8464 let step = call_method(iterator, "next", Vec::new())?;
8465 let done = get_prop_chain(&step, "done")?;
8466 if with_host(|h| h.truthy(&done)) {
8467 break;
8468 }
8469 out.push(get_prop_chain(&step, "value")?);
8470 }
8471 Ok(out)
8472}
8473
8474/// Property read that walks the prototype chain (used by iteration helpers).
8475/// Whether the builtin named `n` is CALLABLE. Most are (`Array`, `parseInt`,
8476/// `Math.floor`); the exceptions are the namespace objects a script can only
8477/// read properties off (`Math`, `JSON`, every `require()`d core module), which
8478/// report `typeof === "object"` and carry no `name`/`length`.
8479pub fn builtin_is_callable(n: &str) -> bool {
8480 // A `<Ctor>.prototype` handle is a namespace of methods, not a function:
8481 // `typeof Set.prototype` is `"object"`, and treating it as callable made it
8482 // brand `[object Function]`, inspect as `[Function: prototype]`, and answer
8483 // `true` to `instanceof Function`. `Function.prototype` is the one that
8484 // really IS callable (10.2.4: it is an anonymous built-in that returns
8485 // undefined), which is why it is not stripped here.
8486 if n != "Function.prototype" && n.ends_with(".prototype") {
8487 return false;
8488 }
8489 // A `match` rather than a slice scan: this runs on every callability test,
8490 // which is every call and every `ToPrimitive`, and a `contains` over the
8491 // list below compares against all 56 entries before answering "callable" —
8492 // the common case. The compiler turns the arms into a length-then-bytes
8493 // decision tree instead.
8494 !matches!(
8495 n,
8496 "Math"
8497 | "JSON"
8498 | "console"
8499 | "Reflect"
8500 | "process"
8501 | "Atomics"
8502 | "performance"
8503 | "fs"
8504 | "path"
8505 | "os"
8506 | "util"
8507 | "crypto"
8508 | "webcrypto"
8509 | "SubtleCrypto"
8510 | "querystring"
8511 | "events"
8512 | "timers"
8513 | "perf_hooks"
8514 | "async_hooks"
8515 | "diagnostics_channel"
8516 | "v8"
8517 | "dns"
8518 | "punycode"
8519 | "child_process"
8520 | "tty"
8521 | "url"
8522 | "zlib"
8523 | "string_decoder"
8524 | "http"
8525 | "net"
8526 | "buffer"
8527 | "function"
8528 | "path/win32"
8529 | "fs/promises"
8530 | "stream/promises"
8531 | "stream/consumers"
8532 | "stream/web"
8533 | "timers/promises"
8534 | "dns/promises"
8535 | "https"
8536 | "http2"
8537 | "tls"
8538 | "dgram"
8539 | "cluster"
8540 | "worker_threads"
8541 | "readline"
8542 | "readline/promises"
8543 | "repl"
8544 | "vm"
8545 | "domain"
8546 | "trace_events"
8547 | "wasi"
8548 | "inspector"
8549 | "object"
8550 )
8551 // The live `require.cache` view is a plain object to a script, not
8552 // something it can call.
8553 && n != crate::builtins::REQUIRE_CACHE
8554}
8555
8556pub fn get_prop_chain(recv: &Value, name: &str) -> Result<Value, String> {
8557 crate::builtins::get_property(recv, name)
8558}
8559
8560/// Whether `v` is an ECMAScript primitive, i.e. `ToPrimitive` is the identity
8561/// on it. `undefined`, `null`, booleans, numbers, strings, symbols and bigints
8562/// qualify; every other heap cell (objects, arrays, functions, `Map`/`Set`,
8563/// native-tagged instances) is an object and must be converted.
8564pub fn is_primitive(h: &JsHost, v: &Value) -> bool {
8565 match v {
8566 Value::Obj(_) => matches!(
8567 h.get(v),
8568 None | Some(JsObj::Null)
8569 | Some(JsObj::Str(_))
8570 | Some(JsObj::Symbol { .. })
8571 | Some(JsObj::BigInt(_))
8572 ),
8573 _ => true,
8574 }
8575}
8576
8577/// `ToPrimitive(v, hint)` — ECMA-262 7.1.1. `hint` is `"default"`, `"number"`
8578/// or `"string"`.
8579///
8580/// An object carrying a `Symbol.toPrimitive` method (internal key
8581/// `@@toPrimitive`) has it called with the hint and must return a primitive.
8582/// Otherwise `OrdinaryToPrimitive` (7.1.1.1) tries `valueOf` then `toString` —
8583/// the order reversed for the string hint — and takes the FIRST call whose
8584/// result is a primitive. An object that yields no primitive (a null-prototype
8585/// object has neither method) throws V8's
8586/// `TypeError: Cannot convert object to primitive value`.
8587///
8588/// This is the conversion behind `+`, `-`/`*`/`/`/`%`/`**`, the relational
8589/// operators, `==` against a primitive, and `ToPropertyKey` — all of which used
8590/// to read `str_of` directly and so never invoked a user `valueOf`.
8591pub fn to_primitive(v: &Value, hint: &str) -> Result<Value, String> {
8592 if with_host(|h| is_primitive(h, v)) {
8593 return Ok(v.clone());
8594 }
8595 if let Some(f) = protocol_lookup(v, "@@toPrimitive")? {
8596 if with_host(|h| is_callable(h, &f)) {
8597 let hv = with_host(|h| h.new_str(hint.to_string()));
8598 let r = invoke(&f, vec![hv], Some(v.clone()))?;
8599 if with_host(|h| is_primitive(h, &r)) {
8600 return Ok(r);
8601 }
8602 return Err(type_error("Cannot convert object to primitive value"));
8603 }
8604 }
8605 // `Date.prototype[@@toPrimitive]` (21.4.4.45) treats the DEFAULT hint as
8606 // `"string"`, which is why `new Date() + 1` concatenates while
8607 // `new Date() - 1` is arithmetic.
8608 let hint = if hint == "default" && crate::stdlib::native_tag(v).as_deref() == Some("Date") {
8609 "string"
8610 } else {
8611 hint
8612 };
8613 let order = if hint == "string" {
8614 ["toString", "valueOf"]
8615 } else {
8616 ["valueOf", "toString"]
8617 };
8618 // Whether either candidate was actually CALLED. The `[object Tag]` fallback
8619 // below is for exotics whose property funnel exposes no callable
8620 // `toString`, not for an object whose own methods ran and returned
8621 // non-primitives — that case is the spec's TypeError, and branding it
8622 // instead meant `({ valueOf: () => ({}), toString: () => ({}) }) + 1`
8623 // quietly produced `"[object Object]1"`.
8624 let mut called_any = false;
8625 for m in order {
8626 let f = crate::builtins::get_property(v, m).unwrap_or(Value::Undef);
8627 if !with_host(|h| is_callable(h, &f)) {
8628 continue;
8629 }
8630 called_any = true;
8631 // On a Proxy the resolved method is a thunk bound to the TARGET, so
8632 // invoking it directly would stringify the target — `String(new
8633 // Proxy(function f(){}, {}))` reported `f`'s source where V8 reports the
8634 // native-code form. `call_method` re-dispatches the generic
8635 // `Function.prototype`/`Object.prototype` methods against the proxy.
8636 let r = if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8637 call_method(v, m, Vec::new())?
8638 } else {
8639 invoke(&f, Vec::new(), Some(v.clone()))?
8640 };
8641 if with_host(|h| is_primitive(h, &r)) {
8642 return Ok(r);
8643 }
8644 }
8645 // Every object except a null-prototype one inherits `Object.prototype
8646 // .toString`, which always returns a string — so the exhausted-methods
8647 // TypeError is reachable only there. The exotics whose property funnel has
8648 // no `toString` entry of its own (`Map`, `Set`, `Promise`, …) land here and
8649 // get the same `[object Tag]` brand V8 gives them.
8650 // A proxy WITH a `get` trap is not one of those exotics: the trap answered
8651 // for both method names, and if what came back was not callable there is
8652 // nothing left to call — `String(new Proxy({}, { get: () => undefined }))`
8653 // is a TypeError on node, where branding it `[object Object]` invented a
8654 // conversion the trap explicitly refused. A TRAPLESS proxy is different: its
8655 // read forwarded to the target, so `String(new Proxy(new Map(), {}))` gets
8656 // the target's `[object Map]` brand exactly as the bare `Map` does.
8657 if !called_any && !with_host(|h| h.has_null_proto(v)) && !crate::proxy::has_trap(v, "get") {
8658 return crate::builtins::proto_method(v, "Object:toString", Vec::new());
8659 }
8660 Err(type_error("Cannot convert object to primitive value"))
8661}
8662
8663/// `ToString(v)` with `ToPrimitive` method dispatch: an object is converted
8664/// with the string hint (so a user `toString` — or `valueOf`, if `toString`
8665/// is absent or returns an object — is invoked), then rendered by `str_of`.
8666/// Returns a heap string value.
8667pub fn to_string_value(v: &Value) -> Result<Value, String> {
8668 let p = to_primitive(v, "string")?;
8669 // `ToString(symbol)` throws (7.1.17 step 2) — the ONLY conversion a symbol
8670 // refuses. `String(sym)` is the documented exception and is handled at that
8671 // call site, not here, so every implicit coercion (`sym + ''`, `` `${sym}` ``,
8672 // `[sym].join()`) rejects the way node does instead of silently rendering
8673 // `Symbol(desc)`.
8674 if with_host(|h| matches!(h.get(&p), Some(JsObj::Symbol { .. }))) {
8675 return Err(type_error("Cannot convert a Symbol value to a string"));
8676 }
8677 Ok(with_host(|h| {
8678 let s = h.str_of(&p);
8679 h.new_str(s)
8680 }))
8681}
8682
8683/// `String(v)` — 22.1.1.1. Identical to [`to_string_value`] except that a
8684/// SYMBOL argument is allowed and renders as `Symbol(desc)` (step 2a).
8685pub fn string_ctor_value(v: &Value) -> Result<Value, String> {
8686 if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8687 return Ok(with_host(|h| {
8688 let s = h.str_of(v);
8689 h.new_str(s)
8690 }));
8691 }
8692 to_string_value(v)
8693}
8694
8695/// `ToNumber(v)` — ECMA-262 7.1.4 — with the object case going through
8696/// `ToPrimitive(v, number)` first, so `+{ valueOf() { return 7 } }` is `7` and
8697/// `+new Date(0)` is `0`. `JsHost::to_number` alone cannot do this: it runs
8698/// under the host borrow and so can never invoke a JS `valueOf`.
8699pub fn to_number_value(v: &Value) -> Result<f64, String> {
8700 // `ToNumber(symbol)` throws (7.1.4 step 2). It is primitive, so without this
8701 // it fell into `to_number` and quietly produced `NaN` — `Number(Symbol())`
8702 // and `+Symbol()` are both `TypeError` on node v26.7.0.
8703 if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8704 return Err(type_error("Cannot convert a Symbol value to a number"));
8705 }
8706 if let Some(n) = with_host(|h| is_primitive(h, v).then(|| h.to_number(v))) {
8707 return Ok(n);
8708 }
8709 let p = to_primitive(v, "number")?;
8710 // Steps 2-3 apply to the ToPrimitive RESULT, not only to the argument. Only
8711 // the argument was checked, so an object whose conversion yields a symbol or
8712 // a BigInt slipped past: `+Object(9n)` answered 9 and
8713 // `+{ [Symbol.toPrimitive]() { return Symbol('s') } }` answered NaN, where
8714 // both are TypeErrors. `Number(x)` is ToNumeric and keeps its own path,
8715 // which is why `Number(Object(9n))` is still 9.
8716 match with_host(|h| h.get(&p).cloned()) {
8717 Some(JsObj::Symbol { .. }) => Err(type_error("Cannot convert a Symbol value to a number")),
8718 Some(JsObj::BigInt(_)) => Err(type_error("Cannot convert a BigInt value to a number")),
8719 _ => Ok(with_host(|h| h.to_number(&p))),
8720 }
8721}
8722
8723/// `ToPropertyKey(v)` — ECMA-262 7.1.19. A symbol keeps its stable internal
8724/// key; anything else is `ToPrimitive(v, string)` then `ToString`, so
8725/// `obj[{ toString() { return 'k' } }]` really reads `obj.k`.
8726pub fn to_property_key(v: &Value) -> Result<String, String> {
8727 // One borrow for the overwhelmingly common primitive key (`a[i]`, `o[s]`,
8728 // `o[sym]`); only an object key pays for the conversion.
8729 if let Some(k) = with_host(|h| is_primitive(h, v).then(|| h.property_key(v))) {
8730 return Ok(k);
8731 }
8732 let p = to_primitive(v, "string")?;
8733 Ok(with_host(|h| h.str_of(&p)))
8734}
8735
8736/// Whether `h.get(v)` is any callable kind. A Proxy is callable exactly when its
8737/// target is (10.5: the `[[Call]]` slot is installed only for a callable
8738/// target), so `typeof` and every `is_callable` guard agree on one answer.
8739pub fn is_callable(h: &JsHost, v: &Value) -> bool {
8740 match h.get(v) {
8741 // Not every builtin is a function: the namespace objects (`Math`,
8742 // `require('fs')`) and the `<Ctor>.prototype` handles are data, and
8743 // calling one is a `TypeError` in node exactly as `typeof` says.
8744 Some(JsObj::Builtin(n)) => builtin_is_callable(n),
8745 Some(JsObj::Func(_))
8746 | Some(JsObj::BoundMethod { .. })
8747 | Some(JsObj::BoundFunc { .. })
8748 | Some(JsObj::Class(_)) => true,
8749 Some(JsObj::Proxy { target, .. }) => is_callable(h, target),
8750 _ => false,
8751 }
8752}
8753
8754/// Walk `recv`'s own props then its prototype chain for `key`, returning the
8755/// stored value (methods, inherited data props). Does NOT invoke accessors.
8756/// A PROTOCOL lookup — `Symbol.toPrimitive`, `Symbol.hasInstance`, `toJSON`,
8757/// `then` and the rest — which the spec performs with `[[Get]]`.
8758///
8759/// That distinction only shows on a PROXY: `lookup_chain` walks the property
8760/// map and never asks the handler, so a proxy supplying a protocol method
8761/// through its `get` trap was invisible and the operation fell back to the
8762/// default. Everything else takes the cheap chain walk.
8763pub fn protocol_lookup(v: &Value, key: &str) -> Result<Option<Value>, String> {
8764 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8765 let got = crate::builtins::get_property(v, key)?;
8766 return Ok((!matches!(got, Value::Undef)).then_some(got));
8767 }
8768 Ok(with_host(|h| lookup_chain(h, v, key)))
8769}
8770
8771pub fn lookup_chain(h: &JsHost, recv: &Value, key: &str) -> Option<Value> {
8772 if let Some(JsObj::Object(p)) = h.get(recv) {
8773 if let Some(v) = p.get(key) {
8774 return Some(v.clone());
8775 }
8776 }
8777 let mut cur = h.proto_of(recv);
8778 while let Some(p) = cur {
8779 // A chain link may be a plain object OR a function/class (the `router`
8780 // package sets `Router.prototype = function(){}` and hangs its methods off
8781 // that function, so the methods live in the fn-prop side table).
8782 match h.get(&p) {
8783 Some(JsObj::Object(props)) => {
8784 if let Some(v) = props.get(key) {
8785 return Some(v.clone());
8786 }
8787 }
8788 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
8789 if let Some(v) = h.fn_prop(&p, key) {
8790 return Some(v);
8791 }
8792 }
8793 _ => {}
8794 }
8795 cur = h.proto_of(&p);
8796 }
8797 None
8798}
8799
8800/// Find a getter/setter accessor for `key` on `recv` or up its prototype chain.
8801pub fn lookup_accessor(
8802 h: &JsHost,
8803 recv: &Value,
8804 key: &str,
8805) -> Option<(Option<Value>, Option<Value>)> {
8806 if let Some(a) = h.own_accessor(recv, key) {
8807 return Some(a);
8808 }
8809 let mut cur = h.proto_of(recv);
8810 while let Some(p) = cur {
8811 if let Some(a) = h.own_accessor(&p, key) {
8812 return Some(a);
8813 }
8814 cur = h.proto_of(&p);
8815 }
8816 // A STATIC accessor declared by an ancestor class. A subclass reaches its
8817 // parent's statics through `ClassVal.parent`, not the `protos` map the walk
8818 // above reads — classes are not linked there, so that walk ended at once.
8819 // Static methods and fields already inherited because `class_static` does
8820 // this same parent walk for `fn_prop`; only accessors had no equivalent:
8821 //
8822 // class Base { static get kind() { return 'base' } }
8823 // class Sub extends Base {}
8824 // Sub.plain() // worked, a fn_prop
8825 // Sub.kind // undefined; node reads 'base'
8826 //
8827 // The caller invokes the getter with the class it was READ off as `this`,
8828 // so a getter reading `this.x` sees the subclass, per 10.2.4.
8829 let mut cls = recv.clone();
8830 while let Some(JsObj::Class(c)) = h.get(&cls) {
8831 let Some(parent) = c.parent.clone() else {
8832 break;
8833 };
8834 if let Some(a) = h.own_accessor(&parent, key) {
8835 return Some(a);
8836 }
8837 cls = parent;
8838 }
8839 None
8840}
8841
8842/// Register a builtin error prototype (for `instanceof Error` etc.).
8843pub fn set_error_proto(name: &str, proto: Value) {
8844 with_host(|h| {
8845 h.error_protos.insert(name.to_string(), proto);
8846 });
8847}
8848pub fn error_proto(name: &str) -> Option<Value> {
8849 with_host(|h| h.error_protos.get(name).cloned())
8850}
8851/// Error prototype lookup with a borrowed host (used inside a `with_host` block).
8852pub fn error_proto_of(h: &JsHost, name: &str) -> Option<Value> {
8853 h.error_protos.get(name).cloned()
8854}
8855
8856impl JsHost {
8857 /// `Error.prototype.toString` for an object whose prototype chain reaches
8858 /// `Error.prototype`: `"Name"` with an empty message, else `"Name: message"`.
8859 /// `None` for anything that is not an error, so the caller keeps its own
8860 /// stringification.
8861 pub fn error_to_string(&self, v: &Value) -> Option<String> {
8862 let base = self.error_protos.get("Error")?;
8863 let mut cur = self.proto_of(v);
8864 let mut is_error = false;
8865 while let Some(p) = cur {
8866 if self.strict_eq(&p, base) {
8867 is_error = true;
8868 break;
8869 }
8870 cur = self.proto_of(&p);
8871 }
8872 if !is_error {
8873 return None;
8874 }
8875 let name = lookup_chain(self, v, "name")
8876 .map(|n| self.str_of(&n))
8877 .unwrap_or_else(|| "Error".into());
8878 let message = lookup_chain(self, v, "message")
8879 .map(|m| self.str_of(&m))
8880 .unwrap_or_default();
8881 // Node's internal coded errors override `toString` as
8882 // `${name} [${code}]: ${message}` (internal/errors.js NodeError). The
8883 // `@@nodeError` tag marks the errors `synth_error` built from a
8884 // `Name [ERR_CODE]: …` string, so a user error that merely has a `.code`
8885 // property still stringifies plainly.
8886 if let Some(JsObj::Object(p)) = self.get(v) {
8887 if p.contains_key("@@nodeError") {
8888 if let Some(code) = p.get("code").map(|c| self.str_of(c)) {
8889 return Some(format!("{name} [{code}]: {message}"));
8890 }
8891 }
8892 }
8893 Some(match (name.is_empty(), message.is_empty()) {
8894 (true, _) => message,
8895 (false, true) => name,
8896 (false, false) => format!("{name}: {message}"),
8897 })
8898 }
8899}
8900
8901/// The set of builtin error constructor names forming the error hierarchy.
8902pub const ERROR_NAMES: &[&str] = &[
8903 "Error",
8904 "TypeError",
8905 "RangeError",
8906 "SyntaxError",
8907 "ReferenceError",
8908 "EvalError",
8909 "URIError",
8910 "AggregateError",
8911 // `assert`'s error class. It is NOT a global (node exposes it only as
8912 // `assert.AssertionError`, and `GLOBAL_FUNCS` is a separate table), but it
8913 // has to be a name `synth_error` recognizes: without it the head
8914 // `AssertionError [ERR_ASSERTION]: …` failed the class check and fell into
8915 // the `Error` branch with the WHOLE head kept as the message, so `e.name`
8916 // was `Error` and `e.message` carried a prefix node keeps out of it.
8917 "AssertionError",
8918 // The WHATWG error class `AbortSignal.reason` carries. Unlike the others its
8919 // `name` comes from the SECOND constructor argument rather than from the
8920 // class, so its prototype keeps the base default and each instance stamps
8921 // its own name into an internal slot.
8922 "DOMException",
8923];
8924
8925impl JsHost {
8926 /// Lazily build the builtin error prototype chain: `Error.prototype →
8927 /// Object.prototype`, and every specific error's prototype → `Error.prototype`.
8928 /// Populated once; instances link to these so `e instanceof TypeError` and
8929 /// `e instanceof Error` both hold.
8930 /// The real `Buffer.prototype` object, building the
8931 /// `Buffer.prototype → Uint8Array.prototype → Object.prototype` chain on
8932 /// first use.
8933 ///
8934 /// A `Buffer` used to be a bare tagged object with no `[[Prototype]]` at
8935 /// all, so `Object.getPrototypeOf(buf) === Buffer.prototype` read false and
8936 /// `instanceof` had to be special-cased around it. Each prototype is a
8937 /// genuine object carrying `@proto:<Ctor>:<method>` thunks for its instance
8938 /// methods, so `Buffer.prototype.slice.call(buf, 1)` still dispatches the
8939 /// way it did when `Buffer.prototype` was a `Builtin` namespace.
8940 pub fn ensure_native_protos(&mut self) {
8941 // The wrapper prototypes share this registry and this guard would skip
8942 // them, so they are built through their own.
8943 self.ensure_wrapper_protos();
8944 self.ensure_function_kind_protos();
8945 if self.native_protos.contains_key("Buffer") {
8946 return;
8947 }
8948 let obj_proto = self.object_proto();
8949 // `Object.prototype` is the one builtin prototype that already existed as
8950 // a real object (it is the chain root). Register it so `Object.prototype`
8951 // reads resolve to THAT object rather than a fresh `Builtin` namespace —
8952 // otherwise `Object.getPrototypeOf(C.prototype) === Object.prototype`
8953 // compares a real object against a thunk and reads false.
8954 self.native_protos
8955 .insert("Object".to_string(), obj_proto.clone());
8956 for m in crate::builtins::OBJECT_PROTO_METHODS {
8957 let thunk = self.alloc(JsObj::Builtin(format!("@proto:Object:{m}")));
8958 if let Some(JsObj::Object(p)) = self.get_mut(&obj_proto) {
8959 p.insert((*m).to_string(), thunk);
8960 }
8961 self.hide_prop(&obj_proto, m);
8962 }
8963 // `Buffer.prototype → Uint8Array.prototype → %TypedArray%.prototype →
8964 // Object.prototype`, which is the chain node v26.7.0 really has. The
8965 // shared iteration methods (`every`, `map`, `filter`, …) live on the
8966 // `%TypedArray%.prototype` intermediate, NOT on `Uint8Array.prototype`:
8967 // measured, `Uint8Array.prototype.hasOwnProperty('every')` is false in
8968 // Node while the intermediate owns it. `%TypedArray%` is not a global,
8969 // so it is reachable only by walking the chain — exactly as in Node.
8970 // Every element kind gets its own prototype hanging off the shared
8971 // intermediate, so `Object.getPrototypeOf(new Int32Array(1))` is
8972 // `Int32Array.prototype` rather than some other kind's. Linking them all
8973 // to `Uint8Array.prototype` would have been the easy version and would
8974 // have made an `Int32Array` claim the wrong prototype.
8975 let mut chain: Vec<(&str, Value)> = vec![("TypedArray", obj_proto)];
8976 for kind in crate::stdlib::typedarray::ELEMENT_KINDS {
8977 chain.push((kind, Value::Undef)); // parent: %TypedArray%.prototype
8978 }
8979 // `Buffer.prototype`'s parent is `Uint8Array.prototype` specifically.
8980 chain.push(("Buffer", Value::Undef));
8981 let mut prev: Option<Value> = None;
8982 for (ctor, parent) in chain.drain(..) {
8983 let proto = self.new_object(IndexMap::new());
8984 // Each kind hangs off the shared intermediate; `Buffer` hangs off
8985 // `Uint8Array.prototype`; the intermediate itself off
8986 // `Object.prototype`.
8987 let parent = match ctor {
8988 "TypedArray" => parent,
8989 "Buffer" => self
8990 .native_protos
8991 .get("Uint8Array")
8992 .cloned()
8993 .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
8994 _ => self
8995 .native_protos
8996 .get("TypedArray")
8997 .cloned()
8998 .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
8999 };
9000 self.set_proto(&proto, parent);
9001 // `%TypedArray%.prototype` has no reachable constructor global, so
9002 // it gets no `constructor` slot (Node's is the anonymous
9003 // `%TypedArray%` intrinsic).
9004 if ctor != "TypedArray" {
9005 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9006 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9007 p.insert("constructor".into(), ctor_val);
9008 }
9009 self.hide_prop(&proto, "constructor");
9010 }
9011 let methods: &[&str] = match ctor {
9012 "Buffer" => crate::stdlib::buffer::INSTANCE_METHODS,
9013 "TypedArray" => crate::stdlib::typedarray::PROTOTYPE_METHODS,
9014 // `Uint8Array` alone owns the base64/hex pair — no other view
9015 // has them, which is the whole reason they cannot live on the
9016 // shared `%TypedArray%` prototype above.
9017 "Uint8Array" => crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS,
9018 // Every other kind's prototype owns no methods; it inherits them
9019 // from the intermediate above. It does own `BYTES_PER_ELEMENT`,
9020 // which is per-kind and which Node really keeps there (measured:
9021 // `Uint8Array.prototype.hasOwnProperty('BYTES_PER_ELEMENT')`).
9022 _ => &[],
9023 };
9024 if crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor) {
9025 let bpe = Value::Float(crate::stdlib::typedarray::bytes_per_element(ctor) as f64);
9026 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9027 p.insert("BYTES_PER_ELEMENT".into(), bpe);
9028 }
9029 self.hide_prop(&proto, "BYTES_PER_ELEMENT");
9030 }
9031 for m in methods {
9032 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9033 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9034 p.insert((*m).to_string(), thunk);
9035 }
9036 self.hide_prop(&proto, m);
9037 }
9038 self.native_protos.insert(ctor.to_string(), proto.clone());
9039 prev = Some(proto);
9040 }
9041 }
9042
9043 /// `String.prototype`, `Number.prototype` and `Boolean.prototype` as REAL
9044 /// objects.
9045 ///
9046 /// A wrapper built by `new String("a")` needs a genuine `[[Prototype]]`
9047 /// link: `Builtin("String.prototype")` is a thunk namespace that cannot
9048 /// appear on a prototype chain, so `Object.getPrototypeOf(w) ===
9049 /// String.prototype` and `w instanceof String` both read false while the
9050 /// wrapper's methods still resolved through the string funnel. Registering
9051 /// them here puts them on the same footing as `Buffer.prototype`.
9052 /// `GeneratorFunction.prototype`, `AsyncFunction.prototype` and
9053 /// `AsyncGeneratorFunction.prototype` — the intrinsics a generator or async
9054 /// function's `[[Prototype]]` really points at.
9055 ///
9056 /// None are globals (node exposes them only through
9057 /// `Object.getPrototypeOf(function*(){}).constructor`), so they live here
9058 /// rather than among the wrapper constructors. Each hangs off
9059 /// `Function.prototype` and carries the `Symbol.toStringTag` that names it.
9060 pub fn ensure_function_kind_protos(&mut self) {
9061 if self.native_protos.contains_key("GeneratorFunction") {
9062 return;
9063 }
9064 let base = self
9065 .native_protos
9066 .get("Function")
9067 .cloned()
9068 .unwrap_or_else(|| self.object_proto());
9069 for ctor in [
9070 "GeneratorFunction",
9071 "AsyncFunction",
9072 "AsyncGeneratorFunction",
9073 ] {
9074 let proto = self.new_object(IndexMap::new());
9075 self.set_proto(&proto, base.clone());
9076 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9077 let tag = self.new_str(ctor);
9078 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9079 p.insert("constructor".into(), ctor_val);
9080 p.insert("@@toStringTag".into(), tag);
9081 }
9082 self.hide_prop(&proto, "constructor");
9083 self.hide_prop(&proto, "@@toStringTag");
9084 self.native_protos.insert(ctor.to_string(), proto);
9085 }
9086 }
9087
9088 pub fn ensure_wrapper_protos(&mut self) {
9089 if self.native_protos.contains_key("String") {
9090 return;
9091 }
9092 let obj_proto = self.object_proto();
9093 for ctor in ["String", "Number", "Boolean", "Symbol", "BigInt"] {
9094 let proto = self.new_object(IndexMap::new());
9095 self.set_proto(&proto, obj_proto.clone());
9096 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9097 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9098 p.insert("constructor".into(), ctor_val);
9099 }
9100 self.hide_prop(&proto, "constructor");
9101 // The three conversions must be here because `Object.prototype`
9102 // also defines them: without a shadowing entry a wrapper would
9103 // inherit the object forms and `String(new String("a"))` would
9104 // report `[object Object]`.
9105 //
9106 // The REST are here because the prototype is a real object a script
9107 // can read a method OFF of. Only the three were installed, on the
9108 // reasoning that `charAt`/`toFixed`/… reach the primitive through
9109 // `call_method` anyway — true for `s.charAt(0)` and false for the
9110 // generic-borrowing form: `String.prototype.trim` read `undefined`,
9111 // so `String.prototype.trim.call(s)` — and `Number.prototype
9112 // .toFixed.call(n)`, and every `Array.prototype`-style borrow of a
9113 // wrapper method — threw. `Array.prototype`/`Object.prototype`
9114 // already carried their whole method set; these three did not.
9115 let methods: Vec<&str> = ["toString", "valueOf", "toLocaleString"]
9116 .into_iter()
9117 .chain(match ctor {
9118 "String" => crate::builtins::STRING_PROTO_METHODS.iter().copied(),
9119 "Number" => crate::builtins::NUMBER_PROTO_METHODS.iter().copied(),
9120 _ => [].iter().copied(),
9121 })
9122 // The symbol-keyed methods come from the generated intrinsic
9123 // table, so this object advertises exactly the symbol methods
9124 // node defines on it — `String.prototype[Symbol.iterator]` was
9125 // `undefined` because only the string-keyed lists were walked.
9126 .chain(crate::builtins::proto_symbol_methods(ctor))
9127 .collect();
9128 let mut seen: Vec<&str> = Vec::new();
9129 for m in methods {
9130 if seen.contains(&m) {
9131 continue;
9132 }
9133 seen.push(m);
9134 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9135 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9136 p.insert(m.to_string(), thunk);
9137 }
9138 self.hide_prop(&proto, m);
9139 }
9140 // `Symbol.prototype` and `BigInt.prototype` are the two wrapper
9141 // prototypes that carry a `@@toStringTag`; the other three are
9142 // branded by their internal slot instead, and node reports
9143 // `undefined` for their tag. Without it
9144 // `Object.prototype.toString.call(Symbol.prototype)` read
9145 // `[object Object]` where node says `[object Symbol]`.
9146 if matches!(ctor, "Symbol" | "BigInt") {
9147 let tag = self.new_str(ctor.to_string());
9148 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9149 p.insert("@@toStringTag".into(), tag);
9150 }
9151 // Not `hide_prop`: a well-known `@@toStringTag` is read-only as
9152 // well as non-enumerable (20.4.3.6), and `hide_prop` leaves it
9153 // writable.
9154 self.set_prop_attrs(
9155 &proto,
9156 "@@toStringTag",
9157 PropAttrs {
9158 writable: false,
9159 enumerable: false,
9160 configurable: true,
9161 },
9162 );
9163 }
9164 // `Symbol.prototype[@@toPrimitive]` (20.4.3.5) is what a string or
9165 // numeric conversion of a symbol reaches FIRST. Its absence was
9166 // observable in the failure wording: `String(Symbol.prototype)`
9167 // throws in node because `@@toPrimitive` rejects a non-Symbol
9168 // `this`, and here the conversion fell through to `toString` and
9169 // named that method in the message instead.
9170 if ctor == "Symbol" {
9171 let thunk = self.alloc(JsObj::Builtin("@proto:Symbol:@@toPrimitive".to_string()));
9172 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9173 p.insert("@@toPrimitive".into(), thunk);
9174 }
9175 self.hide_prop(&proto, "@@toPrimitive");
9176 }
9177 self.native_protos.insert(ctor.to_string(), proto);
9178 }
9179 }
9180
9181 /// The cached template object for one tagged-template site, if it has been
9182 /// evaluated before.
9183 pub fn template_object(&self, key: (u64, u64)) -> Option<Value> {
9184 self.template_objects.get(&key).cloned()
9185 }
9186 /// Record the template object for one tagged-template site.
9187 pub fn set_template_object(&mut self, key: (u64, u64), v: Value) {
9188 self.template_objects.insert(key, v);
9189 }
9190
9191 /// The real prototype object for a builtin exotic, if it has one.
9192 pub fn native_proto(&self, ctor: &str) -> Option<Value> {
9193 self.native_protos.get(ctor).cloned()
9194 }
9195
9196 /// The constructor name whose `.prototype` object IS `v`, for a prototype
9197 /// this host built as a real object (`String.prototype`, `TypeError
9198 /// .prototype`, `Buffer.prototype`) rather than as a `Builtin` namespace.
9199 ///
9200 /// A prototype is an ORDINARY object: it carries no instance's internal
9201 /// slot, so `Object.prototype.toString.call(TypeError.prototype)` is
9202 /// `[object Object]` and not `[object Error]`. Nothing distinguished the two
9203 /// before, so the brand fell through to the "does it look like an Error"
9204 /// test and answered for the prototype as if it were an instance.
9205 pub fn intrinsic_proto_ctor(&self, v: &Value) -> Option<&str> {
9206 if !matches!(v, Value::Obj(_)) {
9207 return None;
9208 }
9209 self.native_protos
9210 .iter()
9211 .chain(self.error_protos.iter())
9212 .find(|(_, p)| *p == v)
9213 .map(|(name, _)| name.as_str())
9214 }
9215
9216 /// The real `.prototype` object for a native stdlib constructor (`StringDecoder`,
9217 /// `Hash`, `URLSearchParams`, …), built on first read and cached.
9218 ///
9219 /// `Ctor.prototype` used to read `undefined` for every native class outside the
9220 /// hand-written `is_builtin_ctor` list, which broke the ES5 subclassing pattern
9221 /// that libraries still use. `iconv-lite`'s internal codec — reached from
9222 /// `raw-body` on every `express.json()` request — does exactly this:
9223 ///
9224 /// ```text
9225 /// var StringDecoder = require('string_decoder').StringDecoder;
9226 /// if (!StringDecoder.prototype.end) StringDecoder.prototype.end = function () {};
9227 /// function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
9228 /// InternalDecoder.prototype = StringDecoder.prototype;
9229 /// ```
9230 ///
9231 /// The first line threw `Cannot read properties of undefined (reading 'end')`.
9232 ///
9233 /// Methods come from `stdlib::instance_method_lists`, the same table a method
9234 /// READ consults, so the prototype can never advertise a name the dispatcher
9235 /// does not implement. Each is the `@proto:<Ctor>:<method>` thunk that
9236 /// dispatches against its invoke-time `this`, so a subclass instance whose
9237 /// prototype IS this object gets the native implementation. Returns `None` for
9238 /// a tag with no instance methods, leaving those constructors as they were.
9239 pub fn ensure_ctor_proto(&mut self, ctor: &str) -> Option<Value> {
9240 if let Some(p) = self.native_protos.get(ctor) {
9241 return Some(p.clone());
9242 }
9243 // `Buffer` and the typed-array kinds belong to the chain
9244 // `ensure_native_protos` builds. Building one of them here first hung it
9245 // straight off `Object.prototype` AND registered it, which made that
9246 // chain's own `contains_key("Buffer")` guard skip the build for the rest
9247 // of the process: after `Buffer.from([1])`, `Buffer.prototype instanceof
9248 // Uint8Array` read false.
9249 if ctor == "Buffer"
9250 || ctor == "TypedArray"
9251 || crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor)
9252 {
9253 self.ensure_native_protos();
9254 return self.native_protos.get(ctor).cloned();
9255 }
9256 let (own, emitter) = crate::stdlib::instance_method_lists(ctor);
9257 // A class can carry accessors and no methods at all
9258 // (`AsymmetricKeyObject` is only `asymmetricKeyType` and
9259 // `asymmetricKeyDetails`), so an empty method list does not mean there
9260 // is no prototype to build.
9261 let (accessor_list, _) = crate::stdlib::instance_accessors(ctor);
9262 if own.is_empty() && emitter.is_empty() && accessor_list.is_empty() {
9263 return None;
9264 }
9265 // A native class with a real PARENT hangs off that parent's prototype
9266 // rather than straight off `Object.prototype`. The stream hierarchy is
9267 // `Readable → Stream → EventEmitter`, which is what makes
9268 // `new Readable() instanceof Stream` hold and what an ES5 subclass
9269 // doing `Object.create(Stream.prototype)` inherits from.
9270 let parent_proto = match crate::stdlib::native_parent(ctor) {
9271 Some(p) => self
9272 .ensure_ctor_proto(p)
9273 .unwrap_or_else(|| self.object_proto()),
9274 None => self.object_proto(),
9275 };
9276 let proto = self.new_object(IndexMap::new());
9277 self.set_proto(&proto, parent_proto);
9278 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9279 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9280 p.insert("constructor".into(), ctor_val);
9281 }
9282 self.hide_prop(&proto, "constructor");
9283 // A prototype member is ENUMERABLE in node for every class but the few
9284 // written as ES classes, so `for (const k in url)` walks `href` and the
9285 // rest. Hiding all of them made that loop find nothing.
9286 let visible = crate::stdlib::instance_members_enumerable(ctor);
9287 let symbols = crate::builtins::proto_symbol_methods(ctor);
9288 for m in own.iter().chain(emitter.iter()).chain(symbols.iter()) {
9289 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9290 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9291 p.insert((*m).to_string(), thunk);
9292 }
9293 // A symbol-keyed member never enumerates.
9294 if !visible || m.starts_with("@@") {
9295 self.hide_prop(&proto, m);
9296 }
9297 }
9298 // Accessors and the class's `Symbol.toStringTag`, both of which live on
9299 // the PROTOTYPE in node — an instance owns neither.
9300 let (accessors, tag) = crate::stdlib::instance_accessors(ctor);
9301 for (key, settable) in accessors {
9302 let get = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@get@{key}")));
9303 let set =
9304 settable.then(|| self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@set@{key}"))));
9305 self.set_accessor(&proto, key, Some(get), set);
9306 if !visible {
9307 self.hide_prop(&proto, key);
9308 }
9309 }
9310 for m in crate::stdlib::instance_late_methods(ctor) {
9311 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9312 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9313 p.insert((*m).to_string(), thunk);
9314 }
9315 if !visible {
9316 self.hide_prop(&proto, m);
9317 }
9318 }
9319 if !tag.is_empty() {
9320 let tag = self.new_str(tag.to_string());
9321 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9322 p.insert("@@toStringTag".into(), tag);
9323 }
9324 self.hide_prop(&proto, "@@toStringTag");
9325 }
9326 self.native_protos.insert(ctor.to_string(), proto.clone());
9327 Some(proto)
9328 }
9329
9330 /// `<ErrorClass>.prototype`, once [`JsHost::ensure_error_protos`] has run.
9331 /// The error prototypes live in their own table, so `ensure_ctor_proto` —
9332 /// which answers from `native_protos` — does not find them.
9333 pub fn error_proto(&self, name: &str) -> Option<Value> {
9334 self.error_protos.get(name).cloned()
9335 }
9336
9337 pub fn ensure_error_protos(&mut self) {
9338 if !self.error_protos.is_empty() {
9339 return;
9340 }
9341 let obj_proto = self.object_proto();
9342 // Error.prototype first (the shared base).
9343 let err_proto = self.new_object(IndexMap::new());
9344 self.set_proto(&err_proto, obj_proto);
9345 let nm = self.new_str("Error");
9346 let empty = self.new_str("");
9347 let ctor = self.alloc(JsObj::Builtin("Error".into()));
9348 // `Error.prototype.toString` (20.5.3.4) has to be an OWN property here,
9349 // not a fallback the stringifier applies when nothing else matches: it
9350 // exists precisely to shadow `Object.prototype.toString`. Without it,
9351 // the first read of `Error.prototype` or `Object.prototype` — which
9352 // `x instanceof Error` performs, so ordinary code triggers it —
9353 // materialised `Object.prototype.toString`, the chain lookup started
9354 // finding it, and `String(err)` flipped from `Error: m` to
9355 // `[object Error]` for the REST OF THE PROCESS, including errors
9356 // created before the read.
9357 let to_string = self.alloc(JsObj::Builtin("@proto:Error:toString".into()));
9358 if let Some(JsObj::Object(p)) = self.get_mut(&err_proto) {
9359 p.insert("name".into(), nm);
9360 p.insert("message".into(), empty);
9361 p.insert("constructor".into(), ctor);
9362 p.insert("toString".into(), to_string);
9363 }
9364 // Everything on `Error.prototype` is non-enumerable in V8.
9365 for k in ["name", "message", "constructor", "toString"] {
9366 self.hide_prop(&err_proto, k);
9367 }
9368 self.error_protos.insert("Error".into(), err_proto.clone());
9369 for name in &ERROR_NAMES[1..] {
9370 let p = self.new_object(IndexMap::new());
9371 self.set_proto(&p, err_proto.clone());
9372 let nm = self.new_str(*name);
9373 let ctor = self.alloc(JsObj::Builtin((*name).to_string()));
9374 if let Some(JsObj::Object(o)) = self.get_mut(&p) {
9375 o.insert("name".into(), nm);
9376 o.insert("constructor".into(), ctor);
9377 }
9378 self.hide_prop(&p, "name");
9379 self.hide_prop(&p, "constructor");
9380 self.error_protos.insert((*name).to_string(), p);
9381 }
9382 }
9383}
9384
9385// ── Map/Set element access (used by builtins) ────────────────────────────────
9386
9387impl JsHost {
9388 /// A function's `.length`: the count of leading params before the first one
9389 /// with a default or the rest element.
9390 pub fn func_arity(&self, v: &Value) -> usize {
9391 // 20.2.3.2: a bound function's `length` is the target's, less the
9392 // arguments already bound, floored at 0. Reporting 0 for every bound
9393 // function breaks arity dispatch — express picks error-handling
9394 // middleware with `fn.length === 4`, so a bound handler was never
9395 // recognised as one.
9396 if let Some(JsObj::BoundFunc { target, args, .. }) = self.get(v) {
9397 return self.func_arity(&target.clone()).saturating_sub(args.len());
9398 }
9399 // A builtin's arity is the specified one, so `Math.max.bind(null,1)`
9400 // reports 1 rather than the 0 a target of unknown arity would give.
9401 if let Some(JsObj::Builtin(n)) = self.get(v) {
9402 return crate::builtins::builtin_meta(n)
9403 .map(|(_, len)| len as usize)
9404 .unwrap_or(0);
9405 }
9406 let def_id = match self.get(v) {
9407 Some(JsObj::Func(f)) => Some(f.def_id),
9408 Some(JsObj::Class(c)) => match c.ctor.as_ref().and_then(|cf| self.get(cf)) {
9409 Some(JsObj::Func(f)) => Some(f.def_id),
9410 _ => None,
9411 },
9412 _ => None,
9413 };
9414 match def_id.and_then(|id| self.funcs.get(id)) {
9415 Some(def) => def
9416 .params
9417 .iter()
9418 .take_while(|p| !p.rest && !p.has_default)
9419 .count(),
9420 None => 0,
9421 }
9422 }
9423
9424 pub fn is_map(&self, v: &Value) -> bool {
9425 matches!(self.get(v), Some(JsObj::Map { .. }))
9426 }
9427 pub fn is_set(&self, v: &Value) -> bool {
9428 matches!(self.get(v), Some(JsObj::Set { .. }))
9429 }
9430}
9431
9432// ── promises & the event loop ────────────────────────────────────────────────
9433
9434impl JsHost {
9435 /// Allocate a fresh pending promise, returning its heap value.
9436 pub fn new_promise(&mut self) -> Value {
9437 let id = self.promises.len() as u32;
9438 self.promises.push(PromiseCell {
9439 state: PromiseState::Pending,
9440 value: Value::Undef,
9441 reactions: Vec::new(),
9442 handled: false,
9443 });
9444 self.alloc(JsObj::Promise { id })
9445 }
9446 pub fn promise_id(&self, v: &Value) -> Option<u32> {
9447 match self.get(v) {
9448 Some(JsObj::Promise { id }) => Some(*id),
9449 _ => None,
9450 }
9451 }
9452 pub fn promise_state(&self, id: u32) -> PromiseState {
9453 self.promises[id as usize].state
9454 }
9455 pub fn promise_value(&self, id: u32) -> Value {
9456 self.promises[id as usize].value.clone()
9457 }
9458 pub fn promise_mark_handled(&mut self, id: u32) {
9459 self.promises[id as usize].handled = true;
9460 }
9461 /// Take the pending reactions of a promise (called on settle).
9462 pub fn take_reactions(&mut self, id: u32) -> Vec<PromiseReaction> {
9463 std::mem::take(&mut self.promises[id as usize].reactions)
9464 }
9465 pub fn add_reaction(&mut self, id: u32, r: PromiseReaction) {
9466 self.promises[id as usize].reactions.push(r);
9467 }
9468 pub fn settle_promise(&mut self, id: u32, state: PromiseState, value: Value) {
9469 let c = &mut self.promises[id as usize];
9470 if c.state != PromiseState::Pending {
9471 return; // already settled — resolve/reject are one-shot
9472 }
9473 c.state = state;
9474 c.value = value;
9475 }
9476 pub fn queue_micro(&mut self, cb: Value, args: Vec<Value>) {
9477 self.microtasks.push_back(Task::Js { cb, args });
9478 }
9479 pub fn queue_nexttick(&mut self, cb: Value, args: Vec<Value>) {
9480 self.nextticks.push_back(Task::Js { cb, args });
9481 }
9482 /// Schedule a native (Rust) microtask — used by Promise reactions and async
9483 /// resumption.
9484 pub fn queue_micro_native(&mut self, f: Box<dyn FnOnce() -> Result<(), String>>) {
9485 self.microtasks.push_back(Task::Native(f));
9486 }
9487 /// Schedule a macrotask. `interval` is the repeat period for `setInterval`
9488 /// (`None` for the one-shot `setTimeout`/`setImmediate`). Returns the timer
9489 /// id, which the `Timeout`/`Immediate` handle object carries so `clear*`,
9490 /// `ref`/`unref` and `refresh` can find this entry again.
9491 pub fn add_timer(
9492 &mut self,
9493 delay: f64,
9494 callback: Value,
9495 args: Vec<Value>,
9496 interval: Option<f64>,
9497 ) -> u64 {
9498 let id = self.next_timer;
9499 self.next_timer += 1;
9500 // Real deadline for the real-clock path; `setImmediate` (delay < 0) is
9501 // clamped to "now". Virtual-clock ordering still uses `delay`/`seq`.
9502 let deadline = Instant::now() + Duration::from_millis(delay.max(0.0) as u64);
9503 self.macrotasks.push(Timer {
9504 id,
9505 delay,
9506 seq: id,
9507 callback,
9508 args,
9509 cancelled: false,
9510 interval,
9511 refed: true,
9512 deadline,
9513 });
9514 id
9515 }
9516 /// Re-arm a repeating timer that is about to fire, keeping its id (so a
9517 /// `clearInterval` from *inside* the callback cancels this very entry) and
9518 /// taking a fresh `seq` so same-delay peers still round-robin.
9519 ///
9520 /// Called BEFORE the callback runs: if it were called after, the entry would
9521 /// be absent while the callback executed and a `clearInterval(t)` there would
9522 /// cancel nothing, resurrecting an interval the program had stopped.
9523 fn rearm_timer(&mut self, t: &Timer, period: f64) {
9524 let seq = self.next_timer;
9525 self.next_timer += 1;
9526 let deadline = Instant::now() + Duration::from_millis(period.max(0.0) as u64);
9527 self.macrotasks.push(Timer {
9528 id: t.id,
9529 delay: t.delay,
9530 seq,
9531 callback: t.callback.clone(),
9532 args: t.args.clone(),
9533 cancelled: false,
9534 interval: Some(period),
9535 refed: t.refed,
9536 deadline,
9537 });
9538 }
9539 /// `timeout.ref()` / `timeout.unref()` — set the handle bit on a pending
9540 /// timer. A no-op once the timer has fired or been cleared (Node likewise
9541 /// treats `ref`/`unref` on a dead timer as inert).
9542 pub fn set_timer_refed(&mut self, id: u64, refed: bool) {
9543 for t in &mut self.macrotasks {
9544 if t.id == id && !t.cancelled {
9545 t.refed = refed;
9546 }
9547 }
9548 }
9549 /// `timeout.hasRef()` — whether a still-pending timer holds the loop open.
9550 /// A fired or cleared timer reports `false`, matching Node.
9551 pub fn timer_has_ref(&self, id: u64) -> bool {
9552 self.macrotasks
9553 .iter()
9554 .any(|t| t.id == id && !t.cancelled && t.refed)
9555 }
9556 /// `timeout.refresh()` — restart the countdown from now, as if the timer had
9557 /// just been scheduled.
9558 pub fn refresh_timer(&mut self, id: u64) {
9559 let now = Instant::now();
9560 for t in &mut self.macrotasks {
9561 if t.id == id && !t.cancelled {
9562 t.deadline = now + Duration::from_millis(t.delay.max(0.0) as u64);
9563 }
9564 }
9565 }
9566 /// Clone the I/O sender for a background I/O thread.
9567 pub fn io_sender(&self) -> Sender<IoTask> {
9568 self.io_tx.clone()
9569 }
9570 /// Register a live handle (listener/socket/ref'd resource) keeping the loop
9571 /// alive.
9572 pub fn incr_handle(&mut self) {
9573 self.open_handles += 1;
9574 }
9575 /// Release a handle; the loop exits once this reaches `0` with empty queues.
9576 pub fn decr_handle(&mut self) {
9577 self.open_handles = self.open_handles.saturating_sub(1);
9578 }
9579 pub fn open_handles(&self) -> usize {
9580 self.open_handles
9581 }
9582 /// Pop the earliest timer whose real deadline is at or before `now` (I/O
9583 /// path). Ties break by `seq`.
9584 fn pop_due_timer(&mut self, now: Instant) -> Option<Timer> {
9585 let idx = self
9586 .macrotasks
9587 .iter()
9588 .enumerate()
9589 .filter(|(_, t)| !t.cancelled && t.deadline <= now)
9590 .min_by(|(_, a), (_, b)| a.deadline.cmp(&b.deadline).then(a.seq.cmp(&b.seq)))
9591 .map(|(i, _)| i);
9592 idx.map(|i| self.macrotasks.remove(i))
9593 }
9594 /// Time until the earliest pending timer's deadline (I/O path blocking bound),
9595 /// or `None` if no timers are pending. Clamped to `0` for already-due timers.
9596 fn next_timer_timeout(&self, now: Instant) -> Option<Duration> {
9597 self.macrotasks
9598 .iter()
9599 .filter(|t| !t.cancelled)
9600 .map(|t| t.deadline)
9601 .min()
9602 .map(|d| d.saturating_duration_since(now))
9603 }
9604 pub fn cancel_timer(&mut self, id: u64) {
9605 for t in &mut self.macrotasks {
9606 if t.id == id {
9607 t.cancelled = true;
9608 }
9609 }
9610 }
9611 fn pop_next_timer(&mut self) -> Option<Timer> {
9612 // Earliest (delay, seq) fires first — a deterministic virtual clock.
9613 let idx = self
9614 .macrotasks
9615 .iter()
9616 .enumerate()
9617 .filter(|(_, t)| !t.cancelled)
9618 .min_by(|(_, a), (_, b)| {
9619 a.delay
9620 .partial_cmp(&b.delay)
9621 .unwrap_or(std::cmp::Ordering::Equal)
9622 .then(a.seq.cmp(&b.seq))
9623 })
9624 .map(|(i, _)| i);
9625 idx.map(|i| self.macrotasks.remove(i))
9626 }
9627 fn next_microtask(&mut self) -> Option<Task> {
9628 // Node's `processTicksAndRejections` runs in ROUNDS: drain the nextTick
9629 // queue, then drain the microtask queue in full, then repeat if the
9630 // microtasks queued more ticks. A tick queued from INSIDE a microtask
9631 // therefore waits for the rest of that microtask queue.
9632 //
9633 // Preferring ticks on every step interleaved the two, so
9634 // `Promise.resolve().then(() => process.nextTick(f))` ran `f` before the
9635 // promise callbacks queued behind it — the one ordering difference a
9636 // library scheduling work from a `.then` can actually observe.
9637 if !self.draining_micro {
9638 if let Some(t) = self.nextticks.pop_front() {
9639 return Some(t);
9640 }
9641 }
9642 if let Some(t) = self.microtasks.pop_front() {
9643 // Stay in the microtask phase until this queue is exhausted.
9644 self.draining_micro = !self.microtasks.is_empty();
9645 return Some(t);
9646 }
9647 self.draining_micro = false;
9648 self.nextticks.pop_front()
9649 }
9650 fn has_microtasks(&self) -> bool {
9651 !self.nextticks.is_empty() || !self.microtasks.is_empty()
9652 }
9653 /// Whether any pending timer is *referenced* — the timer half of Node's
9654 /// handle count. Only these keep the loop alive; unref'd timers still fire
9655 /// while something else holds the loop open, but never hold it themselves.
9656 fn has_refed_macrotasks(&self) -> bool {
9657 self.macrotasks.iter().any(|t| !t.cancelled && t.refed)
9658 }
9659 /// Whether any pending timer repeats. A repeating timer cannot run on the
9660 /// virtual clock: virtual time never advances, so the interval would re-arm
9661 /// at the same instant forever, spinning a core and starving every
9662 /// longer-delay timer behind it. Its presence forces the real clock.
9663 fn has_pending_interval(&self) -> bool {
9664 self.macrotasks
9665 .iter()
9666 .any(|t| !t.cancelled && t.interval.is_some())
9667 }
9668}
9669
9670/// Drive the event loop to quiescence.
9671///
9672/// **Liveness** is Node's handle count: the loop runs while a microtask is
9673/// pending, an open handle is registered (a listening server, a live socket, an
9674/// in-flight async op), or a *referenced* timer is still pending. That last term
9675/// is what makes `setInterval(fn, 1000)` hold the process open forever, as it
9676/// does in Node — the interval re-arms itself, so a ref'd timer is always
9677/// pending and the loop never reaches its exit condition.
9678///
9679/// Two **clock regimes**, selected per iteration:
9680///
9681/// - **Virtual clock** (no open handles and no repeating timer): the original
9682/// deterministic path — fire the earliest `(delay, seq)` timer immediately, no
9683/// real waiting. Parity output and test speed for ordinary `setTimeout`
9684/// scripts are unchanged.
9685/// - **Real clock** (an open handle, or any pending interval): fire every timer
9686/// whose wall-clock deadline has passed, then BLOCK on the I/O channel
9687/// (`recv_timeout` bounded by the next deadline, or unbounded `recv` if no
9688/// timers) and run the received `IoTask` on the main thread. The host keeps
9689/// its own `Sender`, so `recv` never disconnects while the process should stay
9690/// alive.
9691///
9692/// A repeating timer *must* take this path: virtual time never advances, so an
9693/// interval on the virtual clock would re-fire at the same instant forever,
9694/// spinning a core and starving every longer-delay timer behind it.
9695///
9696/// Errors thrown by a task/timer/I/O dispatch abort the loop (uncaught → surfaced).
9697pub fn run_event_loop() -> Result<(), String> {
9698 // Own the receiver for the loop's duration (blocking `recv` cannot hold a
9699 // host borrow); restore it afterward so a re-entrant run reuses the channel.
9700 let rx = with_host(|h| h.io_rx.take());
9701 let result = drive_event_loop(rx.as_ref());
9702 with_host(|h| h.io_rx = rx);
9703 result
9704}
9705
9706fn drive_event_loop(rx: Option<&Receiver<IoTask>>) -> Result<(), String> {
9707 loop {
9708 // 1) Exhaust the microtask queue (nextTick before promise reactions),
9709 // then report anything that rejected with nobody watching.
9710 while let Some(task) = with_host(|h| h.next_microtask()) {
9711 task.run()?;
9712 }
9713 check_unhandled_rejections()?;
9714
9715 // 2) Liveness (Node's handle count). Nothing referenced left to do ⇒ the
9716 // process exits, dropping any unref'd timers still pending — which is
9717 // why `setTimeout(fn, 1000).unref()` never fires, while an unref'd
9718 // timer behind a ref'd one does.
9719 let alive =
9720 with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
9721 if !alive {
9722 break;
9723 }
9724
9725 // 3) Pick the clock regime for this turn.
9726 let virtual_clock = with_host(|h| h.open_handles() == 0 && !h.has_pending_interval());
9727 if virtual_clock {
9728 // ── virtual-clock regime (unchanged for one-shot timers) ─────────
9729 match with_host(|h| h.pop_next_timer()) {
9730 Some(t) => fire_timer(t)?,
9731 // Unreachable while `alive` holds (a ref'd timer must exist),
9732 // but exiting is the safe reading of "nothing left to run".
9733 None => break,
9734 }
9735 continue;
9736 }
9737
9738 // ── real-clock / blocking-I/O regime ─────────────────────────────────
9739 let now = Instant::now();
9740 if let Some(t) = with_host(|h| h.pop_due_timer(now)) {
9741 fire_timer(t)?;
9742 continue; // re-drain microtasks, re-check deadlines
9743 }
9744 // Nothing due and no pending microtasks: block for the next I/O event,
9745 // bounded by the soonest timer deadline so due timers still fire on time.
9746 let rx = rx.expect("blocking-I/O regime requires the I/O receiver");
9747 let timeout = with_host(|h| h.next_timer_timeout(now));
9748 let recv = match timeout {
9749 Some(d) => rx.recv_timeout(d),
9750 None => rx
9751 .recv()
9752 .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected),
9753 };
9754 match recv {
9755 Ok(task) => task()?,
9756 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} // a timer is now due
9757 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, // no senders left
9758 }
9759 }
9760 Ok(())
9761}
9762
9763/// Run one due timer's callback, first re-arming it if it repeats.
9764///
9765/// The re-arm happens BEFORE the callback runs so that a `clearInterval(t)`
9766/// issued from inside that callback cancels the next occurrence. Re-arming
9767/// afterwards would leave the interval absent from the queue for the duration of
9768/// its own callback, so the `clear` would match nothing and the freshly pushed
9769/// entry would resurrect an interval the program had just stopped.
9770fn fire_timer(t: Timer) -> Result<(), String> {
9771 if let Some(period) = t.interval {
9772 with_host(|h| h.rearm_timer(&t, period));
9773 }
9774 invoke(&t.callback, t.args, None)?;
9775 Ok(())
9776}
9777
9778// ── async functions & promise resolution (native) ────────────────────────────
9779
9780/// Drive a freshly-built async coroutine and return its result promise.
9781fn run_async(gen: Value) -> Value {
9782 let result = with_host(|h| h.new_promise());
9783 let rid = with_host(|h| h.promise_id(&result).unwrap());
9784 drive_async(gen, rid, Value::Undef);
9785 result
9786}
9787
9788/// Resume an async coroutine one step, wiring `await` continuations to promise
9789/// settlement.
9790fn drive_async(gen: Value, rid: u32, send: Value) {
9791 match gen_resume(&gen, send) {
9792 Ok(GenStep::Yield(awaited)) => {
9793 let ap = promise_of(&awaited);
9794 let aid = with_host(|h| h.promise_id(&ap).unwrap());
9795 let gen2 = gen.clone();
9796 subscribe_native(
9797 aid,
9798 Box::new(move |state, val| {
9799 // Resume the coroutine with a `[tag, value]` packet the AWAIT
9800 // op unwraps (tag 1 ⇒ the awaited promise rejected → throw).
9801 let tag = if state == PromiseState::Rejected {
9802 1.0
9803 } else {
9804 0.0
9805 };
9806 let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9807 drive_async(gen2, rid, packet);
9808 Ok(())
9809 }),
9810 );
9811 }
9812 Ok(GenStep::Done(v)) => resolve_promise_val(rid, v),
9813 Err(e) => {
9814 let ev = take_exc_or_error(&e);
9815 reject_promise_val(rid, ev);
9816 }
9817 }
9818}
9819
9820/// The AWAIT op body (runs inside the async coroutine): suspend, yielding the
9821/// awaited value; on resume, unwrap the settlement packet (throwing on reject).
9822pub fn await_value(awaited: Value) -> Result<Value, String> {
9823 // Inside an `async function*`, `await` and `yield` share one coroutine
9824 // yielder, so an awaited value has to be tagged or the driver would hand it
9825 // to the consumer as if the body had yielded it.
9826 let awaited = match CUR_GEN.with(|c| c.get()) {
9827 Some(id) if with_host(|h| h.generators[id as usize].async_gen) => with_host(|h| {
9828 let mut m = IndexMap::new();
9829 m.insert(AWAIT_MARKER.to_string(), awaited);
9830 h.new_object(m)
9831 }),
9832 _ => awaited,
9833 };
9834 let packet = gen_yield(awaited)?;
9835 let items = with_host(|h| h.iter_vec(&packet)).unwrap_or_default();
9836 let tag = items
9837 .first()
9838 .map(|v| with_host(|h| h.to_number(v)))
9839 .unwrap_or(0.0);
9840 let val = items.get(1).cloned().unwrap_or(Value::Undef);
9841 if tag == 1.0 {
9842 with_host(|h| h.exc = Some(val.clone()));
9843 Err(with_host(|h| crate::builtins::error_string(h, &val)))
9844 } else {
9845 Ok(val)
9846 }
9847}
9848
9849/// Hidden key marking an `await` suspension inside an async generator.
9850const AWAIT_MARKER: &str = "@@await";
9851
9852/// The operand of an `await` suspension, or `None` for a real `yield`.
9853fn await_marker(v: &Value) -> Option<Value> {
9854 with_host(|h| match h.get(v) {
9855 Some(JsObj::Object(props)) if props.len() == 1 => props.get(AWAIT_MARKER).cloned(),
9856 _ => None,
9857 })
9858}
9859
9860/// `AsyncGeneratorEnqueue` — queue one request against an `async function*` and
9861/// hand back the promise its `{value, done}` record (or rejection) will settle.
9862///
9863/// All three of `.next`, `.return` and `.throw` come through here, so a request
9864/// never resumes the body while an earlier one is still suspended on an
9865/// internal `await`.
9866pub fn async_gen_enqueue(gen: &Value, req: GenReq) -> Value {
9867 let step = with_host(|h| h.new_promise());
9868 let sid = with_host(|h| h.promise_id(&step).unwrap());
9869 let id = match with_host(|h| match h.get(gen) {
9870 Some(JsObj::Generator { id }) => Some(*id),
9871 _ => None,
9872 }) {
9873 Some(id) => id,
9874 None => return step,
9875 };
9876 with_host(|h| h.generators[id as usize].queue.push_back((req, sid)));
9877 pump_async_gen(gen.clone(), id);
9878 step
9879}
9880
9881/// One `.next(v)` of an `async function*`.
9882pub fn async_gen_step(gen: &Value, send: Value) -> Value {
9883 async_gen_enqueue(gen, GenReq::Next(send))
9884}
9885
9886/// `AsyncGeneratorResumeNext`: start the oldest queued request, unless one is
9887/// already in flight (the body may only be resumed by one request at a time).
9888fn pump_async_gen(gen: Value, id: u32) {
9889 if with_host(|h| h.generators[id as usize].running) {
9890 return;
9891 }
9892 let Some((req, sid)) = with_host(|h| h.generators[id as usize].queue.pop_front()) else {
9893 return;
9894 };
9895 with_host(|h| h.generators[id as usize].running = true);
9896 start_async_gen_req(gen, sid, req);
9897}
9898
9899/// Begin one queued request: resume the body with the completion it carries,
9900/// then hand the outcome to the shared continuation.
9901///
9902/// A RETURN completion always Awaits its value before the body sees it — via
9903/// `AsyncGeneratorUnwrapYieldResumption` (ECMA-262 27.6.3.7) when the generator
9904/// is suspended at a `yield`, and via `AsyncGeneratorAwaitReturn` (27.6.3.9)
9905/// when it is not yet started or already completed. So a `.return()` settles one
9906/// microtask after a `.next()` or `.throw()` issued in its place would, and the
9907/// `finally` it unwinds through runs a tick later too. Skipping that tick lets a
9908/// `.return()` overtake the reactions of the `.next()` it followed.
9909fn start_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9910 if matches!(req, GenReq::Return(_)) {
9911 with_host(|h| {
9912 h.queue_micro_native(Box::new(move || {
9913 resume_async_gen_req(gen, sid, req);
9914 Ok(())
9915 }))
9916 });
9917 return;
9918 }
9919 resume_async_gen_req(gen, sid, req);
9920}
9921
9922/// Deliver a queued completion to the body and settle its step promise.
9923fn resume_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9924 let step = match req {
9925 GenReq::Next(v) => gen_resume(&gen, v),
9926 GenReq::Return(v) => gen_return(&gen, v),
9927 GenReq::Throw(e) => gen_throw(&gen, e),
9928 };
9929 settle_async_gen_step(gen, sid, step);
9930}
9931
9932/// One request has settled: release the body and start the next queued request.
9933fn finish_async_gen_step(gen: Value, id: u32) {
9934 with_host(|h| h.generators[id as usize].running = false);
9935 pump_async_gen(gen, id);
9936}
9937
9938/// Whether `v` is an `async function*` object (its `.next()` yields promises).
9939pub fn is_async_generator(v: &Value) -> bool {
9940 let id = match with_host(|h| match h.get(v) {
9941 Some(JsObj::Generator { id }) => Some(*id),
9942 _ => None,
9943 }) {
9944 Some(id) => id,
9945 None => return false,
9946 };
9947 with_host(|h| h.generators[id as usize].async_gen)
9948}
9949
9950/// A `{ value, done }` iterator-result object.
9951fn iter_record(value: Value, done: bool) -> Value {
9952 with_host(|h| {
9953 let mut m = IndexMap::new();
9954 m.insert("value".to_string(), value);
9955 m.insert("done".to_string(), Value::Bool(done));
9956 h.new_object(m)
9957 })
9958}
9959
9960/// Resume a request that was suspended on an internal `await` (always a normal
9961/// completion — the awaited promise's outcome rides in `packet`).
9962fn drive_async_gen(gen: Value, sid: u32, packet: Value) {
9963 let step = gen_resume(&gen, packet);
9964 settle_async_gen_step(gen, sid, step);
9965}
9966
9967/// Turn one body resumption into a settled step promise: transparently re-drive
9968/// internal `await` suspensions, and settle on the first REAL yield or on the
9969/// body's completion. Shared by the initial resume of a queued request and by
9970/// every await-resumption of it.
9971fn settle_async_gen_step(gen: Value, sid: u32, step: Result<GenStep, String>) {
9972 let id = match with_host(|h| match h.get(&gen) {
9973 Some(JsObj::Generator { id }) => Some(*id),
9974 _ => None,
9975 }) {
9976 Some(id) => id,
9977 None => return,
9978 };
9979 match step {
9980 Ok(GenStep::Yield(v)) => match await_marker(&v) {
9981 Some(awaited) => {
9982 // An internal `await`: settle it, then resume the body. The
9983 // request stays in flight across the suspension.
9984 let ap = promise_of(&awaited);
9985 let aid = with_host(|h| h.promise_id(&ap).unwrap());
9986 subscribe_native(
9987 aid,
9988 Box::new(move |state, val| {
9989 let tag = if state == PromiseState::Rejected {
9990 1.0
9991 } else {
9992 0.0
9993 };
9994 let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9995 drive_async_gen(gen.clone(), sid, packet);
9996 Ok(())
9997 }),
9998 );
9999 }
10000 // ECMA-262 27.6.3.8 AsyncGeneratorYield step 5: the yielded value is
10001 // AWAITED before the step promise settles, so `yield somePromise`
10002 // hands the consumer the RESOLVED value (and costs its microtask).
10003 None => {
10004 let yp = promise_of(&v);
10005 let yid = with_host(|h| h.promise_id(&yp).unwrap());
10006 subscribe_native(
10007 yid,
10008 Box::new(move |state, val| {
10009 if state == PromiseState::Rejected {
10010 reject_promise_val(sid, val);
10011 } else {
10012 resolve_promise_val(sid, iter_record(val, false));
10013 }
10014 finish_async_gen_step(gen.clone(), id);
10015 Ok(())
10016 }),
10017 );
10018 }
10019 },
10020 Ok(GenStep::Done(v)) => {
10021 resolve_promise_val(sid, iter_record(v, true));
10022 finish_async_gen_step(gen, id);
10023 }
10024 Err(e) => {
10025 let ev = take_exc_or_error(&e);
10026 reject_promise_val(sid, ev);
10027 finish_async_gen_step(gen, id);
10028 }
10029 }
10030}
10031
10032/// A promise for `v`: `v` itself if it is already a promise, else a promise
10033/// resolved with `v`.
10034pub fn promise_of(v: &Value) -> Value {
10035 if with_host(|h| h.promise_id(v)).is_some() {
10036 return v.clone();
10037 }
10038 let p = with_host(|h| h.new_promise());
10039 let id = with_host(|h| h.promise_id(&p).unwrap());
10040 resolve_promise_val(id, v.clone());
10041 p
10042}
10043
10044/// Register a native reaction on promise `id` (schedules immediately if already
10045/// settled).
10046pub fn subscribe_native(id: u32, f: Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>) {
10047 // A native continuation (`await`, promise adoption, `for await`) observes a
10048 // rejection exactly as a `.catch` does, so it is not "unhandled".
10049 with_host(|h| h.promise_mark_handled(id));
10050 let state = with_host(|h| h.promise_state(id));
10051 if state == PromiseState::Pending {
10052 with_host(|h| h.add_reaction(id, PromiseReaction::Native(f)));
10053 } else {
10054 let val = with_host(|h| h.promise_value(id));
10055 with_host(|h| h.queue_micro_native(Box::new(move || f(state, val))));
10056 }
10057}
10058
10059/// The Promise "resolve" operation: adopt `value`'s state if it is a promise,
10060/// else fulfill with it.
10061pub fn resolve_promise_val(id: u32, value: Value) {
10062 if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
10063 return;
10064 }
10065 if let Some(vid) = with_host(|h| h.promise_id(&value)) {
10066 if vid == id {
10067 // Resolving a promise with itself → reject with a TypeError.
10068 let e = with_host(|h| {
10069 crate::builtins::synth_error(h, "TypeError: Chaining cycle detected")
10070 });
10071 reject_promise_val(id, e);
10072 return;
10073 }
10074 // A native promise is still a thenable, so the spec routes it through
10075 // `NewPromiseResolveThenableJob` too — one microtask before the adoption
10076 // is even registered. (`await` does NOT pay this: V8's await optimization
10077 // subscribes to a native promise directly, which `await_value` mirrors.)
10078 with_host(|h| {
10079 h.queue_micro_native(Box::new(move || {
10080 subscribe_native(
10081 vid,
10082 Box::new(move |state, val| {
10083 with_host(|h| h.settle_promise(id, state, val.clone()));
10084 schedule_reactions(id);
10085 Ok(())
10086 }),
10087 );
10088 Ok(())
10089 }))
10090 });
10091 return;
10092 }
10093 // ECMA-262 27.2.1.3.2: any OBJECT carrying a callable `then` is assimilated
10094 // through a dedicated job — the promise adopts what `then` reports, it is
10095 // never fulfilled WITH the thenable itself.
10096 if let Some(then) = thenable_then(&value) {
10097 with_host(|h| {
10098 h.queue_micro_native(Box::new(move || resolve_thenable_job(id, value, then)))
10099 });
10100 return;
10101 }
10102 with_host(|h| h.settle_promise(id, PromiseState::Fulfilled, value));
10103 schedule_reactions(id);
10104}
10105
10106/// `value.then` if `value` is an object with a callable `then` — the test that
10107/// makes a value a *thenable*. Primitives (and objects without one) are `None`.
10108fn thenable_then(value: &Value) -> Option<Value> {
10109 // A PROXY is not a plain object and supplies `then` through its `get` trap,
10110 // so both tests below missed it: `Promise.resolve(proxyThenable)` fulfilled
10111 // WITH the proxy instead of adopting it.
10112 if with_host(|h| h.kind_of(value)) == Some(ObjKind::Proxy) {
10113 return protocol_lookup(value, "then")
10114 .ok()
10115 .flatten()
10116 .filter(|f| with_host(|h| is_callable(h, f)));
10117 }
10118 if !with_host(|h| matches!(h.get(value), Some(JsObj::Object(_)))) {
10119 return None;
10120 }
10121 let then = with_host(|h| lookup_chain(h, value, "then"))?;
10122 with_host(|h| is_callable(h, &then)).then_some(then)
10123}
10124
10125/// `NewPromiseResolveThenableJob`: hand the thenable this promise's own resolve /
10126/// reject continuations and let it settle us. A throw out of `then` rejects.
10127fn resolve_thenable_job(id: u32, thenable: Value, then: Value) -> Result<(), String> {
10128 let res = with_host(|h| h.alloc(JsObj::Builtin(format!("@@presolve:{id}"))));
10129 let rej = with_host(|h| h.alloc(JsObj::Builtin(format!("@@preject:{id}"))));
10130 if let Err(e) = invoke(&then, vec![res, rej], Some(thenable)) {
10131 let ev = take_exc_or_error(&e);
10132 reject_promise_val(id, ev);
10133 }
10134 Ok(())
10135}
10136
10137pub fn reject_promise_val(id: u32, value: Value) {
10138 if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
10139 return;
10140 }
10141 with_host(|h| {
10142 h.settle_promise(id, PromiseState::Rejected, value);
10143 h.pending_rejections.push(id);
10144 });
10145 schedule_reactions(id);
10146}
10147
10148/// Report every promise that settled rejected since the last checkpoint and
10149/// still has no handler. Node's default is `--unhandled-rejections=throw`: the
10150/// rejection becomes an uncaught exception (stderr + exit 1) unless a
10151/// `process.on('unhandledRejection')` listener takes it.
10152fn check_unhandled_rejections() -> Result<(), String> {
10153 loop {
10154 let ids: Vec<u32> = with_host(|h| std::mem::take(&mut h.pending_rejections));
10155 if ids.is_empty() {
10156 return Ok(());
10157 }
10158 for id in ids {
10159 let unhandled = with_host(|h| {
10160 h.promise_state(id) == PromiseState::Rejected && !h.promises[id as usize].handled
10161 });
10162 if !unhandled {
10163 continue;
10164 }
10165 // Report each promise at most once, however many checkpoints pass.
10166 with_host(|h| h.promise_mark_handled(id));
10167 let val = with_host(|h| h.promise_value(id));
10168 let listeners = with_host(|h| h.take_process_listeners("unhandledRejection"));
10169 if listeners.is_empty() {
10170 let msg = with_host(|h| crate::builtins::error_string(h, &val));
10171 with_host(|h| h.exc = Some(val));
10172 return Err(msg);
10173 }
10174 let promise = with_host(|h| h.alloc(JsObj::Promise { id }));
10175 for f in listeners {
10176 invoke(&f, vec![val.clone(), promise.clone()], None)?;
10177 }
10178 }
10179 }
10180}
10181
10182/// Drain a settled promise's reactions into microtasks.
10183fn schedule_reactions(id: u32) {
10184 let reactions = with_host(|h| h.take_reactions(id));
10185 let state = with_host(|h| h.promise_state(id));
10186 let value = with_host(|h| h.promise_value(id));
10187 for r in reactions {
10188 let value = value.clone();
10189 match r {
10190 PromiseReaction::Native(f) => {
10191 with_host(|h| h.queue_micro_native(Box::new(move || f(state, value))));
10192 }
10193 PromiseReaction::Js {
10194 on_ful,
10195 on_rej,
10196 result,
10197 } => {
10198 with_host(|h| {
10199 h.queue_micro_native(Box::new(move || {
10200 run_js_reaction(state, value, on_ful, on_rej, result)
10201 }))
10202 });
10203 }
10204 }
10205 }
10206}
10207
10208/// Run a `.then` reaction: call the appropriate handler and settle the result
10209/// promise with its outcome (or pass through if there is no handler).
10210fn run_js_reaction(
10211 state: PromiseState,
10212 value: Value,
10213 on_ful: Value,
10214 on_rej: Value,
10215 result: Value,
10216) -> Result<(), String> {
10217 let rid = match with_host(|h| h.promise_id(&result)) {
10218 Some(i) => i,
10219 None => return Ok(()),
10220 };
10221 let handler = if state == PromiseState::Rejected {
10222 on_rej
10223 } else {
10224 on_ful
10225 };
10226 if with_host(|h| is_callable(h, &handler)) {
10227 match invoke(&handler, vec![value], None) {
10228 Ok(r) => resolve_promise_val(rid, r),
10229 Err(e) => reject_promise_val(rid, take_exc_or_error(&e)),
10230 }
10231 } else if state == PromiseState::Rejected {
10232 reject_promise_val(rid, value);
10233 } else {
10234 resolve_promise_val(rid, value);
10235 }
10236 Ok(())
10237}
10238
10239/// The JS value of a just-caught error: the live `exc` (a real thrown value) or a
10240/// synthesized `Error` from the internal message.
10241pub fn take_exc_or_error(e: &str) -> Value {
10242 with_host(|h| {
10243 h.error.take();
10244 h.exc
10245 .take()
10246 .unwrap_or_else(|| crate::builtins::synth_error(h, e))
10247 })
10248}
10249
10250/// Register a user `.then` reaction (JS handlers + result promise).
10251pub fn promise_then(p: &Value, on_ful: Value, on_rej: Value) -> Value {
10252 let id = match with_host(|h| h.promise_id(p)) {
10253 Some(i) => i,
10254 None => return Value::Undef,
10255 };
10256 with_host(|h| h.promise_mark_handled(id));
10257 // The result is built with the RECEIVER's species, so a subclass promise
10258 // stays a subclass promise through a `.then` chain.
10259 let result = match crate::builtins::promise_species_from(p) {
10260 Ok(Some(sp)) => sp,
10261 _ => with_host(|h| h.new_promise()),
10262 };
10263 let reaction = PromiseReaction::Js {
10264 on_ful,
10265 on_rej,
10266 result: result.clone(),
10267 };
10268 let state = with_host(|h| h.promise_state(id));
10269 if state == PromiseState::Pending {
10270 with_host(|h| h.add_reaction(id, reaction));
10271 } else {
10272 let value = with_host(|h| h.promise_value(id));
10273 if let PromiseReaction::Js {
10274 on_ful,
10275 on_rej,
10276 result,
10277 } = reaction
10278 {
10279 with_host(|h| {
10280 h.queue_micro_native(Box::new(move || {
10281 run_js_reaction(state, value, on_ful, on_rej, result)
10282 }))
10283 });
10284 }
10285 }
10286 result
10287}
10288
10289/// The ReferenceError for touching `this` in a derived constructor before
10290/// `super()` — or returning from one without calling it.
10291pub fn this_before_super_error() -> String {
10292 "ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor".to_string()
10293}
10294
10295/// The intrinsic a builtin name denotes. Two property paths that the spec
10296/// defines as the SAME function object compare `===`: `Number.parseInt` is
10297/// `%parseInt%` (21.1.2.13) and `Number.parseFloat` is `%parseFloat%`
10298/// (21.1.2.12), so `Number.parseInt === parseInt` is `true`.
10299fn builtin_identity(name: &str) -> &str {
10300 match name {
10301 "Number.parseInt" => "parseInt",
10302 "Number.parseFloat" => "parseFloat",
10303 _ => name,
10304 }
10305}