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 // `callable_name`, not the class record's own name: an anonymous
1385 // class expression is named by inference from its binding
1386 // (`const X = class {}`), which lands as an own `name` property,
1387 // and node prints `new X()` as `X {}`.
1388 if let Some(JsObj::Class(_)) = self.get(&c) {
1389 return self.callable_name(&c);
1390 }
1391 }
1392 // A `function F(){}` constructor is not a `class`, so it has no
1393 // `proto_class` entry. V8's `getConstructorName` walks the prototype
1394 // chain for an own `constructor` that is a named function — which is
1395 // what makes `console.log(new F())` print `F { y: 2 }`.
1396 let mut cur = self.proto_of(obj);
1397 while let Some(p) = cur {
1398 let ctor = match self.get(&p) {
1399 Some(JsObj::Object(props)) => props.get("constructor").cloned(),
1400 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => self.fn_prop(&p, "constructor"),
1401 _ => None,
1402 };
1403 if let Some(f) = ctor {
1404 let n = self.callable_name(&f);
1405 if !n.is_empty() {
1406 return n;
1407 }
1408 }
1409 cur = self.proto_of(&p);
1410 }
1411 String::new()
1412 }
1413
1414 /// The prefix node's `getPrefix` gives a builtin collection: `Map(2) ` for
1415 /// a plain one, `M2(2) [Map] ` for an instance of a subclass (constructor
1416 /// name, then the builtin tag in brackets). `size` is `None` for a kind
1417 /// with no count (`Promise`).
1418 fn builtin_prefix(&self, v: &Value, tag: &str, size: Option<usize>) -> String {
1419 let ctor = self.ctor_name(v);
1420 let count = size.map(|n| format!("({n})")).unwrap_or_default();
1421 if ctor.is_empty() || ctor == tag {
1422 format!("{tag}{count} ")
1423 } else {
1424 format!("{ctor}{count} [{tag}] ")
1425 }
1426 }
1427
1428 /// The `[Name]` stub a builtin collection collapses to past the depth
1429 /// limit: `[Map]`, or `[M2 [Map]]` for a subclass instance.
1430 fn builtin_depth_stub(&self, v: &Value, tag: &str) -> String {
1431 let ctor = self.ctor_name(v);
1432 if ctor.is_empty() || ctor == tag {
1433 format!("[{tag}]")
1434 } else {
1435 format!("[{ctor} [{tag}]]")
1436 }
1437 }
1438
1439 /// Whether a callable owns a `prototype` property. `MakeConstructor`
1440 /// (10.2.5) runs for an ordinary function definition and for every
1441 /// generator; an arrow, a `MethodDefinition`, an async function and a bound
1442 /// function are not constructors and own none.
1443 pub fn owns_prototype(&self, v: &Value) -> bool {
1444 match self.get(v) {
1445 Some(JsObj::Class(_)) => true,
1446 Some(JsObj::Func(f)) => match self.funcs.get(f.def_id) {
1447 Some(d) => d.is_generator || !(d.is_arrow || d.is_async || d.is_method),
1448 None => false,
1449 },
1450 _ => false,
1451 }
1452 }
1453
1454 /// A function's own-property table (created on demand).
1455 pub fn fn_prop(&self, v: &Value, name: &str) -> Option<Value> {
1456 if let Value::Obj(i) = v {
1457 self.fn_props.get(i).and_then(|m| m.get(name).cloned())
1458 } else {
1459 None
1460 }
1461 }
1462
1463 /// A class static member, inherited down the constructor chain: a subclass
1464 /// sees its superclass's `static` methods/fields (`Sub.create` → `Base.create`).
1465 pub fn class_static(&self, class_val: &Value, name: &str) -> Option<Value> {
1466 let mut cur = class_val.clone();
1467 loop {
1468 if let Some(v) = self.fn_prop(&cur, name) {
1469 return Some(v);
1470 }
1471 match self.get(&cur) {
1472 Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1473 _ => return None,
1474 }
1475 }
1476 }
1477
1478 /// The first `extends` ancestor that is NOT a user class — the builtin
1479 /// constructor a class chain bottoms out in (`class D extends Array {}` →
1480 /// the `Array` builtin), or `None` for a chain of user classes only.
1481 ///
1482 /// `class_static` walks `ClassVal.parent` and gives up the moment the parent
1483 /// stops being a `Class`, so a static declared by the BUILTIN half of the
1484 /// chain was unreachable: `D.from` read `undefined` where node inherits
1485 /// `Array.from`. Returning the ancestor lets the caller finish the lookup
1486 /// with an ordinary property read, which is what reaches a builtin's
1487 /// statics.
1488 pub fn class_builtin_ancestor(&self, class_val: &Value) -> Option<Value> {
1489 let mut cur = class_val.clone();
1490 loop {
1491 match self.get(&cur) {
1492 Some(JsObj::Class(c)) => cur = c.parent.clone()?,
1493 _ => return Some(cur),
1494 }
1495 }
1496 }
1497 pub fn set_fn_prop(&mut self, v: &Value, name: &str, val: Value) {
1498 if let Value::Obj(i) = v {
1499 self.fn_props
1500 .entry(*i)
1501 .or_default()
1502 .insert(name.to_string(), val);
1503 }
1504 // `name` and `prototype` are own properties of every function/class, but
1505 // never enumerable ones (SetFunctionName 10.2.9, MakeConstructor
1506 // 10.2.5), so `Object.keys(fn)` and `for (k in fn)` report only what a
1507 // script assigned. An ARRAY receiver reaching the same side table has no
1508 // such exotic keys — `arr.name = 'x'` is an ordinary enumerable property.
1509 if !matches!(self.kind_of(v), Some(ObjKind::Func) | Some(ObjKind::Class)) {
1510 return;
1511 }
1512 let attrs = match name {
1513 "name" => PropAttrs {
1514 writable: false,
1515 enumerable: false,
1516 configurable: true,
1517 },
1518 "prototype" => PropAttrs {
1519 writable: true,
1520 enumerable: false,
1521 configurable: false,
1522 },
1523 _ => return,
1524 };
1525 self.set_prop_attrs(v, name, attrs);
1526 }
1527 /// A user-assigned static on a builtin namespace (`Error.prepareStackTrace`).
1528 pub fn builtin_static(&self, ns: &str, name: &str) -> Option<Value> {
1529 self.builtin_statics
1530 .get(ns)
1531 .and_then(|m| m.get(name).cloned())
1532 }
1533 /// Assign a static on a builtin namespace (persists across fresh `Builtin`
1534 /// handles for the same namespace).
1535 pub fn set_builtin_static(&mut self, ns: &str, name: &str, val: Value) {
1536 self.builtin_statics
1537 .entry(ns.to_string())
1538 .or_default()
1539 .insert(name.to_string(), val);
1540 }
1541 /// `delete <ns>.<name>` for a script-assigned static. Reports whether the
1542 /// key was there — without this, `delete Array.prototype.patch` answered
1543 /// true and left the entry in place, so the patch outlived its own removal.
1544 pub fn remove_builtin_static(&mut self, ns: &str, name: &str) -> bool {
1545 self.builtin_statics
1546 .get_mut(ns)
1547 .is_some_and(|m| m.shift_remove(name).is_some())
1548 }
1549 /// Every namespace a script has assigned a static onto, with that
1550 /// namespace's assigned keys — the source of the user-added half of
1551 /// `Object.getOwnPropertyNames(Array.prototype)`.
1552 pub fn builtin_static_keys(&self, ns: &str) -> Vec<String> {
1553 self.builtin_statics
1554 .get(ns)
1555 .map(|m| m.keys().cloned().collect())
1556 .unwrap_or_default()
1557 }
1558 /// Drop an own property from the side table (`delete arr.foo`,
1559 /// `delete fn.tag`). Reports whether the key was there.
1560 pub fn remove_fn_prop(&mut self, v: &Value, name: &str) -> bool {
1561 match v {
1562 Value::Obj(i) => self
1563 .fn_props
1564 .get_mut(i)
1565 .map(|m| m.shift_remove(name).is_some())
1566 .unwrap_or(false),
1567 _ => false,
1568 }
1569 }
1570 pub fn fn_prop_keys(&self, v: &Value) -> Vec<String> {
1571 if let Value::Obj(i) = v {
1572 self.fn_props
1573 .get(i)
1574 .map(|m| m.keys().cloned().collect())
1575 .unwrap_or_default()
1576 } else {
1577 Vec::new()
1578 }
1579 }
1580
1581 /// Install an accessor `(get, set)` for `key` on the object `owner`.
1582 pub fn set_accessor(
1583 &mut self,
1584 owner: &Value,
1585 key: &str,
1586 get: Option<Value>,
1587 set: Option<Value>,
1588 ) {
1589 if let Value::Obj(i) = owner {
1590 // Accessors live in their own table, but JS reports own keys in a
1591 // single insertion order across data AND accessor properties. Drop an
1592 // ordering marker into the property map so
1593 // `{ a: 1, get b() {}, c: 3 }` enumerates a, b, c — not a, c, b.
1594 // The marker is `@@`-prefixed, so it is invisible to every reader.
1595 let marker = format!("{ORD_MARKER}{key}");
1596 match self.get_mut(owner) {
1597 Some(JsObj::Object(props)) => {
1598 if !props.contains_key(key) && !props.contains_key(&marker) {
1599 props.insert(marker, Value::Undef);
1600 }
1601 }
1602 // A function or class keeps its own properties in the fn-prop
1603 // side table, so its ordering marker belongs there. Without it a
1604 // static accessor enumerated AFTER every static field and method
1605 // regardless of where the class body declared it: node reports
1606 // `class A { static s = 2; static get sv(){} static m(){} }` as
1607 // `['sv', 'm', 's']` — the methods and accessors in source order
1608 // first, then the fields — and this reported `['m', 's', 'sv']`.
1609 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1610 let table = self.fn_props.entry(*i).or_default();
1611 if !table.contains_key(key) && !table.contains_key(&marker) {
1612 table.insert(marker, Value::Undef);
1613 }
1614 }
1615 _ => {}
1616 }
1617 let slot = self
1618 .accessors
1619 .entry(*i)
1620 .or_default()
1621 .entry(key.to_string())
1622 .or_insert((None, None));
1623 if get.is_some() {
1624 slot.0 = get;
1625 }
1626 if set.is_some() {
1627 slot.1 = set;
1628 }
1629 }
1630 }
1631 /// The accessor `(get, set)` for `key` directly on `owner` (no chain walk).
1632 /// Drop an own accessor property entirely, marker and all.
1633 ///
1634 /// `delete obj.accessorProp` used to clear only the property map, and an
1635 /// accessor does not live there — so the delete reported success while the
1636 /// getter kept answering and `in` kept reporting the key.
1637 pub fn remove_accessor(&mut self, owner: &Value, key: &str) {
1638 if let Value::Obj(i) = owner {
1639 if let Some(m) = self.accessors.get_mut(i) {
1640 m.shift_remove(key);
1641 }
1642 }
1643 let marker = format!("{ORD_MARKER}{key}");
1644 match self.get_mut(owner) {
1645 Some(JsObj::Object(props)) => {
1646 props.shift_remove(&marker);
1647 }
1648 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
1649 if let Value::Obj(i) = owner {
1650 if let Some(t) = self.fn_props.get_mut(i) {
1651 t.shift_remove(&marker);
1652 }
1653 }
1654 }
1655 _ => {}
1656 }
1657 }
1658
1659 /// Turn an own accessor property into a data property carrying `value`,
1660 /// keeping its place in the own-key order.
1661 ///
1662 /// `set_accessor` records that order with an `@@ord:` marker in the
1663 /// property map rather than a real key, so deleting the accessor and
1664 /// inserting the value would append the key at the end instead. Node
1665 /// reports `{ a: 1, get b() {}, c: 3 }` redefined through
1666 /// `Object.defineProperty(o, 'b', { value })` as `a, b, c`.
1667 pub fn accessor_to_data(&mut self, owner: &Value, key: &str, value: Value) {
1668 if let Value::Obj(i) = owner {
1669 if let Some(m) = self.accessors.get_mut(i) {
1670 m.shift_remove(key);
1671 }
1672 }
1673 let marker = format!("{ORD_MARKER}{key}");
1674 let swap = |map: &mut IndexMap<String, Value>| match map.get_index_of(&marker) {
1675 Some(pos) => {
1676 *map = map
1677 .iter()
1678 .enumerate()
1679 .map(|(n, (k, v))| {
1680 if n == pos {
1681 (key.to_string(), value.clone())
1682 } else {
1683 (k.clone(), v.clone())
1684 }
1685 })
1686 .collect();
1687 }
1688 None => {
1689 map.insert(key.to_string(), value.clone());
1690 }
1691 };
1692 let fn_table = matches!(
1693 self.get(owner),
1694 Some(JsObj::Func(_)) | Some(JsObj::Class(_))
1695 );
1696 if fn_table {
1697 if let Value::Obj(i) = owner {
1698 swap(self.fn_props.entry(*i).or_default());
1699 }
1700 } else if let Some(JsObj::Object(props)) = self.get_mut(owner) {
1701 swap(props);
1702 }
1703 }
1704
1705 /// Move the per-heap-index bookkeeping of `src` onto `dst`.
1706 ///
1707 /// Used when one object becomes another in place (a class extending a
1708 /// builtin exotic). The prototype link is deliberately NOT moved: `dst`
1709 /// already points at the leaf class's prototype, which is the one its
1710 /// methods must resolve through.
1711 pub fn move_index_state(&mut self, src: u32, dst: u32) {
1712 if let Some(holes) = self.array_holes.remove(&src) {
1713 self.array_holes.insert(dst, holes);
1714 }
1715 if let Some(attrs) = self.prop_attrs.remove(&src) {
1716 self.prop_attrs.entry(dst).or_default().extend(attrs);
1717 }
1718 if let Some(props) = self.fn_props.remove(&src) {
1719 self.fn_props.entry(dst).or_default().extend(props);
1720 }
1721 if let Some(acc) = self.accessors.remove(&src) {
1722 self.accessors.entry(dst).or_default().extend(acc);
1723 }
1724 }
1725
1726 pub fn own_accessor(&self, owner: &Value, key: &str) -> Option<(Option<Value>, Option<Value>)> {
1727 if let Value::Obj(i) = owner {
1728 self.accessors.get(i).and_then(|m| m.get(key).cloned())
1729 } else {
1730 None
1731 }
1732 }
1733
1734 /// The own accessor-property keys of `owner`, in installation order.
1735 pub fn own_accessor_keys(&self, owner: &Value) -> Vec<String> {
1736 match owner {
1737 Value::Obj(i) => self
1738 .accessors
1739 .get(i)
1740 .map(|m| m.keys().cloned().collect())
1741 .unwrap_or_default(),
1742 _ => Vec::new(),
1743 }
1744 }
1745
1746 // ── own-property attributes ──────────────────────────────────────────
1747
1748 /// Record non-default attributes for `owner[key]`. Storing the default shape
1749 /// clears the entry so the table only ever holds deviations.
1750 pub fn set_prop_attrs(&mut self, owner: &Value, key: &str, attrs: PropAttrs) {
1751 if let Value::Obj(i) = owner {
1752 if attrs == PropAttrs::default() {
1753 if let Some(m) = self.prop_attrs.get_mut(i) {
1754 m.shift_remove(key);
1755 }
1756 } else {
1757 self.prop_attrs
1758 .entry(*i)
1759 .or_default()
1760 .insert(key.to_string(), attrs);
1761 }
1762 }
1763 }
1764
1765 /// Copy every recorded property attribute from `from` to `to`. A pass that
1766 /// rebuilds an object (`JSON.stringify`'s `toJSON` walk) must carry them
1767 /// across or the copy silently re-exposes non-enumerable slots.
1768 pub fn copy_prop_attrs(&mut self, from: &Value, to: &Value) {
1769 if let (Value::Obj(f), Value::Obj(_)) = (from, to) {
1770 if let Some(m) = self.prop_attrs.get(f).cloned() {
1771 for (k, a) in m {
1772 self.set_prop_attrs(to, &k, a);
1773 }
1774 }
1775 }
1776 }
1777
1778 /// The attributes of own property `owner[key]` (all-true when unrecorded).
1779 pub fn prop_attrs(&self, owner: &Value, key: &str) -> PropAttrs {
1780 // An array's `length` is the array exotic's own property (10.4.2):
1781 // never enumerated and never configurable, and writable until
1782 // `Object.freeze` clears that — which is what stops a `push` from
1783 // extending a frozen array. Reporting it unconditionally writable made
1784 // `Object.isFrozen(Object.freeze([]))` false once the elements started
1785 // being sealed, because `length` was then the one key that never
1786 // followed.
1787 if key == "length" && matches!(self.get(owner), Some(JsObj::Array(_))) {
1788 let writable = match owner {
1789 Value::Obj(i) => self
1790 .prop_attrs
1791 .get(i)
1792 .and_then(|m| m.get(key))
1793 .map(|a| a.writable)
1794 .unwrap_or(true),
1795 _ => true,
1796 };
1797 return PropAttrs {
1798 writable,
1799 enumerable: false,
1800 // An ARGUMENTS object's `length` is an ordinary data property
1801 // (10.4.4.6), so it is configurable where a real array's is
1802 // not. The two share a backing representation here, so the
1803 // exotic's attributes have to be told apart explicitly.
1804 configurable: crate::builtins::is_arguments_h(self, owner),
1805 };
1806 }
1807 match owner {
1808 Value::Obj(i) => self
1809 .prop_attrs
1810 .get(i)
1811 .and_then(|m| m.get(key))
1812 .copied()
1813 .unwrap_or_default(),
1814 _ => PropAttrs::default(),
1815 }
1816 }
1817
1818 /// Whether own property `owner[key]` shows up in `for-in`/`Object.keys`.
1819 /// Internal slots (`@@…`) and private class fields (`#…`) never do.
1820 pub fn is_enumerable(&self, owner: &Value, key: &str) -> bool {
1821 !key.starts_with("@@") && !key.starts_with('#') && self.prop_attrs(owner, key).enumerable
1822 }
1823
1824 /// Mark `owner[key]` non-enumerable, leaving it writable/configurable — the
1825 /// shape of every V8 "hidden but real" own property.
1826 pub fn hide_prop(&mut self, owner: &Value, key: &str) {
1827 self.set_prop_attrs(owner, key, PropAttrs::HIDDEN);
1828 }
1829
1830 /// Whether a plain `owner[key] = v` assignment is allowed to land. A
1831 /// non-writable data property silently ignores the write in sloppy mode,
1832 /// which is the mode every script here runs in; so does adding a *new* key to
1833 /// a non-extensible object.
1834 pub fn can_write_prop(&self, owner: &Value, key: &str) -> bool {
1835 if !self.prop_attrs(owner, key).writable {
1836 return false;
1837 }
1838 // An intrinsic prototype on the chain may define the name NON-WRITABLE,
1839 // and those members own no map entry for the walk below to find:
1840 // `o[Symbol.toStringTag] = 'x'` where `o` inherits from `Map.prototype`
1841 // is refused in node and was creating an own property here, which then
1842 // changed the object's brand.
1843 // The receiver's OWN kind counts too, not only the prototypes an
1844 // explicit link reaches: a plain array inherits `Array.prototype`
1845 // implicitly, with no link for the walk to follow, and
1846 // `a[Symbol.unscopables] = 'x'` is refused there just the same.
1847 //
1848 // Restricted to SYMBOL-keyed members. The string-keyed non-writable
1849 // ones — `Function.prototype.length`/`name`, `String.prototype.length`
1850 // — are also OWN properties of every instance, so the inherited rule
1851 // never decides them; applying it anyway blocked `SetFunctionName`
1852 // itself, and naming the setter in `Object.defineProperty(o, 'v', {set
1853 // (x) {…}})` then threw.
1854 if key.starts_with("@@")
1855 && crate::builtins::own_ctor_name(self, owner)
1856 .into_iter()
1857 .chain(crate::builtins::chain_intrinsic_ctors_h(self, owner))
1858 .any(|c| crate::builtins::is_proto_readonly(c, key))
1859 {
1860 return false;
1861 }
1862 // 10.1.9.2: with no OWN property, the inherited one decides. A
1863 // non-writable data property up the chain blocks the write rather than
1864 // being shadowed — including one on a frozen prototype. Only own
1865 // attributes were consulted, so `Object.create(frozenBase).f = 2`
1866 // quietly created an own property node refuses to create.
1867 //
1868 // An inherited ACCESSOR does not block: its setter runs, and the write
1869 // path checks for one before reaching here.
1870 let has_own = match self.get(owner) {
1871 Some(JsObj::Object(p)) => p.contains_key(key),
1872 _ => true,
1873 };
1874 if !has_own {
1875 let mut cur = self.proto_of(owner);
1876 while let Some(proto) = cur {
1877 if self.own_accessor(&proto, key).is_some() {
1878 break;
1879 }
1880 let present =
1881 matches!(self.get(&proto), Some(JsObj::Object(p)) if p.contains_key(key));
1882 if present {
1883 if !self.prop_attrs(&proto, key).writable {
1884 return false;
1885 }
1886 break;
1887 }
1888 cur = self.proto_of(&proto);
1889 }
1890 }
1891 if self.is_extensible(owner) {
1892 return true;
1893 }
1894 // A non-extensible object refuses a NEW key. Only the plain-object arm
1895 // could name its own keys, so every other shape answered "own" for any
1896 // key at all: `Object.freeze(arr).extra = 1` landed, and so did a write
1897 // to the frozen template object a tagged template hands its tag.
1898 match self.get(owner) {
1899 Some(JsObj::Object(p)) => p.contains_key(key),
1900 Some(JsObj::Array(items)) => {
1901 key == "length"
1902 || key
1903 .parse::<usize>()
1904 .map(|i| i < items.len())
1905 .unwrap_or(false)
1906 || self.fn_prop(owner, key).is_some()
1907 }
1908 // A RegExp's `lastIndex` is an own property, so a merely
1909 // NON-EXTENSIBLE regexp still accepts a write to it.
1910 Some(JsObj::RegExp(_)) => key == "lastIndex" || self.fn_prop(owner, key).is_some(),
1911 // Every other shape keeps its own properties in the fn-prop side
1912 // table (a function's statics, a Map's assigned properties), so
1913 // "does it already own this key" is that table's question. Answering
1914 // a blanket `true` let a NEW key land on a frozen function and a
1915 // frozen Map.
1916 _ => self.fn_prop(owner, key).is_some(),
1917 }
1918 }
1919
1920 /// Mark `v` closed to new properties (`Object.preventExtensions`).
1921 pub fn prevent_extensions(&mut self, v: &Value) {
1922 if let Value::Obj(i) = v {
1923 self.non_extensible.insert(*i);
1924 }
1925 }
1926
1927 pub fn is_extensible(&self, v: &Value) -> bool {
1928 !matches!(v, Value::Obj(i) if self.non_extensible.contains(i))
1929 }
1930
1931 /// Apply `Object.seal` (`freeze == false`) or `Object.freeze` (`true`): close
1932 /// the object and strip `configurable` — and, when freezing, `writable` —
1933 /// from every own property, data and accessor alike.
1934 pub fn seal_object(&mut self, v: &Value, freeze: bool) {
1935 self.prevent_extensions(v);
1936 let mut keys = self.integrity_keys(v);
1937 keys.extend(self.own_accessor_keys(v));
1938 for k in keys {
1939 let mut a = self.prop_attrs(v, &k);
1940 a.configurable = false;
1941 if freeze {
1942 a.writable = false;
1943 }
1944 self.set_prop_attrs(v, &k, a);
1945 }
1946 }
1947
1948 /// The own DATA-property keys SetIntegrityLevel (7.3.15) walks.
1949 ///
1950 /// An array's elements are own properties too, and only the `Object` arm was
1951 /// walked — so `Object.freeze([1, 2])` sealed nothing: `a[0] = 9` wrote
1952 /// through, and the elements still reported `writable: true,
1953 /// configurable: true` while `Object.isFrozen` answered true over an empty
1954 /// key list. `length` is an own property as well, and freezing it is what
1955 /// stops a `push` from extending a frozen array.
1956 fn integrity_keys(&self, v: &Value) -> Vec<String> {
1957 let side_table_keys = |v: &Value| -> Vec<String> {
1958 match v {
1959 Value::Obj(i) => self
1960 .fn_props
1961 .get(i)
1962 .map(|m| m.keys().cloned().collect())
1963 .unwrap_or_default(),
1964 _ => Vec::new(),
1965 }
1966 };
1967 match self.get(v) {
1968 Some(JsObj::Object(p)) => p.keys().cloned().collect(),
1969 // A RegExp's only own property is its `lastIndex` cursor, which
1970 // lives in the `RegExpObj` struct. Without it here `Object.freeze`
1971 // sealed nothing and a frozen regexp's cursor still moved.
1972 Some(JsObj::RegExp(_)) => vec!["lastIndex".to_string()],
1973 // A function's statics and a Map's assigned properties live in the
1974 // fn-prop side table, and freezing has to reach them too.
1975 Some(JsObj::Func(_))
1976 | Some(JsObj::Class(_))
1977 | Some(JsObj::Map { .. })
1978 | Some(JsObj::Set { .. })
1979 | Some(JsObj::Promise { .. }) => side_table_keys(v),
1980 Some(JsObj::Array(items)) => (0..items.len())
1981 .map(|i| i.to_string())
1982 .chain(std::iter::once("length".to_string()))
1983 // A named property stuck on an array (`a.tag = 't'`, a match
1984 // array's `.index`/`.groups`) is an own property too, and
1985 // freezing has to reach it.
1986 .chain(match v {
1987 Value::Obj(i) => self
1988 .fn_props
1989 .get(i)
1990 .map(|m| m.keys().cloned().collect::<Vec<_>>())
1991 .unwrap_or_default(),
1992 _ => Vec::new(),
1993 })
1994 .collect(),
1995 _ => Vec::new(),
1996 }
1997 }
1998
1999 /// `Object.isSealed` (`freeze == false`) / `Object.isFrozen` (`true`).
2000 pub fn is_sealed(&self, v: &Value, freeze: bool) -> bool {
2001 if self.is_extensible(v) {
2002 return false;
2003 }
2004 let mut keys = self.integrity_keys(v);
2005 keys.extend(self.own_accessor_keys(v));
2006 keys.iter().all(|k| {
2007 let a = self.prop_attrs(v, k);
2008 !a.configurable && (!freeze || !a.writable)
2009 })
2010 }
2011
2012 /// A fresh unique `Symbol(desc)` value.
2013 pub fn new_symbol(&mut self, desc: Option<String>) -> Value {
2014 let id = self.next_symbol;
2015 self.next_symbol += 1;
2016 let v = self.alloc(JsObj::Symbol { desc, id });
2017 self.symbols_by_id.insert(id, v.clone());
2018 v
2019 }
2020
2021 /// The symbol VALUE an internal symbol property key (`@@sym:<id>` or a
2022 /// well-known `@@iterator`) came from.
2023 pub fn symbol_of_key(&self, k: &str) -> Option<Value> {
2024 if let Some(id) = k.strip_prefix("@@sym:").and_then(|i| i.parse::<u64>().ok()) {
2025 return self.symbols_by_id.get(&id).cloned();
2026 }
2027 let name = k.strip_prefix("@@")?;
2028 WELL_KNOWN_SYMBOLS
2029 .contains(&name)
2030 .then(|| {
2031 self.symbol_registry
2032 .get(&format!("@@Symbol.{name}"))
2033 .cloned()
2034 })
2035 .flatten()
2036 }
2037
2038 /// The own symbol-keyed property keys of `v` as SYMBOL values —
2039 /// `Object.getOwnPropertySymbols` / the symbol half of `Reflect.ownKeys`.
2040 pub fn own_symbol_keys(&self, v: &Value) -> Vec<Value> {
2041 let keys: Vec<String> = match self.get(v) {
2042 Some(JsObj::Object(p)) => p.keys().cloned().collect(),
2043 // An Array/Function receiver has no property map: its non-index own
2044 // properties — symbol-keyed ones included — live in the fn-prop side
2045 // table, and are just as much own properties as an object's.
2046 Some(_) => self.fn_prop_keys(v),
2047 None => return Vec::new(),
2048 };
2049 keys.iter().filter_map(|k| self.symbol_of_key(k)).collect()
2050 }
2051
2052 /// The own SYMBOL-keyed enumerable `(internal key, value)` pairs of `v` —
2053 /// what `CopyDataProperties` (object spread, `Object.assign`) copies
2054 /// alongside the string keys, and what `Object.keys` / `for-in` /
2055 /// `JSON.stringify` deliberately skip.
2056 pub fn own_symbol_entries(&self, v: &Value) -> Vec<(String, Value)> {
2057 match self.get(v) {
2058 Some(JsObj::Object(p)) => p
2059 .iter()
2060 .filter(|(k, _)| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2061 .map(|(k, val)| (k.clone(), val.clone()))
2062 .collect(),
2063 // Array/Function: the side table (see `own_symbol_keys`).
2064 Some(_) => self
2065 .fn_prop_keys(v)
2066 .into_iter()
2067 .filter(|k| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
2068 .map(|k| {
2069 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
2070 (k, val)
2071 })
2072 .collect(),
2073 None => Vec::new(),
2074 }
2075 }
2076 /// The shared `Symbol.for(key)` value (interned by description).
2077 pub fn symbol_for(&mut self, key: &str) -> Value {
2078 if let Some(v) = self.symbol_registry.get(key) {
2079 return v.clone();
2080 }
2081 let s = self.new_symbol(Some(key.to_string()));
2082 self.symbol_registry.insert(key.to_string(), s.clone());
2083 s
2084 }
2085 /// `Symbol.keyFor(sym)`: the registry key `Symbol.for` interned `sym` under,
2086 /// or `undefined` for a symbol that is not in the registry at all.
2087 ///
2088 /// Matched by symbol IDENTITY, not by description — `Symbol.for('k')` and
2089 /// `Symbol('k')` share a description and only the first is registered. The
2090 /// `@@Symbol.*` well-known entries are registry-internal and never a
2091 /// `keyFor` answer, matching node: `Symbol.keyFor(Symbol.iterator)` is
2092 /// `undefined` there.
2093 pub fn symbol_registry_key(&mut self, sym: &Value) -> Value {
2094 let Some(key) = self
2095 .symbol_registry
2096 .iter()
2097 .find(|(k, v)| self.strict_eq(v, sym) && !k.starts_with("@@Symbol."))
2098 .map(|(k, _)| k.clone())
2099 else {
2100 return Value::Undef;
2101 };
2102 self.new_str(key)
2103 }
2104 /// The well-known `Symbol.iterator` (a fixed shared symbol whose internal
2105 /// property key is `@@iterator`).
2106 pub fn well_known_iterator(&mut self) -> Value {
2107 self.symbol_for("@@Symbol.iterator")
2108 }
2109 /// The well-known `Symbol.asyncIterator` (internal key `@@asyncIterator`).
2110 pub fn well_known_async_iterator(&mut self) -> Value {
2111 self.symbol_for("@@Symbol.asyncIterator")
2112 }
2113 /// A well-known symbol by its ECMAScript name (`toPrimitive`,
2114 /// `toStringTag`, …). Its internal property key is `@@<name>` — see
2115 /// [`WELL_KNOWN_SYMBOLS`] and `property_key`.
2116 ///
2117 /// Its DESCRIPTION is `Symbol.<name>`, so `String(Symbol.iterator)` prints
2118 /// `Symbol(Symbol.iterator)` as V8 does, while the registry key keeps the
2119 /// `@@` prefix — `Symbol.for('Symbol.iterator')` therefore stays a
2120 /// different symbol, and identification is by id, so a user-made
2121 /// `Symbol('Symbol.iterator')` is not mistaken for the well-known one.
2122 pub fn well_known_symbol(&mut self, name: &str) -> Value {
2123 let key = format!("@@Symbol.{name}");
2124 if let Some(v) = self.symbol_registry.get(&key) {
2125 return v.clone();
2126 }
2127 let s = self.new_symbol(Some(format!("Symbol.{name}")));
2128 if let Some(JsObj::Symbol { id, .. }) = self.get(&s) {
2129 self.well_known_ids.insert(*id, name.to_string());
2130 }
2131 self.symbol_registry.insert(key, s.clone());
2132 s
2133 }
2134 /// The internal property-key string for a value used as a key. A `Symbol`
2135 /// maps to a stable per-symbol string so symbol-keyed props round-trip;
2136 /// `Symbol.iterator` maps to the sentinel `@@iterator`.
2137 pub fn property_key(&self, v: &Value) -> String {
2138 if let Some(JsObj::Symbol { id, .. }) = self.get(v) {
2139 if let Some(n) = self.well_known_ids.get(id) {
2140 return format!("@@{n}");
2141 }
2142 return format!("@@sym:{id}");
2143 }
2144 self.str_of(v)
2145 }
2146
2147 pub fn null(&self) -> Value {
2148 self.null_val.clone()
2149 }
2150 pub fn is_null(&self, v: &Value) -> bool {
2151 matches!(self.get(v), Some(JsObj::Null))
2152 }
2153
2154 // ── program loading ──────────────────────────────────────────────────
2155 pub fn program_offsets(&self) -> (usize, usize) {
2156 (self.funcs.len(), self.tries.len())
2157 }
2158 pub fn load_program(&mut self, funcs: Vec<FuncDef>, tries: Vec<TryDef>) {
2159 self.funcs.extend(funcs);
2160 self.tries.extend(tries);
2161 }
2162 /// The source text of function `def_id`, when its program kept one.
2163 pub fn func_source(&self, def_id: usize) -> Option<&str> {
2164 let d = self.funcs.get(def_id)?;
2165 let (start, end) = d.span;
2166 if end == 0 {
2167 return None;
2168 }
2169 self.scripts
2170 .get(d.script? as usize)?
2171 .get(start as usize..end as usize)
2172 }
2173 pub fn try_def(&self, id: usize) -> Option<TryDef> {
2174 self.tries.get(id).cloned()
2175 }
2176
2177 /// What `try` statement `id` HAS — `(has handler, catch parameter name, has
2178 /// finalizer)` — without copying its chunks. Running a `try` used to clone
2179 /// the whole `TryDef`, so a `try` inside a loop deep-copied its block, its
2180 /// handler and its finalizer on every iteration just to learn its shape.
2181 pub fn try_shape(&self, id: usize) -> Option<(bool, Option<String>, bool)> {
2182 let t = self.tries.get(id)?;
2183 Some((
2184 t.handler.is_some(),
2185 t.handler.as_ref().and_then(|(bind, _)| bind.clone()),
2186 t.finalizer.is_some(),
2187 ))
2188 }
2189
2190 /// One `try` part's bytecode: 0 = block, 1 = handler body, 2 = finalizer.
2191 /// Reached only when no pooled VM already holds that chunk.
2192 pub fn try_chunk(&self, id: usize, part: u64) -> Option<Chunk> {
2193 let t = self.tries.get(id)?;
2194 match part {
2195 0 => Some(t.block.clone()),
2196 1 => t.handler.as_ref().map(|(_, body)| body.clone()),
2197 _ => t.finalizer.clone(),
2198 }
2199 }
2200
2201 // ── heap allocation / accessors ──────────────────────────────────────
2202 pub fn alloc(&mut self, obj: JsObj) -> Value {
2203 self.heap.push(obj);
2204 Value::Obj((self.heap.len() - 1) as u32)
2205 }
2206 pub fn get(&self, v: &Value) -> Option<&JsObj> {
2207 if let Value::Obj(i) = v {
2208 self.heap.get(*i as usize)
2209 } else {
2210 None
2211 }
2212 }
2213 pub fn get_mut(&mut self, v: &Value) -> Option<&mut JsObj> {
2214 if let Value::Obj(i) = v {
2215 self.heap.get_mut(*i as usize)
2216 } else {
2217 None
2218 }
2219 }
2220 /// Which variant `v` points at, without copying its contents. Use this in
2221 /// place of `get(v).cloned()` whenever only the tag is needed — see
2222 /// [`ObjKind`].
2223 pub fn kind_of(&self, v: &Value) -> Option<ObjKind> {
2224 self.get(v).map(JsObj::kind)
2225 }
2226 pub fn new_str(&mut self, s: impl Into<String>) -> Value {
2227 self.alloc(JsObj::Str(s.into()))
2228 }
2229 pub fn new_array(&mut self, items: Vec<Value>) -> Value {
2230 self.alloc(JsObj::Array(items))
2231 }
2232
2233 /// Record that `name` was declared as a private method or accessor.
2234 pub fn note_private_method(&mut self, name: &str) {
2235 self.private_methods.insert(name.to_string());
2236 }
2237
2238 /// Whether `name` was declared as a private method/accessor by some class,
2239 /// as opposed to a private field.
2240 pub fn is_private_method(&self, name: &str) -> bool {
2241 self.private_methods.contains(name)
2242 }
2243
2244 /// The name of the class whose body the running function belongs to. Only a
2245 /// method of that class can even mention its private names, so this is the
2246 /// class a failed brand check must name.
2247 /// The `super` binding of the frame now running: the owning class name,
2248 /// whether the method is static, and the home object of an object-literal
2249 /// method. An ARROW captures all three at creation, the way it captures
2250 /// `this` — `super` inside an arrow means the enclosing METHOD's `super`.
2251 /// Whether the activation now running is strict code.
2252 /// Whether `v` is a function whose own body is SLOPPY — not an arrow, and
2253 /// with no `'use strict'` of its own or inherited from its script. This is
2254 /// the receiver test the `arguments`/`caller` poison pill keys on: node
2255 /// decides by the FUNCTION, never by the code doing the reading.
2256 pub fn fn_is_sloppy(&self, v: &Value) -> bool {
2257 match self.get(v) {
2258 Some(JsObj::Func(fv)) => {
2259 !fv.is_arrow && !self.funcs.get(fv.def_id).is_some_and(|d| d.strict)
2260 }
2261 _ => false,
2262 }
2263 }
2264 pub fn current_strict(&self) -> bool {
2265 self.frame().strict
2266 }
2267
2268 /// Mark the frame about to run as STRICT — used for a program whose own top
2269 /// level says `'use strict'`, which has no `FuncDef` to carry the flag.
2270 pub fn set_current_strict(&mut self) {
2271 if let Some(f) = self.frames.last_mut() {
2272 f.strict = true;
2273 }
2274 }
2275
2276 pub fn current_home(&self) -> (Option<String>, bool, Option<Value>) {
2277 (
2278 self.current_home_class_name(),
2279 self.frame().home_static,
2280 self.frame().home_object.clone(),
2281 )
2282 }
2283
2284 pub fn current_home_class_name(&self) -> Option<String> {
2285 match self.get(&self.current_home_class()?) {
2286 Some(JsObj::Class(c)) => Some(c.name.clone()),
2287 _ => None,
2288 }
2289 }
2290
2291 /// Whether `recv` — or anything on its prototype chain — carries the private
2292 /// name `key`. A private FIELD is an own property of the instance; a private
2293 /// METHOD lives on the class prototype, one link up.
2294 pub fn has_private(&self, recv: &Value, key: &str) -> bool {
2295 let mut cur = Some(recv.clone());
2296 while let Some(v) = cur {
2297 let owns = match self.get(&v) {
2298 Some(JsObj::Object(p)) => p.contains_key(key),
2299 Some(JsObj::Class(c)) => c.statics.contains_key(key),
2300 _ => false,
2301 };
2302 if owns || self.own_accessor(&v, key).is_some() || self.fn_prop(&v, key).is_some() {
2303 return true;
2304 }
2305 cur = self.proto_of(&v);
2306 }
2307 false
2308 }
2309
2310 // ── array holes ──────────────────────────────────────────────────────
2311 //
2312 // Every read/write of an array's elision set goes through this block. See
2313 // the `array_holes` field for why the marker lives here rather than in
2314 // `Value`.
2315
2316 /// Whether element `i` of array `arr` is an elided element (a "hole"), as
2317 /// opposed to a stored `undefined`. `false` for anything that is not an
2318 /// array, and for every index of a dense one.
2319 pub fn is_hole(&self, arr: &Value, i: usize) -> bool {
2320 match (arr, ()) {
2321 (Value::Obj(idx), ()) => self.array_holes.get(idx).is_some_and(|hs| hs.contains(&i)),
2322 _ => false,
2323 }
2324 }
2325
2326 /// Whether `arr` has any elided element at all — one hash probe, and the
2327 /// guard every hole-aware code path takes before doing anything slower.
2328 pub fn has_holes(&self, arr: &Value) -> bool {
2329 matches!(arr, Value::Obj(i) if self.array_holes.contains_key(i))
2330 }
2331
2332 /// `arr`'s hole positions in ASCENDING order, or an empty vec if dense.
2333 /// Sorted because every consumer (own-key enumeration, `util.inspect`
2334 /// run-grouping) needs index order, and the backing set has none.
2335 pub fn hole_indices(&self, arr: &Value) -> Vec<usize> {
2336 let Value::Obj(i) = arr else {
2337 return Vec::new();
2338 };
2339 let Some(hs) = self.array_holes.get(i) else {
2340 return Vec::new();
2341 };
2342 let mut v: Vec<usize> = hs.iter().copied().collect();
2343 v.sort_unstable();
2344 v
2345 }
2346
2347 /// Record element `i` of `arr` as elided.
2348 pub fn mark_hole(&mut self, arr: &Value, i: usize) {
2349 if let Value::Obj(idx) = arr {
2350 self.array_holes.entry(*idx).or_default().insert(i);
2351 }
2352 }
2353
2354 /// Record `range` of `arr` as elided (a `new Array(n)`, a `length` grow, or
2355 /// the gap a write past the end opens).
2356 pub fn mark_hole_range(&mut self, arr: &Value, range: std::ops::Range<usize>) {
2357 if range.is_empty() {
2358 return;
2359 }
2360 if let Value::Obj(idx) = arr {
2361 self.array_holes.entry(*idx).or_default().extend(range);
2362 }
2363 }
2364
2365 /// Element `i` now holds a real value: it is no longer a hole. Every write
2366 /// to an array index calls this, which is what keeps a stale hole record
2367 /// from outliving the elision it described.
2368 pub fn clear_hole(&mut self, arr: &Value, i: usize) {
2369 let Value::Obj(idx) = arr else { return };
2370 let Some(hs) = self.array_holes.get_mut(idx) else {
2371 return;
2372 };
2373 hs.remove(&i);
2374 if hs.is_empty() {
2375 self.array_holes.remove(idx);
2376 }
2377 }
2378
2379 /// `arr` is dense from here on (`fill` over the whole array, a fresh
2380 /// dense assignment into an existing handle).
2381 pub fn clear_holes(&mut self, arr: &Value) {
2382 if let Value::Obj(idx) = arr {
2383 self.array_holes.remove(idx);
2384 }
2385 }
2386
2387 /// Copy `src`'s elision set onto `dst`, optionally shifting each position by
2388 /// `f`. Used by every method that derives a new array whose holes track the
2389 /// source's (`slice`, `concat`, `map`).
2390 pub fn copy_holes(&mut self, src: &Value, dst: &Value, f: impl Fn(usize) -> Option<usize>) {
2391 if !self.has_holes(src) {
2392 return;
2393 }
2394 let moved: rustc_hash::FxHashSet<usize> =
2395 self.hole_indices(src).into_iter().filter_map(f).collect();
2396 self.install_holes(dst, moved);
2397 }
2398
2399 /// Rewrite `arr`'s own elision set in place: `f(i)` gives the position each
2400 /// existing hole moves to, or `None` if the mutation removed it. This is the
2401 /// one primitive behind every structural array mutation — `shift` is
2402 /// `i.checked_sub(1)`, `unshift(k)` is `i + k`, `reverse` is `len-1-i`, and
2403 /// `splice` is the general case.
2404 pub fn remap_holes(&mut self, arr: &Value, f: impl Fn(usize) -> Option<usize>) {
2405 if !self.has_holes(arr) {
2406 return;
2407 }
2408 let moved: rustc_hash::FxHashSet<usize> =
2409 self.hole_indices(arr).into_iter().filter_map(f).collect();
2410 self.install_holes(arr, moved);
2411 }
2412
2413 /// Replace `arr`'s elision set outright, dropping the record entirely when
2414 /// the new set is empty so `has_holes` stays a single negative probe for the
2415 /// dense case.
2416 pub fn install_holes(&mut self, arr: &Value, holes: rustc_hash::FxHashSet<usize>) {
2417 let Value::Obj(idx) = arr else { return };
2418 if holes.is_empty() {
2419 self.array_holes.remove(idx);
2420 } else {
2421 self.array_holes.insert(*idx, holes);
2422 }
2423 }
2424
2425 /// Forget any hole at or past `len` — what a `pop`, a `length` shrink or a
2426 /// truncating `splice` leaves behind.
2427 pub fn truncate_holes(&mut self, arr: &Value, len: usize) {
2428 self.remap_holes(arr, |i| (i < len).then_some(i));
2429 }
2430
2431 /// `util.inspect`'s `formatSpecialArray`: the element strings of a SPARSE
2432 /// array, where each maximal run of elided positions collapses to a single
2433 /// `<N empty items>` entry. Returns the entries and whether the last of them
2434 /// is the `... N more items` tail (which the grid layout must not size a
2435 /// column to).
2436 ///
2437 /// The `maxArrayLength` cap counts ENTRIES, not indices, so a run costs one
2438 /// slot however long it is — matching node, where `[ ...Array(200) ]`-style
2439 /// sparse arrays print a single `<200 empty items>`.
2440 fn inspect_sparse(
2441 &self,
2442 v: &Value,
2443 items: &[Value],
2444 indent: usize,
2445 st: &mut InspectCycles,
2446 ) -> (Vec<String>, bool) {
2447 let holes: rustc_hash::FxHashSet<usize> = self.hole_indices(v).into_iter().collect();
2448 let empties = |n: usize| {
2449 let unit = if n == 1 { "item" } else { "items" };
2450 format!("<{n} empty {unit}>")
2451 };
2452 let mut out: Vec<String> = Vec::new();
2453 // The first index not yet accounted for by an entry.
2454 let mut index = 0usize;
2455 for (i, it) in items.iter().enumerate() {
2456 if out.len() >= inspect_max_array_length() {
2457 break;
2458 }
2459 if holes.contains(&i) {
2460 continue;
2461 }
2462 if i > index {
2463 out.push(empties(i - index));
2464 index = i;
2465 if out.len() >= inspect_max_array_length() {
2466 break;
2467 }
2468 }
2469 out.push(self.inspect_lvl(it, indent + 2, st));
2470 index = i + 1;
2471 }
2472 let remaining = items.len() - index;
2473 if remaining == 0 {
2474 return (out, false);
2475 }
2476 if out.len() < inspect_max_array_length() {
2477 // Trailing holes are still `<N empty items>`, not a truncation.
2478 out.push(empties(remaining));
2479 (out, false)
2480 } else {
2481 let unit = if remaining == 1 { "item" } else { "items" };
2482 out.push(format!("... {remaining} more {unit}"));
2483 (out, true)
2484 }
2485 }
2486 pub fn new_object(&mut self, mut props: IndexMap<String, Value>) -> Value {
2487 // Integer-index keys enumerate ascending-first regardless of the order
2488 // they were supplied in (object literal, spread, Object.assign result).
2489 canonicalize_own_keys(&mut props);
2490 // A map carrying the hidden `@@native` tag IS an instance of that native
2491 // class, so it hangs off the class prototype rather than
2492 // `Object.prototype`. Eleven classes — `Hash`, `Cipheriv`,
2493 // `StringDecoder`, `Script`, `URLSearchParams`, `Console`,
2494 // `AbortController` among them — built plain objects instead, so
2495 // `x.constructor.name` read `"Object"` and a chain walk found none of
2496 // the class's methods. Linking HERE means a construction site cannot
2497 // forget it; the tag is already in the map at every one of them.
2498 let tag = props.get("@@native").and_then(|v| self.as_str(v));
2499 let obj = self.alloc(JsObj::Object(props));
2500 if let Some(proto) = tag.and_then(|t| self.ensure_ctor_proto(&t)) {
2501 self.set_proto(&obj, proto);
2502 }
2503 obj
2504 }
2505 pub fn as_str(&self, v: &Value) -> Option<String> {
2506 match v {
2507 Value::Str(s) => Some((**s).clone()),
2508 Value::Obj(_) => match self.get(v) {
2509 Some(JsObj::Str(s)) => Some(s.clone()),
2510 _ => None,
2511 },
2512 _ => None,
2513 }
2514 }
2515
2516 // ── scope / names ────────────────────────────────────────────────────
2517 fn frame(&self) -> &Frame {
2518 self.frames.last().unwrap()
2519 }
2520 fn cur_env(&self) -> Env {
2521 self.frame().env.clone()
2522 }
2523
2524 // ── DAP debug introspection (used only under `--dap`) ────────────────────
2525 /// Number of active call frames (the debugger's step-depth reference).
2526 pub fn frame_depth(&self) -> usize {
2527 self.frames.len()
2528 }
2529 /// Record the source line the innermost frame is executing (DAP line hook).
2530 pub fn set_cur_line(&mut self, line: u32) {
2531 if let Some(f) = self.frames.last_mut() {
2532 f.line = line;
2533 }
2534 }
2535 /// The `.stack` tail for an error created right now: one ` at <name>`
2536 /// line per live frame, innermost first, ending at the module frame.
2537 ///
2538 /// These are the REAL user frames — node-js has no `file:line:column` (the
2539 /// per-frame line is only tracked under `--dap`) and no Node-internal
2540 /// module-loader frames, so `.stack` names the call chain but can never be
2541 /// byte-identical to V8's. The names are what makes a thrown error
2542 /// diagnosable; the missing positions are documented in BUGS.md.
2543 /// V8's `Error.stackTraceLimit` — how many frames a captured stack keeps.
2544 ///
2545 /// The default is 10, it is settable, and setting it to 0 is the documented
2546 /// way to make error construction cheap. It did not exist, so the read was
2547 /// `undefined` and every stack carried every frame regardless.
2548 pub fn stack_trace_limit(&self) -> usize {
2549 match self.builtin_static("Error", "stackTraceLimit") {
2550 Some(v) => {
2551 let n = self.to_number(&v);
2552 if n.is_finite() && n > 0.0 {
2553 n as usize
2554 } else if n.is_nan() || n <= 0.0 {
2555 0
2556 } else {
2557 usize::MAX
2558 }
2559 }
2560 None => 10,
2561 }
2562 }
2563
2564 pub fn stack_frames(&self) -> String {
2565 let limit = self.stack_trace_limit();
2566 if limit == 0 {
2567 return String::new();
2568 }
2569 let mut out = String::new();
2570 for (i, f) in self.frames.iter().enumerate().rev().take(limit) {
2571 let name = match (&f.owner, i) {
2572 (Some(n), _) => n.clone(),
2573 (None, 0) => "Object.<anonymous>".to_string(),
2574 (None, _) => "<anonymous>".to_string(),
2575 };
2576 out.push_str("\n at ");
2577 out.push_str(&name);
2578 }
2579 if out.is_empty() && limit > 0 {
2580 out.push_str("\n at <anonymous>");
2581 }
2582 out
2583 }
2584
2585 /// The call stack as (frame name, line) pairs, innermost first — for the DAP
2586 /// `stackTrace`. `owner` carries the function name where known.
2587 pub fn dbg_stack(&self) -> Vec<(String, u32)> {
2588 self.frames
2589 .iter()
2590 .rev()
2591 .map(|f| {
2592 let name = f.owner.clone().unwrap_or_else(|| "<module>".to_string());
2593 (name, f.line)
2594 })
2595 .collect()
2596 }
2597 /// The innermost frame's locals as (name, inspect) pairs — for DAP `variables`.
2598 pub fn dbg_locals(&self) -> Vec<(String, String)> {
2599 let env = self.cur_env();
2600 let names: Vec<String> = env.borrow().vars.keys().cloned().collect();
2601 names
2602 .into_iter()
2603 .map(|n| {
2604 let v = self.read_name(&n).unwrap_or(Value::Undef);
2605 (n, self.inspect(&v))
2606 })
2607 .collect()
2608 }
2609
2610 /// Scope-chain read: local + enclosing chain, then globals.
2611 /// Whether `name` is a module-top-level binding that has not reached its
2612 /// declaration yet. Separate from [`JsHost::is_tdz`], which answers for a
2613 /// block-scoped one by inspecting the value it holds.
2614 pub fn is_tdz_global(&self, name: &str) -> bool {
2615 self.tdz_globals.contains(name)
2616 }
2617
2618 pub fn read_name(&self, name: &str) -> Option<Value> {
2619 let mut env = Some(self.cur_env());
2620 while let Some(e) = env {
2621 if let Some(v) = e.borrow().vars.get(name) {
2622 return Some(v.clone());
2623 }
2624 env = e.borrow().parent.clone();
2625 }
2626 self.globals.get(name).cloned()
2627 }
2628 pub fn read_global(&self, name: &str) -> Option<Value> {
2629 self.globals.get(name).cloned()
2630 }
2631
2632 /// Whether `name` is bound anywhere on the scope chain or in the globals —
2633 /// `read_name(..).is_some()` without cloning the value it finds. The
2634 /// strict-mode assignment path asks this and nothing else.
2635 pub fn has_name(&self, name: &str) -> bool {
2636 let mut env = Some(self.cur_env());
2637 while let Some(e) = env {
2638 if e.borrow().vars.contains_key(name) {
2639 return true;
2640 }
2641 env = e.borrow().parent.clone();
2642 }
2643 self.globals.contains_key(name)
2644 }
2645
2646 /// Assign to an existing binding up the scope chain, else create a global
2647 /// (JS assignment to an undeclared name targets the global object).
2648 /// Assign to an existing binding, or create a global. Returns `false` when
2649 /// the nearest binding is an immutable (`const`) one, which the caller turns
2650 /// into `TypeError: Assignment to constant variable.` — assigning to a
2651 /// `const` used to succeed SILENTLY, so code that node rejects ran on with
2652 /// a mutated constant.
2653 #[must_use]
2654 pub fn set_name(&mut self, name: &str, val: Value) -> bool {
2655 let mut env = Some(self.cur_env());
2656 while let Some(e) = env {
2657 // `get_mut`, not `contains_key` + `insert`: overwriting an existing
2658 // binding hashed the name twice and allocated a fresh `String` key
2659 // for a key that was already there — once per assignment, so once
2660 // per loop iteration in any counting loop.
2661 //
2662 // The const check runs only at the env that OWNS the name, and the
2663 // `is_empty` guard settles the common (no consts here) case without
2664 // hashing the name again.
2665 let mut b = e.borrow_mut();
2666 if b.vars.contains_key(name) {
2667 if !b.consts.is_empty() && b.consts.contains(name) {
2668 return false;
2669 }
2670 if let Some(slot) = b.vars.get_mut(name) {
2671 *slot = val;
2672 }
2673 return true;
2674 }
2675 drop(b);
2676 env = e.borrow().parent.clone();
2677 }
2678 if self.global_consts.contains(name) {
2679 return false;
2680 }
2681 match self.globals.get_mut(name) {
2682 Some(slot) => *slot = val,
2683 None => {
2684 self.globals.insert(name.to_string(), val);
2685 }
2686 }
2687 true
2688 }
2689
2690 /// Declare a `const` binding: the same placement as [`Self::declare_name`],
2691 /// plus recording the name as immutable in whichever scope received it.
2692 pub fn declare_const_name(&mut self, name: &str, val: Value) {
2693 let f = self.frame();
2694 let to_globals = f.is_module && Rc::ptr_eq(&f.env, &f.base_env);
2695 self.declare_name(name, val);
2696 if to_globals {
2697 self.global_consts.insert(name.to_string());
2698 } else {
2699 self.cur_env().borrow_mut().consts.insert(name.to_string());
2700 }
2701 }
2702
2703 /// The value a lexical binding holds between entering its scope and reaching
2704 /// its declaration — its TEMPORAL DEAD ZONE. One heap object for the whole
2705 /// process, so the check is a heap-index comparison and the marker cannot be
2706 /// produced by any JavaScript expression. It never escapes: every path that
2707 /// could read it throws first.
2708 pub fn tdz_marker(&mut self) -> Value {
2709 if let Some(v) = &self.tdz {
2710 return v.clone();
2711 }
2712 let v = self.alloc(JsObj::Builtin("@@tdz".into()));
2713 self.tdz = Some(v.clone());
2714 v
2715 }
2716
2717 /// Whether `v` is the uninitialized-binding marker.
2718 pub fn is_tdz(&self, v: &Value) -> bool {
2719 matches!((&self.tdz, v), (Some(Value::Obj(a)), Value::Obj(b)) if a == b)
2720 }
2721
2722 /// Declare `name` in the CURRENT scope as uninitialized, unless that scope
2723 /// already binds it. Emitted at the top of every scope for each `let`,
2724 /// `const` and `class` declared directly in it, so a read before the
2725 /// declaration throws instead of finding an OUTER binding of the same name —
2726 /// `let x = 1; { x; let x = 2 }` used to read the outer `1`.
2727 pub fn hoist_tdz(&mut self, name: &str) {
2728 let marker = self.tdz_marker();
2729 let f = self.frame();
2730 // At module top level a lexical binding lives in `globals`, which is ALSO
2731 // what backs `globalThis.<name>` — so parking the marker there exposes it
2732 // to JavaScript, and `const crypto = …` made `globalThis.crypto` read
2733 // back as the marker. Top-level dead zones are tracked in a separate set
2734 // that only the name-read path consults.
2735 if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2736 if !self.globals.contains_key(name) {
2737 self.tdz_globals.insert(name.to_string());
2738 }
2739 return;
2740 }
2741 let env = self.cur_env();
2742 let mut e = env.borrow_mut();
2743 if !e.vars.contains_key(name) {
2744 e.vars.insert(name.to_string(), marker);
2745 }
2746 }
2747
2748 /// Declare a new binding in the current scope (`let`/`const`). At the top of
2749 /// the module frame there is no local env, so those names become globals; once
2750 /// a block scope is open the binding belongs to that block.
2751 pub fn declare_name(&mut self, name: &str, val: Value) {
2752 let f = self.frame();
2753 if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
2754 self.tdz_globals.remove(name);
2755 self.globals.insert(name.to_string(), val);
2756 } else {
2757 self.cur_env()
2758 .borrow_mut()
2759 .vars
2760 .insert(name.to_string(), val);
2761 }
2762 }
2763
2764 /// Declare a `var` (or a hoisted function declaration): FUNCTION-scoped, so it
2765 /// skips every open block scope and lands in the activation's base env.
2766 /// Create a hoisted `var` binding, initialised to `undefined`, only when the
2767 /// name is not already bound in this activation.
2768 ///
2769 /// `var` bindings come into existence when the scope is entered, not where
2770 /// the declaration is written — `f(){ x; var x = 1 }` reads `undefined`
2771 /// rather than throwing. "If absent" is what keeps a parameter intact: in
2772 /// `function f(a) { var a; }` the `var` names a binding that already exists
2773 /// and must not be reset, which is also why a bare `var x;` emits nothing at
2774 /// its own position.
2775 pub fn hoist_var_name(&mut self, name: &str) {
2776 // The ENTRY script's top level is a CommonJS module body, not global
2777 // scope: node wraps every file in a function, so a top-level `var` is a
2778 // local of that wrapper. Binding it into the globals map made
2779 // `var x = 3` at the top of the entry readable as `globalThis.x`, where
2780 // node says `undefined` — a REQUIRED module already ran inside a real
2781 // frame and behaved correctly, so only the entry file differed.
2782 if self.frame().is_module && !self.module_scope {
2783 self.globals.entry(name.to_string()).or_insert(Value::Undef);
2784 return;
2785 }
2786 let base = self.frame().base_env.clone();
2787 let mut env = base.borrow_mut();
2788 if !env.vars.contains_key(name) {
2789 env.vars.insert(name.to_string(), Value::Undef);
2790 }
2791 }
2792
2793 pub fn declare_var_name(&mut self, name: &str, val: Value) {
2794 if self.frame().is_module && !self.module_scope {
2795 self.globals.insert(name.to_string(), val);
2796 return;
2797 }
2798 let base = self.frame().base_env.clone();
2799 base.borrow_mut().vars.insert(name.to_string(), val);
2800 }
2801
2802 /// Enter a fresh block scope.
2803 pub fn push_scope(&mut self) {
2804 let env = self.cur_env();
2805 self.frames.last_mut().unwrap().env = child_env(env);
2806 }
2807
2808 /// Open a scope that is also the activation's VARIABLE environment, and
2809 /// return the previous one so the caller can restore it.
2810 ///
2811 /// A block scope is not enough for a strict direct `eval`: `var` and a
2812 /// hoisted function declaration bind to `base_env`, so they walked straight
2813 /// past a plain `push_scope` and still landed in the caller's function
2814 /// scope. Only `let`/`const` were contained.
2815 pub fn push_var_scope(&mut self) -> Env {
2816 let env = child_env(self.cur_env());
2817 let f = self.frames.last_mut().unwrap();
2818 let prev = std::mem::replace(&mut f.base_env, env.clone());
2819 f.env = env;
2820 prev
2821 }
2822
2823 /// Restore the variable environment a `push_var_scope` replaced.
2824 pub fn pop_var_scope(&mut self, prev: Env) {
2825 let f = self.frames.last_mut().unwrap();
2826 f.env = prev.clone();
2827 f.base_env = prev;
2828 }
2829
2830 /// Leave the innermost block scope (never pops past the activation's base).
2831 pub fn pop_scope(&mut self) {
2832 let cur = self.cur_env();
2833 if Rc::ptr_eq(&cur, &self.frame().base_env) {
2834 return;
2835 }
2836 let parent = cur.borrow().parent.clone();
2837 if let Some(p) = parent {
2838 self.frames.last_mut().unwrap().env = p;
2839 }
2840 }
2841
2842 /// Replace the innermost block scope with a fresh copy of its bindings — the
2843 /// per-iteration environment a `for (let i …)` loop creates, so a closure made
2844 /// in one iteration keeps that iteration's value.
2845 pub fn copy_scope(&mut self) {
2846 let cur = self.cur_env();
2847 if Rc::ptr_eq(&cur, &self.frame().base_env) {
2848 return;
2849 }
2850 let parent = cur.borrow().parent.clone();
2851 let fresh = new_env(parent);
2852 fresh.borrow_mut().vars = cur.borrow().vars.clone();
2853 self.frames.last_mut().unwrap().env = fresh;
2854 }
2855
2856 /// The current block-scope env, for save/restore across a nested chunk.
2857 pub fn scope_snapshot(&self) -> Env {
2858 self.cur_env()
2859 }
2860 pub fn restore_scope(&mut self, env: Env) {
2861 self.frames.last_mut().unwrap().env = env;
2862 }
2863 pub fn set_global(&mut self, name: &str, val: Value) {
2864 self.globals.insert(name.to_string(), val);
2865 }
2866
2867 // ── output capture ───────────────────────────────────────────────────
2868 //
2869 // Every write a *program* makes — `console.log`, `process.stdout.write`,
2870 // `print` — funnels through `write_out`, so turning capture on redirects all
2871 // of them at once. Diagnostics the runtime itself emits (the REPL banner, a
2872 // crash traceback from `main`) deliberately do not: they belong to the
2873 // process, not to the program.
2874
2875 /// Start capturing program output in-process. Any text already captured is
2876 /// discarded, so each run starts clean.
2877 pub fn begin_capture(&mut self) {
2878 self.capture = Some(Vec::new());
2879 }
2880
2881 /// Stop capturing and take everything written since [`begin_capture`],
2882 /// returning the empty string when capture was not on. The captured bytes
2883 /// are rendered lossily: this API hands back a `String`, so a program that
2884 /// wrote non-UTF-8 gets `U+FFFD` here even though the same write reaches a
2885 /// real stdout byte-exact. Use [`end_capture_bytes`] to keep those bytes.
2886 ///
2887 /// [`begin_capture`]: JsHost::begin_capture
2888 /// [`end_capture_bytes`]: JsHost::end_capture_bytes
2889 pub fn end_capture(&mut self) -> String {
2890 String::from_utf8_lossy(&self.capture.take().unwrap_or_default()).into_owned()
2891 }
2892
2893 /// Stop capturing and take the raw bytes, without the lossy transcription
2894 /// [`end_capture`] applies.
2895 ///
2896 /// [`end_capture`]: JsHost::end_capture
2897 pub fn end_capture_bytes(&mut self) -> Vec<u8> {
2898 self.capture.take().unwrap_or_default()
2899 }
2900
2901 /// Whether output is being captured — the one thing a caller needs to know
2902 /// before asking the real stream a question (`isTTY`, cursor position).
2903 pub fn capturing(&self) -> bool {
2904 self.capture.is_some()
2905 }
2906
2907 /// Write program output: into the capture buffer when capturing, else to the
2908 /// process stream `stderr` selects. `s` is written verbatim — callers add
2909 /// their own line ending, as `console.log` does and `process.stdout.write`
2910 /// does not.
2911 pub fn write_out(&mut self, s: &str, stderr: bool) {
2912 self.write_out_bytes(s.as_bytes(), stderr);
2913 }
2914
2915 /// Write program output as raw BYTES. `process.stdout.write(buf)` hands Node
2916 /// a byte string and Node writes it through untouched, so a `Buffer` holding
2917 /// `ff fe 41` reaches stdout as those three bytes. Routing it through a Rust
2918 /// `String` first replaced every non-UTF-8 byte with `U+FFFD` — three bytes
2919 /// became seven — so the byte path exists separately from [`write_out`].
2920 ///
2921 /// [`write_out`]: JsHost::write_out
2922 pub fn write_out_bytes(&mut self, bytes: &[u8], stderr: bool) {
2923 if let Some(buf) = &mut self.capture {
2924 buf.extend_from_slice(bytes);
2925 return;
2926 }
2927 use std::io::Write as _;
2928 if stderr {
2929 let mut e = std::io::stderr();
2930 let _ = e.write_all(bytes);
2931 let _ = e.flush();
2932 } else {
2933 let mut o = std::io::stdout();
2934 let _ = o.write_all(bytes);
2935 let _ = o.flush();
2936 }
2937 }
2938 pub fn del_name(&mut self, name: &str) {
2939 if self
2940 .cur_env()
2941 .borrow_mut()
2942 .vars
2943 .shift_remove(name)
2944 .is_some()
2945 {
2946 return;
2947 }
2948 self.globals.shift_remove(name);
2949 }
2950
2951 pub fn current_this(&self) -> Option<Value> {
2952 self.frame().this_obj.clone()
2953 }
2954
2955 /// The running activation's [`ThisState`].
2956 pub fn this_state(&self) -> ThisState {
2957 self.frame().this_state
2958 }
2959
2960 /// Mark the next user-function activation as a derived constructor.
2961 pub fn mark_next_call_derived_ctor(&mut self) {
2962 self.derived_ctor_next = true;
2963 }
2964
2965 /// BindThisValue (9.1.1.3.1) for a `super()` that has just returned: the
2966 /// nearest derived-constructor activation becomes `Bound`. That is the top
2967 /// frame, or — for `super()` inside an arrow — the constructor below the
2968 /// arrow's own frame. `false` when it was already bound: the second call.
2969 pub fn bind_super_this(&mut self) -> bool {
2970 let Some(f) = self
2971 .frames
2972 .iter_mut()
2973 .rev()
2974 .find(|f| f.this_state != ThisState::Plain)
2975 else {
2976 return true;
2977 };
2978 if f.this_state == ThisState::Bound {
2979 return false;
2980 }
2981 f.this_state = ThisState::Bound;
2982 true
2983 }
2984
2985 /// The object a `super()` call substituted for the instance, if any.
2986 ///
2987 /// `construct_class` allocates the instance up front, so when a base
2988 /// constructor RETURNS an object the substitution happens deep inside the
2989 /// VM, after that allocation. This carries it back out. Each
2990 /// `construct_class` saves and restores the previous value around its own
2991 /// run, so a `new` inside a constructor body cannot steal it.
2992 pub fn take_super_replacement(&mut self) -> Option<Value> {
2993 self.super_replacement.take()
2994 }
2995
2996 pub fn swap_super_replacement(&mut self, v: Option<Value>) -> Option<Value> {
2997 std::mem::replace(&mut self.super_replacement, v)
2998 }
2999
3000 /// Rebind the running activation's `this`.
3001 ///
3002 /// Only `super()` does this: when the parent constructor RETURNS an object,
3003 /// 15.7.15 makes that object the derived instance, so the rest of the
3004 /// derived constructor has to write to it rather than to the one allocated
3005 /// before the call.
3006 pub fn set_current_this(&mut self, v: Value) {
3007 if let Some(f) = self.frames.last_mut() {
3008 f.this_obj = Some(v.clone());
3009 }
3010 self.super_replacement = Some(v);
3011 }
3012 /// The callbacks to run for `event`, consuming any `once` registration in
3013 /// the same step — so a listener that re-emits the event cannot re-enter a
3014 /// one-shot handler.
3015 pub fn take_process_listeners(&mut self, event: &str) -> Vec<Value> {
3016 let Some(list) = self.process_listeners.get_mut(event) else {
3017 return Vec::new();
3018 };
3019 let fired: Vec<Value> = list.iter().map(|l| l.f.clone()).collect();
3020 list.retain(|l| !l.once);
3021 fired
3022 }
3023
3024 /// Bind the TOP-LEVEL `this` — the value a `this` outside any function sees.
3025 ///
3026 /// Node answers differently per entry point and both answers are objects:
3027 /// `node f.js` runs a CommonJS module, so top-level `this` is
3028 /// `module.exports`; `node -e` and `node -` run a Script, so it is
3029 /// `globalThis`. Verified on node v26.7.0 —
3030 /// `console.log(this === globalThis, this === module.exports)` is
3031 /// `false true` from a file and `true false` from `-e` and from stdin. It
3032 /// was `undefined` at every entry point here, so `this.x = 1` at module
3033 /// scope threw instead of populating the exports object.
3034 ///
3035 /// Only the base frame is touched: a plain function call still gets its own
3036 /// (`undefined`) binding rather than inheriting this one.
3037 pub fn set_top_this(&mut self, v: Value) {
3038 if let Some(f) = self.frames.first_mut() {
3039 f.this_obj = Some(v);
3040 }
3041 }
3042 pub fn current_env_capture(&self) -> Env {
3043 self.frame().env.clone()
3044 }
3045 pub fn current_new_target(&self) -> Option<Value> {
3046 self.frame().new_target.clone()
3047 }
3048 fn current_home_class(&self) -> Option<Value> {
3049 self.frame().home_class.clone()
3050 }
3051
3052 /// The `(parent_ctor, this_class_fields)` for a running constructor's
3053 /// `super(...)`, derived from the frame's home class.
3054 pub fn super_context(&self) -> (Option<Value>, Vec<(String, Value, bool)>) {
3055 match self.current_home_class() {
3056 Some(cv) => match self.get(&cv) {
3057 Some(JsObj::Class(c)) => (c.parent.clone(), c.fields.clone()),
3058 _ => (None, Vec::new()),
3059 },
3060 None => (None, Vec::new()),
3061 }
3062 }
3063
3064 /// Resolve `super.name` to either the parent-prototype getter (to be invoked
3065 /// by the caller, outside any host borrow) or a directly-usable value.
3066 pub fn super_resolve(&self, name: &str) -> SuperRef {
3067 // A shorthand method in an OBJECT LITERAL resolves `super` through its
3068 // home object's prototype; only a class method has a home CLASS. With
3069 // nothing tracked for the literal case, `{ m() { super.x() } }` had no
3070 // parent to look in and reported the method missing.
3071 if let Some(home) = self.frame().home_object.clone() {
3072 let target = self.proto_of(&home).unwrap_or(Value::Undef);
3073 if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3074 return SuperRef::Getter(getter);
3075 }
3076 return SuperRef::Data(lookup_chain(self, &target, name).unwrap_or(Value::Undef));
3077 }
3078 let parent = match self
3079 .current_home_class()
3080 .and_then(|cv| match self.get(&cv) {
3081 Some(JsObj::Class(c)) => c.parent.clone(),
3082 _ => None,
3083 }) {
3084 Some(p) => p,
3085 None => return SuperRef::Data(Value::Undef),
3086 };
3087 // A STATIC method's home object is the constructor, so `super.x` reads
3088 // off the parent CONSTRUCTOR; an instance method's is the prototype
3089 // object, so it reads off the parent's prototype. Always taking the
3090 // prototype meant `static s() { return super.s(); }` found nothing and
3091 // then tried to call it.
3092 let target = if self.frame().home_static {
3093 parent.clone()
3094 } else {
3095 match self.get(&parent) {
3096 Some(JsObj::Class(pc)) => pc.proto.clone(),
3097 _ => self.fn_prop(&parent, "prototype").unwrap_or(Value::Undef),
3098 }
3099 };
3100 if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
3101 return SuperRef::Getter(getter);
3102 }
3103 if let Some(v) = lookup_chain(self, &target, name) {
3104 return SuperRef::Data(v);
3105 }
3106 // A static method lives in the fn-prop side table, not the property map.
3107 SuperRef::Data(self.fn_prop(&target, name).unwrap_or(Value::Undef))
3108 }
3109
3110 // ── signals / errors ─────────────────────────────────────────────────
3111 pub fn take_error(&mut self) -> Option<String> {
3112 self.error.take()
3113 }
3114 pub fn raise_str(&mut self, class: &str, msg: &str) -> String {
3115 let s = if msg.is_empty() {
3116 class.to_string()
3117 } else {
3118 format!("{class}: {msg}")
3119 };
3120 self.error = Some(s.clone());
3121 s
3122 }
3123}
3124
3125// ── error constructors ───────────────────────────────────────────────────────
3126
3127pub fn type_error(msg: &str) -> String {
3128 format!("TypeError: {msg}")
3129}
3130pub fn ref_error(name: &str) -> String {
3131 format!("ReferenceError: {name} is not defined")
3132}
3133
3134/// The error a read of a lexical binding still in its TEMPORAL DEAD ZONE
3135/// raises. Distinct from [`ref_error`] on purpose: node says which of the two
3136/// happened, and the difference is how a reader tells a misspelled name from a
3137/// `let` used above its declaration.
3138pub fn tdz_error(name: &str) -> String {
3139 format!("ReferenceError: Cannot access '{name}' before initialization")
3140}
3141pub fn range_error(msg: &str) -> String {
3142 format!("RangeError: {msg}")
3143}
3144
3145/// V8's `String::kMaxLength` on a 64-bit build, in UTF-16 code units — the
3146/// largest string the engine will materialize.
3147///
3148/// Measured on node v26.7.0 (darwin arm64):
3149/// `require('buffer').constants.MAX_STRING_LENGTH` is `536870888`,
3150/// `'a'.repeat(536870888)` succeeds with that length, and
3151/// `'a'.repeat(536870889)` is `RangeError: Invalid string length`.
3152pub const MAX_STRING_LENGTH: usize = 536_870_888;
3153
3154/// The error V8 raises for a string operation whose RESULT would exceed
3155/// [`MAX_STRING_LENGTH`]. It is raised from the length arithmetic, before any
3156/// allocation: `'a'.repeat(2**40)` throws promptly on node where node-js used to
3157/// sit building a 1 TiB `String` until it was killed.
3158pub fn invalid_string_length() -> String {
3159 range_error("Invalid string length")
3160}
3161
3162/// `ToUint32`-validated array length — ECMA-262 10.4.2.2 `ArrayCreate` step 1
3163/// and 10.4.2.4 `ArraySetLength` step 3.
3164///
3165/// A length is legal only if `ToUint32(v)` equals `ToNumber(v)` exactly, so
3166/// `-1`, `1.5`, `NaN`, `Infinity`, `'x'` and `2**32` are all
3167/// `RangeError: Invalid array length` while `'3'` is `3` and `-0` is `0`
3168/// (measured on node v26.7.0: `new Array(-0).length` is `0`, `a.length = '3'`
3169/// leaves `3`, `a.length = 'x'` throws). node-js validated none of them — it
3170/// built `[-1]` from `new Array(-1)`, silently ignored `a.length = -1`, and sat
3171/// materializing four billion elements for `a.length = 2**32`.
3172pub fn to_array_length(v: &Value) -> Result<usize, String> {
3173 // 10.4.2.4 steps 2-3 run TWO conversions: `ToUint32(value)` and then
3174 // `ToNumber(value)`, compared against each other. Both are observable — a
3175 // counting `valueOf` sees two calls in node and saw one here — and the
3176 // second is what makes `arr.length = 1.5` a RangeError rather than 1.
3177 let u32_pass = to_number_value(v)?;
3178 let n = to_number_value(v)?;
3179 let _ = u32_pass;
3180 // `ToUint32`: truncate toward zero, then modulo 2^32.
3181 let u = if n.is_finite() {
3182 (n.trunc() as i64).rem_euclid(1i64 << 32) as u32
3183 } else {
3184 0
3185 };
3186 // `-0` compares equal to `0` here, which is what makes `new Array(-0)` legal.
3187 if (u as f64) != n {
3188 return Err(range_error("Invalid array length"));
3189 }
3190 Ok(u as usize)
3191}
3192
3193/// A Node *coded* error raised from the JS layer: `Name [ERR_CODE]: message`.
3194///
3195/// `builtins::synth_error` parses that head back apart, so the bracketed code
3196/// becomes the enumerable `err.code` that `err.code === 'ERR_INVALID_URL'`-style
3197/// handling reads. Writing the head by hand at each throw site is what left a
3198/// dozen of them with `err.code === undefined` while the message matched.
3199///
3200/// Use this for errors Node raises from `lib/internal/errors.js`, whose `.name`
3201/// is left bracketed while the stack is captured and therefore shows up in both
3202/// `String(err)` and `err.stack` — measured on v26.7.0:
3203///
3204/// ```text
3205/// process.exit(1.5) -> RangeError [ERR_OUT_OF_RANGE]: The value of "code" …
3206/// ```
3207pub fn coded_error(class: &str, code: &str, msg: &str) -> String {
3208 format!("{class} [{code}]: {msg}")
3209}
3210
3211/// The marker `plain_coded_error` hides a code behind, and `synth_error` strips.
3212pub const CODE_MARK: &str = "\u{1}code:";
3213
3214/// Marks an error string as a `DOMException` carrying a WHATWG error NAME
3215/// rather than one of the ECMAScript error classes. WebCrypto and the abort
3216/// APIs reject with these, and the name (`NotSupportedError`) is not a class
3217/// `synth_error` could otherwise recognise.
3218pub const DOM_MARK: &str = "\u{1}dom:";
3219
3220/// A `DOMException` error string: `name` is the WHATWG error name.
3221pub fn dom_error(name: &str, msg: &str) -> String {
3222 format!("{DOM_MARK}{name}\u{1}{msg}")
3223}
3224
3225/// A Node coded error raised from the *native* layer: `.code` is set, but the
3226/// name is never bracketed, so `String(err)` is the plain `Name: message`.
3227///
3228/// The distinction is observable and is not a stylistic choice — on v26.7.0,
3229/// `String(new URL("/x") error)` is `TypeError: Invalid URL` with
3230/// `.code === 'ERR_INVALID_URL'`, while the JS-layer `process.exit(1.5)` error
3231/// brackets its code into the very same two reads. Encoding both through one
3232/// `Name [CODE]:` head would have to pick one and be wrong about the other.
3233///
3234/// The code rides in a marker at the head of the message rather than in the
3235/// error class, because the class text is exactly what must NOT carry it. The
3236/// marker is an internal wire format between a throw site and `synth_error`; it
3237/// never survives into a `.message`.
3238pub fn plain_coded_error(class: &str, code: &str, msg: &str) -> String {
3239 format!("{class}: {CODE_MARK}{code}\u{1}{msg}")
3240}
3241
3242/// Marks the start of the extra string own properties a
3243/// [`plain_coded_error_with`] error carries after its message.
3244pub const FIELDS_MARK: char = '\u{2}';
3245
3246/// [`plain_coded_error`] plus extra enumerable string own properties, set after
3247/// `code` in the order given — `new URL('x', 'nope')` throws with
3248/// `Object.keys(e)` reading `["code","input","base"]`.
3249///
3250/// Each field is `key\u{3}<byte length>\u{3}value`, so a value (a URL input is
3251/// arbitrary user text) may carry any character, the separators included.
3252pub fn plain_coded_error_with(
3253 class: &str,
3254 code: &str,
3255 msg: &str,
3256 fields: &[(&str, &str)],
3257) -> String {
3258 let mut s = plain_coded_error(class, code, msg);
3259 s.push(FIELDS_MARK);
3260 for (k, v) in fields {
3261 s.push_str(&format!("{k}\u{3}{}\u{3}{v}", v.len()));
3262 }
3263 s
3264}
3265
3266/// An error string as a person reads it: `TypeError: Invalid URL`, with the
3267/// internal code and field markers of [`plain_coded_error`] /
3268/// [`plain_coded_error_with`] removed. An uncaught native error is printed
3269/// from its string, and printed the wire format (`\u{1}code:ERR_INVALID_URL…`).
3270pub fn plain_error_text(e: &str) -> String {
3271 let Some(i) = e.find(CODE_MARK) else {
3272 return e.to_string();
3273 };
3274 let (head, rest) = e.split_at(i);
3275 match rest[CODE_MARK.len()..].split_once('\u{1}') {
3276 Some((_, m)) => format!("{head}{}", split_error_fields(m).0),
3277 None => e.to_string(),
3278 }
3279}
3280
3281/// Split a [`plain_coded_error_with`] message back into the message and its
3282/// fields. A message with no field mark comes back whole with no fields.
3283pub fn split_error_fields(msg: &str) -> (&str, Vec<(&str, &str)>) {
3284 let Some((head, mut rest)) = msg.split_once(FIELDS_MARK) else {
3285 return (msg, Vec::new());
3286 };
3287 let mut fields = Vec::new();
3288 while let Some((k, tail)) = rest.split_once('\u{3}') {
3289 let Some((len, tail)) = tail.split_once('\u{3}') else {
3290 break;
3291 };
3292 let Ok(len) = len.parse::<usize>() else { break };
3293 let Some(v) = tail.get(..len) else { break };
3294 fields.push((k, v));
3295 rest = &tail[len..];
3296 }
3297 (head, fields)
3298}
3299
3300/// `TypeError [ERR_INVALID_ARG_TYPE]: The "<name>" <kind> must be of type
3301/// <expected>. Received …` — Node's single most common argument rejection.
3302pub fn invalid_arg_type(name: &str, kind: &str, expected: &str, v: &Value) -> String {
3303 coded_error(
3304 "TypeError",
3305 "ERR_INVALID_ARG_TYPE",
3306 &format!(
3307 "The \"{name}\" {kind} must be of type {expected}. Received {}",
3308 crate::stdlib::received_desc(v)
3309 ),
3310 )
3311}
3312
3313// ── the fusevm run plumbing ──────────────────────────────────────────────────
3314
3315thread_local! {
3316 static DEBUG_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3317}
3318
3319/// Enable/disable DAP debug execution (`node --dap`).
3320pub fn set_debug_mode(on: bool) {
3321 DEBUG_MODE.with(|d| d.set(on));
3322}
3323
3324// ── join cycle detection ─────────────────────────────────────────────────────
3325
3326thread_local! {
3327 /// Heap handles whose join is in progress, innermost last — V8's JoinStack.
3328 static JOIN_STACK: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
3329}
3330
3331/// V8's `JoinStackPush`: record that `v` is being joined, or report `false` if
3332/// it already is.
3333///
3334/// `Array.prototype.join` (and `toString`/`toLocaleString`, which route through
3335/// it) is the one place the language walks an object graph with no depth bound,
3336/// so every engine cuts re-entrance here: a receiver already on the stack
3337/// contributes the EMPTY STRING rather than recursing. Measured on node v26.7.0,
3338/// `const a=[1]; a.push(a); a.push(2); a.join('-')` is `"1--2"`, and
3339/// `String(a)`/`` `${a}` `` on `a=[a]` are both `""`. node-js had no such cut and
3340/// recursed until the native stack overflowed, ABORTING the process (exit 134) —
3341/// uncatchable, where node returns a string.
3342///
3343/// Only re-entrance is cut, not repetition: `[a,a].join('|')` still renders `a`
3344/// twice, because the first render pops before the second pushes.
3345///
3346/// A `true` return MUST be paired with [`join_stack_pop`].
3347pub fn join_stack_push(v: &Value) -> bool {
3348 match v {
3349 Value::Obj(i) => JOIN_STACK.with(|s| {
3350 let mut s = s.borrow_mut();
3351 if s.contains(i) {
3352 false
3353 } else {
3354 s.push(*i);
3355 true
3356 }
3357 }),
3358 _ => true,
3359 }
3360}
3361
3362/// Pop the innermost [`join_stack_push`].
3363pub fn join_stack_pop() {
3364 JOIN_STACK.with(|s| {
3365 s.borrow_mut().pop();
3366 });
3367}
3368
3369// ── native stack guard ───────────────────────────────────────────────────────
3370
3371thread_local! {
3372 /// Lowest stack address a nested run may start from, or 0 before the
3373 /// running thread's bounds have been measured. Cached because the pthread
3374 /// query is a syscall-free but non-trivial read and this is on every call.
3375 static STACK_FLOOR: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
3376}
3377
3378/// Stack left unusable below the floor, as a fraction of the whole stack: the
3379/// throw itself still has to unwind, build an `Error`, capture `.stack` and run
3380/// whatever `catch` receives it, all of which needs room *below* the deepest
3381/// call that was allowed.
3382const STACK_RESERVE_DIVISOR: usize = 8;
3383/// Floor of that reserve, for a thread whose stack is small enough that an
3384/// eighth of it would not cover the unwind.
3385const STACK_RESERVE_MIN: usize = 512 * 1024;
3386/// Reserve assumed on a platform whose stack bounds cannot be queried. Deliberately
3387/// large relative to a default 8 MiB stack — over-reserving costs recursion
3388/// depth, under-reserving costs the process.
3389const STACK_RESERVE_FALLBACK: usize = 1024 * 1024;
3390
3391/// The address of a local in the caller's frame — how far down the stack
3392/// execution currently is. `black_box` keeps the probe from being optimized into
3393/// a different frame.
3394fn stack_pointer() -> usize {
3395 let probe = 0u8;
3396 std::hint::black_box(&probe) as *const u8 as usize
3397}
3398
3399/// The running thread's `(lowest address, size)` stack bounds.
3400///
3401/// Asked of pthread rather than assumed, because the three threads that run JS
3402/// have three different stacks: the `node` binary's own (`main.rs` reserves
3403/// [`crate::JS_STACK_SIZE`]), a `worker_threads` thread's, and a `cargo test`
3404/// harness thread's. A fixed byte budget would be wrong on two of the three.
3405fn stack_bounds() -> Option<(usize, usize)> {
3406 #[cfg(target_vendor = "apple")]
3407 {
3408 // SAFETY: both calls are pure reads of the calling thread's own
3409 // pthread record; neither allocates nor can fail.
3410 unsafe {
3411 let me = libc::pthread_self();
3412 let top = libc::pthread_get_stackaddr_np(me) as usize;
3413 let size = libc::pthread_get_stacksize_np(me);
3414 if size == 0 || top < size {
3415 return None;
3416 }
3417 Some((top - size, size))
3418 }
3419 }
3420 #[cfg(target_os = "linux")]
3421 {
3422 // SAFETY: `attr` is initialized by `pthread_getattr_np` before it is
3423 // read, only read on the success path, and destroyed on every path.
3424 unsafe {
3425 let mut attr: libc::pthread_attr_t = std::mem::zeroed();
3426 if libc::pthread_getattr_np(libc::pthread_self(), &mut attr) != 0 {
3427 return None;
3428 }
3429 let mut low: *mut libc::c_void = std::ptr::null_mut();
3430 let mut size: libc::size_t = 0;
3431 let ok = libc::pthread_attr_getstack(&attr, &mut low, &mut size) == 0;
3432 libc::pthread_attr_destroy(&mut attr);
3433 if ok && size != 0 {
3434 return Some((low as usize, size));
3435 }
3436 None
3437 }
3438 }
3439 #[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
3440 {
3441 None
3442 }
3443}
3444
3445/// The stack address below which a further nested VM run must throw instead of
3446/// recursing.
3447///
3448/// Every JS call is a Rust-level recursion — `run_user_func_nt` pushes a
3449/// [`Frame`], then `run_chunk_on` builds a whole new `fusevm::VM` on the stack
3450/// and runs the body, whose own calls land back here. Unbounded JS recursion
3451/// therefore used to exhaust the OS stack and ABORT: `fatal runtime error:
3452/// stack overflow`, exit 134, which no `try`/`catch` can see. V8 throws a
3453/// catchable `RangeError: Maximum call stack size exceeded` instead (measured on
3454/// node v26.7.0: `let d=0; function f(){d++;f()}` reports depth 9901).
3455///
3456/// The floor is derived from the thread's real bounds rather than a frame count
3457/// because a node-js frame has no fixed size — a debug build spends ~98 KiB per
3458/// JS call (measured: `node -e 'function f(n){…f(n-1)}'` survived 83 on an 8 MiB
3459/// stack and no more), a release build far less, and a native builtin recursing
3460/// through a user callback spends a different amount again.
3461fn stack_floor() -> usize {
3462 let cached = STACK_FLOOR.with(|c| c.get());
3463 if cached != 0 {
3464 return cached;
3465 }
3466 let floor = match stack_bounds() {
3467 Some((low, size)) => low + (size / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN),
3468 None => stack_pointer().saturating_sub(STACK_RESERVE_FALLBACK),
3469 };
3470 STACK_FLOOR.with(|c| c.set(floor));
3471 floor
3472}
3473
3474/// Stack given to each generator/async coroutine.
3475///
3476/// corosensei's default is 1 MiB, which at a debug build's ~98 KiB per JS call
3477/// left a `function*` body barely ten frames of recursion before it walked off
3478/// the end. The mapping is `PROT_NONE` reserved and `mprotect`ed, so the cost of
3479/// a larger one is address space, not resident memory — but it IS per live
3480/// generator, so this stays far below the entry thread's
3481/// [`crate::JS_STACK_SIZE`]: a program with thousands of concurrent async calls
3482/// has thousands of these.
3483const CORO_STACK_SIZE: usize = 16 * 1024 * 1024;
3484
3485/// The [`stack_floor`] that applies while a coroutine on `stack` is running.
3486fn coro_stack_floor(stack: &impl corosensei::stack::Stack) -> usize {
3487 stack.limit().get() + (CORO_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN)
3488}
3489
3490/// corosensei's own `DefaultStack::default()` size, used only when the
3491/// [`CORO_STACK_SIZE`] reservation is refused and the coroutine therefore runs
3492/// on a stack whose bounds are not ours to read.
3493const CORO_FALLBACK_STACK_SIZE: usize = 1024 * 1024;
3494
3495/// Give a coroutine whose stack bounds are unknown a floor measured from where
3496/// its body starts. Called once, at body entry, on the coroutine's own stack.
3497fn ensure_coroutine_floor() {
3498 if STACK_FLOOR.with(|c| c.get()) != 0 {
3499 return;
3500 }
3501 let budget = CORO_FALLBACK_STACK_SIZE
3502 - (CORO_FALLBACK_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN);
3503 STACK_FLOOR.with(|c| c.set(stack_pointer().saturating_sub(budget)));
3504}
3505
3506/// Install `floor` as the current stack floor, returning the previous one.
3507///
3508/// Used around a coroutine resume, which switches to a stack the thread's
3509/// pthread record knows nothing about. A floor of 0 means "not known" and makes
3510/// the next [`stack_floor`] measure again, which is the right answer for the
3511/// entry thread and a conservative one for a fallback coroutine stack.
3512fn swap_stack_floor(floor: usize) -> usize {
3513 STACK_FLOOR.with(|c| c.replace(floor))
3514}
3515
3516/// Whether the native stack is too close to its floor for one more nested run.
3517pub fn stack_exhausted() -> bool {
3518 stack_pointer() <= stack_floor()
3519}
3520
3521/// The error V8 raises when the call stack is exhausted. Catchable, and with the
3522/// `RangeError` constructor node uses — not a `panic!`.
3523pub fn stack_overflow_error() -> String {
3524 range_error("Maximum call stack size exceeded")
3525}
3526
3527/// Pool key for the body of user function `def_id`.
3528pub fn func_key(def_id: usize) -> u64 {
3529 1 << 40 | def_id as u64
3530}
3531
3532/// Pool key for one part of `try` statement `try_id`: 0 = the block, 1 = the
3533/// handler, 2 = the finalizer.
3534pub fn try_key(try_id: usize, part: u64) -> u64 {
3535 2 << 40 | (try_id as u64) << 2 | part
3536}
3537
3538thread_local! {
3539 /// VMs that have finished a run, kept for the next one — grouped by the
3540 /// chunk they still hold.
3541 ///
3542 /// Every JS call, every `try` block and every generator step runs its chunk
3543 /// through [`run_chunk_on`], which used to build a `fusevm::VM` from
3544 /// scratch: three `Vec` allocations, 70 `register_builtin` writes, an `Arc`
3545 /// for the numeric hook, and the JIT enable — per call. `fib(27)` makes
3546 /// 400k calls, so it built 400k VMs to run 23 ops each.
3547 ///
3548 /// Worse, the caller had to hand over an OWNED `Chunk`, so every call also
3549 /// deep-copied the function's whole compiled body: six `Vec`s, a `String`,
3550 /// and `sub_chunks` recursively. Keying the pool by chunk means a repeated
3551 /// call takes back the VM that already holds that body and copies nothing:
3552 /// `VM::reset` is handed the chunk the VM was already carrying.
3553 ///
3554 /// `VM::reset` keeps the builtin table, the hooks and the JIT setting, so a
3555 /// recycled VM needs none of that again. Each key holds a stack of VMs, and
3556 /// a nested (or recursive) call takes the next one, so a key grows to the
3557 /// deepest simultaneous entry into that function and no further.
3558 static VM_POOL: RefCell<rustc_hash::FxHashMap<u64, Vec<VM>>> =
3559 RefCell::new(rustc_hash::FxHashMap::default());
3560}
3561
3562/// An idle VM filed under `key`, if any.
3563fn take_pooled(key: u64) -> Option<VM> {
3564 VM_POOL.with(|p| p.borrow_mut().get_mut(&key).and_then(|v| v.pop()))
3565}
3566
3567/// File a finished VM under `key` for the next run to take.
3568fn put_pooled(key: u64, vm: VM) {
3569 VM_POOL.with(|p| p.borrow_mut().entry(key).or_default().push(vm));
3570}
3571
3572/// Take a VM ready to run `chunk` — recycled if one is idle, otherwise built
3573/// and fitted with the builtins and hooks a fresh VM needs.
3574fn acquire_vm(chunk: Chunk) -> VM {
3575 if let Some(mut vm) = take_pooled(0) {
3576 vm.reset(chunk);
3577 return vm;
3578 }
3579 let mut vm = VM::new(chunk);
3580 crate::builtins::install(&mut vm);
3581 vm.set_numeric_hook(std::sync::Arc::new(|op, a, b| {
3582 crate::builtins::numeric_hook(op, a, b)
3583 }));
3584 // Under `--dap` the tracing JIT would compile hot loops and skip the
3585 // per-statement `DBG_LINE` markers, so debug runs stay on the pure
3586 // interpreter. The `DBG_LINE` builtin fires the debugger line hook; the
3587 // extension seam mirrors pythonrs should the marker emission ever switch.
3588 // The mode is fixed before the first chunk runs, so a pooled VM can never
3589 // come back wearing the wrong one.
3590 if DEBUG_MODE.with(|d| d.get()) {
3591 vm.set_extension_handler(Box::new(|vm, id, _| {
3592 crate::dap::on_ext(vm, id);
3593 }));
3594 } else {
3595 vm.enable_tracing_jit();
3596 }
3597 vm
3598}
3599
3600/// Register every node-js builtin + the numeric hook on a VM, then run it.
3601///
3602/// For a chunk that runs once — a module body, an `eval` — there is nothing to
3603/// key a pool by, so this resets a spare VM with the caller's chunk. Anything
3604/// that runs repeatedly (a function body, a `try` block) goes through
3605/// [`run_chunk_keyed`] instead and never copies its chunk twice.
3606pub fn run_chunk_on(chunk: Chunk) -> Result<Value, String> {
3607 // Checked before the `VM` is built: `VM::new` + `install` are themselves
3608 // several KiB of frame, so a check after them could already have overflowed.
3609 if stack_exhausted() {
3610 return Err(stack_overflow_error());
3611 }
3612 finish_run(0, acquire_vm(chunk))
3613}
3614
3615/// Run the chunk filed under `key`, building it with `make` only if no VM is
3616/// already holding it. A recycled VM re-runs the chunk it kept, so a repeated
3617/// call copies no bytecode at all.
3618pub fn run_chunk_keyed(key: u64, make: impl FnOnce() -> Chunk) -> Result<Value, String> {
3619 if stack_exhausted() {
3620 return Err(stack_overflow_error());
3621 }
3622 let vm = match take_pooled(key) {
3623 Some(mut vm) => {
3624 // Hand the VM back the chunk it is already carrying: `reset` takes
3625 // an owned `Chunk`, and this is the one place where the owned chunk
3626 // costs nothing.
3627 let held = std::mem::take(&mut vm.chunk);
3628 vm.reset(held);
3629 vm
3630 }
3631 None => acquire_vm(make()),
3632 };
3633 finish_run(key, vm)
3634}
3635
3636/// Run a prepared VM to completion and file it back under `key`.
3637fn finish_run(key: u64, mut vm: VM) -> Result<Value, String> {
3638 let outcome = vm.run();
3639 let result = match outcome {
3640 _ if with_host(|h| h.error.is_some()) => {
3641 Err(with_host(|h| h.take_error()).expect("just checked"))
3642 }
3643 VMResult::Ok(v) => Ok(v),
3644 VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
3645 VMResult::Error(e) => Err(e),
3646 };
3647 put_pooled(key, vm);
3648 result
3649}
3650
3651/// Run `chunk` in the GLOBAL scope instead of the caller's.
3652///
3653/// `run_chunk_on` executes on whatever frame is current, so a nested run sees —
3654/// and can shadow — the *calling function's* locals. That is right for a direct
3655/// `eval`, and wrong for every other runtime-source construct: a `new Function`
3656/// body, an indirect `eval` and `vm.runInThisContext` are all specified to run
3657/// in the global scope (ECMA-262 19.2.1.1 `PerformEval` with a null
3658/// `strictCaller`/`direct` pair; `FunctionBody` is instantiated with the *global*
3659/// environment, 20.2.1.1.1 step 26). Measured against node v26.7.0,
3660/// `function outer(){ let loc = 42; return vm.runInThisContext('typeof loc'); }`
3661/// is `"undefined"` there and was `"number"` here.
3662///
3663/// A `var` the chunk itself declares lands in the top-level scope and persists,
3664/// so successive `vm.runInThisContext` calls share it.
3665pub fn run_chunk_in_global_scope(chunk: Chunk) -> Result<Value, String> {
3666 // An INDIRECT eval really is global code (19.2.1.1 step 6): its `var`s bind
3667 // to the global object, not to the entry module's wrapper scope. The flag
3668 // that keeps the entry script's own `var`s out of the globals map has to be
3669 // lifted for the duration, or `(0, eval)('var g = 1')` stopped reaching
3670 // `globalThis.g`.
3671 let prev_scope = with_host(|h| std::mem::take(&mut h.module_scope));
3672 let out = run_chunk_in_global_scope_inner(chunk);
3673 with_host(|h| h.module_scope = prev_scope);
3674 out
3675}
3676
3677fn run_chunk_in_global_scope_inner(chunk: Chunk) -> Result<Value, String> {
3678 let global_env = with_host(|h| h.global_env.clone());
3679 with_host(|h| {
3680 h.frames.push(Frame {
3681 env: global_env.clone(),
3682 base_env: global_env,
3683 this_obj: None,
3684 new_target: None,
3685 home_class: None,
3686 home_static: false,
3687 home_object: None,
3688 strict: false,
3689 line: 0,
3690 owner: None,
3691 is_module: true,
3692 this_state: ThisState::Plain,
3693 })
3694 });
3695 let r = run_chunk_on(chunk);
3696 with_host(|h| {
3697 h.frames.pop();
3698 });
3699 r
3700}
3701
3702/// Run the top-level program chunk, then drain the event loop (microtasks +
3703/// timers) until quiescent — matching Node, which keeps the process alive while
3704/// pending async work remains.
3705pub fn run_main(chunk: Chunk) -> Result<Value, String> {
3706 with_host(|h| h.module_scope = true);
3707 let r = run_chunk_on(chunk);
3708 with_host(|h| h.signal = None);
3709 if r.is_ok() {
3710 run_event_loop()?;
3711 finish_process_events()?;
3712 }
3713 r
3714}
3715
3716/// The shutdown sequence Node runs once the loop has drained on its own: fire
3717/// `beforeExit` (which MAY schedule more work, in which case the loop runs
3718/// again and `beforeExit` fires again), then fire `exit` exactly once.
3719///
3720/// Neither event fired at all before this existed, so `process.on('exit', …)`
3721/// was a registration with no delivery — a listener whose body printed was
3722/// silently dropped, and one that set `process.exitCode` could not affect the
3723/// status. Measured on node v26.7.0,
3724/// `process.on('exit', c => console.log('exit', c))` prints `exit 0`.
3725///
3726/// An explicit `process.exit()` never reaches here (it leaves the process from
3727/// inside the builtin), and neither does an uncaught exception — matching
3728/// Node, where `beforeExit` is skipped on both paths.
3729fn finish_process_events() -> Result<(), String> {
3730 // Bounded: a `beforeExit` listener that re-arms work every time would spin
3731 // forever, exactly as it does in Node, but a runaway here would hang a
3732 // parity run with no output, so it is capped and then treated as drained.
3733 for _ in 0..1000 {
3734 let code = with_host(|h| h.exit_code).unwrap_or(0);
3735 if !crate::stdlib::process::emit_before_exit(code)? {
3736 break;
3737 }
3738 let more =
3739 with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
3740 if !more {
3741 break;
3742 }
3743 run_event_loop()?;
3744 }
3745 let code = with_host(|h| h.exit_code).unwrap_or(0);
3746 crate::stdlib::process::emit_exit_event(code)
3747}
3748
3749// ── formatting ───────────────────────────────────────────────────────────────
3750
3751/// Format a JS number exactly as `Number.prototype.toString` does for the common
3752/// range (no exponential-notation threshold handling for very large/small).
3753pub fn fmt_number(f: f64) -> String {
3754 if f.is_nan() {
3755 return "NaN".into();
3756 }
3757 if f.is_infinite() {
3758 return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
3759 }
3760 if f == 0.0 {
3761 // Covers -0.0 too: (-0).toString() === "0".
3762 return "0".into();
3763 }
3764 if f < 0.0 {
3765 return format!("-{}", js_number_repr(-f));
3766 }
3767 js_number_repr(f)
3768}
3769
3770/// If `k` is an array-index property key, return its numeric value. Per
3771/// ECMAScript, a String property key `P` is an array index iff
3772/// `ToString(ToUint32(P)) === P` and `ToUint32(P) !== 2^32 - 1` — i.e. a
3773/// canonical decimal (no leading zeros, no sign) in the range `0..=2^32-2`.
3774pub fn array_index(k: &str) -> Option<u32> {
3775 if k.is_empty() {
3776 return None;
3777 }
3778 if k == "0" {
3779 return Some(0);
3780 }
3781 // A leading '0' (other than the lone "0" above) is non-canonical.
3782 if k.as_bytes()[0] == b'0' {
3783 return None;
3784 }
3785 if !k.bytes().all(|b| b.is_ascii_digit()) {
3786 return None;
3787 }
3788 match k.parse::<u64>() {
3789 // Array index must be < 2^32-1; u32::MAX == 2^32-1 is excluded.
3790 Ok(n) if n < u32::MAX as u64 => Some(n as u32),
3791 _ => None,
3792 }
3793}
3794
3795/// Compare two own-property keys for `OrdinaryOwnPropertyKeys` enumeration order:
3796/// integer-index keys sort ascending-numeric and precede all string keys; two
3797/// non-index keys compare `Equal` so a *stable* sort leaves them in insertion
3798/// order. (Symbols are stored as `@@…`/`#…` string keys and are non-index, so
3799/// they also fall into the stable-insertion-order tail.)
3800pub fn key_order_cmp(a: &str, b: &str) -> std::cmp::Ordering {
3801 use std::cmp::Ordering;
3802 match (array_index(a), array_index(b)) {
3803 (Some(x), Some(y)) => x.cmp(&y),
3804 (Some(_), None) => Ordering::Less,
3805 (None, Some(_)) => Ordering::Greater,
3806 (None, None) => Ordering::Equal,
3807 }
3808}
3809
3810/// Reorder an object's own-property map into `OrdinaryOwnPropertyKeys` order in
3811/// place: array-index keys ascending first, then the remaining keys in their
3812/// existing (insertion) order. A no-op unless at least one index key is present,
3813/// so the overwhelmingly common all-string-key object keeps its exact order and
3814/// pays nothing. `IndexMap::sort_by` is a stable sort.
3815pub fn canonicalize_own_keys(props: &mut IndexMap<String, Value>) {
3816 if props.keys().any(|k| array_index(k).is_some()) {
3817 props.sort_by(|ak, _, bk, _| key_order_cmp(ak, bk));
3818 }
3819}
3820
3821/// ECMAScript `Number::toString` layout for a positive, finite, nonzero value.
3822///
3823/// Rust's `Display`/`LowerExp` give the shortest round-trip decimal digits, but
3824/// NOT JavaScript's exponential-vs-fixed threshold: Rust prints `1e21` as
3825/// `1000000000000000000000` and `1e-7` as `0.0000001`, whereas JS prints `1e+21`
3826/// and `1e-7`. So we take the shortest digits from `{:e}` and re-lay them out per
3827/// the spec (steps 5–10 of Number::toString): `k` significant digits `s` with
3828/// decimal exponent `n` (value = s × 10^(n−k)); exponential form only when
3829/// `n > 21` or `n ≤ -6`.
3830fn js_number_repr(a: f64) -> String {
3831 // `{:e}` yields `d[.ddd]e<exp>` with the mantissa in [1, 10) and shortest
3832 // round-trip digits. Split it into the digit string `s` and exponent `E`.
3833 let sci = format!("{a:e}");
3834 let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
3835 let e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
3836 let s: String = mant.chars().filter(|c| *c != '.').collect();
3837 let k = s.len() as i32; // number of significant digits
3838 let n = e + 1; // value = s × 10^(n−k), 10^(k−1) ≤ s < 10^k
3839
3840 if k <= n && n <= 21 {
3841 // Integer with trailing zeros: all digits, then n−k zeros.
3842 let mut out = s;
3843 out.push_str(&"0".repeat((n - k) as usize));
3844 out
3845 } else if 0 < n && n <= 21 {
3846 // Decimal point inside the digit run: n digits, '.', the rest.
3847 format!("{}.{}", &s[..n as usize], &s[n as usize..])
3848 } else if -6 < n && n <= 0 {
3849 // Leading "0." then (−n) zeros then all digits.
3850 format!("0.{}{}", "0".repeat((-n) as usize), s)
3851 } else {
3852 // Exponential form. Exponent digit is n−1, always signed.
3853 let exp = n - 1;
3854 let sign = if exp >= 0 { '+' } else { '-' };
3855 let mag = exp.abs();
3856 if k == 1 {
3857 format!("{s}e{sign}{mag}")
3858 } else {
3859 format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
3860 }
3861 }
3862}
3863
3864impl JsHost {
3865 /// The `typeof` string for `v`.
3866 pub fn type_of(&self, v: &Value) -> &'static str {
3867 match v {
3868 Value::Undef => "undefined",
3869 Value::Bool(_) => "boolean",
3870 Value::Int(_) | Value::Float(_) => "number",
3871 Value::Str(_) => "string",
3872 Value::Obj(_) => match self.get(v) {
3873 Some(JsObj::Str(_)) => "string",
3874 // 10.5's `[[Call]]` slot exists on a proxy exactly when its
3875 // target is callable, so `typeof` classifies by the target —
3876 // `typeof new Proxy(function(){}, {})` is `'function'`. The walk
3877 // is bounded: a proxy of a proxy defers again.
3878 Some(JsObj::Proxy { target, .. }) => {
3879 let mut cur = target;
3880 for _ in 0..100 {
3881 match self.get(cur) {
3882 Some(JsObj::Proxy { target: t, .. }) => cur = t,
3883 _ => break,
3884 }
3885 }
3886 if is_callable(self, cur) {
3887 "function"
3888 } else {
3889 "object"
3890 }
3891 }
3892 Some(JsObj::Func(_))
3893 | Some(JsObj::BoundMethod { .. })
3894 | Some(JsObj::BoundFunc { .. })
3895 | Some(JsObj::Class(_)) => "function",
3896 // A Builtin is a callable (`Array`, `parseInt`, `Math.floor`) —
3897 // `typeof === "function"` — EXCEPT the non-callable namespace
3898 // objects (`Math`, `JSON`, `require('fs')`, …) which are "object".
3899 Some(JsObj::Builtin(n)) => {
3900 if builtin_is_callable(n) {
3901 "function"
3902 } else {
3903 "object"
3904 }
3905 }
3906 Some(JsObj::Symbol { .. }) => "symbol",
3907 Some(JsObj::BigInt(_)) => "bigint",
3908 _ => "object", // arrays, objects, null, Map/Set, generators
3909 },
3910 _ => "object",
3911 }
3912 }
3913
3914 /// JS truthiness: false / 0 / -0 / NaN / "" / null / undefined are falsy.
3915 pub fn truthy(&self, v: &Value) -> bool {
3916 match v {
3917 Value::Undef => false,
3918 Value::Bool(b) => *b,
3919 Value::Int(n) => *n != 0,
3920 Value::Float(f) => *f != 0.0 && !f.is_nan(),
3921 Value::Str(s) => !s.is_empty(),
3922 Value::Obj(_) => match self.get(v) {
3923 Some(JsObj::Str(s)) => !s.is_empty(),
3924 Some(JsObj::Null) => false,
3925 Some(JsObj::BigInt(b)) => !num_traits::Zero::is_zero(b),
3926 _ => true, // arrays, objects, functions
3927 },
3928 _ => true,
3929 }
3930 }
3931
3932 /// Coerce to a number (`ToNumber`): the arithmetic-context conversion.
3933 pub fn to_number(&self, v: &Value) -> f64 {
3934 match v {
3935 Value::Undef => f64::NAN,
3936 Value::Bool(b) => {
3937 if *b {
3938 1.0
3939 } else {
3940 0.0
3941 }
3942 }
3943 Value::Int(n) => *n as f64,
3944 Value::Float(f) => *f,
3945 Value::Str(s) => str_to_number(s),
3946 Value::Obj(_) => match self.get(v) {
3947 Some(JsObj::Str(s)) => str_to_number(s),
3948 Some(JsObj::Null) => 0.0,
3949 Some(JsObj::BigInt(b)) => bigint_to_f64(b),
3950 Some(JsObj::Array(items)) => {
3951 // [] -> 0, [x] -> ToNumber(x), else NaN.
3952 if items.is_empty() {
3953 0.0
3954 } else if items.len() == 1 {
3955 self.to_number(&items[0])
3956 } else {
3957 f64::NAN
3958 }
3959 }
3960 _ => f64::NAN,
3961 },
3962 _ => f64::NAN,
3963 }
3964 }
3965
3966 /// `String(v)` — the string-coercion form (raw, unquoted).
3967 pub fn str_of(&self, v: &Value) -> String {
3968 match v {
3969 Value::Undef => "undefined".into(),
3970 Value::Bool(b) => if *b { "true" } else { "false" }.into(),
3971 Value::Int(n) => n.to_string(),
3972 Value::Float(f) => fmt_number(*f),
3973 Value::Str(s) => (**s).clone(),
3974 Value::Obj(_) => match self.get(v) {
3975 Some(JsObj::Str(s)) => s.clone(),
3976 Some(JsObj::Null) => "null".into(),
3977 Some(JsObj::BigInt(b)) => b.to_string(),
3978 Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
3979 Some(JsObj::Array(items)) => {
3980 // Array.prototype.toString: comma-join, null/undefined -> "".
3981 // Guarded by the JoinStack (see `join_stack_push`) so a
3982 // self-referential array yields "" instead of recursing until
3983 // the native stack aborts the process.
3984 if !join_stack_push(v) {
3985 return String::new();
3986 }
3987 let parts: Vec<String> = items
3988 .iter()
3989 .map(|x| match x {
3990 Value::Undef => String::new(),
3991 _ if self.is_null(x) => String::new(),
3992 _ => self.str_of(x),
3993 })
3994 .collect();
3995 join_stack_pop();
3996 parts.join(",")
3997 }
3998 Some(JsObj::Object(props)) => {
3999 // A native `Buffer` stringifies to its decoded (utf-8)
4000 // contents, matching `buf.toString()` — needed for `'' + buf`,
4001 // template interpolation, and `data += chunk` (the pattern
4002 // Express/body-parser use to read a request body).
4003 if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Buffer") {
4004 let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4005 Some(JsObj::Array(items)) => {
4006 items.iter().map(|x| self.to_number(x) as u8).collect()
4007 }
4008 _ => Vec::new(),
4009 };
4010 String::from_utf8_lossy(&bytes).into_owned()
4011 } else if let Some(s) = self.error_to_string(v) {
4012 s
4013 } else {
4014 "[object Object]".into()
4015 }
4016 }
4017 Some(JsObj::Func(f)) => {
4018 // A function built from runtime source (`new Function`,
4019 // `vm.compileFunction`) retains the exact text V8 synthesizes
4020 // for it, so `Function.prototype.toString` reports what Node
4021 // reports. Every other function slices its span out of the
4022 // script it was parsed from; only one whose program kept no
4023 // text (an AOT image, a `rust { }` desugared file) falls back
4024 // to the placeholder.
4025 if let Some(src) = self.fn_prop(v, "@@source") {
4026 return self.str_of(&src);
4027 }
4028 if let Some(text) = self.func_source(f.def_id) {
4029 return text.to_string();
4030 }
4031 let name = self
4032 .funcs
4033 .get(f.def_id)
4034 .map(|d| d.name.clone())
4035 .unwrap_or_default();
4036 format!("function {name}() {{ [code] }}")
4037 }
4038 // The native-code form names the FUNCTION, not its key:
4039 // `String(Math.max)` is `function max() { [native code] }`.
4040 Some(JsObj::Builtin(n)) => {
4041 // The `console` methods are the exception node itself makes:
4042 // each is a wrapper, so `String(console.log)` is the
4043 // ANONYMOUS native-code form even though `console.log.name`
4044 // is `log`. Measured on v26.8.1.
4045 if n.starts_with("console.") {
4046 "function () { [native code] }".into()
4047 } else if let Some(accessor) = crate::builtins::proto_getter_name(n) {
4048 // An accessor half names itself `get size` / `set
4049 // arguments`, which `builtin_name` cannot build because
4050 // it returns a borrowed `&str`.
4051 format!("function {accessor}() {{ [native code] }}")
4052 } else {
4053 format!(
4054 "function {}() {{ [native code] }}",
4055 crate::builtins::builtin_name(n)
4056 )
4057 }
4058 }
4059 // A method read off an instance names itself the same way the
4060 // prototype method it resolves to does: `String([].slice)` is
4061 // `function slice() { [native code] }`.
4062 Some(JsObj::BoundMethod { name, .. }) => {
4063 format!("function {name}() {{ [native code] }}")
4064 }
4065 Some(JsObj::BoundFunc { .. }) => "function () { [native code] }".into(),
4066 // `Function.prototype.toString` refuses to expose a proxy's
4067 // target: V8 reports the native-code form for a proxy of ANY
4068 // callable, so `String(new Proxy(function f(){}, {}))` is
4069 // `function () { [native code] }`, not `f`'s source.
4070 Some(JsObj::Proxy { .. }) if is_callable(self, v) => {
4071 "function () { [native code] }".into()
4072 }
4073 Some(JsObj::Class(c)) => match c.source_def.and_then(|d| self.func_source(d)) {
4074 Some(text) => text.to_string(),
4075 None => format!("class {} {{ }}", c.name),
4076 },
4077 Some(JsObj::Symbol { desc, .. }) => {
4078 // `String(sym)` is allowed (unlike implicit coercion) and yields
4079 // `Symbol(desc)`.
4080 match desc {
4081 Some(d) => format!("Symbol({d})"),
4082 None => "Symbol()".into(),
4083 }
4084 }
4085 _ => "[object Object]".into(),
4086 },
4087 _ => "[object Object]".into(),
4088 }
4089 }
4090
4091 /// The `Symbol.toStringTag` string `util.inspect` renders as a `[Tag]`
4092 /// prefix. V8 suppresses the tag when it is an OWN ENUMERABLE property,
4093 /// because it is then already listed as a `Symbol(Symbol.toStringTag): …`
4094 /// entry and showing it twice would be wrong.
4095 ///
4096 /// Only a DATA property is seen. A tag supplied by a prototype getter
4097 /// (`class C { get [Symbol.toStringTag]() { … } }`) would need a JS call,
4098 /// which cannot run under the host borrow `inspect` holds — such an object
4099 /// prints without the prefix.
4100 /// `[String: 'ab']` / `[Number: 1]` / `[Boolean: false]` — how node renders
4101 /// a primitive wrapper, distinguishing it from the bare primitive.
4102 fn inspect_wrapper(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Option<String> {
4103 let prim = match self.get(v) {
4104 Some(JsObj::Object(p)) => p.get("@@primitive").cloned()?,
4105 _ => return None,
4106 };
4107 let ctor = match &prim {
4108 Value::Bool(_) => "Boolean",
4109 Value::Int(_) | Value::Float(_) => "Number",
4110 // BigInt and Symbol primitives live on the heap; their boxes are
4111 // `[BigInt: 1n]` and `[Symbol: Symbol(s)]`.
4112 _ => match self.get(&prim) {
4113 Some(JsObj::BigInt(_)) => "BigInt",
4114 Some(JsObj::Symbol { .. }) => "Symbol",
4115 _ => "String",
4116 },
4117 };
4118 let head = format!("[{ctor}: {}]", self.inspect_lvl(&prim, indent, st));
4119 // Extra own properties still print, as `[String: 'ab'] { tag: 1 }`. The
4120 // boxed characters are NOT extras — node hides the index properties of
4121 // a String wrapper, showing only what was added to it.
4122 let width = if ctor == "String" {
4123 self.str_of(&prim).chars().count()
4124 } else {
4125 0
4126 };
4127 let extras: Vec<String> = match self.get(v) {
4128 Some(JsObj::Object(p)) => p
4129 .iter()
4130 .filter(|(k, _)| {
4131 !k.starts_with("@@")
4132 && !k.starts_with('#')
4133 && self.prop_attrs(v, k).enumerable
4134 && !k.parse::<usize>().is_ok_and(|i| i < width)
4135 })
4136 .map(|(k, val)| {
4137 format!("{}: {}", fmt_key(k), self.inspect_lvl(val, indent + 2, st))
4138 })
4139 .collect(),
4140 _ => Vec::new(),
4141 };
4142 if extras.is_empty() {
4143 return Some(head);
4144 }
4145 Some(self.render_object(&extras, &format!("{head} "), indent, st))
4146 }
4147
4148 /// The `key: value` parts for own properties a script attached to an exotic
4149 /// whose contents are internal slots — `new Map([['k',1]])` with `m.x = 5`
4150 /// prints `Map(1) { 'k' => 1, x: 5 }`.
4151 fn side_table_parts(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Vec<String> {
4152 self.fn_prop_keys(v)
4153 .into_iter()
4154 .filter(|k| {
4155 !k.starts_with("@@")
4156 && !k.starts_with('#')
4157 && !is_symbol_key(k)
4158 && self.prop_attrs(v, k).enumerable
4159 })
4160 .map(|k| {
4161 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4162 format!(
4163 "{}: {}",
4164 fmt_key(&k),
4165 self.inspect_lvl(&val, indent + 2, st)
4166 )
4167 })
4168 .collect()
4169 }
4170
4171 fn inspect_tag(&self, v: &Value) -> Option<String> {
4172 let own = matches!(self.get(v), Some(JsObj::Object(p)) if p.contains_key("@@toStringTag"));
4173 if own && self.prop_attrs(v, "@@toStringTag").enumerable {
4174 return None;
4175 }
4176 let t = lookup_chain(self, v, "@@toStringTag")?;
4177 self.as_str(&t)
4178 }
4179
4180 /// `console.log`-style rendering of a top-level argument: bare strings print
4181 /// raw; everything else uses `inspect`.
4182 pub fn console_format(&self, v: &Value) -> String {
4183 match v {
4184 Value::Str(_) => self.str_of(v),
4185 Value::Obj(_) if matches!(self.get(v), Some(JsObj::Str(_))) => self.str_of(v),
4186 _ => self.inspect(v),
4187 }
4188 }
4189
4190 /// `util.inspect`-style rendering (nested; strings quoted).
4191 pub fn inspect(&self, v: &Value) -> String {
4192 self.inspect_lvl(v, 0, &mut InspectCycles::default())
4193 }
4194
4195 /// `util.inspect` at a given indentation level, with the cycle guard applied
4196 /// around the object cases.
4197 ///
4198 /// A value already being rendered further up the chain is a CYCLE, and Node
4199 /// marks both ends of it: the back-edge prints `[Circular *N]` and the
4200 /// object it points back at is prefixed `<ref *N>`. Without this the walk
4201 /// only stopped when the depth limit turned the back-edge into `[Object]`,
4202 /// so `const c={a:1}; c.c=c` printed the misleading
4203 /// `{ a: 1, c: { a: 1, c: { a: 1, c: [Object] } } }` instead of
4204 /// `<ref *1> { a: 1, c: [Circular *1] }`.
4205 ///
4206 /// The `*N` id is only assigned when the back-edge is reached, i.e. while
4207 /// the target's own children are being rendered — so the prefix can only be
4208 /// decided after `inspect_value` returns.
4209 /// Whether `v` renders as a LEAF — a finished string produced without
4210 /// recursing into any child.
4211 ///
4212 /// Node assigns `ctx.currentDepth = recurseTimes` in `formatRaw`, but only
4213 /// after the early returns for the shapes that answer immediately: a bare
4214 /// Date is its ISO string, a regex is its literal, an empty container is its
4215 /// braces, and a Buffer is whatever its `[util.inspect.custom]` says. None of
4216 /// those record a depth, so a group containing one is not pushed over the
4217 /// `compact` threshold by it — `util.inspect([new Date(0), null], { compact:
4218 /// 1 })` stays on one line. Charging them a level broke exactly those groups.
4219 fn renders_without_expanding(&self, v: &Value) -> bool {
4220 let plain_props = |p: &IndexMap<String, Value>| {
4221 p.keys().all(|k| k.starts_with("@@") || k.starts_with('#'))
4222 };
4223 match self.get(v) {
4224 // A regex never recurses, with or without its hidden `lastIndex`.
4225 Some(JsObj::RegExp(_)) => true,
4226 Some(JsObj::Map { entries, .. }) => entries.is_empty(),
4227 Some(JsObj::Set { entries, .. }) => entries.is_empty(),
4228 Some(JsObj::Array(items)) => items.is_empty() && self.own_symbol_entries(v).is_empty(),
4229 Some(JsObj::Object(p)) => match p.get("@@native").map(|t| self.str_of(t)).as_deref() {
4230 Some("Buffer") => inspect_custom(),
4231 // Own properties added to a Date DO get expanded after it.
4232 Some("Date") => plain_props(p),
4233 Some(_) => false,
4234 None => plain_props(p) && self.own_symbol_entries(v).is_empty(),
4235 },
4236 _ => false,
4237 }
4238 }
4239
4240 fn inspect_lvl(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4241 if !matches!(v, Value::Obj(_)) {
4242 return self.inspect_value(v, indent, st);
4243 }
4244 if st.seen.iter().any(|p| self.strict_eq(p, v)) {
4245 return format!("[Circular *{}]", st.mark(self, v));
4246 }
4247 st.seen.push(v.clone());
4248 // Node ASSIGNS `ctx.currentDepth = recurseTimes` on entry to each value
4249 // it expands — not a running maximum — so after the children have been
4250 // rendered it holds the depth of the last chain below this group, which
4251 // is what `reduceToSingleString` compares. A value the depth limit
4252 // stubs out as `[Object]` is never expanded and must not count, or an
4253 // object whose deepest level was elided would break where node joins.
4254 // Only a value node actually EXPANDS advances the depth. A string,
4255 // symbol or bigint is a JS primitive that this host happens to store on
4256 // the heap, so it reaches here as `Value::Obj` where an unboxed number
4257 // returns above — and counting it as a level made any group holding one
4258 // look deeper than it was. Under `compact: 1` that is the difference
4259 // between node's `Map(2) { 'k2' => 8, 'j' => 5 }` and breaking the same
4260 // Map across four lines, because its string KEYS were being charged a
4261 // nesting level.
4262 if indent as i64 <= inspect_indent_limit()
4263 && !is_primitive(self, v)
4264 && !self.renders_without_expanding(v)
4265 {
4266 st.deepest = indent;
4267 }
4268 let body = self.inspect_value(v, indent, st);
4269 st.seen.pop();
4270 match st.id_of(self, v) {
4271 Some(id) => format!("<ref *{id}> {body}"),
4272 None => body,
4273 }
4274 }
4275
4276 /// The rendering itself, once `inspect_lvl` has established that `v` is not
4277 /// a back-edge into an object already on the stack.
4278 fn inspect_value(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
4279 if let Some(s) = self.inspect_wrapper(v, indent, st) {
4280 return s;
4281 }
4282 match v {
4283 Value::Undef => "undefined".into(),
4284 Value::Bool(b) => if *b { "true" } else { "false" }.into(),
4285 Value::Int(n) => n.to_string(),
4286 // `util.inspect` distinguishes negative zero; `String(-0)` does not.
4287 Value::Float(f) if *f == 0.0 && f.is_sign_negative() => "-0".into(),
4288 Value::Float(f) => fmt_number(*f),
4289 Value::Str(s) => quote_str(s),
4290 Value::Obj(_) => match self.get(v) {
4291 Some(JsObj::Str(s)) => quote_str(s),
4292 Some(JsObj::Null) => "null".into(),
4293 // `util.inspect` renders a bigint with the `n` suffix, a regex bare.
4294 Some(JsObj::BigInt(b)) => format!("{b}n"),
4295 // `lastIndex` is a non-enumerable own property of every regex,
4296 // so `showHidden` (and therefore `%o`) appends it:
4297 // `/x/g { [lastIndex]: 0 }`.
4298 Some(JsObj::RegExp(r)) => {
4299 let body = format!("/{}/{}", r.source, r.flags);
4300 if inspect_show_hidden() {
4301 format!("{body} {{ [lastIndex]: {} }}", r.last_index.get())
4302 } else {
4303 body
4304 }
4305 }
4306 // `util.inspect` on node v26.7.0 renders a proxy as
4307 // `Proxy(<target>)` — the target's own rendering, wrapped. It
4308 // deliberately does NOT run the handler's traps, so this stays a
4309 // pure `&self` read like every other inspect arm.
4310 Some(JsObj::Proxy { target, .. }) => {
4311 format!("Proxy({})", self.inspect_lvl(target, indent, st))
4312 }
4313 // `arguments` is backed by an Array but is an ordinary-shaped
4314 // exotic to util.inspect: node prints its indices as quoted
4315 // keys under the `[Arguments]` tag, `[Arguments] { '0': 1 }`.
4316 Some(JsObj::Array(items)) if crate::builtins::is_arguments_h(self, v) => {
4317 if indent as i64 > inspect_indent_limit() {
4318 return "[Arguments]".into();
4319 }
4320 let mut inner: Vec<String> = items
4321 .iter()
4322 .enumerate()
4323 .map(|(i, x)| format!("'{i}': {}", self.inspect_lvl(x, indent + 2, st)))
4324 .collect();
4325 for k in self.fn_prop_keys(v) {
4326 if k.starts_with("@@") || !self.prop_attrs(v, &k).enumerable {
4327 continue;
4328 }
4329 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
4330 inner.push(format!(
4331 "{}: {}",
4332 fmt_key(&k),
4333 self.inspect_lvl(&val, indent + 2, st)
4334 ));
4335 }
4336 if inner.is_empty() {
4337 return "[Arguments] {}".into();
4338 }
4339 self.render_object(&inner, "[Arguments] ", indent, st)
4340 }
4341 Some(JsObj::Array(items)) => {
4342 // Own enumerable non-index string props (e.g. a `str.match(re)`
4343 // result's `index`/`input`/`groups`, or a user-assigned
4344 // `arr.foo`) render after the elements, as `key: value`.
4345 let prop_keys: Vec<String> = self
4346 .fn_prop_keys(v)
4347 .into_iter()
4348 .filter(|k| {
4349 !k.starts_with("@@")
4350 && !k.starts_with('#')
4351 && self.prop_attrs(v, k).enumerable
4352 })
4353 .collect();
4354 // An own enumerable SYMBOL-keyed property renders after the
4355 // string keys as `Symbol(desc): value`, as it does on an
4356 // object receiver.
4357 // An instance of an Array SUBCLASS leads with its
4358 // constructor and length, `Bar(2) [ 1, 2 ]`, as node's
4359 // `getPrefix` does for any non-`Array` constructor.
4360 let sub = match self.proto_of(v) {
4361 Some(_) => self.ctor_name(v),
4362 None => String::new(),
4363 };
4364 let base = if sub.is_empty() || sub == "Array" {
4365 String::new()
4366 } else {
4367 format!("{sub}({}) ", items.len())
4368 };
4369 let sym_entries = self.own_symbol_entries(v);
4370 // Under `showHidden` even an empty array has something to
4371 // show — node prints `[ [length]: 0 ]`, not `[]`.
4372 if items.is_empty()
4373 && prop_keys.is_empty()
4374 && sym_entries.is_empty()
4375 && !inspect_show_hidden()
4376 {
4377 return format!("{base}[]");
4378 }
4379 // Node's default inspect depth is 2 (root = depth 0); deeper
4380 // nesting collapses to `[Array]`. indent grows by 2 per level.
4381 if indent as i64 > inspect_indent_limit() {
4382 return "[Array]".into();
4383 }
4384 // `util.inspect`'s `maxArrayLength` (default 100): only the
4385 // first 100 elements are formatted, and the rest collapse to
4386 // a `... N more items` entry. Without the cap a 120-element
4387 // array printed all 120 — and, because the grid column width
4388 // is computed from what is SHOWN, every column was also one
4389 // character wider than node's.
4390 // A SPARSE array takes node's `formatSpecialArray` path: an
4391 // elided run renders as `<N empty items>` rather than as the
4392 // `undefined` it reads back as.
4393 let (mut inner, has_tail) = if self.has_holes(v) {
4394 self.inspect_sparse(v, items, indent, st)
4395 } else {
4396 let shown = items.len().min(inspect_max_array_length());
4397 let mut inner: Vec<String> = items[..shown]
4398 .iter()
4399 .map(|x| self.inspect_lvl(x, indent + 2, st))
4400 .collect();
4401 let remaining = items.len() - shown;
4402 if remaining > 0 {
4403 let unit = if remaining == 1 { "item" } else { "items" };
4404 inner.push(format!("... {remaining} more {unit}"));
4405 }
4406 (inner, remaining > 0)
4407 };
4408 // `showHidden` exposes the non-enumerable `length`, which an
4409 // array always has. It sorts BEFORE any own property node
4410 // shows (`[ 1, [length]: 1, x: 2 ]`) and, being an entry
4411 // rather than an element, it also turns the column grid off —
4412 // which is why a ten-element array under `showHidden` prints
4413 // on one line rather than as a grid.
4414 let show_hidden = inspect_show_hidden();
4415 if show_hidden {
4416 inner.push(format!("[length]: {}", items.len()));
4417 }
4418 let has_props = show_hidden || !prop_keys.is_empty() || !sym_entries.is_empty();
4419 for k in &prop_keys {
4420 let val = self.fn_prop(v, k).unwrap_or(Value::Undef);
4421 inner.push(format!(
4422 "{}: {}",
4423 fmt_key(k),
4424 self.inspect_lvl(&val, indent + 2, st)
4425 ));
4426 }
4427 for (k, val) in &sym_entries {
4428 let label = match self.symbol_of_key(k) {
4429 Some(s) => self.inspect(&s),
4430 None => continue,
4431 };
4432 inner.push(format!(
4433 "{label}: {}",
4434 self.inspect_lvl(val, indent + 2, st)
4435 ));
4436 }
4437 self.render_array(
4438 &inner,
4439 items,
4440 indent,
4441 ArrayLayout {
4442 has_props,
4443 has_tail,
4444 base: &base,
4445 },
4446 st,
4447 )
4448 }
4449 // `URLSearchParams` renders its pairs, not its slots:
4450 // `URLSearchParams { 'a' => '1', 'b' => '2' }`. Keys repeat,
4451 // which is why it is a pair list rather than a Map rendering.
4452 Some(JsObj::Object(props))
4453 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4454 == Some("URLSearchParams") =>
4455 {
4456 let pairs: Vec<Value> = match props.get("@@pairs").and_then(|a| self.get(a)) {
4457 Some(JsObj::Array(items)) => items.clone(),
4458 _ => Vec::new(),
4459 };
4460 if pairs.is_empty() {
4461 return "URLSearchParams {}".into();
4462 }
4463 let inner: Vec<String> = pairs
4464 .iter()
4465 .filter_map(|kv| match self.get(kv) {
4466 Some(JsObj::Array(p)) if p.len() == 2 => Some(format!(
4467 "{} => {}",
4468 self.inspect_lvl(&p[0], indent + 2, st),
4469 self.inspect_lvl(&p[1], indent + 2, st)
4470 )),
4471 _ => None,
4472 })
4473 .collect();
4474 self.render_object(&inner, "URLSearchParams ", indent, st)
4475 }
4476 // A typed array renders as `Uint8Array(3) [ 1, 2, 3 ]` — its
4477 // constructor and length, then the elements laid out exactly as
4478 // an array's. Without this it fell through to the generic object
4479 // arm and printed the `{ length, byteLength, byteOffset,
4480 // BYTES_PER_ELEMENT }` bookkeeping instead of the CONTENTS,
4481 // which is the whole reason anyone logs one.
4482 Some(JsObj::Object(props))
4483 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4484 == Some("TypedArray") =>
4485 {
4486 let kind = props
4487 .get("@@kind")
4488 .map(|k| self.str_of(k))
4489 .unwrap_or_else(|| "TypedArray".into());
4490 // Rendered as STRINGS: a 64-bit view's elements are BigInts,
4491 // which this shared borrow cannot allocate as values.
4492 let elems = crate::stdlib::typedarray::elems_display(self, v);
4493 // The grid layout sizes its columns from the VALUES; a
4494 // 64-bit view's come back as `undefined` (no allocation is
4495 // possible here), which only affects column padding.
4496 let vals = crate::stdlib::typedarray::elems_with_host(self, v);
4497 let base = format!("{kind}({}) ", elems.len());
4498 if indent as i64 > inspect_indent_limit() {
4499 return format!("[{kind}]");
4500 }
4501 let shown = elems.len().min(inspect_max_array_length());
4502 let mut inner: Vec<String> = elems[..shown].to_vec();
4503 let remaining = elems.len() - shown;
4504 if remaining > 0 {
4505 let unit = if remaining == 1 { "item" } else { "items" };
4506 inner.push(format!("... {remaining} more {unit}"));
4507 }
4508 // A view's whole identity — its element width, its window
4509 // onto the backing store, and the store itself — is
4510 // non-enumerable, so `showHidden` is the only way to see it.
4511 // `util.format('%o', view)` goes through here, since `%o`
4512 // implies `showHidden`.
4513 let show_hidden = inspect_show_hidden();
4514 if show_hidden {
4515 let bpe = crate::stdlib::typedarray::bytes_per_element(&kind);
4516 let byte_offset = props
4517 .get("byteOffset")
4518 .map(|x| self.to_number(x))
4519 .unwrap_or(0.0);
4520 inner.push(format!("[BYTES_PER_ELEMENT]: {bpe}"));
4521 inner.push(format!("[length]: {}", elems.len()));
4522 inner.push(format!("[byteLength]: {}", elems.len() * bpe));
4523 inner.push(format!("[byteOffset]: {}", fmt_number(byte_offset)));
4524 // An ArrayBuffer reached AS a view's backing store is
4525 // rendered by node WITHOUT its contents — just
4526 // `ArrayBuffer { [byteLength]: N }` — even though the
4527 // same buffer inspected on its own leads with
4528 // `[Uint8Contents]`. Recursing through the normal
4529 // ArrayBuffer branch therefore printed the bytes twice,
4530 // once as the view's elements and again as the store's.
4531 let buf_len = props
4532 .get("@@buffer")
4533 .and_then(|b| self.get(b))
4534 .and_then(|o| match o {
4535 JsObj::Object(bp) => bp.get("@@bytes").cloned(),
4536 _ => None,
4537 })
4538 .and_then(|b| {
4539 self.get(&b).map(|o| match o {
4540 JsObj::Array(items) => items.len(),
4541 _ => 0,
4542 })
4543 })
4544 .unwrap_or(0);
4545 inner.push(format!(
4546 "[buffer]: ArrayBuffer {{ [byteLength]: {buf_len} }}"
4547 ));
4548 }
4549 self.render_array(
4550 &inner,
4551 &vals,
4552 indent,
4553 ArrayLayout {
4554 has_props: show_hidden,
4555 has_tail: remaining > 0,
4556 base: &base,
4557 },
4558 st,
4559 )
4560 }
4561 // An `ArrayBuffer` renders its CONTENTS, which is the only way
4562 // to see them — it exposes no indices of its own:
4563 // `ArrayBuffer { [Uint8Contents]: <00 01>, [byteLength]: 2 }`.
4564 Some(JsObj::Object(props))
4565 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4566 == Some("ArrayBuffer") =>
4567 {
4568 let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4569 Some(JsObj::Array(items)) => {
4570 items.iter().map(|x| self.to_number(x) as u8).collect()
4571 }
4572 _ => Vec::new(),
4573 };
4574 let hex: Vec<String> = bytes.iter().map(|b| format!("{b:02x}")).collect();
4575 let mut parts = vec![
4576 format!("[Uint8Contents]: <{}>", hex.join(" ")),
4577 format!("[byteLength]: {}", bytes.len()),
4578 ];
4579 if props.contains_key("@@maxByteLength") {
4580 let max = props
4581 .get("@@maxByteLength")
4582 .map(|m| self.to_number(m))
4583 .unwrap_or(0.0);
4584 parts.insert(1, format!("maxByteLength: {}", fmt_number(max)));
4585 }
4586 self.render_object(&parts, "ArrayBuffer ", indent, st)
4587 }
4588 // A live Map/Set iterator shows what it has left to yield, as
4589 // node's `formatIterator` does: `[Map Entries] { [ 1, 'a' ] }`,
4590 // `[Set Iterator] { 1 }`, and `{ }` once it is exhausted.
4591 Some(JsObj::Object(_))
4592 if crate::builtins::collection_iterator_view(self, v).is_some() =>
4593 {
4594 let (brand, rest) = crate::builtins::collection_iterator_view(self, v)
4595 .unwrap_or(("Map Iterator", Vec::new()));
4596 if indent as i64 > inspect_indent_limit() {
4597 let stub = if brand.starts_with("Map") {
4598 "Map Iterator"
4599 } else {
4600 "Set Iterator"
4601 };
4602 return format!("[Object [{stub}]]");
4603 }
4604 let entries = brand.ends_with("Entries");
4605 let shown = rest.len().min(100);
4606 let mut inner: Vec<String> = rest[..shown]
4607 .iter()
4608 .map(|(k, val)| {
4609 let ks = self.inspect_lvl(k, indent + 2, st);
4610 if entries {
4611 let vs = self.inspect_lvl(val, indent + 2, st);
4612 format!("[ {ks}, {vs} ]")
4613 } else {
4614 ks
4615 }
4616 })
4617 .collect();
4618 if rest.len() > shown {
4619 let more = rest.len() - shown;
4620 inner.push(format!(
4621 "... {more} more item{}",
4622 if more == 1 { "" } else { "s" }
4623 ));
4624 }
4625 if inner.is_empty() {
4626 return format!("[{brand}] {{ }}");
4627 }
4628 self.render_object(&inner, &format!("[{brand}] "), indent, st)
4629 }
4630 // A `Date` renders as its ISO-8601 form. Its time value lives in
4631 // the internal `@@ms` slot, which the generic object branch below
4632 // does not show, so without this arm every Date printed as `{}` —
4633 // including through `console.log(d)`, inside arrays, objects and
4634 // Maps, and in an `assert` diff.
4635 Some(JsObj::Object(props))
4636 if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Date") =>
4637 {
4638 let base = crate::stdlib::date::inspect_with_host(self, v);
4639 // Own properties added to a Date follow the date itself, the
4640 // way node appends them: `2020-01-01T00:00:00.000Z { x: 1 }`.
4641 let extra = self.side_table_parts(v, indent, st);
4642 let mut inner: Vec<String> = props
4643 .iter()
4644 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
4645 .map(|(k, val)| format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)))
4646 .collect();
4647 inner.extend(extra);
4648 if inner.is_empty() {
4649 return base;
4650 }
4651 self.render_object(&inner, &format!("{base} "), indent, st)
4652 }
4653 // A `Buffer` renders as `<Buffer 01 02 03>` — hex bytes, capped
4654 // at 50 with a `... N more byte(s)` tail, exactly as
4655 // `util.inspect` does. Without this a `console.log(buf)` (the
4656 // single most common thing anyone does with a Buffer) printed
4657 // the internal `{ length, byteLength, … }` bookkeeping.
4658 Some(JsObj::Object(props))
4659 if props.get("@@native").map(|t| self.str_of(t)).as_deref()
4660 == Some("Buffer") =>
4661 {
4662 let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
4663 Some(JsObj::Array(items)) => {
4664 items.iter().map(|x| self.to_number(x) as u8).collect()
4665 }
4666 _ => Vec::new(),
4667 };
4668 // `<Buffer …>` is Buffer's `[util.inspect.custom]` hook, not
4669 // the shape of the object. Under `customInspect: false` node
4670 // does not call that hook and falls back to the generic
4671 // byte-view rendering — which is what an `assert` diff shows,
4672 // since assert inspects with the hook disabled so that a
4673 // failure names the differing BYTE rather than two opaque hex
4674 // blobs. The constructor is `Buffer` while the brand is still
4675 // `Uint8Array`, so node prints both.
4676 if !inspect_custom() {
4677 let base = format!("Buffer({}) [Uint8Array] ", bytes.len());
4678 if indent as i64 > inspect_indent_limit() {
4679 return "[Buffer [Uint8Array]]".into();
4680 }
4681 let shown = bytes.len().min(inspect_max_array_length());
4682 let mut inner: Vec<String> =
4683 bytes[..shown].iter().map(|b| b.to_string()).collect();
4684 let vals: Vec<Value> = bytes[..shown]
4685 .iter()
4686 .map(|b| Value::Float(*b as f64))
4687 .collect();
4688 let remaining = bytes.len() - shown;
4689 if remaining > 0 {
4690 let unit = if remaining == 1 { "item" } else { "items" };
4691 inner.push(format!("... {remaining} more {unit}"));
4692 }
4693 return self.render_array(
4694 &inner,
4695 &vals,
4696 indent,
4697 ArrayLayout {
4698 has_props: false,
4699 has_tail: remaining > 0,
4700 base: &base,
4701 },
4702 st,
4703 );
4704 }
4705 const MAX: usize = 50;
4706 let shown: Vec<String> =
4707 bytes.iter().take(MAX).map(|b| format!("{b:02x}")).collect();
4708 let mut out = format!("<Buffer {}", shown.join(" "));
4709 if bytes.len() > MAX {
4710 let more = bytes.len() - MAX;
4711 let unit = if more == 1 { "byte" } else { "bytes" };
4712 out.push_str(&format!(" ... {more} more {unit}"));
4713 }
4714 out.push('>');
4715 out
4716 }
4717 // An Error inspects as its `.stack` — never as an object literal
4718 // exposing the internal `message`/`stack` slots. Any own property
4719 // a script added beyond those follows in braces, as V8 renders
4720 // it: `Error: x\n at … { code: 'C' }`.
4721 Some(JsObj::Object(_)) if self.error_to_string(v).is_some() => {
4722 let mut stack = lookup_chain(self, v, "stack")
4723 .map(|s| self.str_of(&s))
4724 .unwrap_or_else(|| self.error_to_string(v).unwrap_or_default());
4725 // A `DOMException` prints its CLASS and then its name —
4726 // `DOMException [AbortError]: m` — where a plain error
4727 // prints only its stack head.
4728 if let Some(JsObj::Object(p)) = self.get(v) {
4729 if let Some(n) = p.get("@@domName") {
4730 let name = self.str_of(n);
4731 stack = format!(
4732 "DOMException [{name}]{}",
4733 stack.strip_prefix(&name).unwrap_or(&stack)
4734 );
4735 }
4736 }
4737 let extra: Vec<String> = self
4738 .own_enum_key_names(v)
4739 .into_iter()
4740 .filter(|k| k != "name")
4741 .map(|k| {
4742 let val = self.fn_prop(v, &k).unwrap_or_else(|| match self.get(v) {
4743 Some(JsObj::Object(p)) => {
4744 p.get(&k).cloned().unwrap_or(Value::Undef)
4745 }
4746 _ => Value::Undef,
4747 });
4748 format!(
4749 "{}: {}",
4750 fmt_key(&k),
4751 self.inspect_lvl(&val, indent + 2, st)
4752 )
4753 })
4754 .collect();
4755 if extra.is_empty() {
4756 stack
4757 } else {
4758 format!("{stack} {{ {} }}", extra.join(", "))
4759 }
4760 }
4761 Some(JsObj::Object(props)) => {
4762 // Instances print with their constructor name as a prefix
4763 // (`C { x: 1 }`); plain objects have none; a null-prototype
4764 // object (e.g. an `Object.groupBy` result) is tagged
4765 // `[Object: null prototype]`.
4766 let ctor = match self.ctor_name(v) {
4767 n if n.is_empty() => "Object".to_string(),
4768 n => n,
4769 };
4770 let plain_prefix = if ctor == "Object" {
4771 String::new()
4772 } else {
4773 format!("{ctor} ")
4774 };
4775 let prefix = if self.inspects_null_proto(v) {
4776 "[Object: null prototype] ".to_string()
4777 } else {
4778 // An inherited `Symbol.toStringTag` shows as `Ctor [Tag] `.
4779 match self.inspect_tag(v) {
4780 Some(t) if t != ctor => format!("{ctor} [{t}] "),
4781 _ => plain_prefix.clone(),
4782 }
4783 };
4784 // Skip node-js's internal slots (`@@native`, `@@bytes`, …) and
4785 // private class fields; a real symbol-keyed own property is a
4786 // visible one and renders as `Symbol(desc): value`.
4787 // An own ACCESSOR has no value to print: node shows the
4788 // label `[Getter]` / `[Setter]` / `[Getter/Setter]` in its
4789 // place. It is found through the `@@ord:` marker the
4790 // property map holds for it, which is also what puts it in
4791 // declaration order among the data properties. Without this
4792 // an accessor rendered as nothing at all — `{ get z(){} }`
4793 // printed `{}`.
4794 let mut shown: Vec<(String, Result<&Value, &'static str>)> = props
4795 .iter()
4796 .filter_map(|(k, val)| match k.strip_prefix(ORD_MARKER) {
4797 Some(real) => {
4798 let attrs = self.prop_attrs(v, real);
4799 let label = match self.own_accessor(v, real)? {
4800 (Some(_), Some(_)) => "[Getter/Setter]",
4801 (Some(_), None) => "[Getter]",
4802 (None, Some(_)) => "[Setter]",
4803 (None, None) => return None,
4804 };
4805 attrs.enumerable.then(|| (fmt_key(real), Err(label)))
4806 }
4807 // Only an ENUMERABLE own property is shown, as node
4808 // does: a native instance keeps bookkeeping (a
4809 // `URLSearchParams`'s `size`) as a hidden own slot,
4810 // and printing it would report a spec getter as data.
4811 None if !k.starts_with("@@")
4812 && !k.starts_with('#')
4813 && self.prop_attrs(v, k).enumerable =>
4814 {
4815 Some((fmt_key(k), Ok(val)))
4816 }
4817 None => None,
4818 })
4819 .collect();
4820 shown.extend(props.iter().filter_map(|(k, val)| {
4821 let sym = self.symbol_of_key(k)?;
4822 self.prop_attrs(v, k)
4823 .enumerable
4824 .then(|| (self.inspect(&sym), Ok(val)))
4825 }));
4826 if shown.is_empty() {
4827 return format!("{prefix}{{}}");
4828 }
4829 // Depth limit (Node default 2): deeper objects collapse to
4830 // `[Object]` (or `[ClassName]` for a named instance).
4831 if indent as i64 > inspect_indent_limit() {
4832 return if self.inspects_null_proto(v) {
4833 // Already bracketed (`[Object: null prototype]`).
4834 prefix.trim_end().to_string()
4835 } else if plain_prefix.is_empty() {
4836 "[Object]".into()
4837 } else {
4838 format!("[{}]", plain_prefix.trim_end())
4839 };
4840 }
4841 let inner: Vec<String> = shown
4842 .iter()
4843 .map(|(k, val)| match val {
4844 Ok(val) => format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)),
4845 Err(label) => format!("{k}: {label}"),
4846 })
4847 .collect();
4848 self.render_object(&inner, &prefix, indent, st)
4849 }
4850 Some(JsObj::Symbol { desc, .. }) => match desc {
4851 Some(d) => format!("Symbol({d})"),
4852 None => "Symbol()".into(),
4853 },
4854 Some(JsObj::Class(c)) => {
4855 // Named like a function (`callable_name`), so a class
4856 // expression picks up its inferred binding name, and one
4857 // with no name at all prints `(anonymous)`. node's
4858 // `getClassBase` appends `extends <name>` only when the
4859 // parent HAS a name: `class extends null {}` prints
4860 // without it.
4861 let mut name = self.callable_name(v);
4862 if name.is_empty() {
4863 name = "(anonymous)".into();
4864 }
4865 let pname = c
4866 .parent
4867 .as_ref()
4868 .map(|p| self.callable_name(p))
4869 .unwrap_or_default();
4870 let base = if pname.is_empty() {
4871 format!("[class {name}]")
4872 } else {
4873 format!("[class {name} extends {pname}]")
4874 };
4875 self.with_callable_props(v, base, indent, st)
4876 }
4877 // A Map/Set renders its members at the NEXT nesting level, and
4878 // collapses to `[Map]`/`[Set]` past the depth limit exactly as an
4879 // array collapses to `[Array]`. Both used to recurse through
4880 // `inspect`, which restarts at indent 0, so the depth gate never
4881 // fired: nesting printed one level too deep at every depth
4882 // (measured on node v26.7.0, four nested Maps print
4883 // `Map(1) { 'a' => Map(1) { 'b' => Map(1) { 'c' => [Map] } } }`),
4884 // and a SELF-referential Map or Set recursed forever and aborted
4885 // the process — `const m=new Map(); m.set('m',m); console.log(m)`
4886 // died with `fatal runtime error: stack overflow`, which no
4887 // `try`/`catch` can see. An empty one still prints in full at any
4888 // depth, as `[]`/`{}` do.
4889 // A WEAK collection never shows its contents: node prints
4890 // `WeakMap { <items unknown> }` whether it holds anything or
4891 // not, because the entries are not enumerable by design.
4892 Some(JsObj::Map { weak: true, .. }) => "WeakMap { <items unknown> }".into(),
4893 Some(JsObj::Set { weak: true, .. }) => "WeakSet { <items unknown> }".into(),
4894 Some(JsObj::Map { entries, .. }) => {
4895 let extra = self.side_table_parts(v, indent, st);
4896 let prefix = self.builtin_prefix(v, "Map", Some(entries.len()));
4897 if entries.is_empty() && extra.is_empty() {
4898 return format!("{prefix}{{}}");
4899 }
4900 if indent as i64 > inspect_indent_limit() {
4901 return self.builtin_depth_stub(v, "Map");
4902 }
4903 let mut inner: Vec<String> = entries
4904 .values()
4905 .map(|(k, val)| {
4906 // Sequenced, not nested in one `format!`: both arms
4907 // need the same `&mut` cycle state.
4908 let ks = self.inspect_lvl(k, indent + 2, st);
4909 let vs = self.inspect_lvl(val, indent + 2, st);
4910 format!("{ks} => {vs}")
4911 })
4912 .collect();
4913 inner.extend(extra);
4914 // Laid out by the SAME routine as a plain object, not joined
4915 // onto one line unconditionally. `Map`/`Set` were the only
4916 // containers that never consulted `breakLength` or `compact`,
4917 // so every collection wide enough to wrap printed as one long
4918 // line: node breaks a seven-member Set of ten-character
4919 // strings across seven lines, and `util.inspect(m, {compact:
4920 // false})` — which assert's own diff renderer depends on —
4921 // could not break a Map at all. Node builds these through
4922 // `reduceToSingleString` with `braces[0]` of `Map(n) {`, which
4923 // is this `prefix` (the trailing space is the brace gap).
4924 self.render_object(&inner, &prefix, indent, st)
4925 }
4926 Some(JsObj::Set { entries, .. }) => {
4927 let extra = self.side_table_parts(v, indent, st);
4928 let prefix = self.builtin_prefix(v, "Set", Some(entries.len()));
4929 if entries.is_empty() && extra.is_empty() {
4930 return format!("{prefix}{{}}");
4931 }
4932 if indent as i64 > inspect_indent_limit() {
4933 return self.builtin_depth_stub(v, "Set");
4934 }
4935 let mut inner: Vec<String> = entries
4936 .values()
4937 .map(|v| self.inspect_lvl(v, indent + 2, st))
4938 .collect();
4939 inner.extend(extra);
4940 // Same layout routine as a Map (see above). Note node does
4941 // NOT column-group a wide Set the way it grids an array:
4942 // `groupArrayElements` is reached only from the list
4943 // formatter, so a 30-member Set is thirty lines.
4944 self.render_object(&inner, &prefix, indent, st)
4945 }
4946 Some(JsObj::Generator { .. }) => "Object [Generator] {}".into(),
4947 Some(JsObj::Iter { array: Some(_), .. }) => "Object [Array Iterator] {}".into(),
4948 Some(JsObj::Promise { id }) => match self.promises.get(*id as usize) {
4949 Some(c) => {
4950 // `P2 [Promise] { 3 }` for an instance of a subclass.
4951 let prefix = self.builtin_prefix(v, "Promise", None);
4952 match c.state {
4953 PromiseState::Pending => format!("{prefix}{{ <pending> }}"),
4954 PromiseState::Fulfilled => {
4955 format!("{prefix}{{ {} }}", self.inspect_lvl(&c.value, 0, st))
4956 }
4957 PromiseState::Rejected => format!(
4958 "{prefix}{{ <rejected> {} }}",
4959 self.inspect_lvl(&c.value, 0, st)
4960 ),
4961 }
4962 }
4963 None => "Promise { <pending> }".into(),
4964 },
4965 Some(JsObj::Func(f)) => {
4966 // `callable_name`, not the FuncDef name: an anonymous
4967 // function expression gets its name by inference from the
4968 // binding it initialises (`const f = function(){}`), and
4969 // that lands as an own `name` property.
4970 let name = self.callable_name(v);
4971 // util.inspect labels a function by its kind, the same
4972 // string V8 gives it as `Symbol.toStringTag`:
4973 // `[AsyncFunction: af]`, `[GeneratorFunction: g]`.
4974 let kind = match self.funcs.get(f.def_id) {
4975 Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction",
4976 Some(d) if d.is_generator => "GeneratorFunction",
4977 Some(d) if d.is_async => "AsyncFunction",
4978 _ => "Function",
4979 };
4980 let base = if name.is_empty() {
4981 format!("[{kind} (anonymous)]")
4982 } else {
4983 format!("[{kind}: {name}]")
4984 };
4985 self.with_callable_props(v, base, indent, st)
4986 }
4987 Some(JsObj::Builtin(n)) => {
4988 // A namespace object is not a function and must not be
4989 // printed as one. The three ECMAScript namespaces carry a
4990 // `Symbol.toStringTag` and inspect as `Object [Math] {}`;
4991 // their members are all non-enumerable, so the braces really
4992 // are empty. A `require()`d module namespace has no tag and
4993 // node prints its members, which cannot be rendered here —
4994 // formatting a member means allocating its value, and this
4995 // runs under the host borrow.
4996 if !builtin_is_callable(n) {
4997 match crate::builtins::well_known_tag(self, v) {
4998 Some(tag) => format!("Object [{tag}] {{}}"),
4999 // `Set.prototype` inspects under the CONSTRUCTOR's
5000 // name, not the key: node prints `Object [Set] {}`.
5001 None => {
5002 format!("Object [{}] {{}}", n.trim_end_matches(".prototype"))
5003 }
5004 }
5005 } else {
5006 format!("[Function: {}]", crate::builtins::builtin_name(n))
5007 }
5008 }
5009 // A bound method is not anonymous: it is the prototype method it
5010 // resolves to, so `console.log(new Uint8Array(1).set)` reports
5011 // `[Function: set]`.
5012 Some(JsObj::BoundMethod { name, .. }) => format!("[Function: {name}]"),
5013 Some(JsObj::BoundFunc { target, .. }) => {
5014 let n = self.callable_name(target);
5015 if n.is_empty() {
5016 "[Function: bound ]".into()
5017 } else {
5018 format!("[Function: bound {n}]")
5019 }
5020 }
5021 _ => "undefined".into(),
5022 },
5023 _ => "undefined".into(),
5024 }
5025 }
5026
5027 /// Append a callable's own enumerable properties to its `[Function: f]` /
5028 /// `[class C]` base, the way `util.inspect` does: `[Function: f] { a: 1 }`.
5029 /// A callable with none renders as the bare base.
5030 fn with_callable_props(
5031 &self,
5032 v: &Value,
5033 base: String,
5034 indent: usize,
5035 st: &mut InspectCycles,
5036 ) -> String {
5037 let mut inner: Vec<String> = self
5038 .own_enum_key_names(v)
5039 .into_iter()
5040 .map(|k| {
5041 let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
5042 format!(
5043 "{}: {}",
5044 fmt_key(&k),
5045 self.inspect_lvl(&val, indent + 2, st)
5046 )
5047 })
5048 .collect();
5049 for (k, val) in self.own_symbol_entries(v) {
5050 if let Some(sym) = self.symbol_of_key(&k) {
5051 inner.push(format!(
5052 "{}: {}",
5053 self.inspect(&sym),
5054 self.inspect_lvl(&val, indent + 2, st)
5055 ));
5056 }
5057 }
5058 if inner.is_empty() {
5059 return base;
5060 }
5061 self.render_object(&inner, &format!("{base} "), indent, st)
5062 }
5063
5064 /// Render a non-empty array's already-formatted element strings, applying
5065 /// Node's `util.inspect` layout: a single line when it fits, else a multi-line
5066 /// grid via `groupArrayElements` (for >6 entries), else one element per line.
5067 /// `values` is the raw element list (drives numeric right-alignment); `indent`
5068 /// is the array's own indentation level.
5069 fn render_array(
5070 &self,
5071 output: &[String],
5072 values: &[Value],
5073 indent: usize,
5074 opts: ArrayLayout<'_>,
5075 st: &InspectCycles,
5076 ) -> String {
5077 let ArrayLayout {
5078 has_props,
5079 has_tail,
5080 base,
5081 } = opts;
5082 // Group array elements together if the array has more than six entries.
5083 // Arrays carrying extra own props (`index`/`input`/… on a match result)
5084 // are never grid-grouped — Node lays those out plainly.
5085 // `compact: false` (held as 0) also turns the GRID off, not just the
5086 // single-line join. Node reaches `groupArrayElements` only under
5087 // `ctx.compact >= 1`, so `util.inspect(arr, { compact: false })` is one
5088 // element per line however many there are; without this gate a 30-element
5089 // array still came back column-aligned in three rows, which is the form
5090 // assert's diff renderer splits on — every array diff would have been
5091 // computed over grid rows instead of elements.
5092 let entries = output.len();
5093 let (lines, grouped) = if entries > 6 && !has_props && inspect_compact() >= 1 {
5094 group_array_elements(self, output, values, indent, has_tail)
5095 } else {
5096 (output.to_vec(), false)
5097 };
5098 // A typed array prints its constructor and length ahead of the brackets
5099 // (`Uint8Array(3) [ 1, 2, 3 ]`); node counts that as `base` in the
5100 // break-length seed, so a long tag wraps the list one entry sooner.
5101 if output.is_empty() {
5102 return format!("{base}[]");
5103 }
5104 // If no grouping happened, try to line everything up on a single line.
5105 if !grouped {
5106 // start = output.length + indentationLvl + braces[0].len(1) + base + 10
5107 let start = output.len() + indent + 1 + base.chars().count() + 10;
5108 if self.may_compact(indent, st) && is_below_break_length(output, start) {
5109 return format!("{base}[ {} ]", output.join(", "));
5110 }
5111 }
5112 // Otherwise: one (grouped or single) entry per line, indented by indent+2.
5113 let pad = " ".repeat(indent);
5114 let sep = format!(",\n{pad} ");
5115 format!("{base}[\n{pad} {}\n{pad}]", lines.join(&sep))
5116 }
5117
5118 /// Render a non-empty object's already-formatted `key: value` strings with
5119 /// Node's `util.inspect` layout: a single line when it fits `breakLength`,
5120 /// else one property per line indented by `indent + 2`. `prefix` is the
5121 /// constructor/`[Object: null prototype]` tag (with trailing space) or empty.
5122 /// Mirrors `render_array`'s break decision, including the `compact` depth
5123 /// gate.
5124 /// Whether a group at `indent` may be joined onto one line.
5125 ///
5126 /// Node's `reduceToSingleString`: only while the subtree below this group is
5127 /// SHALLOWER than `compact` (default 3). `compact: false` is held as 0, so
5128 /// nothing qualifies and every group breaks.
5129 fn may_compact(&self, indent: usize, st: &InspectCycles) -> bool {
5130 let compact = inspect_compact();
5131 if compact < 1 {
5132 return false;
5133 }
5134 // Levels, not columns: the indent advances by two per level.
5135 let depth_below = (st.deepest.saturating_sub(indent)) / 2;
5136 (depth_below as i64) < compact
5137 }
5138
5139 fn render_object(
5140 &self,
5141 output: &[String],
5142 prefix: &str,
5143 indent: usize,
5144 st: &InspectCycles,
5145 ) -> String {
5146 // start = output.length + indentationLvl + braces[0].len + base(0) + 10.
5147 // For a tagged object Node folds the tag into `braces[0]` (e.g.
5148 // `"Point {"`, `"[Object: null prototype] {"`), so its length is the
5149 // prefix (which carries the trailing space) plus the `{`.
5150 // `sorted: true` orders the RENDERED entries, not the keys. Node sorts
5151 // the finished `key: value` strings (`output.sort()` in `formatRaw` for
5152 // the object shape), which is observably different from sorting keys
5153 // whenever a key needs quoting — `'b-b': 1` sorts under `'`, not `b`.
5154 // `assert`'s diff renderer depends on this: without it two objects
5155 // carrying the same properties in a different insertion order diffed as
5156 // a wholesale rewrite of every line instead of as equal.
5157 let sorted_output;
5158 let output = if inspect_sorted() {
5159 let mut v = output.to_vec();
5160 v.sort();
5161 sorted_output = v;
5162 &sorted_output[..]
5163 } else {
5164 output
5165 };
5166 let braces0 = prefix.chars().count() + 1;
5167 let start = output.len() + indent + braces0 + 10;
5168 if self.may_compact(indent, st) && is_below_break_length(output, start) {
5169 return format!("{prefix}{{ {} }}", output.join(", "));
5170 }
5171 let pad = " ".repeat(indent);
5172 let sep = format!(",\n{pad} ");
5173 format!("{prefix}{{\n{pad} {}\n{pad}}}", output.join(&sep))
5174 }
5175
5176 /// The `.name` of any callable (function/class/builtin/bound).
5177 pub fn callable_name(&self, v: &Value) -> String {
5178 // A user-set `.name` own property wins.
5179 if let Some(n) = self.fn_prop(v, "name") {
5180 return self.str_of(&n);
5181 }
5182 match self.get(v) {
5183 Some(JsObj::Func(f)) => self
5184 .funcs
5185 .get(f.def_id)
5186 .map(|d| d.name.clone())
5187 .unwrap_or_default(),
5188 Some(JsObj::Class(c)) => c.name.clone(),
5189 // Not the whole key: a builtin's `.name` is its last segment, and a
5190 // prototype thunk's key is `@proto:<Ctor>:<method>` — which has no
5191 // `.` at all, so this reported the internal spelling verbatim and
5192 // `console.log(Uint8Array.prototype.set)` printed
5193 // `[Function: @proto:TypedArray:set]`.
5194 Some(JsObj::Builtin(n)) => crate::builtins::builtin_name(n).to_string(),
5195 Some(JsObj::BoundFunc { target, .. }) => {
5196 format!("bound {}", self.callable_name(target))
5197 }
5198 Some(JsObj::BoundMethod { name, .. }) => name.clone(),
5199 _ => String::new(),
5200 }
5201 }
5202
5203 // ── equality / comparison / arithmetic (numeric-hook + builtin paths) ──
5204
5205 /// Strict equality (`===`): same type and same value, no coercion.
5206 pub fn strict_eq(&self, a: &Value, b: &Value) -> bool {
5207 match (a, b) {
5208 (Value::Undef, Value::Undef) => true,
5209 (Value::Bool(x), Value::Bool(y)) => x == y,
5210 (Value::Str(x), Value::Str(y)) => x == y,
5211 _ => {
5212 // Numbers (NaN !== NaN, +0 === -0).
5213 let an = matches!(a, Value::Int(_) | Value::Float(_));
5214 let bn = matches!(b, Value::Int(_) | Value::Float(_));
5215 if an && bn {
5216 let x = self.to_number(a);
5217 let y = self.to_number(b);
5218 return x == y;
5219 }
5220 // BigInt === BigInt compares by value (each literal is a distinct
5221 // heap cell, so reference identity would be wrong). BigInt is never
5222 // `===` a Number (different types).
5223 if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5224 return x == y;
5225 }
5226 // Heap values.
5227 if let (Some(sa), Some(sb)) = (self.as_str(a), self.as_str(b)) {
5228 return sa == sb;
5229 }
5230 let na = self.is_null(a);
5231 let nb = self.is_null(b);
5232 if na || nb {
5233 return na && nb;
5234 }
5235 // A builtin namespace/constructor/prototype is a SINGLETON in JS
5236 // (`Math === Math`, `Array.prototype === Array.prototype`), but
5237 // every bare reference here allocates a fresh handle, so compare
5238 // those by name rather than by heap index.
5239 if let (Some(JsObj::Builtin(x)), Some(JsObj::Builtin(y))) =
5240 (self.get(a), self.get(b))
5241 {
5242 return builtin_identity(x) == builtin_identity(y);
5243 }
5244 // Reference identity for arrays/objects/functions.
5245 matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
5246 }
5247 }
5248 }
5249
5250 /// Whether `v` is `null` or `undefined`.
5251 pub fn is_nullish(&self, v: &Value) -> bool {
5252 matches!(v, Value::Undef) || self.is_null(v)
5253 }
5254
5255 /// The ECMAScript "loose type" of `v` for the `==` algorithm: `"number"`,
5256 /// `"string"` (primitive or heap string), `"boolean"`, `"undefined"`,
5257 /// `"null"`, or `"object"` (array / plain object / function).
5258 fn js_type(&self, v: &Value) -> &'static str {
5259 match v {
5260 Value::Undef => "undefined",
5261 Value::Bool(_) => "boolean",
5262 Value::Int(_) | Value::Float(_) => "number",
5263 Value::Str(_) => "string",
5264 Value::Obj(_) => match self.get(v) {
5265 Some(JsObj::Str(_)) => "string",
5266 Some(JsObj::Null) => "null",
5267 Some(JsObj::BigInt(_)) => "bigint",
5268 _ => "object",
5269 },
5270 _ => "object",
5271 }
5272 }
5273
5274 /// Loose equality (`==`) following the ECMAScript Abstract Equality Comparison.
5275 /// Objects reduce via `ToPrimitive` (which for our heap objects is always their
5276 /// string `toString`), so `[0] == "0"` is `true` (string compare of `"0"`) but
5277 /// `[0] == ""` is `false` — never a number coercion of the object.
5278 pub fn loose_eq(&self, a: &Value, b: &Value) -> bool {
5279 // Same type: identical to `===` (number==number, string==string, etc.).
5280 if self.strict_eq(a, b) {
5281 return true;
5282 }
5283 let ta = self.js_type(a);
5284 let tb = self.js_type(b);
5285 // null and undefined are loosely equal only to each other.
5286 if self.is_nullish(a) || self.is_nullish(b) {
5287 return self.is_nullish(a) && self.is_nullish(b);
5288 }
5289 // BigInt ⇄ (Number | String | Boolean | Object): compare mathematical
5290 // values (both-BigInt was already settled by the `strict_eq` above).
5291 if ta == "bigint" || tb == "bigint" {
5292 return self.bigint_loose_eq(a, b);
5293 }
5294 if ta == tb {
5295 // Same type but not strict-equal (and not nullish) ⇒ not equal.
5296 return false;
5297 }
5298 // number ⇄ string: compare as numbers.
5299 if (ta == "number" && tb == "string") || (ta == "string" && tb == "number") {
5300 return self.to_number(a) == self.to_number(b);
5301 }
5302 // boolean side coerces to number, then recompares.
5303 if ta == "boolean" {
5304 return self.loose_eq(&Value::Float(self.to_number(a)), b);
5305 }
5306 if tb == "boolean" {
5307 return self.loose_eq(a, &Value::Float(self.to_number(b)));
5308 }
5309 // object ⇄ (number|string): ToPrimitive the object (→ its string form),
5310 // then recompare as string==string or number==string.
5311 if ta == "object" && (tb == "number" || tb == "string") {
5312 let pa = self.str_of(a);
5313 return if tb == "string" {
5314 pa == self.str_of(b)
5315 } else {
5316 str_to_number(&pa) == self.to_number(b)
5317 };
5318 }
5319 if tb == "object" && (ta == "number" || ta == "string") {
5320 let pb = self.str_of(b);
5321 return if ta == "string" {
5322 self.str_of(a) == pb
5323 } else {
5324 self.to_number(a) == str_to_number(&pb)
5325 };
5326 }
5327 false
5328 }
5329
5330 /// The numeric-hook arithmetic/relational fallback for non-native operands
5331 /// (called by fusevm when at least one operand isn't `Int`/`Float`).
5332 pub fn arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5333 use NumOp::*;
5334 match op {
5335 Add => {
5336 // `+`: if either operand is a string, concatenate string forms;
5337 // otherwise numeric addition.
5338 let a_str = self.prefers_string(a);
5339 let b_str = self.prefers_string(b);
5340 if a_str || b_str {
5341 // String concatenation wins even with a bigint operand
5342 // (`1n + "x"` → `"1x"`).
5343 let s = format!("{}{}", self.str_of(a), self.str_of(b));
5344 Ok(self.new_str(s))
5345 } else if self.is_bigint_val(a) || self.is_bigint_val(b) {
5346 self.bigint_arith(op, a, b)
5347 } else {
5348 Ok(Value::Float(self.to_number(a) + self.to_number(b)))
5349 }
5350 }
5351 Sub | Mul | Div | Mod | Pow if self.is_bigint_val(a) || self.is_bigint_val(b) => {
5352 self.bigint_arith(op, a, b)
5353 }
5354 Sub => Ok(Value::Float(self.to_number(a) - self.to_number(b))),
5355 Mul => Ok(Value::Float(self.to_number(a) * self.to_number(b))),
5356 Div => Ok(Value::Float(self.to_number(a) / self.to_number(b))),
5357 Mod => Ok(Value::Float(js_mod(self.to_number(a), self.to_number(b)))),
5358 Pow => Ok(Value::Float(crate::builtins::js_pow(
5359 self.to_number(a),
5360 self.to_number(b),
5361 ))),
5362 Neg if self.is_bigint_val(a) => self.bigint_arith(op, a, b),
5363 Neg => Ok(Value::Float(-self.to_number(a))),
5364 Lt | Le | Gt | Ge => Ok(Value::Bool(self.relational(op, a, b))),
5365 Eq => Ok(Value::Bool(self.loose_eq(a, b))),
5366 Ne => Ok(Value::Bool(!self.loose_eq(a, b))),
5367 }
5368 }
5369
5370 /// Whether `v`'s primitive (`ToPrimitive` with the default hint) is a string,
5371 /// which drives `+` toward concatenation. Primitive strings qualify, and so
5372 /// do heap objects whose default `ToPrimitive` is their (string) `toString`:
5373 /// arrays (`[1,2,3]+3 → "1,2,33"`), plain objects (`{}+[] → "[object Object]"`),
5374 /// and functions. `null`/`undefined`/`boolean`/`number` do not.
5375 fn prefers_string(&self, v: &Value) -> bool {
5376 match v {
5377 Value::Str(_) => true,
5378 // A BigInt's `ToPrimitive` is the bigint itself (numeric), NOT a string,
5379 // so `1n + 2n` is bigint addition, not concatenation. `null` has no
5380 // string primitive either.
5381 Value::Obj(_) => !matches!(
5382 self.get(v),
5383 Some(JsObj::Null) | Some(JsObj::BigInt(_)) | None
5384 ),
5385 _ => false,
5386 }
5387 }
5388
5389 /// Relational comparison (`< <= > >=`) with JS coercion: string/string is
5390 /// lexicographic, otherwise numeric (NaN yields false).
5391 fn relational(&self, op: NumOp, a: &Value, b: &Value) -> bool {
5392 use std::cmp::Ordering;
5393 let ord = if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
5394 // BigInt < BigInt: exact (no f64 precision loss for large magnitudes).
5395 x.cmp(&y)
5396 } else if let (Some(x), Some(y)) = (self.as_str(a), self.as_str(b)) {
5397 // 7.2.13 IsLessThan compares CODE UNITS, which is not Rust's `str`
5398 // order once an astral character meets a BMP one — see `utf16`.
5399 crate::utf16::cmp_units(&x, &y)
5400 } else {
5401 let x = self.to_number(a);
5402 let y = self.to_number(b);
5403 match x.partial_cmp(&y) {
5404 Some(o) => o,
5405 None => return false, // NaN operand
5406 }
5407 };
5408 match op {
5409 NumOp::Lt => ord == Ordering::Less,
5410 NumOp::Le => ord != Ordering::Greater,
5411 NumOp::Gt => ord == Ordering::Greater,
5412 NumOp::Ge => ord != Ordering::Less,
5413 _ => false,
5414 }
5415 }
5416
5417 /// Bitwise/shift ops with JS ToInt32/ToUint32 semantics — or true
5418 /// arbitrary-width BigInt bitwise when both operands are BigInt (mixing a
5419 /// BigInt with a Number throws, matching Node).
5420 pub fn bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5421 if self.is_bigint_val(a) || self.is_bigint_val(b) {
5422 return self.bigint_bitwise(tag, a, b);
5423 }
5424 let x = to_int32(self.to_number(a));
5425 let y = to_int32(self.to_number(b));
5426 let r: i64 = match tag {
5427 binop::BITAND => (x & y) as i64,
5428 binop::BITOR => (x | y) as i64,
5429 binop::BITXOR => (x ^ y) as i64,
5430 binop::SHL => (x.wrapping_shl((y as u32) & 31)) as i64,
5431 binop::SHR => (x >> ((y as u32) & 31)) as i64,
5432 binop::USHR => (to_uint32(self.to_number(a)) >> ((y as u32) & 31)) as i64,
5433 _ => 0,
5434 };
5435 Ok(Value::Float(r as f64))
5436 }
5437
5438 // ── BigInt operations ────────────────────────────────────────────────────
5439 /// Whether `v` is a heap `BigInt`.
5440 pub fn is_bigint_val(&self, v: &Value) -> bool {
5441 matches!(self.get(v), Some(JsObj::BigInt(_)))
5442 }
5443 /// The `BigInt` value of `v` (a heap bigint), else `None`.
5444 pub fn as_bigint(&self, v: &Value) -> Option<num_bigint::BigInt> {
5445 match self.get(v) {
5446 Some(JsObj::BigInt(b)) => Some(b.clone()),
5447 _ => None,
5448 }
5449 }
5450 /// Allocate a heap `BigInt`.
5451 pub fn new_bigint(&mut self, b: num_bigint::BigInt) -> Value {
5452 self.alloc(JsObj::BigInt(b))
5453 }
5454
5455 /// BigInt arithmetic (`+ - * / % **`, unary `-`). Requires BOTH operands to be
5456 /// BigInt for a binary op; mixing a BigInt with a Number throws the exact Node
5457 /// `TypeError` (a string operand is handled as concatenation before we get
5458 /// here). Division/`%` truncate toward zero; `**` needs a non-negative
5459 /// exponent.
5460 fn bigint_arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5461 use num_traits::{Signed, Zero};
5462 use NumOp::*;
5463 if op == Neg {
5464 let x = self.as_bigint(a).expect("bigint_arith Neg on non-bigint");
5465 return Ok(self.new_bigint(-x));
5466 }
5467 let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5468 (Some(x), Some(y)) => (x, y),
5469 // Exactly one side is a BigInt → the other is a Number/Boolean: illegal.
5470 _ => {
5471 return Err(type_error(
5472 "Cannot mix BigInt and other types, use explicit conversions",
5473 ))
5474 }
5475 };
5476 let r = match op {
5477 Add => x + y,
5478 Sub => x - y,
5479 Mul => x * y,
5480 Div => {
5481 if y.is_zero() {
5482 return Err("RangeError: Division by zero".into());
5483 }
5484 x / y // truncates toward zero (matches JS BigInt division)
5485 }
5486 Mod => {
5487 if y.is_zero() {
5488 return Err("RangeError: Division by zero".into());
5489 }
5490 x % y // sign follows the dividend (truncated), like JS
5491 }
5492 Pow => {
5493 if y.is_negative() {
5494 return Err("RangeError: Exponent must be positive".into());
5495 }
5496 let exp = num_traits::ToPrimitive::to_u32(&y)
5497 .ok_or_else(|| "RangeError: Maximum BigInt size exceeded".to_string())?;
5498 num_traits::Pow::pow(x, exp)
5499 }
5500 _ => return Err(type_error("unsupported BigInt operation")),
5501 };
5502 Ok(self.new_bigint(r))
5503 }
5504
5505 /// BigInt bitwise (`& | ^ << >>`); `>>>` has no BigInt form. Both operands must
5506 /// be BigInt (mixing throws).
5507 fn bigint_bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
5508 let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
5509 (Some(x), Some(y)) => (x, y),
5510 _ => {
5511 return Err(type_error(
5512 "Cannot mix BigInt and other types, use explicit conversions",
5513 ))
5514 }
5515 };
5516 let r = match tag {
5517 binop::BITAND => x & y,
5518 binop::BITOR => x | y,
5519 binop::BITXOR => x ^ y,
5520 binop::SHL => {
5521 let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5522 if n >= 0 {
5523 x << (n as usize)
5524 } else {
5525 x >> ((-n) as usize)
5526 }
5527 }
5528 binop::SHR => {
5529 let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
5530 if n >= 0 {
5531 x >> (n as usize)
5532 } else {
5533 x << ((-n) as usize)
5534 }
5535 }
5536 binop::USHR => {
5537 return Err(type_error(
5538 "BigInts have no unsigned right shift, use >> instead",
5539 ))
5540 }
5541 _ => return Err(type_error("unsupported BigInt operation")),
5542 };
5543 Ok(self.new_bigint(r))
5544 }
5545
5546 /// BigInt ⇄ (Number | Boolean | String | Object) loose equality (`==`). Both
5547 /// being BigInt was already handled by `strict_eq`.
5548 fn bigint_loose_eq(&self, a: &Value, b: &Value) -> bool {
5549 // Order so `big` is the BigInt side and `other` the counterpart.
5550 let (big, other) = match (self.as_bigint(a), self.as_bigint(b)) {
5551 (Some(x), _) => (x, b),
5552 (_, Some(y)) => (y, a),
5553 _ => return false,
5554 };
5555 match other {
5556 Value::Bool(bo) => big == num_bigint::BigInt::from(*bo as i64),
5557 Value::Int(n) => big == num_bigint::BigInt::from(*n),
5558 Value::Float(f) => {
5559 // Equal only when the float is an integer with the same value.
5560 if !f.is_finite() || f.fract() != 0.0 {
5561 return false;
5562 }
5563 bigint_to_f64(&big) == *f
5564 }
5565 Value::Str(s) => match parse_bigint_str(s) {
5566 Some(bs) => big == bs,
5567 None => false,
5568 },
5569 Value::Obj(_) => match self.get(other) {
5570 // A heap string parses like a primitive string.
5571 Some(JsObj::Str(s)) => parse_bigint_str(s).map(|bs| big == bs).unwrap_or(false),
5572 _ => {
5573 // Other objects reduce via ToPrimitive (their string form).
5574 let s = self.str_of(other);
5575 parse_bigint_str(&s).map(|bs| big == bs).unwrap_or(false)
5576 }
5577 },
5578 _ => false,
5579 }
5580 }
5581}
5582
5583/// Parse a string to a BigInt under JS `StringToBigInt` rules: trimmed, empty →
5584/// `0n`, decimal or `0x`/`0o`/`0b` prefixed; any junk → `None`.
5585pub fn parse_bigint_str(s: &str) -> Option<num_bigint::BigInt> {
5586 let t = crate::utf16::js_trim(s);
5587 if t.is_empty() {
5588 return Some(num_bigint::BigInt::from(0));
5589 }
5590 let (radix, digits) = if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5591 (16, h)
5592 } else if let Some(o) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5593 (8, o)
5594 } else if let Some(bb) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5595 (2, bb)
5596 } else {
5597 (10, t)
5598 };
5599 num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
5600}
5601
5602/// Coerce a BigInt to `f64` (for `Number(bigint)` and mixed relational compares);
5603/// out-of-range magnitudes become ±Infinity, matching Node.
5604pub fn bigint_to_f64(b: &num_bigint::BigInt) -> f64 {
5605 num_traits::ToPrimitive::to_f64(b).unwrap_or_else(|| {
5606 if num_traits::Signed::is_negative(b) {
5607 f64::NEG_INFINITY
5608 } else {
5609 f64::INFINITY
5610 }
5611 })
5612}
5613
5614/// JS `%` remainder (sign follows the dividend; matches `f64::rem`).
5615fn js_mod(a: f64, b: f64) -> f64 {
5616 a % b
5617}
5618
5619/// Cycle bookkeeping for one `util.inspect` render.
5620///
5621/// `seen` is the chain of objects currently being rendered (an entry appearing
5622/// twice is a back-edge), and `refs` records every object a back-edge pointed
5623/// at, in first-encountered order — its position + 1 is the `*N` id Node prints
5624/// in `[Circular *N]` / `<ref *N>`.
5625/// How an array-shaped group is laid out, beyond its entries themselves.
5626#[derive(Clone, Copy)]
5627struct ArrayLayout<'a> {
5628 /// Extra own properties follow the elements, which suppresses grid grouping.
5629 has_props: bool,
5630 /// `output`'s last entry is the `... N more items` tail rather than a real
5631 /// element, so the grid must not size a column to it.
5632 has_tail: bool,
5633 /// A constructor tag printed before the brackets, with a trailing space
5634 /// (`"Uint8Array(3) "`), or empty for a plain array.
5635 base: &'a str,
5636}
5637
5638#[derive(Default)]
5639struct InspectCycles {
5640 seen: Vec<Value>,
5641 refs: Vec<Value>,
5642 /// The indent level of the value most recently EXPANDED — node's
5643 /// `ctx.currentDepth`. `reduceToSingleString` puts a group on one line only
5644 /// while `currentDepth - thisDepth < compact`, so without it a deeply
5645 /// nested object printed on one line where node breaks the outer levels.
5646 deepest: usize,
5647}
5648
5649impl InspectCycles {
5650 /// Record `v` as a cycle target (idempotent) and return its 1-based id.
5651 fn mark(&mut self, h: &JsHost, v: &Value) -> usize {
5652 if let Some(id) = self.id_of(h, v) {
5653 return id;
5654 }
5655 self.refs.push(v.clone());
5656 self.refs.len()
5657 }
5658
5659 /// The `*N` id already assigned to `v`, if any.
5660 fn id_of(&self, h: &JsHost, v: &Value) -> Option<usize> {
5661 self.refs
5662 .iter()
5663 .position(|p| h.strict_eq(p, v))
5664 .map(|i| i + 1)
5665 }
5666}
5667
5668thread_local! {
5669 /// The active `util.inspect` `depth` (nesting levels shown before collapsing
5670 /// to `[Object]`/`[Array]`). Node's default is 2; `util.inspect(v,{depth:N})`
5671 /// overrides it for one call, `console.log`/`util.format` use the default.
5672 /// Signed, because `util.inspect(v, { depth: -1 })` is legal and means
5673 /// "already past the limit" — everything collapses to `[Object]` at the top
5674 /// level. Held as `usize` it read as an enormous depth and expanded fully.
5675 static INSPECT_MAX_DEPTH: std::cell::Cell<i64> = const { std::cell::Cell::new(2) };
5676
5677 /// `util.inspect`'s `compact` option. Node's default is the NUMBER 3: a
5678 /// group is put on one line only when the subtree below it is shallower
5679 /// than this. `compact: false` is held as 0, which no subtree depth is
5680 /// below, so every group breaks — which is exactly what node does.
5681 static INSPECT_COMPACT: std::cell::Cell<i64> = const { std::cell::Cell::new(DEFAULT_COMPACT) };
5682
5683 /// `util.inspect`'s `breakLength`. Node's default is 128, but `util.inspect`
5684 /// itself passes 80.
5685 static INSPECT_BREAK_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(80) };
5686
5687 /// `util.inspect`'s `sorted` option: emit an object's own keys in code-unit
5688 /// order instead of insertion order. Off by default. `assert`'s diff renderer
5689 /// turns it on so that two objects built with the same keys in a different
5690 /// order diff as equal rather than as a wholesale rewrite.
5691 static INSPECT_SORTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5692
5693 /// `util.inspect`'s `maxArrayLength`: how many entries are formatted before
5694 /// the rest collapse into `... N more items`. Node's default is 100;
5695 /// `Infinity`/`null` means "all", held here as `usize::MAX`.
5696 static INSPECT_MAX_ARRAY_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(DEFAULT_MAX_ARRAY_LENGTH) };
5697
5698 /// `util.inspect`'s `customInspect` option: whether a value's own
5699 /// `[util.inspect.custom]` rendering is used. On by default; `assert` turns
5700 /// it off so a diff shows an object's real structure rather than whatever
5701 /// summary it prefers to print.
5702 static INSPECT_CUSTOM: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
5703
5704 /// `util.inspect`'s `showHidden`: reveal the non-enumerable slots a value
5705 /// carries — an array's `length`, a typed array's element width and window
5706 /// onto its backing store. Off by default; `util.format`'s `%o` turns it on.
5707 static INSPECT_SHOW_HIDDEN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5708}
5709
5710/// Set the `util.inspect` `showHidden` option for the next render.
5711pub fn set_inspect_show_hidden(s: bool) {
5712 INSPECT_SHOW_HIDDEN.with(|x| x.set(s));
5713}
5714
5715pub(crate) fn inspect_show_hidden() -> bool {
5716 INSPECT_SHOW_HIDDEN.with(|x| x.get())
5717}
5718
5719/// Set the `util.inspect` `customInspect` option for the next render.
5720pub fn set_inspect_custom(c: bool) {
5721 INSPECT_CUSTOM.with(|x| x.set(c));
5722}
5723
5724pub(crate) fn inspect_custom() -> bool {
5725 INSPECT_CUSTOM.with(|x| x.get())
5726}
5727
5728/// Set the `util.inspect` `sorted` option for the next render.
5729pub fn set_inspect_sorted(s: bool) {
5730 INSPECT_SORTED.with(|x| x.set(s));
5731}
5732
5733pub(crate) fn inspect_sorted() -> bool {
5734 INSPECT_SORTED.with(|x| x.get())
5735}
5736
5737/// Set the `util.inspect` `maxArrayLength` for the next render.
5738pub fn set_inspect_max_array_length(n: usize) {
5739 INSPECT_MAX_ARRAY_LENGTH.with(|x| x.set(n));
5740}
5741
5742pub(crate) fn inspect_max_array_length() -> usize {
5743 INSPECT_MAX_ARRAY_LENGTH.with(|x| x.get())
5744}
5745
5746/// Set the `util.inspect` `compact` option for the next render (0 for `false`).
5747pub fn set_inspect_compact(c: i64) {
5748 INSPECT_COMPACT.with(|x| x.set(c));
5749}
5750
5751/// Set the `util.inspect` `breakLength` for the next render.
5752pub fn set_inspect_break_length(n: usize) {
5753 INSPECT_BREAK_LENGTH.with(|x| x.set(n));
5754}
5755
5756fn inspect_compact() -> i64 {
5757 INSPECT_COMPACT.with(|x| x.get())
5758}
5759
5760/// Set the `util.inspect` depth for the next render (restore to 2 after).
5761pub fn set_inspect_max_depth(d: i64) {
5762 INSPECT_MAX_DEPTH.with(|c| c.set(d));
5763}
5764/// Twice the configured depth, which is what the inspect walk compares its
5765/// indent against. Saturating, because `util.inspect(x, { depth: null })` and
5766/// `{ depth: Infinity }` both set the depth to `usize::MAX`, and doubling that
5767/// overflowed and panicked the process — an abort no script could catch.
5768fn inspect_indent_limit() -> i64 {
5769 inspect_max_depth().saturating_mul(2)
5770}
5771
5772fn inspect_max_depth() -> i64 {
5773 INSPECT_MAX_DEPTH.with(|c| c.get())
5774}
5775
5776/// ECMA-262 `ToInt32` (7.1.6): truncate toward zero, reduce modulo 2^32, then
5777/// reinterpret as signed.
5778///
5779/// The reduction has to happen in `f64`, not by casting through `i64`. Rust
5780/// saturates an out-of-range float-to-int cast, so `1e300 as i64` is `i64::MAX`
5781/// and `1e300 | 0` came out `-1` where every engine says `0`; the same
5782/// saturation made `1e300 >>> 0` report `4294967295`. `rem_euclid` on a
5783/// power-of-two modulus is exact for every finite double, so this is the whole
5784/// fix — and it is the form `Math.clz32` already used.
5785pub(crate) fn to_int32(f: f64) -> i32 {
5786 to_uint32(f) as i32
5787}
5788pub(crate) fn to_uint32(f: f64) -> u32 {
5789 if !f.is_finite() {
5790 return 0;
5791 }
5792 f.trunc().rem_euclid(4294967296.0) as u32
5793}
5794
5795/// Parse a string in numeric context (`ToNumber`): trimmed, empty -> 0.
5796fn str_to_number(s: &str) -> f64 {
5797 let t = crate::utf16::js_trim(s);
5798 if t.is_empty() {
5799 return 0.0;
5800 }
5801 if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
5802 return i64::from_str_radix(hex, 16)
5803 .map(|n| n as f64)
5804 .unwrap_or(f64::NAN);
5805 }
5806 if let Some(oct) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
5807 return i64::from_str_radix(oct, 8)
5808 .map(|n| n as f64)
5809 .unwrap_or(f64::NAN);
5810 }
5811 if let Some(bin) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
5812 return i64::from_str_radix(bin, 2)
5813 .map(|n| n as f64)
5814 .unwrap_or(f64::NAN);
5815 }
5816 match t {
5817 "Infinity" | "+Infinity" => f64::INFINITY,
5818 "-Infinity" => f64::NEG_INFINITY,
5819 _ => t.parse::<f64>().unwrap_or(f64::NAN),
5820 }
5821}
5822
5823/// `util.inspect` break length (the width past which entries wrap). Node's default.
5824fn break_length() -> usize {
5825 INSPECT_BREAK_LENGTH.with(|x| x.get())
5826}
5827/// Node's default `compact` setting (the `compact * 4` column cap term).
5828/// Node's DEFAULT `compact` setting, and the initial value of
5829/// `INSPECT_COMPACT`. The grid's column cap is `compact * 4`, so it has to be
5830/// read through `inspect_compact()` at render time: under `{ compact: 1 }` node
5831/// lays a byte array out four columns wide, and the hardcoded 3 gave twelve.
5832const DEFAULT_COMPACT: i64 = 3;
5833/// Node's default `maxArrayLength` — the initial value of
5834/// `INSPECT_MAX_ARRAY_LENGTH`, which `util.inspect(v, { maxArrayLength: N })`
5835/// overrides per call. Read it through `inspect_max_array_length()`, never
5836/// directly: as a bare constant the option had no effect and a 120-element array
5837/// was truncated at 100 even under `maxArrayLength: Infinity`.
5838pub(crate) const DEFAULT_MAX_ARRAY_LENGTH: usize = 100;
5839
5840/// Whether `output` fits on a single line — a faithful port of Node's
5841/// `isBelowBreakLength` (no colors, no `base`). `start` is the caller's seed
5842/// length (braces + indentation + slack).
5843fn is_below_break_length(output: &[String], start: usize) -> bool {
5844 let limit = break_length();
5845 let mut total = output.len() + start;
5846 if total + output.len() > limit {
5847 return false;
5848 }
5849 for o in output {
5850 if o.contains('\n') {
5851 return false;
5852 }
5853 total += o.chars().count();
5854 if total > limit {
5855 return false;
5856 }
5857 }
5858 true
5859}
5860
5861/// Faithful port of Node's `util.inspect` `groupArrayElements`: lay out the
5862/// already-formatted element strings into an aligned multi-column grid. Returns
5863/// `(lines, grouped)` — `grouped` is false when Node would leave the output
5864/// ungrouped (so the caller falls back to single-line / one-per-line).
5865fn group_array_elements(
5866 host: &JsHost,
5867 output: &[String],
5868 values: &[Value],
5869 indentation_lvl: usize,
5870 has_tail: bool,
5871) -> (Vec<String>, bool) {
5872 let separator_space = 2usize; // ", " between entries
5873 // A `... N more items` tail is not an element: node drops it from the grid
5874 // (`outputLength--`) so it neither widens a column nor occupies a cell, then
5875 // re-appends it as its own final line.
5876 let output_length = output.len() - usize::from(has_tail);
5877 let data_len: Vec<usize> = output.iter().map(|o| o.chars().count()).collect();
5878 let mut total_length = 0usize;
5879 let mut max_length = 0usize;
5880 for &len in &data_len[..output_length] {
5881 total_length += len + separator_space;
5882 if len > max_length {
5883 max_length = len;
5884 }
5885 }
5886 let actual_max = max_length + separator_space;
5887 // Only group when ≥3 entries fit across AND the entries aren't wildly uneven.
5888 if !(actual_max * 3 + indentation_lvl < break_length()
5889 && (total_length as f64 / actual_max as f64 > 5.0 || max_length <= 6))
5890 {
5891 return (output.to_vec(), false);
5892 }
5893 let approx_char_heights = 2.5f64;
5894 let average_bias = (actual_max as f64 - total_length as f64 / output_length as f64).sqrt();
5895 let biased_max = (actual_max as f64 - 3.0 - average_bias).max(1.0);
5896 // Ideally a square grid; capped by break length, compact*4, and 15 columns.
5897 let columns = [
5898 ((approx_char_heights * biased_max * output_length as f64).sqrt() / biased_max).round()
5899 as i64,
5900 ((break_length() - indentation_lvl) as f64 / actual_max as f64).floor() as i64,
5901 inspect_compact().saturating_mul(4),
5902 15,
5903 ]
5904 .into_iter()
5905 .min()
5906 .unwrap();
5907 if columns <= 1 {
5908 return (output.to_vec(), false);
5909 }
5910 let columns = columns as usize;
5911 // The widest entry (plus separator) in each column.
5912 let mut max_line_length = vec![0usize; columns];
5913 for (i, slot) in max_line_length.iter_mut().enumerate() {
5914 let mut line_length = 0;
5915 let mut j = i;
5916 while j < output_length {
5917 if data_len[j] > line_length {
5918 line_length = data_len[j];
5919 }
5920 j += columns;
5921 }
5922 *slot = line_length + separator_space;
5923 }
5924 // Right-align (padStart) only when every element is a number/bigint.
5925 let pad_start = values.iter().all(|v| {
5926 matches!(v, Value::Int(_) | Value::Float(_))
5927 || matches!(host.get(v), Some(JsObj::BigInt(_)))
5928 });
5929 let mut tmp = Vec::new();
5930 let mut i = 0;
5931 while i < output_length {
5932 let max = (i + columns).min(output_length);
5933 let mut str_line = String::new();
5934 let mut j = i;
5935 while j < max.saturating_sub(1) {
5936 // `output[j]` has no colors here, so padding == max_line_length[col].
5937 let col = j - i;
5938 let cell = format!("{}, ", output[j]);
5939 let target = max_line_length[col];
5940 str_line.push_str(&pad_to(&cell, target, pad_start));
5941 j += 1;
5942 }
5943 // The last cell of the row: right-aligned entries pad without the ", ".
5944 if pad_start {
5945 let col = j - i;
5946 let target = max_line_length[col] - separator_space;
5947 str_line.push_str(&pad_to(&output[j], target, true));
5948 } else {
5949 str_line.push_str(&output[j]);
5950 }
5951 tmp.push(str_line);
5952 i += columns;
5953 }
5954 if has_tail {
5955 tmp.push(output[output_length].clone());
5956 }
5957 (tmp, true)
5958}
5959
5960/// Pad `s` to `width` chars: right-justified when `pad_start`, else left-justified.
5961/// (Padding is measured in chars; already ANSI-free here.)
5962fn pad_to(s: &str, width: usize, pad_start: bool) -> String {
5963 let len = s.chars().count();
5964 if len >= width {
5965 return s.to_string();
5966 }
5967 let fill = " ".repeat(width - len);
5968 if pad_start {
5969 format!("{fill}{s}")
5970 } else {
5971 format!("{s}{fill}")
5972 }
5973}
5974
5975/// Quote a string the way `util.inspect` does — a port of `strEscape` in Node's
5976/// `lib/internal/util/inspect.js`.
5977///
5978/// The quote character is chosen so the contents need as little escaping as
5979/// possible: single quotes normally, double quotes when the string contains a
5980/// `'` but no `"`, and a backtick when it contains both (and neither a backtick
5981/// nor a `${`). Only the ACTIVE quote is backslash-escaped, alongside `\` and
5982/// the C0 controls + DEL, which use Node's `meta` table (`\n`, `\t`, `\b`,
5983/// `\f`, `\r` short forms; `\x0B`, `\x1F`, `\x7F` uppercase-hex otherwise).
5984fn quote_str(s: &str) -> String {
5985 let quote = if !s.contains('\'') {
5986 '\''
5987 } else if !s.contains('"') {
5988 '"'
5989 } else if !s.contains('`') && !s.contains("${") {
5990 '`'
5991 } else {
5992 '\''
5993 };
5994 let mut out = String::with_capacity(s.len() + 2);
5995 out.push(quote);
5996 for c in s.chars() {
5997 match c {
5998 _ if c == quote => {
5999 out.push('\\');
6000 out.push(c);
6001 }
6002 '\\' => out.push_str("\\\\"),
6003 '\u{8}' => out.push_str("\\b"),
6004 '\t' => out.push_str("\\t"),
6005 '\n' => out.push_str("\\n"),
6006 '\u{c}' => out.push_str("\\f"),
6007 '\r' => out.push_str("\\r"),
6008 '\u{0}'..='\u{1f}' | '\u{7f}' => out.push_str(&format!("\\x{:02X}", c as u32)),
6009 _ => out.push(c),
6010 }
6011 }
6012 out.push(quote);
6013 out
6014}
6015
6016/// Render an object key: bare if it is a valid identifier, quoted otherwise.
6017fn fmt_key(k: &str) -> String {
6018 let ok = !k.is_empty()
6019 && k.chars()
6020 .next()
6021 .map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
6022 .unwrap_or(false)
6023 && k.chars()
6024 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
6025 if ok {
6026 k.to_string()
6027 } else {
6028 quote_str(k)
6029 }
6030}
6031
6032// ── iteration ────────────────────────────────────────────────────────────────
6033
6034impl JsHost {
6035 /// Collect an iterable into a vector of values (arrays, strings, Map/Set).
6036 /// Generators and user `Symbol.iterator` objects go through `iter_all`, which
6037 /// holds no host borrow across resumes.
6038 pub fn iter_vec(&mut self, v: &Value) -> Result<Vec<Value>, String> {
6039 match self.get(v) {
6040 Some(JsObj::Array(items)) => Ok(items.clone()),
6041 Some(JsObj::Str(s)) => {
6042 let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
6043 Ok(chars.into_iter().map(|c| self.new_str(c)).collect())
6044 }
6045 // A live array iterator, read from where its cursor stands. The
6046 // backing slots only: `iter_all` steps one through `[[Get]]` instead.
6047 Some(JsObj::Iter {
6048 idx,
6049 array: Some((arr, kind)),
6050 ..
6051 }) => {
6052 let (idx, kind) = (*idx, *kind);
6053 let items = match self.get(arr) {
6054 Some(JsObj::Array(items)) if idx < items.len() => items[idx..].to_vec(),
6055 _ => Vec::new(),
6056 };
6057 Ok(items
6058 .into_iter()
6059 .enumerate()
6060 .map(|(n, v)| {
6061 let key = Value::Float((idx + n) as f64);
6062 match kind {
6063 ArrayIterKind::Keys => key,
6064 ArrayIterKind::Values => v,
6065 ArrayIterKind::Entries => self.new_array(vec![key, v]),
6066 }
6067 })
6068 .collect())
6069 }
6070 Some(JsObj::Iter { items, idx, .. }) => Ok(items[*idx..].to_vec()),
6071 Some(JsObj::Set { entries, .. }) => Ok(entries.values().cloned().collect()),
6072 Some(JsObj::Map { entries, .. }) => {
6073 // Map iterates as `[key, value]` pairs.
6074 let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
6075 Ok(pairs
6076 .into_iter()
6077 .map(|(k, v)| self.new_array(vec![k, v]))
6078 .collect())
6079 }
6080 // A `Buffer` iterates over its BYTES and a typed array over its
6081 // ELEMENTS — both are iterable in Node. Only `@@bytes` was handled
6082 // here, so `[...buf]` worked while `[...new Uint8Array([1])]` threw
6083 // "object is not iterable", which is the same invariant holding at
6084 // one of its two sites.
6085 Some(JsObj::Object(props))
6086 if props.contains_key("@@bytes") || props.contains_key("@@buffer") =>
6087 {
6088 // Iterating a view over a DETACHED buffer throws, naming the
6089 // `values` iterator — reading its elements answers zero length,
6090 // but spreading it is a method call and does not.
6091 if crate::stdlib::typedarray::view_detached_h(self, v) {
6092 return Err(crate::stdlib::typedarray::detached_error(
6093 "%TypedArray%.prototype",
6094 "values",
6095 false,
6096 ));
6097 }
6098 Ok(crate::stdlib::typedarray::elems_mut_host(self, v))
6099 }
6100 // V8 names the VALUE, not its type: `[...5]` is `5 is not iterable`,
6101 // `[...{}]` is `{} is not iterable`. Reporting `typeof` instead
6102 // produced `number is not iterable`, which no engine emits.
6103 _ => {
6104 let shown = self.inspect(v);
6105 Err(type_error(&format!("{shown} is not iterable")))
6106 }
6107 }
6108 }
6109
6110 /// Enumerable string keys of an object/array (for `for-in`). Internal
6111 /// symbol-keyed props (`@@…`) are not enumerable.
6112 /// `for-in` visits own enumerable keys, then every *inherited* enumerable key
6113 /// not already seen, walking the whole prototype chain. Class methods and the
6114 /// builtin prototypes are non-enumerable, so in practice this only surfaces
6115 /// keys a script put on a prototype itself (`F.prototype.y = 2`) — but that
6116 /// is exactly the constructor-function idiom older packages are written in.
6117 pub fn enum_keys(&mut self, v: &Value) -> Vec<Value> {
6118 let mut keys = self.own_enum_key_names(v);
6119 let mut cur = self.proto_of(v);
6120 let mut hops = 0;
6121 while let Some(p) = cur {
6122 // A cyclic or pathologically deep chain must not hang the loop.
6123 hops += 1;
6124 if hops > 100 || matches!(p, Value::Undef) || self.is_null(&p) {
6125 break;
6126 }
6127 for k in self.own_enum_key_names(&p) {
6128 if !keys.contains(&k) {
6129 keys.push(k);
6130 }
6131 }
6132 cur = self.proto_of(&p);
6133 }
6134 keys.into_iter().map(|k| self.new_str(k)).collect()
6135 }
6136
6137 /// The own *enumerable* string keys of `v`, in property order — the single
6138 /// source of truth behind `for-in`, `Object.keys`/`values`/`entries`,
6139 /// object spread, `Object.assign` and `JSON.stringify`. Internal slots
6140 /// (`@@…`), private fields (`#…`) and anything marked non-enumerable via
6141 /// `prop_attrs` are excluded.
6142 pub fn own_enum_key_names(&self, v: &Value) -> Vec<String> {
6143 self.own_key_names(v, true)
6144 }
6145
6146 /// Own string keys of `v` in insertion order. `enum_only` drops the
6147 /// non-enumerable ones (`Object.keys`); otherwise every own key is reported
6148 /// (`getOwnPropertyNames`/`Reflect.ownKeys`).
6149 pub fn own_key_names(&self, v: &Value, enum_only: bool) -> Vec<String> {
6150 let mut keys = self.own_enum_data_keys(v, enum_only);
6151 // A global a SCRIPT created (`x = 1` with no declaration) is an own
6152 // ENUMERABLE property of the global object, but lives in the globals map
6153 // rather than in its property map — so no listing saw it, while
6154 // `globalThis.x` read it back and its descriptor called it enumerable.
6155 if self.is_global_object(v) {
6156 for k in self.globals.keys() {
6157 if !keys.contains(k) {
6158 keys.push(k.clone());
6159 }
6160 }
6161 }
6162 // A RegExp's `lastIndex` is a SYNTHESIZED own property — it lives in the
6163 // `RegExpObj` struct, not a property map — so nothing above can list it.
6164 // Non-enumerable, so only `getOwnPropertyNames` sees it.
6165 if !enum_only && matches!(self.get(v), Some(JsObj::RegExp(_))) {
6166 keys.push("lastIndex".to_string());
6167 }
6168 // An accessor defined before its object had any ordering marker (a class
6169 // prototype accessor, say) still has to appear.
6170 for k in self.own_accessor_keys(v) {
6171 if (!enum_only || self.prop_attrs(v, &k).enumerable) && !keys.contains(&k) {
6172 keys.push(k);
6173 }
6174 }
6175 keys
6176 }
6177
6178 /// The keys that own a slot in the object's property map, in insertion
6179 /// order, resolving accessor ordering markers back to their real key.
6180 /// Every global a SCRIPT created, in creation order — the own enumerable
6181 /// keys of the global object that live in the globals map rather than in
6182 /// its property map. `x = 1` with no declaration makes one, and
6183 /// `Object.keys(globalThis)` reports it in node.
6184 pub fn script_global_names(&self) -> Vec<String> {
6185 self.globals.keys().cloned().collect()
6186 }
6187 /// Drop a global a script created. Reports whether it was there.
6188 pub fn remove_global(&mut self, name: &str) -> bool {
6189 self.globals.shift_remove(name).is_some()
6190 }
6191 fn own_enum_data_keys(&self, v: &Value, enum_only: bool) -> Vec<String> {
6192 match self.get(v) {
6193 // A `Buffer` is an index-keyed exotic: its own enumerable keys are
6194 // `"0".."len-1"` (the bytes live in the hidden `@@bytes` slot), never
6195 // the `length`/`byteLength` view metadata, which V8 keeps on the
6196 // prototype chain or as non-enumerable own slots.
6197 // A `Buffer` and every other typed array are index-keyed exotics:
6198 // their own enumerable keys are `"0".."len-1"` (the elements live in
6199 // a hidden slot), never the `length`/`byteLength` view metadata,
6200 // which V8 keeps on the prototype chain or as non-enumerable own
6201 // slots. Only `Buffer` had this arm, so `Object.keys(u8)` was empty
6202 // and `JSON.stringify(u8)` was `{}` where node gives
6203 // `{"0":10,"1":9}` — `hasOwnProperty(0)` already answered true, so
6204 // the two views of the same question disagreed.
6205 Some(JsObj::Object(props))
6206 if matches!(
6207 props.get("@@native").map(|t| self.str_of(t)).as_deref(),
6208 Some("Buffer") | Some("TypedArray")
6209 ) =>
6210 {
6211 // A view over a DETACHED buffer has no index properties at all:
6212 // its own `length` still holds the old count, so reading that
6213 // back left `Object.keys` listing eight names over no bytes.
6214 if crate::stdlib::typedarray::view_detached_h(self, v) {
6215 return Vec::new();
6216 }
6217 // A Buffer counts its byte store; every other view reports the
6218 // element count of its window onto the ArrayBuffer.
6219 let n = match props.get("@@bytes").and_then(|b| self.get(b)) {
6220 Some(JsObj::Array(items)) => items.len(),
6221 _ => props
6222 .get("length")
6223 .map(|l| self.to_number(l))
6224 .unwrap_or(0.0) as usize,
6225 };
6226 (0..n).map(|i| i.to_string()).collect()
6227 }
6228 Some(JsObj::Object(props)) => props
6229 .keys()
6230 .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6231 Some(real) => Some(real.to_string()),
6232 None if !k.starts_with("@@") && !k.starts_with('#') => Some(k.clone()),
6233 None => None,
6234 })
6235 .filter(|k| !enum_only || self.prop_attrs(v, k).enumerable)
6236 .collect(),
6237 // A STRING is an index-keyed exotic too (10.4.3): its own keys are
6238 // its UTF-16 code-unit indices, plus the non-enumerable `length`.
6239 // Without this arm every whole-object view of a string primitive was
6240 // empty — `for (const k in 'ab')` iterated nothing, `Object.keys`
6241 // and `Object.assign({}, 'ab')` reported `{}` — while `'ab'[0]` and
6242 // `'ab'.length` answered normally, so the two views disagreed. The
6243 // spread form `{...'ab'}` went through a different path and was
6244 // already right, which is what made the gap easy to miss.
6245 Some(JsObj::Str(s)) => {
6246 let mut keys: Vec<String> =
6247 (0..crate::utf16::len(s)).map(|i| i.to_string()).collect();
6248 if !enum_only {
6249 keys.push("length".into());
6250 }
6251 keys
6252 }
6253 // `OrdinaryOwnPropertyKeys` on an array exotic: the integer indices
6254 // ascending, then the exotic non-enumerable `length`, then the
6255 // ordinary string keys in insertion order. Those ordinary keys have
6256 // no property map to live in — a `str.match()` result's
6257 // `index`/`input`/`groups` and any user-assigned `arr.foo` are kept
6258 // in the fn-prop side table — so they are read back from there.
6259 Some(JsObj::Array(items)) => {
6260 // An ELIDED element is not an own property at all, so it
6261 // contributes no key — the difference behind
6262 // `Object.keys([1,,3])` being `['0','2']`.
6263 let mut keys: Vec<String> = (0..items.len())
6264 .filter(|i| !self.is_hole(v, *i))
6265 .map(|i| i.to_string())
6266 .collect();
6267 if !enum_only {
6268 keys.push("length".into());
6269 }
6270 keys.extend(self.fn_prop_keys(v).into_iter().filter(|k| {
6271 !k.starts_with("@@")
6272 && !k.starts_with('#')
6273 && (!enum_only || self.prop_attrs(v, k).enumerable)
6274 }));
6275 keys
6276 }
6277 // A function/class keeps every own property in the side table. Its
6278 // exotic `name`/`length`/`prototype` and its class methods are all
6279 // non-enumerable, so under `enum_only` what is left is exactly what
6280 // a script assigned; `getOwnPropertyNames` reports the exotics too,
6281 // in V8's order (`length`, `name`, `prototype`, then the rest).
6282 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
6283 let mut keys: Vec<String> = Vec::new();
6284 if !enum_only {
6285 keys.push("length".into());
6286 keys.push("name".into());
6287 if self.owns_prototype(v) {
6288 keys.push("prototype".into());
6289 }
6290 }
6291 let rest: Vec<String> = self
6292 .fn_prop_keys(v)
6293 .into_iter()
6294 // An accessor's ordering marker resolves back to its real
6295 // key, so a static getter enumerates where it was declared.
6296 .filter_map(|k| match k.strip_prefix(ORD_MARKER) {
6297 Some(real) => Some(real.to_string()),
6298 None if !k.starts_with("@@") && !k.starts_with('#') => Some(k),
6299 None => None,
6300 })
6301 .filter(|k| {
6302 !keys.contains(k) && (!enum_only || self.prop_attrs(v, k).enumerable)
6303 })
6304 .collect();
6305 keys.extend(rest);
6306 keys
6307 }
6308 // A builtin namespace (`require('buffer')`, `Buffer`) enumerates the
6309 // members node-js implements, so a package that copies a namespace
6310 // key-by-key gets the working set instead of an empty object.
6311 Some(JsObj::Builtin(ns)) => crate::stdlib::namespace_keys(&ns.clone()),
6312 // A `Map`/`Set`/`Promise`/`RegExp`/generator holds only its internal
6313 // slots, so what a script assigned lives in the side table — and is
6314 // just as much an own property as an object's.
6315 Some(_) => self
6316 .fn_prop_keys(v)
6317 .into_iter()
6318 .filter(|k| {
6319 !k.starts_with("@@")
6320 && !k.starts_with('#')
6321 && (!enum_only || self.prop_attrs(v, k).enumerable)
6322 })
6323 .collect(),
6324 _ => Vec::new(),
6325 }
6326 }
6327
6328 /// The own enumerable `(key, value)` pairs of `v`. Buffer index keys resolve
6329 /// through the byte store; everything else reads the property map. Own
6330 /// accessor keys come back as `Undef` here — `own_enum_entries_deep` runs
6331 /// their getters, which cannot happen under the host borrow.
6332 pub fn own_enum_entries(&self, v: &Value) -> Vec<(String, Value)> {
6333 self.own_enum_key_names(v)
6334 .into_iter()
6335 .map(|k| {
6336 let val = match self.get(v) {
6337 // A Buffer's index keys read out of the hidden `@@bytes`
6338 // array; resolve inline rather than through
6339 // `buffer::byte_get`, which would re-borrow the host.
6340 Some(JsObj::Object(props)) => props.get(&k).cloned().unwrap_or_else(|| {
6341 // A Buffer's elements live in `@@bytes` and every
6342 // other typed array's in `@@elems`; both are index
6343 // keys with no entry in the property map.
6344 match k.parse::<usize>() {
6345 Ok(i) => crate::stdlib::typedarray::elems_with_host(self, v)
6346 .get(i)
6347 .cloned()
6348 .unwrap_or(Value::Undef),
6349 _ => Value::Undef,
6350 }
6351 }),
6352 // A Map/Set/Promise/RegExp/generator keeps every own
6353 // property in the side table.
6354 Some(
6355 JsObj::Map { .. }
6356 | JsObj::Set { .. }
6357 | JsObj::Promise { .. }
6358 | JsObj::RegExp(_)
6359 | JsObj::Generator { .. }
6360 | JsObj::Symbol { .. }
6361 | JsObj::BigInt(_)
6362 | JsObj::Iter { .. },
6363 ) => self.fn_prop(v, &k).unwrap_or(Value::Undef),
6364 // An index reads the element; any other own key (`foo`,
6365 // a match result's `index`) lives in the side table.
6366 Some(JsObj::Array(items)) => k
6367 .parse::<usize>()
6368 .ok()
6369 .and_then(|i| items.get(i).cloned())
6370 .or_else(|| self.fn_prop(v, &k))
6371 .unwrap_or(Value::Undef),
6372 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
6373 self.fn_prop(v, &k).unwrap_or(Value::Undef)
6374 }
6375 _ => Value::Undef,
6376 };
6377 (k, val)
6378 })
6379 .collect()
6380 }
6381}
6382
6383/// The own enumerable `(key, value)` pairs of `v` with every enumerable own
6384/// accessor's getter invoked — the observable shape `Object.values`,
6385/// `Object.entries`, object spread and `JSON.stringify` all need. Must be called
6386/// outside a `with_host` borrow because a getter re-enters the host.
6387pub fn own_enum_entries_deep(v: &Value) -> Result<Vec<(String, Value)>, String> {
6388 // A Proxy has no property map at all: its own enumerable entries come from
6389 // the `ownKeys` + `getOwnPropertyDescriptor` + `get` traps. A trap that
6390 // throws surfaces as an empty result here because this signature is
6391 // infallible; the callers that MUST propagate a trap throw (`Object.keys`
6392 // and friends) go through `builtins::object_keys`, which does.
6393 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
6394 return crate::proxy::own_enum_entries(v);
6395 }
6396 // A builtin namespace (`require('path')`, `Buffer`) has no property map at
6397 // all: its members are resolved on demand by `namespace_property`, which
6398 // re-enters the host and so cannot run inside `own_enum_entries`'s borrow.
6399 // Without this, spread and `Object.assign` copied the namespace's KEYS with
6400 // `undefined` for every value — measured against node v26.7.0,
6401 // `{...require('path')}.join` was `undefined` here and a function there,
6402 // while `Object.entries(require('path'))` (which resolves through
6403 // `builtins`, not through this borrow) was already correct. Two enumeration
6404 // paths, one of them silently value-less.
6405 if let Some(ns) = with_host(|h| match h.get(v) {
6406 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6407 _ => None,
6408 }) {
6409 return Ok(with_host(|h| h.own_enum_key_names(v))
6410 .into_iter()
6411 .map(|k| {
6412 let val = crate::builtins::namespace_property(&ns, &k);
6413 (k, val)
6414 })
6415 .collect());
6416 }
6417 // A string primitive's own entries are its code units. `own_enum_entries`
6418 // cannot build them: allocating the one-character string for each index
6419 // needs `&mut` host access, and it runs under a shared borrow.
6420 if let Some(sv) = with_host(|h| match h.get(v) {
6421 Some(JsObj::Str(s)) => Some(s.clone()),
6422 _ => None,
6423 }) {
6424 let units = crate::utf16::Units::of(&sv);
6425 return Ok(with_host(|h| {
6426 (0..units.len())
6427 .filter_map(|i| units.unit_str(i).map(|c| (i.to_string(), h.new_str(c))))
6428 .collect()
6429 }));
6430 }
6431 let accessor_keys: Vec<String> = with_host(|h| {
6432 h.own_accessor_keys(v)
6433 .into_iter()
6434 .filter(|k| h.prop_attrs(v, k).enumerable)
6435 .collect()
6436 });
6437 let entries = with_host(|h| h.own_enum_entries(v));
6438 // A getter that THROWS propagates: `Object.entries`, `Object.assign`,
6439 // object spread and `JSON.stringify` all read through here, and every one
6440 // of them swallowed the exception and reported the property as absent (or
6441 // as `null`) instead.
6442 let mut out = Vec::with_capacity(entries.len());
6443 for (k, val) in entries {
6444 if accessor_keys.contains(&k) {
6445 out.push((k.clone(), get_prop_chain(v, &k)?));
6446 } else {
6447 out.push((k, val));
6448 }
6449 }
6450 Ok(out)
6451}
6452
6453// ── function invocation ──────────────────────────────────────────────────────
6454
6455/// Marshal a JS call argument into a native fusevm `Value` for `rust { }` FFI.
6456/// JS strings ride as `Value::Obj(JsObj::Str)` heap handles, which fusevm's
6457/// marshaller cannot read (it calls `Value::to_str`, which returns `"(obj:N)"`
6458/// for a handle); rewrite them to a native `Value::Str`. Numbers are already
6459/// native `Value::Int`/`Value::Float`, so they pass through (fusevm coerces
6460/// Float→i64/f64 per the export signature).
6461fn marshal_ffi_arg(v: &Value) -> Value {
6462 match v {
6463 Value::Obj(_) => match with_host(|h| h.as_str(v)) {
6464 Some(s) => Value::str(s),
6465 None => v.clone(),
6466 },
6467 _ => v.clone(),
6468 }
6469}
6470
6471/// Resolve a bare name and call it (`f(args)`, `parseInt(args)`).
6472pub fn call_named(name: &str, args: Vec<Value>) -> Result<Value, String> {
6473 // Inline Rust FFI: the `rust { ... }` desugar emits `__rust_compile(b64,
6474 // line)`; compile + register the block's exported functions, returning JS
6475 // `undefined` (`Value::Undef`).
6476 if name == "__rust_compile" {
6477 let b64 = args
6478 .first()
6479 .map(|v| with_host(|h| h.str_of(v)))
6480 .unwrap_or_default();
6481 return fusevm::ffi::compile_and_register(&b64).map(|_| Value::Undef);
6482 }
6483 if let Some(v) = with_host(|h| h.read_name(name)) {
6484 return invoke(&v, args, None);
6485 }
6486 // A DIRECT eval — the literal `eval(src)` call form — is the ONLY one that
6487 // evaluates in the CALLER's scope; `(0, eval)(src)`, `const e = eval; e(src)`
6488 // and `[eval][0](src)` all reach the same function value but are INDIRECT
6489 // evals and evaluate in the global scope (ECMA-262 19.2.1.1 `PerformEval`).
6490 // This is the one place the two forms are distinguishable without a compiler
6491 // change: `call_named` is reached only from `ops::CALL`, which the compiler
6492 // emits exclusively for a bare-identifier callee, while every value-call form
6493 // goes through `invoke` → `call_builtin_function`. The `read_name` miss above
6494 // has already established that `eval` is not shadowed by a user binding.
6495 if name == "eval" {
6496 return crate::builtins::eval_source(args.first(), true);
6497 }
6498 if crate::builtins::is_known_builtin(name) {
6499 return crate::builtins::call_builtin_function(name, args);
6500 }
6501 // A `rust { ... }` block's exported functions are callable by bareword.
6502 // Reached only after user names/globals and builtins all miss, so JS code
6503 // always wins; the registry membership check keeps this off the hot path.
6504 if fusevm::ffi::is_registered(name) {
6505 let margs: Vec<Value> = args.iter().map(marshal_ffi_arg).collect();
6506 if let Some(r) = fusevm::ffi::try_call(name, &margs) {
6507 return r;
6508 }
6509 }
6510 Err(ref_error(name))
6511}
6512
6513thread_local! {
6514 /// The constructor a builtin STATIC is currently being invoked on.
6515 ///
6516 /// `A.from(x)` on `class A extends Array` re-dispatches against the `Array`
6517 /// builtin, which is reached by NAME and so cannot see `A`. The species
6518 /// rules need it: `Array.from`, `Array.of` and every `Promise` static build
6519 /// their result with `this`, so on a subclass they must construct through
6520 /// it. A stack, since one static can call another.
6521 static STATIC_THIS: std::cell::RefCell<Vec<Value>> =
6522 const { std::cell::RefCell::new(Vec::new()) };
6523}
6524
6525/// Run `f` with `recv` recorded as the receiver of a builtin static call.
6526pub fn with_static_this<R>(recv: &Value, f: impl FnOnce() -> R) -> R {
6527 STATIC_THIS.with(|s| s.borrow_mut().push(recv.clone()));
6528 let out = f();
6529 STATIC_THIS.with(|s| {
6530 s.borrow_mut().pop();
6531 });
6532 out
6533}
6534
6535/// The constructor the running builtin static was called on, if it was reached
6536/// through a subclass rather than directly.
6537pub fn current_static_this() -> Option<Value> {
6538 STATIC_THIS.with(|s| s.borrow().last().cloned())
6539}
6540
6541/// `recv.name(args)`.
6542pub fn call_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
6543 // `undefined.foo()` is a `[[Get]]` and THEN a call (13.3.6 EvaluateCall), so
6544 // the failure is the property read, not the call: node reports
6545 // `Cannot read properties of undefined (reading 'foo')`. node-js ran the
6546 // whole method dispatch against the nullish receiver, found nothing, and
6547 // reported `undefined.foo is not a function` — the wrong error class of
6548 // message for the single most common runtime fault in JS, and one that
6549 // points at the callee instead of at the base that was nullish.
6550 if with_host(|h| h.is_nullish(recv)) {
6551 return Err(type_error(&format!(
6552 "Cannot read properties of {} (reading '{name}')",
6553 with_host(|h| h.str_of(recv))
6554 )));
6555 }
6556 // `this.#m(…)` is a `[[PrivateGet]]` followed by a call, so the brand check
6557 // comes first: an unbranded receiver throws here rather than reporting the
6558 // method missing. Only a `#`-prefixed name pays the extra probe.
6559 if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
6560 return Err(crate::builtins::private_brand_message(name, false));
6561 }
6562 // `proxy.m(…)` is 13.3.6 `EvaluateCall`: `Get(proxy, "m")` — through the
6563 // `get` trap — then a call with the PROXY as `this`. The `lookup_*` shortcuts
6564 // below all read a property map a proxy does not have.
6565 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
6566 let f = crate::builtins::get_property(recv, name)?;
6567 if !with_host(|h| is_callable(h, &f)) {
6568 return Err(type_error(&format!("{name} is not a function")));
6569 }
6570 // `Function.prototype.call`/`apply`/`bind`/`toString` and the REFLECTIVE
6571 // `Object.prototype` methods are generic over `this`. node-js models each
6572 // as a thunk BOUND to the object it was read off — through a proxy, that
6573 // is the target — so invoking the thunk answers for the target and skips
6574 // the traps entirely: `pf.call(1, 2)` never reached the `apply` trap and
6575 // `p.hasOwnProperty(k)` never reached the descriptor trap. Re-dispatch
6576 // those against the PROXY, which is the `this` the real method receives.
6577 //
6578 // `toString`/`valueOf`/`toLocaleString` are deliberately NOT re-dispatched
6579 // for a non-callable proxy: they resolve by the TARGET's kind (a proxy of
6580 // an array stringifies `1,2` through `Array.prototype.toString`, not
6581 // `[object Object]`), which the bound thunk already gets right.
6582 if with_host(|h| matches!(h.get(&f), Some(JsObj::BoundMethod { .. }))) {
6583 if with_host(|h| is_callable(h, recv)) {
6584 if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6585 return Ok(r);
6586 }
6587 }
6588 if matches!(
6589 name,
6590 "hasOwnProperty" | "propertyIsEnumerable" | "isPrototypeOf"
6591 ) {
6592 return crate::builtins::object_builtin_method(recv, name, args);
6593 }
6594 // The three above resolve by the TARGET's kind, and the thunk is
6595 // already bound to the target — so it must be invoked WITHOUT a
6596 // receiver override. Passing the proxy as `this` made the
6597 // `BoundMethod` arm of `invoke` prefer it over its own receiver and
6598 // call straight back into this branch, so `String(new Proxy({}, {}))`
6599 // recursed until the stack overflowed and the process aborted.
6600 if matches!(name, "toString" | "valueOf" | "toLocaleString") {
6601 return invoke(&f, args, None);
6602 }
6603 }
6604 return invoke(&f, args, Some(recv.clone()));
6605 }
6606 // Namespace builtins (`console`, `Math`, `JSON`, ...): dispatch by qualified
6607 // name.
6608 if let Some(ns) = with_host(|h| match h.get(recv) {
6609 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
6610 _ => None,
6611 }) {
6612 let qualified = format!("{ns}.{name}");
6613 if crate::builtins::is_known_builtin(&qualified) {
6614 return crate::builtins::call_builtin_function(&qualified, args);
6615 }
6616 }
6617 // Object / instance: an accessor getter that yields a function, an own or
6618 // inherited method (class methods live on the prototype chain), then an
6619 // Object.prototype builtin (hasOwnProperty …). Resolve via `lookup_*`
6620 // directly — NOT get_property — so the Object.prototype-builtin fallback
6621 // never routes back through a BoundMethod and recurses.
6622 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
6623 // A native stdlib instance (`Buffer`/crypto `Hash`/`EventEmitter`/`URL`/
6624 // fs `Stats`/http `ServerResponse`…) carries a hidden `@@native` tag.
6625 // A user-added or reparented-prototype method takes precedence over the
6626 // native dispatcher — matching JS resolution order (own → prototype
6627 // chain). This is what lets Express work: it does
6628 // `Object.setPrototypeOf(res, app.response)` and calls `res.send(...)`,
6629 // where `send` is a plain function on the reparented prototype. Native
6630 // instance methods (`res.end`/`write`/…) are NOT stored as plain
6631 // function properties, so `lookup_chain` misses them and we fall through
6632 // to `instance_call` for the real native behavior.
6633 if let Some(tag) = crate::stdlib::native_tag(recv) {
6634 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6635 if with_host(|h| is_callable(h, &f)) {
6636 return invoke(&f, args, Some(recv.clone()));
6637 }
6638 }
6639 // `Object.prototype` methods reach a native instance too — a Buffer
6640 // inherits `hasOwnProperty`/`isPrototypeOf` through its prototype
6641 // chain, and the native dispatcher has no entry for them.
6642 if crate::builtins::is_object_builtin_method(name)
6643 && !crate::stdlib::instance_has_method(&tag, name)
6644 {
6645 return crate::builtins::object_builtin_method(recv, name, args);
6646 }
6647 return crate::stdlib::instance_call(&tag, recv, name, args);
6648 }
6649 // A primitive wrapper forwards to the primitive's method table, the
6650 // same way a native instance forwards to its tag's. A user method on
6651 // the wrapper or anywhere on its chain still wins first.
6652 if let Some(prim) = crate::builtins::wrapped_primitive(recv) {
6653 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6654 if with_host(|h| is_callable(h, &f)) {
6655 return invoke(&f, args, Some(recv.clone()));
6656 }
6657 }
6658 // The reflective `Object.prototype` methods answer for the WRAPPER
6659 // — `w.hasOwnProperty("0")` asks about the wrapper's own index
6660 // properties, not about the string.
6661 if crate::builtins::is_object_builtin_method(name) {
6662 return crate::builtins::object_builtin_method(recv, name, args);
6663 }
6664 return call_method(&prim, name, args);
6665 }
6666 if let Some((Some(getter), _)) = with_host(|h| lookup_accessor(h, recv, name)) {
6667 let f = invoke(&getter, Vec::new(), Some(recv.clone()))?;
6668 if with_host(|h| is_callable(h, &f)) {
6669 return invoke(&f, args, Some(recv.clone()));
6670 }
6671 }
6672 // A Proxy in the prototype chain serves the method through its `get`
6673 // trap. `lookup_chain` below reads property maps, which a proxy has none
6674 // of, so without this `child.m()` on `Object.create(proxy)` reported
6675 // "m is not a function" even though `child.m` already read correctly.
6676 if crate::builtins::proxy_proto_link(recv, name).is_some() {
6677 let f = crate::builtins::get_property(recv, name)?;
6678 if !with_host(|h| is_callable(h, &f)) {
6679 return Err(type_error(&format!("{name} is not a function")));
6680 }
6681 return invoke(&f, args, Some(recv.clone()));
6682 }
6683 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6684 if with_host(|h| is_callable(h, &f)) {
6685 return invoke(&f, args, Some(recv.clone()));
6686 }
6687 return Err(type_error(&format!("{name} is not a function")));
6688 }
6689 // A method patched onto `Object.prototype`. `lookup_chain` cannot find
6690 // it: a plain object is not LINKED to the intrinsic prototype object,
6691 // its `Object.prototype` members are synthesized instead. So
6692 // `Object.prototype.tap = f; ({}).tap()` reported "is not a function"
6693 // while `({}).tap` already read back as `f`.
6694 if let Some(f) = crate::builtins::inherited_builtin_static(recv, name) {
6695 if with_host(|h| is_callable(h, &f)) {
6696 return invoke(&f, args, Some(recv.clone()));
6697 }
6698 }
6699 // A method from an intrinsic prototype this object's CHAIN passes
6700 // through — `Object.create(Array.prototype).push(1)`. The read already
6701 // resolves it through the same owner oracle; dispatch reported "is not
6702 // a function", the read and the call disagreeing once more.
6703 if let Some(owner) = crate::builtins::inherited_method_owner_pub(recv, name) {
6704 if owner != "Object" {
6705 return crate::builtins::proto_method(recv, &format!("{owner}:{name}"), args);
6706 }
6707 }
6708 if crate::builtins::is_object_builtin_method(name) {
6709 return crate::builtins::object_builtin_method(recv, name, args);
6710 }
6711 if name == "constructor" {
6712 if let Some(r) = call_default_ctor(recv, &args) {
6713 return r;
6714 }
6715 }
6716 return Err(type_error(&format!("{name} is not a function")));
6717 }
6718 // Function value methods: call / apply / bind, then any static method stored
6719 // on the function object.
6720 if matches!(
6721 with_host(|h| h.kind_of(recv)),
6722 Some(ObjKind::Func)
6723 | Some(ObjKind::Class)
6724 | Some(ObjKind::BoundFunc)
6725 | Some(ObjKind::BoundMethod)
6726 | Some(ObjKind::Builtin)
6727 ) {
6728 if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
6729 return Ok(r);
6730 }
6731 // A static method (own or inherited): `this` is the constructor (`recv`).
6732 let stat = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6733 with_host(|h| h.class_static(recv, name))
6734 } else {
6735 with_host(|h| h.fn_prop(recv, name))
6736 };
6737 if let Some(f) = stat {
6738 if with_host(|h| is_callable(h, &f)) {
6739 return invoke(&f, args, Some(recv.clone()));
6740 }
6741 }
6742 // `class_static` only walks user-class `extends` links, so a chain that
6743 // bottoms out in a BUILTIN constructor (`class D extends Array {}`)
6744 // could not reach that builtin's statics: `D.from([1,2])` threw
6745 // "from is not a function" even though `typeof D.from` said `function`.
6746 // Re-dispatch the call against that ancestor, which is what reaches a
6747 // builtin namespace's methods.
6748 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
6749 if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
6750 if with_host(|h| h.kind_of(&anc)) == Some(ObjKind::Builtin) {
6751 // The subclass is recorded so a species-aware static
6752 // (`Array.from`, `Promise.resolve`, …) builds its result
6753 // through it rather than through the builtin.
6754 return with_static_this(recv, || call_method(&anc, name, args));
6755 }
6756 }
6757 }
6758 // A method inherited via the function's [[Prototype]] chain (set with
6759 // `Object.setPrototypeOf(fn, proto)`) — the `router` package's router
6760 // functions inherit `route`/`use`/`get`/… from `Router.prototype`.
6761 if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
6762 if with_host(|h| is_callable(h, &f)) {
6763 return invoke(&f, args, Some(recv.clone()));
6764 }
6765 }
6766 // An `Object.prototype` method invoked with a builtin namespace/prototype
6767 // as `this` (`hasOwnProperty.call(Map.prototype, 'get')`, the get-intrinsic
6768 // ownership probe) — dispatch it against the builtin receiver.
6769 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin)
6770 && crate::builtins::is_object_builtin_method(name)
6771 {
6772 return crate::builtins::object_builtin_method(recv, name, args);
6773 }
6774 }
6775 if name == "constructor" {
6776 if let Some(r) = call_default_ctor(recv, &args) {
6777 return r;
6778 }
6779 }
6780 // Type methods (array/string/number, Map/Set/Symbol/generator methods).
6781 crate::builtins::call_type_method(recv, name, args)
6782}
6783
6784/// `x.constructor(...)` invoked as a CALL when nothing on `x`'s prototype chain
6785/// owns a `constructor` slot.
6786///
6787/// Reading the property already resolves a builtin instance's native constructor
6788/// (the `constructor` arm of `builtins::get_property`), but the CALL path only
6789/// consulted the prototype chain, so the two disagreed:
6790/// `(function(){}).constructor === Function` read `true` while
6791/// `(function(){}).constructor('return 9')` threw
6792/// `TypeError: constructor is not a function`. That call form is exactly how
6793/// `get-intrinsic` — a transitive dependency of express — reaches the `Function`
6794/// constructor. Resolved here through the same one definition the read uses, so
6795/// the two can no longer drift apart. `None` means "not resolvable/callable",
6796/// leaving the caller's original error in place.
6797fn call_default_ctor(recv: &Value, args: &[Value]) -> Option<Result<Value, String>> {
6798 let ctor = crate::builtins::get_property(recv, "constructor").ok()?;
6799 with_host(|h| is_callable(h, &ctor)).then(|| invoke(&ctor, args.to_vec(), None))
6800}
6801
6802/// Call any callable value.
6803pub fn invoke(callable: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6804 // `[[Call]]` on a Proxy runs the `apply` trap (or forwards to the target).
6805 // Probed by kind first so the ordinary call path never clones its arguments.
6806 if with_host(|h| h.kind_of(callable)) == Some(ObjKind::Proxy) {
6807 return crate::proxy::apply(callable, args, this).map(|r| r.expect("kind_of said Proxy"));
6808 }
6809 let obj = with_host(|h| h.get(callable).cloned());
6810 match obj {
6811 // A builtin-prototype method thunk (`Object.prototype.toString`): dispatch
6812 // against the invoke-time `this` (supplied by `.call`/`.apply`).
6813 Some(JsObj::Builtin(name)) if name.starts_with("@proto:") => {
6814 let recv = this.unwrap_or(Value::Undef);
6815 crate::builtins::proto_method(&recv, &name["@proto:".len()..], args)
6816 }
6817 // An intrinsic prototype's GETTER, borrowed off its descriptor — the
6818 // form a library uses to read a slot from an arbitrary receiver
6819 // (`Object.getOwnPropertyDescriptor(Map.prototype, 'size').get
6820 // .call(m)`). It brand-checks `this` and reads, or throws naming
6821 // itself.
6822 // The setter half of the `arguments`/`caller` poison pill — the only
6823 // intrinsic accessor here that has one, and it throws like its getter.
6824 Some(JsObj::Builtin(name)) if name.starts_with("@protoset:") => {
6825 let _ = &name;
6826 let recv = this.unwrap_or(Value::Undef);
6827 // The setter half accepts silently for the same receivers the
6828 // getter answers for, and throws for the rest.
6829 if with_host(|h| h.fn_is_sloppy(&recv)) {
6830 Ok(Value::Undef)
6831 } else {
6832 Err(type_error(crate::builtins::POISON_PILL))
6833 }
6834 }
6835 Some(JsObj::Builtin(name)) if name.starts_with("@protoget:") => {
6836 let recv = this.unwrap_or(Value::Undef);
6837 let rest = &name["@protoget:".len()..];
6838 let (ctor, key) = rest.split_once(':').unwrap_or((rest, ""));
6839 crate::builtins::proto_getter_call(ctor, key, &recv)
6840 }
6841 // `NativeCtor.call(obj, …)` — ES5 "constructor stealing", still shipped by
6842 // libraries that predate `class`. `iconv-lite`'s internal codec is exactly
6843 // this:
6844 //
6845 // function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
6846 // InternalDecoder.prototype = StringDecoder.prototype;
6847 //
6848 // A native constructor builds a fresh tagged object, so initializing the
6849 // SUPPLIED object means building one and moving its slots across.
6850 //
6851 // The guard is deliberately narrow: `obj` must already inherit from THIS
6852 // constructor's prototype, i.e. the subclass really did adopt it. Without
6853 // that, `Date.call(x)` and `Buffer.call(x)` — which in JS ignore `this` and
6854 // return a string / a buffer — would start mutating `x` instead.
6855 Some(JsObj::Builtin(ref name)) if steals_ctor(name, this.as_ref()) => {
6856 let target = this.expect("guard checked");
6857 let built = crate::stdlib::construct(name, &args)
6858 .expect("guard checked a native constructor")?;
6859 adopt_native_slots(&target, &built);
6860 Ok(Value::Undef)
6861 }
6862 Some(JsObj::Builtin(name)) => crate::builtins::call_builtin_function(&name, args),
6863 Some(JsObj::Func(fv)) => run_user_func_of(&fv, args, this, Some(callable.clone())),
6864 // A method read off an object is modelled as a thunk BOUND to it, but an
6865 // explicit `.call`/`.apply` receiver still wins — `Function.prototype.call`
6866 // rebinds `this`, and every `Array.prototype` method is generic over it, so
6867 // `[].slice.call(arrayLike)` must run against the ARGUMENT. Dropping the
6868 // override made that read back as the empty array the thunk was read off.
6869 // A nullish override is ignored: it carries no receiver to dispatch on.
6870 Some(JsObj::BoundMethod { recv, name }) => {
6871 let target = match &this {
6872 Some(t) if !matches!(t, Value::Undef) && !with_host(|h| h.is_null(t)) => t,
6873 _ => &recv,
6874 };
6875 // A thunk read off an ARRAY carries an `Array.prototype` method, and
6876 // those are generic over `this` — route the rebound call through
6877 // `proto_method` so an array-LIKE receiver takes the generic path
6878 // instead of being told the method does not exist.
6879 if with_host(|h| h.kind_of(&recv)) == Some(ObjKind::Array) {
6880 return crate::builtins::proto_method(target, &format!("Array:{name}"), args);
6881 }
6882 call_method(target, &name, args)
6883 }
6884 Some(JsObj::BoundFunc {
6885 target,
6886 this: bthis,
6887 args: pre,
6888 }) => {
6889 let mut all = pre;
6890 all.extend(args);
6891 invoke(&target, all, Some(bthis))
6892 }
6893 Some(JsObj::Class(c)) => Err(type_error(&format!(
6894 "Class constructor {} cannot be invoked without 'new'",
6895 c.name
6896 ))),
6897 _ => Err(type_error(&format!(
6898 "{} is not a function",
6899 with_host(|h| h.str_of(callable))
6900 ))),
6901 }
6902}
6903
6904/// Whether calling the native constructor `name` with `this` is the ES5
6905/// constructor-stealing pattern rather than an ordinary call.
6906///
6907/// True only when `name` really is a native stdlib constructor AND `this` is a
6908/// plain object that already inherits from that constructor's prototype — the
6909/// signature of `Sub.prototype = Native.prototype; Native.call(this, …)`. An
6910/// object that merely happens to be passed as `this` does not qualify, so
6911/// `Date.call(x)` / `Buffer.call(x)` keep their JS meaning (ignore `this`).
6912fn steals_ctor(name: &str, this: Option<&Value>) -> bool {
6913 let Some(target) = this else { return false };
6914 if !with_host(|h| matches!(h.get(target), Some(JsObj::Object(_)))) {
6915 return false;
6916 }
6917 // Already initialized (e.g. a re-entrant call) — nothing to steal.
6918 if crate::stdlib::native_tag(target).is_some() {
6919 return false;
6920 }
6921 let Some(proto) = with_host(|h| h.ensure_ctor_proto(name)) else {
6922 return false;
6923 };
6924 let mut cur = with_host(|h| h.proto_of(target));
6925 while let Some(p) = cur {
6926 if p == proto {
6927 return true;
6928 }
6929 cur = with_host(|h| h.proto_of(&p));
6930 }
6931 false
6932}
6933
6934/// Move a freshly-constructed native instance's state onto `target`, so an
6935/// object built by a subclass constructor becomes a working instance of the
6936/// native class. Copies every own key the native constructor set — the hidden
6937/// `@@`-prefixed slots that carry the state AND the plain ones it exposes
6938/// (`StringDecoder`'s `encoding`) — without disturbing keys `target` already has.
6939fn adopt_native_slots(target: &Value, built: &Value) {
6940 let slots: Vec<(String, Value)> = with_host(|h| match h.get(built) {
6941 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
6942 _ => Vec::new(),
6943 });
6944 with_host(|h| {
6945 if let Some(JsObj::Object(p)) = h.get_mut(target) {
6946 for (k, v) in slots {
6947 p.insert(k, v);
6948 }
6949 }
6950 });
6951}
6952
6953/// Execute a user function/closure body on a fresh frame.
6954pub fn run_user_func(fv: &FuncVal, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
6955 run_user_func_of(fv, args, this, None)
6956}
6957
6958/// [`run_user_func`] with the function VALUE the call came through, which the
6959/// `arguments` object needs for its `callee`.
6960pub fn run_user_func_of(
6961 fv: &FuncVal,
6962 args: Vec<Value>,
6963 this: Option<Value>,
6964 callee: Option<Value>,
6965) -> Result<Value, String> {
6966 run_user_func_full(fv, args, this, None, callee)
6967}
6968
6969/// As `run_user_func`, but with an explicit `new.target` (set by `new`).
6970pub fn run_user_func_nt(
6971 fv: &FuncVal,
6972 args: Vec<Value>,
6973 this: Option<Value>,
6974 new_target: Option<Value>,
6975) -> Result<Value, String> {
6976 run_user_func_full(fv, args, this, new_target, None)
6977}
6978
6979fn run_user_func_full(
6980 fv: &FuncVal,
6981 args: Vec<Value>,
6982 this: Option<Value>,
6983 new_target: Option<Value>,
6984 callee: Option<Value>,
6985) -> Result<Value, String> {
6986 // Consumed first, before anything here can start another call.
6987 let derived_ctor = with_host(|h| std::mem::take(&mut h.derived_ctor_next));
6988 // Only the light fields: cloning the whole `FuncDef` cloned its `Chunk` —
6989 // the entire compiled body, `sub_chunks` and all — on every single call.
6990 // The chunk is now reached once per pooled VM, in the two arms below.
6991 let (params, is_generator, is_async, is_arrow_def, def_name) = with_host(|h| {
6992 let d = &h.funcs[fv.def_id];
6993 (
6994 d.params.clone(),
6995 d.is_generator,
6996 d.is_async,
6997 d.is_arrow,
6998 d.name.clone(),
6999 )
7000 });
7001 let env = new_env(fv.env.clone());
7002 // Bind the simple/rest arg slots; destructuring + defaults run in the body
7003 // prologue (compiled ahead of the user statements).
7004 let fn_is_sloppy = with_host(|h| !h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
7005 bind_params(
7006 &env,
7007 ¶ms,
7008 args,
7009 is_arrow_def,
7010 callee.as_ref(),
7011 fn_is_sloppy && !is_arrow_def,
7012 );
7013 // Arrow functions capture `this` lexically; regular functions receive it.
7014 let mut this_val = if fv.is_arrow { fv.this.clone() } else { this };
7015 // 10.2.1.2 OrdinaryCallBindThis: in SLOPPY mode an absent or nullish `this`
7016 // becomes the global object. Only a strict function keeps `undefined`, and
7017 // an arrow has no `this` of its own to substitute. Leaving it undefined
7018 // meant a plain `f()`, a detached method, a callback and `f.call(null)` all
7019 // saw `undefined` where node sees `globalThis`.
7020 let sloppy_this = !fv.is_arrow
7021 && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict))
7022 && match &this_val {
7023 None => true,
7024 Some(v) => matches!(v, Value::Undef) || with_host(|h| h.is_null(v)),
7025 };
7026 if sloppy_this {
7027 this_val = Some(with_host(|h| h.global_object()));
7028 } else if !fv.is_arrow && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)) {
7029 // The other half of OrdinaryCallBindThis: a SLOPPY function boxes a
7030 // primitive `this` with `ToObject`, so `f.call(5)` sees a `Number`
7031 // wrapper rather than the number. Only strict mode passes it through.
7032 if let Some(t) = this_val.clone() {
7033 let boxed = crate::builtins::to_object(&t);
7034 this_val = Some(boxed);
7035 }
7036 }
7037 // A generator function does not run its body on call — it returns a suspended
7038 // generator over the already-bound frame.
7039 if is_generator {
7040 let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
7041 let gen = make_generator(
7042 chunk,
7043 env,
7044 this_val,
7045 fv.home_class.clone(),
7046 fv.home_static,
7047 fv.home_object.clone(),
7048 with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
7049 );
7050 if is_async {
7051 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(&gen).cloned()) {
7052 with_host(|h| h.generators[id as usize].async_gen = true);
7053 }
7054 }
7055 return Ok(gen);
7056 }
7057 // An async function runs on a coroutine and returns a Promise: it executes
7058 // synchronously up to the first `await`, then continues via microtasks.
7059 if is_async {
7060 let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
7061 let gen = make_generator(
7062 chunk,
7063 env,
7064 this_val,
7065 fv.home_class.clone(),
7066 fv.home_static,
7067 fv.home_object.clone(),
7068 with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
7069 );
7070 return Ok(run_async(gen));
7071 }
7072 let home = fv
7073 .home_class
7074 .as_ref()
7075 .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
7076 // Resolved BEFORE the borrow below: reading the function table re-enters
7077 // the host, and doing it inside the frame-push closure double-borrows.
7078 let fn_strict = with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
7079 with_host(|h| {
7080 h.frames.push(Frame {
7081 base_env: env.clone(),
7082 env,
7083 this_obj: this_val,
7084 new_target,
7085 home_class: home,
7086 home_static: fv.home_static,
7087 home_object: fv.home_object.clone(),
7088 strict: fn_strict,
7089 line: 0,
7090 owner: Some(def_name),
7091 is_module: false,
7092 this_state: if derived_ctor {
7093 ThisState::Pending
7094 } else {
7095 ThisState::Plain
7096 },
7097 })
7098 });
7099 let r = run_chunk_keyed(func_key(fv.def_id), || {
7100 with_host(|h| h.funcs[fv.def_id].chunk.clone())
7101 });
7102 let (sig, this_state) = with_host(|h| {
7103 let frame = h.frames.pop();
7104 (h.signal.take(), frame.map(|f| f.this_state))
7105 });
7106 let ret = match r {
7107 Err(e) => return Err(e),
7108 Ok(_) => match sig {
7109 Some(Signal::Return(v)) => v,
7110 _ => Value::Undef,
7111 },
7112 };
7113 // 10.2.2 [[Construct]] steps 10-12 for a derived constructor: an object
7114 // return wins; any other non-undefined return is a TypeError; and falling
7115 // off the end (or `return;`) needs `this` to have been bound by `super()`.
7116 if derived_ctor && !returns_object(&ret) {
7117 if !matches!(ret, Value::Undef) {
7118 return Err(type_error(
7119 "Derived constructors may only return object or undefined",
7120 ));
7121 }
7122 if this_state == Some(ThisState::Pending) {
7123 return Err(this_before_super_error());
7124 }
7125 }
7126 Ok(ret)
7127}
7128
7129/// Bind positional args into a fresh call environment. The compiler emits the
7130/// param names in `def.params`; a `...rest` slot collects the tail as an array.
7131fn bind_params(
7132 env: &Env,
7133 params: &[ParamSlot],
7134 args: Vec<Value>,
7135 is_arrow: bool,
7136 callee: Option<&Value>,
7137 sloppy: bool,
7138) {
7139 let mut vars = VarMap::default();
7140 let mut i = 0;
7141 for slot in params {
7142 if slot.rest {
7143 let rest: Vec<Value> = args.get(i..).map(|s| s.to_vec()).unwrap_or_default();
7144 let arr = with_host(|h| h.new_array(rest));
7145 vars.insert(slot.name.clone(), arr);
7146 } else {
7147 let v = args.get(i).cloned().unwrap_or(Value::Undef);
7148 vars.insert(slot.name.clone(), v);
7149 i += 1;
7150 }
7151 }
7152 // `arguments` array (simple approximation — see BUGS.md: it is a real
7153 // Array, not an Arguments exotic). An ARROW function never gets one:
7154 // `FunctionDeclarationInstantiation` (10.2.11) creates the binding only for
7155 // a non-arrow, so `arguments` inside an arrow resolves lexically to the
7156 // enclosing function's. Binding an empty one here made
7157 // `function f(){ const g = () => [...arguments]; }` see zero args.
7158 if !is_arrow {
7159 let args_arr = with_host(|h| {
7160 let a = h.new_array(args);
7161 // Marked so it can be told apart from an ordinary array: node's
7162 // `arguments` is an exotic, and without the mark
7163 // `Array.isArray(arguments)` was true, the brand was
7164 // `[object Array]` and `util.types.isArgumentsObject` was false.
7165 // The backing representation stays an Array, which is what keeps
7166 // indices, `length`, spread and `for-of` working.
7167 h.set_fn_prop(&a, "@@arguments", Value::Bool(true));
7168 // `callee` is the function itself in SLOPPY code (it is a poison
7169 // pill only in strict, which the read path handles). It read back
7170 // `undefined`, so the pre-`class` self-reference idiom
7171 // `(function(){ arguments.callee })` found nothing.
7172 if let Some(f) = callee {
7173 if sloppy {
7174 h.set_fn_prop(&a, "@@callee", f.clone());
7175 }
7176 }
7177 a
7178 });
7179 vars.entry("arguments".to_string()).or_insert(args_arr);
7180 }
7181 env.borrow_mut().vars = vars;
7182}
7183
7184/// Construct an instance with `new` — creates a fresh object, binds it as
7185/// `this`, runs the constructor, and returns the object (unless the constructor
7186/// returns its own object).
7187pub fn construct(ctor: &Value, args: Vec<Value>) -> Result<Value, String> {
7188 construct_nt(ctor, args, ctor.clone())
7189}
7190
7191/// `new` with an explicit `new.target` (differs from `ctor` when a derived class
7192/// calls `super(...)` — the target stays the originally-`new`ed class).
7193pub fn construct_nt(ctor: &Value, args: Vec<Value>, new_target: Value) -> Result<Value, String> {
7194 // `new proxy(…)` runs the `construct` trap (or forwards to the target).
7195 if with_host(|h| h.kind_of(ctor)) == Some(ObjKind::Proxy) {
7196 return crate::proxy::construct(ctor, args, &new_target)
7197 .map(|r| r.expect("kind_of said Proxy"));
7198 }
7199 let obj = with_host(|h| h.get(ctor).cloned());
7200 match obj {
7201 Some(JsObj::Class(_)) => construct_class(ctor, args, new_target),
7202 Some(JsObj::Func(fv)) => {
7203 // Only an ORDINARY function has a `[[Construct]]` slot. An arrow, a
7204 // `function*` and an `async function` are callable but not
7205 // constructable (10.2.2 is installed only for the ordinary case), so
7206 // `new` on one is a TypeError — node-js instead ran the body and
7207 // handed back a half-built instance (for a generator, an object whose
7208 // constructor had returned a suspended generator).
7209 let non_ctor = with_host(|h| {
7210 h.funcs
7211 .get(fv.def_id)
7212 // A MethodDefinition is in the same boat: `new ({m(){}}).m()`
7213 // is `TypeError: o.m is not a constructor` on node v26.7.0,
7214 // which is also why a method owns no `prototype`.
7215 .map(|d| d.is_generator || d.is_async || d.is_method)
7216 .unwrap_or(false)
7217 });
7218 if fv.is_arrow || non_ctor {
7219 return Err(not_a_constructor(ctor));
7220 }
7221 // A plain constructor function: instance delegates to `fn.prototype`
7222 // (auto-created with a `.constructor` back-link if not yet accessed).
7223 let inst = with_host(|h| {
7224 let o = h.new_object(IndexMap::new());
7225 let proto = h.fn_prop(ctor, "prototype").unwrap_or_else(|| {
7226 let p = h.new_object(IndexMap::new());
7227 if let Some(JsObj::Object(pp)) = h.get_mut(&p) {
7228 pp.insert("constructor".to_string(), ctor.clone());
7229 }
7230 // `F.prototype.constructor` is non-enumerable in JS.
7231 h.hide_prop(&p, "constructor");
7232 h.set_fn_prop(ctor, "prototype", p.clone());
7233 p
7234 });
7235 h.set_proto(&o, proto);
7236 o
7237 });
7238 let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target))?;
7239 if returns_object(&r) {
7240 Ok(r)
7241 } else {
7242 Ok(inst)
7243 }
7244 }
7245 Some(JsObj::Builtin(name)) => crate::builtins::construct_builtin(&name, args),
7246 Some(JsObj::BoundFunc {
7247 target, args: pre, ..
7248 }) => {
7249 let mut all = pre;
7250 all.extend(args);
7251 construct_nt(&target, all, new_target)
7252 }
7253 _ => Err(not_a_constructor(ctor)),
7254 }
7255}
7256
7257/// `TypeError: <callee> is not a constructor`.
7258///
7259/// V8 names the callee by its SOURCE TEXT (`new g()` reports `g`, `new o.m()`
7260/// reports `o.m`); node-js keeps no spans, so a named callable is reported by
7261/// its name — the same string in the common case — and anything else by its
7262/// value.
7263fn not_a_constructor(ctor: &Value) -> String {
7264 let name = with_host(|h| match h.callable_name(ctor) {
7265 n if n.is_empty() => h.str_of(ctor),
7266 n => n,
7267 });
7268 type_error(&format!("{name} is not a constructor"))
7269}
7270
7271/// Whether a constructor's return value is an object (so `new` yields it instead
7272/// of the fresh instance). In JS "object" includes functions — the `router`
7273/// package's constructor `return router` (a function) must be honored, or the
7274/// returned router loses its callable identity.
7275fn returns_object(r: &Value) -> bool {
7276 matches!(
7277 with_host(|h| h.get(r).cloned()),
7278 Some(JsObj::Object(_))
7279 | Some(JsObj::Array(_))
7280 | Some(JsObj::Map { .. })
7281 | Some(JsObj::Set { .. })
7282 | Some(JsObj::Func(_))
7283 | Some(JsObj::Class(_))
7284 | Some(JsObj::BoundFunc { .. })
7285 | Some(JsObj::BoundMethod { .. })
7286 | Some(JsObj::RegExp(_))
7287 )
7288}
7289
7290/// Construct a `class` instance: allocate the object linked to `C.prototype`,
7291/// run field initializers + the constructor (which may call `super(...)`).
7292fn construct_class(
7293 class_val: &Value,
7294 args: Vec<Value>,
7295 new_target: Value,
7296) -> Result<Value, String> {
7297 let cv = match with_host(|h| h.get(class_val).cloned()) {
7298 Some(JsObj::Class(c)) => c,
7299 _ => return Err(type_error("not a class")),
7300 };
7301 // Resolve the prototype of the *most-derived* class being `new`ed, so an
7302 // instance created through a `super()` chain still delegates to the leaf
7303 // prototype (correct method resolution).
7304 let leaf_proto = match with_host(|h| h.get(&new_target).cloned()) {
7305 Some(JsObj::Class(c)) => c.proto.clone(),
7306 _ => cv.proto.clone(),
7307 };
7308 let inst = with_host(|h| {
7309 let o = h.new_object(IndexMap::new());
7310 h.set_proto(&o, leaf_proto.clone());
7311 o
7312 });
7313 // A `super()` deeper in may substitute the instance; the previous value is
7314 // restored so a `new` inside a constructor body cannot be mistaken for one.
7315 let saved = with_host(|h| h.swap_super_replacement(None));
7316 let ran = run_class_ctor(&cv, &inst, args, &new_target);
7317 let substituted = with_host(|h| {
7318 let s = h.take_super_replacement();
7319 h.swap_super_replacement(saved);
7320 s
7321 });
7322 // A constructor that returns an object replaces the instance (`new`
7323 // semantics); failing that, whatever `super()` substituted for it.
7324 match ran? {
7325 Some(obj) if returns_object(&obj) => Ok(obj),
7326 _ => Ok(substituted.unwrap_or(inst)),
7327 }
7328}
7329
7330/// Run one class's field initializers then its constructor on an existing
7331/// instance. Returns the constructor's explicit object return (if any). For a
7332/// base class this is the whole init; for a derived class the constructor body
7333/// reaches `super(...)` which recurses into the parent.
7334fn run_class_ctor(
7335 cv: &ClassVal,
7336 inst: &Value,
7337 args: Vec<Value>,
7338 new_target: &Value,
7339) -> Result<Option<Value>, String> {
7340 // A derived class must run its fields AFTER super() returns; SUPER_CALL does
7341 // that. A base class initializes fields before the constructor body.
7342 if cv.parent.is_none() {
7343 init_fields(cv, inst)?;
7344 }
7345 match &cv.ctor {
7346 Some(ctor_fn) => {
7347 let fv = match with_host(|h| h.get(ctor_fn).cloned()) {
7348 Some(JsObj::Func(f)) => f,
7349 _ => return Err(type_error("class constructor is not a function")),
7350 };
7351 if cv.parent.is_some() {
7352 with_host(|h| h.mark_next_call_derived_ctor());
7353 }
7354 let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7355 return Ok(Some(r));
7356 }
7357 None => {
7358 // Default constructor: `constructor(...a){ super(...a); }` for a
7359 // derived class, empty for a base class.
7360 if let Some(parent) = &cv.parent {
7361 // A base constructor's returned object becomes the instance, so
7362 // the implicit `constructor(...a){ super(...a) }` hands it on.
7363 if let Some(replacement) = super_construct(parent, args, inst, new_target)? {
7364 init_fields(cv, &replacement)?;
7365 return Ok(Some(replacement));
7366 }
7367 init_fields(cv, inst)?;
7368 }
7369 }
7370 }
7371 Ok(None)
7372}
7373
7374/// Evaluate and assign a class's instance-field initializers on `inst`.
7375fn init_fields(cv: &ClassVal, inst: &Value) -> Result<(), String> {
7376 for (name, thunk, name_anon) in &cv.fields {
7377 init_one_field(inst, name, thunk, *name_anon)?;
7378 }
7379 Ok(())
7380}
7381
7382/// Evaluate ONE instance-field initializer thunk and install the result on
7383/// `inst`.
7384///
7385/// Shared by the base-class path (`init_fields`) and the derived-class path
7386/// that runs after `super(...)`; the two used to be separate loops, and only the
7387/// first canonicalized an array-index key.
7388///
7389/// `name_anon` carries 15.7.10's NamedEvaluation: `class C { f = function(){} }`
7390/// gives the function the name `f`. It is decided by the compiler from the
7391/// syntax, never from the value.
7392pub fn init_one_field(
7393 inst: &Value,
7394 name: &str,
7395 thunk: &Value,
7396 name_anon: bool,
7397) -> Result<(), String> {
7398 // The thunk is an arrow capturing the class scope; run it with `this`=inst
7399 // so `this.other`-referencing initializers work.
7400 let val = invoke(thunk, Vec::new(), Some(inst.clone()))?;
7401 with_host(|h| {
7402 if name_anon {
7403 let s = h.new_str(name.to_string());
7404 h.set_fn_prop(&val, "name", s);
7405 }
7406 if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7407 let is_new = !props.contains_key(name);
7408 props.insert(name.to_string(), val);
7409 if is_new && array_index(name).is_some() {
7410 canonicalize_own_keys(props);
7411 }
7412 }
7413 });
7414 Ok(())
7415}
7416
7417/// Run a parent constructor as part of `super(...)`: dispatch on the parent's
7418/// kind (class vs plain function vs builtin) using the existing instance.
7419/// Run the parent constructor against `inst`.
7420///
7421/// Returns the object the parent's `[[Construct]]` produced when that is NOT
7422/// `inst` — a base constructor is allowed to `return` one, and 15.7.15 makes it
7423/// the derived instance too. The caller rebinds `this` to it, so the rest of the
7424/// derived constructor writes to the object `new` will hand back.
7425pub fn super_construct(
7426 parent: &Value,
7427 args: Vec<Value>,
7428 inst: &Value,
7429 new_target: &Value,
7430) -> Result<Option<Value>, String> {
7431 match with_host(|h| h.get(parent).cloned()) {
7432 Some(JsObj::Class(pcv)) => Ok(run_class_ctor(&pcv, inst, args, new_target)?
7433 .filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst)))),
7434 Some(JsObj::Func(fv)) => {
7435 let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
7436 Ok(Some(r).filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst))))
7437 }
7438 Some(JsObj::Builtin(name)) => {
7439 let built = crate::builtins::construct_builtin(&name, args)?;
7440 // An EXOTIC parent (`class A extends Array`) keeps its behaviour in
7441 // the heap variant, not in a property map, so copying own props
7442 // cannot carry it: the instance has to BECOME the built object.
7443 // Without this `new (class extends Array {})().push` was not a
7444 // function, and the same for Map, Set, RegExp, Promise and
7445 // Function — subclassing a builtin produced a plain object.
7446 if !become_exotic(inst, &built) {
7447 // An `Error` subclass is ordinary: its state IS own properties.
7448 adopt_own_props(inst, &built);
7449 }
7450 Ok(None)
7451 }
7452 // A Proxy parent (`class D extends new Proxy(B, {})`): `super(…)` is
7453 // `[[Construct]]` on the proxy, so the `construct` trap runs (or forwards
7454 // to the target). node-js initializes an ALREADY-allocated `inst` rather
7455 // than adopting the constructor's return value, so what the proxy built
7456 // is moved across — the same move the builtin arm makes.
7457 Some(JsObj::Proxy { .. }) => {
7458 let built = construct_nt(parent, args, new_target.clone())?;
7459 if !become_exotic(inst, &built) {
7460 adopt_own_props(inst, &built);
7461 }
7462 Ok(None)
7463 }
7464 _ => Err(type_error("super is not a constructor")),
7465 }
7466}
7467
7468/// Move `built`'s own properties (and their attributes) onto `inst`. Used where
7469/// a parent constructor produces a fresh object but node-js's class model has
7470/// already allocated the instance `this` is bound to.
7471/// Replace `inst`'s heap object with `built`'s, so an instance whose class
7472/// extends a builtin EXOTIC really is one.
7473///
7474/// `inst` keeps its identity and its prototype link — the leaf class's
7475/// prototype, which is what method resolution and `instanceof` walk — while its
7476/// contents become the exotic the parent constructor produced. The side tables
7477/// keyed by heap index (array holes, property attributes, the fn-prop table)
7478/// move across with it.
7479///
7480/// Returns false for a variant whose state is ordinary own properties
7481/// (`Error`), which the caller copies instead.
7482fn become_exotic(inst: &Value, built: &Value) -> bool {
7483 let exotic = matches!(
7484 with_host(|h| h.get(built).cloned()),
7485 Some(JsObj::Array(_))
7486 | Some(JsObj::Map { .. })
7487 | Some(JsObj::Set { .. })
7488 | Some(JsObj::RegExp(_))
7489 | Some(JsObj::Promise { .. })
7490 | Some(JsObj::Func(_))
7491 | Some(JsObj::Str(_))
7492 | Some(JsObj::BigInt(_))
7493 | Some(JsObj::Symbol { .. })
7494 );
7495 if !exotic {
7496 return false;
7497 }
7498 let (Value::Obj(dst), Value::Obj(src)) = (inst, built) else {
7499 return false;
7500 };
7501 let (dst, src) = (*dst, *src);
7502 with_host(|h| {
7503 if let Some(obj) = h.get(built).cloned() {
7504 if let Some(slot) = h.get_mut(inst) {
7505 *slot = obj;
7506 }
7507 }
7508 h.move_index_state(src, dst);
7509 });
7510 true
7511}
7512
7513fn adopt_own_props(inst: &Value, built: &Value) {
7514 let entries: Vec<(String, Value)> = with_host(|h| match h.get(built) {
7515 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
7516 _ => Vec::new(),
7517 });
7518 with_host(|h| {
7519 let keys: Vec<String> = entries.iter().map(|(k, _)| k.clone()).collect();
7520 if let Some(JsObj::Object(props)) = h.get_mut(inst) {
7521 for (k, v) in entries {
7522 props.insert(k, v);
7523 }
7524 canonicalize_own_keys(props);
7525 }
7526 // The copied slots keep the attributes the source gave them, so
7527 // `class E extends Error` instances hide `message`/`stack` too.
7528 for k in keys {
7529 let a = h.prop_attrs(built, &k);
7530 h.set_prop_attrs(inst, &k, a);
7531 }
7532 });
7533}
7534
7535// ── class construction (runtime) ─────────────────────────────────────────────
7536
7537/// Build a class constructor value from its parts. The compiler emits (via
7538/// `MKCLASS`) the evaluated parent (or undefined) and the constructor closure (or
7539/// undefined for a default constructor); methods/getters/setters/statics/fields
7540/// are installed afterward by `DEF_MEMBER`/`DEF_FIELD`.
7541pub fn build_class(name: &str, parent: Value, ctor: Value, source_def: Option<usize>) -> Value {
7542 // A Proxy parent (`class D extends new Proxy(B, {})`): `D.prototype`'s
7543 // `[[Prototype]]` is `Get(parent, "prototype")` — a read that runs the `get`
7544 // trap and so re-enters the host, which the borrow below cannot allow.
7545 // Without it the link fell back to `Object.prototype` and every inherited
7546 // method went missing.
7547 let proxy_parent_proto = (with_host(|h| h.kind_of(&parent)) == Some(ObjKind::Proxy))
7548 .then(|| crate::builtins::get_property(&parent, "prototype").ok())
7549 .flatten();
7550 with_host(|h| {
7551 let parent_opt = if matches!(parent, Value::Undef) {
7552 None
7553 } else {
7554 Some(parent.clone())
7555 };
7556 // The class prototype delegates to the parent's prototype (or
7557 // Object.prototype for a base class). Extending a builtin error links to
7558 // that error's prototype so `instanceof Error` holds for the subclass.
7559 let parent_proto = match &parent_opt {
7560 Some(_) if proxy_parent_proto.is_some() => {
7561 proxy_parent_proto.clone().expect("checked is_some")
7562 }
7563 Some(p) => match h.get(p).cloned() {
7564 Some(JsObj::Class(pc)) => pc.proto.clone(),
7565 Some(JsObj::Builtin(bn)) => {
7566 h.ensure_error_protos();
7567 h.ensure_native_protos();
7568 // `class S extends String {}` links to the REAL
7569 // `String.prototype`, the same way an error subclass links
7570 // to its error prototype. Without it `S.prototype`'s
7571 // `[[Prototype]]` fell back to `Object.prototype`, so
7572 // `new S("hi") instanceof String` read false and
7573 // `String(new S("hi"))` reported `[object String]` instead
7574 // of `hi`.
7575 error_proto_of(h, &bn)
7576 .or_else(|| h.native_proto(&bn))
7577 .or_else(|| h.fn_prop(p, "prototype"))
7578 .unwrap_or_else(|| h.object_proto())
7579 }
7580 _ => h
7581 .fn_prop(p, "prototype")
7582 .unwrap_or_else(|| h.object_proto()),
7583 },
7584 None => h.object_proto(),
7585 };
7586 let proto = h.new_object(IndexMap::new());
7587 h.set_proto(&proto, parent_proto);
7588 let ctor_opt = if matches!(ctor, Value::Undef) {
7589 None
7590 } else {
7591 Some(ctor.clone())
7592 };
7593 // Give the constructor closure its home class (for `super.method()`), and
7594 // record its `.name`.
7595 if let Some(cf) = &ctor_opt {
7596 if let Some(JsObj::Func(f)) = h.get_mut(cf) {
7597 f.home_class = Some(name.to_string());
7598 }
7599 }
7600 let cval = ClassVal {
7601 name: name.to_string(),
7602 ctor: ctor_opt,
7603 parent: parent_opt,
7604 proto: proto.clone(),
7605 statics: IndexMap::new(),
7606 fields: Vec::new(),
7607 source_def,
7608 };
7609 let class_val = h.alloc(JsObj::Class(cval));
7610 h.class_registry.insert(name.to_string(), class_val.clone());
7611 // Link prototype → class (for instance display + `constructor`), and give
7612 // the class its own `prototype` fn-prop so `C.prototype` reads work.
7613 h.tag_proto_class(&proto, class_val.clone());
7614 h.set_fn_prop(&class_val, "prototype", proto.clone());
7615 // `Class.prototype.constructor === Class`.
7616 if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
7617 p.insert("constructor".to_string(), class_val.clone());
7618 }
7619 h.hide_prop(&proto, "constructor");
7620 class_val
7621 })
7622}
7623
7624/// Install a method / getter / setter on a class (`DEF_MEMBER`). `kind` is a
7625/// `member::*` tag; `is_static` targets the constructor side.
7626pub fn define_member(class_val: &Value, name: &str, kind: i64, is_static: bool, func: Value) {
7627 with_host(|h| {
7628 let cname = match h.get(class_val) {
7629 Some(JsObj::Class(c)) => c.name.clone(),
7630 _ => String::new(),
7631 };
7632 // A private method/accessor: remember which class declared it, so a
7633 // brand-check failure can name the class the way node does. A static
7634 // FIELD is data, not a method, so it keeps the field wording.
7635 if name.starts_with('#') && kind != member::STATIC_FIELD {
7636 h.note_private_method(name);
7637 }
7638 // Give the method its home class for `super.x()`, and record whether it
7639 // is static — `super` resolves against a different object either way.
7640 if let Some(JsObj::Func(f)) = h.get_mut(&func) {
7641 f.home_class = Some(cname);
7642 f.home_static = is_static;
7643 }
7644 // Static members live on the constructor (fn-props / static accessors);
7645 // instance members on the prototype.
7646 let target = if is_static {
7647 class_val.clone()
7648 } else {
7649 match h.get(class_val) {
7650 Some(JsObj::Class(c)) => c.proto.clone(),
7651 _ => return,
7652 }
7653 };
7654 match kind {
7655 member::GET => h.set_accessor(&target, name, Some(func), None),
7656 member::SET => h.set_accessor(&target, name, None, Some(func)),
7657 _ => {
7658 // A static field is enumerable (`Object.keys(C)` lists it) unlike
7659 // a method, so it must not reach the `hide_prop` below.
7660 if kind == member::STATIC_FIELD {
7661 if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7662 c.statics.insert(name.to_string(), func.clone());
7663 }
7664 h.set_fn_prop(class_val, name, func);
7665 return;
7666 }
7667 if is_static {
7668 if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7669 c.statics.insert(name.to_string(), func.clone());
7670 }
7671 h.set_fn_prop(class_val, name, func);
7672 } else if let Some(JsObj::Object(p)) = h.get_mut(&target) {
7673 p.insert(name.to_string(), func);
7674 }
7675 }
7676 }
7677 // Class methods and accessors are non-enumerable (ES2015 ClassDefinition-
7678 // Evaluation), so `for (k in instance)` walking the prototype chain never
7679 // yields them and `Object.keys(C.prototype)` is empty.
7680 h.hide_prop(&target, name);
7681 });
7682}
7683
7684/// Register an instance-field initializer thunk on a class (`DEF_FIELD`).
7685pub fn define_field(class_val: &Value, name: &str, thunk: Value, name_anon: bool) {
7686 with_host(|h| {
7687 if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
7688 c.fields.push((name.to_string(), thunk, name_anon));
7689 }
7690 });
7691}
7692
7693/// The `[[Prototype]]` object a constructor value hands to its instances
7694/// (`Ctor.prototype`), for `instanceof`.
7695fn ctor_prototype(h: &JsHost, ctor: &Value) -> Option<Value> {
7696 match h.get(ctor) {
7697 Some(JsObj::Class(c)) => Some(c.proto.clone()),
7698 Some(JsObj::Func(_)) => h.fn_prop(ctor, "prototype"),
7699 // A builtin's prototype lives in one of two registries: the error
7700 // prototypes, or the native exotic prototypes (`Buffer.prototype`,
7701 // `Uint8Array.prototype`). Consulting only the first made `instanceof`
7702 // blind to the real `Buffer.prototype → Uint8Array.prototype` chain, so
7703 // `Buffer.prototype instanceof Uint8Array` read false even though the
7704 // link was there — the instance case only passed via a native-tag
7705 // special case, which a prototype object does not carry.
7706 Some(JsObj::Builtin(name)) => h
7707 .error_protos
7708 .get(name)
7709 .or_else(|| h.native_protos.get(name))
7710 .cloned(),
7711 Some(JsObj::BoundFunc { target, .. }) => ctor_prototype(h, &target.clone()),
7712 _ => None,
7713 }
7714}
7715
7716/// `ctor.prototype` in the SAME representation `builtins::prototype_of` yields,
7717/// so a chain walk driven by that function can compare the two with `strict_eq`.
7718///
7719/// `ctor_prototype` answers only for the constructors whose prototype object
7720/// really exists on the heap (classes, user functions, the error and native
7721/// exotics). A bare builtin like `Object`/`Array` has none there — its instances
7722/// report `h.object_proto()` / a `Builtin("<C>.prototype")` handle — so this
7723/// mirrors that fallback rather than reporting "no prototype" and failing every
7724/// comparison.
7725fn walk_target_prototype(ctor: &Value) -> Option<Value> {
7726 if let Some(p) = with_host(|h| ctor_prototype(h, ctor)) {
7727 return Some(p);
7728 }
7729 let name = with_host(|h| match h.get(ctor) {
7730 Some(JsObj::Builtin(n)) => Some(n.clone()),
7731 _ => None,
7732 })?;
7733 if name == "Object" {
7734 return Some(with_host(|h| h.object_proto()));
7735 }
7736 Some(with_host(|h| {
7737 h.alloc(JsObj::Builtin(format!("{name}.prototype")))
7738 }))
7739}
7740
7741/// V8's "not a function" wording for a value that was expected to be callable.
7742/// A number/string/boolean is named WITH its value (`number 1 is not a
7743/// function`, `string "s" is not a function`); every other type is named by type
7744/// alone (`object is not a function`, `symbol is not a function`).
7745pub fn not_a_function_message(v: &Value) -> String {
7746 with_host(|h| match v {
7747 Value::Undef => "undefined is not a function".into(),
7748 Value::Bool(b) => format!("boolean {b} is not a function"),
7749 Value::Int(_) | Value::Float(_) => format!("number {} is not a function", h.str_of(v)),
7750 Value::Str(s) => format!("string \"{s}\" is not a function"),
7751 Value::Obj(_) => match h.get(v) {
7752 Some(JsObj::Str(s)) => format!("string \"{s}\" is not a function"),
7753 Some(JsObj::Symbol { .. }) => "symbol is not a function".into(),
7754 Some(JsObj::BigInt(_)) => "bigint is not a function".into(),
7755 _ => "object is not a function".into(),
7756 },
7757 _ => "object is not a function".into(),
7758 })
7759}
7760
7761/// `obj instanceof ctor` — walk `obj`'s prototype chain looking for
7762/// `ctor.prototype`.
7763pub fn instance_of(obj: &Value, ctor: &Value) -> Result<bool, String> {
7764 // 13.10.2 InstanceofOperator step 3: a `Symbol.hasInstance` method on the
7765 // right-hand side REPLACES the prototype-chain walk entirely, and it is
7766 // consulted before the callability check — which is why a plain (uncallable)
7767 // object that defines it is a legal `instanceof` right-hand side.
7768 if matches!(ctor, Value::Obj(_)) {
7769 // `class C { static [Symbol.hasInstance](){} }` and a method defined on a
7770 // plain function both land in the fn-prop side table (which
7771 // `class_static` reads, following the `extends` chain), NOT in an object
7772 // property map — so consulting only `lookup_chain` would find the object
7773 // literal form and silently miss the two forms V8 users actually write.
7774 let handler = match with_host(|h| h.class_static(ctor, "@@hasInstance")) {
7775 Some(f) => Some(f),
7776 None => protocol_lookup(ctor, "@@hasInstance")?,
7777 };
7778 // GetMethod (7.3.11) treats only `undefined`/`null` as "absent"; anything
7779 // else that is not callable is a TypeError, so a data property here does
7780 // NOT fall back to the prototype walk.
7781 match handler {
7782 Some(f) if with_host(|h| is_callable(h, &f)) => {
7783 let r = invoke(&f, vec![obj.clone()], Some(ctor.clone()))?;
7784 return Ok(with_host(|h| h.truthy(&r)));
7785 }
7786 Some(f)
7787 if !matches!(f, Value::Undef)
7788 && !with_host(|h| matches!(h.get(&f), Some(JsObj::Null))) =>
7789 {
7790 return Err(type_error(¬_a_function_message(&f)));
7791 }
7792 _ => {}
7793 }
7794 }
7795 // 13.10.2 InstanceofOperator validates the RIGHT-hand side FIRST, so
7796 // `1 instanceof 3` throws even though the left side could never match.
7797 // Returning early on the left side skipped that check entirely.
7798 let ctor_callable = with_host(|h| {
7799 matches!(
7800 h.get(ctor),
7801 Some(JsObj::Func(_))
7802 | Some(JsObj::Class(_))
7803 | Some(JsObj::Builtin(_))
7804 | Some(JsObj::BoundFunc { .. })
7805 )
7806 });
7807 if !ctor_callable {
7808 // V8 has TWO messages here and they are not interchangeable: a primitive
7809 // right-hand side is "not an object", an object that is merely not
7810 // callable is "not callable". Only the second was implemented, so
7811 // `1 instanceof 3` reported nothing at all.
7812 return Err(type_error(if with_host(|h| !is_primitive(h, ctor)) {
7813 "Right-hand side of 'instanceof' is not callable"
7814 } else {
7815 "Right-hand side of 'instanceof' is not an object"
7816 }));
7817 }
7818 // A non-object left-hand side is never an instance — but only after the
7819 // right-hand side has been validated above.
7820 if !matches!(obj, Value::Obj(_)) {
7821 return Ok(false);
7822 }
7823 // A Proxy shares no heap variant with its target, so the structural arms
7824 // below would misclassify it. 10.5.3 says `OrdinaryHasInstance` walks
7825 // `[[GetPrototypeOf]]`, i.e. the handler's `getPrototypeOf` trap — run that
7826 // walk here, which also gives a custom trap the final say.
7827 if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Proxy) {
7828 with_host(|h| {
7829 h.ensure_error_protos();
7830 h.ensure_native_protos();
7831 });
7832 let Some(target) = walk_target_prototype(ctor) else {
7833 return Ok(false);
7834 };
7835 let mut cur = crate::proxy::get_prototype_of(obj)?.unwrap_or(Value::Undef);
7836 for _ in 0..100 {
7837 if matches!(cur, Value::Undef) || with_host(|h| h.is_null(&cur)) {
7838 return Ok(false);
7839 }
7840 if with_host(|h| h.strict_eq(&cur, &target)) {
7841 return Ok(true);
7842 }
7843 cur = crate::builtins::prototype_of(&cur);
7844 }
7845 return Ok(false);
7846 }
7847 // Builtin constructors whose instances aren't prototype-linked in our model
7848 // (arrays/plain objects/functions) get a structural instanceof.
7849 if let Some(JsObj::Builtin(name)) = with_host(|h| h.get(ctor).cloned()) {
7850 // …but an object whose chain PASSES THROUGH the intrinsic prototype is
7851 // an instance regardless of its own kind, which is the whole of the ES5
7852 // subclassing pattern: `F.prototype = Object.create(Array.prototype)`
7853 // makes `new F() instanceof Array` true. A structural test alone said
7854 // false.
7855 if crate::builtins::chain_intrinsic_ctors_pub(obj).contains(&name.as_str()) {
7856 return Ok(true);
7857 }
7858 let kind = with_host(|h| h.get(obj).cloned());
7859 match name.as_str() {
7860 "Array" => return Ok(matches!(kind, Some(JsObj::Array(_)))),
7861 "Function" => return Ok(with_host(|h| is_callable(h, obj))),
7862 // Map/Set/Promise instances are distinct heap variants, not
7863 // prototype-linked, so match them structurally (a WeakMap/WeakSet is a
7864 // Map/Set with `weak: true`, so `weakMap instanceof Map` is false).
7865 "Map" => return Ok(matches!(kind, Some(JsObj::Map { weak: false, .. }))),
7866 "WeakMap" => return Ok(matches!(kind, Some(JsObj::Map { weak: true, .. }))),
7867 "Set" => return Ok(matches!(kind, Some(JsObj::Set { weak: false, .. }))),
7868 "WeakSet" => return Ok(matches!(kind, Some(JsObj::Set { weak: true, .. }))),
7869 "Promise" => return Ok(matches!(kind, Some(JsObj::Promise { .. }))),
7870 // A RegExp is its own heap variant too, not a prototype-linked object.
7871 "RegExp" => return Ok(matches!(kind, Some(JsObj::RegExp(_)))),
7872 "Object" => {
7873 // Everything object-typed except a null-prototype object is an
7874 // Object instance.
7875 let is_obj = matches!(
7876 kind,
7877 Some(JsObj::Object(_))
7878 | Some(JsObj::Array(_))
7879 // A namespace object and a builtin function are both
7880 // `instanceof Object`: `Math instanceof Object` is true.
7881 | Some(JsObj::Builtin(_))
7882 | Some(JsObj::Func(_))
7883 | Some(JsObj::Class(_))
7884 | Some(JsObj::Map { .. })
7885 | Some(JsObj::Set { .. })
7886 | Some(JsObj::Promise { .. })
7887 | Some(JsObj::Generator { .. })
7888 | Some(JsObj::RegExp(_))
7889 );
7890 if is_obj {
7891 // A null-prototype object (Object.create(null) or
7892 // setPrototypeOf(o, null)) is NOT an Object instance.
7893 if with_host(|h| h.has_null_proto(obj)) {
7894 return Ok(false);
7895 }
7896 return Ok(true);
7897 }
7898 return Ok(false);
7899 }
7900 // A Node `Buffer` IS a `Uint8Array` subclass instance.
7901 "Uint8Array" if crate::stdlib::native_tag(obj).as_deref() == Some("Buffer") => {
7902 return Ok(true);
7903 }
7904 // Every typed array carries the same `TypedArray` tag; the constructor
7905 // it is an instance of is its ELEMENT KIND.
7906 k if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") => {
7907 return Ok(crate::stdlib::typedarray::kind_of(obj) == k);
7908 }
7909 // A native-tagged instance (`WeakRef`, `FinalizationRegistry`,
7910 // `TextEncoder`, …) is an instance of the builtin whose name matches
7911 // its hidden `@@native` tag.
7912 other => {
7913 if crate::stdlib::native_tag(obj).as_deref() == Some(other) {
7914 return Ok(true);
7915 }
7916 }
7917 }
7918 }
7919 with_host(|h| h.ensure_error_protos());
7920 // The native exotic prototypes are built lazily; `instanceof` may be the
7921 // first thing to ask for them, so materialise them before the chain walk.
7922 with_host(|h| h.ensure_native_protos());
7923 let target = match with_host(|h| ctor_prototype(h, ctor)) {
7924 Some(p) => p,
7925 None => return Ok(false),
7926 };
7927 let mut cur = with_host(|h| h.proto_of(obj));
7928 while let Some(p) = cur {
7929 if with_host(|h| h.strict_eq(&p, &target)) {
7930 return Ok(true);
7931 }
7932 cur = with_host(|h| h.proto_of(&p));
7933 }
7934 Ok(false)
7935}
7936
7937// ── generators (stackful coroutines, same-thread via corosensei) ─────────────
7938
7939impl JsHost {
7940 /// Swap the volatile execution context in one shot, returning the previous
7941 /// one — installs a generator's context on resume, pulls it back on suspend.
7942 fn install_gen_ctx(&mut self, mut c: GenContext) -> GenContext {
7943 std::mem::swap(&mut self.frames, &mut c.frames);
7944 std::mem::swap(&mut self.error, &mut c.error);
7945 std::mem::swap(&mut self.exc, &mut c.exc);
7946 std::mem::swap(&mut self.signal, &mut c.signal);
7947 c
7948 }
7949 pub fn is_generator_val(&self, v: &Value) -> bool {
7950 matches!(self.get(v), Some(JsObj::Generator { .. }))
7951 }
7952 /// Whether `v` is an ASYNC generator object — the borrow-free form of
7953 /// [`is_async_generator`], usable from code already holding the host.
7954 pub fn is_async_gen_val(&self, v: &Value) -> bool {
7955 match self.get(v) {
7956 Some(JsObj::Generator { id }) => self
7957 .generators
7958 .get(*id as usize)
7959 .map(|g| g.async_gen)
7960 .unwrap_or(false),
7961 _ => false,
7962 }
7963 }
7964 pub fn gen_done(&self, id: u32) -> bool {
7965 self.generators
7966 .get(id as usize)
7967 .map(|g| g.done)
7968 .unwrap_or(true)
7969 }
7970 fn gen_started(&self, id: u32) -> bool {
7971 self.generators
7972 .get(id as usize)
7973 .map(|g| g.started)
7974 .unwrap_or(false)
7975 }
7976}
7977
7978/// Build a suspended generator whose body is `chunk`, run in a frame with the
7979/// already-bound `env`. Nothing executes until the first `gen_resume`.
7980fn make_generator(
7981 chunk: Chunk,
7982 env: Env,
7983 this_val: Option<Value>,
7984 home_class: Option<String>,
7985 home_static: bool,
7986 home_object: Option<Value>,
7987 strict: bool,
7988) -> Value {
7989 let home = home_class
7990 .as_ref()
7991 .and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
7992 let frame = Frame {
7993 base_env: env.clone(),
7994 env,
7995 this_obj: this_val,
7996 new_target: None,
7997 home_class: home,
7998 home_static,
7999 home_object,
8000 strict,
8001 line: 0,
8002 owner: None,
8003 is_module: false,
8004 this_state: ThisState::Plain,
8005 };
8006 let id = with_host(|h| {
8007 let id = h.generators.len() as u32;
8008 h.generators.push(GenCell {
8009 coro: None,
8010 yielder: std::ptr::null(),
8011 ctx: GenContext {
8012 frames: vec![frame],
8013 ..GenContext::default()
8014 },
8015 done: false,
8016 started: false,
8017 inject: None,
8018 async_gen: false,
8019 queue: std::collections::VecDeque::new(),
8020 running: false,
8021 stack_floor: 0,
8022 });
8023 id
8024 });
8025 let body = move |yielder: &corosensei::Yielder<Value, Value>, _first: Value| {
8026 ensure_coroutine_floor();
8027 // Same thread → publish the yielder so `yield` (deep in the body's VM)
8028 // can reach it. Valid for the whole body lifetime.
8029 with_host(|h| h.generators[id as usize].yielder = yielder as *const _ as *const ());
8030 let r = run_chunk_on(chunk);
8031 // A `return` inside the body leaves a Return signal carrying the final
8032 // value; capture it so `.next()` reports it as the completion value.
8033 let ret = with_host(|h| match h.signal.take() {
8034 Some(Signal::Return(v)) => v,
8035 _ => Value::Undef,
8036 });
8037 r.map(|_| ret)
8038 };
8039 // The body's stack is allocated here rather than left to `Coroutine::new` so
8040 // that its size is ours to choose and, above all, so its `limit()` is known:
8041 // that address is what `stack_exhausted` must compare against while the body
8042 // runs, since a coroutine does NOT run on the thread stack pthread reports.
8043 // A refused reservation still yields a working generator on corosensei's own
8044 // 1 MiB default, with a floor derived on entry instead.
8045 let (coro, floor) = match corosensei::stack::DefaultStack::new(CORO_STACK_SIZE) {
8046 Ok(stack) => {
8047 let floor = coro_stack_floor(&stack);
8048 (corosensei::Coroutine::with_stack(stack, body), floor)
8049 }
8050 Err(_) => (corosensei::Coroutine::new(body), 0),
8051 };
8052 with_host(|h| {
8053 h.generators[id as usize].coro = Some(coro);
8054 h.generators[id as usize].stack_floor = floor;
8055 });
8056 with_host(|h| h.alloc(JsObj::Generator { id }))
8057}
8058
8059/// `yield v` — suspend the running generator, handing `v` to the resumer; returns
8060/// the value the next `gen_resume(x)` supplies (a `.next(x)` argument).
8061pub fn gen_yield(v: Value) -> Result<Value, String> {
8062 let id = match CUR_GEN.with(|c| c.get()) {
8063 Some(id) => id,
8064 None => return Err(type_error("yield outside a generator")),
8065 };
8066 let yp = with_host(|h| h.generators[id as usize].yielder);
8067 // SAFETY: same-thread coroutine; the yielder lives for the whole body, and we
8068 // only reach here from inside that body (its stack is live).
8069 let yielder = unsafe { &*(yp as *const corosensei::Yielder<Value, Value>) };
8070 let sent = yielder.suspend(v);
8071 // On resume, a `.return(v)`/`.throw(e)` may have queued a forced completion:
8072 // convert it into a Return signal / thrown value so the body unwinds and any
8073 // `finally` runs, exactly as a source-level `return`/`throw` would.
8074 if let Some(inj) = with_host(|h| h.generators[id as usize].inject.take()) {
8075 match inj {
8076 GenInject::Return(rv) => {
8077 with_host(|h| h.signal = Some(Signal::Return(rv)));
8078 return Ok(Value::Undef);
8079 }
8080 GenInject::Throw(ev) => {
8081 let msg = with_host(|h| crate::builtins::error_string(h, &ev));
8082 with_host(|h| h.exc = Some(ev));
8083 return Err(msg);
8084 }
8085 }
8086 }
8087 Ok(sent)
8088}
8089
8090/// `generator.return(v)`: force the generator to complete, running any pending
8091/// `finally`. If it is already done (or never started) it just reports
8092/// `{value:v, done:true}` without executing the body.
8093pub fn gen_return(gen: &Value, v: Value) -> Result<GenStep, String> {
8094 let id = match with_host(|h| h.get(gen).cloned()) {
8095 Some(JsObj::Generator { id }) => id,
8096 _ => return Err(type_error("not a generator")),
8097 };
8098 // Not started yet (coro present, ctx never resumed) OR already done → no body
8099 // to unwind: complete immediately with the supplied value.
8100 let started = with_host(|h| h.gen_started(id));
8101 if with_host(|h| h.generators[id as usize].done) || !started {
8102 with_host(|h| h.generators[id as usize].done = true);
8103 return Ok(GenStep::Done(v));
8104 }
8105 with_host(|h| h.generators[id as usize].inject = Some(GenInject::Return(v)));
8106 gen_resume(gen, Value::Undef)
8107}
8108
8109/// `generator.throw(e)`: inject a throw at the suspension point, running any
8110/// pending `finally` and letting an enclosing `try/catch` in the body handle it.
8111pub fn gen_throw(gen: &Value, e: Value) -> Result<GenStep, String> {
8112 let id = match with_host(|h| h.get(gen).cloned()) {
8113 Some(JsObj::Generator { id }) => id,
8114 _ => return Err(type_error("not a generator")),
8115 };
8116 let started = with_host(|h| h.gen_started(id));
8117 if with_host(|h| h.generators[id as usize].done) || !started {
8118 // A throw into a done/unstarted generator propagates to the caller.
8119 with_host(|h| h.generators[id as usize].done = true);
8120 let msg = with_host(|h| crate::builtins::error_string(h, &e));
8121 with_host(|h| h.exc = Some(e));
8122 return Err(msg);
8123 }
8124 with_host(|h| h.generators[id as usize].inject = Some(GenInject::Throw(e)));
8125 gen_resume(gen, Value::Undef)
8126}
8127
8128/// Outcome of resuming a generator: a yielded value (not done), or the final
8129/// completion value (done).
8130pub enum GenStep {
8131 Yield(Value),
8132 Done(Value),
8133}
8134
8135/// Resume a generator until its next `yield` or its body returns. Preserves the
8136/// shared host: the coroutine is taken out so the body re-enters `with_host`
8137/// freely, and the volatile context is swapped so the caller's frames/signal
8138/// survive the switch.
8139pub fn gen_resume(gen: &Value, send: Value) -> Result<GenStep, String> {
8140 let id = match with_host(|h| h.get(gen).cloned()) {
8141 Some(JsObj::Generator { id }) => id,
8142 _ => return Err(type_error("not a generator")),
8143 };
8144 if with_host(|h| h.generators[id as usize].done) {
8145 return Ok(GenStep::Done(Value::Undef));
8146 }
8147 let mut coro = match with_host(|h| h.generators[id as usize].coro.take()) {
8148 Some(c) => c,
8149 None => return Err("TypeError: generator already executing".into()),
8150 };
8151 with_host(|h| h.generators[id as usize].started = true);
8152 let gen_ctx = with_host(|h| std::mem::take(&mut h.generators[id as usize].ctx));
8153 let caller_ctx = with_host(|h| h.install_gen_ctx(gen_ctx));
8154 let prev = CUR_GEN.with(|c| c.replace(Some(id)));
8155 // The body runs on the coroutine's OWN stack, so the guard's floor has to
8156 // move with it and move back on suspend — generators nest, and a resume from
8157 // inside another generator must restore that one's floor, not the thread's.
8158 let coro_floor = with_host(|h| h.generators[id as usize].stack_floor);
8159 let caller_floor = swap_stack_floor(coro_floor);
8160
8161 let out = coro.resume(send); // no host borrow held; body drives its own VM
8162
8163 let measured = swap_stack_floor(caller_floor);
8164 // A coroutine on corosensei's default stack has no known bounds, so the
8165 // floor it measured for itself on first entry is kept for later resumes.
8166 if coro_floor == 0 && measured != 0 {
8167 with_host(|h| h.generators[id as usize].stack_floor = measured);
8168 }
8169 CUR_GEN.with(|c| c.set(prev));
8170 let mut gen_ctx = with_host(|h| h.install_gen_ctx(caller_ctx));
8171 // A `throw` inside the body left the thrown VALUE in the generator's context,
8172 // which the swap above just stashed away. Hand it to the caller so the
8173 // rejection/catch keeps the original error object instead of a string rebuild.
8174 let thrown = gen_ctx.exc.take();
8175 with_host(|h| {
8176 if let Some(v) = thrown {
8177 h.exc = Some(v);
8178 }
8179 h.generators[id as usize].ctx = gen_ctx;
8180 h.generators[id as usize].coro = Some(coro);
8181 });
8182
8183 match out {
8184 corosensei::CoroutineResult::Yield(y) => Ok(GenStep::Yield(y)),
8185 corosensei::CoroutineResult::Return(r) => {
8186 // Release the coroutine — and with it the mmap'd stack it owns —
8187 // the moment the body completes. `h.generators` only ever grows (an
8188 // id is never reused), so a program that awaits in a loop otherwise
8189 // accumulates one whole [`CORO_STACK_SIZE`] reservation per call for
8190 // the life of the process. A finished generator is never resumed:
8191 // `gen_resume` returns `Done` on the `done` flag before it looks.
8192 with_host(|h| {
8193 let g = &mut h.generators[id as usize];
8194 g.done = true;
8195 g.coro = None;
8196 });
8197 match r {
8198 Ok(v) => Ok(GenStep::Done(v)),
8199 Err(e) => Err(e),
8200 }
8201 }
8202 }
8203}
8204
8205/// Force a generator to completion (used by `.return()` and abandoned loops):
8206/// marks it done without running further.
8207pub fn gen_close(gen: &Value) {
8208 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(gen).cloned()) {
8209 with_host(|h| h.generators[id as usize].done = true);
8210 }
8211}
8212
8213// ── iteration protocol (arrays, strings, Map/Set, generators, Symbol.iterator) ─
8214
8215/// Convert a Map/Set key value into a `MapKey` under SameValueZero.
8216pub fn map_key(h: &JsHost, v: &Value) -> MapKey {
8217 match v {
8218 Value::Undef => MapKey::Undef,
8219 Value::Bool(b) => MapKey::Bool(*b),
8220 Value::Int(n) => MapKey::Num(norm_num_bits(*n as f64)),
8221 Value::Float(f) => MapKey::Num(norm_num_bits(*f)),
8222 Value::Str(s) => MapKey::Str((**s).clone()),
8223 Value::Obj(i) => match h.get(v) {
8224 Some(JsObj::Str(s)) => MapKey::Str(s.clone()),
8225 Some(JsObj::Null) => MapKey::Null,
8226 Some(JsObj::BigInt(b)) => MapKey::Big(b.to_string()),
8227 Some(JsObj::Builtin(n)) => MapKey::Intrinsic(builtin_identity(n).to_string()),
8228 _ => MapKey::Ref(*i),
8229 },
8230 _ => MapKey::Undef,
8231 }
8232}
8233
8234/// Canonical bit pattern for a Map/Set numeric key: `NaN` → one value, `-0` → `+0`.
8235fn norm_num_bits(f: f64) -> u64 {
8236 if f.is_nan() {
8237 return f64::NAN.to_bits();
8238 }
8239 if f == 0.0 {
8240 return 0.0f64.to_bits(); // fold -0 into +0
8241 }
8242 f.to_bits()
8243}
8244
8245/// Fully materialize any iterable into a vector of values.
8246/// Pull at most `n` values, then close the iterator — 8.6.2
8247/// IteratorBindingInitialization, which is what an array destructuring pattern
8248/// without a `...rest` element performs.
8249///
8250/// The distinction from [`iter_all`] is not an optimization. A pattern names a
8251/// fixed number of targets, so the spec pulls exactly that many and calls
8252/// IteratorClose on whatever is left; draining instead made
8253///
8254/// ```text
8255/// const [first] = infiniteGenerator();
8256/// ```
8257///
8258/// run forever. It is also observable on any finite iterator, as the count of
8259/// `next()` calls and whether `return()` ever ran.
8260///
8261/// A `...rest` element genuinely consumes the remainder, so those patterns keep
8262/// using `iter_all` and an unbounded source hangs there in node too.
8263pub fn iter_take(v: &Value, n: usize) -> Result<Vec<Value>, String> {
8264 // A Proxy iterates through its traps, which materialize eagerly; there is
8265 // no step-wise form to bound, so this keeps the draining behaviour.
8266 if let Some(items) = crate::proxy::iterate(v)? {
8267 return Ok(items.into_iter().take(n).collect());
8268 }
8269 if with_host(|h| h.is_generator_val(v)) {
8270 let mut out = Vec::new();
8271 while out.len() < n {
8272 match gen_resume(v, Value::Undef)? {
8273 GenStep::Yield(x) => out.push(x),
8274 _ => return Ok(out), // ran out on its own; nothing left to close
8275 }
8276 }
8277 // Stopped early: `.return()` resumes it at the yield so `finally` runs.
8278 let _ = gen_return(v, Value::Undef);
8279 return Ok(out);
8280 }
8281 if let Some(iter_fn) = user_iterator_fn(v) {
8282 let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8283 let mut out = Vec::new();
8284 while out.len() < n {
8285 let step = call_method(&iterator, "next", Vec::new())?;
8286 // Read first: resolving the property re-enters the host, so doing
8287 // it inside the `with_host` closure double-borrows and aborts.
8288 let done = get_prop_chain(&step, "done")?;
8289 if with_host(|h| h.truthy(&done)) {
8290 return Ok(out);
8291 }
8292 out.push(get_prop_chain(&step, "value")?);
8293 }
8294 // IteratorClose: `return` is optional on the protocol, and a throw from
8295 // it is swallowed here the way a normal (non-abrupt) completion does.
8296 if let Ok(ret) = get_prop_chain(&iterator, "return") {
8297 if with_host(|h| is_callable(h, &ret)) {
8298 let _ = invoke(&ret, Vec::new(), Some(iterator.clone()));
8299 }
8300 }
8301 return Ok(out);
8302 }
8303 // Arrays, strings, Map/Set: already materialized, and their built-in
8304 // iterators carry no `return`, so there is nothing to close. The same
8305 // reachability rule as `iter_all` applies — this is the DESTRUCTURING
8306 // entry point, and `const [x] = a` bound 1 from an array whose prototype no
8307 // longer carried `Symbol.iterator`.
8308 if !crate::builtins::own_intrinsic_reachable_pub(v) {
8309 let shown = with_host(|h| h.inspect(v));
8310 return Err(type_error(&format!("{shown} is not iterable")));
8311 }
8312 with_host(|h| h.iter_vec(v)).map(|items| items.into_iter().take(n).collect())
8313}
8314
8315pub fn iter_all(v: &Value) -> Result<Vec<Value>, String> {
8316 // A Proxy iterates through its traps (see `crate::proxy::iterate`); it has
8317 // no heap variant `iter_vec` could recognise.
8318 if let Some(items) = crate::proxy::iterate(v)? {
8319 return Ok(items);
8320 }
8321 // Generators / user iterators must resume without a live host borrow.
8322 if with_host(|h| h.is_generator_val(v)) {
8323 let mut out = Vec::new();
8324 while let GenStep::Yield(x) = gen_resume(v, Value::Undef)? {
8325 out.push(x);
8326 }
8327 return Ok(out);
8328 }
8329 // A live array iterator is stepped to the end, so an accessor or a hole
8330 // reads as `a[i]` does and the iterator is left exhausted, as spreading it
8331 // leaves it in node.
8332 if with_host(|h| matches!(h.get(v), Some(JsObj::Iter { array: Some(_), .. }))) {
8333 let mut out = Vec::new();
8334 while let Some(Some(x)) = crate::builtins::iter_step(v) {
8335 out.push(x);
8336 }
8337 return Ok(out);
8338 }
8339 // Object with a user-defined Symbol.iterator: drive its iterator protocol.
8340 // Checked BEFORE the reachability guard below, since an own `Symbol
8341 // .iterator` makes a value iterable no matter what its prototype is.
8342 if let Some(iter_fn) = user_iterator_fn(v) {
8343 let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
8344 return drain_iterator(&iterator);
8345 }
8346 // The fast paths below read a builtin's backing storage directly, which is
8347 // only legitimate while that builtin's `Symbol.iterator` is still
8348 // reachable: replacing the prototype takes it away, and node then reports
8349 // the value as not iterable. Spread, destructuring and `Array.from`'s
8350 // iterable branch all funnel through here.
8351 if !crate::builtins::own_intrinsic_reachable_pub(v) {
8352 let shown = with_host(|h| h.inspect(v));
8353 return Err(type_error(&format!("{shown} is not iterable")));
8354 }
8355 // A String wrapper iterates its code POINTS, exactly as the primitive does
8356 // (22.1.3.34) — `[...new String("ab")]` is `["a","b"]`, not a TypeError.
8357 if let Some(prim) = crate::builtins::wrapped_primitive(v) {
8358 if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) {
8359 return iter_all(&prim);
8360 }
8361 }
8362 // An array's index ACCESSORS are not in its backing vector, so iterating one
8363 // (spread, `for-of`, `Array.from`) has to resolve them the way the
8364 // `Array.prototype` methods do.
8365 let mut items = with_host(|h| h.iter_vec(v))?;
8366 if with_host(|h| matches!(h.get(v), Some(JsObj::Array(_)))) {
8367 crate::builtins::resolve_index_accessors_pub(v, &mut items);
8368 }
8369 Ok(items)
8370}
8371
8372// ── async iteration (`for await (… of …)`) ───────────────────────────────────
8373
8374/// Obtain an async iterator for `for await`. If `src` has a `Symbol.asyncIterator`
8375/// method, use it (its `.next()` returns a promise of `{value, done}`); otherwise
8376/// fall back to the sync iterable, materialized into a `JsObj::Iter` whose values
8377/// are awaited one at a time by `async_step`.
8378pub fn get_async_iterator(src: &Value) -> Result<Value, String> {
8379 if let Some(f) = user_async_iterator_fn(src) {
8380 return invoke(&f, Vec::new(), Some(src.clone()));
8381 }
8382 // An `async function*` object IS its own async iterator; draining it into a
8383 // list here would run the whole body (and any `finally`) before the consumer
8384 // sees the first value.
8385 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(src).cloned()) {
8386 if with_host(|h| h.generators[id as usize].async_gen) {
8387 return Ok(src.clone());
8388 }
8389 }
8390 let items = iter_all(src)?;
8391 Ok(with_host(|h| {
8392 h.alloc(JsObj::Iter {
8393 items,
8394 idx: 0,
8395 array: None,
8396 })
8397 }))
8398}
8399
8400/// If `v` has an own/inherited `Symbol.asyncIterator` method, return it.
8401fn user_async_iterator_fn(v: &Value) -> Option<Value> {
8402 // A PROXY supplies the protocol through its `get` trap and is not a plain
8403 // object, so the shape test below rejects it outright.
8404 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8405 return protocol_lookup(v, "@@asyncIterator")
8406 .ok()
8407 .flatten()
8408 .filter(|f| with_host(|h| is_callable(h, f)));
8409 }
8410 let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8411 if !is_plain {
8412 return None;
8413 }
8414 // Full property resolution, not a stored-property lookup — the same reason
8415 // `user_iterator_fn` does it for the SYNC protocol. A NATIVE-tagged object
8416 // dispatches its methods through the stdlib method table rather than a
8417 // property map, so `lookup_chain` reported no `Symbol.asyncIterator` for one
8418 // even though reading it gives a function: `for await (const v of
8419 // timersPromises.setInterval(…))` said the iterator "is not iterable".
8420 let f = crate::builtins::get_property(v, "@@asyncIterator").ok()?;
8421 with_host(|h| is_callable(h, &f)).then_some(f)
8422}
8423
8424/// One step of a `for await` loop: return a Promise that settles to a
8425/// `{value, done}` record. For a native async iterator this is `iter.next()`
8426/// (already a promise of the record). For the sync fallback it pops the next raw
8427/// value, awaits it, and packages `{value: resolved, done:false}` (or
8428/// `{done:true}` at exhaustion).
8429pub fn async_step(iterator: &Value) -> Result<Value, String> {
8430 // An `async function*` object: resume it through the await-aware driver.
8431 if let Some(JsObj::Generator { id }) = with_host(|h| h.get(iterator).cloned()) {
8432 if with_host(|h| h.generators[id as usize].async_gen) {
8433 return Ok(async_gen_step(iterator, Value::Undef));
8434 }
8435 }
8436 // Sync-fallback iterator: drive it here, awaiting each yielded value.
8437 if let Some(JsObj::Iter { items, idx, .. }) = with_host(|h| h.get(iterator).cloned()) {
8438 if idx >= items.len() {
8439 // `AsyncFromSyncIteratorContinuation` resolves the record THROUGH a
8440 // promise even at exhaustion, so the `done: true` step costs the same
8441 // two microtask ticks a value step does.
8442 let step = with_host(|h| h.new_promise());
8443 let sid = with_host(|h| h.promise_id(&step).unwrap());
8444 with_host(|h| {
8445 h.queue_micro_native(Box::new(move || {
8446 resolve_promise_val(sid, iter_record(Value::Undef, true));
8447 Ok(())
8448 }))
8449 });
8450 return Ok(step);
8451 }
8452 let raw = items[idx].clone();
8453 with_host(|h| {
8454 if let Some(JsObj::Iter { idx, .. }) = h.get_mut(iterator) {
8455 *idx += 1;
8456 }
8457 });
8458 // Await the raw value (adopts a promise's resolution), then wrap.
8459 let step = with_host(|h| h.new_promise());
8460 let sid = with_host(|h| h.promise_id(&step).unwrap());
8461 let raw_p = promise_of(&raw);
8462 let raw_id = with_host(|h| h.promise_id(&raw_p).unwrap());
8463 subscribe_native(
8464 raw_id,
8465 Box::new(move |state, val| {
8466 if state == PromiseState::Rejected {
8467 reject_promise_val(sid, val);
8468 } else {
8469 resolve_promise_val(sid, iter_record(val, false));
8470 }
8471 Ok(())
8472 }),
8473 );
8474 return Ok(step);
8475 }
8476 // Native async iterator: `iter.next()` returns the {value,done} promise.
8477 let r = call_method(iterator, "next", Vec::new())?;
8478 Ok(promise_of(&r))
8479}
8480
8481/// If `v` has an own/inherited `Symbol.iterator` method (internal key
8482/// `@@iterator`), return it. Arrays/strings use the native fast path instead.
8483pub fn user_iterator_fn(v: &Value) -> Option<Value> {
8484 let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
8485 if !is_plain {
8486 return None;
8487 }
8488 // Full property resolution, not a stored-property lookup: a NATIVE-tagged
8489 // object (`URLSearchParams`, `Headers`) dispatches its methods through the
8490 // stdlib method table rather than a property map, so `lookup_chain` reported
8491 // no `Symbol.iterator` for one even though reading it gave a function —
8492 // `[...new URLSearchParams('a=1')]` threw `{} is not iterable`.
8493 let f = crate::builtins::get_property(v, "@@iterator").ok()?;
8494 with_host(|h| is_callable(h, &f)).then_some(f)
8495}
8496
8497/// Drive an iterator object (one with a `.next()` returning `{value, done}`) to
8498/// exhaustion.
8499/// Step `src`'s iterator, handing each value to `f`, and CLOSE the iterator if
8500/// `f` exits abruptly (7.4.9 IteratorClose).
8501///
8502/// The difference from `iter_all` + a loop is that this never materializes the
8503/// whole sequence: `Array.from(infinite, mapFn)` where `mapFn` throws has to
8504/// stop at the first call, and draining first means it never gets there at all.
8505pub fn iter_for_each(
8506 src: &Value,
8507 mut f: impl FnMut(Value, usize) -> Result<(), String>,
8508) -> Result<(), String> {
8509 // Only a USER iterator can be infinite or observe its own close; every
8510 // other shape is already a finite materialized sequence.
8511 let Some(iter_fn) = user_iterator_fn(src) else {
8512 for (i, v) in iter_all(src)?.into_iter().enumerate() {
8513 f(v, i)?;
8514 }
8515 return Ok(());
8516 };
8517 let iterator = invoke(&iter_fn, Vec::new(), Some(src.clone()))?;
8518 let mut i = 0usize;
8519 loop {
8520 let step = call_method(&iterator, "next", Vec::new())?;
8521 let done = get_prop_chain(&step, "done")?;
8522 if with_host(|h| h.truthy(&done)) {
8523 return Ok(());
8524 }
8525 let value = get_prop_chain(&step, "value")?;
8526 if let Err(e) = f(value, i) {
8527 // The callback's error wins over anything `return()` raises, so a
8528 // throwing `return` is swallowed here (7.4.9 step 6).
8529 let _ = close_iterator(&iterator);
8530 return Err(e);
8531 }
8532 i += 1;
8533 }
8534}
8535
8536/// Call `iterator.return()` if it has one, as IteratorClose does.
8537pub fn close_iterator(iterator: &Value) -> Result<(), String> {
8538 let has = crate::builtins::get_property(iterator, "return")?;
8539 if with_host(|h| is_callable(h, &has)) {
8540 call_method(iterator, "return", Vec::new())?;
8541 }
8542 Ok(())
8543}
8544
8545pub(crate) fn drain_iterator(iterator: &Value) -> Result<Vec<Value>, String> {
8546 let mut out = Vec::new();
8547 loop {
8548 let step = call_method(iterator, "next", Vec::new())?;
8549 let done = get_prop_chain(&step, "done")?;
8550 if with_host(|h| h.truthy(&done)) {
8551 break;
8552 }
8553 out.push(get_prop_chain(&step, "value")?);
8554 }
8555 Ok(out)
8556}
8557
8558/// Property read that walks the prototype chain (used by iteration helpers).
8559/// Whether the builtin named `n` is CALLABLE. Most are (`Array`, `parseInt`,
8560/// `Math.floor`); the exceptions are the namespace objects a script can only
8561/// read properties off (`Math`, `JSON`, every `require()`d core module), which
8562/// report `typeof === "object"` and carry no `name`/`length`.
8563pub fn builtin_is_callable(n: &str) -> bool {
8564 // A `<Ctor>.prototype` handle is a namespace of methods, not a function:
8565 // `typeof Set.prototype` is `"object"`, and treating it as callable made it
8566 // brand `[object Function]`, inspect as `[Function: prototype]`, and answer
8567 // `true` to `instanceof Function`. `Function.prototype` is the one that
8568 // really IS callable (10.2.4: it is an anonymous built-in that returns
8569 // undefined), which is why it is not stripped here.
8570 if n != "Function.prototype" && n.ends_with(".prototype") {
8571 return false;
8572 }
8573 // A `match` rather than a slice scan: this runs on every callability test,
8574 // which is every call and every `ToPrimitive`, and a `contains` over the
8575 // list below compares against all 56 entries before answering "callable" —
8576 // the common case. The compiler turns the arms into a length-then-bytes
8577 // decision tree instead.
8578 !matches!(
8579 n,
8580 "Math"
8581 | "JSON"
8582 | "console"
8583 | "Reflect"
8584 | "process"
8585 | "Atomics"
8586 | "performance"
8587 | "fs"
8588 | "path"
8589 | "os"
8590 | "util"
8591 | "crypto"
8592 | "webcrypto"
8593 | "SubtleCrypto"
8594 | "querystring"
8595 | "events"
8596 | "timers"
8597 | "perf_hooks"
8598 | "async_hooks"
8599 | "diagnostics_channel"
8600 | "v8"
8601 | "dns"
8602 | "punycode"
8603 | "child_process"
8604 | "tty"
8605 | "url"
8606 | "zlib"
8607 | "string_decoder"
8608 | "http"
8609 | "net"
8610 | "buffer"
8611 | "function"
8612 | "path/win32"
8613 | "fs/promises"
8614 | "stream/promises"
8615 | "stream/consumers"
8616 | "stream/web"
8617 | "timers/promises"
8618 | "dns/promises"
8619 | "https"
8620 | "http2"
8621 | "tls"
8622 | "dgram"
8623 | "cluster"
8624 | "worker_threads"
8625 | "readline"
8626 | "readline/promises"
8627 | "repl"
8628 | "vm"
8629 | "domain"
8630 | "trace_events"
8631 | "wasi"
8632 | "inspector"
8633 | "object"
8634 )
8635 // The live `require.cache` view is a plain object to a script, not
8636 // something it can call.
8637 && n != crate::builtins::REQUIRE_CACHE
8638}
8639
8640pub fn get_prop_chain(recv: &Value, name: &str) -> Result<Value, String> {
8641 crate::builtins::get_property(recv, name)
8642}
8643
8644/// Whether `v` is an ECMAScript primitive, i.e. `ToPrimitive` is the identity
8645/// on it. `undefined`, `null`, booleans, numbers, strings, symbols and bigints
8646/// qualify; every other heap cell (objects, arrays, functions, `Map`/`Set`,
8647/// native-tagged instances) is an object and must be converted.
8648pub fn is_primitive(h: &JsHost, v: &Value) -> bool {
8649 match v {
8650 Value::Obj(_) => matches!(
8651 h.get(v),
8652 None | Some(JsObj::Null)
8653 | Some(JsObj::Str(_))
8654 | Some(JsObj::Symbol { .. })
8655 | Some(JsObj::BigInt(_))
8656 ),
8657 _ => true,
8658 }
8659}
8660
8661/// `ToPrimitive(v, hint)` — ECMA-262 7.1.1. `hint` is `"default"`, `"number"`
8662/// or `"string"`.
8663///
8664/// An object carrying a `Symbol.toPrimitive` method (internal key
8665/// `@@toPrimitive`) has it called with the hint and must return a primitive.
8666/// Otherwise `OrdinaryToPrimitive` (7.1.1.1) tries `valueOf` then `toString` —
8667/// the order reversed for the string hint — and takes the FIRST call whose
8668/// result is a primitive. An object that yields no primitive (a null-prototype
8669/// object has neither method) throws V8's
8670/// `TypeError: Cannot convert object to primitive value`.
8671///
8672/// This is the conversion behind `+`, `-`/`*`/`/`/`%`/`**`, the relational
8673/// operators, `==` against a primitive, and `ToPropertyKey` — all of which used
8674/// to read `str_of` directly and so never invoked a user `valueOf`.
8675pub fn to_primitive(v: &Value, hint: &str) -> Result<Value, String> {
8676 if with_host(|h| is_primitive(h, v)) {
8677 return Ok(v.clone());
8678 }
8679 if let Some(f) = protocol_lookup(v, "@@toPrimitive")? {
8680 if with_host(|h| is_callable(h, &f)) {
8681 let hv = with_host(|h| h.new_str(hint.to_string()));
8682 let r = invoke(&f, vec![hv], Some(v.clone()))?;
8683 if with_host(|h| is_primitive(h, &r)) {
8684 return Ok(r);
8685 }
8686 return Err(type_error("Cannot convert object to primitive value"));
8687 }
8688 }
8689 // `Date.prototype[@@toPrimitive]` (21.4.4.45) treats the DEFAULT hint as
8690 // `"string"`, which is why `new Date() + 1` concatenates while
8691 // `new Date() - 1` is arithmetic.
8692 let hint = if hint == "default" && crate::stdlib::native_tag(v).as_deref() == Some("Date") {
8693 "string"
8694 } else {
8695 hint
8696 };
8697 let order = if hint == "string" {
8698 ["toString", "valueOf"]
8699 } else {
8700 ["valueOf", "toString"]
8701 };
8702 // Whether either candidate was actually CALLED. The `[object Tag]` fallback
8703 // below is for exotics whose property funnel exposes no callable
8704 // `toString`, not for an object whose own methods ran and returned
8705 // non-primitives — that case is the spec's TypeError, and branding it
8706 // instead meant `({ valueOf: () => ({}), toString: () => ({}) }) + 1`
8707 // quietly produced `"[object Object]1"`.
8708 let mut called_any = false;
8709 for m in order {
8710 let f = crate::builtins::get_property(v, m).unwrap_or(Value::Undef);
8711 if !with_host(|h| is_callable(h, &f)) {
8712 continue;
8713 }
8714 called_any = true;
8715 // On a Proxy the resolved method is a thunk bound to the TARGET, so
8716 // invoking it directly would stringify the target — `String(new
8717 // Proxy(function f(){}, {}))` reported `f`'s source where V8 reports the
8718 // native-code form. `call_method` re-dispatches the generic
8719 // `Function.prototype`/`Object.prototype` methods against the proxy.
8720 let r = if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8721 call_method(v, m, Vec::new())?
8722 } else {
8723 invoke(&f, Vec::new(), Some(v.clone()))?
8724 };
8725 if with_host(|h| is_primitive(h, &r)) {
8726 return Ok(r);
8727 }
8728 }
8729 // Every object except a null-prototype one inherits `Object.prototype
8730 // .toString`, which always returns a string — so the exhausted-methods
8731 // TypeError is reachable only there. The exotics whose property funnel has
8732 // no `toString` entry of its own (`Map`, `Set`, `Promise`, …) land here and
8733 // get the same `[object Tag]` brand V8 gives them.
8734 // A proxy WITH a `get` trap is not one of those exotics: the trap answered
8735 // for both method names, and if what came back was not callable there is
8736 // nothing left to call — `String(new Proxy({}, { get: () => undefined }))`
8737 // is a TypeError on node, where branding it `[object Object]` invented a
8738 // conversion the trap explicitly refused. A TRAPLESS proxy is different: its
8739 // read forwarded to the target, so `String(new Proxy(new Map(), {}))` gets
8740 // the target's `[object Map]` brand exactly as the bare `Map` does.
8741 if !called_any && !with_host(|h| h.has_null_proto(v)) && !crate::proxy::has_trap(v, "get") {
8742 return crate::builtins::proto_method(v, "Object:toString", Vec::new());
8743 }
8744 Err(type_error("Cannot convert object to primitive value"))
8745}
8746
8747/// `ToString(v)` with `ToPrimitive` method dispatch: an object is converted
8748/// with the string hint (so a user `toString` — or `valueOf`, if `toString`
8749/// is absent or returns an object — is invoked), then rendered by `str_of`.
8750/// Returns a heap string value.
8751pub fn to_string_value(v: &Value) -> Result<Value, String> {
8752 let p = to_primitive(v, "string")?;
8753 // `ToString(symbol)` throws (7.1.17 step 2) — the ONLY conversion a symbol
8754 // refuses. `String(sym)` is the documented exception and is handled at that
8755 // call site, not here, so every implicit coercion (`sym + ''`, `` `${sym}` ``,
8756 // `[sym].join()`) rejects the way node does instead of silently rendering
8757 // `Symbol(desc)`.
8758 if with_host(|h| matches!(h.get(&p), Some(JsObj::Symbol { .. }))) {
8759 return Err(type_error("Cannot convert a Symbol value to a string"));
8760 }
8761 Ok(with_host(|h| {
8762 let s = h.str_of(&p);
8763 h.new_str(s)
8764 }))
8765}
8766
8767/// `String(v)` — 22.1.1.1. Identical to [`to_string_value`] except that a
8768/// SYMBOL argument is allowed and renders as `Symbol(desc)` (step 2a).
8769pub fn string_ctor_value(v: &Value) -> Result<Value, String> {
8770 if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8771 return Ok(with_host(|h| {
8772 let s = h.str_of(v);
8773 h.new_str(s)
8774 }));
8775 }
8776 to_string_value(v)
8777}
8778
8779/// `ToNumber(v)` — ECMA-262 7.1.4 — with the object case going through
8780/// `ToPrimitive(v, number)` first, so `+{ valueOf() { return 7 } }` is `7` and
8781/// `+new Date(0)` is `0`. `JsHost::to_number` alone cannot do this: it runs
8782/// under the host borrow and so can never invoke a JS `valueOf`.
8783pub fn to_number_value(v: &Value) -> Result<f64, String> {
8784 // `ToNumber(symbol)` throws (7.1.4 step 2). It is primitive, so without this
8785 // it fell into `to_number` and quietly produced `NaN` — `Number(Symbol())`
8786 // and `+Symbol()` are both `TypeError` on node v26.7.0.
8787 if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
8788 return Err(type_error("Cannot convert a Symbol value to a number"));
8789 }
8790 if let Some(n) = with_host(|h| is_primitive(h, v).then(|| h.to_number(v))) {
8791 return Ok(n);
8792 }
8793 let p = to_primitive(v, "number")?;
8794 // Steps 2-3 apply to the ToPrimitive RESULT, not only to the argument. Only
8795 // the argument was checked, so an object whose conversion yields a symbol or
8796 // a BigInt slipped past: `+Object(9n)` answered 9 and
8797 // `+{ [Symbol.toPrimitive]() { return Symbol('s') } }` answered NaN, where
8798 // both are TypeErrors. `Number(x)` is ToNumeric and keeps its own path,
8799 // which is why `Number(Object(9n))` is still 9.
8800 match with_host(|h| h.get(&p).cloned()) {
8801 Some(JsObj::Symbol { .. }) => Err(type_error("Cannot convert a Symbol value to a number")),
8802 Some(JsObj::BigInt(_)) => Err(type_error("Cannot convert a BigInt value to a number")),
8803 _ => Ok(with_host(|h| h.to_number(&p))),
8804 }
8805}
8806
8807/// `ToPropertyKey(v)` — ECMA-262 7.1.19. A symbol keeps its stable internal
8808/// key; anything else is `ToPrimitive(v, string)` then `ToString`, so
8809/// `obj[{ toString() { return 'k' } }]` really reads `obj.k`.
8810pub fn to_property_key(v: &Value) -> Result<String, String> {
8811 // One borrow for the overwhelmingly common primitive key (`a[i]`, `o[s]`,
8812 // `o[sym]`); only an object key pays for the conversion.
8813 if let Some(k) = with_host(|h| is_primitive(h, v).then(|| h.property_key(v))) {
8814 return Ok(k);
8815 }
8816 let p = to_primitive(v, "string")?;
8817 Ok(with_host(|h| h.str_of(&p)))
8818}
8819
8820/// Whether `h.get(v)` is any callable kind. A Proxy is callable exactly when its
8821/// target is (10.5: the `[[Call]]` slot is installed only for a callable
8822/// target), so `typeof` and every `is_callable` guard agree on one answer.
8823pub fn is_callable(h: &JsHost, v: &Value) -> bool {
8824 match h.get(v) {
8825 // Not every builtin is a function: the namespace objects (`Math`,
8826 // `require('fs')`) and the `<Ctor>.prototype` handles are data, and
8827 // calling one is a `TypeError` in node exactly as `typeof` says.
8828 Some(JsObj::Builtin(n)) => builtin_is_callable(n),
8829 Some(JsObj::Func(_))
8830 | Some(JsObj::BoundMethod { .. })
8831 | Some(JsObj::BoundFunc { .. })
8832 | Some(JsObj::Class(_)) => true,
8833 Some(JsObj::Proxy { target, .. }) => is_callable(h, target),
8834 _ => false,
8835 }
8836}
8837
8838/// Walk `recv`'s own props then its prototype chain for `key`, returning the
8839/// stored value (methods, inherited data props). Does NOT invoke accessors.
8840/// A PROTOCOL lookup — `Symbol.toPrimitive`, `Symbol.hasInstance`, `toJSON`,
8841/// `then` and the rest — which the spec performs with `[[Get]]`.
8842///
8843/// That distinction only shows on a PROXY: `lookup_chain` walks the property
8844/// map and never asks the handler, so a proxy supplying a protocol method
8845/// through its `get` trap was invisible and the operation fell back to the
8846/// default. Everything else takes the cheap chain walk.
8847pub fn protocol_lookup(v: &Value, key: &str) -> Result<Option<Value>, String> {
8848 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8849 let got = crate::builtins::get_property(v, key)?;
8850 return Ok((!matches!(got, Value::Undef)).then_some(got));
8851 }
8852 Ok(with_host(|h| lookup_chain(h, v, key)))
8853}
8854
8855pub fn lookup_chain(h: &JsHost, recv: &Value, key: &str) -> Option<Value> {
8856 if let Some(JsObj::Object(p)) = h.get(recv) {
8857 if let Some(v) = p.get(key) {
8858 return Some(v.clone());
8859 }
8860 }
8861 let mut cur = h.proto_of(recv);
8862 while let Some(p) = cur {
8863 // A chain link may be a plain object OR a function/class (the `router`
8864 // package sets `Router.prototype = function(){}` and hangs its methods off
8865 // that function, so the methods live in the fn-prop side table).
8866 match h.get(&p) {
8867 Some(JsObj::Object(props)) => {
8868 if let Some(v) = props.get(key) {
8869 return Some(v.clone());
8870 }
8871 }
8872 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
8873 if let Some(v) = h.fn_prop(&p, key) {
8874 return Some(v);
8875 }
8876 }
8877 _ => {}
8878 }
8879 cur = h.proto_of(&p);
8880 }
8881 None
8882}
8883
8884/// Find a getter/setter accessor for `key` on `recv` or up its prototype chain.
8885pub fn lookup_accessor(
8886 h: &JsHost,
8887 recv: &Value,
8888 key: &str,
8889) -> Option<(Option<Value>, Option<Value>)> {
8890 if let Some(a) = h.own_accessor(recv, key) {
8891 return Some(a);
8892 }
8893 let mut cur = h.proto_of(recv);
8894 while let Some(p) = cur {
8895 if let Some(a) = h.own_accessor(&p, key) {
8896 return Some(a);
8897 }
8898 cur = h.proto_of(&p);
8899 }
8900 // A STATIC accessor declared by an ancestor class. A subclass reaches its
8901 // parent's statics through `ClassVal.parent`, not the `protos` map the walk
8902 // above reads — classes are not linked there, so that walk ended at once.
8903 // Static methods and fields already inherited because `class_static` does
8904 // this same parent walk for `fn_prop`; only accessors had no equivalent:
8905 //
8906 // class Base { static get kind() { return 'base' } }
8907 // class Sub extends Base {}
8908 // Sub.plain() // worked, a fn_prop
8909 // Sub.kind // undefined; node reads 'base'
8910 //
8911 // The caller invokes the getter with the class it was READ off as `this`,
8912 // so a getter reading `this.x` sees the subclass, per 10.2.4.
8913 let mut cls = recv.clone();
8914 while let Some(JsObj::Class(c)) = h.get(&cls) {
8915 let Some(parent) = c.parent.clone() else {
8916 break;
8917 };
8918 if let Some(a) = h.own_accessor(&parent, key) {
8919 return Some(a);
8920 }
8921 cls = parent;
8922 }
8923 None
8924}
8925
8926/// Register a builtin error prototype (for `instanceof Error` etc.).
8927pub fn set_error_proto(name: &str, proto: Value) {
8928 with_host(|h| {
8929 h.error_protos.insert(name.to_string(), proto);
8930 });
8931}
8932pub fn error_proto(name: &str) -> Option<Value> {
8933 with_host(|h| h.error_protos.get(name).cloned())
8934}
8935/// Error prototype lookup with a borrowed host (used inside a `with_host` block).
8936pub fn error_proto_of(h: &JsHost, name: &str) -> Option<Value> {
8937 h.error_protos.get(name).cloned()
8938}
8939
8940impl JsHost {
8941 /// `Error.prototype.toString` for an object whose prototype chain reaches
8942 /// `Error.prototype`: `"Name"` with an empty message, else `"Name: message"`.
8943 /// `None` for anything that is not an error, so the caller keeps its own
8944 /// stringification.
8945 pub fn error_to_string(&self, v: &Value) -> Option<String> {
8946 let base = self.error_protos.get("Error")?;
8947 let mut cur = self.proto_of(v);
8948 let mut is_error = false;
8949 while let Some(p) = cur {
8950 if self.strict_eq(&p, base) {
8951 is_error = true;
8952 break;
8953 }
8954 cur = self.proto_of(&p);
8955 }
8956 if !is_error {
8957 return None;
8958 }
8959 let name = lookup_chain(self, v, "name")
8960 .map(|n| self.str_of(&n))
8961 .unwrap_or_else(|| "Error".into());
8962 let message = lookup_chain(self, v, "message")
8963 .map(|m| self.str_of(&m))
8964 .unwrap_or_default();
8965 // Node's internal coded errors override `toString` as
8966 // `${name} [${code}]: ${message}` (internal/errors.js NodeError). The
8967 // `@@nodeError` tag marks the errors `synth_error` built from a
8968 // `Name [ERR_CODE]: …` string, so a user error that merely has a `.code`
8969 // property still stringifies plainly.
8970 if let Some(JsObj::Object(p)) = self.get(v) {
8971 if p.contains_key("@@nodeError") {
8972 if let Some(code) = p.get("code").map(|c| self.str_of(c)) {
8973 return Some(format!("{name} [{code}]: {message}"));
8974 }
8975 }
8976 }
8977 Some(match (name.is_empty(), message.is_empty()) {
8978 (true, _) => message,
8979 (false, true) => name,
8980 (false, false) => format!("{name}: {message}"),
8981 })
8982 }
8983}
8984
8985/// The set of builtin error constructor names forming the error hierarchy.
8986pub const ERROR_NAMES: &[&str] = &[
8987 "Error",
8988 "TypeError",
8989 "RangeError",
8990 "SyntaxError",
8991 "ReferenceError",
8992 "EvalError",
8993 "URIError",
8994 "AggregateError",
8995 // `assert`'s error class. It is NOT a global (node exposes it only as
8996 // `assert.AssertionError`, and `GLOBAL_FUNCS` is a separate table), but it
8997 // has to be a name `synth_error` recognizes: without it the head
8998 // `AssertionError [ERR_ASSERTION]: …` failed the class check and fell into
8999 // the `Error` branch with the WHOLE head kept as the message, so `e.name`
9000 // was `Error` and `e.message` carried a prefix node keeps out of it.
9001 "AssertionError",
9002 // The WHATWG error class `AbortSignal.reason` carries. Unlike the others its
9003 // `name` comes from the SECOND constructor argument rather than from the
9004 // class, so its prototype keeps the base default and each instance stamps
9005 // its own name into an internal slot.
9006 "DOMException",
9007];
9008
9009impl JsHost {
9010 /// Lazily build the builtin error prototype chain: `Error.prototype →
9011 /// Object.prototype`, and every specific error's prototype → `Error.prototype`.
9012 /// Populated once; instances link to these so `e instanceof TypeError` and
9013 /// `e instanceof Error` both hold.
9014 /// The real `Buffer.prototype` object, building the
9015 /// `Buffer.prototype → Uint8Array.prototype → Object.prototype` chain on
9016 /// first use.
9017 ///
9018 /// A `Buffer` used to be a bare tagged object with no `[[Prototype]]` at
9019 /// all, so `Object.getPrototypeOf(buf) === Buffer.prototype` read false and
9020 /// `instanceof` had to be special-cased around it. Each prototype is a
9021 /// genuine object carrying `@proto:<Ctor>:<method>` thunks for its instance
9022 /// methods, so `Buffer.prototype.slice.call(buf, 1)` still dispatches the
9023 /// way it did when `Buffer.prototype` was a `Builtin` namespace.
9024 pub fn ensure_native_protos(&mut self) {
9025 // The wrapper prototypes share this registry and this guard would skip
9026 // them, so they are built through their own.
9027 self.ensure_wrapper_protos();
9028 self.ensure_function_kind_protos();
9029 if self.native_protos.contains_key("Buffer") {
9030 return;
9031 }
9032 let obj_proto = self.object_proto();
9033 // `Object.prototype` is the one builtin prototype that already existed as
9034 // a real object (it is the chain root). Register it so `Object.prototype`
9035 // reads resolve to THAT object rather than a fresh `Builtin` namespace —
9036 // otherwise `Object.getPrototypeOf(C.prototype) === Object.prototype`
9037 // compares a real object against a thunk and reads false.
9038 self.native_protos
9039 .insert("Object".to_string(), obj_proto.clone());
9040 for m in crate::builtins::OBJECT_PROTO_METHODS {
9041 let thunk = self.alloc(JsObj::Builtin(format!("@proto:Object:{m}")));
9042 if let Some(JsObj::Object(p)) = self.get_mut(&obj_proto) {
9043 p.insert((*m).to_string(), thunk);
9044 }
9045 self.hide_prop(&obj_proto, m);
9046 }
9047 // `Buffer.prototype → Uint8Array.prototype → %TypedArray%.prototype →
9048 // Object.prototype`, which is the chain node v26.7.0 really has. The
9049 // shared iteration methods (`every`, `map`, `filter`, …) live on the
9050 // `%TypedArray%.prototype` intermediate, NOT on `Uint8Array.prototype`:
9051 // measured, `Uint8Array.prototype.hasOwnProperty('every')` is false in
9052 // Node while the intermediate owns it. `%TypedArray%` is not a global,
9053 // so it is reachable only by walking the chain — exactly as in Node.
9054 // Every element kind gets its own prototype hanging off the shared
9055 // intermediate, so `Object.getPrototypeOf(new Int32Array(1))` is
9056 // `Int32Array.prototype` rather than some other kind's. Linking them all
9057 // to `Uint8Array.prototype` would have been the easy version and would
9058 // have made an `Int32Array` claim the wrong prototype.
9059 let mut chain: Vec<(&str, Value)> = vec![("TypedArray", obj_proto)];
9060 for kind in crate::stdlib::typedarray::ELEMENT_KINDS {
9061 chain.push((kind, Value::Undef)); // parent: %TypedArray%.prototype
9062 }
9063 // `Buffer.prototype`'s parent is `Uint8Array.prototype` specifically.
9064 chain.push(("Buffer", Value::Undef));
9065 let mut prev: Option<Value> = None;
9066 for (ctor, parent) in chain.drain(..) {
9067 let proto = self.new_object(IndexMap::new());
9068 // Each kind hangs off the shared intermediate; `Buffer` hangs off
9069 // `Uint8Array.prototype`; the intermediate itself off
9070 // `Object.prototype`.
9071 let parent = match ctor {
9072 "TypedArray" => parent,
9073 "Buffer" => self
9074 .native_protos
9075 .get("Uint8Array")
9076 .cloned()
9077 .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
9078 _ => self
9079 .native_protos
9080 .get("TypedArray")
9081 .cloned()
9082 .unwrap_or_else(|| prev.clone().expect("intermediate built first")),
9083 };
9084 self.set_proto(&proto, parent);
9085 // `%TypedArray%.prototype` has no reachable constructor global, so
9086 // it gets no `constructor` slot (Node's is the anonymous
9087 // `%TypedArray%` intrinsic).
9088 if ctor != "TypedArray" {
9089 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9090 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9091 p.insert("constructor".into(), ctor_val);
9092 }
9093 self.hide_prop(&proto, "constructor");
9094 }
9095 let methods: &[&str] = match ctor {
9096 "Buffer" => crate::stdlib::buffer::INSTANCE_METHODS,
9097 "TypedArray" => crate::stdlib::typedarray::PROTOTYPE_METHODS,
9098 // `Uint8Array` alone owns the base64/hex pair — no other view
9099 // has them, which is the whole reason they cannot live on the
9100 // shared `%TypedArray%` prototype above.
9101 "Uint8Array" => crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS,
9102 // Every other kind's prototype owns no methods; it inherits them
9103 // from the intermediate above. It does own `BYTES_PER_ELEMENT`,
9104 // which is per-kind and which Node really keeps there (measured:
9105 // `Uint8Array.prototype.hasOwnProperty('BYTES_PER_ELEMENT')`).
9106 _ => &[],
9107 };
9108 if crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor) {
9109 let bpe = Value::Float(crate::stdlib::typedarray::bytes_per_element(ctor) as f64);
9110 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9111 p.insert("BYTES_PER_ELEMENT".into(), bpe);
9112 }
9113 self.hide_prop(&proto, "BYTES_PER_ELEMENT");
9114 }
9115 for m in methods {
9116 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9117 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9118 p.insert((*m).to_string(), thunk);
9119 }
9120 self.hide_prop(&proto, m);
9121 }
9122 self.native_protos.insert(ctor.to_string(), proto.clone());
9123 prev = Some(proto);
9124 }
9125 }
9126
9127 /// `String.prototype`, `Number.prototype` and `Boolean.prototype` as REAL
9128 /// objects.
9129 ///
9130 /// A wrapper built by `new String("a")` needs a genuine `[[Prototype]]`
9131 /// link: `Builtin("String.prototype")` is a thunk namespace that cannot
9132 /// appear on a prototype chain, so `Object.getPrototypeOf(w) ===
9133 /// String.prototype` and `w instanceof String` both read false while the
9134 /// wrapper's methods still resolved through the string funnel. Registering
9135 /// them here puts them on the same footing as `Buffer.prototype`.
9136 /// `GeneratorFunction.prototype`, `AsyncFunction.prototype` and
9137 /// `AsyncGeneratorFunction.prototype` — the intrinsics a generator or async
9138 /// function's `[[Prototype]]` really points at.
9139 ///
9140 /// None are globals (node exposes them only through
9141 /// `Object.getPrototypeOf(function*(){}).constructor`), so they live here
9142 /// rather than among the wrapper constructors. Each hangs off
9143 /// `Function.prototype` and carries the `Symbol.toStringTag` that names it.
9144 pub fn ensure_function_kind_protos(&mut self) {
9145 if self.native_protos.contains_key("GeneratorFunction") {
9146 return;
9147 }
9148 let base = self
9149 .native_protos
9150 .get("Function")
9151 .cloned()
9152 .unwrap_or_else(|| self.object_proto());
9153 for ctor in [
9154 "GeneratorFunction",
9155 "AsyncFunction",
9156 "AsyncGeneratorFunction",
9157 ] {
9158 let proto = self.new_object(IndexMap::new());
9159 self.set_proto(&proto, base.clone());
9160 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9161 let tag = self.new_str(ctor);
9162 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9163 p.insert("constructor".into(), ctor_val);
9164 p.insert("@@toStringTag".into(), tag);
9165 }
9166 self.hide_prop(&proto, "constructor");
9167 self.hide_prop(&proto, "@@toStringTag");
9168 self.native_protos.insert(ctor.to_string(), proto);
9169 }
9170 }
9171
9172 pub fn ensure_wrapper_protos(&mut self) {
9173 if self.native_protos.contains_key("String") {
9174 return;
9175 }
9176 let obj_proto = self.object_proto();
9177 for ctor in ["String", "Number", "Boolean", "Symbol", "BigInt"] {
9178 let proto = self.new_object(IndexMap::new());
9179 self.set_proto(&proto, obj_proto.clone());
9180 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9181 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9182 p.insert("constructor".into(), ctor_val);
9183 }
9184 self.hide_prop(&proto, "constructor");
9185 // The three conversions must be here because `Object.prototype`
9186 // also defines them: without a shadowing entry a wrapper would
9187 // inherit the object forms and `String(new String("a"))` would
9188 // report `[object Object]`.
9189 //
9190 // The REST are here because the prototype is a real object a script
9191 // can read a method OFF of. Only the three were installed, on the
9192 // reasoning that `charAt`/`toFixed`/… reach the primitive through
9193 // `call_method` anyway — true for `s.charAt(0)` and false for the
9194 // generic-borrowing form: `String.prototype.trim` read `undefined`,
9195 // so `String.prototype.trim.call(s)` — and `Number.prototype
9196 // .toFixed.call(n)`, and every `Array.prototype`-style borrow of a
9197 // wrapper method — threw. `Array.prototype`/`Object.prototype`
9198 // already carried their whole method set; these three did not.
9199 let methods: Vec<&str> = ["toString", "valueOf", "toLocaleString"]
9200 .into_iter()
9201 .chain(match ctor {
9202 "String" => crate::builtins::STRING_PROTO_METHODS.iter().copied(),
9203 "Number" => crate::builtins::NUMBER_PROTO_METHODS.iter().copied(),
9204 _ => [].iter().copied(),
9205 })
9206 // The symbol-keyed methods come from the generated intrinsic
9207 // table, so this object advertises exactly the symbol methods
9208 // node defines on it — `String.prototype[Symbol.iterator]` was
9209 // `undefined` because only the string-keyed lists were walked.
9210 .chain(crate::builtins::proto_symbol_methods(ctor))
9211 .collect();
9212 let mut seen: Vec<&str> = Vec::new();
9213 for m in methods {
9214 if seen.contains(&m) {
9215 continue;
9216 }
9217 seen.push(m);
9218 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9219 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9220 p.insert(m.to_string(), thunk);
9221 }
9222 self.hide_prop(&proto, m);
9223 }
9224 // `Symbol.prototype` and `BigInt.prototype` are the two wrapper
9225 // prototypes that carry a `@@toStringTag`; the other three are
9226 // branded by their internal slot instead, and node reports
9227 // `undefined` for their tag. Without it
9228 // `Object.prototype.toString.call(Symbol.prototype)` read
9229 // `[object Object]` where node says `[object Symbol]`.
9230 if matches!(ctor, "Symbol" | "BigInt") {
9231 let tag = self.new_str(ctor.to_string());
9232 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9233 p.insert("@@toStringTag".into(), tag);
9234 }
9235 // Not `hide_prop`: a well-known `@@toStringTag` is read-only as
9236 // well as non-enumerable (20.4.3.6), and `hide_prop` leaves it
9237 // writable.
9238 self.set_prop_attrs(
9239 &proto,
9240 "@@toStringTag",
9241 PropAttrs {
9242 writable: false,
9243 enumerable: false,
9244 configurable: true,
9245 },
9246 );
9247 }
9248 // `Symbol.prototype[@@toPrimitive]` (20.4.3.5) is what a string or
9249 // numeric conversion of a symbol reaches FIRST. Its absence was
9250 // observable in the failure wording: `String(Symbol.prototype)`
9251 // throws in node because `@@toPrimitive` rejects a non-Symbol
9252 // `this`, and here the conversion fell through to `toString` and
9253 // named that method in the message instead.
9254 if ctor == "Symbol" {
9255 let thunk = self.alloc(JsObj::Builtin("@proto:Symbol:@@toPrimitive".to_string()));
9256 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9257 p.insert("@@toPrimitive".into(), thunk);
9258 }
9259 self.hide_prop(&proto, "@@toPrimitive");
9260 }
9261 self.native_protos.insert(ctor.to_string(), proto);
9262 }
9263 }
9264
9265 /// The cached template object for one tagged-template site, if it has been
9266 /// evaluated before.
9267 pub fn template_object(&self, key: (u64, u64)) -> Option<Value> {
9268 self.template_objects.get(&key).cloned()
9269 }
9270 /// Record the template object for one tagged-template site.
9271 pub fn set_template_object(&mut self, key: (u64, u64), v: Value) {
9272 self.template_objects.insert(key, v);
9273 }
9274
9275 /// The real prototype object for a builtin exotic, if it has one.
9276 pub fn native_proto(&self, ctor: &str) -> Option<Value> {
9277 self.native_protos.get(ctor).cloned()
9278 }
9279
9280 /// The constructor name whose `.prototype` object IS `v`, for a prototype
9281 /// this host built as a real object (`String.prototype`, `TypeError
9282 /// .prototype`, `Buffer.prototype`) rather than as a `Builtin` namespace.
9283 ///
9284 /// A prototype is an ORDINARY object: it carries no instance's internal
9285 /// slot, so `Object.prototype.toString.call(TypeError.prototype)` is
9286 /// `[object Object]` and not `[object Error]`. Nothing distinguished the two
9287 /// before, so the brand fell through to the "does it look like an Error"
9288 /// test and answered for the prototype as if it were an instance.
9289 pub fn intrinsic_proto_ctor(&self, v: &Value) -> Option<&str> {
9290 if !matches!(v, Value::Obj(_)) {
9291 return None;
9292 }
9293 self.native_protos
9294 .iter()
9295 .chain(self.error_protos.iter())
9296 .find(|(_, p)| *p == v)
9297 .map(|(name, _)| name.as_str())
9298 }
9299
9300 /// The real `.prototype` object for a native stdlib constructor (`StringDecoder`,
9301 /// `Hash`, `URLSearchParams`, …), built on first read and cached.
9302 ///
9303 /// `Ctor.prototype` used to read `undefined` for every native class outside the
9304 /// hand-written `is_builtin_ctor` list, which broke the ES5 subclassing pattern
9305 /// that libraries still use. `iconv-lite`'s internal codec — reached from
9306 /// `raw-body` on every `express.json()` request — does exactly this:
9307 ///
9308 /// ```text
9309 /// var StringDecoder = require('string_decoder').StringDecoder;
9310 /// if (!StringDecoder.prototype.end) StringDecoder.prototype.end = function () {};
9311 /// function InternalDecoder(options, codec) { StringDecoder.call(this, codec.enc); }
9312 /// InternalDecoder.prototype = StringDecoder.prototype;
9313 /// ```
9314 ///
9315 /// The first line threw `Cannot read properties of undefined (reading 'end')`.
9316 ///
9317 /// Methods come from `stdlib::instance_method_lists`, the same table a method
9318 /// READ consults, so the prototype can never advertise a name the dispatcher
9319 /// does not implement. Each is the `@proto:<Ctor>:<method>` thunk that
9320 /// dispatches against its invoke-time `this`, so a subclass instance whose
9321 /// prototype IS this object gets the native implementation. Returns `None` for
9322 /// a tag with no instance methods, leaving those constructors as they were.
9323 pub fn ensure_ctor_proto(&mut self, ctor: &str) -> Option<Value> {
9324 if let Some(p) = self.native_protos.get(ctor) {
9325 return Some(p.clone());
9326 }
9327 // `Buffer` and the typed-array kinds belong to the chain
9328 // `ensure_native_protos` builds. Building one of them here first hung it
9329 // straight off `Object.prototype` AND registered it, which made that
9330 // chain's own `contains_key("Buffer")` guard skip the build for the rest
9331 // of the process: after `Buffer.from([1])`, `Buffer.prototype instanceof
9332 // Uint8Array` read false.
9333 if ctor == "Buffer"
9334 || ctor == "TypedArray"
9335 || crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor)
9336 {
9337 self.ensure_native_protos();
9338 return self.native_protos.get(ctor).cloned();
9339 }
9340 let (own, emitter) = crate::stdlib::instance_method_lists(ctor);
9341 // A class can carry accessors and no methods at all
9342 // (`AsymmetricKeyObject` is only `asymmetricKeyType` and
9343 // `asymmetricKeyDetails`), so an empty method list does not mean there
9344 // is no prototype to build.
9345 let (accessor_list, _) = crate::stdlib::instance_accessors(ctor);
9346 if own.is_empty() && emitter.is_empty() && accessor_list.is_empty() {
9347 return None;
9348 }
9349 // A native class with a real PARENT hangs off that parent's prototype
9350 // rather than straight off `Object.prototype`. The stream hierarchy is
9351 // `Readable → Stream → EventEmitter`, which is what makes
9352 // `new Readable() instanceof Stream` hold and what an ES5 subclass
9353 // doing `Object.create(Stream.prototype)` inherits from.
9354 let parent_proto = match crate::stdlib::native_parent(ctor) {
9355 Some(p) => self
9356 .ensure_ctor_proto(p)
9357 .unwrap_or_else(|| self.object_proto()),
9358 None => self.object_proto(),
9359 };
9360 let proto = self.new_object(IndexMap::new());
9361 self.set_proto(&proto, parent_proto);
9362 let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
9363 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9364 p.insert("constructor".into(), ctor_val);
9365 }
9366 self.hide_prop(&proto, "constructor");
9367 // A prototype member is ENUMERABLE in node for every class but the few
9368 // written as ES classes, so `for (const k in url)` walks `href` and the
9369 // rest. Hiding all of them made that loop find nothing.
9370 let visible = crate::stdlib::instance_members_enumerable(ctor);
9371 let symbols = crate::builtins::proto_symbol_methods(ctor);
9372 for m in own.iter().chain(emitter.iter()).chain(symbols.iter()) {
9373 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9374 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9375 p.insert((*m).to_string(), thunk);
9376 }
9377 // A symbol-keyed member never enumerates.
9378 if !visible || m.starts_with("@@") {
9379 self.hide_prop(&proto, m);
9380 }
9381 }
9382 // Accessors and the class's `Symbol.toStringTag`, both of which live on
9383 // the PROTOTYPE in node — an instance owns neither.
9384 let (accessors, tag) = crate::stdlib::instance_accessors(ctor);
9385 for (key, settable) in accessors {
9386 let get = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@get@{key}")));
9387 let set =
9388 settable.then(|| self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@set@{key}"))));
9389 self.set_accessor(&proto, key, Some(get), set);
9390 if !visible {
9391 self.hide_prop(&proto, key);
9392 }
9393 }
9394 for m in crate::stdlib::instance_late_methods(ctor) {
9395 let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
9396 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9397 p.insert((*m).to_string(), thunk);
9398 }
9399 if !visible {
9400 self.hide_prop(&proto, m);
9401 }
9402 }
9403 if !tag.is_empty() {
9404 let tag = self.new_str(tag.to_string());
9405 if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
9406 p.insert("@@toStringTag".into(), tag);
9407 }
9408 self.hide_prop(&proto, "@@toStringTag");
9409 }
9410 self.native_protos.insert(ctor.to_string(), proto.clone());
9411 Some(proto)
9412 }
9413
9414 /// `<ErrorClass>.prototype`, once [`JsHost::ensure_error_protos`] has run.
9415 /// The error prototypes live in their own table, so `ensure_ctor_proto` —
9416 /// which answers from `native_protos` — does not find them.
9417 pub fn error_proto(&self, name: &str) -> Option<Value> {
9418 self.error_protos.get(name).cloned()
9419 }
9420
9421 pub fn ensure_error_protos(&mut self) {
9422 if !self.error_protos.is_empty() {
9423 return;
9424 }
9425 let obj_proto = self.object_proto();
9426 // Error.prototype first (the shared base).
9427 let err_proto = self.new_object(IndexMap::new());
9428 self.set_proto(&err_proto, obj_proto);
9429 let nm = self.new_str("Error");
9430 let empty = self.new_str("");
9431 let ctor = self.alloc(JsObj::Builtin("Error".into()));
9432 // `Error.prototype.toString` (20.5.3.4) has to be an OWN property here,
9433 // not a fallback the stringifier applies when nothing else matches: it
9434 // exists precisely to shadow `Object.prototype.toString`. Without it,
9435 // the first read of `Error.prototype` or `Object.prototype` — which
9436 // `x instanceof Error` performs, so ordinary code triggers it —
9437 // materialised `Object.prototype.toString`, the chain lookup started
9438 // finding it, and `String(err)` flipped from `Error: m` to
9439 // `[object Error]` for the REST OF THE PROCESS, including errors
9440 // created before the read.
9441 let to_string = self.alloc(JsObj::Builtin("@proto:Error:toString".into()));
9442 if let Some(JsObj::Object(p)) = self.get_mut(&err_proto) {
9443 p.insert("name".into(), nm);
9444 p.insert("message".into(), empty);
9445 p.insert("constructor".into(), ctor);
9446 p.insert("toString".into(), to_string);
9447 }
9448 // Everything on `Error.prototype` is non-enumerable in V8.
9449 for k in ["name", "message", "constructor", "toString"] {
9450 self.hide_prop(&err_proto, k);
9451 }
9452 self.error_protos.insert("Error".into(), err_proto.clone());
9453 for name in &ERROR_NAMES[1..] {
9454 let p = self.new_object(IndexMap::new());
9455 self.set_proto(&p, err_proto.clone());
9456 let nm = self.new_str(*name);
9457 let ctor = self.alloc(JsObj::Builtin((*name).to_string()));
9458 if let Some(JsObj::Object(o)) = self.get_mut(&p) {
9459 o.insert("name".into(), nm);
9460 o.insert("constructor".into(), ctor);
9461 }
9462 self.hide_prop(&p, "name");
9463 self.hide_prop(&p, "constructor");
9464 self.error_protos.insert((*name).to_string(), p);
9465 }
9466 }
9467}
9468
9469// ── Map/Set element access (used by builtins) ────────────────────────────────
9470
9471impl JsHost {
9472 /// A function's `.length`: the count of leading params before the first one
9473 /// with a default or the rest element.
9474 pub fn func_arity(&self, v: &Value) -> usize {
9475 // 20.2.3.2: a bound function's `length` is the target's, less the
9476 // arguments already bound, floored at 0. Reporting 0 for every bound
9477 // function breaks arity dispatch — express picks error-handling
9478 // middleware with `fn.length === 4`, so a bound handler was never
9479 // recognised as one.
9480 if let Some(JsObj::BoundFunc { target, args, .. }) = self.get(v) {
9481 return self.func_arity(&target.clone()).saturating_sub(args.len());
9482 }
9483 // A builtin's arity is the specified one, so `Math.max.bind(null,1)`
9484 // reports 1 rather than the 0 a target of unknown arity would give.
9485 if let Some(JsObj::Builtin(n)) = self.get(v) {
9486 return crate::builtins::builtin_meta(n)
9487 .map(|(_, len)| len as usize)
9488 .unwrap_or(0);
9489 }
9490 let def_id = match self.get(v) {
9491 Some(JsObj::Func(f)) => Some(f.def_id),
9492 Some(JsObj::Class(c)) => match c.ctor.as_ref().and_then(|cf| self.get(cf)) {
9493 Some(JsObj::Func(f)) => Some(f.def_id),
9494 _ => None,
9495 },
9496 _ => None,
9497 };
9498 match def_id.and_then(|id| self.funcs.get(id)) {
9499 Some(def) => def
9500 .params
9501 .iter()
9502 .take_while(|p| !p.rest && !p.has_default)
9503 .count(),
9504 None => 0,
9505 }
9506 }
9507
9508 pub fn is_map(&self, v: &Value) -> bool {
9509 matches!(self.get(v), Some(JsObj::Map { .. }))
9510 }
9511 pub fn is_set(&self, v: &Value) -> bool {
9512 matches!(self.get(v), Some(JsObj::Set { .. }))
9513 }
9514}
9515
9516// ── promises & the event loop ────────────────────────────────────────────────
9517
9518impl JsHost {
9519 /// Allocate a fresh pending promise, returning its heap value.
9520 pub fn new_promise(&mut self) -> Value {
9521 let id = self.promises.len() as u32;
9522 self.promises.push(PromiseCell {
9523 state: PromiseState::Pending,
9524 value: Value::Undef,
9525 reactions: Vec::new(),
9526 handled: false,
9527 });
9528 self.alloc(JsObj::Promise { id })
9529 }
9530 pub fn promise_id(&self, v: &Value) -> Option<u32> {
9531 match self.get(v) {
9532 Some(JsObj::Promise { id }) => Some(*id),
9533 _ => None,
9534 }
9535 }
9536 pub fn promise_state(&self, id: u32) -> PromiseState {
9537 self.promises[id as usize].state
9538 }
9539 pub fn promise_value(&self, id: u32) -> Value {
9540 self.promises[id as usize].value.clone()
9541 }
9542 pub fn promise_mark_handled(&mut self, id: u32) {
9543 self.promises[id as usize].handled = true;
9544 }
9545 /// Take the pending reactions of a promise (called on settle).
9546 pub fn take_reactions(&mut self, id: u32) -> Vec<PromiseReaction> {
9547 std::mem::take(&mut self.promises[id as usize].reactions)
9548 }
9549 pub fn add_reaction(&mut self, id: u32, r: PromiseReaction) {
9550 self.promises[id as usize].reactions.push(r);
9551 }
9552 pub fn settle_promise(&mut self, id: u32, state: PromiseState, value: Value) {
9553 let c = &mut self.promises[id as usize];
9554 if c.state != PromiseState::Pending {
9555 return; // already settled — resolve/reject are one-shot
9556 }
9557 c.state = state;
9558 c.value = value;
9559 }
9560 pub fn queue_micro(&mut self, cb: Value, args: Vec<Value>) {
9561 self.microtasks.push_back(Task::Js { cb, args });
9562 }
9563 pub fn queue_nexttick(&mut self, cb: Value, args: Vec<Value>) {
9564 self.nextticks.push_back(Task::Js { cb, args });
9565 }
9566 /// Schedule a native (Rust) microtask — used by Promise reactions and async
9567 /// resumption.
9568 pub fn queue_micro_native(&mut self, f: Box<dyn FnOnce() -> Result<(), String>>) {
9569 self.microtasks.push_back(Task::Native(f));
9570 }
9571 /// Schedule a macrotask. `interval` is the repeat period for `setInterval`
9572 /// (`None` for the one-shot `setTimeout`/`setImmediate`). Returns the timer
9573 /// id, which the `Timeout`/`Immediate` handle object carries so `clear*`,
9574 /// `ref`/`unref` and `refresh` can find this entry again.
9575 pub fn add_timer(
9576 &mut self,
9577 delay: f64,
9578 callback: Value,
9579 args: Vec<Value>,
9580 interval: Option<f64>,
9581 ) -> u64 {
9582 let id = self.next_timer;
9583 self.next_timer += 1;
9584 // Real deadline for the real-clock path; `setImmediate` (delay < 0) is
9585 // clamped to "now". Virtual-clock ordering still uses `delay`/`seq`.
9586 let deadline = Instant::now() + Duration::from_millis(delay.max(0.0) as u64);
9587 self.macrotasks.push(Timer {
9588 id,
9589 delay,
9590 seq: id,
9591 callback,
9592 args,
9593 cancelled: false,
9594 interval,
9595 refed: true,
9596 deadline,
9597 });
9598 id
9599 }
9600 /// Re-arm a repeating timer that is about to fire, keeping its id (so a
9601 /// `clearInterval` from *inside* the callback cancels this very entry) and
9602 /// taking a fresh `seq` so same-delay peers still round-robin.
9603 ///
9604 /// Called BEFORE the callback runs: if it were called after, the entry would
9605 /// be absent while the callback executed and a `clearInterval(t)` there would
9606 /// cancel nothing, resurrecting an interval the program had stopped.
9607 fn rearm_timer(&mut self, t: &Timer, period: f64) {
9608 let seq = self.next_timer;
9609 self.next_timer += 1;
9610 let deadline = Instant::now() + Duration::from_millis(period.max(0.0) as u64);
9611 self.macrotasks.push(Timer {
9612 id: t.id,
9613 delay: t.delay,
9614 seq,
9615 callback: t.callback.clone(),
9616 args: t.args.clone(),
9617 cancelled: false,
9618 interval: Some(period),
9619 refed: t.refed,
9620 deadline,
9621 });
9622 }
9623 /// `timeout.ref()` / `timeout.unref()` — set the handle bit on a pending
9624 /// timer. A no-op once the timer has fired or been cleared (Node likewise
9625 /// treats `ref`/`unref` on a dead timer as inert).
9626 pub fn set_timer_refed(&mut self, id: u64, refed: bool) {
9627 for t in &mut self.macrotasks {
9628 if t.id == id && !t.cancelled {
9629 t.refed = refed;
9630 }
9631 }
9632 }
9633 /// `timeout.hasRef()` — whether a still-pending timer holds the loop open.
9634 /// A fired or cleared timer reports `false`, matching Node.
9635 pub fn timer_has_ref(&self, id: u64) -> bool {
9636 self.macrotasks
9637 .iter()
9638 .any(|t| t.id == id && !t.cancelled && t.refed)
9639 }
9640 /// `timeout.refresh()` — restart the countdown from now, as if the timer had
9641 /// just been scheduled.
9642 pub fn refresh_timer(&mut self, id: u64) {
9643 let now = Instant::now();
9644 for t in &mut self.macrotasks {
9645 if t.id == id && !t.cancelled {
9646 t.deadline = now + Duration::from_millis(t.delay.max(0.0) as u64);
9647 }
9648 }
9649 }
9650 /// Clone the I/O sender for a background I/O thread.
9651 pub fn io_sender(&self) -> Sender<IoTask> {
9652 self.io_tx.clone()
9653 }
9654 /// Register a live handle (listener/socket/ref'd resource) keeping the loop
9655 /// alive.
9656 pub fn incr_handle(&mut self) {
9657 self.open_handles += 1;
9658 }
9659 /// Release a handle; the loop exits once this reaches `0` with empty queues.
9660 pub fn decr_handle(&mut self) {
9661 self.open_handles = self.open_handles.saturating_sub(1);
9662 }
9663 pub fn open_handles(&self) -> usize {
9664 self.open_handles
9665 }
9666 /// Pop the earliest timer whose real deadline is at or before `now` (I/O
9667 /// path). Ties break by `seq`.
9668 fn pop_due_timer(&mut self, now: Instant) -> Option<Timer> {
9669 let idx = self
9670 .macrotasks
9671 .iter()
9672 .enumerate()
9673 .filter(|(_, t)| !t.cancelled && t.deadline <= now)
9674 .min_by(|(_, a), (_, b)| a.deadline.cmp(&b.deadline).then(a.seq.cmp(&b.seq)))
9675 .map(|(i, _)| i);
9676 idx.map(|i| self.macrotasks.remove(i))
9677 }
9678 /// Time until the earliest pending timer's deadline (I/O path blocking bound),
9679 /// or `None` if no timers are pending. Clamped to `0` for already-due timers.
9680 fn next_timer_timeout(&self, now: Instant) -> Option<Duration> {
9681 self.macrotasks
9682 .iter()
9683 .filter(|t| !t.cancelled)
9684 .map(|t| t.deadline)
9685 .min()
9686 .map(|d| d.saturating_duration_since(now))
9687 }
9688 pub fn cancel_timer(&mut self, id: u64) {
9689 for t in &mut self.macrotasks {
9690 if t.id == id {
9691 t.cancelled = true;
9692 }
9693 }
9694 }
9695 fn pop_next_timer(&mut self) -> Option<Timer> {
9696 // Earliest (delay, seq) fires first — a deterministic virtual clock.
9697 let idx = self
9698 .macrotasks
9699 .iter()
9700 .enumerate()
9701 .filter(|(_, t)| !t.cancelled)
9702 .min_by(|(_, a), (_, b)| {
9703 a.delay
9704 .partial_cmp(&b.delay)
9705 .unwrap_or(std::cmp::Ordering::Equal)
9706 .then(a.seq.cmp(&b.seq))
9707 })
9708 .map(|(i, _)| i);
9709 idx.map(|i| self.macrotasks.remove(i))
9710 }
9711 fn next_microtask(&mut self) -> Option<Task> {
9712 // Node's `processTicksAndRejections` runs in ROUNDS: drain the nextTick
9713 // queue, then drain the microtask queue in full, then repeat if the
9714 // microtasks queued more ticks. A tick queued from INSIDE a microtask
9715 // therefore waits for the rest of that microtask queue.
9716 //
9717 // Preferring ticks on every step interleaved the two, so
9718 // `Promise.resolve().then(() => process.nextTick(f))` ran `f` before the
9719 // promise callbacks queued behind it — the one ordering difference a
9720 // library scheduling work from a `.then` can actually observe.
9721 if !self.draining_micro {
9722 if let Some(t) = self.nextticks.pop_front() {
9723 return Some(t);
9724 }
9725 }
9726 if let Some(t) = self.microtasks.pop_front() {
9727 // Stay in the microtask phase until this queue is exhausted.
9728 self.draining_micro = !self.microtasks.is_empty();
9729 return Some(t);
9730 }
9731 self.draining_micro = false;
9732 self.nextticks.pop_front()
9733 }
9734 fn has_microtasks(&self) -> bool {
9735 !self.nextticks.is_empty() || !self.microtasks.is_empty()
9736 }
9737 /// Whether any pending timer is *referenced* — the timer half of Node's
9738 /// handle count. Only these keep the loop alive; unref'd timers still fire
9739 /// while something else holds the loop open, but never hold it themselves.
9740 fn has_refed_macrotasks(&self) -> bool {
9741 self.macrotasks.iter().any(|t| !t.cancelled && t.refed)
9742 }
9743 /// Whether any pending timer repeats. A repeating timer cannot run on the
9744 /// virtual clock: virtual time never advances, so the interval would re-arm
9745 /// at the same instant forever, spinning a core and starving every
9746 /// longer-delay timer behind it. Its presence forces the real clock.
9747 fn has_pending_interval(&self) -> bool {
9748 self.macrotasks
9749 .iter()
9750 .any(|t| !t.cancelled && t.interval.is_some())
9751 }
9752}
9753
9754/// Drive the event loop to quiescence.
9755///
9756/// **Liveness** is Node's handle count: the loop runs while a microtask is
9757/// pending, an open handle is registered (a listening server, a live socket, an
9758/// in-flight async op), or a *referenced* timer is still pending. That last term
9759/// is what makes `setInterval(fn, 1000)` hold the process open forever, as it
9760/// does in Node — the interval re-arms itself, so a ref'd timer is always
9761/// pending and the loop never reaches its exit condition.
9762///
9763/// Two **clock regimes**, selected per iteration:
9764///
9765/// - **Virtual clock** (no open handles and no repeating timer): the original
9766/// deterministic path — fire the earliest `(delay, seq)` timer immediately, no
9767/// real waiting. Parity output and test speed for ordinary `setTimeout`
9768/// scripts are unchanged.
9769/// - **Real clock** (an open handle, or any pending interval): fire every timer
9770/// whose wall-clock deadline has passed, then BLOCK on the I/O channel
9771/// (`recv_timeout` bounded by the next deadline, or unbounded `recv` if no
9772/// timers) and run the received `IoTask` on the main thread. The host keeps
9773/// its own `Sender`, so `recv` never disconnects while the process should stay
9774/// alive.
9775///
9776/// A repeating timer *must* take this path: virtual time never advances, so an
9777/// interval on the virtual clock would re-fire at the same instant forever,
9778/// spinning a core and starving every longer-delay timer behind it.
9779///
9780/// Errors thrown by a task/timer/I/O dispatch abort the loop (uncaught → surfaced).
9781pub fn run_event_loop() -> Result<(), String> {
9782 // Own the receiver for the loop's duration (blocking `recv` cannot hold a
9783 // host borrow); restore it afterward so a re-entrant run reuses the channel.
9784 let rx = with_host(|h| h.io_rx.take());
9785 let result = drive_event_loop(rx.as_ref());
9786 with_host(|h| h.io_rx = rx);
9787 result
9788}
9789
9790fn drive_event_loop(rx: Option<&Receiver<IoTask>>) -> Result<(), String> {
9791 loop {
9792 // 1) Exhaust the microtask queue (nextTick before promise reactions),
9793 // then report anything that rejected with nobody watching.
9794 while let Some(task) = with_host(|h| h.next_microtask()) {
9795 task.run()?;
9796 }
9797 check_unhandled_rejections()?;
9798
9799 // 2) Liveness (Node's handle count). Nothing referenced left to do ⇒ the
9800 // process exits, dropping any unref'd timers still pending — which is
9801 // why `setTimeout(fn, 1000).unref()` never fires, while an unref'd
9802 // timer behind a ref'd one does.
9803 let alive =
9804 with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
9805 if !alive {
9806 break;
9807 }
9808
9809 // 3) Pick the clock regime for this turn.
9810 let virtual_clock = with_host(|h| h.open_handles() == 0 && !h.has_pending_interval());
9811 if virtual_clock {
9812 // ── virtual-clock regime (unchanged for one-shot timers) ─────────
9813 match with_host(|h| h.pop_next_timer()) {
9814 Some(t) => fire_timer(t)?,
9815 // Unreachable while `alive` holds (a ref'd timer must exist),
9816 // but exiting is the safe reading of "nothing left to run".
9817 None => break,
9818 }
9819 continue;
9820 }
9821
9822 // ── real-clock / blocking-I/O regime ─────────────────────────────────
9823 let now = Instant::now();
9824 if let Some(t) = with_host(|h| h.pop_due_timer(now)) {
9825 fire_timer(t)?;
9826 continue; // re-drain microtasks, re-check deadlines
9827 }
9828 // Nothing due and no pending microtasks: block for the next I/O event,
9829 // bounded by the soonest timer deadline so due timers still fire on time.
9830 let rx = rx.expect("blocking-I/O regime requires the I/O receiver");
9831 let timeout = with_host(|h| h.next_timer_timeout(now));
9832 let recv = match timeout {
9833 Some(d) => rx.recv_timeout(d),
9834 None => rx
9835 .recv()
9836 .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected),
9837 };
9838 match recv {
9839 Ok(task) => task()?,
9840 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} // a timer is now due
9841 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, // no senders left
9842 }
9843 }
9844 Ok(())
9845}
9846
9847/// Run one due timer's callback, first re-arming it if it repeats.
9848///
9849/// The re-arm happens BEFORE the callback runs so that a `clearInterval(t)`
9850/// issued from inside that callback cancels the next occurrence. Re-arming
9851/// afterwards would leave the interval absent from the queue for the duration of
9852/// its own callback, so the `clear` would match nothing and the freshly pushed
9853/// entry would resurrect an interval the program had just stopped.
9854fn fire_timer(t: Timer) -> Result<(), String> {
9855 if let Some(period) = t.interval {
9856 with_host(|h| h.rearm_timer(&t, period));
9857 }
9858 invoke(&t.callback, t.args, None)?;
9859 Ok(())
9860}
9861
9862// ── async functions & promise resolution (native) ────────────────────────────
9863
9864/// Drive a freshly-built async coroutine and return its result promise.
9865fn run_async(gen: Value) -> Value {
9866 let result = with_host(|h| h.new_promise());
9867 let rid = with_host(|h| h.promise_id(&result).unwrap());
9868 drive_async(gen, rid, Value::Undef);
9869 result
9870}
9871
9872/// Resume an async coroutine one step, wiring `await` continuations to promise
9873/// settlement.
9874fn drive_async(gen: Value, rid: u32, send: Value) {
9875 match gen_resume(&gen, send) {
9876 Ok(GenStep::Yield(awaited)) => {
9877 let ap = promise_of(&awaited);
9878 let aid = with_host(|h| h.promise_id(&ap).unwrap());
9879 let gen2 = gen.clone();
9880 subscribe_native(
9881 aid,
9882 Box::new(move |state, val| {
9883 // Resume the coroutine with a `[tag, value]` packet the AWAIT
9884 // op unwraps (tag 1 ⇒ the awaited promise rejected → throw).
9885 let tag = if state == PromiseState::Rejected {
9886 1.0
9887 } else {
9888 0.0
9889 };
9890 let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
9891 drive_async(gen2, rid, packet);
9892 Ok(())
9893 }),
9894 );
9895 }
9896 Ok(GenStep::Done(v)) => resolve_promise_val(rid, v),
9897 Err(e) => {
9898 let ev = take_exc_or_error(&e);
9899 reject_promise_val(rid, ev);
9900 }
9901 }
9902}
9903
9904/// The AWAIT op body (runs inside the async coroutine): suspend, yielding the
9905/// awaited value; on resume, unwrap the settlement packet (throwing on reject).
9906pub fn await_value(awaited: Value) -> Result<Value, String> {
9907 // Inside an `async function*`, `await` and `yield` share one coroutine
9908 // yielder, so an awaited value has to be tagged or the driver would hand it
9909 // to the consumer as if the body had yielded it.
9910 let awaited = match CUR_GEN.with(|c| c.get()) {
9911 Some(id) if with_host(|h| h.generators[id as usize].async_gen) => with_host(|h| {
9912 let mut m = IndexMap::new();
9913 m.insert(AWAIT_MARKER.to_string(), awaited);
9914 h.new_object(m)
9915 }),
9916 _ => awaited,
9917 };
9918 let packet = gen_yield(awaited)?;
9919 let items = with_host(|h| h.iter_vec(&packet)).unwrap_or_default();
9920 let tag = items
9921 .first()
9922 .map(|v| with_host(|h| h.to_number(v)))
9923 .unwrap_or(0.0);
9924 let val = items.get(1).cloned().unwrap_or(Value::Undef);
9925 if tag == 1.0 {
9926 with_host(|h| h.exc = Some(val.clone()));
9927 Err(with_host(|h| crate::builtins::error_string(h, &val)))
9928 } else {
9929 Ok(val)
9930 }
9931}
9932
9933/// Hidden key marking an `await` suspension inside an async generator.
9934const AWAIT_MARKER: &str = "@@await";
9935
9936/// The operand of an `await` suspension, or `None` for a real `yield`.
9937fn await_marker(v: &Value) -> Option<Value> {
9938 with_host(|h| match h.get(v) {
9939 Some(JsObj::Object(props)) if props.len() == 1 => props.get(AWAIT_MARKER).cloned(),
9940 _ => None,
9941 })
9942}
9943
9944/// `AsyncGeneratorEnqueue` — queue one request against an `async function*` and
9945/// hand back the promise its `{value, done}` record (or rejection) will settle.
9946///
9947/// All three of `.next`, `.return` and `.throw` come through here, so a request
9948/// never resumes the body while an earlier one is still suspended on an
9949/// internal `await`.
9950pub fn async_gen_enqueue(gen: &Value, req: GenReq) -> Value {
9951 let step = with_host(|h| h.new_promise());
9952 let sid = with_host(|h| h.promise_id(&step).unwrap());
9953 let id = match with_host(|h| match h.get(gen) {
9954 Some(JsObj::Generator { id }) => Some(*id),
9955 _ => None,
9956 }) {
9957 Some(id) => id,
9958 None => return step,
9959 };
9960 with_host(|h| h.generators[id as usize].queue.push_back((req, sid)));
9961 pump_async_gen(gen.clone(), id);
9962 step
9963}
9964
9965/// One `.next(v)` of an `async function*`.
9966pub fn async_gen_step(gen: &Value, send: Value) -> Value {
9967 async_gen_enqueue(gen, GenReq::Next(send))
9968}
9969
9970/// `AsyncGeneratorResumeNext`: start the oldest queued request, unless one is
9971/// already in flight (the body may only be resumed by one request at a time).
9972fn pump_async_gen(gen: Value, id: u32) {
9973 if with_host(|h| h.generators[id as usize].running) {
9974 return;
9975 }
9976 let Some((req, sid)) = with_host(|h| h.generators[id as usize].queue.pop_front()) else {
9977 return;
9978 };
9979 with_host(|h| h.generators[id as usize].running = true);
9980 start_async_gen_req(gen, sid, req);
9981}
9982
9983/// Begin one queued request: resume the body with the completion it carries,
9984/// then hand the outcome to the shared continuation.
9985///
9986/// A RETURN completion always Awaits its value before the body sees it — via
9987/// `AsyncGeneratorUnwrapYieldResumption` (ECMA-262 27.6.3.7) when the generator
9988/// is suspended at a `yield`, and via `AsyncGeneratorAwaitReturn` (27.6.3.9)
9989/// when it is not yet started or already completed. So a `.return()` settles one
9990/// microtask after a `.next()` or `.throw()` issued in its place would, and the
9991/// `finally` it unwinds through runs a tick later too. Skipping that tick lets a
9992/// `.return()` overtake the reactions of the `.next()` it followed.
9993fn start_async_gen_req(gen: Value, sid: u32, req: GenReq) {
9994 if matches!(req, GenReq::Return(_)) {
9995 with_host(|h| {
9996 h.queue_micro_native(Box::new(move || {
9997 resume_async_gen_req(gen, sid, req);
9998 Ok(())
9999 }))
10000 });
10001 return;
10002 }
10003 resume_async_gen_req(gen, sid, req);
10004}
10005
10006/// Deliver a queued completion to the body and settle its step promise.
10007fn resume_async_gen_req(gen: Value, sid: u32, req: GenReq) {
10008 let step = match req {
10009 GenReq::Next(v) => gen_resume(&gen, v),
10010 GenReq::Return(v) => gen_return(&gen, v),
10011 GenReq::Throw(e) => gen_throw(&gen, e),
10012 };
10013 settle_async_gen_step(gen, sid, step);
10014}
10015
10016/// One request has settled: release the body and start the next queued request.
10017fn finish_async_gen_step(gen: Value, id: u32) {
10018 with_host(|h| h.generators[id as usize].running = false);
10019 pump_async_gen(gen, id);
10020}
10021
10022/// Whether `v` is an `async function*` object (its `.next()` yields promises).
10023pub fn is_async_generator(v: &Value) -> bool {
10024 let id = match with_host(|h| match h.get(v) {
10025 Some(JsObj::Generator { id }) => Some(*id),
10026 _ => None,
10027 }) {
10028 Some(id) => id,
10029 None => return false,
10030 };
10031 with_host(|h| h.generators[id as usize].async_gen)
10032}
10033
10034/// A `{ value, done }` iterator-result object.
10035fn iter_record(value: Value, done: bool) -> Value {
10036 with_host(|h| {
10037 let mut m = IndexMap::new();
10038 m.insert("value".to_string(), value);
10039 m.insert("done".to_string(), Value::Bool(done));
10040 h.new_object(m)
10041 })
10042}
10043
10044/// Resume a request that was suspended on an internal `await` (always a normal
10045/// completion — the awaited promise's outcome rides in `packet`).
10046fn drive_async_gen(gen: Value, sid: u32, packet: Value) {
10047 let step = gen_resume(&gen, packet);
10048 settle_async_gen_step(gen, sid, step);
10049}
10050
10051/// Turn one body resumption into a settled step promise: transparently re-drive
10052/// internal `await` suspensions, and settle on the first REAL yield or on the
10053/// body's completion. Shared by the initial resume of a queued request and by
10054/// every await-resumption of it.
10055fn settle_async_gen_step(gen: Value, sid: u32, step: Result<GenStep, String>) {
10056 let id = match with_host(|h| match h.get(&gen) {
10057 Some(JsObj::Generator { id }) => Some(*id),
10058 _ => None,
10059 }) {
10060 Some(id) => id,
10061 None => return,
10062 };
10063 match step {
10064 Ok(GenStep::Yield(v)) => match await_marker(&v) {
10065 Some(awaited) => {
10066 // An internal `await`: settle it, then resume the body. The
10067 // request stays in flight across the suspension.
10068 let ap = promise_of(&awaited);
10069 let aid = with_host(|h| h.promise_id(&ap).unwrap());
10070 subscribe_native(
10071 aid,
10072 Box::new(move |state, val| {
10073 let tag = if state == PromiseState::Rejected {
10074 1.0
10075 } else {
10076 0.0
10077 };
10078 let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
10079 drive_async_gen(gen.clone(), sid, packet);
10080 Ok(())
10081 }),
10082 );
10083 }
10084 // ECMA-262 27.6.3.8 AsyncGeneratorYield step 5: the yielded value is
10085 // AWAITED before the step promise settles, so `yield somePromise`
10086 // hands the consumer the RESOLVED value (and costs its microtask).
10087 None => {
10088 let yp = promise_of(&v);
10089 let yid = with_host(|h| h.promise_id(&yp).unwrap());
10090 subscribe_native(
10091 yid,
10092 Box::new(move |state, val| {
10093 if state == PromiseState::Rejected {
10094 reject_promise_val(sid, val);
10095 } else {
10096 resolve_promise_val(sid, iter_record(val, false));
10097 }
10098 finish_async_gen_step(gen.clone(), id);
10099 Ok(())
10100 }),
10101 );
10102 }
10103 },
10104 Ok(GenStep::Done(v)) => {
10105 resolve_promise_val(sid, iter_record(v, true));
10106 finish_async_gen_step(gen, id);
10107 }
10108 Err(e) => {
10109 let ev = take_exc_or_error(&e);
10110 reject_promise_val(sid, ev);
10111 finish_async_gen_step(gen, id);
10112 }
10113 }
10114}
10115
10116/// A promise for `v`: `v` itself if it is already a promise, else a promise
10117/// resolved with `v`.
10118pub fn promise_of(v: &Value) -> Value {
10119 if with_host(|h| h.promise_id(v)).is_some() {
10120 return v.clone();
10121 }
10122 let p = with_host(|h| h.new_promise());
10123 let id = with_host(|h| h.promise_id(&p).unwrap());
10124 resolve_promise_val(id, v.clone());
10125 p
10126}
10127
10128/// Register a native reaction on promise `id` (schedules immediately if already
10129/// settled).
10130pub fn subscribe_native(id: u32, f: Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>) {
10131 // A native continuation (`await`, promise adoption, `for await`) observes a
10132 // rejection exactly as a `.catch` does, so it is not "unhandled".
10133 with_host(|h| h.promise_mark_handled(id));
10134 let state = with_host(|h| h.promise_state(id));
10135 if state == PromiseState::Pending {
10136 with_host(|h| h.add_reaction(id, PromiseReaction::Native(f)));
10137 } else {
10138 let val = with_host(|h| h.promise_value(id));
10139 with_host(|h| h.queue_micro_native(Box::new(move || f(state, val))));
10140 }
10141}
10142
10143/// The Promise "resolve" operation: adopt `value`'s state if it is a promise,
10144/// else fulfill with it.
10145pub fn resolve_promise_val(id: u32, value: Value) {
10146 if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
10147 return;
10148 }
10149 if let Some(vid) = with_host(|h| h.promise_id(&value)) {
10150 if vid == id {
10151 // Resolving a promise with itself → reject with a TypeError.
10152 let e = with_host(|h| {
10153 crate::builtins::synth_error(h, "TypeError: Chaining cycle detected")
10154 });
10155 reject_promise_val(id, e);
10156 return;
10157 }
10158 // A native promise is still a thenable, so the spec routes it through
10159 // `NewPromiseResolveThenableJob` too — one microtask before the adoption
10160 // is even registered. (`await` does NOT pay this: V8's await optimization
10161 // subscribes to a native promise directly, which `await_value` mirrors.)
10162 with_host(|h| {
10163 h.queue_micro_native(Box::new(move || {
10164 subscribe_native(
10165 vid,
10166 Box::new(move |state, val| {
10167 with_host(|h| h.settle_promise(id, state, val.clone()));
10168 schedule_reactions(id);
10169 Ok(())
10170 }),
10171 );
10172 Ok(())
10173 }))
10174 });
10175 return;
10176 }
10177 // ECMA-262 27.2.1.3.2: any OBJECT carrying a callable `then` is assimilated
10178 // through a dedicated job — the promise adopts what `then` reports, it is
10179 // never fulfilled WITH the thenable itself.
10180 if let Some(then) = thenable_then(&value) {
10181 with_host(|h| {
10182 h.queue_micro_native(Box::new(move || resolve_thenable_job(id, value, then)))
10183 });
10184 return;
10185 }
10186 with_host(|h| h.settle_promise(id, PromiseState::Fulfilled, value));
10187 schedule_reactions(id);
10188}
10189
10190/// `value.then` if `value` is an object with a callable `then` — the test that
10191/// makes a value a *thenable*. Primitives (and objects without one) are `None`.
10192fn thenable_then(value: &Value) -> Option<Value> {
10193 // A PROXY is not a plain object and supplies `then` through its `get` trap,
10194 // so both tests below missed it: `Promise.resolve(proxyThenable)` fulfilled
10195 // WITH the proxy instead of adopting it.
10196 if with_host(|h| h.kind_of(value)) == Some(ObjKind::Proxy) {
10197 return protocol_lookup(value, "then")
10198 .ok()
10199 .flatten()
10200 .filter(|f| with_host(|h| is_callable(h, f)));
10201 }
10202 if !with_host(|h| matches!(h.get(value), Some(JsObj::Object(_)))) {
10203 return None;
10204 }
10205 let then = with_host(|h| lookup_chain(h, value, "then"))?;
10206 with_host(|h| is_callable(h, &then)).then_some(then)
10207}
10208
10209/// `NewPromiseResolveThenableJob`: hand the thenable this promise's own resolve /
10210/// reject continuations and let it settle us. A throw out of `then` rejects.
10211fn resolve_thenable_job(id: u32, thenable: Value, then: Value) -> Result<(), String> {
10212 let res = with_host(|h| h.alloc(JsObj::Builtin(format!("@@presolve:{id}"))));
10213 let rej = with_host(|h| h.alloc(JsObj::Builtin(format!("@@preject:{id}"))));
10214 if let Err(e) = invoke(&then, vec![res, rej], Some(thenable)) {
10215 let ev = take_exc_or_error(&e);
10216 reject_promise_val(id, ev);
10217 }
10218 Ok(())
10219}
10220
10221pub fn reject_promise_val(id: u32, value: Value) {
10222 if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
10223 return;
10224 }
10225 with_host(|h| {
10226 h.settle_promise(id, PromiseState::Rejected, value);
10227 h.pending_rejections.push(id);
10228 });
10229 schedule_reactions(id);
10230}
10231
10232/// Report every promise that settled rejected since the last checkpoint and
10233/// still has no handler. Node's default is `--unhandled-rejections=throw`: the
10234/// rejection becomes an uncaught exception (stderr + exit 1) unless a
10235/// `process.on('unhandledRejection')` listener takes it.
10236fn check_unhandled_rejections() -> Result<(), String> {
10237 loop {
10238 let ids: Vec<u32> = with_host(|h| std::mem::take(&mut h.pending_rejections));
10239 if ids.is_empty() {
10240 return Ok(());
10241 }
10242 for id in ids {
10243 let unhandled = with_host(|h| {
10244 h.promise_state(id) == PromiseState::Rejected && !h.promises[id as usize].handled
10245 });
10246 if !unhandled {
10247 continue;
10248 }
10249 // Report each promise at most once, however many checkpoints pass.
10250 with_host(|h| h.promise_mark_handled(id));
10251 let val = with_host(|h| h.promise_value(id));
10252 let listeners = with_host(|h| h.take_process_listeners("unhandledRejection"));
10253 if listeners.is_empty() {
10254 let msg = with_host(|h| crate::builtins::error_string(h, &val));
10255 with_host(|h| h.exc = Some(val));
10256 return Err(msg);
10257 }
10258 let promise = with_host(|h| h.alloc(JsObj::Promise { id }));
10259 for f in listeners {
10260 invoke(&f, vec![val.clone(), promise.clone()], None)?;
10261 }
10262 }
10263 }
10264}
10265
10266/// Drain a settled promise's reactions into microtasks.
10267fn schedule_reactions(id: u32) {
10268 let reactions = with_host(|h| h.take_reactions(id));
10269 let state = with_host(|h| h.promise_state(id));
10270 let value = with_host(|h| h.promise_value(id));
10271 for r in reactions {
10272 let value = value.clone();
10273 match r {
10274 PromiseReaction::Native(f) => {
10275 with_host(|h| h.queue_micro_native(Box::new(move || f(state, value))));
10276 }
10277 PromiseReaction::Js {
10278 on_ful,
10279 on_rej,
10280 result,
10281 } => {
10282 with_host(|h| {
10283 h.queue_micro_native(Box::new(move || {
10284 run_js_reaction(state, value, on_ful, on_rej, result)
10285 }))
10286 });
10287 }
10288 }
10289 }
10290}
10291
10292/// Run a `.then` reaction: call the appropriate handler and settle the result
10293/// promise with its outcome (or pass through if there is no handler).
10294fn run_js_reaction(
10295 state: PromiseState,
10296 value: Value,
10297 on_ful: Value,
10298 on_rej: Value,
10299 result: Value,
10300) -> Result<(), String> {
10301 let rid = match with_host(|h| h.promise_id(&result)) {
10302 Some(i) => i,
10303 None => return Ok(()),
10304 };
10305 let handler = if state == PromiseState::Rejected {
10306 on_rej
10307 } else {
10308 on_ful
10309 };
10310 if with_host(|h| is_callable(h, &handler)) {
10311 match invoke(&handler, vec![value], None) {
10312 Ok(r) => resolve_promise_val(rid, r),
10313 Err(e) => reject_promise_val(rid, take_exc_or_error(&e)),
10314 }
10315 } else if state == PromiseState::Rejected {
10316 reject_promise_val(rid, value);
10317 } else {
10318 resolve_promise_val(rid, value);
10319 }
10320 Ok(())
10321}
10322
10323/// The JS value of a just-caught error: the live `exc` (a real thrown value) or a
10324/// synthesized `Error` from the internal message.
10325pub fn take_exc_or_error(e: &str) -> Value {
10326 with_host(|h| {
10327 h.error.take();
10328 h.exc
10329 .take()
10330 .unwrap_or_else(|| crate::builtins::synth_error(h, e))
10331 })
10332}
10333
10334/// Register a user `.then` reaction (JS handlers + result promise).
10335pub fn promise_then(p: &Value, on_ful: Value, on_rej: Value) -> Value {
10336 let id = match with_host(|h| h.promise_id(p)) {
10337 Some(i) => i,
10338 None => return Value::Undef,
10339 };
10340 with_host(|h| h.promise_mark_handled(id));
10341 // The result is built with the RECEIVER's species, so a subclass promise
10342 // stays a subclass promise through a `.then` chain.
10343 let result = match crate::builtins::promise_species_from(p) {
10344 Ok(Some(sp)) => sp,
10345 _ => with_host(|h| h.new_promise()),
10346 };
10347 let reaction = PromiseReaction::Js {
10348 on_ful,
10349 on_rej,
10350 result: result.clone(),
10351 };
10352 let state = with_host(|h| h.promise_state(id));
10353 if state == PromiseState::Pending {
10354 with_host(|h| h.add_reaction(id, reaction));
10355 } else {
10356 let value = with_host(|h| h.promise_value(id));
10357 if let PromiseReaction::Js {
10358 on_ful,
10359 on_rej,
10360 result,
10361 } = reaction
10362 {
10363 with_host(|h| {
10364 h.queue_micro_native(Box::new(move || {
10365 run_js_reaction(state, value, on_ful, on_rej, result)
10366 }))
10367 });
10368 }
10369 }
10370 result
10371}
10372
10373/// The ReferenceError for touching `this` in a derived constructor before
10374/// `super()` — or returning from one without calling it.
10375pub fn this_before_super_error() -> String {
10376 "ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor".to_string()
10377}
10378
10379/// The intrinsic a builtin name denotes. Two property paths that the spec
10380/// defines as the SAME function object compare `===`: `Number.parseInt` is
10381/// `%parseInt%` (21.1.2.13) and `Number.parseFloat` is `%parseFloat%`
10382/// (21.1.2.12), so `Number.parseInt === parseInt` is `true`.
10383fn builtin_identity(name: &str) -> &str {
10384 match name {
10385 "Number.parseInt" => "parseInt",
10386 "Number.parseFloat" => "parseFloat",
10387 _ => name,
10388 }
10389}