nodejs/builtins.rs
1//! Builtin op handlers (compiler-emitted `CallBuiltin` ids) plus the JS standard
2//! library (`console`, `Math`, `JSON`, `Object`, array/string methods) reachable
3//! from the host. Handlers pop their arguments off the VM operand stack and
4//! return the result value, which the VM pushes back.
5
6use crate::host::{self, ops, with_host, FuncVal, JsObj, ObjKind};
7use fusevm::{NumOp, Value, VM};
8use indexmap::IndexMap;
9
10/// Register every node-js builtin id on a VM.
11pub fn install(vm: &mut VM) {
12 vm.register_builtin(ops::GETLOCAL, b_getlocal);
13 vm.register_builtin(ops::SETLOCAL, b_setlocal);
14 vm.register_builtin(ops::SETLOCAL_STRICT, b_setlocal_strict);
15 vm.register_builtin(ops::DECLARE, b_declare);
16 vm.register_builtin(ops::DECLARE_CONST, b_declare_const);
17 vm.register_builtin(ops::MARK_HOLE, b_mark_hole);
18 vm.register_builtin(ops::DELNAME, b_delname);
19 vm.register_builtin(ops::GETATTR, b_getattr);
20 vm.register_builtin(ops::SETATTR, b_setattr);
21 vm.register_builtin(ops::GETITEM, b_getitem);
22 vm.register_builtin(ops::SETITEM, b_setitem);
23 vm.register_builtin(ops::DELITEM, b_delitem);
24 vm.register_builtin(ops::MKSTR, b_mkstr);
25 vm.register_builtin(ops::MKARR, b_mkarr);
26 vm.register_builtin(ops::MKOBJ, b_mkobj);
27 vm.register_builtin(ops::CALL, b_call);
28 vm.register_builtin(ops::CALL_METHOD, b_call_method);
29 vm.register_builtin(ops::CALL_VALUE, b_call_value);
30 vm.register_builtin(ops::NEW, b_new);
31 vm.register_builtin(ops::TRUTHY, b_truthy);
32 vm.register_builtin(ops::TOSTR, b_tostr);
33 vm.register_builtin(ops::MKFUNC, b_mkfunc);
34 vm.register_builtin(ops::GETITER, b_getiter);
35 vm.register_builtin(ops::FORITER, b_foriter);
36 vm.register_builtin(ops::FORIN_KEYS, b_forin_keys);
37 vm.register_builtin(ops::FORIN_ALIVE, b_forin_alive);
38 vm.register_builtin(ops::HOIST_TDZ, b_hoist_tdz);
39 vm.register_builtin(ops::NEW_SPREAD, b_new_spread);
40 vm.register_builtin(ops::SUPER_CALL_SPREAD, b_super_call_spread);
41 vm.register_builtin(ops::CONTAINS, b_contains);
42 vm.register_builtin(ops::SIG_RETURN, b_sig_return);
43 vm.register_builtin(ops::BINOP, b_binop);
44 vm.register_builtin(ops::UNARY, b_unary);
45 vm.register_builtin(ops::STRICT_EQ, b_strict_eq);
46 vm.register_builtin(ops::LOOSE_EQ, b_loose_eq);
47 vm.register_builtin(ops::TYPEOF, b_typeof);
48 vm.register_builtin(ops::LOAD_NULL, b_load_null);
49 vm.register_builtin(ops::THROW, b_throw);
50 vm.register_builtin(ops::TRY, b_try);
51 vm.register_builtin(ops::NULLISH, b_nullish);
52 vm.register_builtin(ops::UNPACK, b_unpack);
53 vm.register_builtin(ops::BUILD_ARGS, b_build_args);
54 vm.register_builtin(ops::THIS, b_this);
55 vm.register_builtin(ops::INSTANCEOF, b_instanceof);
56 vm.register_builtin(ops::DELPROP_NAME, b_delprop_name);
57 vm.register_builtin(ops::APPLY, b_apply);
58 vm.register_builtin(ops::APPLY_METHOD, b_apply_method);
59 vm.register_builtin(ops::OBJ_REST, b_obj_rest);
60 vm.register_builtin(ops::DIV, b_div);
61 vm.register_builtin(ops::POW, b_pow);
62 vm.register_builtin(ops::MKCLASS, b_mkclass);
63 vm.register_builtin(ops::DEF_MEMBER, b_def_member);
64 vm.register_builtin(ops::DEF_FIELD, b_def_field);
65 vm.register_builtin(ops::SUPER_CALL, b_super_call);
66 vm.register_builtin(ops::SUPER_GET, b_super_get);
67 vm.register_builtin(ops::YIELD, b_yield);
68 vm.register_builtin(ops::PROPKEY, b_propkey);
69 vm.register_builtin(ops::NEW_TARGET, b_new_target);
70 vm.register_builtin(ops::AWAIT, b_await);
71 vm.register_builtin(ops::DEF_ACCESSOR, b_def_accessor);
72 vm.register_builtin(ops::DBG_LINE, b_dbg_line);
73 vm.register_builtin(ops::MKBIGINT, b_mkbigint);
74 vm.register_builtin(ops::MKREGEX, b_mkregex);
75 vm.register_builtin(ops::TAG_TMPL, b_tag_tmpl);
76 vm.register_builtin(ops::GET_ASYNC_ITER, b_get_async_iter);
77 vm.register_builtin(ops::ASYNC_STEP, b_async_step);
78 vm.register_builtin(ops::NUM_STEP, b_num_step);
79 vm.register_builtin(ops::ITER_CLOSE, b_iter_close);
80 vm.register_builtin(ops::TYPEOF_NAME, b_typeof_name);
81 vm.register_builtin(ops::SIG_BREAK, b_sig_break);
82 vm.register_builtin(ops::SIG_CONTINUE, b_sig_continue);
83 vm.register_builtin(ops::SIG_UNWIND, b_sig_unwind);
84 vm.register_builtin(ops::PUSH_SCOPE, b_push_scope);
85 vm.register_builtin(ops::POP_SCOPE, b_pop_scope);
86 vm.register_builtin(ops::COPY_SCOPE, b_copy_scope);
87 vm.register_builtin(ops::DECLARE_VAR, b_declare_var);
88 vm.register_builtin(ops::HOIST_VAR, b_hoist_var);
89 vm.register_builtin(ops::NAMED_EVAL, b_named_eval);
90}
91
92/// `ITER_CLOSE`: close the iterator on the stack (a for-of `break`). A generator
93/// runs its pending `finally`; a user iterator object gets its `.return()` called
94/// if present; a plain materialized iterator just drops. Returns `undefined`.
95/// `IteratorClose` (7.4.9): resume a generator with a forced return so its
96/// pending `finally` runs, or invoke a user iterator's `.return()`. A value that
97/// is neither is left alone.
98pub(crate) fn close_iterator(it: &Value) -> Result<(), String> {
99 if with_host(|h| h.is_generator_val(it)) {
100 host::gen_return(it, Value::Undef)?;
101 return Ok(());
102 }
103 if matches!(with_host(|h| h.get(it).cloned()), Some(JsObj::Object(_))) {
104 if let Some(f) = with_host(|h| host::lookup_chain(h, it, "return")) {
105 if with_host(|h| host::is_callable(h, &f)) {
106 host::invoke(&f, Vec::new(), Some(it.clone()))?;
107 }
108 }
109 }
110 Ok(())
111}
112
113fn b_iter_close(vm: &mut VM, _: u8) -> Value {
114 let it = vm.pop();
115 // A `finally` may print or yield, but the loop is done either way; an error
116 // it raises still propagates.
117 match close_iterator(&it) {
118 Ok(()) => Value::Undef,
119 Err(e) => abort(vm, e),
120 }
121}
122
123/// `NUM_STEP`: the `++`/`--` core. Pops `old` and the step `tag` (`+1`/`-1`),
124/// pushes `ToNumeric(old)` (a BigInt stays a BigInt, else a Number), and returns
125/// `old ± 1` in the SAME numeric type — so `x++` on a BigInt neither coerces to
126/// Number nor throws the mix error.
127fn b_num_step(vm: &mut VM, _: u8) -> Value {
128 let old = vm.pop();
129 let tag = match vm.pop() {
130 Value::Int(n) => n,
131 Value::Float(f) => f as i64,
132 _ => 1,
133 };
134 if with_host(|h| h.is_bigint_val(&old)) {
135 let b = with_host(|h| h.as_bigint(&old)).unwrap();
136 let old_n = with_host(|h| h.new_bigint(b.clone()));
137 let new = with_host(|h| h.new_bigint(b + num_bigint::BigInt::from(tag)));
138 vm.push(old_n);
139 new
140 } else {
141 let n = with_host(|h| h.to_number(&old));
142 vm.push(Value::Float(n));
143 Value::Float(n + tag as f64)
144 }
145}
146
147/// `ASYNC_STEP`: one step of a `for await` loop — returns a Promise of the
148/// `{value, done}` record (see `host::async_step`).
149fn b_async_step(vm: &mut VM, _: u8) -> Value {
150 let iter = vm.pop();
151 let r = host::async_step(&iter);
152 finish(vm, r)
153}
154
155/// `MKBIGINT`: pop the canonical decimal digit string constant, allocate the heap
156/// BigInt. The lexer already validated the digits, so parsing cannot fail here.
157fn b_mkbigint(vm: &mut VM, _: u8) -> Value {
158 let digits = sval(&vm.pop());
159 match digits.parse::<num_bigint::BigInt>() {
160 Ok(b) => with_host(|h| h.new_bigint(b)),
161 Err(_) => abort(vm, host::type_error("invalid BigInt literal")),
162 }
163}
164
165/// `TAG_TMPL`: invoke a tagged template. The compiler emits the operands as
166/// `[tag, n, m, cooked×n, raw×n, values×m]` (see `compile_tagged_template`).
167/// Builds the `strings` array (carrying its `.raw` array) and calls
168/// `tag(strings, ...values)`.
169/// Reject a non-callable where node's scheduling entry points demand one.
170///
171/// Every one of them validates SYNCHRONOUSLY — `try { queueMicrotask(1) }
172/// catch` catches an `ERR_INVALID_ARG_TYPE` in node. Here the value was queued
173/// unchecked and the failure surfaced from the event loop instead, as an
174/// uncaught `1 is not a function` that killed the process past any `try` around
175/// the call.
176fn require_callback(cb: &Value) -> Result<(), String> {
177 if with_host(|h| host::is_callable(h, cb)) {
178 return Ok(());
179 }
180 Err(host::invalid_arg_type(
181 "callback", "argument", "function", cb,
182 ))
183}
184
185fn b_tag_tmpl(vm: &mut VM, argc: u8) -> Value {
186 // The chunk holding this site, read before the operands are popped and
187 // before any host borrow: together with the compiler's per-site ordinal it
188 // names the Parse Node whose template object 13.2.8.4 caches.
189 let chunk = vm.chunk.op_hash;
190 let mut all = pop_n(vm, argc as usize);
191 let int_of = |v: &Value| match v {
192 Value::Int(n) => *n as usize,
193 Value::Float(f) => *f as usize,
194 _ => 0,
195 };
196 let this = all.remove(0);
197 let tag = all.remove(0);
198 let n = int_of(&all.remove(0));
199 let mcount = int_of(&all.remove(0));
200 let site = int_of(&all.remove(0)) as u64;
201 let cooked: Vec<Value> = all.drain(0..n.min(all.len())).collect();
202 let raw: Vec<Value> = all.drain(0..n.min(all.len())).collect();
203 let values: Vec<Value> = all.drain(0..mcount.min(all.len())).collect();
204 // GetTemplateObject caches by Parse Node, so a site evaluated twice hands
205 // back the SAME object — the whole point of the caching, since a tag that
206 // memoizes on the strings array (lit-html, graphql-tag) re-parses its
207 // template on every call without it.
208 let key = (chunk, site);
209 let strings = match with_host(|h| h.template_object(key)) {
210 Some(cached) => cached,
211 None => {
212 // strings = cooked array; strings.raw = raw array.
213 let strings = with_host(|h| h.new_array(cooked));
214 let raw_arr = with_host(|h| h.new_array(raw));
215 // `raw` is an own property that is neither writable, enumerable, nor
216 // configurable, so it stays out of `Object.keys(strings)` while
217 // `getOwnPropertyNames` still reports it.
218 with_host(|h| {
219 h.set_fn_prop(&strings, "raw", raw_arr.clone());
220 h.set_prop_attrs(
221 &strings,
222 "raw",
223 host::PropAttrs {
224 writable: false,
225 enumerable: false,
226 configurable: false,
227 },
228 );
229 // Steps 12-13 run SetIntegrityLevel(frozen) on the raw array and
230 // then on the template object itself. Without them a tag could
231 // write through its own strings array and corrupt every later
232 // evaluation of the site — which is exactly what caching makes
233 // reachable, so the freeze and the cache belong together.
234 h.seal_object(&raw_arr, true);
235 h.seal_object(&strings, true);
236 h.set_template_object(key, strings.clone());
237 });
238 strings
239 }
240 };
241 let mut call_args = vec![strings];
242 call_args.extend(values);
243 let this = match this {
244 Value::Undef => None,
245 v => Some(v),
246 };
247 let r = host::invoke(&tag, call_args, this);
248 finish(vm, r)
249}
250
251/// `GET_ASYNC_ITER`: obtain an async iterator for `for await (… of …)`. If the
252/// value has a `Symbol.asyncIterator`, use it; otherwise fall back to its sync
253/// iterator (each yielded value is awaited). Returns the iterator object/handle.
254fn b_get_async_iter(vm: &mut VM, _: u8) -> Value {
255 let src = vm.pop();
256 let r = host::get_async_iterator(&src).map_err(|e| {
257 // `for await` names the source AND says ASYNC: `for await (const x of
258 // o)` is `o is not async iterable`. Built here rather than through
259 // `name_call_site`, whose suffix table has no entry that composes.
260 match host::call_site_text(vm) {
261 Some(t) if e.ends_with(" is not iterable") => {
262 host::type_error(&format!("{t} is not async iterable"))
263 }
264 _ => e,
265 }
266 });
267 finish(vm, r)
268}
269
270/// `MKREGEX`: pop `(pattern, flags)`, translate the JS pattern to a Rust `regex`,
271/// and allocate a `RegExp`. A pattern using a JS feature Rust `regex` cannot
272/// express (backreference/lookaround) throws a `SyntaxError` here.
273fn b_mkregex(vm: &mut VM, _: u8) -> Value {
274 let flags = sval(&vm.pop());
275 let pattern = sval(&vm.pop());
276 match crate::regexp::build_regexp(&pattern, &flags) {
277 Ok(v) => v,
278 Err(e) => abort(vm, e),
279 }
280}
281
282/// DAP per-statement marker (`node --dap` only; the compiler emits this before
283/// each statement under `debug`). Pops the source line pushed by the preceding
284/// `LoadInt` and fires the debugger line hook, which pauses at breakpoints/step
285/// targets. Returns `undefined` (the compiler pops it). A no-op unless a debug
286/// session is active.
287fn b_dbg_line(vm: &mut VM, _: u8) -> Value {
288 let line = match vm.pop() {
289 Value::Int(n) => n as u32,
290 _ => 0,
291 };
292 crate::dap::on_debug_line(line);
293 Value::Undef
294}
295
296/// Install an object-literal getter/setter on an object (`kind` is `member::GET`
297/// or `member::SET`). Keeps the object on the stack.
298fn b_def_accessor(vm: &mut VM, _: u8) -> Value {
299 let func = vm.pop();
300 let kind = match vm.pop() {
301 Value::Int(n) => n,
302 _ => 0,
303 };
304 let name = sval(&vm.pop());
305 let obj = vm.pop();
306 with_host(|h| {
307 if kind == host::member::SET {
308 h.set_accessor(&obj, &name, None, Some(func));
309 } else {
310 h.set_accessor(&obj, &name, Some(func), None);
311 }
312 });
313 obj
314}
315
316fn b_await(vm: &mut VM, _: u8) -> Value {
317 let v = vm.pop();
318 match host::await_value(v) {
319 Ok(r) => r,
320 Err(e) => abort(vm, e),
321 }
322}
323
324// ── classes / super / generators / property keys (compiler-emitted ops) ──────
325
326fn b_mkclass(vm: &mut VM, argc: u8) -> Value {
327 // The fourth argument, when present, is the FuncDef carrying the class's
328 // source span.
329 let source_def = match argc {
330 4 => match vm.pop() {
331 Value::Int(n) => Some(n as usize),
332 _ => None,
333 },
334 _ => None,
335 };
336 let ctor = vm.pop();
337 let parent = vm.pop();
338 let name = sval(&vm.pop());
339 host::build_class(&name, parent, ctor, source_def)
340}
341
342fn b_def_member(vm: &mut VM, _: u8) -> Value {
343 let func = vm.pop();
344 let is_static = matches!(vm.pop(), Value::Bool(true));
345 let kind = match vm.pop() {
346 Value::Int(n) => n,
347 _ => 0,
348 };
349 let name = sval(&vm.pop());
350 let class_val = vm.pop();
351 host::define_member(&class_val, &name, kind, is_static, func);
352 class_val
353}
354
355fn b_def_field(vm: &mut VM, _: u8) -> Value {
356 // `name_anon`: the initializer was an anonymous function definition, so
357 // 15.7.10 NamedEvaluation names its result after the field. Syntactic —
358 // decided by the compiler, not re-derived from the produced value.
359 let name_anon = matches!(vm.pop(), Value::Bool(true));
360 let thunk = vm.pop();
361 let name = sval(&vm.pop());
362 let class_val = vm.pop();
363 host::define_field(&class_val, &name, thunk, name_anon);
364 class_val
365}
366
367/// `super(...args)` in a derived constructor: run the parent constructor on the
368/// current `this`, then this class's field initializers.
369/// `SUPER_CALL_SPREAD` — `super(...xs)`, where the argument list is built at
370/// run time. Shares everything below with the fixed-arity form; only where the
371/// arguments come from differs.
372fn b_super_call_spread(vm: &mut VM, _: u8) -> Value {
373 let arr = vm.pop();
374 let args = host::iter_all(&arr).unwrap_or_default();
375 super_call_with(vm, args)
376}
377
378fn b_super_call(vm: &mut VM, argc: u8) -> Value {
379 let args = pop_n(vm, argc as usize);
380 super_call_with(vm, args)
381}
382
383fn super_call_with(vm: &mut VM, args: Vec<Value>) -> Value {
384 let this = with_host(|h| h.current_this());
385 let this = match this {
386 Some(t) => t,
387 None => return abort(vm, host::type_error("'super' keyword unexpected here")),
388 };
389 // The class whose constructor is running = the running method's home class.
390 let (parent, fields) = with_host(|h| h.super_context());
391 let (parent, fields) = match parent {
392 Some(p) => (p, fields),
393 None => return abort(vm, host::type_error("'super' keyword unexpected here")),
394 };
395 let nt = with_host(|h| h.current_new_target()).unwrap_or_else(|| this.clone());
396 let this = match host::super_construct(&parent, args, &this, &nt) {
397 Err(e) => return abort(vm, e),
398 // The parent returned an object of its own: 15.7.15 makes THAT the
399 // instance, so `this` is rebound to it for the rest of the constructor
400 // and it is what `new` hands back.
401 Ok(Some(replacement)) => {
402 with_host(|h| h.set_current_this(replacement.clone()));
403 replacement
404 }
405 Ok(None) => this,
406 };
407 if !with_host(|h| h.bind_super_this()) {
408 return abort(
409 vm,
410 "ReferenceError: Super constructor may only be called once".to_string(),
411 );
412 }
413 // Run this (derived) class's own instance-field initializers after super.
414 for (name, thunk, name_anon) in fields {
415 if let Err(e) = host::init_one_field(&this, &name, &thunk, name_anon) {
416 return abort(vm, e);
417 }
418 }
419 Value::Undef
420}
421
422/// `super.name` — a method from the parent's prototype, or a getter's result.
423fn b_super_get(vm: &mut VM, _: u8) -> Value {
424 let name = sval(&vm.pop());
425 match with_host(|h| h.super_resolve(&name)) {
426 host::SuperRef::Data(v) => v,
427 host::SuperRef::Getter(getter) => {
428 let this = with_host(|h| h.current_this());
429 match host::invoke(&getter, Vec::new(), this) {
430 Ok(v) => v,
431 Err(e) => abort(vm, e),
432 }
433 }
434 }
435}
436
437/// Close every loop iterator parked on `vm`'s stack at the op now executing,
438/// innermost first. Called where a chunk is about to be halted abruptly, since
439/// the code that would ordinarily close them is being jumped over.
440///
441/// A close runs user code (a generator's `finally`), which can itself throw; the
442/// error is deliberately dropped, because it must not replace the completion
443/// that caused the unwind.
444fn close_parked_iters(vm: &mut VM) {
445 let n = host::parked_iters(vm);
446 if n == 0 {
447 return;
448 }
449 // The completion that caused the unwind is already pending on the host.
450 // Closing an iterator resumes ANOTHER generator, which settles its own
451 // signal/error state, so the pending one is saved across the close and put
452 // back — otherwise the outer `.return()` would be lost.
453 let saved = with_host(|h| (h.signal.take(), h.error.take()));
454 for _ in 0..n {
455 let it = vm.pop();
456 let _ = close_iterator(&it);
457 }
458 with_host(|h| {
459 h.signal = saved.0;
460 h.error = saved.1;
461 });
462}
463
464fn b_yield(vm: &mut VM, _: u8) -> Value {
465 let v = vm.pop();
466 match host::gen_yield(v) {
467 Ok(sent) => {
468 // A `.return()`/`.throw()` injected on resume sets a pending Return
469 // signal (or error); halt the chunk so the body unwinds through any
470 // enclosing `try/finally`, exactly like a source `return`/`throw`.
471 if with_host(|h| h.error.is_some() || h.signal.is_some()) {
472 // Halting jumps past the loop exits, so the `for…of` / `yield*`
473 // iterators parked on this chunk's stack would be abandoned
474 // still-suspended. They sit directly beneath the yielded value
475 // (innermost last), and the compiler recorded how many are
476 // there for this exact op.
477 close_parked_iters(vm);
478 vm.ip = vm.chunk.ops.len();
479 }
480 sent
481 }
482 // An injected `.throw()` comes back as an error rather than a signal,
483 // and abandons the parked iterators the same way. The thrown value is
484 // already on the host as `exc`; `close_parked_iters` puts back whatever
485 // it saves, so the close cannot swallow it.
486 Err(e) => {
487 close_parked_iters(vm);
488 abort(vm, e)
489 }
490 }
491}
492
493/// `PROPKEY` — ToPropertyKey (7.1.19) for an object literal's COMPUTED key.
494///
495/// It called `JsHost::property_key` directly, which is the primitive-only half
496/// of the conversion, so an object key never ran `ToPrimitive`:
497/// `{ [{toString(){return "TS"}}]: 1 }` keyed on `"[object Object]"` while the
498/// member form `a[o] = 1` — which does go through `host::to_property_key` —
499/// keyed on `"TS"`. The two forms are the same abstract operation and now share
500/// the same implementation.
501fn b_propkey(vm: &mut VM, _: u8) -> Value {
502 let v = vm.pop();
503 match host::to_property_key(&v) {
504 Ok(k) => with_host(|h| h.new_str(k)),
505 Err(e) => abort(vm, e),
506 }
507}
508
509fn b_new_target(_vm: &mut VM, _: u8) -> Value {
510 with_host(|h| h.current_new_target().unwrap_or(Value::Undef))
511}
512
513/// `a / b` with JS/IEEE-754 semantics. fusevm's native `Op::Div` returns `Undef`
514/// for a zero divisor (so a frontend whose `/` differs must lower to a builtin —
515/// its own documented guidance), but JavaScript requires `x/0 === ±Infinity` and
516/// `0/0 === NaN`, so `/` is lowered here instead.
517///
518/// Being a builtin rather than a native op means it does NOT reach the numeric
519/// hook, so `/` was the one arithmetic operator that never ran `ToPrimitive`:
520/// `({valueOf(){return 7}}) / 2` was `NaN` where every other operator gave
521/// `3.5`, and `new Date(2) / 1` was `NaN` instead of `2`. It goes through the
522/// hook now, so `/` coerces exactly as `*` and `-` do.
523fn b_div(vm: &mut VM, _: u8) -> Value {
524 let b = vm.pop();
525 let a = vm.pop();
526 let r = numeric_hook(NumOp::Div, &a, &b);
527 finish(vm, r)
528}
529
530/// `a ** b`. Same reason `/` is a builtin: fusevm's native `Op::Pow` is IEEE-754
531/// `pow`, which returns 1 for `(-1) ** Infinity` and for `1 ** NaN` where the
532/// spec says NaN. Routing through the numeric hook also keeps BigInt `**` on the
533/// one code path that already handles it.
534fn b_pow(vm: &mut VM, _: u8) -> Value {
535 let b = vm.pop();
536 let a = vm.pop();
537 let r = numeric_hook(NumOp::Pow, &a, &b);
538 finish(vm, r)
539}
540
541/// `{ ...rest } = obj`: a new object of `obj`'s own keys minus the excluded set.
542fn b_obj_rest(vm: &mut VM, _: u8) -> Value {
543 let excluded = vm.pop();
544 let obj = vm.pop();
545 // The excluded keys are normalized exactly as a property READ normalizes
546 // them, not merely stringified: a symbol key lives on the object under its
547 // internal `@@sym:<id>` spelling, and `str_of` renders it `Symbol(k)`, which
548 // matches no key at all — so `const { [sym]: v, ...rest } = o` left the
549 // symbol-keyed property in `rest`.
550 let excl: Vec<String> = with_host(|h| h.iter_vec(&excluded))
551 .unwrap_or_default()
552 .iter()
553 .filter_map(|v| host::to_property_key(v).ok())
554 .collect();
555 // CopyDataProperties (ECMA-262 7.3.25) copies the own ENUMERABLE keys,
556 // symbol-keyed ones included. `own_enum_key_names` is what `Object.keys`
557 // uses, so an ACCESSOR is in the list — reading the property map directly
558 // missed one entirely, and `const { ...r } = { get g() {…} }` produced an
559 // object with no `g` and never ran the getter.
560 // A PROXY answers from its traps — `ownKeys`, then a
561 // `getOwnPropertyDescriptor` per key to test enumerability — which
562 // `own_enum_key_names` cannot see. Rest over one produced an empty object
563 // and ran no traps at all.
564 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
565 let keys = match crate::proxy::own_keys(&obj) {
566 Ok(k) => k.unwrap_or_default(),
567 Err(e) => return abort(vm, e),
568 };
569 let mut pairs: Vec<(String, Value)> = Vec::new();
570 for k in keys {
571 if excl.contains(&k) {
572 continue;
573 }
574 // The enumerability test and the READ interleave per key, as node's
575 // trap log shows — testing every key first and then reading them
576 // all produced the right object through the wrong trap sequence.
577 match crate::proxy::own_enumerable(&obj, &k) {
578 Ok(false) => continue,
579 Ok(true) => {}
580 Err(e) => return abort(vm, e),
581 }
582 match get_property(&obj, &k) {
583 Ok(v) => pairs.push((k, v)),
584 Err(e) => return abort(vm, e),
585 }
586 }
587 return with_host(|h| h.new_object(pairs.into_iter().collect()));
588 }
589 let keys: Vec<String> = with_host(|h| {
590 // `own_enum_key_names` is the STRING half — the same list `Object.keys`
591 // gives, so an accessor is in it. The symbol-keyed half lives in the
592 // property map under the internal `@@sym:` spelling and has to be
593 // collected separately, since `Object.keys` deliberately omits it.
594 let mut ks = h.own_enum_key_names(&obj);
595 if let Some(JsObj::Object(m)) = h.get(&obj) {
596 for k in m.keys() {
597 if host::is_symbol_key(k) && h.prop_attrs(&obj, k).enumerable {
598 ks.push(k.clone());
599 }
600 }
601 }
602 ks
603 })
604 .into_iter()
605 .filter(|k| {
606 // An internal slot (`@@native`, `@@bytes`, …) or a private class field
607 // is not a property; a SYMBOL key shares the `@@` prefix but is one, so
608 // the two cases cannot be told apart by the prefix alone.
609 !excl.contains(k)
610 && (host::is_symbol_key(k) || !(k.starts_with("@@") || k.starts_with('#')))
611 })
612 .collect();
613 // Each value is read through `[[Get]]`, OUTSIDE the host borrow: a getter is
614 // user code and re-entering the VM under the borrow aborts the process.
615 let mut pairs: Vec<(String, Value)> = Vec::with_capacity(keys.len());
616 for k in keys {
617 match get_property(&obj, &k) {
618 Ok(v) => pairs.push((k, v)),
619 Err(e) => return abort(vm, e),
620 }
621 }
622 with_host(|h| {
623 let props: IndexMap<String, Value> = pairs.into_iter().collect();
624 h.new_object(props)
625 })
626}
627
628// ── helpers ──────────────────────────────────────────────────────────────────
629
630fn pop_n(vm: &mut VM, n: usize) -> Vec<Value> {
631 let mut v = Vec::with_capacity(n);
632 for _ in 0..n {
633 v.push(vm.pop());
634 }
635 v.reverse();
636 v
637}
638
639/// Read a compiler-internal name string (native `Value::Str` or heap `str`).
640fn sval(v: &Value) -> String {
641 if let Value::Str(s) = v {
642 return (**s).clone();
643 }
644 with_host(|h| h.as_str(v)).unwrap_or_default()
645}
646
647/// The same string, without `sval`'s deep copy. Every identifier the compiler
648/// emits is a `Value::Str` constant, so a variable read or write that went
649/// through `sval` heap-allocated and memcpy'd the NAME once per access — on the
650/// hot path of every loop. `Value::Str` is an `Arc<String>`, so cloning the
651/// handle is a refcount bump instead.
652fn sname(v: &Value) -> std::sync::Arc<String> {
653 match v {
654 Value::Str(s) => s.clone(),
655 _ => std::sync::Arc::new(sval(v)),
656 }
657}
658
659fn abort(vm: &mut VM, e: String) -> Value {
660 with_host(|h| h.error = Some(e));
661 vm.ip = vm.chunk.ops.len();
662 Value::Undef
663}
664
665/// Halt the chunk if a call left an error or non-local signal pending.
666fn finish(vm: &mut VM, r: Result<Value, String>) -> Value {
667 match r {
668 Ok(v) => {
669 if with_host(|h| h.error.is_some() || h.signal.is_some()) {
670 vm.ip = vm.chunk.ops.len();
671 }
672 v
673 }
674 Err(e) => abort(vm, e),
675 }
676}
677
678// ── name handlers ─────────────────────────────────────────────────────────────
679
680/// The value a bare global identifier resolves to, or `None` if unbound.
681///
682/// Shared by `b_getlocal` (the `x` form) and the `globalThis.x` property read,
683/// which must agree: a name reachable one way and not the other is exactly the
684/// discrepancy that left `globalThis.process` undefined while `process` worked.
685pub(crate) fn global_binding(name: &str) -> Option<Value> {
686 global_binding_from(name, false)
687}
688
689/// [`global_binding`] restricted to what the GLOBAL OBJECT really holds.
690///
691/// A `globalThis.x` read falls back to the same lazy binding a bare `x` gets,
692/// which is what makes `globalThis.Math` and `globalThis.process` work — but
693/// the bare-identifier lookup walks the SCOPE CHAIN, so while any function was
694/// running its locals were readable off `globalThis`: `function f() { let zzq =
695/// 2; return typeof globalThis.zzq }` answered for a name the global object has
696/// never heard of. Only the globals map and the lazy builtins below may answer
697/// here.
698pub(crate) fn global_object_binding(name: &str) -> Option<Value> {
699 global_binding_from(name, true)
700}
701
702fn global_binding_from(name: &str, object_only: bool) -> Option<Value> {
703 let bound = with_host(|h| {
704 if object_only {
705 h.read_global(name)
706 } else {
707 h.read_name(name)
708 }
709 });
710 if let Some(v) = bound {
711 return Some(v);
712 }
713 // Globals bound lazily: numeric sentinels + builtin namespaces.
714 match name {
715 "undefined" => return Some(Value::Undef),
716 "NaN" => return Some(Value::Float(f64::NAN)),
717 "Infinity" => return Some(Value::Float(f64::INFINITY)),
718 // One object, not a fresh one per read: `globalThis === globalThis` is
719 // `true` in JS, and `globalThis.x = 1` is readable back as
720 // `globalThis.x`. Both were false while each read minted a new object.
721 // `global` is Node's alias for the same object.
722 "globalThis" | "global" => return Some(with_host(|h| h.global_object())),
723 // The WHATWG `crypto` global IS `require('crypto').webcrypto`, not the
724 // node-flavoured module: `globalThis.crypto.randomUUID` exists while
725 // `globalThis.crypto.createHash` does not.
726 "crypto" => return Some(with_host(|h| h.alloc(JsObj::Builtin("webcrypto".into())))),
727 _ => {}
728 }
729 if is_namespace(name) || is_known_builtin(name) {
730 return Some(with_host(|h| h.alloc(JsObj::Builtin(name.to_string()))));
731 }
732 None
733}
734
735fn b_getlocal(vm: &mut VM, _: u8) -> Value {
736 let name = sname(&vm.pop());
737 // A module-top-level dead zone is tracked by NAME rather than by a parked
738 // marker, so that the marker is never reachable as `globalThis.<name>`. It
739 // only applies when nothing on the scope chain SHADOWS the name — a class's
740 // own inner binding for its name does exactly that while its static
741 // initializers run.
742 if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
743 return abort(vm, host::tdz_error(&name));
744 }
745 match global_binding(&name) {
746 // The binding EXISTS but has not reached its declaration yet.
747 Some(v) if with_host(|h| h.is_tdz(&v)) => abort(vm, host::tdz_error(&name)),
748 Some(v) => v,
749 None => abort(vm, host::ref_error(&name)),
750 }
751}
752
753/// `HOIST_TDZ` — declare one `let`/`const`/`class` name as uninitialized at the
754/// top of the scope that declares it.
755fn b_hoist_tdz(vm: &mut VM, _: u8) -> Value {
756 let name = sname(&vm.pop());
757 with_host(|h| h.hoist_tdz(&name));
758 Value::Undef
759}
760
761/// The three global VALUE properties that are `{writable: false}` (19.1.1-19.1.3).
762/// Assigning to one is a silent no-op in sloppy code and a `TypeError` in strict
763/// code — and, either way, never rebinds the name.
764const READONLY_GLOBALS: [&str; 3] = ["undefined", "NaN", "Infinity"];
765
766fn readonly_global_error(name: &str) -> String {
767 host::type_error(&format!(
768 "Cannot assign to read only property '{name}' of object '#<Object>'"
769 ))
770}
771
772fn b_setlocal(vm: &mut VM, _: u8) -> Value {
773 let val = vm.pop();
774 let name = sname(&vm.pop());
775 // Sloppy assignment to a non-writable global is DISCARDED, not applied:
776 // `undefined = 1` used to rebind the name and make every later `undefined`
777 // read back as `1`.
778 if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
779 return val;
780 }
781 // Assigning to a binding still in its temporal dead zone throws too —
782 // `{ x = 1; let x }` is a ReferenceError, not an initialization.
783 if with_host(|h| match h.read_name(&name) {
784 Some(v) => h.is_tdz(&v),
785 None => h.is_tdz_global(&name),
786 }) {
787 return abort(vm, host::tdz_error(&name));
788 }
789 // An assignment to a `const` binding throws (8.5.2 SetMutableBinding on an
790 // immutable binding). This used to succeed silently.
791 if !with_host(|h| h.set_name(&name, val.clone())) {
792 return abort(vm, host::type_error("Assignment to constant variable."));
793 }
794 val
795}
796
797/// Strict-mode `x = v` (6.2.5.6 `PutValue` with an unresolvable reference):
798/// where sloppy code silently creates a global, strict code throws
799/// `ReferenceError: x is not defined`.
800///
801/// A separate opcode rather than a runtime flag: strictness is a static property
802/// of the code, so the compiler already knows which of the two an assignment is
803/// and sloppy code — everything in a CommonJS module without the directive —
804/// keeps the exact instruction it had.
805fn b_setlocal_strict(vm: &mut VM, _: u8) -> Value {
806 let val = vm.pop();
807 let name = sname(&vm.pop());
808 if !binding_exists(&name) {
809 return abort(vm, host::ref_error(&name));
810 }
811 if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
812 return abort(vm, readonly_global_error(&name));
813 }
814 if !with_host(|h| h.set_name(&name, val.clone())) {
815 return abort(vm, host::type_error("Assignment to constant variable."));
816 }
817 val
818}
819
820/// Whether `name` resolves to anything — a scope binding, a global, or a lazily
821/// materialised builtin namespace. `global_binding` answers the same question
822/// but ALLOCATES the namespace object to do it, which an assignment then throws
823/// away.
824fn binding_exists(name: &str) -> bool {
825 if with_host(|h| h.has_name(name)) {
826 return true;
827 }
828 matches!(
829 name,
830 "undefined" | "NaN" | "Infinity" | "globalThis" | "global"
831 ) || is_namespace(name)
832 || is_known_builtin(name)
833}
834
835fn b_declare(vm: &mut VM, _: u8) -> Value {
836 let val = vm.pop();
837 let name = sname(&vm.pop());
838 with_host(|h| h.declare_name(&name, val.clone()));
839 val
840}
841
842/// `const x = …`: like `DECLARE`, but the binding is immutable, so a later
843/// assignment to the name throws instead of overwriting it.
844fn b_declare_const(vm: &mut VM, _: u8) -> Value {
845 let val = vm.pop();
846 let name = sname(&vm.pop());
847 with_host(|h| h.declare_const_name(&name, val.clone()));
848 val
849}
850
851/// `var x = …` / a hoisted `function f(){}`: bind at function scope, skipping any
852/// open block scopes, so the name outlives the block it was written in.
853/// `var` hoisting: create the binding as `undefined` unless it already exists.
854fn b_hoist_var(vm: &mut VM, _: u8) -> Value {
855 let name = sname(&vm.pop());
856 with_host(|h| h.hoist_var_name(&name));
857 Value::Undef
858}
859
860fn b_declare_var(vm: &mut VM, _: u8) -> Value {
861 let val = vm.pop();
862 let name = sname(&vm.pop());
863 with_host(|h| h.declare_var_name(&name, val.clone()));
864 val
865}
866
867fn b_push_scope(_: &mut VM, _: u8) -> Value {
868 with_host(|h| h.push_scope());
869 Value::Undef
870}
871
872fn b_pop_scope(_: &mut VM, _: u8) -> Value {
873 with_host(|h| h.pop_scope());
874 Value::Undef
875}
876
877fn b_copy_scope(_: &mut VM, _: u8) -> Value {
878 with_host(|h| h.copy_scope());
879 Value::Undef
880}
881
882fn b_delname(vm: &mut VM, _: u8) -> Value {
883 let name = sval(&vm.pop());
884 with_host(|h| h.del_name(&name));
885 Value::Bool(true)
886}
887
888fn b_this(vm: &mut VM, _: u8) -> Value {
889 if with_host(|h| h.this_state()) == host::ThisState::Pending {
890 return abort(vm, host::this_before_super_error());
891 }
892 with_host(|h| h.current_this().unwrap_or(Value::Undef))
893}
894
895fn b_load_null(_vm: &mut VM, _: u8) -> Value {
896 with_host(|h| h.null())
897}
898
899// ── attribute / item handlers ─────────────────────────────────────────────────
900
901fn b_getattr(vm: &mut VM, _: u8) -> Value {
902 let name = sval(&vm.pop());
903 let recv = vm.pop();
904 match get_property(&recv, &name) {
905 Ok(v) => v,
906 Err(e) => abort(vm, e),
907 }
908}
909
910/// Read `recv.name` (also the computed-key path for string keys). Walks own
911/// properties, accessors, and the prototype chain (class methods / getters).
912/// Read one small piece out of `recv`'s heap cell under a short borrow.
913///
914/// The closure must not call back into the host (`with_host` is a `RefCell`
915/// borrow and re-entering panics) — which is exactly why it hands back only the
916/// value needed: the caller re-enters freely afterwards. This replaces the old
917/// `h.get(recv).cloned()` habit, which deep-copied a whole `Vec`/`IndexMap`/
918/// `String` just to look at it.
919fn peek<R>(recv: &Value, f: impl FnOnce(&JsObj) -> Option<R>) -> Option<R> {
920 with_host(|h| h.get(recv).and_then(f))
921}
922
923/// The nearest `[[Prototype]]` link of `recv` that is a Proxy, when the chain
924/// reaches it without a closer link already owning `name`.
925///
926/// A proxy prototype answers only from the position it occupies in the chain: a
927/// nearer prototype that owns the key (as a data property or an accessor) still
928/// wins, exactly as `OrdinaryGet` walks one link at a time.
929pub(crate) fn proxy_proto_link(recv: &Value, name: &str) -> Option<Value> {
930 with_host(|h| {
931 let mut cur = h.proto_of(recv);
932 for _ in 0..100 {
933 let p = cur?;
934 match h.get(&p) {
935 Some(JsObj::Proxy { .. }) => return Some(p),
936 Some(JsObj::Object(props)) if props.contains_key(name) => return None,
937 _ => {}
938 }
939 if h.own_accessor(&p, name).is_some() {
940 return None;
941 }
942 cur = h.proto_of(&p);
943 }
944 None
945 })
946}
947
948/// The CommonJS wrapper's parameters. They are function locals in Node, not
949/// global-object properties, so `globalThis.require` is `undefined` and
950/// `Object.getOwnPropertyDescriptor(globalThis, 'module')` reports no property —
951/// even though the bare `require` and `module` both work.
952const CJS_WRAPPER_LOCALS: &[&str] = &[
953 "require",
954 "module",
955 "exports",
956 "__filename",
957 "__dirname",
958 "__cjs_require",
959 "__cjs_resolve",
960];
961
962/// The globals node exposes as ENUMERABLE own properties of the global object —
963/// the timer family and the WHATWG additions, measured on v26.8.1. Everything
964/// else (`Math`, `parseInt`, the constructors) is non-enumerable.
965const ENUMERABLE_GLOBALS: &[&str] = &[
966 "global",
967 "clearImmediate",
968 "setImmediate",
969 "clearInterval",
970 "clearTimeout",
971 "setInterval",
972 "setTimeout",
973 "queueMicrotask",
974 "structuredClone",
975 "atob",
976 "btoa",
977 "performance",
978 "fetch",
979 "crypto",
980 "navigator",
981 "sessionStorage",
982];
983
984pub fn get_property(recv: &Value, name: &str) -> Result<Value, String> {
985 // A `#`-prefixed key is a PRIVATE name. `[[PrivateGet]]` (7.3.31) throws
986 // when the receiver carries no such private element — it does NOT read back
987 // as `undefined`, which is what `C.prototype.method.call({})` used to do.
988 if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
989 return Err(private_brand_message(name, false));
990 }
991 get_property_recv(recv, name, recv)
992}
993
994/// The `TypeError` a failed private brand check raises. Node words it two ways:
995/// a private METHOD or accessor names the class the receiver should have been an
996/// instance of, while a private FIELD names the member.
997pub fn private_brand_message(name: &str, writing: bool) -> String {
998 if with_host(|h| h.is_private_method(name)) {
999 if let Some(class) = with_host(|h| h.current_home_class_name()) {
1000 return host::type_error(&format!("Receiver must be an instance of class {class}"));
1001 }
1002 }
1003 let verb = if writing { "write" } else { "read" };
1004 let prep = if writing { "to" } else { "from" };
1005 host::type_error(&format!(
1006 "Cannot {verb} private member {name} {prep} an object whose class did not declare it"
1007 ))
1008}
1009
1010/// `[[Get]](name, receiver)` — 10.1.8. `receiver` is the object the read STARTED
1011/// from and is what a getter sees as `this`; it differs from `recv` only when the
1012/// read was forwarded down a prototype chain, which is why `Reflect.get(t, k, r)`
1013/// and a Proxy `get` trap's third argument both need it. Every ordinary read
1014/// passes `recv` itself.
1015/// Re-format an error's `.stack` header on its first read, the way V8 does.
1016///
1017/// The constructor could only stamp the name it was called with, so a subclass
1018/// that sets `this.name` after `super()` — or any `e.name = …` / `e.message = …`
1019/// before the first read — left a stale header. Node re-reads both properties at
1020/// format time, including one inherited from the prototype (`E.prototype.name`).
1021///
1022/// It is formatted ONCE: node caches the string, so renaming AFTER a read does
1023/// not change what later reads return. `@@stackRaw` is the not-yet-formatted
1024/// marker and is dropped here; an explicit `e.stack = …` drops it too, so an
1025/// assignment is never clobbered by a later read.
1026/// The key of node's DEFAULT `Error.prepareStackTrace`. Recognised by name so
1027/// the ordinary stack path can skip the hook round-trip when nothing custom is
1028/// installed.
1029pub const DEFAULT_PREPARE: &str = "ErrorPrepareStackTrace";
1030
1031pub fn materialize_stack(recv: &Value) {
1032 let Some(frames) = with_host(|h| match h.get(recv) {
1033 Some(JsObj::Object(p)) => p.get("@@stackRaw").cloned(),
1034 _ => None,
1035 }) else {
1036 return;
1037 };
1038 // A custom `Error.prepareStackTrace` replaces the string entirely (V8's
1039 // stack-introspection hook, which every source-map library installs). It was
1040 // honoured only by `Error.captureStackTrace`, so an ordinary `err.stack`
1041 // read bypassed it and handed back the default text.
1042 let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
1043 if let Some(f) = prep.filter(|f| {
1044 // The default hook produces exactly what the fast path below produces,
1045 // so it is skipped rather than called.
1046 !matches!(
1047 with_host(|h| h.get(f).cloned()),
1048 Some(JsObj::Builtin(ref n)) if n == DEFAULT_PREPARE
1049 ) && matches!(
1050 with_host(|h| h.get(f).cloned()),
1051 Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
1052 )
1053 }) {
1054 // Clear the raw marker FIRST: the hook may read `.stack` itself, and a
1055 // second materialization would re-enter this path forever.
1056 with_host(|h| {
1057 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1058 p.shift_remove("@@stackRaw");
1059 }
1060 });
1061 let limit = with_host(|h| h.stack_trace_limit());
1062 if let Ok(sites) = crate::module::callsite_stack(limit) {
1063 if let Ok(out) = host::invoke(&f, vec![recv.clone(), sites], None) {
1064 with_host(|h| {
1065 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1066 p.insert("stack".into(), out);
1067 }
1068 });
1069 return;
1070 }
1071 }
1072 }
1073 with_host(|h| {
1074 let frames = h.str_of(&frames);
1075 let name = host::lookup_chain(h, recv, "name")
1076 .map(|v| h.str_of(&v))
1077 .unwrap_or_else(|| "Error".to_string());
1078 let message = host::lookup_chain(h, recv, "message")
1079 .map(|v| h.str_of(&v))
1080 .unwrap_or_default();
1081 let header = if message.is_empty() {
1082 name
1083 } else {
1084 format!("{name}: {message}")
1085 };
1086 let sv = h.new_str(format!("{header}{frames}"));
1087 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1088 p.insert("stack".into(), sv);
1089 p.shift_remove("@@stackRaw");
1090 }
1091 });
1092}
1093
1094pub fn get_property_recv(recv: &Value, name: &str, receiver: &Value) -> Result<Value, String> {
1095 // `[[Get]]` on a Proxy: the handler's `get` trap, or a forward to the
1096 // target. Checked before anything else so no ordinary-object shortcut can
1097 // read past the handler.
1098 if let Some(v) = crate::proxy::get(recv, name, receiver)? {
1099 return Ok(v);
1100 }
1101 if with_host(|h| h.is_nullish(recv)) {
1102 return Err(host::type_error(&format!(
1103 "Cannot read properties of {} (reading '{name}')",
1104 with_host(|h| h.str_of(recv))
1105 )));
1106 }
1107 if name == "stack" {
1108 materialize_stack(recv);
1109 }
1110 // A `DOMException`'s `name`/`message`/`code` are prototype accessors over
1111 // internal slots, so they resolve here rather than out of a property map.
1112 if let Some(v) = dom_exception_slot(recv, name) {
1113 return Ok(v);
1114 }
1115 // A read off `globalThis` for a name the object does not own falls back to
1116 // the same lazy global binding the bare identifier gets. Without it the
1117 // global object was an empty bag: `globalThis.process`, `.console`, `.Math`
1118 // and `.JSON` were all `undefined`, so `process === globalThis.process` was
1119 // `false` and any `globalThis.X` feature probe reported the feature missing.
1120 if with_host(|h| h.is_global_object(recv)) {
1121 let own = with_host(|h| match h.get(recv) {
1122 Some(JsObj::Object(p)) => p.contains_key(name),
1123 _ => false,
1124 });
1125 // The CommonJS wrapper's parameters are function locals in Node, not
1126 // global-object properties: `typeof globalThis.require` is `undefined`
1127 // there even though the bare `require` works.
1128 if !own && !CJS_WRAPPER_LOCALS.contains(&name) {
1129 if let Some(v) = global_object_binding(name) {
1130 return Ok(v);
1131 }
1132 }
1133 }
1134 // Accessor (own or inherited getter) takes precedence over the chain walk.
1135 // The getter runs with the RECEIVER as `this`, not the object that owns it.
1136 if let Some((getter, _)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
1137 return match getter {
1138 Some(g) => host::invoke(&g, Vec::new(), Some(receiver.clone())),
1139 None => Ok(Value::Undef), // set-only property reads as undefined
1140 };
1141 }
1142 // `Symbol.toStringTag` read as an ordinary property. The builtins that carry
1143 // one expose it to a plain read, not just to `Object.prototype.toString` —
1144 // `new Uint8Array(1)[Symbol.toStringTag]` is `'Uint8Array'`, and a `Buffer`
1145 // inherits `'Uint8Array'` from the typed-array prototype it now really has.
1146 // Anything the receiver's own chain provides wins (a class may define its
1147 // own getter), so this is only the fallback.
1148 if name == "@@toStringTag" && with_host(|h| host::lookup_chain(h, recv, name)).is_none() {
1149 if let Some(tag) = with_host(|h| well_known_tag(h, recv)) {
1150 return Ok(with_host(|h| h.new_str(tag)));
1151 }
1152 }
1153 // `constructor`: a user class/function sets it on the prototype chain, and
1154 // that wins; otherwise every builtin instance reports its native
1155 // constructor (so `[].constructor`, `new Map().constructor`,
1156 // `Promise.resolve(1).constructor`, `(5).constructor` match Node).
1157 if name == "constructor" {
1158 if let Some(v) = with_host(|h| {
1159 match h.get(recv) {
1160 Some(JsObj::Object(p)) => p.get("constructor").cloned(),
1161 _ => None,
1162 }
1163 .or_else(|| host::lookup_chain(h, recv, "constructor"))
1164 }) {
1165 return Ok(v);
1166 }
1167 // An intrinsic prototype the receiver's CHAIN reaches owns a
1168 // `constructor` too, and it wins over the receiver's own kind:
1169 // `Object.create(Map.prototype).constructor` is `Map`, not `Object`.
1170 // Deciding from the kind alone also mis-named the receiver in every
1171 // message that renders one — the brand-check errors say `#<Map>`.
1172 if let Some(c) = chain_intrinsic_ctors(recv)
1173 .into_iter()
1174 .find(|c| is_builtin_ctor(c))
1175 {
1176 return Ok(with_host(|h| h.alloc(JsObj::Builtin(c.to_string()))));
1177 }
1178 if let Some(cn) = with_host(|h| default_ctor_name(h, recv)) {
1179 return Ok(with_host(|h| h.alloc(JsObj::Builtin(cn.to_string()))));
1180 }
1181 }
1182 // `__proto__` (Annex B B.2.2.1) is an accessor on `Object.prototype`, so it
1183 // answers for EVERY object that inherits from it, not only plain ones —
1184 // `[].__proto__` is `Array.prototype`. Only the plain-object arm handled it,
1185 // so an array, function or builtin instance read `undefined`. An object with
1186 // a null prototype inherits no such accessor and reads `undefined`, which is
1187 // why this is skipped there rather than answering `null`.
1188 if name == "__proto__"
1189 && !with_host(|h| h.has_null_proto(recv))
1190 && peek(recv, |o| match o {
1191 JsObj::Object(p) => Some(p.contains_key("__proto__")),
1192 _ => Some(false),
1193 }) != Some(true)
1194 {
1195 return Ok(prototype_of(recv));
1196 }
1197 // An ACCESSOR member read off the intrinsic prototype ITSELF is not a
1198 // method: it RUNS the getter with that prototype as `this`, and all but two
1199 // of `RegExp.prototype`'s then fail their brand check and throw. Every one
1200 // answered `undefined`, so both the value and the failure were invisible.
1201 // Both representations of a prototype reach here — the namespace handles
1202 // and the real objects (`Symbol.prototype`, `String.prototype`).
1203 if let Some(ctor) = intrinsic_proto_of(recv) {
1204 if is_proto_accessor(&ctor, name) {
1205 return proto_getter_call(&ctor, name, recv);
1206 }
1207 }
1208 let kind = with_host(|h| h.kind_of(recv));
1209 #[allow(unused_mut)]
1210 let mut out = match kind {
1211 Some(ObjKind::Object) => {
1212 let numeric = !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit());
1213 // A view over a DETACHED buffer reports zero extent. Its own
1214 // `length`/`byteLength`/`byteOffset` properties still hold the old
1215 // numbers — the buffer does not know its views, so it cannot rewrite
1216 // them — and reading them straight back made a detached view still
1217 // look eight bytes long.
1218 if matches!(name, "length" | "byteLength" | "byteOffset")
1219 && crate::stdlib::typedarray::view_detached(recv)
1220 {
1221 match crate::stdlib::native_tag(recv).as_deref() {
1222 Some("TypedArray") => return Ok(Value::Float(0.0)),
1223 // A DataView THROWS where a typed array answers zero — its
1224 // extent accessors are brand-checked and node reports the
1225 // getter by name.
1226 Some("DataView") => {
1227 return Err(crate::stdlib::typedarray::detached_error(
1228 "get DataView.prototype",
1229 name,
1230 false,
1231 ))
1232 }
1233 _ => {}
1234 }
1235 }
1236 // Typed-array element read (`ta[i]`): elements live in a hidden
1237 // `@@elems`, not as own numeric props, so intercept integer keys.
1238 if numeric && crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray") {
1239 if let Some(v) = crate::stdlib::typedarray::elem_get(recv, name) {
1240 return Ok(v);
1241 }
1242 }
1243 // `buf[i]`: a Buffer's bytes live in a hidden `@@bytes` array, not as
1244 // own numeric props, so integer keys read through to it.
1245 if numeric
1246 && peek(recv, |o| match o {
1247 JsObj::Object(p) => Some(p.contains_key("@@bytes")),
1248 _ => None,
1249 })
1250 .unwrap_or(false)
1251 {
1252 return Ok(crate::stdlib::buffer::byte_get(recv, name));
1253 }
1254 if let Some(v) = peek(recv, |o| match o {
1255 JsObj::Object(p) => p.get(name).cloned(),
1256 _ => None,
1257 }) {
1258 v
1259 } else if let Some(link) = proxy_proto_link(recv, name) {
1260 // A Proxy sitting in the prototype chain. `OrdinaryGet` (10.1.8.1
1261 // step 4) forwards to the parent's `[[Get]]` with the ORIGINAL
1262 // receiver, so the trap sees the child as `receiver` and `this`
1263 // inside a trap-served getter resolves to the child, not the
1264 // proxy. `lookup_chain` cannot do this: it reads property maps,
1265 // and a proxy has none.
1266 return Ok(crate::proxy::get(&link, name, recv)?.expect("link is a proxy"));
1267 } else if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1268 // A method / data property inherited from the prototype chain.
1269 v
1270 } else if crate::stdlib::native_tag(recv)
1271 .map(|tag| crate::stdlib::instance_has_method(&tag, name))
1272 .unwrap_or(false)
1273 {
1274 // A native instance method read as a property (`server.listen`) →
1275 // a bound method, dispatched via `instance_call` when invoked.
1276 bound_method(recv, name)
1277 } else if is_object_method(name) && !with_host(|h| h.has_null_proto(recv)) {
1278 // `Object.create(null)` inherits nothing, so `toString`/`valueOf`
1279 // read as `undefined` there — which is also what makes
1280 // `Object.create(null) + 1` the spec `TypeError` instead of a
1281 // silent `"[object Object]1"`.
1282 bound_method(recv, name)
1283 } else {
1284 Value::Undef
1285 }
1286 }
1287 Some(ObjKind::Class) | Some(ObjKind::Func) | Some(ObjKind::BoundFunc) => {
1288 function_property(recv, name)
1289 }
1290 // A method READ off an instance (`[].slice`, `new Map().get`) is a bound
1291 // thunk here. It is a function value, so it answers the function
1292 // properties: `[].slice.name` was `undefined` where node reports
1293 // `slice`, and `String([].slice)` fell through to
1294 // `Object.prototype.toString`.
1295 Some(ObjKind::BoundMethod) => bound_method_property(recv, name),
1296 Some(ObjKind::Symbol) => match name {
1297 "description" => {
1298 match peek(recv, |o| match o {
1299 JsObj::Symbol { desc, .. } => desc.clone(),
1300 _ => None,
1301 }) {
1302 Some(d) => with_host(|h| h.new_str(d)),
1303 None => Value::Undef,
1304 }
1305 }
1306 "toString" => bound_method(recv, name),
1307 // Anything else a symbol answers, it inherits from
1308 // `Symbol.prototype`. The arm used to stop at `undefined`, so
1309 // `Symbol('x')[Symbol.toPrimitive]` and `Symbol('x').valueOf` read
1310 // as absent even though the prototype defines both — a symbol is an
1311 // ordinary object for the purpose of a property LOOKUP, only its
1312 // methods are branded.
1313 _ => with_host(|h| {
1314 h.ensure_wrapper_protos();
1315 h.native_proto("Symbol")
1316 })
1317 .and_then(|p| with_host(|h| host::lookup_chain(h, &p, name)))
1318 .unwrap_or(Value::Undef),
1319 },
1320 Some(ObjKind::BigInt) => {
1321 if matches!(
1322 name,
1323 "toString" | "valueOf" | "toLocaleString" | "constructor"
1324 ) {
1325 bound_method(recv, name)
1326 } else {
1327 Value::Undef
1328 }
1329 }
1330 Some(ObjKind::RegExp) => {
1331 // A RegExp holds no collection, so cloning the compiled pattern here
1332 // does not scale with any input size; `regexp_property` re-enters the
1333 // host to allocate `source`/`flags`, so it cannot run under a borrow.
1334 let r = peek(recv, |o| match o {
1335 JsObj::RegExp(r) => Some(r.clone()),
1336 _ => None,
1337 });
1338 match r {
1339 Some(r) => crate::regexp::regexp_property(&r, name).unwrap_or_else(|| {
1340 // An OWN property beats the prototype method of the same
1341 // name, which is ordinary resolution order. It mattered once
1342 // the symbol-keyed methods existed: `re[Symbol.match] =
1343 // false` disowns the regexp label (7.2.8), and the method
1344 // was shadowing the assignment so the value never took.
1345 if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1346 return v;
1347 }
1348 if crate::regexp::is_regexp_method(name) {
1349 bound_method(recv, name)
1350 } else {
1351 Value::Undef
1352 }
1353 }),
1354 None => Value::Undef,
1355 }
1356 }
1357 // A WeakMap/WeakSet has NO `size` (its contents are not observable), so
1358 // the read must be `undefined` rather than a live count.
1359 Some(ObjKind::Map) => {
1360 let (len, weak) = peek(recv, |o| match o {
1361 JsObj::Map { entries, weak } => Some((entries.len(), *weak)),
1362 _ => None,
1363 })
1364 .unwrap_or((0, false));
1365 match name {
1366 "size" if !weak => Value::Float(len as f64),
1367 "@@iterator" => bound_method(recv, name),
1368 _ if is_map_method(name) => bound_method(recv, name),
1369 _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1370 }
1371 }
1372 Some(ObjKind::Set) => {
1373 let (len, weak) = peek(recv, |o| match o {
1374 JsObj::Set { entries, weak } => Some((entries.len(), *weak)),
1375 _ => None,
1376 })
1377 .unwrap_or((0, false));
1378 match name {
1379 "size" if !weak => Value::Float(len as f64),
1380 "@@iterator" => bound_method(recv, name),
1381 _ if is_set_method(name) => bound_method(recv, name),
1382 _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1383 }
1384 }
1385 Some(ObjKind::Generator) => {
1386 // A generator IS its own iterator, so it answers for the matching
1387 // symbol — `@@asyncIterator` for an async one, `@@iterator` for a
1388 // sync one. Neither was advertised, so `ag()[Symbol.asyncIterator]`
1389 // was `undefined` even though `for await` over it worked through a
1390 // different path.
1391 let want = if with_host(|h| h.is_async_gen_val(recv)) {
1392 "@@asyncIterator"
1393 } else {
1394 "@@iterator"
1395 };
1396 if name == want || is_generator_method(name) || crate::stdlib::iterator::is_helper(name)
1397 {
1398 bound_method(recv, name)
1399 } else {
1400 with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1401 }
1402 }
1403 Some(ObjKind::Promise) => {
1404 if matches!(name, "then" | "catch" | "finally") {
1405 bound_method(recv, name)
1406 } else {
1407 with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1408 }
1409 }
1410 Some(ObjKind::Iter) => {
1411 if matches!(name, "next" | "return" | "@@iterator")
1412 || crate::stdlib::iterator::is_helper(name)
1413 {
1414 bound_method(recv, name)
1415 } else {
1416 with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1417 }
1418 }
1419 Some(ObjKind::Array) => {
1420 if name == "length" {
1421 let n = peek(recv, |o| match o {
1422 JsObj::Array(items) => Some(items.len()),
1423 _ => None,
1424 })
1425 .unwrap_or(0);
1426 Value::Float(n as f64)
1427 } else if let Ok(i) = name.parse::<usize>() {
1428 peek(recv, |o| match o {
1429 JsObj::Array(items) => items.get(i).cloned(),
1430 _ => None,
1431 })
1432 // An index PAST an `arguments` object's length is an ordinary
1433 // own property in the side table, since adding one must not
1434 // move `length`. The array read alone could not see it, so the
1435 // write was invisible to every later read.
1436 .or_else(|| with_host(|h| h.fn_prop(recv, name)))
1437 .unwrap_or(Value::Undef)
1438 } else if name == "@@iterator"
1439 || is_object_method(name)
1440 // An `arguments` object is array-BACKED here but is not an
1441 // Array: node's exposes no `Array.prototype` method, which is
1442 // exactly why the idiom is `Array.prototype.slice.call(args)`.
1443 // Exposing them made `arguments.map` a function.
1444 || (is_array_method(name) && !is_arguments(recv))
1445 {
1446 bound_method(recv, name)
1447 } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1448 // Extra own props attached to an array (e.g. `RegExp.exec` result's
1449 // `.index`/`.input`/`.groups`).
1450 v
1451 } else {
1452 Value::Undef
1453 }
1454 }
1455 Some(ObjKind::Str) => {
1456 // `.length` and `s[i]` count UTF-16 code units, not code points.
1457 if name == "length" {
1458 let n = peek(recv, |o| match o {
1459 JsObj::Str(s) => Some(crate::utf16::len(s)),
1460 _ => None,
1461 })
1462 .unwrap_or(0);
1463 Value::Float(n as f64)
1464 } else if let Ok(i) = name.parse::<usize>() {
1465 match peek(recv, |o| match o {
1466 JsObj::Str(s) => crate::utf16::Units::of(s).unit_str(i),
1467 _ => None,
1468 }) {
1469 Some(c) => with_host(|h| h.new_str(c)),
1470 None => Value::Undef,
1471 }
1472 } else if name == "@@iterator" || is_string_method(name) {
1473 bound_method(recv, name)
1474 } else {
1475 Value::Undef
1476 }
1477 }
1478 Some(ObjKind::Builtin) => {
1479 let ns = peek(recv, |o| match o {
1480 JsObj::Builtin(ns) => Some(ns.clone()),
1481 _ => None,
1482 })
1483 .unwrap_or_default();
1484 let v = namespace_property(&ns, name);
1485 // `Function.prototype`'s methods READ off a builtin function. The
1486 // CALL forms (`Math.max.call(null, 1, 2)`) already dispatched, but
1487 // the read answered `undefined` — so `typeof Math.max.bind` was
1488 // `"undefined"`, and `String(Math.max)` found no `toString` to
1489 // invoke and fell back to `Object.prototype.toString`'s
1490 // `[object Function]` where node reports the native-code form.
1491 if matches!(v, Value::Undef)
1492 && is_function_method(name)
1493 && host::builtin_is_callable(&ns)
1494 {
1495 return Ok(bound_method(recv, name));
1496 }
1497 v
1498 }
1499 _ => {
1500 // Primitive numbers/booleans: method access -> bound method.
1501 if matches!(recv, Value::Float(_) | Value::Int(_)) && is_number_method(name) {
1502 bound_method(recv, name)
1503 } else {
1504 Value::Undef
1505 }
1506 }
1507 };
1508 // Every object INHERITS the `Object.prototype` methods, and each kind's
1509 // read arm above knows only its OWN. So `typeof new Map().toString`,
1510 // `typeof f.hasOwnProperty` and `typeof /a/.propertyIsEnumerable` all
1511 // answered `undefined` — for Map the CALL already worked, which is the
1512 // read and the dispatch disagreeing about the same method.
1513 //
1514 // Which prototype owns the name is decided by the same helper the `in`
1515 // operator uses, so the two cannot drift, and the result is the SHARED
1516 // intrinsic rather than a per-read thunk.
1517 // `arguments.callee` (and `.caller`) is a POISON PILL in strict code — the
1518 // accessor throws rather than answering, which is how a strict function
1519 // keeps its caller unreachable. It read back as `undefined` here, which a
1520 // feature probe reads as "not supported" rather than "forbidden".
1521 // Measured: on an ARGUMENTS object only `callee` is poisoned (`caller` is
1522 // simply absent and reads `undefined`); on a strict FUNCTION both `caller`
1523 // and `arguments` are.
1524 if name == "callee" && is_arguments(recv) && with_host(|h| h.current_strict()) {
1525 return Err(host::type_error(POISON_PILL));
1526 }
1527 if matches!(name, "caller" | "arguments")
1528 && matches!(
1529 with_host(|h| h.kind_of(recv)),
1530 Some(ObjKind::Func) | Some(ObjKind::Class)
1531 )
1532 {
1533 return poison_pill_read(recv);
1534 }
1535 // `arguments.callee` in SLOPPY code is the running function — the
1536 // pre-`class` self-reference idiom. It read back `undefined`.
1537 if name == "callee" && is_arguments(recv) {
1538 if let Some(f) = with_host(|h| h.fn_prop(recv, "@@callee")) {
1539 return Ok(f);
1540 }
1541 }
1542 // A method SYNTHESIZED from the receiver's kind is only reachable while the
1543 // receiver's intrinsic prototype is still on its chain. `Object
1544 // .setPrototypeOf(a, {})` must make `a.join` `undefined`; the kind arm
1545 // above answers from the kind alone and cannot know the link changed. Only
1546 // a synthesized value is dropped — the two shapes a method read produces —
1547 // and only when the receiver does not own the name itself.
1548 if matches!(
1549 with_host(|h| h.get(&out).cloned()),
1550 Some(JsObj::BoundMethod { .. })
1551 ) || matches!(
1552 with_host(|h| h.get(&out).cloned()),
1553 Some(JsObj::Builtin(ns)) if ns.starts_with("@proto:")
1554 ) {
1555 // The kind arms synthesize their OWN kind's methods, so that is the
1556 // prototype whose reachability decides. Clearing the value here lets
1557 // the `inherited_method_owner` fallback below re-supply the
1558 // `Object.prototype` form where one exists — which is why
1559 // `a.toString` stays a function after the link is replaced while
1560 // `a.join` does not.
1561 if !own_intrinsic_reachable(recv) && !has_own_for_shadow(recv, name) {
1562 out = Value::Undef;
1563 }
1564 }
1565 // A key the receiver does not OWN is looked up on its prototype chain. The
1566 // exotic arms above answer from their own storage and stop, so an array
1567 // given a prototype inherited nothing through a read: with
1568 // `Object.setPrototypeOf(a, {1: 'q'})`, `a[1]` was `undefined` at an elided
1569 // index and at one past the end, while `1 in a` already answered true —
1570 // the two views of the same question disagreeing. An accessor was found
1571 // (`lookup_accessor` walks), so only DATA properties went missing.
1572 //
1573 // A plain object's arm already consults the chain, and an array with no
1574 // explicit prototype has no links to walk, so this changes neither.
1575 if !name.starts_with('#') && !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
1576 if matches!(out, Value::Undef) {
1577 if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1578 return Ok(v);
1579 }
1580 }
1581 // Then a monkey-patched intrinsic prototype member, which shadows the
1582 // synthesized one: after `Array.prototype.join = f`, `[1, 2].join` must
1583 // BE `f`. An explicitly-set prototype above wins over it, as the chain
1584 // order requires.
1585 if let Some(v) = inherited_builtin_static(recv, name) {
1586 return Ok(v);
1587 }
1588 }
1589 if matches!(out, Value::Undef) && !name.starts_with('#') {
1590 if let Some(owner) = inherited_method_owner(recv, name) {
1591 // An INHERITED accessor runs, it does not hand back a thunk, and
1592 // its brand check is about the receiver's internal slot rather than
1593 // its chain — `Object.create(Map.prototype).size` throws in node
1594 // even though `Map.prototype` is right there above it. This
1595 // answered `undefined`, which is the value a real Map would never
1596 // give and a plain object should never reach.
1597 if is_proto_accessor(owner, name) && !getter_in_flight(owner, name) {
1598 return proto_getter_call(owner, name, recv);
1599 }
1600 let key = format!("@proto:{owner}:{name}");
1601 if builtin_meta(&key).is_some() {
1602 return Ok(with_host(|h| h.alloc(JsObj::Builtin(key))));
1603 }
1604 // A DATA member of the prototype — `Array.prototype[Symbol
1605 // .unscopables]` is an object, not a method, so it is in neither
1606 // function table. Read it off the prototype itself rather than
1607 // answering `undefined`: an instance inherits it.
1608 let v = namespace_property(&format!("{owner}.prototype"), name);
1609 if !matches!(v, Value::Undef) {
1610 return Ok(v);
1611 }
1612 }
1613 }
1614 Ok(out)
1615}
1616
1617/// The namespace name of the `require.cache` view. A `Builtin` rather than an
1618/// object literal because the module cache is the single source of truth: a
1619/// populated copy would answer reads correctly and silently ignore a `delete`,
1620/// which is the operation the property exists for.
1621pub const REQUIRE_CACHE: &str = "__cjs_cache";
1622
1623/// The builtin constructor name for a value with no own/inherited `constructor`
1624/// property, so `x.constructor` (and thus `x.constructor.name`) matches Node for
1625/// arrays, plain objects, Map/Set, promises, iterators, functions, and boxed
1626/// primitives. `None` ⇒ leave `.constructor` as `undefined` (e.g. generators,
1627/// whose `.constructor.name` is `""` in Node — not worth modelling).
1628fn default_ctor_name(h: &host::JsHost, recv: &Value) -> Option<&'static str> {
1629 match h.get(recv) {
1630 Some(JsObj::Array(_)) => Some("Array"),
1631 Some(JsObj::Object(props)) => {
1632 // A native instance reports its own constructor, not Object — e.g.
1633 // `qs` does `buf.constructor.isBuffer(buf)`, so a Buffer's
1634 // `.constructor` must be `Buffer` (which carries `isBuffer`). Read
1635 // the `@@native` tag off the already-borrowed host (calling
1636 // `native_tag`, which re-enters `with_host`, would double-borrow).
1637 match props.get("@@native").map(|t| h.str_of(t)).as_deref() {
1638 Some("Buffer") => Some("Buffer"),
1639 Some("URL") => Some("URL"),
1640 Some("Date") => Some("Date"),
1641 Some("WeakRef") => Some("WeakRef"),
1642 Some("FinalizationRegistry") => Some("FinalizationRegistry"),
1643 Some("TextEncoder") => Some("TextEncoder"),
1644 Some("TextDecoder") => Some("TextDecoder"),
1645 Some("EventEmitter") => Some("EventEmitter"),
1646 Some("Timeout") => Some("Timeout"),
1647 Some("Immediate") => Some("Immediate"),
1648 _ => Some("Object"),
1649 }
1650 }
1651 Some(JsObj::Map { weak, .. }) => Some(if *weak { "WeakMap" } else { "Map" }),
1652 Some(JsObj::Set { weak, .. }) => Some(if *weak { "WeakSet" } else { "Set" }),
1653 Some(JsObj::Promise { .. }) => Some("Promise"),
1654 Some(JsObj::Str(_)) => Some("String"),
1655 Some(JsObj::Symbol { .. }) => Some("Symbol"),
1656 Some(JsObj::BigInt(_)) => Some("BigInt"),
1657 Some(JsObj::RegExp(_)) => Some("RegExp"),
1658 Some(JsObj::Iter { .. }) => Some("Iterator"),
1659 Some(JsObj::Func(f)) => {
1660 // A generator or async function is NOT an ordinary function: its
1661 // `[[Prototype]]` is `GeneratorFunction.prototype` (or the async
1662 // variants'), and so is its `constructor`. All three reported plain
1663 // `Function`, so `g.constructor.name` was `Function` where node
1664 // says `GeneratorFunction`.
1665 Some(match h.funcs.get(f.def_id) {
1666 Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction",
1667 Some(d) if d.is_generator => "GeneratorFunction",
1668 Some(d) if d.is_async => "AsyncFunction",
1669 _ => "Function",
1670 })
1671 }
1672 Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => Some("Function"),
1673 _ => match recv {
1674 Value::Float(_) | Value::Int(_) => Some("Number"),
1675 Value::Bool(_) => Some("Boolean"),
1676 _ => None,
1677 },
1678 }
1679}
1680
1681/// The builtin constructor *functions*, so `Ctor.name` is the constructor name.
1682/// Excludes the non-callable namespaces (`Math`, `JSON`, `console`, `Reflect`,
1683/// `process`), whose `.name` is `undefined` in Node.
1684///
1685/// Most are also globals, but not all: `Timeout`/`Immediate` are unexposed in
1686/// Node (`typeof Timeout === 'undefined'`) yet still name themselves through a
1687/// handle's `.constructor.name`, so they belong here and not in `GLOBALS`.
1688/// The builtins that expose a `Symbol.species` accessor. Each returns `this`,
1689/// so a subclass is its own species unless it overrides the getter.
1690fn has_species(name: &str) -> bool {
1691 matches!(
1692 name,
1693 "Array" | "Map" | "Set" | "WeakMap" | "WeakSet" | "Promise" | "RegExp" | "ArrayBuffer"
1694 ) || crate::stdlib::typedarray::is_ctor(name)
1695}
1696
1697fn is_builtin_ctor(name: &str) -> bool {
1698 matches!(
1699 name,
1700 "Array"
1701 | "Object"
1702 | "Number"
1703 | "String"
1704 | "Boolean"
1705 | "Symbol"
1706 | "Function"
1707 | "Map"
1708 | "Set"
1709 | "WeakMap"
1710 | "WeakSet"
1711 | "Promise"
1712 | "BigInt"
1713 | "Iterator"
1714 | "RegExp"
1715 | "Date"
1716 | "ArrayBuffer"
1717 | "DataView"
1718 | "Uint8Array"
1719 | "Int8Array"
1720 | "Uint8ClampedArray"
1721 | "Int16Array"
1722 | "Uint16Array"
1723 | "Int32Array"
1724 | "Uint32Array"
1725 | "Float32Array"
1726 | "Float64Array"
1727 | "BigInt64Array"
1728 | "BigUint64Array"
1729 | "WeakRef"
1730 | "FinalizationRegistry"
1731 | "TextEncoder"
1732 | "TextDecoder"
1733 | "IncomingMessage"
1734 | "ServerResponse"
1735 | "EventEmitter"
1736 | "Buffer"
1737 | "URL"
1738 | "URLSearchParams"
1739 | "Timeout"
1740 | "Immediate"
1741 ) || host::ERROR_NAMES.contains(&name)
1742 // The stream base classes are constructors too, and `require('stream')`
1743 // IS `Stream`, so `require('stream').name` has to answer.
1744 || crate::stdlib::stream::is_class(name)
1745}
1746
1747/// The intrinsic key of the method `<instance>.<method>` resolves to, so a bound
1748/// thunk can look its `name`/`length` up in the same table a
1749/// `<Ctor>.prototype.<method>` thunk uses. `None` when the receiver has no
1750/// builtin constructor to name (a native stdlib instance, whose methods are
1751/// node's own JS and have no specified arity).
1752fn bound_method_key(recv: &Value, method: &str) -> Option<String> {
1753 let ctor = with_host(|h| default_ctor_name(h, recv))?;
1754 Some(format!("@proto:{ctor}:{method}"))
1755}
1756
1757/// `[[Get]]` on a bound method thunk. It is a function, so `name`, `length` and
1758/// the `Function.prototype` methods all answer; `length` only when the intrinsic
1759/// table knows the method, because inventing an arity is worse than the
1760/// `undefined` a caller can test for.
1761fn bound_method_property(recv: &Value, name: &str) -> Value {
1762 let method = peek(recv, |o| match o {
1763 JsObj::BoundMethod { name, .. } => Some(name.clone()),
1764 _ => None,
1765 })
1766 .unwrap_or_default();
1767 let key = peek(recv, |o| match o {
1768 JsObj::BoundMethod { recv, .. } => Some(recv.clone()),
1769 _ => None,
1770 })
1771 .and_then(|inner| bound_method_key(&inner, &method));
1772 let meta = key.as_deref().and_then(builtin_meta);
1773 match name {
1774 "name" => {
1775 let n = meta.map(|(n, _)| n.to_string()).unwrap_or(method);
1776 with_host(|h| h.new_str(n))
1777 }
1778 "length" => match meta {
1779 Some((_, len)) => Value::Float(len as f64),
1780 None => Value::Undef,
1781 },
1782 _ if is_function_method(name) => bound_method(recv, name),
1783 _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1784 }
1785}
1786
1787fn bound_method(recv: &Value, name: &str) -> Value {
1788 // An ECMAScript intrinsic is ONE function object shared by every instance:
1789 // `[1].push === Array.prototype.push` and `[1].push === [2].push` are both
1790 // true. Reading one off an instance used to mint a fresh thunk bound to that
1791 // instance, so every such comparison answered false — and a detached method
1792 // kept working on the receiver it was read off, where node throws because it
1793 // has no `this` at all.
1794 if let Some(key) = bound_method_key(recv, name) {
1795 if builtin_meta(&key).is_some() {
1796 return with_host(|h| h.alloc(JsObj::Builtin(key)));
1797 }
1798 }
1799 with_host(|h| {
1800 h.alloc(JsObj::BoundMethod {
1801 recv: recv.clone(),
1802 name: name.to_string(),
1803 })
1804 })
1805}
1806
1807/// `Object.prototype` methods reachable on any object.
1808fn is_object_method(name: &str) -> bool {
1809 matches!(
1810 name,
1811 "hasOwnProperty"
1812 | "isPrototypeOf"
1813 | "propertyIsEnumerable"
1814 | "toString"
1815 | "toLocaleString"
1816 | "valueOf"
1817 | "constructor"
1818 | "__defineGetter__"
1819 | "__defineSetter__"
1820 | "__lookupGetter__"
1821 | "__lookupSetter__"
1822 )
1823}
1824
1825/// The `Object.prototype` methods installed as thunks on the real
1826/// `Object.prototype` object, so `Object.prototype.toString.call(x)` and a class
1827/// prototype's inherited `hasOwnProperty` both resolve through the chain.
1828pub const OBJECT_PROTO_METHODS: &[&str] = &[
1829 "hasOwnProperty",
1830 "isPrototypeOf",
1831 "propertyIsEnumerable",
1832 "toString",
1833 "toLocaleString",
1834 "valueOf",
1835 "__defineGetter__",
1836 "__defineSetter__",
1837 "__lookupGetter__",
1838 "__lookupSetter__",
1839];
1840
1841/// A typed array with elements cannot be frozen or sealed: its indices are
1842/// non-configurable by construction, so making them non-writable would violate
1843/// the invariant, and node refuses outright rather than half-applying it. An
1844/// EMPTY view and a `DataView` are both fine.
1845/// `TestIntegrityLevel` (7.3.16) — `Object.isFrozen` / `Object.isSealed`.
1846///
1847/// Over a PROXY it is a sequence of traps (`isExtensible`, `ownKeys`, then a
1848/// `getOwnPropertyDescriptor` per key), not a question for the host: the proxy
1849/// OBJECT was being inspected, so a frozen proxy answered false and the handler
1850/// never saw the query.
1851fn integrity_level(v: &Value, freeze: bool) -> Result<Value, String> {
1852 if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
1853 return Ok(Value::Bool(with_host(|h| h.is_sealed(v, freeze))));
1854 }
1855 // An EXTENSIBLE object is neither sealed nor frozen, whatever its keys say.
1856 if crate::proxy::is_extensible(v)?.unwrap_or(true) {
1857 return Ok(Value::Bool(false));
1858 }
1859 for key in crate::proxy::own_keys(v)?.unwrap_or_default() {
1860 let Some(d) = crate::proxy::get_own_descriptor(v, &key)? else {
1861 continue;
1862 };
1863 let flag = |name: &str| {
1864 with_host(|h| match h.get(&d) {
1865 Some(JsObj::Object(p)) => p.get(name).map(|x| h.truthy(x)).unwrap_or(false),
1866 _ => false,
1867 })
1868 };
1869 let is_data = with_host(
1870 |h| matches!(h.get(&d), Some(JsObj::Object(p)) if !p.contains_key("get") && !p.contains_key("set")),
1871 );
1872 if flag("configurable") || (freeze && is_data && flag("writable")) {
1873 return Ok(Value::Bool(false));
1874 }
1875 }
1876 Ok(Value::Bool(true))
1877}
1878
1879/// `SetIntegrityLevel` (7.3.15) over a PROXY, which is a sequence of TRAPS —
1880/// `preventExtensions`, then `ownKeys`, then a `getOwnPropertyDescriptor` and a
1881/// `defineProperty` per key. It ran none of them: the host sealed the proxy
1882/// OBJECT, so the handler never saw the operation and the target was untouched.
1883///
1884/// Returns false for a non-proxy, which takes the ordinary path.
1885fn seal_proxy(v: &Value, freeze: bool) -> Result<bool, String> {
1886 if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
1887 return Ok(false);
1888 }
1889 if !crate::proxy::prevent_extensions(v)? {
1890 return Err(host::type_error("Object.freeze called on non-object"));
1891 }
1892 let keys = crate::proxy::own_keys(v)?.unwrap_or_default();
1893 for key in keys {
1894 // SEALING asks for no descriptor at all — it only strips
1895 // `configurable`, which is the same for a data property and an
1896 // accessor. FREEZING has to know which it is, because only a data
1897 // property has a `writable` to strip, and that is the one extra trap
1898 // call node makes.
1899 let accessor = if freeze {
1900 let Some(cur) = crate::proxy::get_own_descriptor(v, &key)? else {
1901 continue;
1902 };
1903 with_host(
1904 |h| matches!(h.get(&cur), Some(JsObj::Object(p)) if p.contains_key("get") || p.contains_key("set")),
1905 )
1906 } else {
1907 false
1908 };
1909 let desc = with_host(|h| {
1910 let mut m: IndexMap<String, Value> = IndexMap::new();
1911 m.insert("configurable".into(), Value::Bool(false));
1912 if freeze && !accessor {
1913 m.insert("writable".into(), Value::Bool(false));
1914 }
1915 h.new_object(m)
1916 });
1917 if !crate::proxy::define_property(v, &key, &desc)? {
1918 return Err(host::type_error(&format!(
1919 "'defineProperty' on proxy: trap returned falsish for property '{key}'"
1920 )));
1921 }
1922 }
1923 Ok(true)
1924}
1925
1926fn reject_sealing_a_view(v: &Value, verb: &str) -> Result<(), String> {
1927 let has_elements = matches!(
1928 crate::stdlib::native_tag(v).as_deref(),
1929 Some("TypedArray") | Some("Buffer")
1930 ) && !crate::stdlib::typedarray::elem_values(v).is_empty();
1931 if has_elements {
1932 return Err(host::type_error(&format!(
1933 "Cannot {verb} array buffer views with elements"
1934 )));
1935 }
1936 Ok(())
1937}
1938
1939pub fn is_object_builtin_method(name: &str) -> bool {
1940 matches!(
1941 name,
1942 "hasOwnProperty"
1943 | "isPrototypeOf"
1944 | "propertyIsEnumerable"
1945 | "toString"
1946 | "toLocaleString"
1947 | "valueOf"
1948 | "__defineGetter__"
1949 | "__defineSetter__"
1950 | "__lookupGetter__"
1951 | "__lookupSetter__"
1952 )
1953}
1954
1955/// The `Symbol.toStringTag` STRING on `recv`'s chain, if any — steps 16-17 of
1956/// 20.1.3.6, the hook by which a class names its own brand.
1957///
1958/// A Proxy has no chain to probe: the step is an unconditional
1959/// `Get(O, @@toStringTag)`, so its `get` trap decides. Probing first (as an
1960/// ordinary receiver does, to keep the read off objects that carry no tag)
1961/// would always miss and brand every tagged proxy `[object Object]`.
1962///
1963/// The read runs OUTSIDE the host borrow so a getter-valued tag can be invoked.
1964fn to_string_tag(recv: &Value) -> Result<Option<String>, String> {
1965 let tagged = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
1966 || with_host(|h| {
1967 host::lookup_chain(h, recv, "@@toStringTag").is_some()
1968 || host::lookup_accessor(h, recv, "@@toStringTag").is_some()
1969 });
1970 if !tagged {
1971 return Ok(None);
1972 }
1973 let t = get_property(recv, "@@toStringTag")?;
1974 Ok(with_host(|h| h.as_str(&t)))
1975}
1976
1977/// Dispatch an `Object.prototype` builtin method on an object/instance.
1978pub fn object_builtin_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
1979 match name {
1980 // Annex B B.2.2.2-B.2.2.5. Legacy, but still present in node and still
1981 // reached by pre-`defineProperty` libraries; all four were missing, so
1982 // `o.__defineGetter__` threw "is not a function".
1983 "__defineGetter__" | "__defineSetter__" => {
1984 let getter = name == "__defineGetter__";
1985 let f = args.get(1).cloned().unwrap_or(Value::Undef);
1986 if !with_host(|h| host::is_callable(h, &f)) {
1987 return Err(host::type_error(&format!(
1988 "Object.prototype.{name}: Expecting function"
1989 )));
1990 }
1991 let key = host::to_property_key(&arg0(&args))?;
1992 let desc = with_host(|h| {
1993 let mut m: IndexMap<String, Value> = IndexMap::new();
1994 m.insert(if getter { "get" } else { "set" }.into(), f);
1995 m.insert("enumerable".into(), Value::Bool(true));
1996 m.insert("configurable".into(), Value::Bool(true));
1997 h.new_object(m)
1998 });
1999 apply_descriptor(recv, &key, &desc)?;
2000 Ok(Value::Undef)
2001 }
2002 "__lookupGetter__" | "__lookupSetter__" => {
2003 let want_get = name == "__lookupGetter__";
2004 let key = host::to_property_key(&arg0(&args))?;
2005 // Walks the prototype chain, unlike `getOwnPropertyDescriptor`.
2006 let found = with_host(|h| host::lookup_accessor(h, recv, &key));
2007 Ok(match found {
2008 Some((g, st)) => {
2009 let side = if want_get { g } else { st };
2010 side.unwrap_or(Value::Undef)
2011 }
2012 None => Value::Undef,
2013 })
2014 }
2015 "hasOwnProperty" => {
2016 let k = host::to_property_key(&arg0(&args))?;
2017 // The global object OWNS its lazily-bound builtins and every global
2018 // a script created; neither lives in its property map.
2019 if with_host(|h| h.is_global_object(recv))
2020 && !CJS_WRAPPER_LOCALS.contains(&k.as_str())
2021 && global_object_binding(&k).is_some()
2022 {
2023 return Ok(Value::Bool(true));
2024 }
2025 // A builtin namespace/prototype receiver (`Map.prototype`) reports
2026 // ownership via `has_property` (its methods resolve as thunks).
2027 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin) {
2028 return Ok(Value::Bool(has_property(recv, &k)?));
2029 }
2030 // `HasOwnProperty` (7.3.12) is `[[GetOwnProperty]]`, so on a Proxy it
2031 // is the `getOwnPropertyDescriptor` trap — NOT the `has` trap and not
2032 // the target's property map.
2033 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
2034 let d = crate::proxy::get_own_descriptor(recv, &k)?.unwrap_or(Value::Undef);
2035 return Ok(Value::Bool(!matches!(d, Value::Undef)));
2036 }
2037 // A Buffer's / typed array's own keys are its element indices: the
2038 // `length`/`byteLength` slots are internal bookkeeping, and V8
2039 // reports `hasOwnProperty('length')` as false for a typed array.
2040 // Shared with the `in` operator so the two cannot drift apart.
2041 if let Some(hit) = crate::stdlib::typedarray::has_index(recv, &k) {
2042 return Ok(Value::Bool(hit));
2043 }
2044 // A function's `length`/`name`/`prototype` and a RegExp's
2045 // `lastIndex` are SYNTHESIZED own properties: they read back but
2046 // own no map entry, so this answered false where node says true.
2047 if synthesized_own_descriptor(recv, &k).is_some() {
2048 return Ok(Value::Bool(true));
2049 }
2050 if uses_side_table(recv) {
2051 return Ok(Value::Bool(with_host(|h| h.fn_prop(recv, &k).is_some())));
2052 }
2053 let has = with_host(|h| match h.get(recv) {
2054 Some(JsObj::Object(p)) => p.contains_key(&k) || h.own_accessor(recv, &k).is_some(),
2055 Some(JsObj::Array(items)) => {
2056 k == "length"
2057 || k.parse::<usize>()
2058 .map(|i| i < items.len() && !h.is_hole(recv, i))
2059 .unwrap_or(false)
2060 }
2061 _ => false,
2062 });
2063 Ok(Value::Bool(has))
2064 }
2065 "isPrototypeOf" => {
2066 let target = arg0(&args);
2067 // The ARGUMENT is what gets walked, so a proxy there needs its
2068 // `getPrototypeOf` trap for the FIRST hop: `proto_of` reads a link a
2069 // proxy does not hold, which reported `false` for every proxy. From
2070 // the second hop on the chain is ordinary objects again, walked by
2071 // the recorded link exactly as before.
2072 let mut cur = match crate::proxy::get_prototype_of(&target)? {
2073 Some(p) => Some(p).filter(|p| !with_host(|h| h.is_null(p))),
2074 None => with_host(|h| h.proto_of(&target)),
2075 };
2076 while let Some(p) = cur {
2077 if with_host(|h| h.strict_eq(&p, recv)) {
2078 return Ok(Value::Bool(true));
2079 }
2080 cur = with_host(|h| h.proto_of(&p));
2081 }
2082 Ok(Value::Bool(false))
2083 }
2084 "propertyIsEnumerable" => {
2085 let k = with_host(|h| h.str_of(&arg0(&args)));
2086 // Own *and* enumerable — a non-enumerable own slot reads false. On a
2087 // Proxy that question is `[[GetOwnProperty]]`, i.e. the descriptor
2088 // trap, since there is no property map to enumerate.
2089 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
2090 let has = crate::proxy::own_enum_string_keys(recv)?.contains(&k);
2091 return Ok(Value::Bool(has));
2092 }
2093 let has = with_host(|h| h.own_enum_key_names(recv).contains(&k));
2094 Ok(Value::Bool(has))
2095 }
2096 "toString" => {
2097 // An instance with a custom `toString` up the chain is handled by
2098 // call_method before reaching here; this is the default — and the
2099 // default consults `Symbol.toStringTag` (20.1.3.6 steps 16-17).
2100 // Only the EXPLICIT `Object.prototype.toString.call(o)` did, so a
2101 // tagged object branded itself `[object T]` when asked one way and
2102 // `[object Object]` when converted the other (`String(o)`, `${o}`,
2103 // `o + ''`, `o.toString()`), which is the path ordinary code takes.
2104 if let Some(t) = to_string_tag(recv)? {
2105 return Ok(with_host(|h| h.new_str(format!("[object {t}]"))));
2106 }
2107 Ok(with_host(|h| {
2108 let s = h.str_of(recv);
2109 h.new_str(s)
2110 }))
2111 }
2112 // `Object.prototype.toLocaleString` (20.1.3.5) is defined as
2113 // `Invoke(this, "toString")` — no locale behavior of its own. It was
2114 // installed as a thunk on `Object.prototype` but had no dispatch arm, so
2115 // calling it threw `is not a function` on every plain object.
2116 "toLocaleString" => {
2117 let v = host::call_method(recv, "toString", Vec::new())?;
2118 Ok(v)
2119 }
2120 "valueOf" => Ok(recv.clone()),
2121 _ => Err(host::type_error(&format!("{name} is not a function"))),
2122 }
2123}
2124
2125/// `Function.prototype` methods (`call`/`apply`/`bind`) plus `Symbol.prototype`/
2126/// generator handling done elsewhere. Returns `Ok(None)` if `name` is not one of
2127/// these (so the caller can try statics).
2128pub fn function_builtin_method(
2129 recv: &Value,
2130 name: &str,
2131 args: &[Value],
2132) -> Result<Option<Value>, String> {
2133 match name {
2134 "call" => {
2135 let this = args.first().cloned();
2136 let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2137 Ok(Some(host::invoke(recv, rest, this)?))
2138 }
2139 "apply" => {
2140 let this = args.first().cloned();
2141 let arr = args.get(1).cloned().unwrap_or(Value::Undef);
2142 // `Function.prototype.apply` takes an ARRAY-LIKE, not an iterable
2143 // (10.2.4.3 → CreateListFromArrayLike): `f.apply(null, arguments)`
2144 // and `f.apply(null, {length: 2, 0: 'x', 1: 'y'})` are the shapes
2145 // this is written for, and both produced an empty list. A nullish
2146 // second argument means no arguments at all.
2147 let call_args = if matches!(arr, Value::Undef) || with_host(|h| h.is_null(&arr)) {
2148 Vec::new()
2149 } else {
2150 create_list_from_array_like(&arr)?
2151 };
2152 Ok(Some(host::invoke(recv, call_args, this)?))
2153 }
2154 "bind" => {
2155 let this = args.first().cloned().unwrap_or(Value::Undef);
2156 let pre = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2157 Ok(Some(with_host(|h| {
2158 h.alloc(JsObj::BoundFunc {
2159 target: recv.clone(),
2160 this,
2161 args: pre,
2162 })
2163 })))
2164 }
2165 "toString" => Ok(Some(with_host(|h| {
2166 let s = h.str_of(recv);
2167 h.new_str(s)
2168 }))),
2169 _ => Ok(None),
2170 }
2171}
2172
2173fn is_function_method(name: &str) -> bool {
2174 matches!(name, "call" | "apply" | "bind" | "toString")
2175}
2176fn is_map_method(name: &str) -> bool {
2177 matches!(
2178 name,
2179 "get" | "set" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
2180 )
2181}
2182fn is_set_method(name: &str) -> bool {
2183 matches!(
2184 name,
2185 "add"
2186 | "has"
2187 | "delete"
2188 | "clear"
2189 | "forEach"
2190 | "keys"
2191 | "values"
2192 | "entries"
2193 | "union"
2194 | "intersection"
2195 | "difference"
2196 | "symmetricDifference"
2197 | "isSubsetOf"
2198 | "isSupersetOf"
2199 | "isDisjointFrom"
2200 )
2201}
2202fn is_generator_method(name: &str) -> bool {
2203 matches!(name, "next" | "return" | "throw")
2204}
2205
2206/// A property read on a function/class value: own fn-props (statics, name,
2207/// prototype, length) plus inherited statics and `call`/`apply`/`bind`.
2208fn function_property(recv: &Value, name: &str) -> Value {
2209 // A class static, inherited down the constructor chain.
2210 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
2211 if let Some(v) = with_host(|h| h.class_static(recv, name)) {
2212 return v;
2213 }
2214 // A class's own `name` and `length` are its own, not the builtin
2215 // ancestor's: `class A extends Array {}` has `A.name === "A"` and
2216 // `A.length === 0`, but both were read off `Array`. Only a class that
2217 // WOULD fall through to an ancestor takes this path; a plain class keeps
2218 // the ordinary computation below.
2219 if matches!(name, "name" | "length")
2220 && with_host(|h| h.class_static(recv, name)).is_none()
2221 && with_host(|h| h.class_builtin_ancestor(recv))
2222 .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2223 {
2224 if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2225 return v;
2226 }
2227 if name == "name" {
2228 let n = with_host(|h| h.callable_name(recv));
2229 return with_host(|h| h.new_str(n));
2230 }
2231 // The class's own constructor decides its arity; with no explicit
2232 // one the implicit `constructor(...args)` has length 0.
2233 let ctor = with_host(|h| match h.get(recv) {
2234 Some(JsObj::Class(c)) => c.ctor.clone(),
2235 _ => None,
2236 });
2237 return match ctor {
2238 Some(c) => get_property(&c, "length").unwrap_or(Value::Float(0.0)),
2239 None => Value::Float(0.0),
2240 };
2241 }
2242 // `Symbol.species` is an accessor returning `this`, so a subclass that
2243 // does not override it IS its own species. Reading it off the builtin
2244 // ancestor below would answer with the ancestor — `A[Symbol.species]`
2245 // came back as `Array`, which sent every derived result to a plain
2246 // array.
2247 if name == "@@species"
2248 && with_host(|h| h.class_static(recv, "@@species")).is_none()
2249 && with_host(|h| h.class_builtin_ancestor(recv))
2250 .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2251 {
2252 return recv.clone();
2253 }
2254 // The chain may bottom out in a BUILTIN constructor (`class D extends
2255 // Array {}`), whose statics `class_static` cannot see — it only walks
2256 // `ClassVal.parent` links between user classes. Finish the lookup with an
2257 // ordinary read on that ancestor so `D.from` inherits `Array.from`.
2258 if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
2259 if let Ok(v) = get_property(&anc, name) {
2260 if !matches!(v, Value::Undef) {
2261 return v;
2262 }
2263 }
2264 }
2265 } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2266 return v;
2267 }
2268 // A method inherited via the function's [[Prototype]] chain (set with
2269 // `Object.setPrototypeOf(fn, proto)` — the `router` package makes each router
2270 // *function* inherit `route`/`use`/`get`/… from `Router.prototype` this way).
2271 if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
2272 return v;
2273 }
2274 match name {
2275 "name" => with_host(|h| {
2276 let n = h.callable_name(recv);
2277 h.new_str(n)
2278 }),
2279 "length" => Value::Float(with_host(|h| h.func_arity(recv)) as f64),
2280 "prototype" => ensure_fn_prototype(recv),
2281 _ if is_function_method(name) => bound_method(recv, name),
2282 _ => Value::Undef,
2283 }
2284}
2285
2286/// The `.prototype` of a function value, auto-created on first access (as Node
2287/// does for every non-arrow function) with `.constructor` linking back. Arrow
2288/// functions have no `prototype`.
2289fn ensure_fn_prototype(recv: &Value) -> Value {
2290 if let Some(p) = with_host(|h| h.fn_prop(recv, "prototype")) {
2291 return p;
2292 }
2293 // Only a constructor gets one: an arrow, a method definition and an async
2294 // function are not constructors, and a class sets its own (10.2.5).
2295 if with_host(|h| h.kind_of(recv)) != Some(ObjKind::Func) {
2296 return Value::Undef;
2297 }
2298 if !with_host(|h| h.owns_prototype(recv)) {
2299 return Value::Undef;
2300 }
2301 with_host(|h| {
2302 let proto = h.new_object(IndexMap::new());
2303 if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
2304 p.insert("constructor".to_string(), recv.clone());
2305 }
2306 h.hide_prop(&proto, "constructor");
2307 h.set_fn_prop(recv, "prototype", proto.clone());
2308 proto
2309 })
2310}
2311
2312/// The numeric constants a core namespace owns, in the order node reports them
2313/// under `getOwnPropertyNames`. ONE table rather than a value match plus a name
2314/// list: the enumeration and the read have to agree, and they did not — every
2315/// one of these read correctly while `Object.getOwnPropertyNames(Math)` omitted
2316/// all eight of Math's, so a member that plainly exists was invisible to any
2317/// reflective copy of the namespace.
2318///
2319/// Each is `{ writable: false, enumerable: false, configurable: false }`, which
2320/// is what separates them from the methods alongside them.
2321pub fn namespace_constants(ns: &str) -> &'static [(&'static str, f64)] {
2322 const MATH: &[(&str, f64)] = &[
2323 ("E", std::f64::consts::E),
2324 ("LN10", std::f64::consts::LN_10),
2325 ("LN2", std::f64::consts::LN_2),
2326 ("LOG10E", std::f64::consts::LOG10_E),
2327 ("LOG2E", std::f64::consts::LOG2_E),
2328 ("PI", std::f64::consts::PI),
2329 ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2),
2330 ("SQRT2", std::f64::consts::SQRT_2),
2331 ];
2332 const NUMBER: &[(&str, f64)] = &[
2333 ("MAX_VALUE", f64::MAX),
2334 // The smallest positive value a Number can hold, which is the
2335 // smallest SUBNORMAL double (`5e-324`), not Rust's
2336 // `f64::MIN_POSITIVE` — that is the smallest *normal* double,
2337 // `2.2250738585072014e-308`, ~256 binary orders of magnitude too
2338 // large.
2339 // The literal, not `f64::from_bits(1)`: that is only const-callable from
2340 // Rust 1.83 and this crate's MSRV is 1.80. It parses to the same
2341 // bit pattern — the smallest positive subnormal.
2342 ("MIN_VALUE", 5e-324),
2343 ("NaN", f64::NAN),
2344 ("NEGATIVE_INFINITY", f64::NEG_INFINITY),
2345 ("POSITIVE_INFINITY", f64::INFINITY),
2346 ("MAX_SAFE_INTEGER", 9007199254740991.0),
2347 ("MIN_SAFE_INTEGER", -9007199254740991.0),
2348 ("EPSILON", f64::EPSILON),
2349 ];
2350 match ns {
2351 "Math" => MATH,
2352 "Number" => NUMBER,
2353 _ => &[],
2354 }
2355}
2356
2357/// The descriptor of `<ns>.<key>`, whose attributes fall into four groups —
2358/// measured on node v26.8.1:
2359///
2360/// ```text
2361/// Math.PI, Number.MAX_SAFE_INTEGER, Number.prototype w=false e=false c=false
2362/// Math.max.name, Math.max.length w=false e=false c=true
2363/// Math.floor, Array.from, Array.prototype.slice w=true e=false c=true
2364/// require('path').join w=true e=true c=true
2365/// ```
2366///
2367/// So: a constant (and a constructor's `prototype`) is frozen, a function's own
2368/// `name`/`length` is read-only but configurable, and everything else is an
2369/// ordinary method — enumerable exactly when the namespace enumerates it, which
2370/// is what separates a core module's exports from an ECMAScript namespace's.
2371fn builtin_member_descriptor(ns: &str, key: &str, value: Value) -> Value {
2372 let frozen = namespace_constants(ns).iter().any(|(k, _)| *k == key)
2373 || key == "prototype"
2374 || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key));
2375 let own_fn_meta = matches!(key, "name" | "length") && host::builtin_is_callable(ns);
2376 // A key a SCRIPT assigned is an ordinary writable/enumerable/configurable
2377 // data property, whatever the namespace's built-in members look like — the
2378 // synthesized answer reported it non-enumerable, so a monkey-patched member
2379 // described itself as one of the intrinsics.
2380 let assigned = !intrinsic_proto_member(ns, key)
2381 && !crate::stdlib::namespace_keys(ns).iter().any(|k| k == key)
2382 && with_host(|h| h.builtin_static(ns, key).is_some());
2383 let enumerable = assigned
2384 || (!frozen && !own_fn_meta && crate::stdlib::namespace_keys(ns).iter().any(|k| k == key));
2385 with_host(|h| {
2386 let mut m: IndexMap<String, Value> = IndexMap::new();
2387 m.insert("value".into(), value);
2388 m.insert(
2389 "writable".into(),
2390 Value::Bool(assigned || (!frozen && !own_fn_meta)),
2391 );
2392 m.insert("enumerable".into(), Value::Bool(enumerable));
2393 m.insert("configurable".into(), Value::Bool(assigned || !frozen));
2394 h.new_object(m)
2395 })
2396}
2397
2398/// Whether `<ns>.<key>` may be deleted — the `configurable` half of
2399/// [`builtin_member_descriptor`], split out so `delete` can ask without
2400/// building a descriptor object.
2401/// Whether `key` is one of the members the intrinsic prototype namespace `ns`
2402/// really defines — as opposed to a name a script added. An assignment over one
2403/// of these is a `[[Set]]` and leaves its attributes alone.
2404fn intrinsic_proto_member(ns: &str, key: &str) -> bool {
2405 intrinsic_proto_members(ns).is_some_and(|members| {
2406 members
2407 .iter()
2408 .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
2409 })
2410}
2411
2412fn builtin_member_configurable(ns: &str, key: &str) -> bool {
2413 !(namespace_constants(ns).iter().any(|(k, _)| *k == key)
2414 || key == "prototype"
2415 || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key)))
2416}
2417
2418/// The value of `<ns>.<name>` when it is one of those constants.
2419fn namespace_constant(ns: &str, name: &str) -> Option<f64> {
2420 namespace_constants(ns)
2421 .iter()
2422 .find(|(k, _)| *k == name)
2423 .map(|(_, v)| *v)
2424}
2425
2426/// Whether `ctor` is a WebIDL interface, whose prototype members are plain
2427/// assigned — and so ENUMERABLE — rather than the non-enumerable ones an
2428/// ECMAScript builtin defines. The generated member table records the same
2429/// distinction with its `+` prefix.
2430fn is_webidl_proto(ctor: &str) -> bool {
2431 intrinsic_proto_members(&format!("{ctor}.prototype"))
2432 .is_some_and(|ms| ms.iter().any(|m| m.starts_with('+')))
2433}
2434
2435/// The intrinsic constructor a value's own kind implies — the prototype it
2436/// inherits with no explicit link.
2437pub(crate) fn own_ctor_name(h: &host::JsHost, v: &Value) -> Option<&'static str> {
2438 default_ctor_name(h, v)
2439}
2440
2441/// Whether `ctor.prototype` defines `key` as a NON-WRITABLE data property, so
2442/// an object inheriting it refuses an assignment to that name.
2443pub(crate) fn is_proto_readonly(ctor: &str, key: &str) -> bool {
2444 crate::arity::PROTO_READONLY
2445 .binary_search_by(|(k, _)| (*k).cmp(ctor))
2446 .ok()
2447 .is_some_and(|i| crate::arity::PROTO_READONLY[i].1.contains(&key))
2448}
2449
2450/// Whether `ctor.prototype` defines `key` as an ACCESSOR rather than a data
2451/// property or a method.
2452pub(crate) fn is_proto_accessor(ctor: &str, key: &str) -> bool {
2453 crate::arity::PROTO_ACCESSORS
2454 .binary_search_by(|(k, _)| (*k).cmp(ctor))
2455 .ok()
2456 .is_some_and(|i| crate::arity::PROTO_ACCESSORS[i].1.contains(&key))
2457}
2458
2459/// The constructor whose `.prototype` IS `recv`, whichever of the two
2460/// representations it uses — a `Builtin` namespace handle or a real object.
2461pub(crate) fn intrinsic_proto_of(recv: &Value) -> Option<String> {
2462 with_host(|h| match h.get(recv) {
2463 Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
2464 _ => h.intrinsic_proto_ctor(recv).map(str::to_string),
2465 })
2466}
2467
2468/// The getter function of an intrinsic prototype accessor, as a first-class
2469/// value — what `Object.getOwnPropertyDescriptor(Map.prototype, 'size').get`
2470/// hands back, and the form a library uses to borrow one.
2471fn proto_getter(ctor: &str, key: &str) -> Value {
2472 with_host(|h| h.alloc(JsObj::Builtin(format!("@protoget:{ctor}:{key}"))))
2473}
2474
2475/// Whether `recv` carries the internal slot `ctor`'s accessor demands. This is
2476/// a BRAND check, not a chain walk: `Object.create(Map.prototype).size` throws
2477/// in node even though `Map.prototype` is right there on the chain.
2478fn brand_matches(recv: &Value, ctor: &str) -> bool {
2479 if let Some(tag) = crate::stdlib::native_tag(recv) {
2480 if tag == ctor || (ctor == "TypedArray" && tag == "TypedArray") {
2481 return true;
2482 }
2483 }
2484 match ctor {
2485 "TypedArray" => crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray"),
2486 "ArrayBuffer" => with_host(
2487 |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@bytes")),
2488 ),
2489 _ => {
2490 let own = match wrapped_primitive(recv).as_ref().and_then(wrapper_ctor_of) {
2491 Some(c) => Some(c),
2492 None => with_host(|h| default_ctor_name(h, recv)),
2493 };
2494 own == Some(ctor)
2495 }
2496 }
2497}
2498
2499thread_local! {
2500 /// The `(ctor, key)` prototype accessors whose tail read is in flight.
2501 ///
2502 /// A getter's last step reads the value off the receiver, and when the
2503 /// receiver does not STORE it that read walks the chain, finds the same
2504 /// accessor and runs it again: `new TextDecoder().fatal` recursed until the
2505 /// stack overflowed and aborted the process. An accessor already in flight
2506 /// answers `undefined` for its own key rather than re-entering — the value
2507 /// a missing internal slot has, and what node reports for one.
2508 static GETTERS_IN_FLIGHT: std::cell::RefCell<Vec<(String, String)>> =
2509 const { std::cell::RefCell::new(Vec::new()) };
2510}
2511
2512/// Whether `ctor`'s `key` getter is already running further down the stack.
2513fn getter_in_flight(ctor: &str, key: &str) -> bool {
2514 GETTERS_IN_FLIGHT.with(|g| g.borrow().iter().any(|(c, k)| c == ctor && k == key))
2515}
2516
2517/// Invoke an intrinsic prototype's getter against `recv` — the body behind the
2518/// `@protoget:` thunks.
2519///
2520/// Reading one OFF THE PROTOTYPE (`Map.prototype.size`) is the case that was
2521/// wrong: it answered `undefined` where node runs the getter, fails the brand
2522/// check and throws. `RegExp.prototype` is the documented exception — 22.2.6.10
2523/// and .13 return `"(?:)"` and `""` for it specifically, so the one receiver
2524/// that would otherwise throw for every flag reads two of them back.
2525pub(crate) fn proto_getter_call(ctor: &str, key: &str, recv: &Value) -> Result<Value, String> {
2526 let is_the_prototype = with_host(
2527 |h| matches!(h.get(recv), Some(JsObj::Builtin(ns)) if *ns == format!("{ctor}.prototype")),
2528 );
2529 if is_the_prototype && ctor == "RegExp" {
2530 // 22.2.6.x each carry the same step: when `this` IS `%RegExp.prototype%`
2531 // the getter returns rather than throwing. `source` and `flags` have
2532 // their own values there; every flag getter answers `undefined`.
2533 return Ok(match key {
2534 "source" => with_host(|h| h.new_str("(?:)".to_string())),
2535 "flags" => with_host(|h| h.new_str(String::new())),
2536 _ => Value::Undef,
2537 });
2538 }
2539 // `RegExp.prototype.flags` (22.2.6.5) is the one that is GENERIC: it reads
2540 // the individual flag properties off whatever object it is handed and
2541 // concatenates their letters, so a plain object answers `""` rather than
2542 // throwing, and one carrying `global`/`ignoreCase` answers `"gi"`.
2543 if ctor == "RegExp" && key == "flags" && !brand_matches(recv, ctor) {
2544 if !with_host(|h| is_object_like(h, recv)) {
2545 return Err(regexp_brand_error(key, recv));
2546 }
2547 let mut out = String::new();
2548 for (prop, letter) in REGEXP_FLAG_LETTERS {
2549 let v = get_property(recv, prop)?;
2550 if with_host(|h| h.truthy(&v)) {
2551 out.push(*letter);
2552 }
2553 }
2554 return Ok(with_host(|h| h.new_str(out)));
2555 }
2556 // `Function.prototype.arguments`/`caller` are POISON PILLS (10.2.4.1): both
2557 // the getter and the setter throw for every receiver, which is how a strict
2558 // function keeps its caller unreachable. They are not brand checks and do
2559 // not name the receiver.
2560 if ctor == "Function" && matches!(key, "arguments" | "caller") {
2561 return poison_pill_read(recv);
2562 }
2563 if !brand_matches(recv, ctor) {
2564 return Err(match ctor {
2565 "RegExp" => regexp_brand_error(key, recv),
2566 "Symbol" => {
2567 host::type_error("Symbol.prototype.description requires that 'this' be a Symbol")
2568 }
2569 _ => host::type_error(&format!(
2570 "Method get {ctor}.prototype.{key} called on incompatible receiver {}",
2571 brand_receiver_string(recv)
2572 )),
2573 });
2574 }
2575 // A native instance keeps an accessor's value in the hidden `@@<key>` slot,
2576 // so that the public name can be a getter on the prototype rather than an
2577 // own enumerable property. Read it straight: the chain walk below would
2578 // find this same accessor and run it again.
2579 if let Some(v) = with_host(|h| match h.get(recv) {
2580 Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
2581 _ => None,
2582 }) {
2583 return Ok(v);
2584 }
2585 GETTERS_IN_FLIGHT.with(|g| g.borrow_mut().push((ctor.to_string(), key.to_string())));
2586 let out = get_property(recv, key);
2587 GETTERS_IN_FLIGHT.with(|g| {
2588 g.borrow_mut().pop();
2589 });
2590 out
2591}
2592
2593/// `Function.prototype.arguments`/`caller` read against `recv`.
2594///
2595/// The pill is conditional and the condition is the RECEIVER, not the reading
2596/// code: a sloppy non-arrow function answers `null` (node stopped populating
2597/// these long ago but kept them readable), and everything else — an arrow, a
2598/// strict function, a non-function — throws. Keying it on the READER's
2599/// strictness, which is what this did, made `strictFn.arguments` answer
2600/// `undefined` from sloppy code and a sloppy function throw from strict code:
2601/// wrong in both directions.
2602pub(crate) fn poison_pill_read(recv: &Value) -> Result<Value, String> {
2603 if with_host(|h| h.fn_is_sloppy(recv)) {
2604 return Ok(with_host(|h| h.null()));
2605 }
2606 Err(host::type_error(POISON_PILL))
2607}
2608
2609/// The message both halves of the `arguments`/`caller` poison pill throw.
2610pub(crate) const POISON_PILL: &str = "'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them";
2611
2612/// How a REJECTED receiver is rendered in a brand-check message.
2613///
2614/// `no_side_effects_string` answers for most of them, but two kinds differ:
2615/// an intrinsic PROTOTYPE renders `#<Map>` rather than `[object Map]`, and so
2616/// does an `ArrayBuffer`/`DataView` instance, which this host tags natively and
2617/// that function therefore brands. Node draws the line at whether the value is
2618/// one of the ES5-era classes (`Array`, `Date`, `RegExp` are `[object X]`); the
2619/// two cases here are the ones that fall on the other side of it.
2620fn brand_receiver_string(recv: &Value) -> String {
2621 if let Some(ctor) = intrinsic_proto_of(recv) {
2622 return format!("#<{ctor}>");
2623 }
2624 match crate::stdlib::native_tag(recv).as_deref() {
2625 Some(tag @ ("ArrayBuffer" | "DataView")) => format!("#<{tag}>"),
2626 _ => no_side_effects_string(recv),
2627 }
2628}
2629
2630/// The flag properties `RegExp.prototype.flags` reads, in the order 22.2.6.5
2631/// concatenates their letters.
2632const REGEXP_FLAG_LETTERS: &[(&str, char)] = &[
2633 ("hasIndices", 'd'),
2634 ("global", 'g'),
2635 ("ignoreCase", 'i'),
2636 ("multiline", 'm'),
2637 ("dotAll", 's'),
2638 ("unicode", 'u'),
2639 ("unicodeSets", 'v'),
2640 ("sticky", 'y'),
2641];
2642
2643/// `RegExp.prototype`'s flag getters word their brand failure their own way,
2644/// and `flags` distinguishes a non-object receiver from a non-RegExp one
2645/// because 22.2.6.5 reads the individual flags off any object it is given.
2646fn regexp_brand_error(key: &str, recv: &Value) -> String {
2647 if key == "flags" && !with_host(|h| matches!(recv, Value::Obj(_)) && !h.is_null(recv)) {
2648 return host::type_error(&format!(
2649 "RegExp.prototype.flags getter called on non-object {}",
2650 no_side_effects_string(recv)
2651 ));
2652 }
2653 host::type_error(&format!(
2654 "RegExp.prototype.{key} getter called on non-RegExp object"
2655 ))
2656}
2657
2658/// A property on a builtin namespace object (`Math.PI`, `Number.MAX_SAFE_INTEGER`,
2659/// `console.log`).
2660pub fn namespace_property(ns: &str, name: &str) -> Value {
2661 // `require.cache[id]` — a LIVE view of the module cache, not a copy, so a
2662 // read sees whatever is loaded now and `delete` (see `delete_property`)
2663 // actually invalidates.
2664 if ns == REQUIRE_CACHE {
2665 return crate::module::cache_get(name).unwrap_or(Value::Undef);
2666 }
2667 // A property a SCRIPT assigned onto this namespace wins over everything
2668 // synthesized below, including a member the namespace really has. That is
2669 // what monkey-patching an intrinsic is: `Array.prototype.join = f` must make
2670 // `[1, 2].join()` call `f`, and a polyfill's `Array.prototype.at = impl` has
2671 // to read back at all. Only the two `Error` hooks consulted this table, so
2672 // every other assignment onto a builtin — the whole polyfill idiom — was
2673 // stored by `set_property` and then never read: the write appeared to
2674 // succeed, `Object.isExtensible` said true, and the value came back
2675 // `undefined`.
2676 if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2677 return v;
2678 }
2679 // The ENTRY script's `require` is this builtin rather than the per-module
2680 // closure, so its `cache` has to be handed out here too.
2681 // `require.extensions` — the legacy loader map. Deprecated but still read
2682 // (and sometimes written) by tooling that hooks module loading, and it was
2683 // absent entirely. The three keys node ships are present; installing a
2684 // custom loader through them is NOT honoured by this runtime's loader, so
2685 // the map reports what it can serve rather than pretending otherwise.
2686 // `util.promisify.custom` — the registered symbol a module attaches to a
2687 // callback function to supply its own promisified form. It was `undefined`,
2688 // so the lookup that decides whether to use one always missed.
2689 if ns == "util.promisify" && name == "custom" {
2690 return with_host(|h| h.symbol_for("nodejs.util.promisify.custom"));
2691 }
2692 // `process.memoryUsage.rss()` — node's fast path for the one figure that
2693 // does not need the whole object built.
2694 if ns == "process.memoryUsage" && name == "rss" {
2695 return with_host(|h| h.alloc(JsObj::Builtin("process.memoryUsage.rss".to_string())));
2696 }
2697 if ns == "require" && name == "extensions" {
2698 return with_host(|h| {
2699 let mut m: IndexMap<String, Value> = IndexMap::new();
2700 for ext in [".js", ".json", ".node"] {
2701 let f = h.alloc(JsObj::Builtin(format!("@@extension:{ext}")));
2702 m.insert(ext.to_string(), f);
2703 }
2704 h.new_object(m)
2705 });
2706 }
2707 // `require.resolve.paths(spec)` — the directories a lookup would search:
2708 // `null` for a core module, the `node_modules` chain otherwise.
2709 if ns == "require.resolve" && name == "paths" {
2710 return with_host(|h| h.alloc(JsObj::Builtin("require.resolve.paths".to_string())));
2711 }
2712 if ns == "require" && name == "cache" {
2713 return with_host(|h| h.alloc(JsObj::Builtin(REQUIRE_CACHE.to_string())));
2714 }
2715 // The legacy numeric codes `DOMException` carries as statics
2716 // (`DOMException.ABORT_ERR` is 20), named by uppercasing the error name.
2717 if ns == "DOMException" {
2718 if let Some((_, code)) = DOM_EXCEPTION_CODES
2719 .iter()
2720 .find(|(n, _)| legacy_code_name(n) == name)
2721 {
2722 return Value::Float(*code);
2723 }
2724 }
2725 // Numeric constants.
2726 if let Some(k) = namespace_constant(ns, name) {
2727 return Value::Float(k);
2728 }
2729 // `Ctor.name` on a builtin constructor is the constructor name (`Array.name`
2730 // === "Array"); non-callable namespaces (`Math`/`JSON`) fall through to
2731 // `undefined`.
2732 // `GeneratorFunction.prototype` and the two async variants are REAL objects
2733 // in `native_protos`, not `Builtin("X.prototype")` namespace handles — they
2734 // sit on the prototype chain of every generator/async function, which a
2735 // handle cannot do. Without this the read fell through to `undefined`.
2736 if name == "prototype"
2737 && matches!(
2738 ns,
2739 "GeneratorFunction" | "AsyncFunction" | "AsyncGeneratorFunction"
2740 )
2741 {
2742 return with_host(|h| {
2743 h.ensure_native_protos();
2744 h.native_proto(ns).unwrap_or(Value::Undef)
2745 });
2746 }
2747 // `Error.prepareStackTrace` has a DEFAULT hook in node
2748 // (`ErrorPrepareStackTrace`), so a library probing `if
2749 // (Error.prepareStackTrace)` finds one. Reading `undefined` sent that probe
2750 // down the wrong branch. The default renders the header plus the frames,
2751 // which is what the fast path in `materialize_stack` already produces — it
2752 // recognises this exact builtin and skips the round trip.
2753 if ns == "Error" && name == "prepareStackTrace" {
2754 return with_host(|h| h.builtin_static("Error", "prepareStackTrace")).unwrap_or_else(
2755 || with_host(|h| h.alloc(JsObj::Builtin(DEFAULT_PREPARE.to_string()))),
2756 );
2757 }
2758 // `Error.stackTraceLimit` defaults to 10 and is settable; an assignment
2759 // lands in the builtin-static side table, which the read below consults
2760 // first. Without a default the READ was `undefined`, so a library doing
2761 // `const old = Error.stackTraceLimit` and restoring it later installed
2762 // `undefined` and disabled the limit permanently.
2763 if ns == "Error" && name == "stackTraceLimit" {
2764 return with_host(|h| h.builtin_static("Error", "stackTraceLimit"))
2765 .unwrap_or(Value::Float(10.0));
2766 }
2767 // `Ctor[Symbol.species]` is an accessor returning `this` on every builtin
2768 // that has one (23.1.2.5, 27.2.4.7, …). It was absent, so the species
2769 // protocol had nothing to read and every derived result came back a plain
2770 // builtin.
2771 if name == "@@species" && has_species(ns) {
2772 return with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())));
2773 }
2774 if name == "name" && is_builtin_ctor(ns) {
2775 return with_host(|h| h.new_str(ns.to_string()));
2776 }
2777 // A well-known symbol (`Symbol.iterator`, `Symbol.toPrimitive`, …) used as a
2778 // computed property/method key.
2779 if ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&name) {
2780 return with_host(|h| h.well_known_symbol(name));
2781 }
2782 // Non-function constants on a stdlib namespace (`path.sep`, `os.EOL`,
2783 // `buffer.Buffer`, `url.URL`).
2784 if let Some(v) = crate::stdlib::constant(ns, name) {
2785 return v;
2786 }
2787 // `Ctor.prototype` on a builtin constructor (`Object.prototype`,
2788 // `Array.prototype`, …): a prototype namespace whose methods are callable
2789 // thunks (`Object.prototype.toString.call(x)` is a load-time idiom in the
2790 // `get-intrinsic`/`function-bind` family).
2791 if name == "prototype" && is_builtin_ctor(ns) {
2792 // Same reasoning as the native prototypes below, for the error
2793 // hierarchy: `new Error(...)` links its `[[Prototype]]` to the REAL
2794 // `error_protos` object, so `Error.prototype` has to read back that same
2795 // object. It resolved to a fresh `Builtin("Error.prototype")` thunk
2796 // instead, which is a FUNCTION — so `Object.getPrototypeOf(new
2797 // Error("x")) === Error.prototype` was false, and `typeof
2798 // Error.prototype` was `"function"` where node says `"object"`.
2799 if host::ERROR_NAMES.contains(&ns) {
2800 if let Some(p) = with_host(|h| {
2801 h.ensure_error_protos();
2802 host::error_proto_of(h, ns)
2803 }) {
2804 return p;
2805 }
2806 }
2807 // `Buffer`/`Uint8Array` have real prototype *objects* — a Buffer's
2808 // `[[Prototype]]` points at one, so `Object.getPrototypeOf(buf) ===
2809 // Buffer.prototype` must compare equal, which a freshly-allocated
2810 // `Builtin` handle never can.
2811 if let Some(p) = with_host(|h| {
2812 h.ensure_native_protos();
2813 h.native_proto(ns)
2814 }) {
2815 return p;
2816 }
2817 let _ = ns;
2818 return with_host(|h| h.alloc(JsObj::Builtin(format!("{ns}.prototype"))));
2819 }
2820 // A NATIVE stdlib constructor's `.prototype` (`StringDecoder`, `Hash`,
2821 // `URLSearchParams`, …). These are absent from `is_builtin_ctor`, so the arm
2822 // above never fired and the read produced `undefined` — which broke the ES5
2823 // subclassing pattern libraries still ship. `iconv-lite`'s internal codec
2824 // reads `StringDecoder.prototype.end` at load, and threw
2825 // `Cannot read properties of undefined (reading 'end')`. Built from the same
2826 // instance-method table a method read consults, so the two cannot disagree.
2827 if name == "prototype" {
2828 if let Some(p) = with_host(|h| h.ensure_ctor_proto(ns)) {
2829 return p;
2830 }
2831 }
2832 // A method read off a builtin prototype namespace (`Array.prototype.slice`):
2833 // a `@proto:<Ctor>:<method>` thunk that, when invoked (typically via
2834 // `.call`/`.apply`), dispatches `method` against the invoke-time `this`.
2835 //
2836 // The thunk is minted only for a name the prototype REALLY carries. Minting
2837 // one unconditionally made every absent name answer with a function:
2838 // `Array.prototype.totallyBogus` was `[Function: totallyBogus]` where node
2839 // says `undefined`, and so was every well-known symbol a prototype does not
2840 // define — `Array.prototype[Symbol.toStringTag]` came back a function
2841 // instead of `undefined`, which is a value `Object.prototype.toString` and
2842 // every `typeof`/truthiness test downstream then read wrong.
2843 //
2844 // Existence is decided by the generated intrinsic table, which is read out
2845 // of the reference engine, so this cannot drift from what node defines.
2846 // A name the prototype does not define but `Object.prototype` does is
2847 // INHERITED, and node hands back Object.prototype's own function object
2848 // (`Map.prototype.toString === Object.prototype.toString` is `true`), so it
2849 // resolves to the `Object` thunk rather than a per-ctor one. That is also
2850 // what makes `String(Map.prototype)` print `[object Map]`: `Map.prototype`
2851 // has no own `toString`, and the inherited one is the generic tag reader,
2852 // not a Map method that rejects a non-Map `this`.
2853 if let Some(ctor) = ns.strip_suffix(".prototype") {
2854 // `Array.prototype[Symbol.unscopables]` (23.1.3.38) is a DATA property,
2855 // not an intrinsic function, so it is not in the arity table the lookup
2856 // above consults. It lists the methods a `with` block must NOT bring
2857 // into scope — the ones added after `with` existed, so old code using a
2858 // variable of the same name keeps working.
2859 if name == "@@unscopables" && ctor == "Array" {
2860 return with_host(|h| {
2861 let mut m: IndexMap<String, Value> = IndexMap::new();
2862 for k in [
2863 "at",
2864 "copyWithin",
2865 "entries",
2866 "fill",
2867 "find",
2868 "findIndex",
2869 "findLast",
2870 "findLastIndex",
2871 "flat",
2872 "flatMap",
2873 "includes",
2874 "keys",
2875 "toReversed",
2876 "toSorted",
2877 "toSpliced",
2878 "values",
2879 ] {
2880 m.insert(k.to_string(), Value::Bool(true));
2881 }
2882 let o = h.new_object(m);
2883 let null = h.null();
2884 h.set_proto(&o, null);
2885 o
2886 });
2887 }
2888 if builtin_meta(&format!("@proto:{ctor}:{name}")).is_some() {
2889 return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:{ctor}:{name}"))));
2890 }
2891 if ctor != "Object" && builtin_meta(&format!("@proto:Object:{name}")).is_some() {
2892 return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:Object:{name}"))));
2893 }
2894 // `constructor` is excluded from the table because it is not a method:
2895 // it is the constructor function itself, and node compares equal
2896 // (`Array.prototype.constructor === Array`). It used to resolve to a
2897 // `@proto:Array:constructor` thunk, which is a different object every
2898 // read and so never compared equal to anything.
2899 if name == "constructor" && is_builtin_ctor(ctor) {
2900 return with_host(|h| h.alloc(JsObj::Builtin(ctor.to_string())));
2901 }
2902 return Value::Undef;
2903 }
2904 let qualified = format!("{ns}.{name}");
2905 if is_known_builtin(&qualified) {
2906 return with_host(|h| h.alloc(JsObj::Builtin(qualified)));
2907 }
2908 // A property the user stuck on this builtin namespace (`Error.prepareStackTrace`).
2909 if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2910 return v;
2911 }
2912 // A builtin FUNCTION's own `name` and `length` (10.3.3-4: every one has
2913 // both). `Math.max.name` was `undefined` — as was every `.name` a library
2914 // reads to identify a callback it was handed. The non-callable namespaces
2915 // fall through: `Math.name` and `require('fs').length` really are undefined.
2916 if host::builtin_is_callable(ns) {
2917 match name {
2918 "name" => {
2919 if let Some(n) = proto_getter_name(ns) {
2920 return with_host(|h| h.new_str(n));
2921 }
2922 return with_host(|h| h.new_str(builtin_name(ns).to_string()));
2923 }
2924 // Only the intrinsics have a specified arity; a core-module
2925 // function's is a property of node's own JS source, so it stays
2926 // `undefined` rather than being invented here.
2927 "length" => {
2928 // A getter takes no argument (10.2.9 / the accessor grammar),
2929 // so its `length` is 0 — it is not in the intrinsic table,
2930 // which holds only named functions.
2931 if proto_getter_name(ns).is_some() {
2932 return Value::Float(0.0);
2933 }
2934 if let Some((_, len)) = builtin_meta(ns) {
2935 return Value::Float(len as f64);
2936 }
2937 }
2938 _ => {}
2939 }
2940 }
2941 Value::Undef
2942}
2943
2944/// Dispatch a `@proto:<Ctor>:<method>` thunk (a method read off a builtin
2945/// prototype, e.g. `Object.prototype.toString`) against `recv` (its invoke-time
2946/// `this`). `Object.prototype.toString` yields the `[object Tag]` brand string
2947/// libraries type-check on; every other method routes through normal method
2948/// dispatch on `recv`.
2949/// The TypeError a `<Ctor>.prototype.<method>` thunk throws when it is invoked
2950/// with NO receiver — `const f = [].push; f(1)`.
2951///
2952/// Reading a method off an instance used to mint a thunk bound to that
2953/// instance, so a detached method silently kept working on the object it came
2954/// from. Now that it is the shared intrinsic, a bare call has no `this` and has
2955/// to say so. Node words it four ways, and which one a method gets is not
2956/// something that can be derived — the split was measured across every method
2957/// of each prototype:
2958///
2959/// ```text
2960/// ToObject(this) "Cannot convert undefined or null to object"
2961/// RequireObjectCoercible "<Ctor>.prototype.<m> called on null or undefined"
2962/// brand check "<Ctor>.prototype.<m> requires that 'this' be a <X>"
2963/// everything else the generic incompatible-receiver message
2964/// ```
2965fn nullish_receiver_error(ctor: &str, method: &str, recv: &str) -> Option<String> {
2966 // `Array.prototype` splits: the CALLBACK-taking methods plus `concat` and
2967 // the two `indexOf` family members name themselves, the rest go through
2968 // `ToObject` and report its message.
2969 const ARRAY_NAMED: &[&str] = &[
2970 "concat",
2971 "every",
2972 "filter",
2973 "find",
2974 "findIndex",
2975 "findLast",
2976 "findLastIndex",
2977 "forEach",
2978 "indexOf",
2979 "map",
2980 "reduce",
2981 "reduceRight",
2982 "some",
2983 ];
2984 const TO_OBJECT: &str = "Cannot convert undefined or null to object";
2985 let named = |c: &str| format!("{c}.prototype.{method} called on null or undefined");
2986 let branded =
2987 |c: &str, want: &str| format!("{c}.prototype.{method} requires that 'this' be a {want}");
2988 // The generic form names the receiver, so a `null` one must not be reported
2989 // as `undefined`.
2990 let generic = |c: &str, m: &str| {
2991 format!("Method {c}.prototype.{m} called on incompatible receiver {recv}")
2992 };
2993 Some(match ctor {
2994 "Array" if ARRAY_NAMED.contains(&method) => named("Array"),
2995 "Array" => TO_OBJECT.to_string(),
2996 // `Object.prototype.toString` is the one method that ACCEPTS a nullish
2997 // receiver — it answers `[object Undefined]`.
2998 "Object" if method == "toString" => return None,
2999 "Object" if method == "toLocaleString" => named("Object"),
3000 "Object" => TO_OBJECT.to_string(),
3001 // Both aliases report the LEGACY name in the message, which is the one
3002 // place `name` and the message disagree.
3003 "String" if method == "trimStart" => named("String").replace("trimStart", "trimLeft"),
3004 "String" if method == "trimEnd" => named("String").replace("trimEnd", "trimRight"),
3005 "String" if matches!(method, "toString" | "valueOf") => branded("String", "String"),
3006 "String" => named("String"),
3007 "Number" => branded("Number", "Number"),
3008 "Boolean" => branded("Boolean", "Boolean"),
3009 "Symbol" => branded("Symbol", "Symbol"),
3010 "Function" if method == "bind" => "Bind must be called on a function".to_string(),
3011 "Function" if matches!(method, "call" | "apply") => format!(
3012 "Function.prototype.{method} was called on undefined, which is undefined and not a function"
3013 ),
3014 "Function" => branded("Function", "Function"),
3015 // `Promise.prototype.catch`/`finally` are written in terms of `then`, so
3016 // a nullish receiver fails inside them and reports that instead.
3017 "Promise" if method == "catch" => {
3018 "Cannot read properties of undefined (reading 'then')".to_string()
3019 }
3020 "Promise" if method == "finally" => {
3021 "Promise.prototype.finally called on non-object".to_string()
3022 }
3023 "Date" if method == "toJSON" => TO_OBJECT.to_string(),
3024 // The plain GETTERS and `valueOf` read `[[DateValue]]` directly and
3025 // report that slot check; every setter, every `to*String` and the two
3026 // legacy year methods go through the generic receiver check first.
3027 "Date"
3028 if method == "valueOf"
3029 || (method.starts_with("get") && method != "getYear") =>
3030 {
3031 "this is not a Date object.".to_string()
3032 }
3033 // An ALIAS reports the method it aliases: `toGMTString` IS `toUTCString`
3034 // and `Set.prototype.keys` IS `values`, one function object each.
3035 "Date" if method == "toGMTString" => generic("Date", "toUTCString"),
3036 "Set" if method == "keys" => generic("Set", "values"),
3037 // Everything else that is brand-checked names itself. Node reaches this
3038 // wording from a `[[GetOwnProperty]]`-style slot check; here the check
3039 // is the receiver's kind, and only the message has to agree.
3040 "ArrayBuffer" | "DataView" | "RegExp" | "WeakRef" | "Map" | "Set" | "WeakMap"
3041 | "WeakSet" | "Promise" | "Date" => generic(ctor, method),
3042 "URLSearchParams" => "Value of \"this\" must be of type URLSearchParams".to_string(),
3043 // Node's `URL` methods fail while reaching for their internal state, and
3044 // report the read that failed rather than the method.
3045 "URL" => "Cannot read properties of undefined (reading 'URL')".to_string(),
3046 _ => return None,
3047 })
3048}
3049
3050/// Whether `<ctor>.prototype.<method>` begins with a `this<Type>Value` brand
3051/// check (21.1.3, 20.3.3, 22.1.3.29/.35, 21.2.3). Every `Number.prototype`
3052/// method does; of `String.prototype` only `toString`/`valueOf` do — the rest
3053/// are generic and coerce their receiver with `ToString`.
3054fn is_brand_checked_primitive_method(ctor: &str, method: &str) -> bool {
3055 match ctor {
3056 "Number" => matches!(
3057 method,
3058 "toString" | "toLocaleString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision"
3059 ),
3060 "BigInt" => matches!(method, "toString" | "toLocaleString" | "valueOf"),
3061 "String" | "Boolean" => matches!(method, "toString" | "valueOf"),
3062 _ => false,
3063 }
3064}
3065
3066/// `this<Type>Value(recv)` for `ctor` ∈ Number/String/Boolean/BigInt: the
3067/// primitive itself, the primitive a wrapper boxes, or — for the three
3068/// prototypes that are themselves wrappers (21.1.3, 22.1.3, 20.3.3) — the
3069/// prototype's own `+0` / `""` / `false`. `None` is the TypeError case.
3070fn this_primitive_value(ctor: &str, recv: &Value) -> Option<Value> {
3071 let expected = match ctor {
3072 "Number" => "number",
3073 "String" => "string",
3074 "Boolean" => "boolean",
3075 "BigInt" => "bigint",
3076 _ => return None,
3077 };
3078 let is_expected = |v: &Value| with_host(|h| h.type_of(v)) == expected;
3079 if is_expected(recv) {
3080 return Some(recv.clone());
3081 }
3082 if let Some(prim) = wrapped_primitive(recv).filter(is_expected) {
3083 return Some(prim);
3084 }
3085 if with_host(|h| h.intrinsic_proto_ctor(recv) == Some(ctor)) {
3086 return match ctor {
3087 "Number" => Some(Value::Float(0.0)),
3088 "String" => Some(with_host(|h| h.new_str(""))),
3089 "Boolean" => Some(Value::Bool(false)),
3090 _ => None,
3091 };
3092 }
3093 None
3094}
3095
3096pub fn proto_method(recv: &Value, ctor_method: &str, args: Vec<Value>) -> Result<Value, String> {
3097 let (ctor, method) = ctor_method.split_once(':').unwrap_or(("", ctor_method));
3098 // A prototype ACCESSOR installed by `ensure_ctor_proto`: it reads or writes
3099 // the instance's hidden `@@<name>` slot, which is where the value lives now
3100 // that the public name is a getter rather than an own property.
3101 if let Some(key) = method.strip_prefix("@get@") {
3102 if let Some(v) = with_host(|h| match h.get(recv) {
3103 Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
3104 _ => None,
3105 }) {
3106 return Ok(v);
3107 }
3108 // No stored slot: the value is COMPUTED, so ask the class. `KeyObject`'s
3109 // `symmetricKeySize` is the secret's byte length, which nothing stores.
3110 let tag = crate::stdlib::native_tag(recv).unwrap_or_default();
3111 return crate::stdlib::instance_call(&tag, recv, method, args);
3112 }
3113 if let Some(key) = method.strip_prefix("@set@") {
3114 let v = args.first().cloned().unwrap_or(Value::Undef);
3115 with_host(|h| {
3116 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
3117 p.insert(format!("@@{key}"), v);
3118 }
3119 });
3120 crate::stdlib::instance_accessor_written(ctor, key, recv);
3121 return Ok(Value::Undef);
3122 }
3123 if with_host(|h| h.is_nullish(recv)) {
3124 let shown = if with_host(|h| h.is_null(recv)) {
3125 "null"
3126 } else {
3127 "undefined"
3128 };
3129 if let Some(msg) = nullish_receiver_error(ctor, method, shown) {
3130 return Err(format!("TypeError: {msg}"));
3131 }
3132 }
3133 // `Error.prototype.toString` (20.5.3.4): `name`, `message`, or `name:
3134 // message`, read off the chain so a subclass's `this.name = 'E'` is honored.
3135 if ctor == "Error" && method == "toString" {
3136 // A `DOMException` keeps its `name`/`message` in internal slots, so the
3137 // chain read below would find the class name on the prototype instead.
3138 if let Some(n) = dom_exception_slot(recv, "name") {
3139 let name = with_host(|h| h.str_of(&n));
3140 let msg = dom_exception_slot(recv, "message")
3141 .map(|m| with_host(|h| h.str_of(&m)))
3142 .unwrap_or_default();
3143 let s = if msg.is_empty() {
3144 name
3145 } else {
3146 format!("{name}: {msg}")
3147 };
3148 return Ok(with_host(|h| h.new_str(s)));
3149 }
3150 // `name` and `message` are read with `[[Get]]` (20.5.3.4 steps 3 and 5),
3151 // so a PROXY supplies them through its `get` trap. Reading the stored
3152 // ones first made `String(new Proxy(err, handler))` ignore the handler.
3153 let via_proxy = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy);
3154 let stored = (!via_proxy).then(|| with_host(|h| h.error_to_string(recv)));
3155 let s = match stored.flatten() {
3156 Some(s) => s,
3157 None => {
3158 let read = |k: &str| -> Result<Option<String>, String> {
3159 Ok(host::protocol_lookup(recv, k)?.map(|v| with_host(|h| h.str_of(&v))))
3160 };
3161 let name = read("name")?.unwrap_or_else(|| "Error".into());
3162 let msg = read("message")?.unwrap_or_default();
3163 if msg.is_empty() {
3164 name
3165 } else {
3166 format!("{name}: {msg}")
3167 }
3168 }
3169 };
3170 return Ok(with_host(|h| h.new_str(s)));
3171 }
3172 // The methods that read their receiver through `thisNumberValue` /
3173 // `thisBooleanValue` / `thisStringValue` / `thisBigIntValue` accept only the
3174 // primitive, its wrapper, or the prototype object (which carries the zero
3175 // value) — anything else is a TypeError naming the method. Unchecked,
3176 // `Number.prototype.valueOf.call({})` answered `{}`, `toFixed.call({})`
3177 // reported "toFixed is not a function", and `Number.prototype.valueOf()`
3178 // recursed through the generic conversion until the stack overflowed.
3179 if is_brand_checked_primitive_method(ctor, method) {
3180 let Some(prim) = this_primitive_value(ctor, recv) else {
3181 return Err(format!(
3182 "TypeError: {ctor}.prototype.{method} requires that 'this' be a {ctor}"
3183 ));
3184 };
3185 return host::call_method(&prim, method, args);
3186 }
3187 // A primitive wrapper's `toString`/`valueOf`/`toLocaleString`: unwrap and
3188 // answer as the boxed primitive does. `Number.prototype.toString.call(5)`
3189 // arrives with an already-primitive receiver and needs no unwrapping.
3190 if matches!(ctor, "String" | "Number" | "Boolean") {
3191 let prim = wrapped_primitive(recv).unwrap_or_else(|| recv.clone());
3192 return host::call_method(&prim, method, args);
3193 }
3194 // `thisSymbolValue`/`thisBigIntValue` (20.4.3, 21.2.3) accept a WRAPPER as
3195 // readily as the primitive, and neither was unwrapped here. A BigInt
3196 // wrapper's `valueOf` therefore re-entered the generic conversion, which
3197 // looked `valueOf` up again and called it again: `+Object(9n)` recursed
3198 // until the stack overflowed and ABORTED the process, which no try/catch can
3199 // see. A Symbol wrapper failed the brand check below instead and reported
3200 // that `this` was not a Symbol, when it is one. Only a real wrapper is
3201 // unwrapped — `Symbol.prototype` itself boxes nothing and still has to reach
3202 // the brand check.
3203 if matches!(ctor, "Symbol" | "BigInt") {
3204 if let Some(prim) = wrapped_primitive(recv) {
3205 return host::call_method(&prim, method, args);
3206 }
3207 }
3208 if ctor == "Object" && method == "toString" {
3209 // Steps 16-17 of 20.1.3.6: a `Symbol.toStringTag` STRING on the receiver
3210 // (own or inherited, data property or getter) replaces the builtin brand,
3211 // which is how a class advertises its own (`class C { get
3212 // [Symbol.toStringTag]() { return 'Cee' } }` → `[object Cee]`). The read
3213 // runs outside the host borrow so an accessor can be invoked.
3214 // A Proxy has no chain to probe: 20.1.3.6 step 15 is an unconditional
3215 // `Get(O, @@toStringTag)`, so the `get` trap decides. Probing first (as
3216 // the ordinary receiver does, to keep the read off objects that have no
3217 // tag) would always miss and brand every tagged proxy `[object Object]`.
3218 if let Some(s) = to_string_tag(recv)? {
3219 return Ok(with_host(|h| h.new_str(format!("[object {s}]"))));
3220 }
3221 return Ok(with_host(|h| h.new_str(object_tag(h, recv))));
3222 }
3223 // These thunks now live on the real `Object.prototype` object, i.e. on the
3224 // receiver's own chain — routing back through `call_method` would re-resolve
3225 // this very thunk and recurse.
3226 if ctor == "Object" && is_object_builtin_method(method) {
3227 return object_builtin_method(recv, method, args);
3228 }
3229 // `EventEmitter.prototype.<m>` mixed onto a receiver (express's `app`): run the
3230 // emitter method directly against `recv` (routing back through `call_method`
3231 // would re-resolve the mixed-in thunk and recurse).
3232 if ctor == "EventEmitter" {
3233 return crate::stdlib::events::instance_call(recv, method, args);
3234 }
3235 // Same recursion hazard for the exotics with a real prototype object: the
3236 // thunk now lives ON the receiver's prototype chain, so `call_method` would
3237 // re-resolve this very thunk. Dispatch straight to the native instance
3238 // implementation when the receiver is in fact an instance of `ctor`.
3239 if ctor == "Buffer" && crate::stdlib::native_tag(recv).as_deref() == Some("Buffer") {
3240 return crate::stdlib::buffer::instance_call(recv, method, &args);
3241 }
3242 // The shared typed-array methods now live on the `%TypedArray%.prototype`
3243 // intermediate, so their thunks are tagged `TypedArray`; `Uint8Array` still
3244 // appears for anything read directly off `Uint8Array.prototype`. Both
3245 // dispatch the same way, and both must bypass `call_method` or the thunk
3246 // would re-resolve itself off the receiver's chain and recurse.
3247 if ctor == "Uint8Array" || ctor == "TypedArray" {
3248 match crate::stdlib::native_tag(recv).as_deref() {
3249 Some("Buffer") => return crate::stdlib::buffer::instance_call(recv, method, &args),
3250 Some("TypedArray") => {
3251 return crate::stdlib::typedarray::instance_call(recv, method, &args)
3252 }
3253 _ => {}
3254 }
3255 }
3256 // `Array.prototype.<m>.call(arrayLike)` — every `Array.prototype` method is
3257 // GENERIC over `this` (23.1.3: each starts with `ToObject(this)` and
3258 // `LengthOfArrayLike`), which is what makes
3259 // `Array.prototype.slice.call(arguments)` the idiom it is. The receiver here
3260 // is not an Array, so `call_method` would report the method missing.
3261 if ctor == "Array" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Array) {
3262 return array_generic(recv, method, args);
3263 }
3264 // The general form of the two special cases above: a thunk taken off a native
3265 // constructor's real prototype, invoked with a receiver that IS an instance of
3266 // that constructor. Routing back through `call_method` would re-resolve this
3267 // very thunk off the receiver's own chain and recurse forever, which is why
3268 // each such prototype needed a hand-written bypass; now they all have one.
3269 // A SUBCLASS counts: `SecretKeyObject` reaches `KeyObject.prototype.equals`
3270 // through its chain, and requiring an exact tag match sent that call back
3271 // into `call_method`, which re-resolved this same thunk and recursed until
3272 // the stack overflowed.
3273 if let Some(tag) = crate::stdlib::native_tag(recv) {
3274 let mut c = Some(tag.as_str());
3275 while let Some(t) = c {
3276 if t == ctor {
3277 return crate::stdlib::instance_call(&tag, recv, method, args);
3278 }
3279 c = crate::stdlib::native_parent(t);
3280 }
3281 }
3282 // A BRANDED method reached with a receiver that has no such internal slot.
3283 // Every arm above dispatches a receiver that IS an instance, so arriving
3284 // here with one of these constructors means the brand check failed — the
3285 // spec's very first step for each of them (24.2.3.x reads `[[SetData]]`,
3286 // 24.1.3.x `[[MapData]]`, 27.2.5.4 `[[PromiseState]]`, 23.2.3.x
3287 // `ValidateTypedArray`). Falling through to ordinary dispatch reported
3288 // `union is not a function`, which says the method does not exist rather
3289 // than that the receiver is the wrong kind of object.
3290 // `Date.prototype`'s methods split in two: the ones that read the time value
3291 // (`ThisTimeValue`, 21.4.4.x) report `this is not a Date object.`, and the
3292 // rest take the ordinary branded form. Measured on node v26.8.1:
3293 // `Date.prototype.getTime.call({})` is the first, `.toISOString.call({})`
3294 // and `.setHours.call({})` the second.
3295 if ctor == "Date" && crate::stdlib::native_tag(recv).as_deref() != Some("Date") {
3296 const THIS_TIME_VALUE: &[&str] = &[
3297 "getTime",
3298 "valueOf",
3299 "getYear",
3300 "getFullYear",
3301 "getMonth",
3302 "getDate",
3303 "getDay",
3304 "getHours",
3305 "getMinutes",
3306 "getSeconds",
3307 "getMilliseconds",
3308 "getUTCFullYear",
3309 "getUTCMonth",
3310 "getUTCDate",
3311 "getUTCDay",
3312 "getUTCHours",
3313 "getUTCMinutes",
3314 "getUTCSeconds",
3315 "getUTCMilliseconds",
3316 "getTimezoneOffset",
3317 ];
3318 if THIS_TIME_VALUE.contains(&method) {
3319 return Err(host::type_error("this is not a Date object."));
3320 }
3321 // `toJSON` (21.4.4.37) is deliberately generic — it converts the
3322 // receiver and INVOKES `toISOString` on it, so it fails on the missing
3323 // method rather than on a brand.
3324 if method != "toJSON" {
3325 return Err(host::type_error(&format!(
3326 "Method Date.prototype.{method} called on incompatible receiver {}",
3327 no_side_effects_string(recv)
3328 )));
3329 }
3330 }
3331 // `%TypedArray%.prototype`'s methods split the same way: `ValidateTypedArray`
3332 // (23.2.4.4) reports `this is not a typed array.`, while the handful that
3333 // check the receiver at the call boundary take the branded form. Measured
3334 // over all 27 shared methods on node v26.8.1; `toString` is the one that is
3335 // genuinely generic (it is `Array.prototype.toString`) and never brands.
3336 if matches!(ctor, "TypedArray" | "Uint8Array")
3337 && !matches!(
3338 crate::stdlib::native_tag(recv).as_deref(),
3339 Some("TypedArray") | Some("Buffer")
3340 )
3341 {
3342 const BRANDED: &[&str] = &[
3343 "slice",
3344 "subarray",
3345 "join",
3346 "sort",
3347 "at",
3348 "toReversed",
3349 "toSorted",
3350 "toLocaleString",
3351 ];
3352 if BRANDED.contains(&method) {
3353 return Err(host::type_error(&format!(
3354 "Method %TypedArray%.prototype.{method} called on incompatible receiver {}",
3355 no_side_effects_string(recv)
3356 )));
3357 }
3358 // The four base64/hex methods brand themselves against `Uint8Array`
3359 // specifically — a WRONG view is as incompatible as a plain object, and
3360 // the generic guard here cannot tell those apart.
3361 if crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS.contains(&method) {
3362 return Err(host::type_error(&format!(
3363 "Method Uint8Array.prototype.{method} called on incompatible receiver {}",
3364 no_side_effects_string(recv)
3365 )));
3366 }
3367 if method != "toString" {
3368 return Err(host::type_error("this is not a typed array."));
3369 }
3370 }
3371 // `Function.prototype.call`/`apply`/`bind` with a callable PROXY as `this`
3372 // (`pf.call(null, 4, 5)`, reached through the target's chain). Handing
3373 // that back to `call_method` read `call` off the proxy again, which
3374 // resolved to this same thunk, and recursed until the stack overflowed and
3375 // aborted the process. The three are defined on the callee alone, so they
3376 // run here: the proxy's `apply` trap (or its target) gets the call.
3377 // `toString` recursed the same way.
3378 if ctor == "Function"
3379 && matches!(method, "call" | "apply" | "bind" | "toString")
3380 && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
3381 {
3382 let mut rest = args.into_iter();
3383 let this_arg = rest.next().unwrap_or(Value::Undef);
3384 match method {
3385 "call" => return host::invoke(recv, rest.collect(), Some(this_arg)),
3386 "apply" => {
3387 let list = match rest.next() {
3388 None | Some(Value::Undef) => Vec::new(),
3389 Some(v) if with_host(|h| h.is_null(&v)) => Vec::new(),
3390 Some(v) => create_list_from_array_like(&v)?,
3391 };
3392 return host::invoke(recv, list, Some(this_arg));
3393 }
3394 "bind" => {
3395 let target = recv.clone();
3396 let pre: Vec<Value> = rest.collect();
3397 return Ok(with_host(|h| {
3398 h.alloc(JsObj::BoundFunc {
3399 target,
3400 this: this_arg,
3401 args: pre,
3402 })
3403 }));
3404 }
3405 // A proxy has no source text; V8 prints the native form for it.
3406 _ => return Ok(with_host(|h| h.new_str("function () { [native code] }"))),
3407 }
3408 }
3409 // `Symbol.prototype`'s methods are branded, and the receiver that reaches
3410 // them is very often NOT a symbol: `Symbol.prototype` itself is an ordinary
3411 // object. Without this check `Symbol.prototype.toString()` re-entered the
3412 // generic string conversion, which looked `toString` up again and called it
3413 // again — an infinite recursion that overflowed the stack and ABORTED the
3414 // process, which no `try`/`catch` can see. Node throws a plain TypeError.
3415 // The wording is Symbol's own, not the "incompatible receiver" form the
3416 // collections use.
3417 if ctor == "Symbol" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Symbol) {
3418 // A symbol-KEYED method is named in brackets rather than after a dot:
3419 // node's wording is `Symbol.prototype [ @@toPrimitive ] requires …`.
3420 // That is the message `String(Symbol.prototype)` produces, since the
3421 // conversion reaches `@@toPrimitive` before it would reach `toString`.
3422 let named = match method.strip_prefix("@@") {
3423 Some(sym) => format!("Symbol.prototype [ @@{sym} ]"),
3424 None => format!("Symbol.prototype.{method}"),
3425 };
3426 return Err(host::type_error(&format!(
3427 "{named} requires that 'this' be a Symbol"
3428 )));
3429 }
3430 if let Some(label) = branded_method_label(ctor, recv) {
3431 return Err(host::type_error(&format!(
3432 "Method {label}.prototype.{method} called on incompatible receiver {}",
3433 no_side_effects_string(recv)
3434 )));
3435 }
3436 host::call_method(recv, method, args)
3437}
3438
3439/// The name a branded prototype method reports itself under when its receiver
3440/// fails the brand check, or `None` when `ctor`'s methods are generic over
3441/// `this` (every `Array.prototype` and `Object.prototype` method is) or the
3442/// receiver really is an instance.
3443///
3444fn branded_method_label(ctor: &str, recv: &Value) -> Option<&'static str> {
3445 let kind = with_host(|h| h.kind_of(recv));
3446 // `weak` is part of the brand: a `WeakSet` has `[[WeakSetData]]`, not
3447 // `[[SetData]]`, so `Set.prototype.has.call(new WeakSet())` is incompatible
3448 // even though both are `JsObj::Set` here.
3449 let weak = peek(recv, |o| match o {
3450 JsObj::Set { weak, .. } | JsObj::Map { weak, .. } => Some(*weak),
3451 _ => None,
3452 })
3453 .unwrap_or(false);
3454 let ok = match ctor {
3455 "Set" => kind == Some(ObjKind::Set) && !weak,
3456 "WeakSet" => kind == Some(ObjKind::Set) && weak,
3457 "Map" => kind == Some(ObjKind::Map) && !weak,
3458 "WeakMap" => kind == Some(ObjKind::Map) && weak,
3459 "Promise" => kind == Some(ObjKind::Promise),
3460 _ => return None,
3461 };
3462 if ok {
3463 return None;
3464 }
3465 Some(match ctor {
3466 "Set" => "Set",
3467 "WeakSet" => "WeakSet",
3468 "Map" => "Map",
3469 "WeakMap" => "WeakMap",
3470 _ => "Promise",
3471 })
3472}
3473
3474/// V8's `Object::NoSideEffectsToString`, the rendering an engine-thrown message
3475/// uses for a value it must not run user code on. Measured on node v26.8.1
3476/// through `Map.prototype.get.call(x)`:
3477///
3478/// ```text
3479/// 5 / 'str' / true / null / undefined / 9n the value's own ToString
3480/// Symbol('s') Symbol(s)
3481/// function f(){} its source text
3482/// new Error('e') Error: e
3483/// {} / new (class A {}) #<Object> / #<A>
3484/// new Map() / Promise.resolve() #<Map> / #<Promise>
3485/// [] / new Date() / /re/ / new Uint8Array() [object Array] / [object Date] / …
3486/// { toString() {} } / Object.create(null) [object Object]
3487/// ```
3488///
3489/// The split is one test: a receiver whose `toString` is still
3490/// `Object.prototype.toString` prints `#<Constructor>`, and any other receiver
3491/// prints what the BUILTIN brand would be — V8 never calls the user's method,
3492/// which is why an object with its own `toString` prints `[object Object]` and
3493/// not what that method returns.
3494fn no_side_effects_string(recv: &Value) -> String {
3495 if with_host(|h| host::is_primitive(h, recv)) || with_host(|h| host::is_callable(h, recv)) {
3496 return with_host(|h| h.str_of(recv));
3497 }
3498 if let Some(s) = with_host(|h| h.error_to_string(recv)) {
3499 return s;
3500 }
3501 // `native_tag` re-enters the host, so it is read BEFORE the borrow below
3502 // rather than inside it.
3503 let native = crate::stdlib::native_tag(recv).is_some();
3504 let brands_itself = with_host(|h| {
3505 // `Object.prototype.toString` reaches every object as a thunk on the
3506 // real prototype object, so its presence proves nothing; only a
3507 // toString the receiver's chain OVERRIDES it with counts.
3508 let overridden = host::lookup_chain(h, recv, "toString")
3509 .map(|f| !matches!(h.get(&f), Some(JsObj::Builtin(n)) if n == "@proto:Object:toString"))
3510 .unwrap_or(false);
3511 native
3512 || overridden
3513 || h.has_null_proto(recv)
3514 || !matches!(
3515 h.kind_of(recv),
3516 Some(ObjKind::Object)
3517 | Some(ObjKind::Map)
3518 | Some(ObjKind::Set)
3519 | Some(ObjKind::Promise)
3520 )
3521 });
3522 if brands_itself {
3523 return with_host(|h| object_tag(h, recv));
3524 }
3525 let ctor = get_property(recv, "constructor")
3526 .ok()
3527 .map(|c| with_host(|h| h.callable_name(&c)))
3528 .filter(|n| !n.is_empty())
3529 .unwrap_or_else(|| "Object".to_string());
3530 format!("#<{ctor}>")
3531}
3532
3533/// The value of `v[Symbol.toStringTag]` for a builtin that genuinely carries
3534/// one, or `None` when reading that symbol must yield `undefined`.
3535///
3536/// Every builtin brand is already computed in exactly one place (`object_tag`),
3537/// so this reuses it and subtracts the legacy builtins, which brand for
3538/// `Object.prototype.toString` but expose no `Symbol.toStringTag` property.
3539/// The subtracted list is measured against node v26.7.0, not assumed: `[]`,
3540/// `function(){}`, `{}`, `new Date()`, `/x/` and `new Error()` all read
3541/// `undefined`, while `Map`/`Set`/`Promise`/typed arrays/`ArrayBuffer`/
3542/// `DataView`/`WeakRef`/`FinalizationRegistry`/`BigInt`/`Symbol`/generators/
3543/// async+generator functions/`Math`/`JSON`/`Reflect`/`URL`/`URLSearchParams`/
3544/// `TextEncoder`/`TextDecoder` all read their brand.
3545pub(crate) fn well_known_tag(h: &host::JsHost, v: &Value) -> Option<String> {
3546 // A primitive never carries the symbol except a BigInt/Symbol wrapper, both
3547 // of which `object_tag` already brands.
3548 let tag = object_brand(h, v);
3549 const NO_TAG: &[&str] = &[
3550 "Undefined",
3551 "Null",
3552 "Boolean",
3553 "Number",
3554 "String",
3555 "Array",
3556 "Function",
3557 "Object",
3558 "Date",
3559 "RegExp",
3560 "Error",
3561 ];
3562 if NO_TAG.contains(&tag.as_str()) {
3563 return None;
3564 }
3565 Some(tag)
3566}
3567
3568/// The constructor name of the nearest intrinsic prototype on `v`'s chain that
3569/// carries an own `Symbol.toStringTag`, if any.
3570fn chain_tag_ctor(h: &host::JsHost, v: &Value) -> Option<String> {
3571 let mut cur = h.proto_of(v);
3572 for _ in 0..100 {
3573 let p = cur?;
3574 if h.is_null(&p) {
3575 return None;
3576 }
3577 let name = match h.get(&p) {
3578 Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
3579 _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
3580 }
3581 // A CLASS prototype is not linked to the builtin its class extends —
3582 // the relationship lives on the class value — so the walk crosses over
3583 // there, or `Object.create(D.prototype)` for `class D extends Map`
3584 // finds nothing.
3585 .or_else(|| {
3586 h.class_owning_proto(&p)
3587 .and_then(|c| h.class_builtin_ancestor(&c))
3588 .map(|b| h.callable_name(&b))
3589 .filter(|n| !n.is_empty())
3590 });
3591 if let Some(n) = name {
3592 if intrinsic_proto_members(&format!("{n}.prototype"))
3593 .is_some_and(|ms| ms.contains(&"@@toStringTag"))
3594 {
3595 return Some(n);
3596 }
3597 }
3598 cur = h.proto_of(&p);
3599 }
3600 None
3601}
3602
3603/// The `Object.prototype.toString` brand tag for `v` (`[object Array]` etc.).
3604/// Every builtin exotic object reports its own brand, which is how packages
3605/// type-test values they did not construct (`toString.call(x) ===
3606/// '[object Uint8Array]'`). A `Buffer` reports `Uint8Array` because in Node it
3607/// IS a `Uint8Array` subclass and inherits that `Symbol.toStringTag`.
3608pub(crate) fn object_tag(h: &host::JsHost, v: &Value) -> String {
3609 format!("[object {}]", object_brand(h, v))
3610}
3611
3612/// The bare brand name behind `Object.prototype.toString` (`Array`, `Uint8Array`
3613/// …), without the `[object …]` wrapper. Split out so the brand and the
3614/// `Symbol.toStringTag` property read cannot disagree about what a value is.
3615/// Whether `v` is a function's `arguments` object.
3616///
3617/// Backed by an Array so indices, `length`, spread and `for-of` all work, but
3618/// marked so it does not pass for one: node's is an exotic, and `isArray`, the
3619/// brand and `util.types.isArgumentsObject` all have to tell them apart.
3620pub fn is_arguments(v: &Value) -> bool {
3621 with_host(|h| is_arguments_h(h, v))
3622}
3623
3624/// `is_arguments` for a caller that already holds the host borrow — `object_brand`
3625/// runs under one, and re-entering through `with_host` aborts the process.
3626pub fn is_arguments_h(h: &host::JsHost, v: &Value) -> bool {
3627 h.fn_prop(v, "@@arguments").is_some()
3628}
3629
3630fn object_brand(h: &host::JsHost, v: &Value) -> String {
3631 // A `<C>.prototype` this host built as a real object is an ORDINARY object:
3632 // it holds no instance slot, so only the branded few report anything but
3633 // `[object Object]`. Checked before the match because those prototypes are
3634 // plain `JsObj::Object`s and would otherwise be branded by whatever their
3635 // own properties happen to look like — `TypeError.prototype` has `name` and
3636 // `message`, which read as an Error instance.
3637 if let Some(ctor) = h.intrinsic_proto_ctor(v) {
3638 return if BRANDED_PROTOS.contains(&ctor) {
3639 ctor.to_string()
3640 } else {
3641 "Object".to_string()
3642 };
3643 }
3644 let tag: String = match v {
3645 Value::Undef => "Undefined".into(),
3646 Value::Bool(_) => "Boolean".into(),
3647 Value::Int(_) | Value::Float(_) => "Number".into(),
3648 Value::Str(_) => "String".into(),
3649 Value::Obj(_) => match h.get(v) {
3650 Some(JsObj::Null) => "Null".into(),
3651 Some(JsObj::Str(_)) => "String".into(),
3652 Some(JsObj::Array(_)) if is_arguments_h(h, v) => "Arguments".into(),
3653 Some(JsObj::Array(_)) => "Array".into(),
3654 // A lazy iterator helper brands as node does.
3655 Some(JsObj::Object(p))
3656 if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("IteratorHelper") =>
3657 {
3658 "Iterator Helper".into()
3659 }
3660 // A `DOMException` brands by its class, not as a plain `Error`.
3661 Some(JsObj::Object(p)) if p.contains_key("@@domName") => "DOMException".into(),
3662 // 20.1.3.6 steps 5-8 brand a wrapper by its internal slot, so
3663 // `Object.prototype.toString.call(new Number(1))` is
3664 // `[object Number]` rather than `[object Object]`.
3665 Some(JsObj::Object(p)) if p.contains_key("@@primitive") => match p["@@primitive"] {
3666 Value::Bool(_) => "Boolean".into(),
3667 Value::Int(_) | Value::Float(_) => "Number".into(),
3668 _ => "String".into(),
3669 },
3670 // 20.1.3.6 step 3 brands by `IsArray`, which follows a Proxy to its
3671 // `[[ProxyTarget]]` — `Object.prototype.toString.call(new Proxy([],
3672 // {}))` is `'[object Array]'`. Everything else about a proxy brands
3673 // as a plain Object (a `Symbol.toStringTag` read through the `get`
3674 // trap is handled by the caller, before this).
3675 Some(JsObj::Proxy { target, .. }) => {
3676 let mut cur = target;
3677 for _ in 0..100 {
3678 match h.get(cur) {
3679 Some(JsObj::Proxy { target: t, .. }) => cur = t,
3680 _ => break,
3681 }
3682 }
3683 match h.get(cur) {
3684 Some(JsObj::Array(_)) => "Array".into(),
3685 _ => "Object".into(),
3686 }
3687 }
3688 // `function*` / `async function` / `async function*` carry their own
3689 // `Symbol.toStringTag` in V8 (27.3.3.2, 27.7.3.2, 27.4.3.2).
3690 Some(JsObj::Func(f)) => match h.funcs.get(f.def_id) {
3691 Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction".into(),
3692 Some(d) if d.is_generator => "GeneratorFunction".into(),
3693 Some(d) if d.is_async => "AsyncFunction".into(),
3694 _ => "Function".into(),
3695 },
3696 // `Math`/`JSON`/`Reflect` are namespace OBJECTS, not callables, and
3697 // brand by name (21.3.1.9, 25.5.3, 28.1.14).
3698 Some(JsObj::Builtin(n)) if matches!(n.as_str(), "Math" | "JSON" | "Reflect") => {
3699 n.clone()
3700 }
3701 // A `<Ctor>.prototype` object brands as the constructor it belongs
3702 // to — `Object.prototype.toString.call(Set.prototype)` is
3703 // `[object Set]` — and a `require()`d module namespace is a plain
3704 // object. Neither is a function, so neither brands as one.
3705 Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
3706 match n.strip_suffix(".prototype") {
3707 Some(ctor) if BRANDED_PROTOS.contains(&ctor) => ctor.to_string(),
3708 _ => "Object".into(),
3709 }
3710 }
3711 Some(JsObj::Class(_))
3712 | Some(JsObj::Builtin(_))
3713 | Some(JsObj::BoundFunc { .. })
3714 | Some(JsObj::BoundMethod { .. }) => "Function".into(),
3715 // A suspended generator object is `[object Generator]`; an async one
3716 // `[object AsyncGenerator]`.
3717 Some(JsObj::Generator { .. }) if h.is_async_gen_val(v) => "AsyncGenerator".into(),
3718 Some(JsObj::Generator { .. }) => "Generator".into(),
3719 Some(JsObj::RegExp(_)) => "RegExp".into(),
3720 Some(JsObj::Map { weak, .. }) => if *weak { "WeakMap" } else { "Map" }.into(),
3721 Some(JsObj::Set { weak, .. }) => if *weak { "WeakSet" } else { "Set" }.into(),
3722 Some(JsObj::Promise { .. }) => "Promise".into(),
3723 Some(JsObj::Symbol { .. }) => "Symbol".into(),
3724 Some(JsObj::BigInt(_)) => "BigInt".into(),
3725 // Native-tagged instances brand by their tag; a typed array brands by
3726 // its element kind (`@@kind`), and every Error subclass is `Error`.
3727 Some(JsObj::Object(p)) => match p.get("@@native").map(|t| h.str_of(t)).as_deref() {
3728 Some("TypedArray") => p
3729 .get("@@kind")
3730 .map(|k| h.str_of(k))
3731 .unwrap_or_else(|| "Uint8Array".into()),
3732 Some("Buffer") => "Uint8Array".into(),
3733 // Every native class that really carries a `Symbol.toStringTag`
3734 // in Node brands by its own name. Verified against node v26:
3735 // `Object.prototype.toString.call(new WeakRef({}))` is
3736 // `[object WeakRef]`. The rest of the `@@native` tags
3737 // (`EventEmitter`, `Server`, `Hash`, `Readable`, …) are plain
3738 // classes with NO tag, so they stay `[object Object]` — listing
3739 // them here would invent a brand Node does not have.
3740 Some(
3741 t @ ("ArrayBuffer"
3742 | "DataView"
3743 | "Date"
3744 | "WeakRef"
3745 | "FinalizationRegistry"
3746 | "TextEncoder"
3747 | "TextDecoder"
3748 | "URL"
3749 | "URLSearchParams"),
3750 ) => t.into(),
3751 _ if has_error_data(h, v) => "Error".into(),
3752 _ => "Object".into(),
3753 },
3754 _ => "Object".into(),
3755 },
3756 // node-js only produces the Value variants above; fusevm's shell-oriented
3757 // variants never arise here.
3758 _ => "Object".into(),
3759 };
3760 // Nothing about the value itself brands it. An ordinary object whose CHAIN
3761 // reaches an intrinsic prototype carrying an own `Symbol.toStringTag`
3762 // borrows that one: 20.1.3.6 step 15 is a `Get`, which walks.
3763 // `Object.prototype.toString.call(Object.create(Map.prototype))` is
3764 // `[object Map]` and was `[object Object]`.
3765 //
3766 // Only as a FALLBACK, and only for the prototypes that REALLY carry the
3767 // symbol. A typed array reaches `%TypedArray%.prototype`, whose tag is an
3768 // ACCESSOR returning the specific kind, so consulting the chain FIRST
3769 // branded every view `[object TypedArray]` instead of `[object Uint8Array]`
3770 // — three records caught it. `Error.prototype` carries no tag at all, so
3771 // inheriting from it borrows nothing.
3772 if tag == "Object" && !has_error_data(h, v) {
3773 if let Some(ctor) = chain_tag_ctor(h, v) {
3774 return ctor;
3775 }
3776 }
3777 tag
3778}
3779
3780fn b_setattr(vm: &mut VM, _: u8) -> Value {
3781 let val = vm.pop();
3782 let name = sval(&vm.pop());
3783 let recv = vm.pop();
3784 if let Err(e) = set_property(&recv, &name, val.clone()) {
3785 return abort(vm, e);
3786 }
3787 val
3788}
3789
3790/// `NAMED_EVAL` — SetFunctionName (10.2.9) for a function whose name is only
3791/// known at run time, i.e. one defined under a COMPUTED key: `{ [k]: () => {} }`,
3792/// `class C { static [k] = function(){} }`.
3793///
3794/// The compiler emits this ONLY where the grammar says NamedEvaluation applies
3795/// (`IsAnonymousFunctionDefinition` is a syntactic predicate, not a runtime one:
3796/// `{ m: someAlreadyAnonymousFn }` must NOT be renamed), so the name is set
3797/// unconditionally here.
3798///
3799/// A symbol key becomes `[description]` per step 2 of SetFunctionName; `kind`
3800/// contributes the accessor prefix, so `{ get [k](){} }` is `get <key>`.
3801fn b_named_eval(vm: &mut VM, _: u8) -> Value {
3802 let func = vm.pop();
3803 let kind = vm.pop().to_int();
3804 let key = vm.pop();
3805 let key = sval(&key);
3806 // `@@sym:<id>` / `@@iterator` — an internal symbol key. Step 2: an empty
3807 // description gives the empty name, not `[undefined]`.
3808 let base = match with_host(|h| h.symbol_of_key(&key)) {
3809 Some(sym) => match with_host(|h| h.get(&sym).cloned()) {
3810 Some(JsObj::Symbol {
3811 desc: Some(desc), ..
3812 }) => format!("[{desc}]"),
3813 _ => String::new(),
3814 },
3815 None => key,
3816 };
3817 let name = match kind {
3818 host::member::GET => format!("get {base}"),
3819 host::member::SET => format!("set {base}"),
3820 _ => base,
3821 };
3822 with_host(|h| {
3823 let s = h.new_str(name);
3824 h.set_fn_prop(&func, "name", s);
3825 });
3826 func
3827}
3828
3829/// `[[Set]]` reachable from `crate::proxy`'s no-trap forward, which has to land
3830/// on the same path a plain `o.k = v` takes.
3831pub fn set_property_pub(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3832 set_property(recv, name, val)
3833}
3834
3835/// An object's OWN property as `(value, writable, configurable, is_accessor)`,
3836/// or `None` when it has none. Reads through a Proxy's
3837/// `getOwnPropertyDescriptor` trap, so it answers for any object.
3838pub fn own_prop_facts(obj: &Value, key: &str) -> Option<(Value, bool, bool, bool)> {
3839 let k = with_host(|h| h.new_str(key.to_string()));
3840 let d = own_descriptor_pub(obj, k).ok()?;
3841 if matches!(d, Value::Undef) {
3842 return None;
3843 }
3844 let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
3845 // Each read is hoisted out of the `with_host` borrow: `get_property` takes
3846 // the host itself, so reading inside the closure double-borrows.
3847 let value = field("value");
3848 let writable = field("writable");
3849 let configurable = field("configurable");
3850 let truthy = |v: &Value| with_host(|h| h.truthy(v));
3851 let is_accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
3852 Some((value, truthy(&writable), truthy(&configurable), is_accessor))
3853}
3854
3855/// `OrdinarySetWithOwnDescriptor` (10.1.9.2) with a receiver distinct from the
3856/// object the lookup started on — what `Reflect.set(t, k, v, receiver)` and a
3857/// proxy `set` trap forwarding to it both need.
3858///
3859/// The distinction that matters: an accessor found on `target`'s chain RUNS,
3860/// with `receiver` as `this`; a data property does not write to `target` at all
3861/// but is CREATED on `receiver` through its `[[DefineOwnProperty]]`. Routing
3862/// that second case back through `[[Set]]` made a proxy receiver re-enter its
3863/// own `set` trap forever — the trap body `Reflect.set(t, k, v, recv)` is the
3864/// documented way to forward a write, so the recursion hit every faithful
3865/// handler.
3866pub fn set_with_receiver(
3867 target: &Value,
3868 key: &str,
3869 val: Value,
3870 receiver: &Value,
3871) -> Result<bool, String> {
3872 // A proxy target answers through its own trap, which re-enters here with
3873 // whatever receiver the handler passes on.
3874 if crate::proxy::parts(target).is_some() {
3875 return crate::proxy::set(target, key, &val, receiver);
3876 }
3877 // An accessor anywhere on the target's chain wins, and sees `receiver`.
3878 if let Some((_, setter)) = with_host(|h| host::lookup_accessor(h, target, key)) {
3879 return match setter {
3880 Some(s) => {
3881 host::invoke(&s, vec![val], Some(receiver.clone()))?;
3882 Ok(true)
3883 }
3884 // A getter with no setter refuses the write rather than shadowing it.
3885 None => Ok(false),
3886 };
3887 }
3888 if !with_host(|h| h.can_write_prop(target, key)) {
3889 return Ok(false);
3890 }
3891 // Steps 3.b-3.d: only an object can receive the property, and its OWN
3892 // property decides — an accessor or a read-only slot refuses, and every
3893 // other case defines a plain data property.
3894 //
3895 // `is_object_like`, not a shape test: a string, a symbol and a bigint are
3896 // PRIMITIVES that ride as `Value::Obj` handles here, so the shape check
3897 // passed them through to `defineProperty`, which then threw `called on
3898 // non-object` where 10.1.9.2 step 3.b simply reports `false`.
3899 if !with_host(|h| is_object_like(h, receiver)) {
3900 return Ok(false);
3901 }
3902 if let Some((_, writable, _, is_accessor)) = own_prop_facts(receiver, key) {
3903 if is_accessor || !writable {
3904 return Ok(false);
3905 }
3906 }
3907 // Steps 3.d.iii and 3.e both DEFINE, they do not assign: a setter inherited
3908 // by the receiver must not run, and a proxy receiver must reach its
3909 // `defineProperty` trap rather than its `set` trap.
3910 let desc = with_host(|h| {
3911 let mut m: IndexMap<String, Value> = IndexMap::new();
3912 m.insert("value".into(), val);
3913 m.insert("writable".into(), Value::Bool(true));
3914 m.insert("enumerable".into(), Value::Bool(true));
3915 m.insert("configurable".into(), Value::Bool(true));
3916 h.new_object(m)
3917 });
3918 if crate::proxy::parts(receiver).is_some() {
3919 return crate::proxy::define_property(receiver, key, &desc);
3920 }
3921 let k = with_host(|h| h.new_str(key.to_string()));
3922 define_property_pub(receiver, k, desc)?;
3923 Ok(true)
3924}
3925
3926/// Whether the first argument is a PRIMITIVE — including the three that ride as
3927/// heap handles, which a shape test misses.
3928fn is_primitive_arg(args: &[Value]) -> bool {
3929 let v = arg0(args);
3930 with_host(|h| host::is_primitive(h, &v))
3931}
3932
3933/// The `TypeError` a refused write raises in strict code, worded as V8 does.
3934///
3935/// Adding a key to a non-extensible object reports differently from assigning
3936/// to a read-only one, and the object is named by its brand — `#<Object>` for a
3937/// plain object, `[object Array]` for an array.
3938fn write_refused(recv: &Value, name: &str) -> String {
3939 let extensible = with_host(|h| h.is_extensible(recv));
3940 // Which of the two messages applies turns on whether the key already
3941 // EXISTS. Every shape that keeps its own properties in the fn-prop side
3942 // table answered a blanket `true` here, so adding a key to a frozen
3943 // function reported "read only" where node reports "not extensible".
3944 let has_own = with_host(|h| match h.get(recv) {
3945 Some(JsObj::Object(p)) => p.contains_key(name),
3946 Some(JsObj::Array(items)) => {
3947 name.parse::<usize>()
3948 .map(|i| i < items.len())
3949 .unwrap_or(false)
3950 || h.fn_prop(recv, name).is_some()
3951 }
3952 Some(JsObj::RegExp(_)) => name == "lastIndex" || h.fn_prop(recv, name).is_some(),
3953 _ => h.fn_prop(recv, name).is_some(),
3954 });
3955 if !extensible && !has_own {
3956 return host::type_error(&format!(
3957 "Cannot add property {name}, object is not extensible"
3958 ));
3959 }
3960 // The receiver renders the way every other brand-check message renders one
3961 // — `#<Object>`, `[object Array]`, `[object RegExp]`, `#<Map>`, `#<C>` for a
3962 // class instance, `Error: m` for an error. Only Array was special-cased, so
3963 // every other exotic reported `#<Object>`.
3964 host::type_error(&format!(
3965 "Cannot assign to read only property '{name}' of object '{}'",
3966 no_side_effects_string(recv)
3967 ))
3968}
3969
3970fn set_property(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3971 // 6.2.5.6 `PutValue` begins with `RequireObjectCoercible`: writing any
3972 // property of `undefined` or `null` throws, naming the key. Every such
3973 // write was silently discarded, so `u.x = 1` — the mirror of the single
3974 // most common runtime fault in JS, which the READ side already reports —
3975 // looked like it had succeeded.
3976 if with_host(|h| h.is_nullish(recv)) {
3977 return Err(host::type_error(&format!(
3978 "Cannot set properties of {} (setting '{name}')",
3979 with_host(|h| h.str_of(recv))
3980 )));
3981 }
3982 // A write to a PRIMITIVE receiver has no target — `ToObject` makes a
3983 // throwaway wrapper — so it is discarded in sloppy code and throws in
3984 // strict (10.1.9.2 / 6.2.5.6 again). The refusal was silent in both.
3985 // `is_primitive` rather than a shape test: a string, a symbol and a bigint
3986 // ride as `Value::Obj` handles in this host, so a check for a non-`Obj`
3987 // value caught only numbers and booleans.
3988 if with_host(|h| host::is_primitive(h, recv)) && with_host(|h| h.current_strict()) {
3989 return Err(host::type_error(&format!(
3990 "Cannot create property '{name}' on {} '{}'",
3991 with_host(|h| h.type_of(recv)),
3992 with_host(|h| h.str_of(recv))
3993 )));
3994 }
3995 // `[[PrivateSet]]` (7.3.32) refuses a receiver that carries no such private
3996 // element. The class's own field initializers install theirs directly
3997 // (`host::init_one_field`), so a declaration never reaches this check.
3998 if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
3999 return Err(private_brand_message(name, true));
4000 }
4001 // `[[Set]]` on a Proxy: the handler's `set` trap, or a forward to the target.
4002 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
4003 // A `set` trap that returns falsish REFUSED the write: silent in sloppy
4004 // code, a TypeError in strict, exactly as an ordinary refused write is.
4005 if crate::proxy::set(recv, name, &val, recv)? {
4006 return Ok(());
4007 }
4008 if with_host(|h| h.current_strict()) {
4009 return Err(host::type_error(&format!(
4010 "'set' on proxy: trap returned falsish for property '{name}'"
4011 )));
4012 }
4013 return Ok(());
4014 }
4015 // `globalThis.x = 1` creates a real global binding, so the bare `x` reads it
4016 // back. Writing only the own property left the two views disagreeing:
4017 // `globalThis.zz` was 7 while `zz` was still a `ReferenceError`.
4018 if with_host(|h| h.is_global_object(recv)) && !name.starts_with("@@") {
4019 with_host(|h| h.set_name(name, val.clone()));
4020 }
4021 // `obj.__proto__ = p` re-links the prototype — but only for the two values
4022 // the Annex B setter accepts, an Object or `null`. Everything else is a
4023 // silent no-op in Node (`o.__proto__ = 5` leaves `Object.getPrototypeOf(o)`
4024 // untouched and creates no own key), and a null-prototype object inherits
4025 // no such setter at all, so there the assignment is an ORDINARY own
4026 // property write. Re-linking unconditionally made `o.__proto__ = 5` set the
4027 // prototype to the number 5.
4028 if name == "__proto__" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
4029 if with_host(|h| h.has_null_proto(recv)) {
4030 // falls through to the ordinary own-property write below
4031 } else {
4032 let assignable =
4033 with_host(|h| h.is_null(&val) || matches!(h.kind_of(&val), Some(ObjKind::Object)));
4034 if assignable {
4035 // The `__proto__` setter runs `[[SetPrototypeOf]]`, which a
4036 // NON-EXTENSIBLE object refuses — and unlike an ordinary
4037 // refused write, the setter throws in sloppy code too. It was
4038 // rewriting the link of a frozen object.
4039 if would_cycle(recv, &val) {
4040 return Err(host::type_error("Cyclic __proto__ value"));
4041 }
4042 if !with_host(|h| h.is_extensible(recv)) && !same_prototype(recv, &val) {
4043 return Err(host::type_error(&format!(
4044 "{} is not extensible",
4045 no_side_effects_string(recv)
4046 )));
4047 }
4048 with_host(|h| h.set_proto(recv, val));
4049 }
4050 return Ok(());
4051 }
4052 }
4053 // Every environment value is a STRING. `process.env.PORT = 8080` stores
4054 // "8080", so `process.env.PORT + 1` concatenates the way it does in a real
4055 // process; storing the number made it add instead.
4056 if !name.starts_with("@@")
4057 && with_host(
4058 |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4059 )
4060 {
4061 let text = with_host(|h| h.str_of(&val));
4062 // Write THROUGH to the real environment as well. `process.env` is not a
4063 // private map: node applies the change to the process, so a child
4064 // spawned afterwards inherits it. Keeping it only in the JS object meant
4065 // `process.env.NODE_ENV = 'production'` was invisible to every
4066 // `spawnSync`/`execSync` that followed.
4067 std::env::set_var(name, &text);
4068 let sv = with_host(|h| h.new_str(text));
4069 with_host(|h| {
4070 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4071 p.insert(name.to_string(), sv);
4072 }
4073 });
4074 return Ok(());
4075 }
4076 // Assigning `e.stack` wins permanently: drop the not-yet-formatted marker so
4077 // no later read re-derives a header over the top of the assigned value.
4078 if name == "stack" {
4079 with_host(|h| {
4080 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4081 p.shift_remove("@@stackRaw");
4082 }
4083 });
4084 }
4085 // An inherited/own setter accessor intercepts the write. This is checked
4086 // BEFORE the writable test because 10.1.9.2 branches on the descriptor
4087 // kind first: `writable` is a data-property attribute and means nothing on
4088 // an accessor, where the setter alone decides. Testing it first meant an
4089 // accessor defined through `Object.defineProperty` — which leaves
4090 // `writable` false, having no such field — silently swallowed every write
4091 // instead of calling its setter, so the standard clone idiom
4092 // `Object.create(proto, Object.getOwnPropertyDescriptors(src))` produced an
4093 // object whose setters did nothing. An accessor from an object literal
4094 // carries all-true attributes, which is why only the former broke.
4095 if let Some((getter, setter)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
4096 if let Some(setter) = setter {
4097 let _ = host::invoke(&setter, vec![val], Some(recv.clone()));
4098 return Ok(());
4099 }
4100 // Only a getter: the write is refused — silent in sloppy mode, a
4101 // TypeError in strict code. The `return` above matters, since a
4102 // successful setter call must not fall into this.
4103 let _ = getter;
4104 if with_host(|h| h.current_strict()) {
4105 return Err(host::type_error(&format!(
4106 "Cannot set property {name} of #<Object> which has only a getter"
4107 )));
4108 }
4109 return Ok(());
4110 }
4111 // A non-writable property, or a new key on a non-extensible object, refuses
4112 // the write. In SLOPPY mode that is silent; in strict code it is a
4113 // TypeError, and the ASSIGNMENT SITE decides which — not the object. Every
4114 // refusal used to be silent, so `'use strict'` did not catch a write to a
4115 // frozen object, which is most of the reason to freeze one.
4116 if !with_host(|h| h.can_write_prop(recv, name)) {
4117 if with_host(|h| h.current_strict()) {
4118 return Err(write_refused(recv, name));
4119 }
4120 return Ok(());
4121 }
4122 // Writing `name`/`prototype`/statics on a function value.
4123 if matches!(
4124 with_host(|h| h.kind_of(recv)),
4125 Some(ObjKind::Func) | Some(ObjKind::Class)
4126 ) {
4127 with_host(|h| h.set_fn_prop(recv, name, val));
4128 return Ok(());
4129 }
4130 // Writing a static onto a builtin namespace/ctor (`Error.prepareStackTrace`).
4131 // Each bare reference is a fresh `Builtin` handle, so route to the stable
4132 // per-namespace side table rather than the per-index `fn_props`.
4133 if let Some(ns) = peek(recv, |o| match o {
4134 JsObj::Builtin(ns) => Some(ns.clone()),
4135 _ => None,
4136 }) {
4137 // `process.exitCode` is an accessor in Node, not a data property: the
4138 // setter validates and stores the code the process will finally exit
4139 // with. Landing it in the generic static table made it a write-only
4140 // decoration — `process.exitCode = 3` read back as 3 and the process
4141 // still exited 0.
4142 if ns == "process" && name == "exitCode" {
4143 return crate::stdlib::process::set_exit_code(&val);
4144 }
4145 with_host(|h| h.set_builtin_static(&ns, name, val));
4146 return Ok(());
4147 }
4148 // A write onto a REAL intrinsic prototype object (`Object.prototype`,
4149 // `String.prototype`, `TypeError.prototype`) is mirrored into the
4150 // per-namespace side table as well as the object's own map. Instances are
4151 // not linked to these objects by `proto_of` — the chain walk never reaches
4152 // them — so the mirror is what makes `String.prototype.pad = f` visible as
4153 // `"x".pad`. The own-map write below still happens, so reading the
4154 // prototype itself and enumerating it keep working unchanged.
4155 if let Some(ns) = with_host(|h| {
4156 h.intrinsic_proto_ctor(recv)
4157 .map(str::to_string)
4158 .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4159 }) {
4160 with_host(|h| h.set_builtin_static(&format!("{ns}.prototype"), name, val.clone()));
4161 }
4162 // `re.lastIndex = n` on a RegExp advances/resets its match cursor. The
4163 // writability check above already refused it on a FROZEN regexp, which it
4164 // could only do once `integrity_keys` learned that `lastIndex` is an own
4165 // property.
4166 if name == "lastIndex" {
4167 if let Some(n) = with_host(|h| match h.get(recv) {
4168 Some(JsObj::RegExp(_)) => Some(h.to_number(&val)),
4169 _ => None,
4170 }) {
4171 with_host(|h| {
4172 if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
4173 r.last_index = if n.is_finite() && n >= 0.0 {
4174 crate::utf16::U16Index::new(n as usize)
4175 } else {
4176 crate::utf16::U16Index::ZERO
4177 };
4178 }
4179 });
4180 return Ok(());
4181 }
4182 }
4183 // An `arguments` object is an ORDINARY object with a `length` data property,
4184 // not an array: a write PAST the end adds an index and leaves `length`
4185 // alone. The array backing grew it instead, so `f(1)` followed by
4186 // `arguments[1] = 9` reported `arguments.length` as 2.
4187 if let Ok(i) = name.parse::<usize>() {
4188 if is_arguments(recv) && i >= array_len(recv) {
4189 with_host(|h| h.set_fn_prop(recv, name, val));
4190 return Ok(());
4191 }
4192 }
4193 // Typed-array element write (`ta[i] = v`): coerce + store into `@@elems`.
4194 if !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
4195 let is_ta = crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray");
4196 if is_ta && crate::stdlib::typedarray::elem_set(recv, name, &val)? {
4197 return Ok(());
4198 }
4199 // An index write to a view over a DETACHED buffer is DROPPED. Falling
4200 // through would store it as an ordinary own property, which then showed
4201 // up in `getOwnPropertyDescriptor` over a buffer with no bytes.
4202 if is_ta && crate::stdlib::typedarray::view_detached(recv) {
4203 return Ok(());
4204 }
4205 // `buf[i] = n` writes through to the Buffer's hidden byte array.
4206 if crate::stdlib::buffer::byte_set(recv, name, &val) {
4207 return Ok(());
4208 }
4209 }
4210 // Any own property on an exotic with no property map of its own. This sits
4211 // BELOW the exotic-specific writes above, so a RegExp's `lastIndex` still
4212 // moves its match cursor rather than being shadowed by a side-table entry.
4213 if uses_side_table(recv) {
4214 with_host(|h| h.set_fn_prop(recv, name, val));
4215 return Ok(());
4216 }
4217 // An arbitrary own prop on an array (e.g. exec-result `.index`/`.input`).
4218 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array)
4219 && name != "length"
4220 && name.parse::<usize>().is_err()
4221 {
4222 with_host(|h| h.set_fn_prop(recv, name, val));
4223 return Ok(());
4224 }
4225 // `arr.length = n` (10.4.2.4 `ArraySetLength`) validates BEFORE it resizes,
4226 // and does so outside the host borrow because `ToNumber` may run a user
4227 // `valueOf`. An invalid length throws instead of being silently coerced to 0.
4228 let new_len = if name == "length" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array) {
4229 let want = host::to_array_length(&val)?;
4230 // 10.4.2.4 steps 15-17: shrinking deletes from the END downwards and
4231 // STOPS at the first element that cannot be deleted, leaving the length
4232 // just past it. Truncating regardless discarded a non-configurable
4233 // element and reported a length node would not have accepted.
4234 let floor = with_host(|h| {
4235 let old = match h.get(recv) {
4236 Some(JsObj::Array(items)) => items.len(),
4237 _ => 0,
4238 };
4239 let mut stop = want;
4240 for i in (want..old).rev() {
4241 if !h.prop_attrs(recv, &i.to_string()).configurable {
4242 stop = i + 1;
4243 break;
4244 }
4245 }
4246 stop
4247 });
4248 Some(floor.max(want))
4249 } else {
4250 None
4251 };
4252 with_host(|h| match h.get_mut(recv) {
4253 Some(JsObj::Object(props)) => {
4254 // Adding a *new* array-index key must re-place it into ascending
4255 // integer-key order (updating an existing key keeps its position).
4256 let is_new = !props.contains_key(name);
4257 props.insert(name.to_string(), val);
4258 if is_new && host::array_index(name).is_some() {
4259 host::canonicalize_own_keys(props);
4260 }
4261 }
4262 Some(JsObj::Array(items)) => {
4263 if let Some(n) = new_len {
4264 // Growing `length` appends HOLES (`a=[1]; a.length=3` still has
4265 // just the one own key); shrinking drops any hole past the end.
4266 let old = items.len();
4267 items.resize(n, Value::Undef);
4268 if n > old {
4269 h.mark_hole_range(recv, old..n);
4270 } else {
4271 h.truncate_holes(recv, n);
4272 }
4273 } else if let Ok(i) = name.parse::<usize>() {
4274 // A write PAST the end leaves the skipped positions elided.
4275 let old = items.len();
4276 if i >= old {
4277 items.resize(i + 1, Value::Undef);
4278 }
4279 items[i] = val;
4280 if i > old {
4281 h.mark_hole_range(recv, old..i);
4282 }
4283 // …and the written index itself is no longer one. This is the
4284 // single site that keeps a hole record from outliving the
4285 // elision it describes: every array element write in the
4286 // language reaches it.
4287 h.clear_hole(recv, i);
4288 }
4289 }
4290 _ => {}
4291 });
4292 Ok(())
4293}
4294
4295fn b_getitem(vm: &mut VM, _: u8) -> Value {
4296 let idx = vm.pop();
4297 let recv = vm.pop();
4298 let key = match host::to_property_key(&idx) {
4299 Ok(k) => k,
4300 Err(e) => return abort(vm, e),
4301 };
4302 match get_property(&recv, &key) {
4303 Ok(v) => v,
4304 Err(e) => abort(vm, e),
4305 }
4306}
4307
4308fn b_setitem(vm: &mut VM, _: u8) -> Value {
4309 let val = vm.pop();
4310 let idx = vm.pop();
4311 let recv = vm.pop();
4312 let key = match host::to_property_key(&idx) {
4313 Ok(k) => k,
4314 Err(e) => return abort(vm, e),
4315 };
4316 if let Err(e) = set_property(&recv, &key, val.clone()) {
4317 return abort(vm, e);
4318 }
4319 val
4320}
4321
4322/// `[[Delete]]` (10.1.10) for an already-resolved property key: the one place
4323/// `delete o[k]`, `delete o.k` and `Reflect.deleteProperty` all go through, so
4324/// the three cannot drift. Reports `false` for a non-configurable property
4325/// (sloppy mode ignores the failure rather than throwing) and `true` otherwise,
4326/// which is also what deleting an absent key reports.
4327pub fn delete_property(recv: &Value, key: &str) -> Result<bool, String> {
4328 // 13.5.1.2 step 5 runs `ToObject` on the base, which a nullish one refuses.
4329 // `delete u.x` reported success instead.
4330 if with_host(|h| h.is_nullish(recv)) {
4331 return Err(host::type_error(
4332 "Cannot convert undefined or null to object",
4333 ));
4334 }
4335 // `[[Delete]]` on a Proxy runs the handler's `deleteProperty` trap, which may
4336 // throw — the reason this reports a `Result` rather than a bare `bool`.
4337 if let Some(b) = crate::proxy::delete(recv, key)? {
4338 return Ok(b);
4339 }
4340 // `delete globalThis.x` removes a global a script created. It lives in the
4341 // globals map, not the object's property map, so the ordinary path reported
4342 // success and removed nothing — the binding stayed readable afterwards.
4343 if with_host(|h| h.is_global_object(recv)) && with_host(|h| h.remove_global(key)) {
4344 return Ok(true);
4345 }
4346 // `delete require.cache[id]` drops the module so the next `require` of that
4347 // file runs it again — the whole point of exposing the cache.
4348 if peek(recv, |o| match o {
4349 JsObj::Builtin(ns) => Some(ns == REQUIRE_CACHE),
4350 _ => None,
4351 }) == Some(true)
4352 {
4353 return Ok(crate::module::cache_delete(key));
4354 }
4355 // `delete process.env.X` unsets the variable in the PROCESS, not just in the
4356 // JS view, so a child spawned afterwards no longer sees it.
4357 if !key.starts_with("@@")
4358 && with_host(
4359 |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4360 )
4361 {
4362 std::env::remove_var(key);
4363 }
4364 // A member of a builtin NAMESPACE (`Math.PI`, `Number.MAX_VALUE`,
4365 // `Object.prototype`) is non-configurable when it is a constant or a
4366 // constructor's `prototype`, and `delete` of one answers false without
4367 // removing anything. There is no property map behind a namespace, so the
4368 // ordinary attribute lookup below cannot tell — it reported success for
4369 // every one of them.
4370 // A REAL intrinsic prototype object carries the write in its own map AND in
4371 // the side table the instance read consults, so the delete has to clear
4372 // both. Clearing only the map left `Object.prototype.patch` deleted as far
4373 // as the prototype was concerned and still inherited by every object.
4374 if let Some(ns) = with_host(|h| {
4375 h.intrinsic_proto_ctor(recv)
4376 .map(str::to_string)
4377 .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4378 }) {
4379 with_host(|h| h.remove_builtin_static(&format!("{ns}.prototype"), key));
4380 }
4381 if let Some(ns) = peek(recv, |o| match o {
4382 JsObj::Builtin(ns) => Some(ns.clone()),
4383 _ => None,
4384 }) {
4385 // A script-assigned static is an ordinary configurable property and is
4386 // removed from the side table the assignment landed in. Falling through
4387 // to the attribute check below answered true and deleted nothing, so a
4388 // patch survived its own `delete`.
4389 if with_host(|h| h.remove_builtin_static(&ns, key)) {
4390 return Ok(true);
4391 }
4392 if ns != REQUIRE_CACHE && !builtin_member_configurable(&ns, key) {
4393 return Ok(false);
4394 }
4395 }
4396 if !with_host(|h| h.prop_attrs(recv, key).configurable) {
4397 return Ok(false);
4398 }
4399 // An accessor lives in its own table, not the property map, so removing it
4400 // has to be explicit — otherwise `delete` reported success while the getter
4401 // kept answering and `in` kept reporting the key.
4402 if with_host(|h| h.own_accessor(recv, key).is_some()) {
4403 with_host(|h| h.remove_accessor(recv, key));
4404 return Ok(true);
4405 }
4406 with_host(|h| {
4407 let index = key.parse::<usize>();
4408 match h.get_mut(recv) {
4409 Some(JsObj::Object(props)) => {
4410 props.shift_remove(key);
4411 return;
4412 }
4413 Some(JsObj::Array(items)) => {
4414 if let Ok(i) = index {
4415 if i < items.len() {
4416 // `delete a[i]` punches a HOLE: the length is unchanged
4417 // but the index stops being an own property.
4418 items[i] = Value::Undef;
4419 h.mark_hole(recv, i);
4420 }
4421 return;
4422 }
4423 }
4424 _ => {}
4425 }
4426 // A non-index key on an array (`arr.foo`, `arr[sym]`), or any own key on
4427 // a function/class, is an ordinary own property kept in the side table.
4428 h.remove_fn_prop(recv, key);
4429 });
4430 Ok(true)
4431}
4432
4433fn b_delitem(vm: &mut VM, _: u8) -> Value {
4434 let strict = vm.pop();
4435 let idx = vm.pop();
4436 let recv = vm.pop();
4437 // `delete o[k]` keys through ToPropertyKey (7.1.19), exactly as the read and
4438 // the write do: `String(k)` would turn a Symbol into its `Symbol(desc)`
4439 // description and delete a key nothing ever wrote.
4440 let key = match host::to_property_key(&idx) {
4441 Ok(k) => k,
4442 Err(e) => return abort(vm, e),
4443 };
4444 match delete_property(&recv, &key) {
4445 Ok(false) if with_host(|h| h.truthy(&strict)) => {
4446 abort(vm, refused_delete_error(&recv, &key))
4447 }
4448 Ok(b) => Value::Bool(b),
4449 Err(e) => abort(vm, e),
4450 }
4451}
4452
4453fn b_delprop_name(vm: &mut VM, _: u8) -> Value {
4454 let strict = vm.pop();
4455 let name = sval(&vm.pop());
4456 let recv = vm.pop();
4457 match delete_property(&recv, &name) {
4458 Ok(false) if with_host(|h| h.truthy(&strict)) => {
4459 abort(vm, refused_delete_error(&recv, &name))
4460 }
4461 Ok(b) => Value::Bool(b),
4462 Err(e) => abort(vm, e),
4463 }
4464}
4465
4466/// The TypeError a STRICT `delete` of a non-configurable property raises. The
4467/// receiver renders the way every other brand-check message renders one.
4468fn refused_delete_error(recv: &Value, key: &str) -> String {
4469 // A PROXY names the trap that refused. Only the `delete` OPERATOR reports
4470 // it; `Reflect.deleteProperty` answers `false`, which is why this lives
4471 // here rather than in the shared `[[Delete]]`.
4472 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
4473 return host::type_error(&format!(
4474 "'deleteProperty' on proxy: trap returned falsish for property '{key}'"
4475 ));
4476 }
4477 // A non-callable builtin NAMESPACE renders as a plain object here — node
4478 // reports `#<Object>` for `Math`, not its `[object Math]` brand.
4479 let shown = match peek(recv, |o| match o {
4480 JsObj::Builtin(ns) => Some(ns.clone()),
4481 _ => None,
4482 }) {
4483 Some(ns) if !host::builtin_is_callable(&ns) => "#<Object>".to_string(),
4484 _ => no_side_effects_string(recv),
4485 };
4486 host::type_error(&format!("Cannot delete property '{key}' of {shown}"))
4487}
4488
4489// ── constructors ──────────────────────────────────────────────────────────────
4490
4491fn b_mkstr(vm: &mut VM, argc: u8) -> Value {
4492 let parts = pop_n(vm, argc as usize);
4493 let s: String = with_host(|h| parts.iter().map(|p| h.str_of(p)).collect());
4494 with_host(|h| h.new_str(s))
4495}
4496
4497fn b_mkarr(vm: &mut VM, argc: u8) -> Value {
4498 let items = pop_n(vm, argc as usize);
4499 with_host(|h| h.new_array(items))
4500}
4501
4502/// `MARK_HOLE [arr, index]`: record `arr[index]` as an ELIDED element. Emitted
4503/// only for an array literal that actually contains an elision, so a dense
4504/// literal costs nothing. Returns `undefined`; the array stays on the stack
4505/// underneath (the compiler `Dup`s it).
4506fn b_mark_hole(vm: &mut VM, _: u8) -> Value {
4507 let idx = vm.pop();
4508 let arr = vm.pop();
4509 let i = match idx {
4510 Value::Int(i) if i >= 0 => i as usize,
4511 _ => return Value::Undef,
4512 };
4513 with_host(|h| h.mark_hole(&arr, i));
4514 Value::Undef
4515}
4516
4517fn b_mkobj(vm: &mut VM, argc: u8) -> Value {
4518 let flat = pop_n(vm, argc as usize);
4519 let mut props: IndexMap<String, Value> = IndexMap::new();
4520 // A literal `__proto__: x` key sets the object's prototype (not an own prop).
4521 let mut proto_override: Option<Value> = None;
4522 let mut method_keys: Vec<String> = Vec::new();
4523 let mut i = 0;
4524 while i + 2 < flat.len() || (i + 2 == flat.len() && flat.len() % 3 == 0 && i < flat.len()) {
4525 if i + 2 >= flat.len() {
4526 break;
4527 }
4528 // Tag 2: an ACCESSOR's position. An accessor lives in its own table, so
4529 // the literal reserves its slot here with the `@@ord:` marker key that
4530 // `own_enum_data_keys` resolves back — otherwise `{ get g(){}, d: 2 }`
4531 // enumerated `d, g`, because `DEF_ACCESSOR` runs after `MKOBJ` and its
4532 // marker landed at the end.
4533 if matches!(flat[i], Value::Int(2)) {
4534 let key = with_host(|h| h.str_of(&flat[i + 1]));
4535 props
4536 .entry(format!("{}{key}", host::ORD_MARKER))
4537 .or_insert(Value::Undef);
4538 i += 3;
4539 continue;
4540 }
4541 // Tag 3: a METHOD DEFINITION — an ordinary property whose key is also
4542 // recorded so the literal can become its `[[HomeObject]]` below.
4543 if matches!(flat[i], Value::Int(3)) {
4544 let key = with_host(|h| h.str_of(&flat[i + 1]));
4545 method_keys.push(key.clone());
4546 props.insert(key, flat[i + 2].clone());
4547 i += 3;
4548 continue;
4549 }
4550 let spread = matches!(flat[i], Value::Int(1));
4551 if spread {
4552 let src = flat[i + 1].clone();
4553 // A STRING source spreads its index properties (`{..."ab"}` is
4554 // `{0:'a',1:'b'}`): CopyDataProperties (7.3.25) calls ToObject, and a
4555 // String exotic object owns one enumerable property per UTF-16 code
4556 // UNIT (10.4.3). `own_enum_entries_deep` only walks heap objects, so
4557 // a string source contributed nothing and `{..."ab"}` was `{}`.
4558 // Every other primitive (number/boolean/symbol) boxes to an object
4559 // with no own enumerable properties, and null/undefined are ignored,
4560 // so those correctly stay no-ops on the path below.
4561 if let Some(s) = with_host(|h| h.as_str(&src)) {
4562 for idx in 0..crate::utf16::len(&s) {
4563 if let Ok(ch) = get_property(&src, &idx.to_string()) {
4564 props.insert(idx.to_string(), ch);
4565 }
4566 }
4567 i += 3;
4568 continue;
4569 }
4570 // Object spread copies own *enumerable* properties only — never the
4571 // hidden `@@…` slots (copying `@@native` used to turn `{...buf}`
4572 // into something that still claimed to be a Buffer) and never a
4573 // property a descriptor marked non-enumerable.
4574 // A getter that throws during spread propagates as a thrown value,
4575 // which in the VM means aborting the frame.
4576 let entries = match host::own_enum_entries_deep(&src) {
4577 Ok(e) => e,
4578 Err(e) => return abort(vm, e),
4579 };
4580 for (k, v) in entries {
4581 props.insert(k, v);
4582 }
4583 // `CopyDataProperties` (7.3.25) copies own enumerable SYMBOL keys
4584 // too — only `Object.keys`/`for-in`/`JSON.stringify` skip them.
4585 for (k, v) in with_host(|h| h.own_symbol_entries(&src)) {
4586 props.insert(k, v);
4587 }
4588 } else {
4589 let key = with_host(|h| h.str_of(&flat[i + 1]));
4590 if key == "__proto__" {
4591 proto_override = Some(flat[i + 2].clone());
4592 } else {
4593 props.insert(key, flat[i + 2].clone());
4594 }
4595 }
4596 i += 3;
4597 }
4598 with_host(|h| {
4599 let o = h.new_object(props);
4600 if let Some(p) = proto_override {
4601 if matches!(p, Value::Obj(_)) {
4602 h.set_proto(&o, p);
4603 }
4604 }
4605 // A method DEFINED here takes the literal as its `[[HomeObject]]`, which
4606 // is what `super` inside it resolves through. The home object is fixed
4607 // at definition, so a method that merely arrives as a value
4608 // (`{ m: other.m }`) keeps the one it was defined with — stamping every
4609 // method-valued property instead rebound the original and changed what
4610 // IT resolved.
4611 for key in &method_keys {
4612 let m = match h.get(&o) {
4613 Some(JsObj::Object(p)) => p.get(key).cloned(),
4614 _ => None,
4615 };
4616 if let Some(m) = m {
4617 if let Some(JsObj::Func(f)) = h.get_mut(&m) {
4618 f.home_object = Some(o.clone());
4619 }
4620 }
4621 }
4622 o
4623 })
4624}
4625
4626fn b_mkfunc(vm: &mut VM, _: u8) -> Value {
4627 let def_id = match vm.pop() {
4628 Value::Int(n) => n as usize,
4629 Value::Float(f) => f as usize,
4630 _ => return abort(vm, "internal: MKFUNC id".into()),
4631 };
4632 let (is_arrow, self_name) = with_host(|h| match h.funcs.get(def_id) {
4633 Some(d) => (
4634 d.is_arrow,
4635 (d.self_name && !d.name.is_empty()).then(|| d.name.clone()),
4636 ),
4637 None => (false, None),
4638 });
4639 with_host(|h| {
4640 let mut env = h.current_env_capture();
4641 let this = h.current_this();
4642 // An arrow has no `super` of its own: it uses the enclosing METHOD's,
4643 // exactly as it uses the enclosing `this`. Nothing was captured, so
4644 // `super.m()` inside an arrow reported the method missing — in a class
4645 // method as well as an object literal.
4646 let (home_class, home_static, home_object) = if is_arrow {
4647 h.current_home()
4648 } else {
4649 (None, false, None)
4650 };
4651 // A named function expression closes over an extra scope holding its own
4652 // name, so the body can recurse through it (`function f(){ … f() … }`)
4653 // independently of whatever the outer binding is later set to.
4654 if self_name.is_some() {
4655 env = host::child_env(env);
4656 }
4657 let f = h.alloc(JsObj::Func(FuncVal {
4658 def_id,
4659 env: Some(env.clone()),
4660 this,
4661 is_arrow,
4662 home_class,
4663 home_static,
4664 home_object,
4665 }));
4666 if let Some(n) = self_name {
4667 env.borrow_mut().vars.insert(n, f.clone());
4668 }
4669 f
4670 })
4671}
4672
4673// ── truthiness / coercion / equality ──────────────────────────────────────────
4674
4675fn b_truthy(vm: &mut VM, _: u8) -> Value {
4676 let v = vm.pop();
4677 Value::Bool(with_host(|h| h.truthy(&v)))
4678}
4679
4680fn b_nullish(vm: &mut VM, _: u8) -> Value {
4681 let v = vm.pop();
4682 Value::Bool(with_host(|h| h.is_nullish(&v)))
4683}
4684
4685fn b_tostr(vm: &mut VM, _: u8) -> Value {
4686 let v = vm.pop();
4687 // ToString with user-`toString`/`valueOf` dispatch (template interpolation,
4688 // `String(x)`, object keys).
4689 match host::to_string_value(&v) {
4690 Ok(s) => s,
4691 Err(e) => abort(vm, e),
4692 }
4693}
4694
4695fn b_typeof(vm: &mut VM, _: u8) -> Value {
4696 let v = vm.pop();
4697 with_host(|h| {
4698 let t = h.type_of(&v);
4699 h.new_str(t)
4700 })
4701}
4702
4703/// `typeof <bare ident>`: read the name like `b_getlocal` but return "undefined"
4704/// (never a ReferenceError) when the name is unbound — JS `typeof` semantics.
4705fn b_typeof_name(vm: &mut VM, _: u8) -> Value {
4706 let name = sval(&vm.pop());
4707 // `typeof` does NOT excuse the temporal dead zone: it answers "undefined"
4708 // for an UNBOUND name, but a `let` above its declaration is bound and
4709 // throws. Reading the marker's type answered "function".
4710 if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
4711 return abort(vm, host::tdz_error(&name));
4712 }
4713 if let Some(v) = with_host(|h| h.read_name(&name)) {
4714 if with_host(|h| h.is_tdz(&v)) {
4715 return abort(vm, host::tdz_error(&name));
4716 }
4717 }
4718 // Bound name (user variable) → typeof its value.
4719 if let Some(v) = with_host(|h| h.read_name(&name)) {
4720 return with_host(|h| {
4721 let t = h.type_of(&v);
4722 h.new_str(t)
4723 });
4724 }
4725 // Lazily-bound globals mirror `b_getlocal`: resolve to the same value it
4726 // would produce, then take its type (so object-namespaces like `console`/
4727 // `Math`/`JSON`/`process` report "object", constructors report "function").
4728 let t = match name.as_str() {
4729 "undefined" => "undefined".to_string(),
4730 "NaN" | "Infinity" => "number".to_string(),
4731 "globalThis" | "global" => "object".to_string(),
4732 n if is_namespace(n) || is_known_builtin(n) => {
4733 let v = with_host(|h| h.alloc(JsObj::Builtin(name.clone())));
4734 with_host(|h| h.type_of(&v)).to_string()
4735 }
4736 _ => "undefined".to_string(), // genuinely unbound → JS returns "undefined"
4737 };
4738 with_host(|h| h.new_str(t))
4739}
4740
4741fn b_strict_eq(vm: &mut VM, _: u8) -> Value {
4742 let b = vm.pop();
4743 let a = vm.pop();
4744 Value::Bool(with_host(|h| h.strict_eq(&a, &b)))
4745}
4746
4747fn b_loose_eq(vm: &mut VM, _: u8) -> Value {
4748 let b = vm.pop();
4749 let a = vm.pop();
4750 // Abstract Equality steps 10-11 (7.2.15): object ⇄ primitive converts the
4751 // object with `ToPrimitive` — a JS `valueOf`/`Symbol.toPrimitive` call, so it
4752 // runs before the host borrow. Object ⇄ object stays a reference check.
4753 let (a, b) = match with_host(|h| (host::is_primitive(h, &a), host::is_primitive(h, &b))) {
4754 (false, true) if coerces_against_object(&b) => match host::to_primitive(&a, "default") {
4755 Ok(p) => (p, b),
4756 Err(e) => return abort(vm, e),
4757 },
4758 (true, false) if coerces_against_object(&a) => match host::to_primitive(&b, "default") {
4759 Ok(p) => (a, p),
4760 Err(e) => return abort(vm, e),
4761 },
4762 _ => (a, b),
4763 };
4764 Value::Bool(with_host(|h| h.loose_eq(&a, &b)))
4765}
4766
4767fn b_instanceof(vm: &mut VM, _: u8) -> Value {
4768 let ctor = vm.pop();
4769 let obj = vm.pop();
4770 match host::instance_of(&obj, &ctor) {
4771 Ok(b) => Value::Bool(b),
4772 Err(e) => abort(vm, e),
4773 }
4774}
4775
4776// ── bitwise / unary ───────────────────────────────────────────────────────────
4777
4778fn b_binop(vm: &mut VM, _: u8) -> Value {
4779 let b = vm.pop();
4780 let a = vm.pop();
4781 let tag = match vm.pop() {
4782 Value::Int(n) => n,
4783 _ => 0,
4784 };
4785 // Both operands are ToPrimitive-d with the number hint before ToInt32
4786 // (ECMA-262 13.12.1), which has to happen outside the host borrow.
4787 let r = host::to_primitive(&a, "number")
4788 .and_then(|a| host::to_primitive(&b, "number").map(|b| (a, b)))
4789 .and_then(|(a, b)| with_host(|h| h.bitwise(tag, &a, &b)));
4790 finish(vm, r)
4791}
4792
4793fn b_unary(vm: &mut VM, _: u8) -> Value {
4794 let v = vm.pop();
4795 let tag = match vm.pop() {
4796 Value::Int(n) => n,
4797 _ => 0,
4798 };
4799 // Unary `+`/`~` on a BigInt: `+` is a hard TypeError in JS; `~x` is `-x - 1`
4800 // computed in arbitrary precision.
4801 if with_host(|h| h.is_bigint_val(&v)) {
4802 return match tag {
4803 host::unop::POS => abort(
4804 vm,
4805 host::type_error("Cannot convert a BigInt value to a number"),
4806 ),
4807 host::unop::BITNOT => {
4808 let b = with_host(|h| h.as_bigint(&v)).unwrap();
4809 let r = -(b + num_bigint::BigInt::from(1));
4810 with_host(|h| h.new_bigint(r))
4811 }
4812 _ => Value::Undef,
4813 };
4814 }
4815 // `ToNumber` outside the host borrow: an object operand's `valueOf` /
4816 // `Symbol.toPrimitive` is a JS call, so it cannot run under `with_host`.
4817 let n = match host::to_number_value(&v) {
4818 Ok(n) => n,
4819 Err(e) => return abort(vm, e),
4820 };
4821 match tag {
4822 host::unop::POS => Value::Float(n),
4823 host::unop::BITNOT => {
4824 let i = if n.is_finite() {
4825 n.trunc() as i64 as i32
4826 } else {
4827 0
4828 };
4829 Value::Float(!i as f64)
4830 }
4831 _ => Value::Undef,
4832 }
4833}
4834
4835// ── membership ────────────────────────────────────────────────────────────────
4836
4837fn b_contains(vm: &mut VM, _: u8) -> Value {
4838 let container = vm.pop();
4839 let key = vm.pop();
4840 // `x in y` requires y to be an object. V8 names both operands:
4841 // `Cannot use 'in' operator to search for 'a' in 5`.
4842 // A heap-backed PRIMITIVE — a string, a symbol, a bigint — is a
4843 // `Value::Obj` in this host but is not an object, so the shape test alone
4844 // let `'length' in 'ab'` and `'description' in Symbol('x')` answer `true`
4845 // where node throws. `is_primitive` is the same predicate `ToObject` and
4846 // `typeof` use, so the three cannot disagree about what an object is.
4847 if !matches!(container, Value::Obj(_)) || with_host(|h| host::is_primitive(h, &container)) {
4848 let (k, c) = with_host(|h| (h.property_key(&key), h.str_of(&container)));
4849 return abort(
4850 vm,
4851 host::type_error(&format!(
4852 "Cannot use 'in' operator to search for '{k}' in {c}"
4853 )),
4854 );
4855 }
4856 let k = match host::to_property_key(&key) {
4857 Ok(k) => k,
4858 Err(e) => return abort(vm, e),
4859 };
4860 match has_property(&container, &k) {
4861 Ok(b) => Value::Bool(b),
4862 Err(e) => abort(vm, e),
4863 }
4864}
4865
4866// ── control ───────────────────────────────────────────────────────────────────
4867
4868fn b_sig_return(vm: &mut VM, _: u8) -> Value {
4869 let v = vm.pop();
4870 with_host(|h| h.signal = Some(host::Signal::Return(v.clone())));
4871 vm.ip = vm.chunk.ops.len();
4872 v
4873}
4874
4875/// `break [label]` whose target loop lives in an enclosing chunk (the statement is
4876/// inside a `try` block, which the host runs as its own chunk). Raise the signal
4877/// and halt this chunk; `SIG_UNWIND` after the `TRY` op re-dispatches it.
4878fn b_sig_break(vm: &mut VM, _: u8) -> Value {
4879 let label = sval(&vm.pop());
4880 let label = (!label.is_empty()).then_some(label);
4881 with_host(|h| h.signal = Some(host::Signal::Break(label)));
4882 vm.ip = vm.chunk.ops.len();
4883 Value::Undef
4884}
4885
4886/// `continue [label]` out of a `try` block — see [`b_sig_break`].
4887fn b_sig_continue(vm: &mut VM, _: u8) -> Value {
4888 let label = sval(&vm.pop());
4889 let label = (!label.is_empty()).then_some(label);
4890 with_host(|h| h.signal = Some(host::Signal::Continue(label)));
4891 vm.ip = vm.chunk.ops.len();
4892 Value::Undef
4893}
4894
4895/// Dispatch a pending control signal at the instruction after a `TRY`. `tag`
4896/// describes what the `try` is nested in (see [`host::unwind`]):
4897///
4898/// * no signal → `NONE`, execution continues normally;
4899/// * `Return`, or no enclosing loop in this chunk → halt the chunk so the signal
4900/// keeps travelling outward;
4901/// * `break`/`continue` targeting the enclosing loop → consume it and report
4902/// `BREAK`/`CONTINUE` so the compiler-emitted jump lands on the loop's exit /
4903/// continue target;
4904/// * a LABELED `break`/`continue` for some outer loop → report `BREAK` but leave
4905/// the signal pending, so leaving this loop re-dispatches it one level out.
4906fn b_sig_unwind(vm: &mut VM, _: u8) -> Value {
4907 let cont_tag = sval(&vm.pop());
4908 let brk_tag = sval(&vm.pop());
4909 let sig = match with_host(|h| h.signal.clone()) {
4910 Some(s) => s,
4911 None => return Value::Int(host::unwind::NONE),
4912 };
4913 // Nothing in this chunk can catch a `break`: halt so the signal keeps going.
4914 let propagate = |vm: &mut VM| {
4915 vm.ip = vm.chunk.ops.len();
4916 Value::Int(host::unwind::NONE)
4917 };
4918 match &sig {
4919 host::Signal::Return(_) => propagate(vm),
4920 host::Signal::Break(label) => {
4921 if brk_tag == host::unwind::NO_LOOP {
4922 return propagate(vm);
4923 }
4924 let mine = match label {
4925 None => true, // unlabeled: always the innermost enclosing context
4926 Some(l) => brk_tag == *l,
4927 };
4928 if mine {
4929 with_host(|h| h.signal = None);
4930 }
4931 // Not ours: still leave this context by its break exit, keeping the
4932 // signal pending for the next dispatch point one level out.
4933 Value::Int(host::unwind::BREAK)
4934 }
4935 host::Signal::Continue(label) => {
4936 let mine = match label {
4937 // Unlabeled `continue` binds to the innermost continue-catching
4938 // loop — which a `switch` between here and it is NOT.
4939 None => cont_tag != host::unwind::NO_LOOP,
4940 Some(l) => cont_tag == *l,
4941 };
4942 if mine {
4943 with_host(|h| h.signal = None);
4944 return Value::Int(host::unwind::CONTINUE);
4945 }
4946 if brk_tag == host::unwind::NO_LOOP {
4947 return propagate(vm);
4948 }
4949 // The target loop is further out: exit the innermost context here and
4950 // re-dispatch there.
4951 Value::Int(host::unwind::BREAK)
4952 }
4953 }
4954}
4955
4956fn b_throw(vm: &mut VM, _: u8) -> Value {
4957 let v = vm.pop();
4958 let msg = with_host(|h| {
4959 h.exc = Some(v.clone());
4960 // Prefer an error object's message for the top-level report.
4961 error_display(h, &v)
4962 });
4963 abort(vm, msg)
4964}
4965
4966fn error_display(h: &host::JsHost, v: &Value) -> String {
4967 if let Some(JsObj::Object(props)) = h.get(v) {
4968 let name = props
4969 .get("name")
4970 .map(|x| h.str_of(x))
4971 .unwrap_or_else(|| "Error".into());
4972 if let Some(m) = props.get("message") {
4973 return format!("Uncaught {name}: {}", h.str_of(m));
4974 }
4975 }
4976 format!("Uncaught {}", h.str_of(v))
4977}
4978
4979fn b_try(vm: &mut VM, _: u8) -> Value {
4980 let id = match vm.pop() {
4981 Value::Int(n) => n as usize,
4982 _ => return abort(vm, "internal: TRY id".into()),
4983 };
4984 // Shape only. Running a `try` used to clone the whole `TryDef` — its block,
4985 // its handler and its finalizer bytecode — every time control entered it,
4986 // which for a `try` inside a loop is once per iteration.
4987 let (has_handler, catch_bind, has_finalizer) = match with_host(|h| h.try_shape(id)) {
4988 Some(t) => t,
4989 None => return abort(vm, "internal: unknown try id".into()),
4990 };
4991 let mut pending: Option<String> = None;
4992 // Each sub-block runs as its own chunk on THIS frame, so a throw part-way
4993 // through can leave block scopes open. Snapshot the scope and restore it
4994 // before the handler and after the whole statement.
4995 let scope = with_host(|h| h.scope_snapshot());
4996
4997 with_host(|h| h.push_scope()); // the try block is its own block scope
4998 let body_res = host::run_chunk_keyed(host::try_key(id, 0), || {
4999 with_host(|h| h.try_chunk(id, 0)).expect("try block exists")
5000 });
5001 with_host(|h| h.restore_scope(scope.clone()));
5002 let signal_after = with_host(|h| h.signal.is_some());
5003 if let Err(e) = body_res {
5004 if signal_after {
5005 pending = Some(e);
5006 } else if has_handler {
5007 // Bind the thrown value (or a synthesized error) to the catch param.
5008 let thrown =
5009 with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
5010 with_host(|h| {
5011 h.error = None;
5012 h.exc = None;
5013 });
5014 // The catch parameter is block-scoped to the handler.
5015 with_host(|h| h.push_scope());
5016 if let Some(name) = &catch_bind {
5017 with_host(|h| h.declare_name(name, thrown));
5018 }
5019 let hres = host::run_chunk_keyed(host::try_key(id, 1), || {
5020 with_host(|h| h.try_chunk(id, 1)).expect("handler exists")
5021 });
5022 with_host(|h| h.restore_scope(scope.clone()));
5023 if let Err(e2) = hres {
5024 pending = Some(e2);
5025 }
5026 } else {
5027 pending = Some(e);
5028 }
5029 }
5030
5031 // finally always runs; a finally error/signal supersedes.
5032 if has_finalizer {
5033 let sig_before = with_host(|h| h.signal.take());
5034 with_host(|h| h.push_scope()); // ditto for `finally`
5035 let fres = host::run_chunk_keyed(host::try_key(id, 2), || {
5036 with_host(|h| h.try_chunk(id, 2)).expect("finalizer exists")
5037 });
5038 with_host(|h| h.restore_scope(scope.clone()));
5039 match fres {
5040 Ok(_) => {
5041 if with_host(|h| h.signal.is_none()) {
5042 // The finalizer completed normally: the try/catch block's own
5043 // abrupt completion resumes.
5044 with_host(|h| h.signal = sig_before);
5045 } else {
5046 // ECMA-262 14.15.3 TryStatement evaluation: when the finalizer's
5047 // completion is abrupt (`return`/`break`/`continue` inside
5048 // `finally`), that completion REPLACES the try/catch block's —
5049 // including a pending throw, which is discarded, not rethrown.
5050 pending = None;
5051 with_host(|h| {
5052 h.error = None;
5053 h.exc = None;
5054 });
5055 }
5056 }
5057 Err(e) => pending = Some(e),
5058 }
5059 }
5060
5061 if let Some(e) = pending {
5062 return abort(vm, e);
5063 }
5064 Value::Undef
5065}
5066
5067/// Synthesize an `Error`-shaped object from an internal error string, linked to
5068/// the matching builtin error prototype so `instanceof`/`.constructor` work.
5069pub(crate) fn synth_error(h: &mut host::JsHost, e: &str) -> Value {
5070 h.ensure_error_protos();
5071 // A `DOMException` marker: the WHATWG error NAME rides in the string, since
5072 // it is not one of the ECMAScript error classes below.
5073 if let Some(rest) = e.strip_prefix(host::DOM_MARK) {
5074 if let Some((name, msg)) = rest.split_once('\u{1}') {
5075 return dom_exception_with(h, name, msg);
5076 }
5077 }
5078 // A `Name [ERR_CODE]: message` head carries a Node error `code` next to the
5079 // error class, exactly as Node's internal errors render it in `.stack`.
5080 let (head, rest) = match e.split_once(": ") {
5081 Some((n, m)) => (n, m.to_string()),
5082 None => ("", e.to_string()),
5083 };
5084 let (base, code) = match head.split_once(" [") {
5085 Some((n, c)) if c.ends_with(']') => (n, Some(c[..c.len() - 1].to_string())),
5086 _ => (head, None),
5087 };
5088 let (name, mut message) = if host::ERROR_NAMES.contains(&base) {
5089 (base.to_string(), rest)
5090 } else {
5091 ("Error".to_string(), e.to_string())
5092 };
5093 // A `host::plain_coded_error` marker: the code rides at the head of the
5094 // MESSAGE rather than in the class, because Node's native-layer errors set
5095 // `.code` while leaving `String(err)` unbracketed (`TypeError: Invalid URL`
5096 // with `code === 'ERR_INVALID_URL'`). Strip it back off here — the marker is
5097 // internal and must never reach a user-visible `.message`.
5098 let mut code = code;
5099 // Whether `String(err)`/`err.stack` show `Name [CODE]:` — true for the
5100 // bracketed head, false for the marker form.
5101 let mut bracketed = code.is_some();
5102 // Extra own properties (`input`, `base`) from `host::plain_coded_error_with`.
5103 let mut fields: Vec<(String, String)> = Vec::new();
5104 if let Some(rest) = message.strip_prefix(host::CODE_MARK) {
5105 if let Some((c, m)) = rest.split_once('\u{1}') {
5106 code = Some(c.to_string());
5107 bracketed = false;
5108 let (m, fs) = host::split_error_fields(m);
5109 fields = fs
5110 .into_iter()
5111 .map(|(k, v)| (k.to_string(), v.to_string()))
5112 .collect();
5113 message = m.to_string();
5114 }
5115 }
5116 let mut props: IndexMap<String, Value> = IndexMap::new();
5117 let mv = h.new_str(message.clone());
5118 props.insert("message".into(), mv);
5119 if let Some(c) = &code {
5120 let cv = h.new_str(c.clone());
5121 props.insert("code".into(), cv);
5122 for (k, v) in fields {
5123 let fv = h.new_str(v);
5124 props.insert(k, fv);
5125 }
5126 if bracketed {
5127 // Marks this as a Node JS-layer error, whose `toString` brackets the
5128 // code. A native-layer error has the same `.code` and does not.
5129 props.insert("@@nodeError".into(), Value::Bool(true));
5130 }
5131 }
5132 let label = match (&code, bracketed) {
5133 (Some(c), true) => format!("{name} [{c}]"),
5134 _ => name.clone(),
5135 };
5136 let frames = h.stack_frames();
5137 let stack = if message.is_empty() {
5138 format!("{label}{frames}")
5139 } else {
5140 format!("{label}: {message}{frames}")
5141 };
5142 let sv = h.new_str(stack);
5143 props.insert("stack".into(), sv);
5144 // A libuv system-error message is itself the canonical encoding of the
5145 // error's metadata — `ENOENT: no such file or directory, open '/x'` — so a
5146 // filesystem/network failure recovers the enumerable `code`/`errno`/
5147 // `syscall`/`path` own properties that `err.code === 'ENOENT'` checks (the
5148 // single most common error-handling idiom in Node packages) depend on.
5149 for (k, v) in syscall_error_fields(&message) {
5150 let sv = match v {
5151 SysField::Str(s) => h.new_str(s),
5152 SysField::Num(n) => Value::Float(n),
5153 };
5154 props.insert(k.into(), sv);
5155 }
5156 let obj = h.new_object(props);
5157 if let Some(p) = host::error_proto_of(h, &name) {
5158 h.set_proto(&obj, p);
5159 }
5160 // `message`/`stack` are non-enumerable; a Node `ERR_*` error's `code` is not
5161 // (`Object.keys(e)` on an `ERR_INVALID_ARG_TYPE` reads `["code"]`).
5162 h.hide_prop(&obj, "message");
5163 h.hide_prop(&obj, "stack");
5164 obj
5165}
5166
5167enum SysField {
5168 Str(String),
5169 Num(f64),
5170}
5171
5172/// Decompose a libuv-shaped message (`ECODE: reason, syscall 'path'`) into the
5173/// own properties Node hangs off a system error. Returns empty for any message
5174/// that is not in that shape.
5175fn syscall_error_fields(message: &str) -> Vec<(&'static str, SysField)> {
5176 let (code, rest) = match message.split_once(": ") {
5177 Some((c, r))
5178 if c.len() >= 2
5179 && c.starts_with('E')
5180 && c.bytes()
5181 .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()) =>
5182 {
5183 (c, r)
5184 }
5185 _ => return Vec::new(),
5186 };
5187 let mut out: Vec<(&'static str, SysField)> = vec![
5188 ("errno", SysField::Num(errno_for(code))),
5189 ("code", SysField::Str(code.to_string())),
5190 ];
5191 // `reason, syscall 'path'` — the path is optional (`EPIPE: …, write`).
5192 if let Some((_, tail)) = rest.split_once(", ") {
5193 let (syscall, path) = match tail.split_once(" '") {
5194 // A two-path message ends `'from' -> 'to'`; `err.path` is the FIRST
5195 // one, so the scan stops at its closing quote rather than at the
5196 // end of the line — which had been swallowing `' -> 'dest` into the
5197 // path for every `rename` and `copyFile` failure.
5198 Some((s, p)) => (s, p.split_once('\'').map(|(first, _)| first)),
5199 None => (tail, None),
5200 };
5201 out.push(("syscall", SysField::Str(syscall.to_string())));
5202 if let Some(p) = path {
5203 out.push(("path", SysField::Str(p.to_string())));
5204 }
5205 }
5206 out
5207}
5208
5209/// The negative `errno` Node reports for a libuv error code on this platform.
5210/// Only the codes `err_str` can produce are mapped; anything else reports the
5211/// generic `EIO` number rather than inventing a value.
5212fn errno_for(code: &str) -> f64 {
5213 let n: i32 = match code {
5214 "ENOENT" => 2,
5215 "EACCES" => 13,
5216 "EEXIST" => 17,
5217 "ENOTDIR" => 20,
5218 "EISDIR" => 21,
5219 "EINVAL" => 22,
5220 "EPIPE" => 32,
5221 "ENOTEMPTY" => 66,
5222 _ => 5, // EIO
5223 };
5224 -f64::from(n)
5225}
5226
5227// ── iteration ─────────────────────────────────────────────────────────────────
5228
5229fn b_getiter(vm: &mut VM, _: u8) -> Value {
5230 let v = vm.pop();
5231 // A generator is its own iterator (resumed lazily by FORITER).
5232 if with_host(|h| h.is_generator_val(&v)) {
5233 return v;
5234 }
5235 // A Proxy's iterator comes from its traps, materialized eagerly: the
5236 // `lookup_chain` probe below reads the property map a proxy does not have.
5237 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5238 return match crate::proxy::iterate(&v) {
5239 Ok(Some(items)) => with_host(|h| {
5240 h.alloc(JsObj::Iter {
5241 items,
5242 idx: 0,
5243 array: None,
5244 })
5245 }),
5246 Ok(None) => abort(vm, "internal: kind_of said Proxy".into()),
5247 Err(e) => abort(vm, e),
5248 };
5249 }
5250 // Arrays and strings take the direct path below: they have no iterator
5251 // state to preserve and are the hot case, so they must not pay a property
5252 // lookup and a call per loop.
5253 let direct = matches!(
5254 with_host(|h| h.kind_of(&v)),
5255 Some(ObjKind::Array) | Some(ObjKind::Str)
5256 );
5257 // …but only while their `Symbol.iterator` is still reachable. It comes from
5258 // the intrinsic prototype, so replacing the link takes it away: node reports
5259 // `a is not iterable` for an array whose prototype is a plain object, where
5260 // the fast path below iterated the backing vector regardless.
5261 if !own_intrinsic_reachable(&v)
5262 && !matches!(
5263 get_property(&v, "@@iterator"),
5264 Ok(ref f) if with_host(|h| host::is_callable(h, f))
5265 )
5266 {
5267 let shown = with_host(|h| h.inspect(&v));
5268 let msg = host::type_error(&format!("{shown} is not iterable"));
5269 return abort(vm, host::name_call_site(vm, &shown, msg));
5270 }
5271 // Anything else with a `Symbol.iterator`: call it for the iterator object.
5272 //
5273 // Resolved as a full property READ, not a stored-property lookup. A
5274 // NATIVE-tagged object (`URLSearchParams`, `Headers`, `Map`, `Set`)
5275 // dispatches its methods through the stdlib table rather than a property
5276 // map, so a `lookup_chain` probe found nothing and the loop fell through to
5277 // materializing the value — which threw for `URLSearchParams` and
5278 // snapshotted for `Map`. Spreading the same object already worked, because
5279 // that path had been fixed and this one had not.
5280 if !direct {
5281 if let Ok(iter_fn) = get_property(&v, "@@iterator") {
5282 if with_host(|h| host::is_callable(h, &iter_fn)) {
5283 return match host::invoke(&iter_fn, Vec::new(), Some(v.clone())) {
5284 Ok(it) => it,
5285 Err(e) => abort(vm, e),
5286 };
5287 }
5288 }
5289 }
5290 // An array is iterated live, as its `values()` iterator does: a snapshot
5291 // missed every push during the loop, so a worklist `for (const n of q)
5292 // q.push(…)` stopped after the first element.
5293 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Array) {
5294 return array_iterator(&v, host::ArrayIterKind::Values);
5295 }
5296 match with_host(|h| h.iter_vec(&v)) {
5297 Ok(items) => with_host(|h| {
5298 h.alloc(JsObj::Iter {
5299 items,
5300 idx: 0,
5301 array: None,
5302 })
5303 }),
5304 // V8 names the SOURCE EXPRESSION, not the value: `for (const x of a)`
5305 // reports `a is not iterable`. The text was recorded for this op.
5306 Err(e) => {
5307 let shown = with_host(|h| h.inspect(&v));
5308 let named = host::name_call_site(vm, &shown, e);
5309 abort(vm, named)
5310 }
5311 }
5312}
5313
5314fn b_forin_keys(vm: &mut VM, _: u8) -> Value {
5315 let v = vm.pop();
5316 // `for-in` over a Proxy is 14.7.5.9 `EnumerateObjectProperties`: the
5317 // `ownKeys` trap filtered by `[[GetOwnProperty]]`'s `enumerable`. Both traps
5318 // are user code, so this cannot run inside `enum_keys`'s `&mut` host borrow.
5319 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5320 // `ownKeys` ONLY. The `enumerable` filter is 14.7.5.10's per-key
5321 // `[[GetOwnProperty]]`, which `FORIN_ALIVE` runs at the moment each key
5322 // is visited — so the `getOwnPropertyDescriptor` traps interleave with
5323 // the body the way node's do, instead of all firing up front.
5324 return match crate::proxy::own_keys(&v) {
5325 Ok(keys) => with_host(|h| {
5326 let out: Vec<Value> = keys
5327 .unwrap_or_default()
5328 .into_iter()
5329 .filter(|k| !host::is_symbol_key(k))
5330 .map(|k| h.new_str(k))
5331 .collect();
5332 h.new_array(out)
5333 }),
5334 Err(e) => abort(vm, e),
5335 };
5336 }
5337 let mut keys = with_host(|h| h.enum_keys(&v));
5338 // A member patched onto the receiver's INTRINSIC prototype is enumerable
5339 // and inherited, so `for-in` visits it after the own keys — but the
5340 // intrinsic prototypes are not links `enum_keys` can walk, so its chain
5341 // pass never reaches them.
5342 if !with_host(|h| h.has_null_proto(&v)) {
5343 let seen: Vec<String> = keys.iter().map(|k| with_host(|h| h.str_of(k))).collect();
5344 for ns in intrinsic_proto_namespaces(&v) {
5345 for k in with_host(|h| h.builtin_static_keys(&ns)) {
5346 if !seen.contains(&k) && !intrinsic_proto_member(&ns, &k) {
5347 keys.push(with_host(|h| h.new_str(k)));
5348 }
5349 }
5350 }
5351 }
5352 with_host(|h| h.new_array(keys))
5353}
5354
5355/// The intrinsic prototype namespaces `v` inherits from, nearest first — its
5356/// own constructor's and then `Object`'s, the same two steps
5357/// `inherited_builtin_static` looks a value up in.
5358fn intrinsic_proto_namespaces(v: &Value) -> Vec<String> {
5359 let ctor = match wrapped_primitive(v).as_ref().and_then(wrapper_ctor_of) {
5360 Some(c) => Some(c),
5361 None if is_arguments(v) => Some("Object"),
5362 None => with_host(|h| default_ctor_name(h, v)),
5363 };
5364 let mut out: Vec<String> = ctor
5365 .filter(|c| *c != "Object")
5366 .map(|c| format!("{c}.prototype"))
5367 .into_iter()
5368 .collect();
5369 out.push("Object.prototype".to_string());
5370 out
5371}
5372
5373/// `FORIN_ALIVE` — is `key` STILL an enumerable property of `obj`?
5374///
5375/// `for-in` takes its key list once (14.7.5.10 builds it lazily, but a snapshot
5376/// of the enumerable keys is observationally the same for everything except
5377/// this), and the body can delete a key before the loop reaches it. Node does
5378/// not visit a key deleted that way; without this check `delete d.z` inside the
5379/// loop still produced `x,y,z`.
5380///
5381/// The check is `[[GetOwnProperty]]`-shaped rather than `in`: on a Proxy it runs
5382/// the `getOwnPropertyDescriptor` trap, which is what node runs, and NOT the
5383/// `has` trap, which node never fires for `for-in`. That also puts each trap
5384/// call immediately before its visit, matching node's interleaving — the trap
5385/// log used to show every `gopd` up front because the key list was filtered
5386/// eagerly.
5387fn b_forin_alive(vm: &mut VM, _: u8) -> Value {
5388 let key = vm.pop();
5389 let obj = vm.pop();
5390 let name = with_host(|h| h.str_of(&key));
5391 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
5392 return match crate::proxy::own_enumerable(&obj, &name) {
5393 Ok(b) => Value::Bool(b),
5394 Err(e) => abort(vm, e),
5395 };
5396 }
5397 // A STRING's keys are its character indices. `in` is not defined on a string
5398 // primitive at all, so the ordinary path below has no answer for one and
5399 // `for (const i in 'abc')` came back empty.
5400 if let Some(s) = with_host(|h| h.as_str(&obj)) {
5401 let len = crate::utf16::len(&s);
5402 return Value::Bool(name.parse::<usize>().is_ok_and(|i| i < len));
5403 }
5404 // Any other receiver: EXISTENCE only. Node re-checks that the key is still
5405 // there and does NOT re-check enumerability — making one non-enumerable
5406 // mid-loop still visits it, where re-filtering on `enumerable` dropped it.
5407 // (The Proxy branch above does re-check, because there the answer comes from
5408 // the trap node itself calls.)
5409 Value::Bool(has_property_ordinary(&obj, &name))
5410}
5411
5412fn b_foriter(vm: &mut VM, _: u8) -> Value {
5413 let it = match vm.stack.last() {
5414 Some(v) => v.clone(),
5415 None => return abort(vm, "internal: FORITER with empty stack".into()),
5416 };
5417 // A built-in iterator: a snapshot (strings, a Proxy's items) or live over
5418 // an array.
5419 if let Some(step) = iter_step(&it) {
5420 return match step {
5421 Some(v) => {
5422 vm.push(v);
5423 Value::Bool(true)
5424 }
5425 None => Value::Bool(false),
5426 };
5427 }
5428 // Generator: resume one step.
5429 if with_host(|h| h.is_generator_val(&it)) {
5430 return match host::gen_resume(&it, Value::Undef) {
5431 Ok(host::GenStep::Yield(v)) => {
5432 vm.push(v);
5433 Value::Bool(true)
5434 }
5435 Ok(host::GenStep::Done(_)) => Value::Bool(false),
5436 Err(e) => abort(vm, e),
5437 };
5438 }
5439 // A user iterator object with a `.next()` returning `{ value, done }`.
5440 match host::call_method(&it, "next", Vec::new()) {
5441 Ok(step) => {
5442 let done = get_property(&step, "done")
5443 .map(|d| with_host(|h| h.truthy(&d)))
5444 .unwrap_or(true);
5445 if done {
5446 Value::Bool(false)
5447 } else {
5448 match get_property(&step, "value") {
5449 Ok(v) => {
5450 vm.push(v);
5451 Value::Bool(true)
5452 }
5453 Err(e) => abort(vm, e),
5454 }
5455 }
5456 }
5457 Err(e) => abort(vm, e),
5458 }
5459}
5460
5461fn b_unpack(vm: &mut VM, _: u8) -> Value {
5462 let star = match vm.pop() {
5463 Value::Int(n) => n,
5464 _ => -1,
5465 };
5466 let count = match vm.pop() {
5467 Value::Int(n) => n as usize,
5468 _ => 0,
5469 };
5470 let iterable = vm.pop();
5471 // Without a `...rest` element the pattern needs exactly `count` values and
5472 // must then close the iterator; draining hung on an unbounded source.
5473 let items = match if star < 0 {
5474 host::iter_take(&iterable, count)
5475 } else {
5476 host::iter_all(&iterable)
5477 } {
5478 Ok(v) => v,
5479 // Destructuring a non-iterable names the SOURCE EXPRESSION, the way
5480 // `for-of` does: `const [x] = o` reports `o is not iterable`. The text
5481 // was recorded for this op at compile time.
5482 Err(e) => {
5483 // Node names the source only when the pattern's right-hand side is
5484 // a plain IDENTIFIER — `const [x] = o` is `o is not iterable`.
5485 // Anything else (a member, a call, a nested pattern, a parameter)
5486 // reports the TYPE instead, with the property note. Measured across
5487 // twelve shapes rather than guessed.
5488 let msg = match host::call_site_text(vm) {
5489 Some(text) => host::type_error(&format!("{text} is not iterable")),
5490 None if e.ends_with(" is not iterable") => {
5491 host::type_error(¬_iterable_typed(&iterable))
5492 }
5493 None => e,
5494 };
5495 return abort(vm, msg);
5496 }
5497 };
5498 let ordered: Vec<Value> = if star < 0 {
5499 (0..count)
5500 .map(|i| items.get(i).cloned().unwrap_or(Value::Undef))
5501 .collect()
5502 } else {
5503 let si = star as usize;
5504 let after = count.saturating_sub(si + 1);
5505 let rest_end = items.len().saturating_sub(after).max(si);
5506 let mut out: Vec<Value> = Vec::with_capacity(count);
5507 for i in 0..si {
5508 out.push(items.get(i).cloned().unwrap_or(Value::Undef));
5509 }
5510 let rest: Vec<Value> = items
5511 .get(si..rest_end)
5512 .map(|s| s.to_vec())
5513 .unwrap_or_default();
5514 out.push(with_host(|h| h.new_array(rest)));
5515 for j in 0..after {
5516 out.push(items.get(rest_end + j).cloned().unwrap_or(Value::Undef));
5517 }
5518 out
5519 };
5520 if ordered.is_empty() {
5521 return Value::Undef;
5522 }
5523 for it in ordered[1..].iter().rev().cloned() {
5524 vm.push(it);
5525 }
5526 ordered[0].clone()
5527}
5528
5529fn b_build_args(vm: &mut VM, argc: u8) -> Value {
5530 let flat = pop_n(vm, argc as usize);
5531 let mut out = Vec::new();
5532 // Elided positions of an array literal (tag 2), recorded as the run-time
5533 // index each lands on — which only this walk knows, because a preceding
5534 // spread contributes an unknown number of elements. Call-argument lists,
5535 // the other `BUILD_ARGS` caller, cannot contain an elision, so this stays
5536 // empty for them.
5537 let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
5538 let mut i = 0;
5539 while i + 1 < flat.len() {
5540 let val = flat[i + 1].clone();
5541 match flat[i] {
5542 // Tag 1 is an ARRAY-LITERAL spread, tag 3 a CALL-ARGUMENT one. They
5543 // report a non-iterable differently, which is the only reason the
5544 // two are told apart here.
5545 Value::Int(1) => match host::iter_all(&val).map_err(|e| {
5546 let shown = with_host(|h| h.inspect(&val));
5547 host::name_call_site(vm, &shown, e)
5548 }) {
5549 Ok(items) => out.extend(items),
5550 Err(e) => return abort(vm, e),
5551 },
5552 Value::Int(3) => match host::iter_all(&val) {
5553 Ok(items) => out.extend(items),
5554 Err(e) => {
5555 // A NULLISH spread names the value and what could not be
5556 // read off it; anything else names the missing protocol.
5557 let shown = with_host(|h| h.is_nullish(&val).then(|| h.str_of(&val)));
5558 return abort(
5559 vm,
5560 match shown {
5561 Some(s) => host::type_error(&format!(
5562 "{s} is not iterable (cannot read property {s})"
5563 )),
5564 None if e.ends_with(" is not iterable") => host::type_error(
5565 "Spread syntax requires ...iterable[Symbol.iterator] to be a function",
5566 ),
5567 None => e,
5568 },
5569 );
5570 }
5571 },
5572 Value::Int(2) => {
5573 holes.insert(out.len());
5574 out.push(Value::Undef);
5575 }
5576 _ => out.push(val),
5577 }
5578 i += 2;
5579 }
5580 with_host(|h| {
5581 let arr = h.new_array(out);
5582 h.install_holes(&arr, holes);
5583 arr
5584 })
5585}
5586
5587// ── calls ──────────────────────────────────────────────────────────────────────
5588
5589fn b_call(vm: &mut VM, argc: u8) -> Value {
5590 let mut args = pop_n(vm, argc as usize);
5591 let name = sval(&args.remove(0));
5592 let r = host::call_named(&name, args);
5593 // A bare name that resolved to a non-callable reports the VALUE
5594 // (`undefined is not a function`); node names the identifier. Resolving it
5595 // again to learn what the message said costs nothing off the error path.
5596 let r = r.map_err(|e| {
5597 let shown = global_binding(&name)
5598 .map(|v| with_host(|h| h.str_of(&v)))
5599 .unwrap_or_default();
5600 host::name_call_site(vm, &shown, e)
5601 });
5602 finish(vm, r)
5603}
5604
5605/// `recv[0](…)` — a computed call whose key is an ARRAY INDEX rather than a
5606/// method name. `call_method` resolves by name and bottoms out in
5607/// `call_type_method`, which knows `sort`/`slice` and not `"0"`, so an element
5608/// that happens to be a function reported "is not a function". Read the element
5609/// and invoke it with `recv` as `this`, which is the receiver 13.3.6 gives it.
5610/// A computed call's key is a property key, so it goes through ToPropertyKey:
5611/// `arr[0](…)` looks up `"0"`. `sval` only unwraps an existing `Value::Str` and
5612/// answers "" for a number, which turned `arr[0]()` into a call to the method
5613/// named "" — so the key is stringified here instead.
5614fn call_key_of(v: &Value) -> String {
5615 if let Value::Str(s) = v {
5616 return (**s).clone();
5617 }
5618 // `ToPropertyKey`, not `ToString`. A SYMBOL key has an internal `@@name`
5619 // spelling that `str_of` does not produce — it renders
5620 // `Symbol(Symbol.iterator)` — so `obj[Symbol.iterator]()` dispatched a
5621 // method by that display text and reported it was not a function, for every
5622 // object including a plain literal with a computed symbol method. Reading
5623 // the same property without calling it worked, which is what hid this.
5624 with_host(|h| h.property_key(v))
5625}
5626
5627fn index_element_call(recv: &Value, name: &str, args: &[Value]) -> Option<Result<Value, String>> {
5628 if name.is_empty() || !name.bytes().all(|b| b.is_ascii_digit()) {
5629 return None;
5630 }
5631 let f = get_property(recv, name).ok()?;
5632 with_host(|h| host::is_callable(h, &f))
5633 .then(|| host::invoke(&f, args.to_vec(), Some(recv.clone())))
5634}
5635
5636fn b_call_method(vm: &mut VM, argc: u8) -> Value {
5637 let mut args = pop_n(vm, argc as usize);
5638 let recv = args.remove(0);
5639 let name = call_key_of(&args.remove(0));
5640 if let Some(r) = index_element_call(&recv, &name, &args) {
5641 return finish(vm, r);
5642 }
5643 let r = host::call_method(&recv, &name, args);
5644 // `z.f()` on a missing method is `z.f is not a function` in node, not
5645 // `f is not a function`: V8 names the callee as the source wrote it. The
5646 // text was recorded for this op at compile time.
5647 let r = r.map_err(|e| host::name_call_site(vm, &name, e));
5648 finish(vm, r)
5649}
5650
5651fn b_call_value(vm: &mut VM, argc: u8) -> Value {
5652 let mut args = pop_n(vm, argc as usize);
5653 let callable = args.remove(0);
5654 let r = host::invoke(&callable, args, None);
5655 // The callee here is an expression, not a name, so the message it produced
5656 // describes the VALUE (`undefined is not a function`); node names the
5657 // expression. Same site table, keyed on that rendering.
5658 let r = r.map_err(|e| {
5659 let shown = with_host(|h| h.str_of(&callable));
5660 host::name_call_site(vm, &shown, e)
5661 });
5662 finish(vm, r)
5663}
5664
5665/// `NEW_SPREAD` — `new C(...xs)`, where the argument list is a run-time array
5666/// rather than a fixed count of stack slots.
5667///
5668/// `compile_new` used to compile each argument with `compile_expr`, and a
5669/// spread there evaluates to the SPREAD OBJECT itself — so `new C(...[1, 2])`
5670/// passed the array as one argument and `new Date(...[2020, 0, 1])` built an
5671/// Invalid Date.
5672fn b_new_spread(vm: &mut VM, _: u8) -> Value {
5673 let args_arr = vm.pop();
5674 let ctor = vm.pop();
5675 let args = host::iter_all(&args_arr).unwrap_or_default();
5676 let r = host::construct(&ctor, args).map_err(|e| {
5677 let shown = with_host(|h| h.str_of(&ctor));
5678 host::name_call_site(vm, &shown, e)
5679 });
5680 finish(vm, r)
5681}
5682
5683fn b_new(vm: &mut VM, argc: u8) -> Value {
5684 let mut args = pop_n(vm, argc as usize);
5685 let ctor = args.remove(0);
5686 let r = host::construct(&ctor, args);
5687 // `new (o.a.b.c)()` on a non-constructor names the expression, as a failed
5688 // call does.
5689 let r = r.map_err(|e| {
5690 let shown = with_host(|h| h.str_of(&ctor));
5691 host::name_call_site(vm, &shown, e)
5692 });
5693 finish(vm, r)
5694}
5695
5696fn b_apply(vm: &mut VM, _: u8) -> Value {
5697 let args_arr = vm.pop();
5698 let callable = vm.pop();
5699 let args = host::iter_all(&args_arr).unwrap_or_default();
5700 let r = host::invoke(&callable, args, None);
5701 finish(vm, r)
5702}
5703
5704fn b_apply_method(vm: &mut VM, _: u8) -> Value {
5705 let args_arr = vm.pop();
5706 let name = call_key_of(&vm.pop());
5707 let recv = vm.pop();
5708 let args = host::iter_all(&args_arr).unwrap_or_default();
5709 if let Some(r) = index_element_call(&recv, &name, &args) {
5710 return finish(vm, r);
5711 }
5712 let r = host::call_method(&recv, &name, args);
5713 finish(vm, r)
5714}
5715
5716// ── numeric hook ──────────────────────────────────────────────────────────────
5717
5718/// Host callback for arithmetic fusevm cannot complete natively (a non-`Int`/
5719/// non-`Float` operand). Supplies JavaScript `+` concatenation and coercion.
5720///
5721/// Every operand is run through `ToPrimitive` FIRST (ECMA-262 13.15.3 for `+`,
5722/// 13.6.3 for the other arithmetic ops, 13.10.1 for the relational ones), which
5723/// is what invokes a user `valueOf`/`Symbol.toPrimitive`. It has to happen here
5724/// rather than inside `JsHost::arith`, because calling back into JS re-enters
5725/// the VM and `arith` runs under the host's `RefCell` borrow.
5726pub fn numeric_hook(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5727 use NumOp::*;
5728 let (a, b) = match op {
5729 // `==`/`!=` only convert when the OTHER side is a primitive that can be
5730 // compared numerically or textually; `{} == {}` stays a reference check.
5731 Eq | Ne => {
5732 let (pa, pb) = with_host(|h| (host::is_primitive(h, a), host::is_primitive(h, b)));
5733 match (pa, pb) {
5734 (false, true) if coerces_against_object(b) => {
5735 (host::to_primitive(a, "default")?, b.clone())
5736 }
5737 (true, false) if coerces_against_object(a) => {
5738 (a.clone(), host::to_primitive(b, "default")?)
5739 }
5740 _ => (a.clone(), b.clone()),
5741 }
5742 }
5743 // `+` uses the default hint (`valueOf` first, but a string result still
5744 // selects concatenation); everything else uses the number hint.
5745 Add => (
5746 host::to_primitive(a, "default")?,
5747 host::to_primitive(b, "default")?,
5748 ),
5749 _ => (
5750 host::to_primitive(a, "number")?,
5751 host::to_primitive(b, "number")?,
5752 ),
5753 };
5754 reject_symbol_operand(op, &a, &b)?;
5755 with_host(|h| h.arith(op, &a, &b))
5756}
5757
5758/// A symbol has no `ToNumber` and no `ToString`, so every operator except the
5759/// equality family rejects it (7.1.4 step 2, 7.1.17 step 2). node-js instead
5760/// concatenated `Symbol(desc)` into the result.
5761///
5762/// Which of the two messages V8 uses is decided by whether the operation is
5763/// STRING concatenation — measured on node v26.7.0, `Symbol() + ''` is
5764/// `Cannot convert a Symbol value to a string` while `Symbol() + 1`,
5765/// `Symbol() + Symbol()` and `Symbol() * 1` are all
5766/// `Cannot convert a Symbol value to a number`. `==`/`===` never convert
5767/// (`Symbol() == 1` is `false`), so they are left alone.
5768fn reject_symbol_operand(op: NumOp, a: &Value, b: &Value) -> Result<(), String> {
5769 use NumOp::*;
5770 if matches!(op, Eq | Ne) {
5771 return Ok(());
5772 }
5773 let (sym, concat) = with_host(|h| {
5774 let is_sym = |v: &Value| matches!(h.get(v), Some(JsObj::Symbol { .. }));
5775 let is_str =
5776 |v: &Value| matches!(v, Value::Str(_)) || matches!(h.get(v), Some(JsObj::Str(_)));
5777 (is_sym(a) || is_sym(b), is_str(a) || is_str(b))
5778 });
5779 if !sym {
5780 return Ok(());
5781 }
5782 Err(host::type_error(if matches!(op, Add) && concat {
5783 "Cannot convert a Symbol value to a string"
5784 } else {
5785 "Cannot convert a Symbol value to a number"
5786 }))
5787}
5788
5789/// Whether a primitive `v` makes `==` against an object convert that object
5790/// (7.2.15 steps 10-11): numbers, strings, bigints and symbols do; `null`,
5791/// `undefined` and booleans are settled without a `ToPrimitive` call
5792/// (a boolean is coerced to a number first, and then it does).
5793fn coerces_against_object(v: &Value) -> bool {
5794 match v {
5795 Value::Undef => false,
5796 Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_) => true,
5797 _ => with_host(|h| !h.is_null(v)),
5798 }
5799}
5800
5801// ══ standard library ═══════════════════════════════════════════════════════════
5802
5803/// Namespaces reachable as bare globals.
5804fn is_namespace(name: &str) -> bool {
5805 matches!(
5806 name,
5807 "console"
5808 | "Math"
5809 | "JSON"
5810 | "Object"
5811 | "Array"
5812 | "Number"
5813 | "String"
5814 | "Boolean"
5815 | "Symbol"
5816 | "Reflect"
5817 | "Promise"
5818 | "process"
5819 | "Buffer"
5820 | "URL"
5821 | "URLSearchParams"
5822 )
5823}
5824
5825const GLOBAL_FUNCS: &[&str] = &[
5826 "parseInt",
5827 "parseFloat",
5828 "isNaN",
5829 "isFinite",
5830 "encodeURIComponent",
5831 "decodeURIComponent",
5832 "encodeURI",
5833 "decodeURI",
5834 // Annex B legacy encoders. Still globals on every engine, and still called
5835 // by pre-`encodeURIComponent` library code.
5836 "escape",
5837 "unescape",
5838 "eval",
5839 "String",
5840 "Number",
5841 "Boolean",
5842 "Array",
5843 "Object",
5844 "Function",
5845 "Symbol",
5846 "Map",
5847 "Set",
5848 "WeakMap",
5849 "WeakSet",
5850 "Promise",
5851 "Error",
5852 "TypeError",
5853 "RangeError",
5854 "SyntaxError",
5855 "ReferenceError",
5856 "EvalError",
5857 "URIError",
5858 "AggregateError",
5859 "DOMException",
5860 "Iterator",
5861 "BigInt",
5862 "RegExp",
5863 "Date",
5864 "ArrayBuffer",
5865 "DataView",
5866 "Uint8Array",
5867 "Int8Array",
5868 "Uint8ClampedArray",
5869 "Int16Array",
5870 "Uint16Array",
5871 "Int32Array",
5872 "Uint32Array",
5873 "Float32Array",
5874 "Float64Array",
5875 "BigInt64Array",
5876 "BigUint64Array",
5877 "WeakRef",
5878 "FinalizationRegistry",
5879 "TextEncoder",
5880 "TextDecoder",
5881 // WHATWG Fetch globals (see `stdlib::fetch`).
5882 "fetch",
5883 "Headers",
5884 "Request",
5885 "Response",
5886 "Blob",
5887 "File",
5888 "FormData",
5889 "AbortController",
5890 "AbortSignal",
5891 "queueMicrotask",
5892 "setTimeout",
5893 "setInterval",
5894 "setImmediate",
5895 "clearTimeout",
5896 "clearInterval",
5897 "clearImmediate",
5898 "structuredClone",
5899 // Base64 helpers. They existed only as `require('buffer').btoa`, but node
5900 // exposes both as globals, so `btoa('abc')` was a ReferenceError.
5901 "btoa",
5902 "atob",
5903 "Proxy",
5904 "require",
5905 // CommonJS loader dispatch targets referenced by per-module `require`
5906 // closures (see `module.rs`); never written by user code.
5907 "__cjs_require",
5908 "__cjs_resolve",
5909 "__cjs_cache",
5910];
5911
5912const NS_METHODS: &[&str] = &[
5913 "console.log",
5914 "console.error",
5915 "console.warn",
5916 "console.info",
5917 "console.debug",
5918 "Math.abs",
5919 "Math.acos",
5920 "Math.acosh",
5921 "Math.asin",
5922 "Math.asinh",
5923 "Math.atan",
5924 "Math.atanh",
5925 "Math.atan2",
5926 "Math.ceil",
5927 "Math.cbrt",
5928 "Math.expm1",
5929 "Math.clz32",
5930 "Math.cos",
5931 "Math.cosh",
5932 "Math.exp",
5933 "Math.floor",
5934 "Math.fround",
5935 "Math.hypot",
5936 "Math.imul",
5937 "Math.log",
5938 "Math.log1p",
5939 "Math.log2",
5940 "Math.log10",
5941 "Math.max",
5942 "Math.min",
5943 "Math.pow",
5944 "Math.random",
5945 "Math.round",
5946 "Math.sign",
5947 "Math.sin",
5948 "Math.sinh",
5949 "Math.sqrt",
5950 "Math.tan",
5951 "Math.tanh",
5952 "Math.trunc",
5953 "JSON.stringify",
5954 "JSON.parse",
5955 "JSON.rawJSON",
5956 "JSON.isRawJSON",
5957 "Object.keys",
5958 "Object.values",
5959 "Object.entries",
5960 "Object.assign",
5961 "Object.freeze",
5962 "Object.is",
5963 "Object.fromEntries",
5964 "Object.getPrototypeOf",
5965 "Object.setPrototypeOf",
5966 "Object.create",
5967 "Object.getOwnPropertyNames",
5968 "Object.getOwnPropertySymbols",
5969 "Object.defineProperty",
5970 "Object.getOwnPropertyDescriptor",
5971 "Object.getOwnPropertyDescriptors",
5972 "Object.defineProperties",
5973 "Object.isFrozen",
5974 "Object.isSealed",
5975 "Object.seal",
5976 "Object.preventExtensions",
5977 "Object.isExtensible",
5978 "Object.hasOwn",
5979 "Object.groupBy",
5980 "Array.isArray",
5981 "Array.from",
5982 "Array.fromAsync",
5983 "Array.of",
5984 "Number.isFinite",
5985 "Number.isInteger",
5986 "Number.isNaN",
5987 "Number.isSafeInteger",
5988 "Number.parseFloat",
5989 "Number.parseInt",
5990 "String.fromCharCode",
5991 "String.fromCodePoint",
5992 "String.raw",
5993 "Symbol.for",
5994 "Symbol.keyFor",
5995 "BigInt.asIntN",
5996 "BigInt.asUintN",
5997 "Proxy.revocable",
5998 "Reflect.defineProperty",
5999 "Reflect.deleteProperty",
6000 "Reflect.apply",
6001 "Reflect.construct",
6002 "Reflect.get",
6003 "Reflect.getOwnPropertyDescriptor",
6004 "Reflect.getPrototypeOf",
6005 "Reflect.has",
6006 "Reflect.isExtensible",
6007 "Reflect.ownKeys",
6008 "Reflect.preventExtensions",
6009 "Reflect.set",
6010 "Reflect.setPrototypeOf",
6011 "Promise.resolve",
6012 "Promise.reject",
6013 "Promise.all",
6014 "Promise.allSettled",
6015 "Promise.race",
6016 "Promise.any",
6017 "Promise.withResolvers",
6018 "Promise.try",
6019 "RegExp.escape",
6020 "Error.isError",
6021 "Map.groupBy",
6022 "Response.json",
6023 "Response.error",
6024 "Response.redirect",
6025 "AbortSignal.abort",
6026 "AbortSignal.timeout",
6027 "process.nextTick",
6028 "Error.captureStackTrace",
6029 "require.resolve",
6030 "require.resolve.paths",
6031 "process.memoryUsage.rss",
6032];
6033
6034/// The `name` and `length` a builtin function reports, from the generated
6035/// intrinsic table ([`crate::arity::BUILTIN_ARITY`]). `None` for a key the table
6036/// does not cover — every non-function namespace (`Math`, `require('fs')`),
6037/// and the core-module functions, whose arity is not specified anywhere.
6038pub fn builtin_meta(key: &str) -> Option<(&'static str, u32)> {
6039 crate::arity::BUILTIN_ARITY
6040 .binary_search_by(|(k, _, _)| (*k).cmp(key))
6041 .ok()
6042 .map(|i| {
6043 let (_, name, len) = crate::arity::BUILTIN_ARITY[i];
6044 (name, len)
6045 })
6046}
6047
6048/// The `name` a builtin function reports. The table answers for an intrinsic;
6049/// anything else falls back to the last segment of the key, which is what the
6050/// name is for every builtin this frontend synthesizes: `@proto:TypedArray:set`
6051/// is `set` and `fs.readFileSync` is `readFileSync`. Reporting the whole key was
6052/// how `[Function: @proto:TypedArray:set]` reached `console.log`.
6053pub fn builtin_name(key: &str) -> &str {
6054 if let Some((name, _)) = builtin_meta(key) {
6055 return name;
6056 }
6057 match key.strip_prefix("@proto:") {
6058 Some(rest) => rest.rsplit(':').next().unwrap_or(rest),
6059 // An accessor's getter is named `get <member>` (10.2.9 SetFunctionName
6060 // with a `get` prefix), which is what `util.inspect` prints for it and
6061 // what a library reads to identify one.
6062 None => key.rsplit('.').next().unwrap_or(key),
6063 }
6064}
6065
6066/// The `name` of an intrinsic accessor's getter thunk, or `None` for anything
6067/// else. Kept out of `builtin_name`'s `&str` return, which cannot own the
6068/// `"get size"` it has to build.
6069pub fn proto_getter_name(key: &str) -> Option<String> {
6070 let (verb, rest) = match key.strip_prefix("@protoget:") {
6071 Some(rest) => ("get", rest),
6072 None => ("set", key.strip_prefix("@protoset:")?),
6073 };
6074 let (_, member) = rest.split_once(':')?;
6075 Some(format!("{verb} {member}"))
6076}
6077
6078pub fn is_known_builtin(name: &str) -> bool {
6079 // Binary search over a sorted INDEX of the two tables rather than a scan of
6080 // both. This runs on every call whose callee is a builtin — `call_method`
6081 // asks it before dispatching `Math.max(…)` or `JSON.parse(…)` — and the
6082 // answer came only after a full scan of `GLOBAL_FUNCS` (77) plus a scan of
6083 // `NS_METHODS` up to the entry — 106 string comparisons for `Math.max`, 120
6084 // for `Object.keys` — because those tables are ordered for ENUMERATION (V8's
6085 // own order for `Math`/`Number`/`Reflect`), not for lookup. Eight probes
6086 // now. The index is built once per process and derived FROM those tables, so
6087 // it cannot drift from them.
6088 //
6089 // That is an operation count, not a measured time, and NO wall-clock win is
6090 // claimed. Re-measured in isolation (this hunk alone applied to the previous
6091 // commit, interleaved against it, minimums over ten rounds each): the A/B
6092 // ratio came out 0.753, 1.072, 0.994 and 0.744 across four repeats, while
6093 // the A/A control — the SAME binary under both labels — came out 1.084,
6094 // 1.072, 0.787 and 1.093. The A/B spread lies inside the A/A spread, so on
6095 // this machine the change is not distinguishable from noise. It is kept for
6096 // the comparison count and because it cannot drift from the tables it is
6097 // derived from, not because anything got faster.
6098 static SORTED: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
6099 let sorted = SORTED.get_or_init(|| {
6100 let mut v: Vec<&'static str> = GLOBAL_FUNCS
6101 .iter()
6102 .chain(NS_METHODS.iter())
6103 .copied()
6104 .collect();
6105 v.sort_unstable();
6106 v
6107 });
6108 sorted.binary_search(&name).is_ok() || is_namespace(name) || crate::stdlib::is_method(name)
6109}
6110
6111// ── dynamic functions (runtime source → callable) ────────────────────────────
6112
6113/// Build a callable from a complete function-expression source text — the ONE
6114/// dynamic-function generator on this frontend.
6115///
6116/// `src` is the exact source V8 synthesizes for the construct, WITHOUT the
6117/// wrapping parentheses needed to parse it as an expression: those are added
6118/// here, and `src` itself is retained so `Function.prototype.toString` reports
6119/// what V8 reports. The two callers synthesize different text and both shapes
6120/// are observable — see `stdlib::vm::compile_function` for the measured diff.
6121///
6122/// The body runs in the MODULE scope, never the constructing function's scope
6123/// (20.2.1.1.1 step 26 instantiates a dynamic function's body against the
6124/// *global* environment). That also makes a `var` inside the body a function
6125/// local: measured on node v26.7.0, `new Function('a','var zz = 5; return zz + a')`
6126/// returns 6 and leaves `globalThis.zz` `undefined`.
6127pub fn dynamic_function(src: &str) -> Result<Value, String> {
6128 let f = crate::eval_in_global_scope(&format!("({src})"))?;
6129 with_host(|h| {
6130 let s = h.new_str(src.to_string());
6131 h.set_fn_prop(&f, "@@source", s);
6132 });
6133 Ok(f)
6134}
6135
6136/// `new Function(p1, …, pN, body)` / `Function(p1, …, pN, body)`.
6137///
6138/// Argument convention (20.2.1.1.1): the LAST argument is the body and the rest
6139/// are parameter-list fragments joined with `,` — so a fragment may itself hold
6140/// several parameters (`new Function('a,b', 'c', …)` takes three). With no
6141/// arguments at all, both the parameter list and the body are empty.
6142///
6143/// Measured on node v26.7.0:
6144///
6145/// ```text
6146/// new Function('a','b','return a+b').toString() === 'function anonymous(a,b\n) {\nreturn a+b\n}'
6147/// new Function().toString() === 'function anonymous(\n) {\n\n}'
6148/// new Function('a,b','c','return [a,b,c]').length === 3
6149/// new Function('a','b','return a+b').name === 'anonymous'
6150/// ```
6151pub fn function_ctor(args: &[Value]) -> Result<Value, String> {
6152 let parts: Vec<String> = args.iter().map(|a| with_host(|h| h.str_of(a))).collect();
6153 let (params, body) = match parts.split_last() {
6154 Some((body, params)) => (params.join(","), body.clone()),
6155 None => (String::new(), String::new()),
6156 };
6157 dynamic_function(&format!("function anonymous({params}\n) {{\n{body}\n}}"))
6158}
6159
6160/// `eval(src)`. `direct` selects the scope the source runs in: a DIRECT eval —
6161/// the literal `eval(...)` call form — evaluates in the CALLER's scope, every
6162/// other route to the same function value is an INDIRECT eval and evaluates in
6163/// the global scope (ECMA-262 19.2.1.1 `PerformEval`). The two are told apart in
6164/// `host::call_named`, which `ops::CALL` reaches and `ops::CALL_VALUE`/`APPLY`
6165/// do not.
6166///
6167/// A non-string argument is returned unchanged (19.2.1.1 step 2).
6168pub fn eval_source(arg: Option<&Value>, direct: bool) -> Result<Value, String> {
6169 let v = arg.cloned().unwrap_or(Value::Undef);
6170 let is_string =
6171 matches!(v, Value::Str(_)) || with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_))));
6172 if !is_string {
6173 return Ok(v);
6174 }
6175 let src = with_host(|h| h.str_of(&v));
6176 // A DIRECT eval inherits the caller's strictness (19.2.1.1 step 10), which
6177 // decides both the early errors the COMPILE raises and the variable
6178 // environment below. An INDIRECT one is global-scope sloppy code.
6179 let caller_strict = direct && with_host(|h| h.current_strict());
6180 let chunk = crate::load_merged(crate::compile_completion_strict(&src, caller_strict)?);
6181 if !direct {
6182 return host::run_chunk_in_global_scope(chunk);
6183 }
6184 // A STRICT direct eval gets its OWN variable environment (19.2.1.1 step 12),
6185 // so its `var`s and function declarations die with it. Only a SLOPPY one
6186 // shares the caller's, which is the form that can inject a binding — and
6187 // sharing it unconditionally meant `eval('var x=1')` inside strict code
6188 // left `x` behind.
6189 let strict = caller_strict
6190 || src.trim_start().starts_with("'use strict'")
6191 || src.trim_start().starts_with("\"use strict\"");
6192 if !strict {
6193 // 19.2.1.1 steps 12-13: a SLOPPY direct eval shares the caller's
6194 // VARIABLE environment — which is what lets `eval('var x=1')` inject a
6195 // binding — but gets a fresh LEXICAL one of its own. A `let`, `const`
6196 // or `class` declared inside therefore dies with the eval; every one of
6197 // them was landing in the caller's scope, so `eval('let a=1')` left `a`
6198 // behind and `let a=1; eval('let a=2')` overwrote it.
6199 //
6200 // `push_scope` is exactly that split: `var` and a hoisted function
6201 // declaration bind to `base_env`, which this does not touch.
6202 with_host(|h| h.push_scope());
6203 let out = host::run_chunk_on(chunk);
6204 with_host(|h| h.pop_scope());
6205 return out;
6206 }
6207 let prev = with_host(|h| h.push_var_scope());
6208 let out = host::run_chunk_on(chunk);
6209 with_host(|h| h.pop_var_scope(prev));
6210 out
6211}
6212
6213/// Call a resolved builtin function (global or `namespace.method`).
6214pub fn call_builtin_function(name: &str, args: Vec<Value>) -> Result<Value, String> {
6215 // `require(spec)`: the ENTRY script's top-level require — core module first,
6216 // else the CommonJS loader resolving from the entry file's directory.
6217 if name == "require" {
6218 let spec = with_host(|h| h.str_of(&arg0(&args)));
6219 return crate::module::require(&spec, &crate::module::entry_dir());
6220 }
6221 // `__cjs_require(spec, fromDir)`: a per-module `require` closure's dispatch
6222 // into the loader, resolving `spec` against the module's own directory.
6223 if name == "__cjs_require" {
6224 let spec = with_host(|h| h.str_of(&arg0(&args)));
6225 let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6226 return crate::module::require(&spec, std::path::Path::new(&from));
6227 }
6228 if name == "process.memoryUsage.rss" {
6229 return Ok(crate::stdlib::process::memory_usage_rss());
6230 }
6231 if name == "require.resolve.paths" {
6232 let spec = with_host(|h| h.str_of(&arg0(&args)));
6233 // A core module is not looked up on disk at all.
6234 if crate::stdlib::is_core(&spec) {
6235 return Ok(with_host(|h| h.null()));
6236 }
6237 let dirs = crate::module::resolve_paths(&spec, &crate::module::entry_dir());
6238 return Ok(with_host(|h| {
6239 let items: Vec<Value> = dirs.into_iter().map(|d| h.new_str(d)).collect();
6240 h.new_array(items)
6241 }));
6242 }
6243 // A `require.extensions` entry. This runtime's loader does not dispatch
6244 // through the map, so calling one is the loader's own behaviour for that
6245 // extension rather than a hook point.
6246 if let Some(ext) = name.strip_prefix("@@extension:") {
6247 let _ = ext;
6248 return Ok(Value::Undef);
6249 }
6250 // `require.resolve(spec)` at the ENTRY level: resolve from the entry dir.
6251 if name == "require.resolve" {
6252 let spec = with_host(|h| h.str_of(&arg0(&args)));
6253 if crate::stdlib::is_core(&spec) {
6254 return Ok(with_host(|h| h.new_str(spec)));
6255 }
6256 return match crate::module::resolve(&spec, &crate::module::entry_dir()) {
6257 Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6258 None => Err(crate::host::plain_coded_error(
6259 "Error",
6260 "MODULE_NOT_FOUND",
6261 &format!("Cannot find module '{spec}'"),
6262 )),
6263 };
6264 }
6265 // `__cjs_resolve(spec, fromDir)`: `require.resolve` — the resolved absolute
6266 // path (core modules resolve to the bare specifier, as in Node).
6267 if name == "__cjs_resolve" {
6268 let spec = with_host(|h| h.str_of(&arg0(&args)));
6269 let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6270 if crate::stdlib::is_core(&spec) {
6271 return Ok(with_host(|h| h.new_str(spec)));
6272 }
6273 return match crate::module::resolve(&spec, std::path::Path::new(&from)) {
6274 Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6275 None => Err(crate::host::plain_coded_error(
6276 "Error",
6277 "MODULE_NOT_FOUND",
6278 &format!("Cannot find module '{spec}'"),
6279 )),
6280 };
6281 }
6282 // `Error.captureStackTrace(target[, ctor])`: V8's stack capture. Sets
6283 // `target.stack`; when a custom `Error.prepareStackTrace` is installed (the
6284 // stack-introspection pattern used by `depd`), it is called with a synthetic
6285 // CallSite array and its result becomes `.stack`, else `.stack` is a string.
6286 if name == "Error.captureStackTrace" {
6287 let target = arg0(&args);
6288 let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
6289 let stack = match prep {
6290 Some(f)
6291 if matches!(
6292 with_host(|h| h.get(&f).cloned()),
6293 Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
6294 ) =>
6295 {
6296 let sites = crate::module::callsite_stack(10)?;
6297 host::invoke(&f, vec![target.clone(), sites], None)?
6298 }
6299 _ => with_host(|h| h.new_str("")),
6300 };
6301 let _ = set_property(&target, "stack", stack);
6302 return Ok(Value::Undef);
6303 }
6304 // Native stdlib module methods (path/os/fs/util/assert/crypto/buffer/url).
6305 if let Some(r) = crate::stdlib::call(name, &args) {
6306 return r;
6307 }
6308 match name {
6309 // Node's DEFAULT `Error.prepareStackTrace`: the `Name: message` header
6310 // followed by one ` at <site>` line per call site. Reachable because
6311 // the read hands the hook out, and a library may call it directly to
6312 // render a stack it captured.
6313 DEFAULT_PREPARE => {
6314 let err = arg0(&args);
6315 let header = with_host(|h| {
6316 let name = host::lookup_chain(h, &err, "name")
6317 .map(|v| h.str_of(&v))
6318 .unwrap_or_else(|| "Error".to_string());
6319 let msg = host::lookup_chain(h, &err, "message")
6320 .map(|v| h.str_of(&v))
6321 .unwrap_or_default();
6322 if msg.is_empty() {
6323 name
6324 } else {
6325 format!("{name}: {msg}")
6326 }
6327 });
6328 let sites = args.get(1).cloned().unwrap_or(Value::Undef);
6329 let lines = with_host(|h| match h.get(&sites) {
6330 Some(JsObj::Array(items)) => items.clone(),
6331 _ => Vec::new(),
6332 });
6333 let mut out = header;
6334 for s in lines {
6335 let rendered = host::to_string_value(&s)
6336 .map(|v| with_host(|h| h.str_of(&v)))
6337 .unwrap_or_default();
6338 out.push_str("\n at ");
6339 out.push_str(&rendered);
6340 }
6341 Ok(with_host(|h| h.new_str(out)))
6342 }
6343 "console.log" | "console.info" | "console.debug" => {
6344 print_line(&args, false)?;
6345 Ok(Value::Undef)
6346 }
6347 "console.error" | "console.warn" => {
6348 print_line(&args, true)?;
6349 Ok(Value::Undef)
6350 }
6351 "parseInt" | "Number.parseInt" => Ok(Value::Float(parse_int(&args)?)),
6352 "parseFloat" | "Number.parseFloat" => Ok(Value::Float(parse_float(&args)?)),
6353 // `isNaN`/`isFinite` are `ToNumber(x)` too (19.2.3/4).
6354 "isNaN" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_nan())),
6355 "isFinite" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_finite())),
6356 "encodeURIComponent" => uri_encode(&arg_to_string(&args, 0)?, false),
6357 "encodeURI" => uri_encode(&arg_to_string(&args, 0)?, true),
6358 "decodeURIComponent" => uri_decode(&arg_to_string(&args, 0)?, false),
6359 "decodeURI" => uri_decode(&arg_to_string(&args, 0)?, true),
6360 "escape" => legacy_escape(&with_host(|h| h.str_of(&arg0(&args)))),
6361 "unescape" => legacy_unescape(&with_host(|h| h.str_of(&arg0(&args)))),
6362 // Reaching `eval` through this table means the eval FUNCTION VALUE was
6363 // called — `(0, eval)(src)`, `const e = eval; e(src)`, `[eval][0](src)`.
6364 // Those are INDIRECT evals and run in the global scope. A literal
6365 // `eval(src)` is intercepted earlier, in `host::call_named`.
6366 "eval" => eval_source(args.first(), false),
6367 // `new Function(...)` and `Function(...)` are the same operation
6368 // (20.2.1.1 `CreateDynamicFunction` is reached from both [[Call]] and
6369 // [[Construct]]), so both route to the one generator.
6370 "Function" => function_ctor(&args),
6371 // `Buffer(arg[, encodingOrOffset[, length]])` — the deprecated call form
6372 // (DEP0005). Node still supports it and still routes it to the same place
6373 // `new Buffer` goes, which is why `safe-buffer`'s legacy `SafeBuffer`
6374 // wrapper is just `return Buffer(arg, encodingOrOffset, length)`. Measured
6375 // on node v26.7.0: `Buffer('abc').toString() === 'abc'`,
6376 // `Buffer([1,2]).toString('hex') === '0102'`, `Buffer(3).length === 3`.
6377 // Node emits DEP0005 once, on stderr, through the same one-shot machinery
6378 // `url.parse`'s DEP0169 uses, so this does too rather than staying silent
6379 // where Node warns.
6380 "Buffer" => {
6381 crate::stdlib::process::emit_deprecation_warning(
6382 "DEP0005",
6383 "Buffer() is deprecated due to security and usability issues. \
6384 Please use the Buffer.alloc(), Buffer.allocUnsafe(), or \
6385 Buffer.from() methods instead.",
6386 );
6387 crate::stdlib::construct("Buffer", &args)
6388 .unwrap_or_else(|| Err(host::type_error("Buffer is not a function")))
6389 }
6390 "Number.isInteger" => Ok(Value::Bool(is_integer(arg0(&args)))),
6391 "Number.isSafeInteger" => Ok(Value::Bool(is_safe_integer(arg0(&args)))),
6392 "Number.isNaN" => Ok(Value::Bool(
6393 matches!(arg0(&args), Value::Float(f) if f.is_nan()),
6394 )),
6395 "Number.isFinite" => Ok(Value::Bool(
6396 matches!(arg0(&args), Value::Float(f) if f.is_finite())
6397 || matches!(arg0(&args), Value::Int(_)),
6398 )),
6399 "String" => {
6400 if args.is_empty() {
6401 Ok(with_host(|h| h.new_str("")))
6402 } else {
6403 // A symbol argument stringifies to `Symbol(desc)` (explicit String()
6404 // is allowed); everything else via ToString method dispatch.
6405 host::string_ctor_value(&args[0])
6406 }
6407 }
6408 // `Number(v)` is NOT plain ToNumber: 21.1.1.1 step 2 converts the object
6409 // first and then explicitly ACCEPTS a BigInt, returning its mathematical
6410 // value as a Number. Only `Number` does — `+v` and `Math.abs(v)` reject
6411 // one — which is why this cannot just call `to_number_value`.
6412 "Number" => Ok(Value::Float(if args.is_empty() {
6413 0.0
6414 } else {
6415 let prim = host::to_primitive(&args[0], "number")?;
6416 match with_host(|h| h.as_bigint(&prim)) {
6417 Some(b) => host::bigint_to_f64(&b),
6418 None => host::to_number_value(&prim)?,
6419 }
6420 })),
6421 "BigInt" => bigint_ctor(&arg0(&args)),
6422 "RegExp" => regexp_ctor(&args),
6423 "BigInt.asIntN" | "BigInt.asUintN" => bigint_as_n(name.ends_with("asUintN"), &args),
6424 "Boolean" => Ok(Value::Bool(with_host(|h| h.truthy(&arg0(&args))))),
6425 // Each argument is truncated to a uint16 and taken as one code UNIT, so
6426 // `String.fromCharCode(0x1D4B3)` is U+D4B3, NOT the astral U+1D4B3, and
6427 // a surrogate PAIR of arguments composes into one character.
6428 "String.fromCharCode" => Ok(with_host(|h| {
6429 let units: Vec<u16> = args
6430 .iter()
6431 .map(|a| crate::utf16::to_uint16(h.to_number(a)))
6432 .collect();
6433 let s = crate::utf16::to_string_lossy(&units);
6434 h.new_str(s)
6435 })),
6436 // `fromCodePoint` takes whole code POINTS and rejects anything that is
6437 // not one — including a lone surrogate, which `fromCharCode` accepts.
6438 "String.fromCodePoint" => {
6439 let mut s = String::new();
6440 for a in &args {
6441 let n = with_host(|h| h.to_number(a));
6442 let cp = if n.is_finite() && n.trunc() == n && (0.0..=0x10FFFF as f64).contains(&n)
6443 {
6444 char::from_u32(n as u32)
6445 } else {
6446 None
6447 };
6448 match cp {
6449 Some(c) => s.push(c),
6450 None => {
6451 return Err(format!(
6452 "RangeError: Invalid code point {}",
6453 with_host(|h| h.str_of(a))
6454 ))
6455 }
6456 }
6457 }
6458 Ok(new_s(s))
6459 }
6460 "String.raw" => string_raw(&args),
6461 // `Array(5)` === `new Array(5)` (length-5 empty), but `Array.of(5)` is `[5]`.
6462 "Array" => construct_builtin("Array", args),
6463 "Array.of" => construct_array_like(host::current_static_this(), args),
6464 // 23.1.2.2 `IsArray` follows a Proxy to its `[[ProxyTarget]]` rather than
6465 // consulting any trap, so `Array.isArray(new Proxy([], {}))` is `true`.
6466 "Array.isArray" => {
6467 let v = arg0(&args);
6468 let subject = crate::proxy::ultimate_target(&v).unwrap_or(v);
6469 Ok(Value::Bool(
6470 matches!(
6471 with_host(|h| h.get(&subject).cloned()),
6472 Some(JsObj::Array(_))
6473 ) && !is_arguments(&subject),
6474 ))
6475 }
6476 "Array.from" => array_from(args),
6477 "Array.fromAsync" => array_from_async(args),
6478 "Object" => Ok(object_call(args)),
6479 "Object.keys" => object_keys(args, 0),
6480 "Object.values" => object_keys(args, 1),
6481 "Object.entries" => object_keys(args, 2),
6482 "Object.assign" => object_assign(args),
6483 "Object.freeze" => {
6484 let v = arg0(&args);
6485 reject_sealing_a_view(&v, "freeze")?;
6486 if seal_proxy(&v, true)? {
6487 return Ok(v);
6488 }
6489 with_host(|h| h.seal_object(&v, true));
6490 Ok(v)
6491 }
6492 "Object.seal" => {
6493 let v = arg0(&args);
6494 reject_sealing_a_view(&v, "seal")?;
6495 if seal_proxy(&v, false)? {
6496 return Ok(v);
6497 }
6498 with_host(|h| h.seal_object(&v, false));
6499 Ok(v)
6500 }
6501 "Object.preventExtensions" => {
6502 let v = arg0(&args);
6503 if crate::proxy::prevent_extensions(&v)? {
6504 return Ok(v);
6505 }
6506 with_host(|h| h.prevent_extensions(&v));
6507 Ok(v)
6508 }
6509 // A PRIMITIVE has no integrity to speak of and 7.3.15/16 answer for it
6510 // without coercion: it is not extensible, and vacuously frozen and
6511 // sealed. Reporting it extensible and unfrozen was the opposite of
6512 // every one of the three.
6513 "Object.isFrozen" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6514 "Object.isSealed" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6515 "Object.isExtensible" if is_primitive_arg(&args) => Ok(Value::Bool(false)),
6516 "Object.isFrozen" => integrity_level(&arg0(&args), true),
6517 "Object.isSealed" => integrity_level(&arg0(&args), false),
6518 "Object.isExtensible" => {
6519 let v = arg0(&args);
6520 match crate::proxy::is_extensible(&v)? {
6521 Some(b) => Ok(Value::Bool(b)),
6522 None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6523 }
6524 }
6525 // Object.is — SameValue: like `===` but NaN is equal to NaN and +0 is
6526 // distinct from -0.
6527 "Object.is" => {
6528 let a = arg0(&args);
6529 let b = args.get(1).cloned().unwrap_or(Value::Undef);
6530 let num = |v: &Value| match v {
6531 Value::Int(n) => Some(*n as f64),
6532 Value::Float(f) => Some(*f),
6533 _ => None,
6534 };
6535 let r = match (num(&a), num(&b)) {
6536 (Some(x), Some(y)) => {
6537 if x.is_nan() && y.is_nan() {
6538 true
6539 } else if x == 0.0 && y == 0.0 {
6540 x.is_sign_negative() == y.is_sign_negative()
6541 } else {
6542 x == y
6543 }
6544 }
6545 _ => with_host(|h| h.strict_eq(&a, &b)),
6546 };
6547 Ok(Value::Bool(r))
6548 }
6549 "Object.fromEntries" => object_from_entries(args),
6550 // `[[GetPrototypeOf]]`: a Proxy answers from its trap (which may throw),
6551 // so the proxy form cannot share `prototype_of`'s infallible signature.
6552 // `Object.getPrototypeOf` coerces a primitive to its wrapper and
6553 // answers; `Reflect.getPrototypeOf` requires an object (28.1.8).
6554 "Object.getPrototypeOf" | "Reflect.getPrototypeOf" => {
6555 if name == "Reflect.getPrototypeOf" {
6556 reflect_require_object(&arg0(&args), "getPrototypeOf")?;
6557 }
6558 let v = arg0(&args);
6559 match crate::proxy::get_prototype_of(&v)? {
6560 Some(p) => Ok(p),
6561 None => Ok(prototype_of(&v)),
6562 }
6563 }
6564 "Object.setPrototypeOf" => {
6565 let obj = arg0(&args);
6566 let proto = args.get(1).cloned().unwrap_or(Value::Undef);
6567 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6568 reject_bad_prototype(&proto)?;
6569 crate::proxy::set_prototype_of(&obj, &proto)?;
6570 return Ok(obj);
6571 }
6572 // 20.1.2.23: `RequireObjectCoercible` on the target, then the
6573 // prototype type check, then — only for an actual object target —
6574 // the extensibility check. A PRIMITIVE target is returned untouched
6575 // (`Object.setPrototypeOf(1, {})` is `1`), which is why the
6576 // extensibility test cannot come first.
6577 if with_host(|h| matches!(obj, Value::Undef) || h.is_null(&obj)) {
6578 return Err(host::type_error(
6579 "Object.setPrototypeOf called on null or undefined",
6580 ));
6581 }
6582 reject_bad_prototype(&proto)?;
6583 if with_host(|h| is_object_like(h, &obj)) {
6584 // Setting the SAME prototype is a no-op and stays legal even on a
6585 // frozen object: node v26.7.0 accepts
6586 // `Object.setPrototypeOf(Object.freeze({}), Object.prototype)`.
6587 // `prototype_of`, not `proto_of`: an object with no EXPLICIT
6588 // link still has `Object.prototype`, and comparing against the
6589 // absent link would call that a change.
6590 if would_cycle(&obj, &proto) {
6591 return Err(host::type_error("Cyclic __proto__ value"));
6592 }
6593 if !same_prototype(&obj, &proto) && !with_host(|h| h.is_extensible(&obj)) {
6594 // The receiver is named by its brand, as every other
6595 // refusal names it — a NULL-PROTOTYPE object is
6596 // `[object Object]`, not `#<Object>`, because it has no
6597 // constructor to name.
6598 return Err(host::type_error(&format!(
6599 "{} is not extensible",
6600 no_side_effects_string(&obj)
6601 )));
6602 }
6603 with_host(|h| h.set_proto(&obj, proto));
6604 }
6605 Ok(obj)
6606 }
6607 "Object.create" => object_create(args),
6608 "Object.getOwnPropertyNames" => object_keys(args, 3),
6609 "Object.getOwnPropertySymbols" => {
6610 let v = arg0(&args);
6611 require_object_coercible(&v)?;
6612 let syms = proxy_or_own_symbol_keys(&v)?;
6613 Ok(with_host(|h| h.new_array(syms)))
6614 }
6615 // `Object.hasOwn(obj, key)` — the static form of `hasOwnProperty`.
6616 "Object.hasOwn" => {
6617 let obj = arg0(&args);
6618 let key = args.get(1).cloned().unwrap_or(Value::Undef);
6619 object_builtin_method(&obj, "hasOwnProperty", vec![key])
6620 }
6621 "Object.defineProperty" => object_define_property(args),
6622 "Object.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6623 "Object.getOwnPropertyDescriptors" => object_get_own_descriptors(args),
6624 "Object.defineProperties" => object_define_properties(args),
6625 // `Object.groupBy(items, cb)` (ES2024): group into a null-prototype object
6626 // keyed by `ToPropertyKey(cb(item, i))`, each value an array of members.
6627 "Object.groupBy" => object_group_by(args),
6628 "Symbol" => Ok(with_host(|h| {
6629 let desc = args
6630 .first()
6631 .filter(|a| !matches!(a, Value::Undef))
6632 .map(|a| h.str_of(a));
6633 h.new_symbol(desc)
6634 })),
6635 "Symbol.for" => Ok(with_host(|h| {
6636 let key = h.str_of(&arg0(&args));
6637 h.symbol_for(&key)
6638 })),
6639 // `Symbol.keyFor(sym)` (20.4.2.6) is a REGISTRY lookup, not a
6640 // description read: it answers only for symbols `Symbol.for` created.
6641 // Returning the description made every symbol look registered —
6642 // `Symbol.keyFor(Symbol("k"))` was `"k"` where node says `undefined`.
6643 "Symbol.keyFor" => Ok(with_host(|h| h.symbol_registry_key(&arg0(&args)))),
6644 "Map" | "WeakMap" | "Set" | "WeakSet" | "Promise" => construct_builtin(name, args),
6645 // `Proxy` has no `[[Call]]` slot: it is constructor-only (28.2.1).
6646 "Proxy" => Err(host::type_error("Constructor Proxy requires 'new'")),
6647 "Proxy.revocable" => crate::proxy::revocable(&args),
6648 // `Reflect.ownKeys` reports EVERY own key, non-enumerable included —
6649 // the same set as `getOwnPropertyNames` (node-js has no symbol-keyed
6650 // own properties, so there is no second half to append).
6651 // `Reflect.ownKeys` is `OwnPropertyKeys` (7.3.23): every own key,
6652 // non-enumerable included, strings first and then the SYMBOLS.
6653 "Reflect.ownKeys" => {
6654 let v = arg0(&args);
6655 reflect_require_object(&v, "ownKeys")?;
6656 let names = object_keys(args, 3)?;
6657 let syms = proxy_or_own_symbol_keys(&v)?;
6658 if syms.is_empty() {
6659 return Ok(names);
6660 }
6661 let mut all = with_host(|h| h.iter_vec(&names)).unwrap_or_default();
6662 all.extend(syms);
6663 Ok(with_host(|h| h.new_array(all)))
6664 }
6665 "Reflect.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6666 // `Reflect.defineProperty` REPORTS success as a boolean where
6667 // `Object.defineProperty` throws (28.1.3). It was propagating the
6668 // throw, so the whole point of the reflective form was lost.
6669 "Reflect.defineProperty" => {
6670 reflect_require_object(&arg0(&args), "defineProperty")?;
6671 Ok(Value::Bool(object_define_property(args).is_ok()))
6672 }
6673 "Reflect.deleteProperty" => {
6674 let obj = arg0(&args);
6675 reflect_require_object(&obj, "deleteProperty")?;
6676 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6677 Ok(Value::Bool(delete_property(&obj, &k)?))
6678 }
6679 "Reflect.setPrototypeOf" => {
6680 let obj = arg0(&args);
6681 let p = args.get(1).cloned().unwrap_or(Value::Undef);
6682 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6683 crate::proxy::set_prototype_of(&obj, &p)?;
6684 return Ok(Value::Bool(true));
6685 }
6686 // 10.1.2.1: a NON-EXTENSIBLE object refuses a prototype change —
6687 // unless the new one is what it already has, which is a no-op. It
6688 // reported success and rewrote the link.
6689 // `Reflect` reports a refusal rather than throwing, for a cycle as
6690 // for a non-extensible receiver.
6691 if would_cycle(&obj, &p) {
6692 return Ok(Value::Bool(false));
6693 }
6694 if !with_host(|h| h.is_extensible(&obj)) {
6695 return Ok(Value::Bool(same_prototype(&obj, &p)));
6696 }
6697 with_host(|h| h.set_proto(&obj, p));
6698 Ok(Value::Bool(true))
6699 }
6700 "Reflect.isExtensible" => {
6701 let v = arg0(&args);
6702 match crate::proxy::is_extensible(&v)? {
6703 Some(b) => Ok(Value::Bool(b)),
6704 None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6705 }
6706 }
6707 "Reflect.preventExtensions" => {
6708 let v = arg0(&args);
6709 if crate::proxy::prevent_extensions(&v)? {
6710 return Ok(Value::Bool(true));
6711 }
6712 with_host(|h| h.prevent_extensions(&v));
6713 Ok(Value::Bool(true))
6714 }
6715 // `Reflect.apply(target, thisArg, argsList)` / `Reflect.construct(t, a)`.
6716 "Reflect.apply" => {
6717 let f = arg0(&args);
6718 let this = args.get(1).cloned();
6719 let list = create_list_from_array_like(&args.get(2).cloned().unwrap_or(Value::Undef))?;
6720 host::invoke(&f, list, this.filter(|t| !with_host(|h| h.is_nullish(t))))
6721 }
6722 // `Reflect.construct(target, args, newTarget)` — the optional third
6723 // argument decides which constructor's `prototype` the instance gets
6724 // (28.1.2). It was ignored, so the result always inherited from
6725 // `target` and `instanceof newTarget` was false.
6726 "Reflect.construct" => {
6727 let f = arg0(&args);
6728 let list = create_list_from_array_like(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6729 let new_target = args.get(2).cloned().unwrap_or_else(|| f.clone());
6730 host::construct_nt(&f, list, new_target)
6731 }
6732 "Reflect.has" => {
6733 let obj = arg0(&args);
6734 reflect_require_object(&obj, "has")?;
6735 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6736 Ok(Value::Bool(has_property(&obj, &k)?))
6737 }
6738 // `Reflect.get(target, key, receiver)` — the optional third argument is
6739 // what a getter sees as `this` (28.1.6). Defaults to the target.
6740 "Reflect.get" => {
6741 let obj = arg0(&args);
6742 reflect_require_object(&obj, "get")?;
6743 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6744 let receiver = args.get(2).cloned().unwrap_or_else(|| obj.clone());
6745 get_property_recv(&obj, &k, &receiver)
6746 }
6747 // `Reflect.set(target, key, value, receiver)` — the optional fourth
6748 // argument is what a setter sees as `this`, and where a DATA property
6749 // lands (28.1.13). It was ignored: the setter ran against the target
6750 // and the property was written there.
6751 "Reflect.set" => {
6752 let obj = arg0(&args);
6753 reflect_require_object(&obj, "set")?;
6754 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6755 let v = args.get(2).cloned().unwrap_or(Value::Undef);
6756 let receiver = args.get(3).cloned().unwrap_or_else(|| obj.clone());
6757 Ok(Value::Bool(set_with_receiver(&obj, &k, v, &receiver)?))
6758 }
6759 "JSON.stringify" => json_stringify(args),
6760 "JSON.parse" => json_parse(args),
6761 "JSON.rawJSON" => json_raw(args),
6762 "JSON.isRawJSON" => json_is_raw(args),
6763 "structuredClone" => structured_clone(args),
6764 // The deferred drain a `Readable.from` schedules; the suffix is the
6765 // stream's heap index.
6766 _ if name.starts_with("@@transformCb:") => {
6767 let idx: u32 = name["@@transformCb:".len()..].parse().unwrap_or(0);
6768 crate::stdlib::stream::transform_callback(&Value::Obj(idx), &args)?;
6769 Ok(Value::Undef)
6770 }
6771 _ if name.starts_with("@@streamFlush:") => {
6772 let idx: u32 = name["@@streamFlush:".len()..].parse().unwrap_or(0);
6773 crate::stdlib::stream::flush_from(&Value::Obj(idx))?;
6774 Ok(Value::Undef)
6775 }
6776 // Same implementation the `buffer` module exposes; only the binding was
6777 // missing.
6778 "btoa" | "atob" => crate::stdlib::buffer::module_call(name, &args)
6779 .unwrap_or_else(|| Err(host::type_error(&format!("{name} is not a function")))),
6780 "fetch" => crate::stdlib::fetch::fetch(&args),
6781 // An `AbortSignal.timeout` deadline reached its macrotask: the thunk's
6782 // suffix is the signal's heap index.
6783 _ if name.starts_with("@@aborttimeout:") => {
6784 let idx: u32 = name["@@aborttimeout:".len()..].parse().unwrap_or(0);
6785 crate::stdlib::fetch::fire_timeout_abort(idx)
6786 }
6787 // The `callback` handed to a `new Writable({ write(chunk, enc, cb) })`
6788 // implementation. Nothing here waits on backpressure, so it only has to
6789 // BE callable — an implementation that ends with `cb()`, which the
6790 // stream contract requires, would otherwise throw.
6791 "@@streamWriteCallback" => Ok(Value::Undef),
6792 "queueMicrotask" | "process.nextTick" => {
6793 let cb = arg0(&args);
6794 require_callback(&cb)?;
6795 let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
6796 enqueue_microtask(name == "process.nextTick", cb, rest);
6797 Ok(Value::Undef)
6798 }
6799 "setTimeout" | "setInterval" | "setImmediate" => {
6800 require_callback(&arg0(&args))?;
6801 Ok(schedule_timer(name, args))
6802 }
6803 "clearTimeout" | "clearInterval" | "clearImmediate" => {
6804 clear_timer(&arg0(&args));
6805 Ok(Value::Undef)
6806 }
6807 "Promise.resolve" => promise_resolve(arg0(&args)),
6808 "Promise.reject" => promise_reject(arg0(&args)),
6809 "Promise.all" => promise_all(args, AllMode::All),
6810 "Promise.allSettled" => promise_all(args, AllMode::AllSettled),
6811 "Promise.race" => promise_race(args, false),
6812 "Promise.any" => promise_race(args, true),
6813 // `Promise.withResolvers()` (ES2024): a new pending promise plus its own
6814 // resolve/reject functions, returned as `{ promise, resolve, reject }`.
6815 "Promise.withResolvers" => promise_with_resolvers(),
6816 "Promise.try" => promise_try(args),
6817 "RegExp.escape" => regexp_escape(args),
6818 "Error.isError" => error_is_error(args),
6819 // `Map.groupBy(items, cb)` (ES2024): group into a `Map` keyed by the raw
6820 // `cb(item, i)` result (SameValueZero), each value an array of members.
6821 "Map.groupBy" => map_group_by(args),
6822 n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
6823 _ if name.starts_with("Math.") => math_fn(&name[5..], &args),
6824 // Internal continuations (Promise resolve/reject fns, `.finally` wrappers).
6825 // The executor a species-constructed promise is built with: it does
6826 // nothing, because the caller settles the result through its id.
6827 "@@pnoop" => Ok(Value::Undef),
6828 _ if name.starts_with("@@presolve:") => {
6829 let id: u32 = name[11..].parse().unwrap_or(0);
6830 host::resolve_promise_val(id, arg0(&args));
6831 Ok(Value::Undef)
6832 }
6833 _ if name.starts_with("@@preject:") => {
6834 let id: u32 = name[10..].parse().unwrap_or(0);
6835 host::reject_promise_val(id, arg0(&args));
6836 Ok(Value::Undef)
6837 }
6838 // The revoker `Proxy.revocable` hands back, keyed by the proxy's heap
6839 // index so calling it twice is the spec's no-op rather than a re-tear.
6840 _ if name.starts_with("@@prevoke:") => {
6841 let i: u32 = name[10..].parse().unwrap_or(0);
6842 Ok(crate::proxy::revoke(i))
6843 }
6844 _ if name.starts_with("@@finpass:") => {
6845 // finally(cb) on fulfill: run cb, await whatever it returned, then
6846 // pass the original value through.
6847 let i: u32 = name["@@finpass:".len()..].parse().unwrap_or(0);
6848 let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6849 Ok(finally_chain(result, arg0(&args), false))
6850 }
6851 _ if name.starts_with("@@finthrow:") => {
6852 // finally(cb) on reject: same, then re-throw the original reason.
6853 let i: u32 = name["@@finthrow:".len()..].parse().unwrap_or(0);
6854 let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6855 Ok(finally_chain(result, arg0(&args), true))
6856 }
6857 // The two thunks `finally_chain` hangs off that awaited promise. Each
6858 // carries the value it must reinstate in a one-slot cell, since a
6859 // builtin is identified only by its name and cannot close over one.
6860 _ if name.starts_with("@@finret:") => {
6861 let i: u32 = name["@@finret:".len()..].parse().unwrap_or(0);
6862 get_property(&Value::Obj(i), "0")
6863 }
6864 _ if name.starts_with("@@finrethrow:") => {
6865 let i: u32 = name["@@finrethrow:".len()..].parse().unwrap_or(0);
6866 let reason = get_property(&Value::Obj(i), "0")?;
6867 with_host(|h| h.exc = Some(reason.clone()));
6868 Err(with_host(|h| error_string(h, &reason)))
6869 }
6870 _ => Err(host::type_error(&format!("{name} is not a function"))),
6871 }
6872}
6873
6874/// `BigInt(x)`: convert a boolean/number/string/bigint to a BigInt. A
6875/// non-integer number is a `RangeError`; an unparseable string a `SyntaxError`
6876/// (matching Node's messages).
6877/// V8 names the offending value: `BigInt(undefined)` is `Cannot convert
6878/// undefined to a BigInt`, `BigInt({})` is `Cannot convert [object Object] to a
6879/// BigInt`. The old text said "value" literally, for every input.
6880fn bigint_convert_error(v: &Value) -> String {
6881 let shown = with_host(|h| h.str_of(v));
6882 host::type_error(&format!("Cannot convert {shown} to a BigInt"))
6883}
6884
6885/// `ToBigInt(v)` — 7.1.13. The conversion every BigInt-typed SINK performs: a
6886/// 64-bit typed array's element write, `DataView.prototype.setBigInt64`, and
6887/// BigInt arithmetic's operand check.
6888///
6889/// It is NOT `BigInt(v)`: a Number is a `TypeError` here (`BigInt(1)` is `1n`,
6890/// but `new BigInt64Array(1)[0] = 1` throws), which is the whole point of the
6891/// separate abstract op. Everything else follows `ToPrimitive(v, number)` then
6892/// the type table — booleans convert (`true` → `1n`), strings parse with a
6893/// `SyntaxError` on failure, and `undefined`/`null`/symbols throw.
6894///
6895/// Measured on node v26.8.1, receiver `new BigInt64Array(1)`:
6896///
6897/// ```text
6898/// a[0] = true → 1n
6899/// a[0] = '12' → 12n
6900/// a[0] = [] → 0n (ToPrimitive → "" → 0n)
6901/// a[0] = ['3'] → 3n
6902/// a[0] = 1 → TypeError: Cannot convert 1 to a BigInt
6903/// a[0] = new Number(3) → TypeError: Cannot convert 3 to a BigInt
6904/// a[0] = 'a' → SyntaxError: Cannot convert a to a BigInt
6905/// a[0] = {} → SyntaxError: Cannot convert [object Object] to a BigInt
6906/// ```
6907pub fn to_bigint(v: &Value) -> Result<num_bigint::BigInt, String> {
6908 let prim = host::to_primitive(v, "number")?;
6909 if let Some(b) = with_host(|h| match h.get(&prim) {
6910 Some(JsObj::BigInt(b)) => Some(b.clone()),
6911 _ => None,
6912 }) {
6913 return Ok(b);
6914 }
6915 match &prim {
6916 Value::Bool(b) => Ok(num_bigint::BigInt::from(*b as i64)),
6917 Value::Str(s) => host::parse_bigint_str(s)
6918 .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt")),
6919 _ if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) => {
6920 let s = with_host(|h| h.str_of(&prim));
6921 host::parse_bigint_str(&s)
6922 .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt"))
6923 }
6924 _ => Err(bigint_convert_error(&prim)),
6925 }
6926}
6927
6928fn bigint_ctor(v: &Value) -> Result<Value, String> {
6929 use num_bigint::BigInt;
6930 let big = match v {
6931 Value::Bool(b) => BigInt::from(*b as i64),
6932 Value::Int(n) => BigInt::from(*n),
6933 Value::Float(f) => {
6934 if !f.is_finite() || f.fract() != 0.0 {
6935 let disp = with_host(|h| h.str_of(v));
6936 return Err(format!(
6937 "RangeError: The number {disp} cannot be converted to a BigInt because it is not an integer"
6938 ));
6939 }
6940 // The decimal EXPANSION, not `fmt_number`: `Number.prototype
6941 // .toString` switches to exponential notation at 1e21, and
6942 // `BigInt::parse_bytes` cannot read `"1e+21"` — so `BigInt(1e21)`
6943 // threw `Cannot convert value to a BigInt` where node returns
6944 // `1000000000000000000000n`. `{:.0}` prints an integral f64's exact
6945 // value, which is also what node reports for a magnitude past the
6946 // exactly-representable range (`BigInt(1e30)` is
6947 // `1000000000000000019884624838656n` in both).
6948 match BigInt::parse_bytes(format!("{f:.0}").as_bytes(), 10) {
6949 Some(b) => b,
6950 None => return Err(bigint_convert_error(v)),
6951 }
6952 }
6953 Value::Str(s) => match host::parse_bigint_str(s) {
6954 Some(b) => b,
6955 None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
6956 },
6957 Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
6958 Some(JsObj::BigInt(b)) => b,
6959 Some(JsObj::Str(s)) => match host::parse_bigint_str(&s) {
6960 Some(b) => b,
6961 None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
6962 },
6963 _ => return Err(bigint_convert_error(v)),
6964 },
6965 _ => return Err(bigint_convert_error(v)),
6966 };
6967 Ok(with_host(|h| h.new_bigint(big)))
6968}
6969
6970/// `new RegExp(source[, flags])` / `RegExp(...)`. A first `RegExp` argument copies
6971/// its source (and flags, unless new ones are given).
6972fn regexp_ctor(args: &[Value]) -> Result<Value, String> {
6973 let (source, existing_flags) = match with_host(|h| h.get(&arg0(args)).cloned()) {
6974 Some(JsObj::RegExp(r)) => (r.source.clone(), Some(r.flags.clone())),
6975 _ => {
6976 let a0 = arg0(args);
6977 // 22.2.4.1 step 9 is `ToString(pattern)`, which a SYMBOL refuses —
6978 // `new RegExp(sym)` was compiling the text `Symbol(d)` into a
6979 // pattern instead of throwing.
6980 let src = if matches!(a0, Value::Undef) {
6981 String::new()
6982 } else {
6983 arg_to_string(args, 0)?
6984 };
6985 (src, None)
6986 }
6987 };
6988 let flags = match args.get(1) {
6989 Some(v) if !matches!(v, Value::Undef) => arg_to_string(args, 1)?,
6990 _ => existing_flags.unwrap_or_default(),
6991 };
6992 // An empty source compiles as the JS canonical `(?:)`.
6993 let src = if source.is_empty() {
6994 "(?:)".to_string()
6995 } else {
6996 source
6997 };
6998 crate::regexp::build_regexp(&src, &flags)
6999}
7000
7001/// `BigInt.asIntN(bits, x)` / `BigInt.asUintN(bits, x)`: wrap `x` to a `bits`-wide
7002/// two's-complement (signed) or unsigned integer.
7003fn bigint_as_n(unsigned: bool, args: &[Value]) -> Result<Value, String> {
7004 use num_bigint::BigInt;
7005 use num_traits::Signed;
7006 let bits = with_host(|h| h.to_number(&arg0(args))) as i64;
7007 if bits < 0 {
7008 return Err("RangeError: Invalid value: not (convertible to) a safe integer".into());
7009 }
7010 let x = match with_host(|h| h.as_bigint(&args.get(1).cloned().unwrap_or(Value::Undef))) {
7011 Some(b) => b,
7012 None => return Err(host::type_error("Cannot convert to a BigInt")),
7013 };
7014 let bits = bits as u32;
7015 if bits == 0 {
7016 return Ok(with_host(|h| h.new_bigint(BigInt::from(0))));
7017 }
7018 let modulus = BigInt::from(1) << bits; // 2^bits
7019 // Reduce into [0, 2^bits); for the signed form fold the top half negative.
7020 let mut r = &x % &modulus;
7021 if r.is_negative() {
7022 r += &modulus;
7023 }
7024 if !unsigned {
7025 let half = BigInt::from(1) << (bits - 1);
7026 if r >= half {
7027 r -= &modulus;
7028 }
7029 }
7030 Ok(with_host(|h| h.new_bigint(r)))
7031}
7032
7033/// `String.raw(callSite, ...subs)`: concatenate the raw quasis (`callSite.raw`)
7034/// interleaved with the substitutions.
7035fn string_raw(args: &[Value]) -> Result<Value, String> {
7036 let call_site = arg0(args);
7037 let raw = get_property(&call_site, "raw")?;
7038 let raws = with_host(|h| h.iter_vec(&raw)).unwrap_or_default();
7039 let mut out = String::new();
7040 for (i, r) in raws.iter().enumerate() {
7041 out.push_str(&with_host(|h| h.str_of(r)));
7042 if i + 1 < raws.len() {
7043 if let Some(sub) = args.get(i + 1) {
7044 out.push_str(&with_host(|h| h.str_of(sub)));
7045 }
7046 }
7047 }
7048 Ok(with_host(|h| h.new_str(out)))
7049}
7050
7051/// `Object(x)`: box/pass-through — for our model, non-object args just return a
7052/// fresh object; objects pass through.
7053/// Whether `v`'s own properties live in the fn-prop SIDE TABLE rather than in a
7054/// property map. A `Map`/`Set`/`Promise`/`RegExp`/generator/symbol/bigint is an
7055/// ordinary object that also has internal slots, so it can carry own properties
7056/// like anything else — but its heap variant holds only those slots, so a write
7057/// had nowhere to go and vanished: `m.x = 5` left `m.x` undefined.
7058pub fn uses_side_table(v: &Value) -> bool {
7059 matches!(
7060 with_host(|h| h.kind_of(v)),
7061 Some(
7062 ObjKind::Map
7063 | ObjKind::Set
7064 | ObjKind::Promise
7065 | ObjKind::RegExp
7066 | ObjKind::Generator
7067 | ObjKind::Symbol
7068 | ObjKind::BigInt
7069 | ObjKind::Iter
7070 )
7071 )
7072}
7073
7074fn object_call(args: Vec<Value>) -> Value {
7075 let a = arg0(&args);
7076 // `Object(v)` is `ToObject(v)` (20.1.1.1): a primitive comes back BOXED,
7077 // not replaced by an empty object. `Object(1).valueOf()` was `undefined`.
7078 if matches!(a, Value::Undef) || with_host(|h| h.is_null(&a)) {
7079 return with_host(|h| h.new_object(IndexMap::new()));
7080 }
7081 to_object(&a)
7082}
7083
7084/// The name of the wrapper a primitive boxes into, or `None` when the value is
7085/// already an object.
7086fn wrapper_ctor_of(v: &Value) -> Option<&'static str> {
7087 match v {
7088 Value::Int(_) | Value::Float(_) => Some("Number"),
7089 Value::Bool(_) => Some("Boolean"),
7090 Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
7091 Some(JsObj::Str(_)) => Some("String"),
7092 Some(JsObj::Symbol { .. }) => Some("Symbol"),
7093 Some(JsObj::BigInt(_)) => Some("BigInt"),
7094 _ => None,
7095 },
7096 _ => None,
7097 }
7098}
7099
7100/// The primitive a wrapper object boxes (`new String("a")` → `"a"`), or `None`
7101/// for every other value. The slot is a hidden `@@primitive` own property —
7102/// the same `@@` marker convention the engine already uses for internal state,
7103/// so it stays out of `Object.keys` and `JSON.stringify` on its own.
7104pub fn wrapped_primitive(v: &Value) -> Option<Value> {
7105 with_host(|h| match h.get(v) {
7106 Some(JsObj::Object(p)) => p.get("@@primitive").cloned(),
7107 _ => None,
7108 })
7109}
7110
7111/// `ToObject(v)` (7.1.18) for a primitive: the wrapper object with the matching
7112/// prototype and a `[[StringData]]`/`[[NumberData]]`/`[[BooleanData]]` slot.
7113///
7114/// A String wrapper also owns its index properties and `length`, which is what
7115/// makes `w[0]`, `w.length` and `Object.keys(w)` answer; all of them are
7116/// non-writable and non-configurable, as the exotic `String` object's are.
7117pub fn to_object(v: &Value) -> Value {
7118 let Some(ctor) = wrapper_ctor_of(v) else {
7119 return v.clone();
7120 };
7121 with_host(|h| h.ensure_wrapper_protos());
7122 let chars: Vec<String> = if ctor == "String" {
7123 with_host(|h| h.str_of(v))
7124 .chars()
7125 .map(|c| c.to_string())
7126 .collect()
7127 } else {
7128 Vec::new()
7129 };
7130 with_host(|h| {
7131 let mut m: IndexMap<String, Value> = IndexMap::new();
7132 for (i, c) in chars.iter().enumerate() {
7133 let s = h.new_str(c.clone());
7134 m.insert(i.to_string(), s);
7135 }
7136 let w = h.new_object(m);
7137 if ctor == "String" {
7138 for i in 0..chars.len() {
7139 h.set_prop_attrs(
7140 &w,
7141 &i.to_string(),
7142 host::PropAttrs {
7143 writable: false,
7144 enumerable: true,
7145 configurable: false,
7146 },
7147 );
7148 }
7149 let len = Value::Float(chars.len() as f64);
7150 if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7151 p.insert("length".into(), len);
7152 }
7153 h.set_prop_attrs(
7154 &w,
7155 "length",
7156 host::PropAttrs {
7157 writable: false,
7158 enumerable: false,
7159 configurable: false,
7160 },
7161 );
7162 }
7163 if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7164 p.insert("@@primitive".into(), v.clone());
7165 }
7166 if let Some(proto) = h.native_proto(ctor) {
7167 h.set_proto(&w, proto);
7168 }
7169 w
7170 })
7171}
7172
7173/// Construct via `new` for the builtin constructors.
7174pub fn construct_builtin(name: &str, args: Vec<Value>) -> Result<Value, String> {
7175 // Native stdlib constructors (`new URL(...)`, `new EventEmitter()`, `new Buffer(...)`).
7176 if let Some(r) = crate::stdlib::construct(name, &args) {
7177 return r;
7178 }
7179 match name {
7180 "Array" => {
7181 // `new Array(n)` -> length-n array; `new Array(a, b)` -> [a, b].
7182 // A single NUMBER argument is a length and is validated as one
7183 // (23.1.1.1 step 6), so `new Array(-1)` / `new Array(1.5)` /
7184 // `new Array(2**32)` are all `RangeError: Invalid array length` on
7185 // node v26.7.0; only a non-number single argument is an element.
7186 if args.len() == 1 {
7187 if let Value::Float(_) | Value::Int(_) = args[0] {
7188 let n = host::to_array_length(&args[0])?;
7189 // Every element of `new Array(n)` is a HOLE, not a stored
7190 // `undefined`: `Object.keys(Array(3))` is `[]`.
7191 return Ok(with_host(|h| {
7192 let a = h.new_array(vec![Value::Undef; n]);
7193 h.mark_hole_range(&a, 0..n);
7194 a
7195 }));
7196 }
7197 }
7198 Ok(with_host(|h| h.new_array(args)))
7199 }
7200 "Object" => Ok(object_call(args)),
7201 // `new String(v)` / `new Number(v)` / `new Boolean(v)` — the wrapper
7202 // form. These were not constructors at all, so every one threw.
7203 "String" => Ok(to_object(&host::to_string_value(
7204 &args
7205 .first()
7206 .cloned()
7207 .unwrap_or_else(|| with_host(|h| h.new_str(String::new()))),
7208 )?)),
7209 "Number" => Ok(to_object(&Value::Float(match args.first() {
7210 Some(a) => host::to_number_value(a)?,
7211 None => 0.0,
7212 }))),
7213 "Boolean" => Ok(to_object(&Value::Bool(with_host(|h| {
7214 h.truthy(&arg0(&args))
7215 })))),
7216 "Map" | "WeakMap" => {
7217 let weak = name == "WeakMap";
7218 let m = with_host(|h| {
7219 h.alloc(JsObj::Map {
7220 entries: indexmap::IndexMap::new(),
7221 weak,
7222 })
7223 });
7224 if let Some(init) = args
7225 .first()
7226 .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7227 {
7228 // Stepped, not drained: an entry that is not a pair has to
7229 // stop the construction at that element and CLOSE the iterator
7230 // (24.1.1.2 step 8). Materializing first meant a bad entry in an
7231 // infinite source was never reached and the constructor HUNG.
7232 host::iter_for_each(init, |p, _| {
7233 // 24.1.1.2 step 8.d: each entry must be an OBJECT. A string
7234 // is iterable, so without this check `new Map(["ab"])`
7235 // happily stored `'a' => 'b'` instead of throwing — and over
7236 // an infinite source it never stopped.
7237 if !with_host(|h| is_object_like(h, &p)) {
7238 let shown = with_host(|h| h.str_of(&p));
7239 return Err(host::type_error(&format!(
7240 "Iterator value {shown} is not an entry object"
7241 )));
7242 }
7243 // The entry is read by INDEX with `[[Get]]` (step 8.e), not
7244 // iterated: an object with a `Symbol.iterator` but no `0`/`1`
7245 // gives `undefined => undefined`, and an array-LIKE entry
7246 // works. Iterating it instead accepted a string as a pair
7247 // and rejected the array-like.
7248 let k = get_property(&p, "0")?;
7249 let v = get_property(&p, "1")?;
7250 map_method(&m, "set", vec![k, v])?;
7251 Ok(())
7252 })?;
7253 }
7254 Ok(m)
7255 }
7256 "Set" | "WeakSet" => {
7257 let weak = name == "WeakSet";
7258 let s = with_host(|h| {
7259 h.alloc(JsObj::Set {
7260 entries: indexmap::IndexMap::new(),
7261 weak,
7262 })
7263 });
7264 if let Some(init) = args
7265 .first()
7266 .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7267 {
7268 host::iter_for_each(init, |v, _| {
7269 set_method(&s, "add", vec![v])?;
7270 Ok(())
7271 })?;
7272 }
7273 Ok(s)
7274 }
7275 "Promise" => new_promise(arg0(&args)),
7276 "Proxy" => crate::proxy::create(&args),
7277 // `new Function(p…, body)` — the same `CreateDynamicFunction` the plain
7278 // call form runs (20.2.1.1). `depd`'s `wrapfunction` builds its
7279 // deprecation wrapper this way, so `require('body-parser')` — and with it
7280 // `require('express')` — dies at load without it.
7281 "Function" => function_ctor(&args),
7282 "RegExp" => regexp_ctor(&args),
7283 "BigInt" => Err(host::type_error("BigInt is not a constructor")),
7284 "Error" => make_error_checked(name, &args),
7285 // `new DOMException(message, name)` — the name is an ARGUMENT, and the
7286 // legacy numeric `code` follows from it.
7287 "DOMException" => Ok(dom_exception(&args)),
7288 n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
7289 _ => Err(host::type_error(&format!("{name} is not a constructor"))),
7290 }
7291}
7292
7293/// The legacy numeric `DOMException.code` a WHATWG error name maps to. A name
7294/// outside the table — including the default `"Error"` — reports 0.
7295pub const DOM_EXCEPTION_CODES: &[(&str, f64)] = &[
7296 ("IndexSizeError", 1.0),
7297 ("DOMStringSizeError", 2.0),
7298 ("HierarchyRequestError", 3.0),
7299 ("WrongDocumentError", 4.0),
7300 ("InvalidCharacterError", 5.0),
7301 ("NoDataAllowedError", 6.0),
7302 ("NoModificationAllowedError", 7.0),
7303 ("NotFoundError", 8.0),
7304 ("NotSupportedError", 9.0),
7305 ("InUseAttributeError", 10.0),
7306 ("InvalidStateError", 11.0),
7307 ("SyntaxError", 12.0),
7308 ("InvalidModificationError", 13.0),
7309 ("NamespaceError", 14.0),
7310 ("InvalidAccessError", 15.0),
7311 ("ValidationError", 16.0),
7312 ("TypeMismatchError", 17.0),
7313 ("SecurityError", 18.0),
7314 ("NetworkError", 19.0),
7315 ("AbortError", 20.0),
7316 ("URLMismatchError", 21.0),
7317 ("QuotaExceededError", 22.0),
7318 ("TimeoutError", 23.0),
7319 ("InvalidNodeTypeError", 24.0),
7320 ("DataCloneError", 25.0),
7321];
7322
7323/// The static name a `DOMException` code is exposed under: the error name minus
7324/// its `Error` suffix, upper-snake-cased, plus `_ERR` — `AbortError` becomes
7325/// `ABORT_ERR`, `IndexSizeError` becomes `INDEX_SIZE_ERR`.
7326fn legacy_code_name(error_name: &str) -> String {
7327 let stem = error_name.strip_suffix("Error").unwrap_or(error_name);
7328 let mut out = String::new();
7329 for (i, c) in stem.chars().enumerate() {
7330 if c.is_ascii_uppercase() && i > 0 {
7331 out.push('_');
7332 }
7333 out.push(c.to_ascii_uppercase());
7334 }
7335 out.push_str("_ERR");
7336 out
7337}
7338
7339/// `new DOMException(message, name)`.
7340///
7341/// The class node's `AbortSignal.reason` rejects with. Its `name` is the second
7342/// ARGUMENT (defaulting to `"Error"`), not the class name, and its `code` is the
7343/// legacy number that name maps to.
7344pub fn dom_exception(args: &[Value]) -> Value {
7345 let message = match args.first() {
7346 None | Some(Value::Undef) => String::new(),
7347 Some(v) => with_host(|h| h.str_of(v)),
7348 };
7349 let name = match args.get(1) {
7350 None | Some(Value::Undef) => "Error".to_string(),
7351 Some(v) => with_host(|h| h.str_of(v)),
7352 };
7353 with_host(|h| dom_exception_with(h, &name, &message))
7354}
7355
7356/// `dom_exception` for a caller that already holds the host borrow.
7357pub(crate) fn dom_exception_with(h: &mut host::JsHost, name: &str, message: &str) -> Value {
7358 let name = name.to_string();
7359 let message = message.to_string();
7360 let code = DOM_EXCEPTION_CODES
7361 .iter()
7362 .find(|(n, _)| *n == name)
7363 .map(|(_, c)| *c)
7364 .unwrap_or(0.0);
7365 let head = if message.is_empty() {
7366 name.clone()
7367 } else {
7368 format!("{name}: {message}")
7369 };
7370 let e = synth_error(h, &head);
7371 {
7372 let nv = h.new_str(name);
7373 let mv = h.new_str(message);
7374 let sv = h.new_str(head);
7375 if let Some(JsObj::Object(p)) = h.get_mut(&e) {
7376 // `name`, `message` and `code` are PROTOTYPE accessors over internal
7377 // slots in node, so `stack` is the instance's only own property.
7378 // Storing them as own keys would show up in
7379 // `Object.getOwnPropertyNames`, which reports just `['stack']`.
7380 p.shift_remove("message");
7381 p.insert("@@domName".into(), nv);
7382 p.insert("@@domMessage".into(), mv);
7383 p.insert("@@domCode".into(), Value::Float(code));
7384 p.insert("stack".into(), sv);
7385 }
7386 h.ensure_error_protos();
7387 if let Some(proto) = host::error_proto_of(h, "DOMException") {
7388 h.set_proto(&e, proto);
7389 }
7390 }
7391 e
7392}
7393
7394/// A `DOMException`'s `name`/`message`/`code`, which live in internal slots
7395/// rather than as own properties. `None` for anything else.
7396pub fn dom_exception_slot(recv: &Value, name: &str) -> Option<Value> {
7397 let slot = match name {
7398 "name" => "@@domName",
7399 "message" => "@@domMessage",
7400 "code" => "@@domCode",
7401 _ => return None,
7402 };
7403 with_host(|h| match h.get(recv) {
7404 Some(JsObj::Object(p)) if p.contains_key("@@domName") => p.get(slot).cloned(),
7405 _ => None,
7406 })
7407}
7408
7409/// Build an `Error` object carrying `msg`, for stdlib callers that need to
7410/// throw a value with extra own properties on it.
7411pub(crate) fn make_error_pub(name: &str, msg: &str) -> Value {
7412 let m = with_host(|h| h.new_str(msg.to_string()));
7413 make_error_inner(name, &[m])
7414}
7415
7416/// [`make_error`] with the message's `ToString` allowed to FAIL. A symbol
7417/// refuses it (20.5.1.1 step 3), so `new Error(sym)` is a TypeError where this
7418/// rendered `Symbol(desc)` into `.message`.
7419fn make_error_checked(name: &str, args: &[Value]) -> Result<Value, String> {
7420 if let Some(m) = args.first().filter(|m| !matches!(m, Value::Undef)) {
7421 // AggregateError's message is its SECOND argument.
7422 let idx = usize::from(name == "AggregateError");
7423 if idx == 0 {
7424 host::to_string_value(m)?;
7425 } else if let Some(m2) = args.get(idx).filter(|m| !matches!(m, Value::Undef)) {
7426 host::to_string_value(m2)?;
7427 }
7428 }
7429 Ok(make_error_inner(name, args))
7430}
7431
7432fn make_error_inner(name: &str, args: &[Value]) -> Value {
7433 // `new AggregateError(errors, message)` takes the causes FIRST; every other
7434 // error constructor takes the message first.
7435 let agg = name == "AggregateError";
7436 let (errors, args) = if agg {
7437 (
7438 Some(args.first().cloned().unwrap_or(Value::Undef)),
7439 args.get(1..).unwrap_or(&[]),
7440 )
7441 } else {
7442 (None, args)
7443 };
7444 with_host(|h| {
7445 h.ensure_error_protos();
7446 let mut props: IndexMap<String, Value> = IndexMap::new();
7447 let msg = args
7448 .first()
7449 .filter(|a| !matches!(a, Value::Undef))
7450 .map(|a| h.str_of(a));
7451 if let Some(m) = &msg {
7452 let mv = h.new_str(m.clone());
7453 props.insert("message".into(), mv);
7454 }
7455 // `.stack` is engine-specific; a simple `Name: message` header line
7456 // suffices for parity (the fuzzer never prints raw stacks).
7457 //
7458 // V8 formats that header LAZILY, on the first read, from whatever `name`
7459 // and `message` the error carries at that moment — which is why the
7460 // near-universal
7461 //
7462 // class MyErr extends Error { constructor(m) { super(m); this.name = 'MyErr'; } }
7463 //
7464 // reports `MyErr: boom` and not the `Error: boom` this built eagerly,
7465 // inside `super()`, before the subclass had renamed anything. `@@stackRaw`
7466 // carries the frames so the read can redo it; see `materialize_stack`.
7467 let frames = h.stack_frames();
7468 let stack = match &msg {
7469 Some(m) if !m.is_empty() => format!("{name}: {m}{frames}"),
7470 _ => format!("{name}{frames}"),
7471 };
7472 let sv = h.new_str(stack);
7473 props.insert("stack".into(), sv);
7474 let raw = h.new_str(frames);
7475 props.insert("@@stackRaw".into(), raw);
7476 if let Some(errs) = errors {
7477 // Materialize the iterable into the own `errors` array property.
7478 let items = h.iter_vec(&errs).unwrap_or_default();
7479 let arr = h.new_array(items);
7480 props.insert("errors".into(), arr);
7481 }
7482 // `new Error(msg, { cause })` (ES2022): installed only when the options
7483 // bag actually has a `cause` key, so `new Error(m, {})` leaves none.
7484 let opts = args.get(1);
7485 if let Some(cause) = opts.and_then(|o| match h.get(o) {
7486 Some(JsObj::Object(p)) => p.get("cause").cloned(),
7487 _ => None,
7488 }) {
7489 props.insert("cause".into(), cause);
7490 }
7491 let e = h.new_object(props);
7492 if let Some(p) = host::error_proto_of(h, name) {
7493 h.set_proto(&e, p);
7494 }
7495 // Every own slot an error constructor installs is non-enumerable in V8,
7496 // which is why `Object.keys(err)` is `[]` and `JSON.stringify(err)` is
7497 // `{}` — properties a *script* later assigns stay enumerable.
7498 for k in ["message", "stack", "errors", "cause", "@@stackRaw"] {
7499 h.hide_prop(&e, k);
7500 }
7501 e
7502 })
7503}
7504
7505fn print_line(args: &[Value], stderr: bool) -> Result<(), String> {
7506 // Node's console.log(...args) === util.format(...args): printf-style
7507 // substitution when the first arg is a format string, else inspect-and-join.
7508 // A directive can THROW (`console.log('%j', 1n)`), and node lets that reach
7509 // the caller instead of printing a line — so nothing is written on failure.
7510 let line: String = crate::stdlib::util::format(args)?;
7511 with_host(|h| h.write_out(&format!("{line}\n"), stderr));
7512 Ok(())
7513}
7514
7515fn arg0(args: &[Value]) -> Value {
7516 args.first().cloned().unwrap_or(Value::Undef)
7517}
7518/// `ToString(arg)` for a builtin's argument — fallible, because a SYMBOL
7519/// refuses the conversion (7.1.17). Every site that reached for `str_of`
7520/// instead rendered `Symbol(desc)` into its result and reported nothing.
7521fn arg_to_string(args: &[Value], i: usize) -> Result<String, String> {
7522 let v = args.get(i).cloned().unwrap_or(Value::Undef);
7523 let sv = host::to_string_value(&v)?;
7524 Ok(with_host(|h| h.str_of(&sv)))
7525}
7526
7527fn arg_num(args: &[Value], i: usize) -> f64 {
7528 with_host(|h| h.to_number(&args.get(i).cloned().unwrap_or(Value::Undef)))
7529}
7530
7531fn is_integer(v: Value) -> bool {
7532 match v {
7533 Value::Int(_) => true,
7534 Value::Float(f) => f.is_finite() && f.fract() == 0.0,
7535 _ => false,
7536 }
7537}
7538fn is_safe_integer(v: Value) -> bool {
7539 match v {
7540 Value::Float(f) => f.is_finite() && f.fract() == 0.0 && f.abs() <= 9007199254740991.0,
7541 Value::Int(_) => true,
7542 _ => false,
7543 }
7544}
7545
7546/// `encodeURI`/`encodeURIComponent`: percent-encode `s`'s UTF-8 bytes, leaving
7547/// the unreserved set unescaped. `encodeURI` additionally preserves the reserved
7548/// URI characters (`;,/?:@&=+$#`) that delimit a URI's structure.
7549fn uri_encode(s: &str, uri: bool) -> Result<Value, String> {
7550 // Always-unescaped (`encodeURIComponent`'s unreserved set), per the spec.
7551 const UNRESERVED: &[u8] =
7552 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
7553 // Reserved characters `encodeURI` leaves intact on top of the unreserved set.
7554 const RESERVED: &[u8] = b";,/?:@&=+$#";
7555 let mut out = String::with_capacity(s.len());
7556 for &b in s.as_bytes() {
7557 if UNRESERVED.contains(&b) || (uri && RESERVED.contains(&b)) {
7558 out.push(b as char);
7559 } else {
7560 out.push('%');
7561 out.push(
7562 char::from_digit((b >> 4) as u32, 16)
7563 .unwrap()
7564 .to_ascii_uppercase(),
7565 );
7566 out.push(
7567 char::from_digit((b & 0xf) as u32, 16)
7568 .unwrap()
7569 .to_ascii_uppercase(),
7570 );
7571 }
7572 }
7573 Ok(with_host(|h| h.new_str(out)))
7574}
7575
7576/// `decodeURI`/`decodeURIComponent`: reverse `%XX` escapes back to UTF-8 text.
7577/// For `decodeURI`, escapes of the reserved delimiters are left as-is (the spec's
7578/// asymmetry with `encodeURI`). Throws `URIError` on a malformed escape.
7579fn uri_decode(s: &str, uri: bool) -> Result<Value, String> {
7580 const RESERVED: &[u8] = b";,/?:@&=+$#";
7581 let bytes = s.as_bytes();
7582 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
7583 let mut i = 0;
7584 while i < bytes.len() {
7585 if bytes[i] == b'%' {
7586 if i + 2 >= bytes.len() {
7587 return Err("URIError: URI malformed".into());
7588 }
7589 let hi = (bytes[i + 1] as char).to_digit(16);
7590 let lo = (bytes[i + 2] as char).to_digit(16);
7591 match (hi, lo) {
7592 (Some(h), Some(l)) => {
7593 let byte = (h * 16 + l) as u8;
7594 // decodeURI keeps reserved-delimiter escapes literal.
7595 if uri && RESERVED.contains(&byte) {
7596 out.extend_from_slice(&bytes[i..i + 3]);
7597 } else {
7598 out.push(byte);
7599 }
7600 i += 3;
7601 }
7602 _ => return Err("URIError: URI malformed".into()),
7603 }
7604 } else {
7605 out.push(bytes[i]);
7606 i += 1;
7607 }
7608 }
7609 match String::from_utf8(out) {
7610 Ok(decoded) => Ok(with_host(|h| h.new_str(decoded))),
7611 Err(_) => Err("URIError: URI malformed".into()),
7612 }
7613}
7614
7615/// `escape` (Annex B.2.1.1) — the pre-`encodeURIComponent` legacy encoder, still
7616/// present in every engine and still reached by old libraries (jQuery's cookie
7617/// plugin, `querystring`-era code). It works on UTF-16 CODE UNITS, not UTF-8
7618/// bytes, which is what separates it from `encodeURIComponent`: a unit below
7619/// `0x100` becomes `%XX`, anything above becomes `%uXXXX`, so an astral
7620/// character yields the two escapes of its surrogate pair
7621/// (`escape("\u{1D4B3}")` is `"%uD835%uDCB3"` on node v26.7.0).
7622///
7623/// The unescaped set is frozen by the spec and is NOT the URI unreserved set —
7624/// it keeps `@*_+-./` and drops `!~'()`.
7625fn legacy_escape(s: &str) -> Result<Value, String> {
7626 const KEEP: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";
7627 let mut out = String::with_capacity(s.len());
7628 for u in s.encode_utf16() {
7629 if u < 0x100 {
7630 if KEEP.contains(&(u as u8)) {
7631 out.push(u as u8 as char);
7632 } else {
7633 out.push_str(&format!("%{u:02X}"));
7634 }
7635 } else {
7636 out.push_str(&format!("%u{u:04X}"));
7637 }
7638 }
7639 Ok(with_host(|h| h.new_str(out)))
7640}
7641
7642/// `unescape` (Annex B.2.1.2) — the inverse of [`legacy_escape`]. Unlike
7643/// `decodeURIComponent` it never throws: a `%` that does not begin a well-formed
7644/// `%XX` or `%uXXXX` escape is passed through literally
7645/// (`unescape("%u0041%42%zz%2")` is `"AB%zz%2"` on node v26.7.0).
7646///
7647/// Decoding is done in code-unit space and re-joined at the end so a
7648/// `%uD835%uDCB3` pair recomposes into the one astral character it came from.
7649fn legacy_unescape(s: &str) -> Result<Value, String> {
7650 let b = s.as_bytes();
7651 let hex = |i: usize, n: usize| -> Option<u16> {
7652 if i + n > b.len() {
7653 return None;
7654 }
7655 let mut v: u16 = 0;
7656 for &c in &b[i..i + n] {
7657 v = v.checked_mul(16)? + (c as char).to_digit(16)? as u16;
7658 }
7659 Some(v)
7660 };
7661 let units: Vec<u16> = s.encode_utf16().collect();
7662 let mut out: Vec<u16> = Vec::with_capacity(units.len());
7663 let mut i = 0;
7664 while i < b.len() {
7665 // Escapes are pure ASCII, so a byte index is a unit index up to here —
7666 // but the tail may not be, so non-`%` bytes are re-decoded as chars.
7667 if b[i] == b'%' {
7668 if let Some(u) = hex(i + 1, 2) {
7669 out.push(u);
7670 i += 3;
7671 continue;
7672 }
7673 if b.get(i + 1) == Some(&b'u') {
7674 if let Some(u) = hex(i + 2, 4) {
7675 out.push(u);
7676 i += 6;
7677 continue;
7678 }
7679 }
7680 }
7681 let c = s[i..].chars().next().unwrap_or('%');
7682 let mut buf = [0u16; 2];
7683 out.extend_from_slice(c.encode_utf16(&mut buf));
7684 i += c.len_utf8();
7685 }
7686 Ok(with_host(|h| {
7687 h.new_str(crate::utf16::to_string_lossy(&out))
7688 }))
7689}
7690
7691/// `parseInt` begins with `ToString(argument)` (19.2.5 step 1), and that step can
7692/// THROW — a Symbol has no string form, so `parseInt([Symbol()])` is a TypeError
7693/// rather than `NaN`. Reading the argument with `str_of` took the object's brand
7694/// instead of converting it, which both swallowed that throw and ignored any
7695/// `toString` the value defines.
7696fn parse_int(args: &[Value]) -> Result<f64, String> {
7697 // Converted BEFORE the host borrow: `to_string_value` can call back into JS.
7698 let sv = host::to_string_value(&arg0(args))?;
7699 // 19.2.5 step 2 is `ToInt32(radix)`, which runs a user `valueOf` — the
7700 // infallible read below does no `ToPrimitive`, so an object radix came out
7701 // as NaN and the parse silently fell back to auto-detection.
7702 let radix = match args.get(1) {
7703 Some(r) if !matches!(r, Value::Undef) => {
7704 vec![arg0(args), Value::Float(to_number_arg(args, 1)?)]
7705 }
7706 _ => args.to_vec(),
7707 };
7708 Ok(parse_int_str(&with_host(|h| h.str_of(&sv)), &radix))
7709}
7710
7711fn parse_int_str(s: &str, args: &[Value]) -> f64 {
7712 // 19.2.5 step 8: an EXPLICIT radix outside 2..=36 is `NaN`, it does not fall
7713 // back to auto-detection. The old `.filter()` silently discarded a bad radix,
7714 // so `parseInt("10", 37)` answered 10 where every engine says NaN.
7715 let radix_arg = args
7716 .get(1)
7717 .map(|r| with_host(|h| host::to_int32(h.to_number(r))));
7718 let radix = match radix_arg {
7719 Some(0) | None => None,
7720 Some(r) if (2..=36).contains(&r) => Some(r as u32),
7721 Some(_) => return f64::NAN,
7722 };
7723 let t = crate::utf16::js_trim_start(s);
7724 let (neg, digits) = match t.strip_prefix('-') {
7725 Some(rest) => (true, rest),
7726 None => (false, t.strip_prefix('+').unwrap_or(t)),
7727 };
7728 let (radix, digits) = match radix {
7729 Some(16) => (
7730 16u32,
7731 digits
7732 .strip_prefix("0x")
7733 .or_else(|| digits.strip_prefix("0X"))
7734 .unwrap_or(digits),
7735 ),
7736 Some(r) => (r, digits),
7737 None => {
7738 if let Some(hex) = digits
7739 .strip_prefix("0x")
7740 .or_else(|| digits.strip_prefix("0X"))
7741 {
7742 (16, hex)
7743 } else {
7744 (10, digits)
7745 }
7746 }
7747 };
7748 let valid: String = digits.chars().take_while(|c| c.is_digit(radix)).collect();
7749 if valid.is_empty() {
7750 return f64::NAN;
7751 }
7752 // Accumulate in `f64`, not `i64`. `i64::from_str_radix` OVERFLOWS past ~19
7753 // digits and the error was mapped to `NaN`, so
7754 // `parseInt("999999999999999999999999")` was NaN instead of 1e+24. The spec
7755 // asks for the mathematical value rounded to a Number, which is what
7756 // repeated multiply-accumulate in `f64` produces.
7757 let n = if radix == 10 {
7758 // Rust's decimal float parser is correctly rounded; digit-by-digit
7759 // multiply-accumulate is not, and drifted a ULP on long inputs
7760 // (`parseInt("999999999999999999999999")` came out
7761 // 1.0000000000000003e+24 rather than 1e+24).
7762 valid.parse::<f64>().unwrap_or(f64::NAN)
7763 } else {
7764 let mut n = 0.0f64;
7765 for c in valid.chars() {
7766 n = n * radix as f64 + c.to_digit(radix).unwrap_or(0) as f64;
7767 }
7768 n
7769 };
7770 if neg {
7771 -n
7772 } else {
7773 n
7774 }
7775}
7776
7777/// `parseFloat` likewise starts from `ToString(argument)`; see `parse_int`.
7778fn parse_float(args: &[Value]) -> Result<f64, String> {
7779 let sv = host::to_string_value(&arg0(args))?;
7780 Ok(parse_float_str(&with_host(|h| h.str_of(&sv))))
7781}
7782
7783fn parse_float_str(s: &str) -> f64 {
7784 let t = crate::utf16::js_trim_start(s);
7785 // `Infinity` / `+Infinity` / `-Infinity` are valid parseFloat prefixes.
7786 let inf_body = t
7787 .strip_prefix('+')
7788 .or_else(|| t.strip_prefix('-'))
7789 .unwrap_or(t);
7790 if inf_body.starts_with("Infinity") {
7791 return if t.starts_with('-') {
7792 f64::NEG_INFINITY
7793 } else {
7794 f64::INFINITY
7795 };
7796 }
7797 // The LONGEST prefix that is itself a complete `StrDecimalLiteral`, which is
7798 // not the same as the longest run of characters that could appear in one:
7799 // `"1e"` and `"1e+"` are `1` in every engine, because the exponent part is
7800 // only valid once a digit follows `e`. Tracking `end` at every character
7801 // accepted the dangling `e`, `parse::<f64>` then failed, and the whole call
7802 // came back NaN.
7803 let mut end = 0;
7804 let bytes = t.as_bytes();
7805 let mut seen_dot = false;
7806 let mut seen_e = false;
7807 let mut digits_before_dot = false;
7808 for (i, &c) in bytes.iter().enumerate() {
7809 match c {
7810 b'0'..=b'9' => {
7811 if !seen_dot && !seen_e {
7812 digits_before_dot = true;
7813 }
7814 end = i + 1;
7815 }
7816 // A sign is only meaningful leading, or straight after the exponent
7817 // marker; it never completes a literal on its own.
7818 b'+' | b'-' if i == 0 || bytes[i - 1] == b'e' || bytes[i - 1] == b'E' => {}
7819 // `1.` is a complete literal; a bare `.` is not.
7820 b'.' if !seen_dot && !seen_e => {
7821 seen_dot = true;
7822 if digits_before_dot {
7823 end = i + 1;
7824 }
7825 }
7826 b'e' | b'E' if !seen_e && end > 0 => seen_e = true,
7827 _ => break,
7828 }
7829 }
7830 if end == 0 {
7831 return f64::NAN;
7832 }
7833 t[..end].parse::<f64>().unwrap_or(f64::NAN)
7834}
7835
7836/// ECMA-262 `Number::exponentiate` (6.1.6.1.3), backing both `Math.pow` and the
7837/// `**` operator. Three clauses differ from IEEE-754 `pow`, which is what Rust's
7838/// `powf` implements: a NaN exponent is NaN even for base 1, a NaN base is NaN
7839/// for any non-zero exponent, and `|base| == 1` with an infinite exponent is NaN
7840/// rather than 1.
7841pub(crate) fn js_pow(base: f64, exp: f64) -> f64 {
7842 if exp == 0.0 {
7843 return 1.0;
7844 }
7845 if base.is_nan() || exp.is_nan() {
7846 return f64::NAN;
7847 }
7848 if base.abs() == 1.0 && exp.is_infinite() {
7849 return f64::NAN;
7850 }
7851 base.powf(exp)
7852}
7853
7854fn math_fn(fname: &str, args: &[Value]) -> Result<Value, String> {
7855 // Every `Math` function coerces its arguments with `ToNumber`, and `ToNumber`
7856 // of a BigInt is a TypeError (7.1.4 step 2) — the whole point of BigInt being
7857 // a separate numeric type. `arg_num` reads a BigInt's magnitude instead, so
7858 // `Math.max(1n)` quietly answered 1 where V8 throws. `Math.random` is the one
7859 // exception: it never reads an argument, so `Math.random(1n)` is fine.
7860 // A BigInt WRAPPER converts to a BigInt and is rejected just as the
7861 // primitive is: `Math.abs(Object(9n))` is a TypeError where it answered NaN.
7862 // The boxed value is read BEFORE the borrow — `wrapped_primitive` borrows
7863 // the host itself and cannot run inside another borrow.
7864 let is_bigint = |a: &Value| {
7865 if with_host(|h| matches!(h.get(a), Some(JsObj::BigInt(_)))) {
7866 return true;
7867 }
7868 match wrapped_primitive(a) {
7869 Some(p) => with_host(|h| matches!(h.get(&p), Some(JsObj::BigInt(_)))),
7870 None => false,
7871 }
7872 };
7873 if fname != "random" && args.iter().any(is_bigint) {
7874 return Err(host::type_error(
7875 "Cannot convert a BigInt value to a number",
7876 ));
7877 }
7878 // Every argument is `ToNumber`d (21.3.2.x), which runs a user `valueOf` and
7879 // can throw from it. `arg_num` does no `ToPrimitive` at all, so
7880 // `Math.max({valueOf: () => 1}, 0)` answered NaN.
7881 // EVERY argument, not a fixed prefix: `Math.max`/`min`/`hypot` are
7882 // variadic, and coercing only the first few silently DROPPED the rest —
7883 // `Math.max(...gen)` over five values answered for four of them.
7884 let mut coerced = Vec::with_capacity(args.len());
7885 for a in args {
7886 if matches!(a, Value::Undef) {
7887 coerced.push(a.clone());
7888 continue;
7889 }
7890 let p = host::to_primitive(a, "number")?;
7891 coerced.push(Value::Float(with_host(|h| h.to_number(&p))));
7892 }
7893 let args: &[Value] = &coerced;
7894 let x = arg_num(args, 0);
7895 let r = match fname {
7896 "floor" => x.floor(),
7897 "ceil" => x.ceil(),
7898 // ECMA-262 `Math.round` (21.3.2.28) transcribed clause by clause. The
7899 // obvious `(x + 0.5).floor()` is NOT this function: the addition rounds
7900 // before the floor sees it, so it answers 1 for the largest double below
7901 // 0.5 (`Math.round(0.49999999999999994)` is 0 in every engine) and it
7902 // perturbs integers above 2^52, where `x + 0.5` is no longer
7903 // representable (`Math.round(4503599627370497)` must be the input).
7904 // Splitting the zero-band cases out first also carries the signed zero
7905 // the spec asks for without a post-hoc patch.
7906 "round" => {
7907 if !x.is_finite() || x == 0.0 {
7908 x
7909 } else if x > 0.0 && x < 0.5 {
7910 0.0
7911 } else if (-0.5..0.0).contains(&x) {
7912 -0.0
7913 } else {
7914 // |x| >= 0.5, so `floor` and the subtraction are both exact
7915 // (every double >= 2^52 is already an integer and yields 0 here).
7916 let f = x.floor();
7917 if x - f >= 0.5 {
7918 f + 1.0
7919 } else {
7920 f
7921 }
7922 }
7923 }
7924 "trunc" => x.trunc(),
7925 "abs" => x.abs(),
7926 "sign" => {
7927 if x.is_nan() {
7928 f64::NAN
7929 } else if x > 0.0 {
7930 1.0
7931 } else if x < 0.0 {
7932 -1.0
7933 } else {
7934 x
7935 }
7936 }
7937 "sqrt" => x.sqrt(),
7938 "cbrt" => x.cbrt(),
7939 "exp" => x.exp(),
7940 "log" => x.ln(),
7941 "log2" => x.log2(),
7942 "log10" => x.log10(),
7943 "sin" => x.sin(),
7944 "cos" => x.cos(),
7945 "tan" => x.tan(),
7946 "asin" => x.asin(),
7947 "acos" => x.acos(),
7948 "atan" => x.atan(),
7949 "atan2" => x.atan2(arg_num(args, 1)),
7950 // Rust `powf` is IEEE-754 `pow`, which is NOT JS `**`/`Math.pow`: IEEE
7951 // makes `pow(x, ±0)` and `pow(±1, y)` return 1 unconditionally, so
7952 // `(-1) ** Infinity` and `1 ** NaN` come back 1 where the spec
7953 // (6.1.6.1.3 Number::exponentiate) says NaN. Only the exponent-is-zero
7954 // clause is shared.
7955 "pow" => js_pow(x, arg_num(args, 1)),
7956 // Hyperbolics and the two precision-preserving log/exp forms.
7957 "sinh" => x.sinh(),
7958 "cosh" => x.cosh(),
7959 "tanh" => x.tanh(),
7960 "asinh" => x.asinh(),
7961 "acosh" => x.acosh(),
7962 "atanh" => x.atanh(),
7963 "log1p" => x.ln_1p(),
7964 "expm1" => x.exp_m1(),
7965 // C-style 32-bit integer multiply: ToInt32 both operands, multiply with
7966 // wraparound, reinterpret as a signed 32-bit result.
7967 "imul" => (host::to_int32(x).wrapping_mul(host::to_int32(arg_num(args, 1)))) as f64,
7968 "hypot" => {
7969 // Scale by the largest magnitude before squaring — this avoids the
7970 // last-ULP error of the naive `sqrt(Σ xᵢ²)` and matches V8's result.
7971 let xs: Vec<f64> = args.iter().map(|a| with_host(|h| h.to_number(a))).collect();
7972 let mut max = 0.0f64;
7973 for x in &xs {
7974 if x.abs() > max {
7975 max = x.abs();
7976 }
7977 }
7978 if xs.iter().any(|x| x.is_infinite()) {
7979 f64::INFINITY
7980 } else if max == 0.0 || !max.is_finite() {
7981 max
7982 } else {
7983 let s: f64 = xs.iter().map(|x| (x / max) * (x / max)).sum();
7984 max * s.sqrt()
7985 }
7986 }
7987 "random" => pseudo_random(),
7988 "max" => {
7989 if args.is_empty() {
7990 f64::NEG_INFINITY
7991 } else {
7992 let mut m = f64::NEG_INFINITY;
7993 for a in args {
7994 let n = with_host(|h| h.to_number(a));
7995 if n.is_nan() {
7996 return Ok(Value::Float(f64::NAN));
7997 }
7998 // `>` cannot separate the zeroes (`0.0 > -0.0` is false), but
7999 // the spec ranks +0 above -0, so `Math.max(-0, 0)` is +0 and
8000 // must not keep the -0 the first iteration installed.
8001 if n > m || (n == m && n == 0.0 && n.is_sign_positive()) {
8002 m = n;
8003 }
8004 }
8005 m
8006 }
8007 }
8008 "min" => {
8009 if args.is_empty() {
8010 f64::INFINITY
8011 } else {
8012 let mut m = f64::INFINITY;
8013 for a in args {
8014 let n = with_host(|h| h.to_number(a));
8015 if n.is_nan() {
8016 return Ok(Value::Float(f64::NAN));
8017 }
8018 // Mirror of `max`: -0 ranks below +0 even though `<` says
8019 // they are equal, so `Math.min(0, -0)` is -0.
8020 if n < m || (n == m && n == 0.0 && n.is_sign_negative()) {
8021 m = n;
8022 }
8023 }
8024 m
8025 }
8026 }
8027 // Count leading zero bits of ToUint32(x) (Math.clz32(1) === 31).
8028 "clz32" => {
8029 let u = if x.is_finite() {
8030 x.trunc().rem_euclid(4294967296.0) as u32
8031 } else {
8032 0
8033 };
8034 u.leading_zeros() as f64
8035 }
8036 // Round to the nearest single-precision float.
8037 "fround" => (x as f32) as f64,
8038 _ => return Err(host::type_error(&format!("Math.{fname} is not a function"))),
8039 };
8040 Ok(Value::Float(r))
8041}
8042
8043/// A small deterministic PRNG for `Math.random` (output is non-reproducible vs
8044/// Node by nature; kept simple).
8045fn pseudo_random() -> f64 {
8046 use std::cell::Cell;
8047 thread_local!(static SEED: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
8048 SEED.with(|s| {
8049 let mut x = s.get();
8050 x ^= x << 13;
8051 x ^= x >> 7;
8052 x ^= x << 17;
8053 s.set(x);
8054 (x >> 11) as f64 / (1u64 << 53) as f64
8055 })
8056}
8057
8058// ── Object.* ──────────────────────────────────────────────────────────────────
8059
8060/// The characters of a string PRIMITIVE, as the `ToObject` wrapper's own index
8061/// properties (10.4.3 `StringExoticObject`).
8062///
8063/// `getOwnPropertyDescriptor` begins with `ToObject`, which boxes a string into
8064/// an exotic object whose own keys are its code-unit indices plus `length`;
8065/// this is the descriptor half of that. (The KEY half lives in
8066/// `JsHost::own_enum_data_keys`, the single source every enumeration path
8067/// reads.) Indices are UTF-16 code units, matching `.length` and `s[i]`.
8068///
8069/// A boxed `String` object is deliberately NOT routed here: it can carry
8070/// ordinary own properties too (`const s = new String('ab'); s.x = 1`), and its
8071/// existing path already reports them alongside the indices.
8072fn string_primitive_units(v: &Value) -> Option<Vec<String>> {
8073 // A JS string primitive rides as a `Value::Obj` handle to `JsObj::Str` (see
8074 // `host.rs`); a BOXED `new String(...)` is a different heap object, so this
8075 // never catches one.
8076 let s = match v {
8077 Value::Str(s) => (**s).clone(),
8078 _ => with_host(|h| match h.get(v) {
8079 Some(JsObj::Str(s)) => Some(s.clone()),
8080 _ => None,
8081 })?,
8082 };
8083 let units = crate::utf16::Units::of(&s);
8084 Some((0..units.len()).filter_map(|i| units.unit_str(i)).collect())
8085}
8086
8087fn object_keys(args: Vec<Value>, mode: u8) -> Result<Value, String> {
8088 let v = arg0(&args);
8089 require_object_coercible(&v)?;
8090 // A Proxy answers from its `ownKeys` trap. `getOwnPropertyNames` (mode 3)
8091 // reports every own STRING key the trap named; the enumerating modes
8092 // additionally filter by each key's `[[GetOwnProperty]]`, so both traps run.
8093 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8094 if mode == 3 {
8095 let keys = crate::proxy::own_keys(&v)?.unwrap_or_default();
8096 return Ok(with_host(|h| {
8097 let out: Vec<Value> = keys
8098 .into_iter()
8099 .filter(|k| !host::is_symbol_key(k))
8100 .map(|k| h.new_str(k))
8101 .collect();
8102 h.new_array(out)
8103 }));
8104 }
8105 // `Object.keys` (mode 0) must run `ownKeys` and `getOwnPropertyDescriptor`
8106 // and STOP — 7.3.23 never performs `[[Get]]` when only keys are wanted.
8107 // Going through `own_enum_entries` fired the `get` trap once per key, so
8108 // the observable trap sequence carried a trailing `get` node does not
8109 // emit, and a trap with side effects ran when it should not have.
8110 if mode == 0 {
8111 let keys = crate::proxy::own_enum_string_keys(&v)?;
8112 return Ok(with_host(|h| {
8113 let out: Vec<Value> = keys.into_iter().map(|k| h.new_str(k)).collect();
8114 h.new_array(out)
8115 }));
8116 }
8117 let entries = crate::proxy::own_enum_entries(&v)?;
8118 return Ok(with_host(|h| {
8119 let out: Vec<Value> = entries
8120 .into_iter()
8121 .map(|(k, val)| match mode {
8122 0 => h.new_str(k),
8123 1 => val,
8124 _ => {
8125 let ks = h.new_str(k);
8126 h.new_array(vec![ks, val])
8127 }
8128 })
8129 .collect();
8130 h.new_array(out)
8131 }));
8132 }
8133 // An intrinsic prototype this host built as a REAL OBJECT — `Symbol
8134 // .prototype`, `String.prototype`, the error hierarchy — answers from the
8135 // generated table too. It was answering from its own property map instead,
8136 // which carries neither the right names nor V8's order: `Symbol.prototype`
8137 // reported `toLocaleString` and omitted `description`, and
8138 // `String.prototype` omitted `length` and every Annex B HTML method.
8139 //
8140 // `ns` is the namespace SPELLING, so the arm below is shared verbatim —
8141 // the two representations of a prototype cannot answer differently.
8142 let real_proto_ns = with_host(|h| h.intrinsic_proto_ctor(&v).map(|c| format!("{c}.prototype")))
8143 .filter(|ns| intrinsic_proto_members(ns).is_some());
8144 // A builtin prototype namespace that exposes enumerable methods for copying
8145 // (`Object.getOwnPropertyNames(EventEmitter.prototype)` — express's mixin).
8146 if let Some(ns) = real_proto_ns.or_else(|| {
8147 with_host(|h| match h.get(&v) {
8148 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
8149 _ => None,
8150 })
8151 }) {
8152 // An INTRINSIC prototype (`Map.prototype`, `URL.prototype`). Members are
8153 // non-enumerable on an ECMAScript builtin and enumerable on a WebIDL
8154 // interface, which the table records per name.
8155 if let Some(members) = intrinsic_proto_members(&ns) {
8156 let ctor = ns.trim_end_matches(".prototype");
8157 let mut names: Vec<String> = members
8158 .iter()
8159 .filter(|m| mode == 3 || m.starts_with('+'))
8160 .map(|m| m.strip_prefix('+').unwrap_or(m).to_string())
8161 // `getOwnPropertyNames` reports STRING keys only; the table's
8162 // `@@` entries are symbol-keyed members and belong to
8163 // `getOwnPropertySymbols` instead.
8164 .filter(|m| !m.starts_with("@@"))
8165 .collect();
8166 // Plus whatever a script patched onto this prototype under a NEW
8167 // name — an ordinary enumerable own property, so it lists in every
8168 // mode. Without it `Object.keys(Array.prototype)` stayed `[]` after
8169 // an assignment that `Array.prototype.patch` read back happily.
8170 //
8171 // Assigning over an EXISTING member is a `[[Set]]`, which leaves
8172 // that member's attributes alone: restoring a saved `join` must not
8173 // turn it into an enumerable key.
8174 for k in with_host(|h| h.builtin_static_keys(&ns)) {
8175 if !intrinsic_proto_member(&ns, &k) && !names.contains(&k) {
8176 names.push(k);
8177 }
8178 }
8179 return Ok(with_host(|h| {
8180 let out: Vec<Value> = names
8181 .iter()
8182 .map(|name| {
8183 // An accessor member has no thunk — `Map.prototype.size`
8184 // is not a function — so a VALUE read of one answers
8185 // undefined rather than synthesizing a callable.
8186 let val = |h: &mut host::JsHost| {
8187 if let Some(v) = h.builtin_static(&ns, name) {
8188 return v;
8189 }
8190 let key = format!("@proto:{ctor}:{name}");
8191 if builtin_meta(&key).is_some() {
8192 h.alloc(JsObj::Builtin(key))
8193 } else {
8194 Value::Undef
8195 }
8196 };
8197 match mode {
8198 1 => val(h),
8199 2 => {
8200 let ks = h.new_str(name.clone());
8201 let v = val(h);
8202 h.new_array(vec![ks, v])
8203 }
8204 _ => h.new_str(name.clone()),
8205 }
8206 })
8207 .collect();
8208 h.new_array(out)
8209 }));
8210 }
8211 if let Some(names) = builtin_proto_method_names(&ns) {
8212 return Ok(with_host(|h| {
8213 let out: Vec<Value> = names
8214 .iter()
8215 .map(|name| match mode {
8216 1 => h.alloc(JsObj::Builtin(format!(
8217 "@proto:{}:{name}",
8218 ns.trim_end_matches(".prototype")
8219 ))),
8220 2 => {
8221 let ks = h.new_str(*name);
8222 let val = h.alloc(JsObj::Builtin(format!(
8223 "@proto:{}:{name}",
8224 ns.trim_end_matches(".prototype")
8225 )));
8226 h.new_array(vec![ks, val])
8227 }
8228 _ => h.new_str(*name),
8229 })
8230 .collect();
8231 h.new_array(out)
8232 }));
8233 }
8234 // A stdlib namespace (`Buffer`, `require('buffer')`): its own enumerable
8235 // keys are the members node-js implements, each resolved to the same
8236 // first-class value a property read would give.
8237 let mut names = crate::stdlib::namespace_keys(&ns);
8238 // A core namespace (`Reflect`, `Math`, `JSON`) has no stdlib key list —
8239 // its members live in the builtin dispatch table. They are
8240 // non-enumerable in V8, so they surface only under
8241 // `getOwnPropertyNames`/`Reflect.ownKeys` (mode 3), never `Object.keys`.
8242 if names.is_empty() && mode == 3 {
8243 let prefix = format!("{ns}.");
8244 // A builtin constructor's own `length`/`name`/`prototype` come
8245 // first, as they do in V8.
8246 if is_builtin_ctor(&ns) {
8247 names.extend(["length", "name", "prototype"].map(str::to_string));
8248 }
8249 names.extend(
8250 NS_METHODS
8251 .iter()
8252 .filter_map(|q| q.strip_prefix(&prefix))
8253 .map(|m| m.to_string()),
8254 );
8255 // The numeric constants are members too. Without them
8256 // `getOwnPropertyNames(Math)` reported 35 of the 43 names node-js
8257 // actually answers — the eight it dropped being `PI` and its
8258 // siblings, which read fine and now own a descriptor as well.
8259 names.extend(
8260 namespace_constants(&ns)
8261 .iter()
8262 .map(|(k, _)| (*k).to_string()),
8263 );
8264 }
8265 // A builtin FUNCTION owns exactly `length` and `name` (10.3.3-4), so
8266 // `Object.getOwnPropertyNames(Math.max)` is `[ 'length', 'name' ]` — it
8267 // was `[]`, which said the function had no properties at all while both
8268 // of them read back a value. `length` is listed only where the intrinsic
8269 // table has an arity, so the names never advertise a read that answers
8270 // `undefined`.
8271 if names.is_empty() && mode == 3 && host::builtin_is_callable(&ns) {
8272 if builtin_meta(&ns).is_some() {
8273 names.push("length".to_string());
8274 }
8275 names.push("name".to_string());
8276 }
8277 // Whatever a script assigned onto the namespace, in assignment order and
8278 // after the built-in members — an ordinary enumerable own property, so
8279 // it surfaces under `Object.keys` too and not only `ownKeys`. These were
8280 // missing from every listing, which made a patched prototype read as
8281 // unpatched to any code that enumerates rather than reads.
8282 for k in with_host(|h| h.builtin_static_keys(&ns)) {
8283 if !names.contains(&k) {
8284 names.push(k);
8285 }
8286 }
8287 if !names.is_empty() {
8288 let entries: Vec<(String, Value)> = names
8289 .into_iter()
8290 .map(|k| {
8291 let val = namespace_property(&ns, &k);
8292 (k, val)
8293 })
8294 .collect();
8295 return Ok(with_host(|h| {
8296 let out: Vec<Value> = entries
8297 .into_iter()
8298 .map(|(k, val)| match mode {
8299 1 => val,
8300 2 => {
8301 let ks = h.new_str(k);
8302 h.new_array(vec![ks, val])
8303 }
8304 _ => h.new_str(k),
8305 })
8306 .collect();
8307 h.new_array(out)
8308 }));
8309 }
8310 }
8311 // mode 3 (`getOwnPropertyNames`) reports every own string key including the
8312 // non-enumerable ones, plus the exotic `length` an array carries.
8313 let entries: Vec<(String, Value)> = with_host(|h| {
8314 if mode == 3 {
8315 // An array's exotic `length` is already placed (after the indices,
8316 // before the ordinary string keys) by `own_key_names`.
8317 return h
8318 .own_key_names(&v, false)
8319 .into_iter()
8320 .map(|k| (k, Value::Undef))
8321 .collect();
8322 }
8323 Vec::new()
8324 });
8325 // `Object.keys` (mode 0) wants NAMES. `own_enum_entries_deep` returns
8326 // key/value pairs, so asking it for them ran every enumerable getter —
8327 // 20.1.2.17 -> 7.3.23 EnumerableOwnProperties only needs `[[GetOwnProperty]]`
8328 // for the enumerable flag, never `[[Get]]`, and a getter can throw or have
8329 // side effects:
8330 //
8331 // let n = 0; const o = { get g() { n++; return 1 } };
8332 // Object.keys(o); n // was 1, node says 0
8333 //
8334 // `values`/`entries` (modes 1 and 2) do read, and still do.
8335 let entries = match mode {
8336 3 => entries,
8337 0 => with_host(|h| h.own_enum_key_names(&v))
8338 .into_iter()
8339 .map(|k| (k, Value::Undef))
8340 .collect(),
8341 _ => host::own_enum_entries_deep(&v)?,
8342 };
8343 Ok(with_host(|h| {
8344 let out: Vec<Value> = entries
8345 .into_iter()
8346 .map(|(k, val)| match mode {
8347 0 | 3 => h.new_str(k),
8348 1 => val,
8349 _ => {
8350 let ks = h.new_str(k);
8351 h.new_array(vec![ks, val])
8352 }
8353 })
8354 .collect();
8355 h.new_array(out)
8356 }))
8357}
8358
8359fn object_assign(args: Vec<Value>) -> Result<Value, String> {
8360 let target = arg0(&args);
8361 // 20.1.2.1 step 1 is `ToObject(target)`, so a nullish TARGET throws while a
8362 // nullish SOURCE is skipped (`Object.assign({}, null)` is `{}`).
8363 require_object_coercible(&target)?;
8364 for src in args.iter().skip(1) {
8365 // `Object.assign` copies own *enumerable* properties, running any getter
8366 // — symbol-keyed ones included (7.3.25).
8367 let entries = host::own_enum_entries_deep(src)?;
8368 let syms = with_host(|h| h.own_symbol_entries(src));
8369 // A plain object target is filled in place (one borrow, then a single
8370 // re-canonicalization of the integer-index keys).
8371 let filled = with_host(|h| {
8372 if let Some(JsObj::Object(p)) = h.get_mut(&target) {
8373 for (k, v) in entries.iter().cloned().chain(syms.iter().cloned()) {
8374 p.insert(k, v);
8375 }
8376 host::canonicalize_own_keys(p);
8377 return true;
8378 }
8379 false
8380 });
8381 // Any OTHER target — an array being the common one — goes through the
8382 // ordinary Set path. The in-place branch above matched `JsObj::Object`
8383 // only, so `Object.assign([1,2], {extra:9})` silently copied NOTHING and
8384 // returned the untouched array: no error, just a missing property. The
8385 // Set path is what an `arr.extra = 9` assignment already used, so index
8386 // and non-index keys land where they do for a direct write.
8387 if !filled {
8388 for (k, v) in entries.into_iter().chain(syms) {
8389 set_property(&target, &k, v)?;
8390 }
8391 }
8392 }
8393 Ok(target)
8394}
8395
8396fn object_from_entries(args: Vec<Value>) -> Result<Value, String> {
8397 let pairs = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
8398 let mut props: IndexMap<String, Value> = IndexMap::new();
8399 for p in pairs {
8400 let kv = with_host(|h| h.iter_vec(&p)).unwrap_or_default();
8401 let key = with_host(|h| h.str_of(&kv.first().cloned().unwrap_or(Value::Undef)));
8402 let val = kv.get(1).cloned().unwrap_or(Value::Undef);
8403 props.insert(key, val);
8404 }
8405 Ok(with_host(|h| h.new_object(props)))
8406}
8407
8408/// `Object.groupBy(items, cb)` — group the iterable `items` into a null-prototype
8409/// object. Keys are `ToPropertyKey(cb(item, index))`; values are arrays of the
8410/// members mapped to that key, in first-seen key order.
8411fn object_group_by(args: Vec<Value>) -> Result<Value, String> {
8412 group_by_check_iterable(&arg0(&args), "Object.groupBy")?;
8413 let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8414 let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
8415 // Stepped, not drained: the callback runs per element, so a throwing one
8416 // stops at the first. Draining first meant an infinite source never reached
8417 // the callback at all and the call HUNG.
8418 host::iter_for_each(&arg0(&args), |item, i| {
8419 let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8420 let key = with_host(|h| h.property_key(&key_v));
8421 groups.entry(key).or_default().push(item);
8422 Ok(())
8423 })?;
8424 let props: IndexMap<String, Value> = with_host(|h| {
8425 groups
8426 .into_iter()
8427 .map(|(k, v)| (k, h.new_array(v)))
8428 .collect()
8429 });
8430 let obj = with_host(|h| h.new_object(props));
8431 // A null-prototype object (as Node returns), so it has no inherited members.
8432 with_host(|h| {
8433 let nv = h.null();
8434 h.set_proto(&obj, nv);
8435 });
8436 Ok(obj)
8437}
8438
8439/// The `groupBy` family words a non-iterable argument its OWN way — a third
8440/// vocabulary, alongside the array-literal spread's and the call spread's:
8441///
8442/// ```text
8443/// null / undefined "<Name> called on null or undefined"
8444/// anything else "<typeof> [value ]is not iterable (cannot read property
8445/// Symbol(Symbol.iterator))"
8446/// ```
8447///
8448/// A plain object, a symbol and a bigint name only their TYPE; a number, a
8449/// string and a boolean name the value too.
8450fn group_by_check_iterable(v: &Value, name: &str) -> Result<(), String> {
8451 if with_host(|h| h.is_nullish(v)) {
8452 return Err(host::type_error(&format!(
8453 "{name} called on null or undefined"
8454 )));
8455 }
8456 // Asked WITHOUT consuming anything: `iter_all` would drain the iterator
8457 // here, so the stepping loop below then saw an exhausted one — the finite
8458 // case returned an empty group and the infinite case was back to hanging.
8459 let iter_fn = get_property(v, "@@iterator").unwrap_or(Value::Undef);
8460 if with_host(|h| host::is_callable(h, &iter_fn)) {
8461 return Ok(());
8462 }
8463 Err(host::type_error(¬_iterable_typed(v)))
8464}
8465
8466/// The `<type> <value> is not iterable (cannot read property
8467/// Symbol(Symbol.iterator))` wording, which node uses wherever the source has
8468/// no name to report: a plain object, a symbol and a bigint name only their
8469/// TYPE; a number, a string and a boolean name the value too.
8470pub(crate) fn not_iterable_typed(v: &Value) -> String {
8471 let shown = with_host(|h| {
8472 let kind = h.type_of(v);
8473 match kind {
8474 "object" | "symbol" | "bigint" => kind.to_string(),
8475 "string" => format!("string \"{}\"", h.str_of(v)),
8476 _ => format!("{kind} {}", h.str_of(v)),
8477 }
8478 });
8479 format!("{shown} is not iterable (cannot read property Symbol(Symbol.iterator))")
8480}
8481
8482/// `Map.groupBy(items, cb)` — like `Object.groupBy` but returns a `Map` keyed by
8483/// the raw `cb(item, index)` value under SameValueZero (so object/any keys work).
8484fn map_group_by(args: Vec<Value>) -> Result<Value, String> {
8485 group_by_check_iterable(&arg0(&args), "Map.groupBy")?;
8486 let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8487 let m = with_host(|h| {
8488 h.alloc(JsObj::Map {
8489 entries: IndexMap::new(),
8490 weak: false,
8491 })
8492 });
8493 // Stepped for the same reason `Object.groupBy` is.
8494 host::iter_for_each(&arg0(&args), |item, i| {
8495 let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8496 let existing = map_method(&m, "get", vec![key_v.clone()])?;
8497 if matches!(existing, Value::Undef) {
8498 let arr = with_host(|h| h.new_array(vec![item]));
8499 map_method(&m, "set", vec![key_v, arr])?;
8500 } else {
8501 with_host(|h| {
8502 if let Some(JsObj::Array(a)) = h.get_mut(&existing) {
8503 a.push(item);
8504 }
8505 });
8506 }
8507 Ok(())
8508 })?;
8509 Ok(m)
8510}
8511
8512/// `Array.fromAsync(items[, mapFn])` — a Promise for an array, awaiting each
8513/// element and each `mapFn` result.
8514///
8515/// Written in JavaScript and compiled once, because the operation IS an async
8516/// function: a Rust builtin runs outside any coroutine and has no way to await,
8517/// so draining a promise from there would mean running the microtask queue by
8518/// hand. Delegating to the engine's own `async`/`for await` keeps the
8519/// suspension semantics — and the ordering they imply — exactly the language's.
8520///
8521/// The source may be an async iterable, a sync iterable, a bare iterator, or an
8522/// array-like. Everything iterable goes through `for await`, which awaits a sync
8523/// source's elements individually — that is what makes
8524/// `Array.fromAsync([1, Promise.resolve(2)])` answer `[1, 2]`. A bare `.next` is
8525/// accepted because an async generator object does not expose
8526/// `Symbol.asyncIterator` on this frontend.
8527fn array_from_async(args: Vec<Value>) -> Result<Value, String> {
8528 thread_local! {
8529 static IMPL: std::cell::RefCell<Option<Value>> = const { std::cell::RefCell::new(None) };
8530 }
8531 const SRC: &str = "(async function (items, mapFn, thisArg) {\n\
8532 const out = []; let i = 0;\n\
8533 const step = async (v) => { const a = await v; out.push(mapFn ? await mapFn.call(thisArg, a, i) : a); i++; };\n\
8534 const iterable = items != null && (typeof items[Symbol.asyncIterator] === 'function'\n\
8535 || typeof items[Symbol.iterator] === 'function' || typeof items.next === 'function');\n\
8536 if (iterable) {\n\
8537 for await (const v of items) { out.push(mapFn ? await mapFn.call(thisArg, v, i) : v); i++; }\n\
8538 return out;\n\
8539 }\n\
8540 const len = items == null ? 0 : (Math.trunc(Number(items.length)) || 0);\n\
8541 while (i < len) { await step(items[i]); }\n\
8542 return out;\n\
8543 })";
8544 let f = IMPL.with(|c| c.borrow().clone());
8545 let f = match f {
8546 Some(f) => f,
8547 None => {
8548 let f = crate::eval_in_global_scope(SRC)?;
8549 IMPL.with(|c| *c.borrow_mut() = Some(f.clone()));
8550 f
8551 }
8552 };
8553 host::invoke(&f, args, None)
8554}
8555
8556fn array_from(args: Vec<Value>) -> Result<Value, String> {
8557 // `Array.from` accepts generators and user iterables, plus array-likes with a
8558 // numeric `.length`.
8559 let src = arg0(&args);
8560 if let Some(cb) = args.get(1).cloned() {
8561 // Stepped, not drained: the mapper runs per element as the iterator
8562 // yields it (23.1.2.1 step 6.e). Materializing the whole sequence first
8563 // meant `Array.from(infiniteIterator, fn)` never reached the mapper at
8564 // all and HUNG, and a throwing mapper could not close the iterator.
8565 let this = this_arg(&args, 2);
8566 let mut out = Vec::new();
8567 let mapped = host::iter_for_each(&src, |v, i| {
8568 out.push(host::invoke(
8569 &cb,
8570 vec![v, Value::Float(i as f64)],
8571 this.clone(),
8572 )?);
8573 Ok(())
8574 });
8575 match mapped {
8576 Ok(()) => {}
8577 // An array-LIKE has no iterator; fall back to its indexed items.
8578 Err(e) if host::user_iterator_fn(&src).is_none() && e.ends_with(" is not iterable") => {
8579 out.clear();
8580 for (i, it) in array_like_items(&src).into_iter().enumerate() {
8581 out.push(host::invoke(
8582 &cb,
8583 vec![it, Value::Float(i as f64)],
8584 this.clone(),
8585 )?);
8586 }
8587 }
8588 Err(e) => return Err(e),
8589 }
8590 return construct_array_like(host::current_static_this(), out);
8591 }
8592 let items = match host::iter_all(&src) {
8593 Ok(v) => v,
8594 Err(_) => array_like_items(&src),
8595 };
8596 // 23.1.2.1 step 5: `Array.from` builds through `this`, so on a subclass the
8597 // result is an instance of it. It always allocated a plain array, which is
8598 // also why `A.from([1]).map(f) instanceof A` was false — the species chain
8599 // never started.
8600 construct_array_like(host::current_static_this(), items)
8601}
8602
8603/// Items of an array-like `{ length, 0, 1, … }` object (for `Array.from`).
8604pub(crate) fn array_like_items(src: &Value) -> Vec<Value> {
8605 // `LengthOfArrayLike` is `ToLength(Get(O, "length"))`, and `ToNumber` runs a
8606 // user `valueOf` — `Array.from({length: {valueOf: () => 1}})` was empty
8607 // because the infallible read does no `ToPrimitive`. A throw from it is
8608 // swallowed here for the same reason the `length` read is: this helper has
8609 // no way to report one, and every caller treats an unreadable length as 0.
8610 let len = get_property(src, "length")
8611 .ok()
8612 .and_then(|l| host::to_primitive(&l, "number").ok())
8613 .map(|l| with_host(|h| h.to_number(&l)))
8614 .unwrap_or(0.0);
8615 if !len.is_finite() || len <= 0.0 {
8616 return Vec::new();
8617 }
8618 (0..len as usize)
8619 .map(|i| get_property(src, &i.to_string()).unwrap_or(Value::Undef))
8620 .collect()
8621}
8622
8623// ── JSON ──────────────────────────────────────────────────────────────────────
8624
8625fn json_stringify(args: Vec<Value>) -> Result<Value, String> {
8626 // A CALLABLE second argument is the replacer function, and it is checked
8627 // before the array form (`IsCallable` precedes `IsArray` in the spec), so a
8628 // callable never also reaches the key-filter path below.
8629 let replacer = args
8630 .get(1)
8631 .filter(|r| with_host(|h| host::is_callable(h, r)))
8632 .cloned();
8633 // `toJSON` and the replacer run BEFORE serialization and are user code, so
8634 // the tree is rewritten first — outside the host borrow `json_str` holds,
8635 // and before the BigInt walk, which has no cycle guard of its own.
8636 //
8637 // The top-level value is a property of a synthetic wrapper `{ "": value }`
8638 // under key `""`, which is exactly the holder the replacer receives as
8639 // `this` on its first call.
8640 let root = arg0(&args);
8641 let wrapper = with_host(|h| {
8642 let mut m: IndexMap<String, Value> = IndexMap::new();
8643 m.insert(String::new(), root.clone());
8644 h.new_object(m)
8645 });
8646 let v = apply_to_json(&wrapper, "", &root, &mut Vec::new(), replacer.as_ref())?;
8647 // A BigInt anywhere in a serializable position is a TypeError (JSON has no
8648 // bigint form), matching Node's exact message.
8649 if with_host(|h| json_has_bigint(h, &v)) {
8650 return Err(host::type_error("Do not know how to serialize a BigInt"));
8651 }
8652 let indent = match args.get(2) {
8653 Some(Value::Float(f)) => " ".repeat((*f as usize).min(10)),
8654 Some(other) => with_host(|h| h.as_str(other)).unwrap_or_default(),
8655 None => String::new(),
8656 };
8657 // A replacer array (args[1]) restricts which object keys are serialized.
8658 let keys: Option<Vec<String>> = args.get(1).and_then(|r| {
8659 with_host(|h| match h.get(r) {
8660 Some(JsObj::Array(items)) => {
8661 Some(items.iter().map(|k| h.str_of(k)).collect::<Vec<_>>())
8662 }
8663 _ => None,
8664 })
8665 });
8666 let s = with_host(|h| json_str(h, &v, &indent, 0, keys.as_deref()));
8667 match s {
8668 Some(s) => Ok(with_host(|h| h.new_str(s))),
8669 None => Ok(Value::Undef),
8670 }
8671}
8672
8673/// One `SerializeJSONProperty(key, holder)` step: rewrite `v` (the value read
8674/// from `holder[key]`) by calling its `toJSON(key)` and then the replacer
8675/// function as `replacer.call(holder, key, value)`, then recurse into whatever
8676/// object survives. Applies to user methods, class methods, and the native
8677/// `Date`/`Buffer`/`URL` accessors alike.
8678///
8679/// Returns a fresh tree; the input is never mutated. `path` carries the chain of
8680/// objects currently being walked so a cyclic structure is reported rather than
8681/// spinning forever.
8682///
8683/// `toJSON` is called on the value ONCE and is NOT re-applied to its own result
8684/// — `{toJSON(){ return {toJSON(){ return 1 }} }}` serializes as `{}` in Node,
8685/// because the inner method is a plain (unserializable) function property of the
8686/// returned object, not a second conversion hook.
8687fn apply_to_json(
8688 holder: &Value,
8689 key: &str,
8690 v: &Value,
8691 path: &mut JsonPath,
8692 rep: Option<&Value>,
8693) -> Result<Value, String> {
8694 let mut v = v.clone();
8695 if matches!(v, Value::Obj(_)) {
8696 let tag = crate::stdlib::native_tag(&v);
8697 // 25.5.2.1 step 2: `toJSON` is looked up with `[[Get]]`, so a PROXY
8698 // supplies one through its `get` trap. `lookup_chain` walks the
8699 // property map and never asks the handler, so a proxy carrying a
8700 // `toJSON` was serialized as a plain object instead of by its own
8701 // method — and node's trap log starts with that `get`.
8702 let to_json = if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8703 get_property(&v, "toJSON")?
8704 } else {
8705 with_host(|h| host::lookup_chain(h, &v, "toJSON")).unwrap_or(Value::Undef)
8706 };
8707 let has_to_json = with_host(|h| host::is_callable(h, &to_json))
8708 || tag
8709 .as_deref()
8710 .map(crate::stdlib::has_to_json)
8711 .unwrap_or(false);
8712 if has_to_json {
8713 let k = with_host(|h| h.new_str(key.to_string()));
8714 v = host::call_method(&v, "toJSON", vec![k])?;
8715 }
8716 }
8717 if let Some(rep) = rep {
8718 let k = with_host(|h| h.new_str(key.to_string()));
8719 v = host::invoke(rep, vec![k, v.clone()], Some(holder.clone()))?;
8720 }
8721 // How `v` was reached from its holder, as V8 names the step in a
8722 // circular-structure message: `index 1` under an array, else `property 'k'`.
8723 let via = if matches!(with_host(|h| h.get(holder).cloned()), Some(JsObj::Array(_))) {
8724 format!("index {key}")
8725 } else {
8726 format!("property '{key}'")
8727 };
8728 json_walk_children(&v, path, &via, rep)
8729}
8730
8731/// The objects `JSON.stringify` is inside of, outermost first, each with the
8732/// step that reached it from its holder (`property 'x'` / `index 1`).
8733type JsonPath = Vec<(String, Value)>;
8734
8735/// V8's `ConstructCircularStructureErrorMessage`: the cycle from the object it
8736/// starts at to the key that closes it. At most the first two and the last one
8737/// intermediate step are listed, with `| ...` standing for the rest.
8738fn circular_json_message(path: &JsonPath, start: usize, closing: &str) -> String {
8739 const PREFIX: usize = 2;
8740 const POSTFIX: usize = 1;
8741 let ctor = |v: &Value| -> String {
8742 with_host(|h| match h.get(v) {
8743 Some(JsObj::Array(_)) if h.proto_of(v).is_none() => "Array".to_string(),
8744 _ => match h.ctor_name(v) {
8745 n if n.is_empty() => "Object".to_string(),
8746 n => n,
8747 },
8748 })
8749 };
8750 let line = |i: usize| {
8751 format!(
8752 "\n | {} -> object with constructor '{}'",
8753 path[i].0,
8754 ctor(&path[i].1)
8755 )
8756 };
8757 let mut msg = format!(
8758 "Converting circular structure to JSON\n --> starting at object with constructor '{}'",
8759 ctor(&path[start].1)
8760 );
8761 let prefix_end = path.len().min(start + 1 + PREFIX);
8762 for i in start + 1..prefix_end {
8763 msg.push_str(&line(i));
8764 }
8765 if path.len() > prefix_end + POSTFIX {
8766 msg.push_str("\n | ...");
8767 }
8768 for i in prefix_end.max(path.len().saturating_sub(POSTFIX))..path.len() {
8769 msg.push_str(&line(i));
8770 }
8771 msg.push_str(&format!("\n --- {closing} closes the circle"));
8772 msg
8773}
8774
8775/// Whether a raw property key of a host object is one `json_str` serializes. The
8776/// internal slots (`@@`-prefixed symbol keys, `#`-prefixed private fields) are
8777/// invisible to JSON, so the replacer must not be invoked for them either.
8778fn json_visible_key(k: &str) -> bool {
8779 !k.starts_with("@@") && !k.starts_with('#')
8780}
8781
8782/// Recurse into the elements/properties of an already-converted value, running
8783/// `apply_to_json` for each with this value as the holder.
8784fn json_walk_children(
8785 v: &Value,
8786 path: &mut JsonPath,
8787 via: &str,
8788 rep: Option<&Value>,
8789) -> Result<Value, String> {
8790 if !matches!(v, Value::Obj(_)) {
8791 return Ok(v.clone());
8792 }
8793 // A value that contains itself has no JSON form.
8794 if let Some(start) = with_host(|h| path.iter().position(|(_, p)| h.strict_eq(p, v))) {
8795 return Err(host::type_error(&circular_json_message(path, start, via)));
8796 }
8797 // A Proxy owns no property map, so it is snapshotted through its traps into
8798 // the plain array/object `SerializeJSONArray`/`SerializeJSONObject` describe
8799 // — which read every member through `[[Get]]`, exactly as the snapshot does.
8800 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8801 let snap = crate::proxy::json_snapshot(v)?;
8802 path.push((via.to_string(), v.clone()));
8803 let out = json_walk_children(&snap, path, via, rep);
8804 path.pop();
8805 return out;
8806 }
8807 let obj = with_host(|h| h.get(v).cloned());
8808 path.push((via.to_string(), v.clone()));
8809 let out = (|| match obj {
8810 Some(JsObj::Array(items)) => {
8811 // Read the elements through the accessor-aware funnel: an index
8812 // with a getter must be SERIALIZED as what the getter returns, and
8813 // the backing vector still holds the stale slot.
8814 let mut resolved = items;
8815 // An index with a getter must be SERIALIZED as what the getter
8816 // returns, and it also forces a rebuild below: keeping the original
8817 // array would hand the serializer back the stale backing vector.
8818 let had_accessor = resolve_index_accessors(v, &mut resolved);
8819 let items = resolved;
8820 let mut out = Vec::with_capacity(items.len());
8821 let mut changed = had_accessor;
8822 for (i, it) in items.iter().enumerate() {
8823 let nv = apply_to_json(v, &i.to_string(), it, path, rep)?;
8824 changed |= !with_host(|h| h.strict_eq(&nv, it));
8825 out.push(nv);
8826 }
8827 // Keep identity when nothing changed, so an enclosing object is not
8828 // needlessly rebuilt (which would drop its property attributes).
8829 if changed {
8830 Ok(with_host(|h| h.new_array(out)))
8831 } else {
8832 Ok(v.clone())
8833 }
8834 }
8835 Some(JsObj::Object(props)) => {
8836 // An enumerable own accessor must have its getter RUN and the result
8837 // serialized. That cannot happen inside `json_str` (which holds the
8838 // host borrow), so materialize here — the same reason `toJSON` is
8839 // applied in this pass.
8840 let has_accessor = with_host(|h| {
8841 h.own_accessor_keys(v)
8842 .iter()
8843 .any(|k| h.prop_attrs(v, k).enumerable)
8844 });
8845 if has_accessor {
8846 let mut next: IndexMap<String, Value> = IndexMap::new();
8847 for (k, val) in host::own_enum_entries_deep(v)? {
8848 let nv = if json_visible_key(&k) {
8849 apply_to_json(v, &k, &val, path, rep)?
8850 } else {
8851 val
8852 };
8853 next.insert(k, nv);
8854 }
8855 return Ok(with_host(|h| h.new_object(next)));
8856 }
8857 // Only rebuild when a descendant actually changed, so plain data keeps
8858 // its identity (and its prototype / native tag).
8859 let mut next: IndexMap<String, Value> = IndexMap::new();
8860 let mut changed = false;
8861 for (k, val) in &props {
8862 let nv = if json_visible_key(k) {
8863 apply_to_json(v, k, val, path, rep)?
8864 } else {
8865 val.clone()
8866 };
8867 changed |= !with_host(|h| h.strict_eq(&nv, val));
8868 next.insert(k.clone(), nv);
8869 }
8870 if changed {
8871 Ok(with_host(|h| {
8872 let o = h.new_object(next);
8873 h.copy_prop_attrs(v, &o);
8874 o
8875 }))
8876 } else {
8877 Ok(v.clone())
8878 }
8879 }
8880 _ => Ok(v.clone()),
8881 })();
8882 path.pop();
8883 out
8884}
8885
8886/// Whether a value tree contains a `BigInt` in a position `JSON.stringify` would
8887/// try to serialize (a value in an array/object) — such a value throws.
8888fn json_has_bigint(h: &host::JsHost, v: &Value) -> bool {
8889 match h.get(v) {
8890 Some(JsObj::BigInt(_)) => true,
8891 Some(JsObj::Array(items)) => items.iter().any(|x| json_has_bigint(h, x)),
8892 Some(JsObj::Object(props)) => props
8893 .iter()
8894 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
8895 .any(|(_, val)| json_has_bigint(h, val)),
8896 _ => false,
8897 }
8898}
8899
8900fn json_str(
8901 h: &host::JsHost,
8902 v: &Value,
8903 indent: &str,
8904 depth: usize,
8905 keys: Option<&[String]>,
8906) -> Option<String> {
8907 let sep = if indent.is_empty() { ":" } else { ": " };
8908 match v {
8909 Value::Undef => None,
8910 Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
8911 Value::Int(n) => Some(n.to_string()),
8912 Value::Float(f) => Some(if f.is_finite() {
8913 host::fmt_number(*f)
8914 } else {
8915 "null".into()
8916 }),
8917 Value::Str(s) => Some(json_quote(s)),
8918 Value::Obj(_) => match h.get(v) {
8919 Some(JsObj::Str(s)) => Some(json_quote(s)),
8920 Some(JsObj::Null) => Some("null".into()),
8921 // A `JSON.rawJSON` marker contributes its text VERBATIM — that is the
8922 // whole point of it, and it is why a number wider than a `double`
8923 // can survive a round trip.
8924 _ if h.fn_prop(v, "@@rawJSON").is_some() => match h.get(v) {
8925 Some(JsObj::Object(p)) => p.get("rawJSON").map(|r| h.str_of(r)),
8926 _ => None,
8927 },
8928 // A Map/Set has no ENTRIES to serialize (they are internal slots),
8929 // but any own property a script attached is serialized like an
8930 // ordinary object's: `JSON.stringify(Object.assign(new Map(), {a:1}))`
8931 // is `{"a":1}`.
8932 Some(JsObj::Map { .. })
8933 | Some(JsObj::Set { .. })
8934 | Some(JsObj::RegExp(_))
8935 // A Promise and a generator are ORDINARY objects to the serializer:
8936 // their state is internal slots, so they contribute no entries and
8937 // render as `{}`. They were being omitted entirely instead, so a
8938 // promise in an array became `null` and one in an object vanished.
8939 | Some(JsObj::Promise { .. })
8940 | Some(JsObj::Generator { .. }) => {
8941 let parts: Vec<String> = h
8942 .own_enum_entries(v)
8943 .into_iter()
8944 .filter(|(k, _)| !k.starts_with("@@") && !host::is_symbol_key(k))
8945 .filter_map(|(k, val)| {
8946 json_str(h, &val, indent, depth + 1, keys)
8947 .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8948 })
8949 .collect();
8950 Some(wrap(&parts, "{", "}", indent, depth))
8951 }
8952 // A NON-callable builtin is a namespace object, not a function, so
8953 // it serializes as one: `JSON.stringify(Math)` is `{}` (its members
8954 // are all non-enumerable), where omitting it made the whole property
8955 // disappear from its holder.
8956 Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
8957 let parts: Vec<String> = crate::stdlib::namespace_keys(n)
8958 .into_iter()
8959 .filter_map(|k| {
8960 let val = h.builtin_static(n, &k)?;
8961 json_str(h, &val, indent, depth + 1, keys)
8962 .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8963 })
8964 .collect();
8965 Some(wrap(&parts, "{", "}", indent, depth))
8966 }
8967 // Functions and symbols are omitted (undefined) as values.
8968 Some(JsObj::Func(_))
8969 | Some(JsObj::Builtin(_))
8970 | Some(JsObj::BoundMethod { .. })
8971 | Some(JsObj::BoundFunc { .. })
8972 | Some(JsObj::Class(_))
8973 | Some(JsObj::Symbol { .. }) => None,
8974 Some(JsObj::Array(items)) => {
8975 if items.is_empty() {
8976 return Some("[]".into());
8977 }
8978 let parts: Vec<String> = items
8979 .iter()
8980 .map(|x| {
8981 json_str(h, x, indent, depth + 1, keys).unwrap_or_else(|| "null".into())
8982 })
8983 .collect();
8984 Some(wrap(&parts, "[", "]", indent, depth))
8985 }
8986 Some(JsObj::Object(props)) if props.contains_key("@@primitive") => {
8987 // 25.5.2.2 step 4: a String/Number/Boolean wrapper serializes as
8988 // the primitive it boxes, not as the object holding it —
8989 // `JSON.stringify(new Number(1))` is `1`, not `{}`.
8990 json_str(h, &props["@@primitive"].clone(), indent, depth, keys)
8991 }
8992 Some(JsObj::Object(props)) => {
8993 // A replacer array restricts (and orders) which keys are emitted.
8994 let parts: Vec<String> = match keys {
8995 Some(allow) => allow
8996 .iter()
8997 .filter_map(|k| {
8998 props.get(k).and_then(|val| {
8999 json_str(h, val, indent, depth + 1, keys)
9000 .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
9001 })
9002 })
9003 .collect(),
9004 None => h
9005 .own_enum_entries(v)
9006 .iter()
9007 .filter_map(|(k, val)| {
9008 json_str(h, val, indent, depth + 1, keys)
9009 .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
9010 })
9011 .collect(),
9012 };
9013 if parts.is_empty() {
9014 return Some("{}".into());
9015 }
9016 Some(wrap(&parts, "{", "}", indent, depth))
9017 }
9018 _ => Some("null".into()),
9019 },
9020 _ => Some("null".into()),
9021 }
9022}
9023
9024fn wrap(parts: &[String], open: &str, close: &str, indent: &str, depth: usize) -> String {
9025 if indent.is_empty() {
9026 format!("{open}{}{close}", parts.join(","))
9027 } else {
9028 let pad = indent.repeat(depth + 1);
9029 let pad_close = indent.repeat(depth);
9030 format!(
9031 "{open}\n{pad}{}\n{pad_close}{close}",
9032 parts.join(&format!(",\n{pad}"))
9033 )
9034 }
9035}
9036
9037fn json_quote(s: &str) -> String {
9038 let mut out = String::from("\"");
9039 for c in s.chars() {
9040 match c {
9041 '"' => out.push_str("\\\""),
9042 '\\' => out.push_str("\\\\"),
9043 '\n' => out.push_str("\\n"),
9044 '\t' => out.push_str("\\t"),
9045 '\r' => out.push_str("\\r"),
9046 // QuoteJSONString (25.5.2.2) names SIX short escapes, not four.
9047 // Backspace and form feed were missing, so they fell through to the
9048 // `\uXXXX` arm below and `JSON.stringify("\b")` produced
9049 // `""` where node produces `"\b"`. Both parse back to the same
9050 // string, so the difference is invisible to a round trip and shows
9051 // up only as a byte mismatch against a fixture or a checksum.
9052 '\u{8}' => out.push_str("\\b"),
9053 '\u{c}' => out.push_str("\\f"),
9054 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
9055 _ => out.push(c),
9056 }
9057 }
9058 out.push('"');
9059 out
9060}
9061
9062fn json_parse(args: Vec<Value>) -> Result<Value, String> {
9063 let s = with_host(|h| h.str_of(&arg0(&args)));
9064 let mut p = JsonParser {
9065 chars: s.chars().collect(),
9066 pos: 0,
9067 prims: Vec::new(),
9068 record: args
9069 .get(1)
9070 .is_some_and(|r| with_host(|h| host::is_callable(h, r))),
9071 };
9072 p.skip_ws();
9073 if p.peek().is_none() {
9074 return Err("SyntaxError: Unexpected end of JSON input".into());
9075 }
9076 let v = p.parse_value()?;
9077 let value_end = p.pos;
9078 p.skip_ws();
9079 // Anything after the top-level value is an error — the parser used to accept
9080 // and silently discard it, so `JSON.parse('{"a":1}x')` succeeded.
9081 if let Some(c) = p.peek() {
9082 // V8 names the token kind only when it butts directly against the value
9083 // (`01` -> "Unexpected number at position 1"); with whitespace between
9084 // it is just a non-whitespace character (`1 2`).
9085 // Only a digit butted directly against a completed number literal —
9086 // V8's number scanner is still in number context there. `5"x"` and
9087 // `[0,1]0` exit the scanner cleanly and get the generic message.
9088 let after_number = value_end > 0
9089 && p.pos == value_end
9090 && p.chars[value_end - 1].is_ascii_digit()
9091 && c.is_ascii_digit();
9092 return Err(if after_number {
9093 p.err_at("Unexpected number", p.pos)
9094 } else {
9095 p.err_trailing(p.pos)
9096 });
9097 }
9098 // Optional reviver: walk bottom-up, transforming each (key, value).
9099 if let Some(reviver) = args
9100 .get(1)
9101 .filter(|r| with_host(|h| host::is_callable(h, r)))
9102 .cloned()
9103 {
9104 // The top-level holder is a fresh `{ "": value }` wrapper, as the spec
9105 // constructs before the walk.
9106 let root = with_host(|h| {
9107 let mut m: IndexMap<String, Value> = IndexMap::new();
9108 m.insert(String::new(), v.clone());
9109 h.new_object(m)
9110 });
9111 return json_revive("", v, &reviver, &root, &p.prims, &mut 0);
9112 }
9113 Ok(v)
9114}
9115
9116/// `JSON.parse` reviver walk: recurse into children first, then call
9117/// `reviver(key, value)`; a returned `undefined` drops the property.
9118///
9119/// The reviver runs with the HOLDER as `this` (25.5.1.1
9120/// InternalizeJSONProperty) — the object or array the key lives in, and at the
9121/// top level a wrapper `{ "": value }`. It was being called with no receiver,
9122/// so `this` was undefined and a reviver could not reach its siblings.
9123/// `JSON.rawJSON(text)` — a marker object whose text `JSON.stringify` emits
9124/// VERBATIM, so a number too large for a `double` survives a round trip
9125/// (`JSON.stringify({n: JSON.rawJSON("12345678901234567890")})`).
9126///
9127/// The validation is not "does `JSON.parse` accept it": node's rule, measured
9128/// across the whole matrix, is
9129///
9130/// ```text
9131/// "" -> SyntaxError: Invalid value for JSON.rawJSON
9132/// leading whitespace -> the parse error for that first character
9133/// a complete literal -> ok
9134/// anything left over -> SyntaxError: Invalid value for JSON.rawJSON
9135/// a broken literal -> the parse error the scanner raised
9136/// ```
9137///
9138/// so `" 1"` reports an unexpected token while `"1 "` and `"1,2"` report the
9139/// invalid-value message even though `JSON.parse` accepts the former and gives
9140/// a token error for the latter.
9141fn json_raw(args: Vec<Value>) -> Result<Value, String> {
9142 const INVALID: &str = "SyntaxError: Invalid value for JSON.rawJSON";
9143 let s = with_host(|h| h.str_of(&arg0(&args)));
9144 if s.is_empty() {
9145 return Err(INVALID.into());
9146 }
9147 let mut p = JsonParser {
9148 chars: s.chars().collect(),
9149 pos: 0,
9150 prims: Vec::new(),
9151 record: false,
9152 };
9153 // An object or an array is rejected where it starts, as leading whitespace
9154 // is — both are "not a primitive", but node reports the token.
9155 if matches!(p.peek(), Some('{') | Some('[')) || p.peek().is_some_and(|c| c.is_whitespace()) {
9156 return Err(p.err_token(0));
9157 }
9158 p.parse_value()?;
9159 if p.pos != p.chars.len() {
9160 // A digit butted against a completed number is still in the number
9161 // scanner, so `"01"` reports the scanner's error rather than leftover
9162 // input — the same distinction `json_parse` draws for trailing text.
9163 if p.chars[p.pos - 1].is_ascii_digit() && p.chars[p.pos].is_ascii_digit() {
9164 return Err(p.err_at("Unexpected number", p.pos));
9165 }
9166 return Err(INVALID.into());
9167 }
9168 // A null prototype and one own `rawJSON` property, frozen — the brand is a
9169 // hidden slot so `Object.keys` stays `["rawJSON"]`.
9170 Ok(with_host(|h| {
9171 let mut m: IndexMap<String, Value> = IndexMap::new();
9172 let text = h.new_str(s);
9173 m.insert("rawJSON".into(), text);
9174 let o = h.new_object(m);
9175 let null = h.null();
9176 h.set_proto(&o, null);
9177 h.set_fn_prop(&o, "@@rawJSON", Value::Bool(true));
9178 h.seal_object(&o, true);
9179 o
9180 }))
9181}
9182
9183/// `JSON.isRawJSON(v)` — the brand check. A hand-built `{ rawJSON: "1" }` is
9184/// NOT one, which is why the marker is a hidden slot rather than the property.
9185fn json_is_raw(args: Vec<Value>) -> Result<Value, String> {
9186 Ok(Value::Bool(is_raw_json(&arg0(&args))))
9187}
9188
9189fn is_raw_json(v: &Value) -> bool {
9190 with_host(|h| h.fn_prop(v, "@@rawJSON")).is_some()
9191}
9192
9193fn json_revive(
9194 key: &str,
9195 val: Value,
9196 reviver: &Value,
9197 holder: &Value,
9198 prims: &[String],
9199 next: &mut usize,
9200) -> Result<Value, String> {
9201 // A PRIMITIVE claims the next recorded source slice before its children
9202 // would — it has none — and a container claims nothing. The walk descends in
9203 // the same order the parse produced them, so one cursor lines the two up.
9204 let is_container =
9205 with_host(|h| matches!(h.get(&val), Some(JsObj::Array(_)) | Some(JsObj::Object(_))));
9206 let source = if !is_container {
9207 let s = prims.get(*next).cloned();
9208 if s.is_some() {
9209 *next += 1;
9210 }
9211 s
9212 } else {
9213 None
9214 };
9215 match with_host(|h| h.get(&val).cloned()) {
9216 Some(JsObj::Array(items)) => {
9217 for i in 0..items.len() {
9218 let elem = with_host(|h| match h.get(&val) {
9219 Some(JsObj::Array(it)) => it[i].clone(),
9220 _ => Value::Undef,
9221 });
9222 let nv = json_revive(&i.to_string(), elem, reviver, &val, prims, next)?;
9223 with_host(|h| {
9224 if let Some(JsObj::Array(it)) = h.get_mut(&val) {
9225 it[i] = nv;
9226 }
9227 });
9228 }
9229 }
9230 Some(JsObj::Object(props)) => {
9231 let keys: Vec<String> = props
9232 .keys()
9233 .filter(|k| !k.starts_with("@@"))
9234 .cloned()
9235 .collect();
9236 for k in keys {
9237 let elem = with_host(|h| match h.get(&val) {
9238 Some(JsObj::Object(p)) => p.get(&k).cloned().unwrap_or(Value::Undef),
9239 _ => Value::Undef,
9240 });
9241 let nv = json_revive(&k, elem, reviver, &val, prims, next)?;
9242 with_host(|h| {
9243 if let Some(JsObj::Object(p)) = h.get_mut(&val) {
9244 if matches!(nv, Value::Undef) {
9245 p.shift_remove(&k);
9246 } else {
9247 p.insert(k.clone(), nv);
9248 }
9249 }
9250 });
9251 }
9252 }
9253 _ => {}
9254 }
9255 let kv = with_host(|h| h.new_str(key.to_string()));
9256 // 25.5.1.1 step 2.b: the reviver's THIRD argument. `{ source }` for a
9257 // primitive, an empty object for an array or an object — node passes it
9258 // either way, and code reading `ctx.source` used to die on `undefined`
9259 // because only two arguments were passed.
9260 let ctx = with_host(|h| {
9261 let mut m: IndexMap<String, Value> = IndexMap::new();
9262 if let Some(s) = source {
9263 let sv = h.new_str(s);
9264 m.insert("source".into(), sv);
9265 }
9266 h.new_object(m)
9267 });
9268 host::invoke(reviver, vec![kv, val, ctx], Some(holder.clone()))
9269}
9270
9271struct JsonParser {
9272 chars: Vec<char>,
9273 pos: usize,
9274 /// Source text of each PRIMITIVE value, in parse order — what the reviver's
9275 /// third argument reports as `context.source` (25.5.1.1). Only collected
9276 /// when a reviver was supplied.
9277 ///
9278 /// A flat list rather than a parallel tree because the reviver walk visits
9279 /// primitives in the same depth-first order the parse produced them, so an
9280 /// index into this is enough to line them up.
9281 prims: Vec<String>,
9282 record: bool,
9283}
9284impl JsonParser {
9285 fn peek(&self) -> Option<char> {
9286 self.chars.get(self.pos).copied()
9287 }
9288
9289 /// `at position N (line L column C)` — the location suffix V8 appends to the
9290 /// positional JSON parse errors. Positions are in UTF-16-ish code units;
9291 /// node-js counts `char`s, which agree for the BMP.
9292 fn at(&self, pos: usize) -> String {
9293 let mut line = 1usize;
9294 let mut col = 1usize;
9295 for c in &self.chars[..pos.min(self.chars.len())] {
9296 if *c == '\n' {
9297 line += 1;
9298 col = 1;
9299 } else {
9300 col += 1;
9301 }
9302 }
9303 format!(" at position {pos} (line {line} column {col})")
9304 }
9305
9306 /// A positional error (`Expected ':' after property name in JSON at …`).
9307 fn err_at(&self, what: &str, pos: usize) -> String {
9308 format!("SyntaxError: {what} in JSON{}", self.at(pos))
9309 }
9310
9311 /// The one positional message V8 does NOT suffix with `in JSON`.
9312 fn err_trailing(&self, pos: usize) -> String {
9313 format!(
9314 "SyntaxError: Unexpected non-whitespace character after JSON{}",
9315 self.at(pos)
9316 )
9317 }
9318
9319 /// V8's default parse error: the offending character plus a window of the
9320 /// source. The whole input is quoted when it is short (<= 20 chars);
9321 /// otherwise a 10-character context window either side of `pos` is shown,
9322 /// elided with `...` on whichever side was cut.
9323 fn err_token(&self, pos: usize) -> String {
9324 const MAX_WHOLE: usize = 20;
9325 const CONTEXT: usize = 10;
9326 let len = self.chars.len();
9327 let Some(c) = self.chars.get(pos) else {
9328 return "SyntaxError: Unexpected end of JSON input".into();
9329 };
9330 // V8 reports the whole input for the JS literals that are famously not
9331 // JSON, without naming an offending character.
9332 let whole: String = self.chars.iter().collect();
9333 if matches!(
9334 whole.as_str(),
9335 "undefined" | "NaN" | "Infinity" | "-Infinity"
9336 ) {
9337 return format!("SyntaxError: \"{whole}\" is not valid JSON");
9338 }
9339 let snippet = if len <= MAX_WHOLE {
9340 format!("\"{whole}\"")
9341 } else {
9342 let start = pos.saturating_sub(CONTEXT);
9343 let end = (pos + CONTEXT).min(len);
9344 let body: String = self.chars[start..end].iter().collect();
9345 let head = if start > 0 { "..." } else { "" };
9346 let tail = if end < len { "..." } else { "" };
9347 format!("{head}\"{body}\"{tail}")
9348 };
9349 format!("SyntaxError: Unexpected token '{c}', {snippet} is not valid JSON")
9350 }
9351
9352 fn skip_ws(&mut self) {
9353 while matches!(
9354 self.peek(),
9355 Some(' ') | Some('\n') | Some('\t') | Some('\r')
9356 ) {
9357 self.pos += 1;
9358 }
9359 }
9360 fn parse_value(&mut self) -> Result<Value, String> {
9361 self.skip_ws();
9362 let start = self.pos;
9363 let prim = matches!(self.peek(), Some(c) if c != '{' && c != '[');
9364 let v = match self.peek() {
9365 Some('{') => self.parse_object(),
9366 Some('[') => self.parse_array(),
9367 Some('"') => {
9368 let s = self.parse_string()?;
9369 Ok(with_host(|h| h.new_str(s)))
9370 }
9371 Some('t') | Some('f') => self.parse_bool(),
9372 Some('n') => {
9373 self.expect_lit("null")?;
9374 Ok(with_host(|h| h.null()))
9375 }
9376 Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
9377 None => Err("SyntaxError: Unexpected end of JSON input".into()),
9378 _ => Err(self.err_token(self.pos)),
9379 }?;
9380 if prim && self.record {
9381 self.prims
9382 .push(self.chars[start..self.pos].iter().collect());
9383 }
9384 Ok(v)
9385 }
9386 fn expect_lit(&mut self, lit: &str) -> Result<(), String> {
9387 for ch in lit.chars() {
9388 match self.peek() {
9389 Some(c) if c == ch => self.pos += 1,
9390 // V8 reports the first character that broke the literal, which is
9391 // why `foo` complains about `'o'` (index 2) and not `'f'`.
9392 None => return Err("SyntaxError: Unexpected end of JSON input".into()),
9393 _ => return Err(self.err_token(self.pos)),
9394 }
9395 }
9396 Ok(())
9397 }
9398 fn parse_bool(&mut self) -> Result<Value, String> {
9399 if self.peek() == Some('t') {
9400 self.expect_lit("true")?;
9401 Ok(Value::Bool(true))
9402 } else {
9403 self.expect_lit("false")?;
9404 Ok(Value::Bool(false))
9405 }
9406 }
9407 /// JSON's number grammar: `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?`.
9408 /// A leading zero does NOT swallow the following digits — `01` parses as `0`
9409 /// and the stray `1` becomes a trailing-token error, which is how V8 reports
9410 /// it. Each way the grammar can run out has its own message.
9411 fn parse_number(&mut self) -> Result<Value, String> {
9412 let start = self.pos;
9413 if self.peek() == Some('-') {
9414 self.pos += 1;
9415 if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9416 return Err(self.err_at("No number after minus sign", self.pos));
9417 }
9418 }
9419 if self.peek() == Some('0') {
9420 self.pos += 1;
9421 } else {
9422 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9423 self.pos += 1;
9424 }
9425 }
9426 if self.peek() == Some('.') {
9427 self.pos += 1;
9428 if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9429 return Err(self.err_at("Unterminated fractional number", self.pos));
9430 }
9431 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9432 self.pos += 1;
9433 }
9434 }
9435 if matches!(self.peek(), Some('e') | Some('E')) {
9436 self.pos += 1;
9437 if matches!(self.peek(), Some('+') | Some('-')) {
9438 self.pos += 1;
9439 }
9440 if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9441 return Err(self.err_at("Exponent part is missing a number", self.pos));
9442 }
9443 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9444 self.pos += 1;
9445 }
9446 }
9447 let s: String = self.chars[start..self.pos].iter().collect();
9448 s.parse::<f64>()
9449 .map(Value::Float)
9450 .map_err(|_| self.err_at("Unexpected number", start))
9451 }
9452 fn parse_string(&mut self) -> Result<String, String> {
9453 self.pos += 1; // opening quote
9454 let mut out = String::new();
9455 loop {
9456 match self.peek() {
9457 None => return Err(self.err_at("Unterminated string", self.pos)),
9458 Some('"') => {
9459 self.pos += 1;
9460 break;
9461 }
9462 Some('\\') => {
9463 self.pos += 1;
9464 match self.peek() {
9465 Some('n') => out.push('\n'),
9466 Some('t') => out.push('\t'),
9467 Some('r') => out.push('\r'),
9468 Some('"') => out.push('"'),
9469 Some('\\') => out.push('\\'),
9470 Some('/') => out.push('/'),
9471 Some('b') => out.push('\u{08}'),
9472 Some('f') => out.push('\u{0C}'),
9473 Some('u') => {
9474 let h: String = self.chars
9475 [self.pos + 1..(self.pos + 5).min(self.chars.len())]
9476 .iter()
9477 .collect();
9478 if let Ok(n) = u32::from_str_radix(&h, 16) {
9479 if let Some(ch) = char::from_u32(n) {
9480 out.push(ch);
9481 }
9482 }
9483 self.pos += 4;
9484 }
9485 _ => {}
9486 }
9487 self.pos += 1;
9488 }
9489 // A raw control character is not legal inside a JSON string; it
9490 // has to be escaped. V8 rejects it rather than passing it through.
9491 Some(c) if (c as u32) < 0x20 => {
9492 return Err(self.err_at("Bad control character in string literal", self.pos))
9493 }
9494 Some(c) => {
9495 out.push(c);
9496 self.pos += 1;
9497 }
9498 }
9499 }
9500 Ok(out)
9501 }
9502 fn parse_array(&mut self) -> Result<Value, String> {
9503 self.pos += 1; // [
9504 let mut items = Vec::new();
9505 self.skip_ws();
9506 if self.peek() == Some(']') {
9507 self.pos += 1;
9508 return Ok(with_host(|h| h.new_array(items)));
9509 }
9510 loop {
9511 items.push(self.parse_value()?);
9512 self.skip_ws();
9513 match self.peek() {
9514 Some(',') => {
9515 self.pos += 1;
9516 }
9517 Some(']') => {
9518 self.pos += 1;
9519 break;
9520 }
9521 _ => return Err(self.err_at("Expected ',' or ']' after array element", self.pos)),
9522 }
9523 }
9524 Ok(with_host(|h| h.new_array(items)))
9525 }
9526 fn parse_object(&mut self) -> Result<Value, String> {
9527 self.pos += 1; // {
9528 let mut props: IndexMap<String, Value> = IndexMap::new();
9529 self.skip_ws();
9530 if self.peek() == Some('}') {
9531 self.pos += 1;
9532 return Ok(with_host(|h| h.new_object(props)));
9533 }
9534 loop {
9535 self.skip_ws();
9536 if self.peek() != Some('"') {
9537 // The first key uses the "or '}'" wording (an empty object is
9538 // still legal there); a key after a comma does not. End of input
9539 // reports the same expectation, at the end position.
9540 return Err(if props.is_empty() {
9541 self.err_at("Expected property name or '}'", self.pos)
9542 } else {
9543 self.err_at("Expected double-quoted property name", self.pos)
9544 });
9545 }
9546 let key = self.parse_string()?;
9547 self.skip_ws();
9548 if self.peek() != Some(':') {
9549 return Err(match self.peek() {
9550 None => "SyntaxError: Unexpected end of JSON input".into(),
9551 _ => self.err_at("Expected ':' after property name", self.pos),
9552 });
9553 }
9554 self.pos += 1;
9555 let val = self.parse_value()?;
9556 props.insert(key, val);
9557 self.skip_ws();
9558 match self.peek() {
9559 Some(',') => {
9560 self.pos += 1;
9561 }
9562 Some('}') => {
9563 self.pos += 1;
9564 break;
9565 }
9566 _ => return Err(self.err_at("Expected ',' or '}' after property value", self.pos)),
9567 }
9568 }
9569 Ok(with_host(|h| h.new_object(props)))
9570 }
9571}
9572
9573// ══ type methods (array / string / number) ═══════════════════════════════════
9574
9575fn is_array_method(name: &str) -> bool {
9576 matches!(
9577 name,
9578 "push"
9579 | "pop"
9580 | "shift"
9581 | "unshift"
9582 | "map"
9583 | "filter"
9584 | "forEach"
9585 | "join"
9586 | "slice"
9587 | "indexOf"
9588 | "lastIndexOf"
9589 | "includes"
9590 | "reduce"
9591 | "concat"
9592 | "reverse"
9593 | "sort"
9594 | "find"
9595 | "findIndex"
9596 | "some"
9597 | "every"
9598 | "flat"
9599 | "fill"
9600 | "splice"
9601 | "keys"
9602 | "values"
9603 | "entries"
9604 | "flatMap"
9605 | "at"
9606 | "toString"
9607 | "reduceRight"
9608 | "findLast"
9609 | "findLastIndex"
9610 | "copyWithin"
9611 )
9612}
9613/// Every `String.prototype` method node-js implements.
9614///
9615/// A LIST rather than a `matches!` arm because the same set has to be installed
9616/// on the real `String.prototype` object: a method read off the prototype
9617/// (`String.prototype.trim.call(s)`, the generic-borrowing idiom libraries use)
9618/// found nothing there, so the two views of "which methods exist" would drift
9619/// if they were written twice.
9620pub(crate) const STRING_PROTO_METHODS: &[&str] = &[
9621 "toUpperCase",
9622 "toLowerCase",
9623 "charAt",
9624 "charCodeAt",
9625 "codePointAt",
9626 "indexOf",
9627 "lastIndexOf",
9628 "includes",
9629 "slice",
9630 "substring",
9631 "substr",
9632 "split",
9633 "trim",
9634 "trimStart",
9635 "trimEnd",
9636 "replace",
9637 "replaceAll",
9638 "repeat",
9639 "startsWith",
9640 "endsWith",
9641 "padStart",
9642 "padEnd",
9643 "concat",
9644 "at",
9645 "toString",
9646 "toLocaleString",
9647 "valueOf",
9648 "match",
9649 "matchAll",
9650 "search",
9651 "normalize",
9652 "localeCompare",
9653 "toLocaleUpperCase",
9654 "toLocaleLowerCase",
9655 "isWellFormed",
9656 "toWellFormed",
9657];
9658
9659fn is_string_method(name: &str) -> bool {
9660 STRING_PROTO_METHODS.contains(&name)
9661}
9662
9663/// Every SYMBOL-keyed intrinsic method the generated table lists for `ctor`,
9664/// spelled the way this frontend spells the key (`@@iterator`).
9665///
9666/// A prototype built as a REAL object (`String.prototype`, `URLSearchParams
9667/// .prototype`) installs its methods from a list, and only the string-keyed
9668/// list was walked — so `String.prototype[Symbol.iterator]` read `undefined`
9669/// while `Array.prototype[Symbol.iterator]`, which resolves through the
9670/// `Builtin` namespace and its table gate, answered a function. Derived from
9671/// the table rather than written out, so it cannot name a method node does not
9672/// define nor miss one it does.
9673pub(crate) fn proto_symbol_methods(ctor: &str) -> Vec<&'static str> {
9674 let prefix = format!("@proto:{ctor}:");
9675 crate::arity::BUILTIN_ARITY
9676 .iter()
9677 .filter_map(|(k, _, _)| k.strip_prefix(prefix.as_str()))
9678 .filter(|m| m.starts_with("@@"))
9679 .collect()
9680}
9681
9682/// The builtin constructors whose `.prototype` object is BRANDED — every other
9683/// `<C>.prototype` is an ordinary object and reports `[object Object]`.
9684///
9685/// Measured on node v26.8.1 over every constructor this frontend knows:
9686///
9687/// ```text
9688/// Array/Object/Number/String/Boolean/Function the ES5 legacy slot prototypes
9689/// Symbol/BigInt/Map/Set/WeakMap/WeakSet carry an own @@toStringTag
9690/// Promise/Iterator/ArrayBuffer/DataView "
9691/// WeakRef/FinalizationRegistry/URL "
9692/// URLSearchParams/TextEncoder/TextDecoder "
9693/// Date/RegExp/Error/TypeError/Uint8Array/… [object Object]
9694/// ```
9695///
9696/// The rule this replaces branded EVERY `<C>.prototype` as `C`, so
9697/// `Object.prototype.toString.call(Date.prototype)` read `[object Date]` — and
9698/// a `Date.prototype.toString` call on a plain object named `[object Date]` in
9699/// its own failure message where node names `[object Object]`.
9700pub(crate) const BRANDED_PROTOS: &[&str] = &[
9701 "Array",
9702 "ArrayBuffer",
9703 "BigInt",
9704 "Boolean",
9705 "DataView",
9706 "FinalizationRegistry",
9707 "Function",
9708 "Iterator",
9709 "Map",
9710 "Number",
9711 "Object",
9712 "Promise",
9713 "Set",
9714 "SharedArrayBuffer",
9715 "String",
9716 "Symbol",
9717 "TextDecoder",
9718 "TextEncoder",
9719 "URL",
9720 "URLSearchParams",
9721 "WeakMap",
9722 "WeakRef",
9723 "WeakSet",
9724];
9725
9726/// Whether `v` is a `RegExp` value (drives the regex path of `match`/`replace`/…).
9727/// A user `Symbol.match`/`replace`/`search`/`split`/`matchAll` method on the
9728/// ARGUMENT, which the string method must delegate to (22.1.3.x step 2).
9729///
9730/// `"abc".match(o)` where `o` defines `Symbol.match` calls that method rather
9731/// than coercing `o` to a pattern — the protocol every regexp-like library
9732/// implements. None of the five were consulted, so a custom matcher was
9733/// silently stringified instead.
9734fn symbol_protocol(arg: &Value, sym: &str) -> Option<Value> {
9735 if matches!(arg, Value::Undef) || with_host(|h| h.is_null(arg)) {
9736 return None;
9737 }
9738 let f = get_property(arg, sym).ok()?;
9739 with_host(|h| host::is_callable(h, &f)).then_some(f)
9740}
9741
9742fn is_regexp_arg(v: &Value) -> bool {
9743 // 7.2.8 `IsRegExp` asks `Symbol.match` FIRST, so an object can declare
9744 // itself a regexp — or a real one can disown the label. Only the heap kind
9745 // was checked, so `"a".startsWith({[Symbol.match]: true})` did not throw
9746 // the TypeError the spec requires.
9747 if let Ok(m) = get_property(v, "@@match") {
9748 if !matches!(m, Value::Undef) {
9749 return with_host(|h| h.truthy(&m));
9750 }
9751 }
9752 with_host(|h| h.kind_of(v)) == Some(ObjKind::RegExp)
9753}
9754
9755/// `str.replace(strPattern, fn)` — a function replacer against a literal (string)
9756/// pattern: replace the first (or all) occurrence, calling `fn(match, offset, s)`.
9757fn replace_str_fn(s: &str, pat: &str, repl: &Value, all: bool) -> Result<String, String> {
9758 if pat.is_empty() {
9759 return Ok(s.to_string());
9760 }
9761 let mut out = String::new();
9762 let mut rest = s;
9763 let mut base = 0usize;
9764 while let Some(pos) = rest.find(pat) {
9765 out.push_str(&rest[..pos]);
9766 let offset = base + pos;
9767 let m = with_host(|h| h.new_str(pat.to_string()));
9768 let str_arg = with_host(|h| h.new_str(s.to_string()));
9769 let r = host::invoke(repl, vec![m, Value::Float(offset as f64), str_arg], None)?;
9770 out.push_str(&with_host(|h| h.str_of(&r)));
9771 let consumed = pos + pat.len();
9772 base += consumed;
9773 rest = &rest[consumed..];
9774 if !all {
9775 break;
9776 }
9777 }
9778 out.push_str(rest);
9779 Ok(out)
9780}
9781/// Every `Number.prototype` method node-js implements — a list for the same
9782/// reason [`STRING_PROTO_METHODS`] is one.
9783pub(crate) const NUMBER_PROTO_METHODS: &[&str] = &[
9784 "toFixed",
9785 "toExponential",
9786 "toString",
9787 "toPrecision",
9788 "toLocaleString",
9789 "valueOf",
9790];
9791
9792fn is_number_method(name: &str) -> bool {
9793 NUMBER_PROTO_METHODS.contains(&name)
9794}
9795
9796/// The exotic kinds whose own dispatch table does NOT already reach the
9797/// `Object.prototype` methods, so the inherited ones have to be routed to.
9798///
9799/// An allowlist rather than a catch-all: a primitive receiver also reaches this
9800/// function, and a Number's `toString` is `Number.prototype.toString` — routing
9801/// it to the object form made `(255).toString(16)` report `[object Number]`.
9802fn inherits_object_methods(recv: &Value) -> bool {
9803 matches!(
9804 with_host(|h| h.kind_of(recv)),
9805 Some(
9806 ObjKind::Map
9807 | ObjKind::Set
9808 | ObjKind::Promise
9809 | ObjKind::RegExp
9810 | ObjKind::Generator
9811 | ObjKind::Symbol
9812 | ObjKind::BigInt
9813 | ObjKind::Iter
9814 )
9815 )
9816}
9817
9818/// Whether `recv`'s own prototype defines `name`, shadowing the
9819/// `Object.prototype` method of that name — `RegExp.prototype.toString` does,
9820/// `Map.prototype` does not.
9821fn overrides_object_method(recv: &Value, name: &str) -> bool {
9822 match with_host(|h| h.kind_of(recv)) {
9823 Some(ObjKind::Map) => is_map_method(name),
9824 Some(ObjKind::Set) => is_set_method(name),
9825 Some(ObjKind::RegExp) => crate::regexp::is_regexp_method(name),
9826 // A Symbol has its own `toString`; `valueOf` is the inherited one,
9827 // which returns the receiver — exactly what a symbol needs.
9828 Some(ObjKind::Symbol) => matches!(name, "toString" | "valueOf" | "@@toPrimitive"),
9829 Some(ObjKind::BigInt) => matches!(name, "toString" | "valueOf" | "toLocaleString"),
9830 _ => false,
9831 }
9832}
9833
9834/// Dispatch `recv.name(args)` for the built-in prototype methods.
9835pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
9836 // A USER method on the receiver's prototype chain wins over the builtin of
9837 // the same name — that is how a `class X extends Array` method is reached,
9838 // since the dispatch below goes straight to the builtin table and has no
9839 // entry for it.
9840 //
9841 // Deliberately restricted to a user function: the shared `Object.prototype`
9842 // carries real `@proto:Object:*` thunks, so accepting any callable made a
9843 // bare `map.toString()` resolve to the object form instead of the builtin
9844 // one the exotic is supposed to use.
9845 if let Some(f) = with_host(|h| host::lookup_chain(h, recv, name)) {
9846 if matches!(
9847 with_host(|h| h.kind_of(&f)),
9848 Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc)
9849 ) {
9850 return host::invoke(&f, args, Some(recv.clone()));
9851 }
9852 }
9853 // A method synthesized from the receiver's KIND is unreachable once its
9854 // intrinsic prototype is off the chain. The read already answers
9855 // `undefined` for one; dispatch has its own table and would still have
9856 // called it, so `Object.setPrototypeOf(a, {}); a.join()` returned "1,2"
9857 // while `a.join` was `undefined` — the read and the call disagreeing again,
9858 // in the opposite direction from the monkey-patch case below.
9859 if !own_intrinsic_reachable(recv)
9860 && inherited_method_owner(recv, name).is_none()
9861 && !has_own_for_shadow(recv, name)
9862 && inherited_builtin_static(recv, name).is_none()
9863 && with_host(|h| host::lookup_chain(h, recv, name)).is_none()
9864 {
9865 return Err(host::type_error(&format!("{name} is not a function")));
9866 }
9867 // A method monkey-patched onto the receiver's intrinsic prototype. The READ
9868 // path resolves these, but dispatch goes straight to the builtin table and
9869 // never consults it, so `Array.prototype.last = f; [1].last()` threw "is not
9870 // a function" while `[1].last` WAS `f` — the read and the call disagreeing
9871 // about the same name, on the one path a polyfill actually uses.
9872 if !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
9873 if let Some(f) = inherited_builtin_static(recv, name) {
9874 if with_host(|h| host::is_callable(h, &f)) {
9875 return host::invoke(&f, args, Some(recv.clone()));
9876 }
9877 }
9878 }
9879 // Every object INHERITS the `Object.prototype` methods, and an exotic that
9880 // does not define its own reaches them the same way. Each kind's dispatch
9881 // table below only knows its own methods, so `new Map().toString()`,
9882 // `promise.hasOwnProperty(k)` and `sym.toLocaleString()` all reported "is
9883 // not a function" — `Object.prototype.toString.call(m)` worked while
9884 // `m.toString()` did not.
9885 // The allowlist is the kinds whose own dispatch table below would otherwise
9886 // claim the name. Every OTHER receiver reaches an `Object.prototype` method
9887 // the same way — a function, a class and a bound function included, where
9888 // `f.hasOwnProperty(k)` reported "is not a function" even though the READ
9889 // resolved it. `inherited_method_owner` decides which prototype owns the
9890 // name, so a kind that defines its own still gets its own.
9891 if is_object_builtin_method(name)
9892 && (inherited_method_owner(recv, name) == Some("Object")
9893 || (inherits_object_methods(recv) && !overrides_object_method(recv, name)))
9894 {
9895 // `toString` goes through the branded form (20.1.3.6), which reads
9896 // `Symbol.toStringTag` and falls back to the receiver's own brand —
9897 // `[object Map]`, not the generic stringification.
9898 if name == "toString" {
9899 return proto_method(recv, "Object:toString", args);
9900 }
9901 return object_builtin_method(recv, name, args);
9902 }
9903 // `Object.prototype.valueOf` is inherited by every exotic that does not
9904 // override it (an Array does not), and returns the receiver. Without this
9905 // the `ToPrimitive` probe on `[o] + ''` reached `array_method("valueOf")`
9906 // and threw `valueOf is not a function`.
9907 if name == "valueOf"
9908 && matches!(
9909 with_host(|h| h.kind_of(recv)),
9910 Some(
9911 ObjKind::Array
9912 | ObjKind::Map
9913 | ObjKind::Set
9914 | ObjKind::Generator
9915 | ObjKind::Promise
9916 | ObjKind::Iter
9917 | ObjKind::RegExp
9918 )
9919 )
9920 {
9921 return Ok(recv.clone());
9922 }
9923 // Only the tag is needed to pick the branch — cloning the receiver here made
9924 // every `arr.push(x)` copy the whole array, so a fill loop was O(n^2).
9925 match with_host(|h| h.kind_of(recv)) {
9926 Some(ObjKind::Array) => array_method(recv, name, args),
9927 Some(ObjKind::Str) => {
9928 // `string_method` consumes the text itself, so this clone is the
9929 // payload, not a tag probe.
9930 let s = peek(recv, |o| match o {
9931 JsObj::Str(s) => Some(s.clone()),
9932 _ => None,
9933 })
9934 .unwrap_or_default();
9935 string_method(&s, name, args)
9936 }
9937 Some(ObjKind::Map) => map_method(recv, name, args),
9938 Some(ObjKind::Set) => set_method(recv, name, args),
9939 Some(ObjKind::Generator) if crate::stdlib::iterator::is_helper(name) => {
9940 crate::stdlib::iterator::call(recv, name, &args)
9941 }
9942 Some(ObjKind::Generator) => generator_method(recv, name, args),
9943 Some(ObjKind::Promise) => promise_method(recv, name, args),
9944 Some(ObjKind::Iter) if crate::stdlib::iterator::is_helper(name) => {
9945 crate::stdlib::iterator::call(recv, name, &args)
9946 }
9947 Some(ObjKind::Iter) => iter_method(recv, name, args),
9948 Some(ObjKind::Symbol) => symbol_method(recv, name, args),
9949 Some(ObjKind::BigInt) => {
9950 let b = peek(recv, |o| match o {
9951 JsObj::BigInt(b) => Some(b.clone()),
9952 _ => None,
9953 })
9954 .unwrap_or_default();
9955 bigint_method(&b, name, args)
9956 }
9957 Some(ObjKind::RegExp) => crate::regexp::regexp_method(recv, name, args),
9958 Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc) => {
9959 match function_builtin_method(recv, name, &args)? {
9960 Some(v) => Ok(v),
9961 None => Err(host::type_error(&format!("{name} is not a function"))),
9962 }
9963 }
9964 Some(ObjKind::Object) => {
9965 if let Some(f) = peek(recv, |o| match o {
9966 JsObj::Object(p) => p.get(name).cloned(),
9967 _ => None,
9968 }) {
9969 host::invoke(&f, args, Some(recv.clone()))
9970 } else if name == "hasOwnProperty" {
9971 let k = with_host(|h| h.str_of(&arg0(&args)));
9972 let has = peek(recv, |o| match o {
9973 JsObj::Object(p) => Some(p.contains_key(&k)),
9974 _ => None,
9975 })
9976 .unwrap_or(false);
9977 Ok(Value::Bool(has))
9978 } else if name == "toString" {
9979 Ok(with_host(|h| h.new_str("[object Object]")))
9980 } else {
9981 Err(host::type_error(&format!("{} is not a function", name)))
9982 }
9983 }
9984 _ => {
9985 // Primitive number/bool/string coercions.
9986 if let Value::Float(_) | Value::Int(_) = recv {
9987 return number_method(with_host(|h| h.to_number(recv)), name, args);
9988 }
9989 if let Some(s) = with_host(|h| h.as_str(recv)) {
9990 return string_method(&s, name, args);
9991 }
9992 // `Boolean.prototype` (20.3.3): a boolean is not a heap object here,
9993 // so it reached no branch at all and `true.toString()` threw `is not
9994 // a function`. Its three methods are `toString`, `valueOf`, and the
9995 // inherited `Object.prototype.toLocaleString` — which
9996 // `[1,'a',true].toLocaleString()` invokes per element, so the hole
9997 // was reachable from the array form too.
9998 if let Value::Bool(b) = recv {
9999 return match name {
10000 "toString" | "toLocaleString" => {
10001 Ok(new_s(if *b { "true" } else { "false" }.to_string()))
10002 }
10003 "valueOf" => Ok(Value::Bool(*b)),
10004 _ => Err(host::type_error(&format!("{name} is not a function"))),
10005 };
10006 }
10007 Err(host::type_error(&format!("{} is not a function", name)))
10008 }
10009 }
10010}
10011
10012/// A copy of the whole backing store, for the methods that genuinely consume
10013/// every element (`map`, `filter`, `join`, …). Never call it just to read
10014/// `.len()` — use [`array_len`], or `push`/`unshift` become O(n) per call.
10015/// A LIVE iterator over a `Map` or `Set`.
10016///
10017/// Node's collection iterators see the collection as it is at each step: an
10018/// entry added during iteration IS visited, and one deleted before it is
10019/// reached is NOT. Ours materialized every entry up front, so both were wrong —
10020/// a loop that deletes as it goes still processed the entries it had removed.
10021///
10022/// The cursor is the last key yielded plus the index it was at. On each step
10023/// the key is located again in the CURRENT order: if it is still there the next
10024/// entry follows it, and if it was itself deleted the stored index now names
10025/// the entry that shifted into its place. That reproduces node for the cases
10026/// its own tests turn on — add-during, delete-ahead, delete-self,
10027/// delete-behind, delete-the-rest and clear — without giving `Map` the
10028/// tombstoned entry list node uses internally.
10029fn collection_iterator(coll: &Value, kind: &str) -> Value {
10030 with_host(|h| {
10031 let mut m = IndexMap::new();
10032 m.insert(
10033 "@@native".into(),
10034 h.new_str("CollectionIterator".to_string()),
10035 );
10036 m.insert("@@coll".into(), coll.clone());
10037 m.insert("@@kind".into(), h.new_str(kind.to_string()));
10038 m.insert("@@started".into(), Value::Bool(false));
10039 m.insert("@@lastIdx".into(), Value::Float(0.0));
10040 h.new_object(m)
10041 })
10042}
10043
10044/// One step of a live collection iterator.
10045pub(crate) fn collection_iterator_next(recv: &Value) -> Result<Value, String> {
10046 let slot = |k: &str| {
10047 with_host(|h| match h.get(recv) {
10048 Some(JsObj::Object(p)) => p.get(k).cloned(),
10049 _ => None,
10050 })
10051 };
10052 let coll = slot("@@coll").unwrap_or(Value::Undef);
10053 let kind = slot("@@kind")
10054 .map(|v| with_host(|h| h.str_of(&v)))
10055 .unwrap_or_default();
10056 let started = slot("@@started").is_some_and(|v| with_host(|h| h.truthy(&v)));
10057 let last_idx = slot("@@lastIdx")
10058 .map(|v| with_host(|h| h.to_number(&v)) as usize)
10059 .unwrap_or(0);
10060 let last_key = slot("@@lastKey");
10061
10062 let next_idx = if !started {
10063 0
10064 } else {
10065 match last_key
10066 .as_ref()
10067 .and_then(|k| with_host(|h| collection_index_of(h, &coll, k)))
10068 {
10069 // Still present: continue after it.
10070 Some(i) => i + 1,
10071 // Deleted since: whatever shifted into its slot is next.
10072 None => last_idx,
10073 }
10074 };
10075 let entry = with_host(|h| collection_entry_at(h, &coll, next_idx));
10076 let Some((k, v)) = entry else {
10077 return Ok(iter_result(Value::Undef, true));
10078 };
10079 with_host(|h| {
10080 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
10081 p.insert("@@started".into(), Value::Bool(true));
10082 p.insert("@@lastIdx".into(), Value::Float(next_idx as f64));
10083 p.insert("@@lastKey".into(), k.clone());
10084 }
10085 });
10086 let out = match kind.as_str() {
10087 "keys" => k,
10088 "values" => v,
10089 _ => with_host(|h| h.new_array(vec![k, v])),
10090 };
10091 Ok(iter_result(out, false))
10092}
10093
10094/// The (key, value) at `idx` in a Map, or (value, value) in a Set.
10095fn collection_entry_at(h: &host::JsHost, coll: &Value, idx: usize) -> Option<(Value, Value)> {
10096 match h.get(coll) {
10097 Some(JsObj::Map { entries, .. }) => entries.get_index(idx).map(|(_, kv)| kv.clone()),
10098 Some(JsObj::Set { entries, .. }) => {
10099 entries.get_index(idx).map(|(_, v)| (v.clone(), v.clone()))
10100 }
10101 _ => None,
10102 }
10103}
10104
10105/// Where `key` currently sits in the collection's order.
10106fn collection_index_of(h: &host::JsHost, coll: &Value, key: &Value) -> Option<usize> {
10107 let mk = host::map_key(h, key);
10108 match h.get(coll) {
10109 Some(JsObj::Map { entries, .. }) => entries.get_index_of(&mk),
10110 Some(JsObj::Set { entries, .. }) => entries.get_index_of(&mk),
10111 _ => None,
10112 }
10113}
10114
10115/// The `thisArg` an iteration method was given, if any.
10116///
10117/// `[1].forEach(fn, thisArg)` binds `thisArg` as the callback's `this`, and so
10118/// do `map`/`filter`/`some`/`every`/`find`/`findIndex`/`findLast`/
10119/// `findLastIndex`/`flatMap`, `Map`/`Set`/TypedArray `forEach`, and
10120/// `Array.from`'s map function. Every one of them was invoking the callback
10121/// with no receiver, so `this` inside it was undefined and the argument did
10122/// nothing.
10123fn this_arg(args: &[Value], idx: usize) -> Option<Value> {
10124 args.get(idx)
10125 .filter(|v| !matches!(v, Value::Undef))
10126 .cloned()
10127}
10128
10129/// The elements of an array, with any INDEX ACCESSOR resolved.
10130///
10131/// `Object.defineProperty(arr, 1, { get })` stores the getter in the accessor
10132/// table, and an array's elements live in a backing vector — so every method
10133/// reading that vector directly (`join`, `map`, `indexOf`, …) saw the stale
10134/// slot and never called the getter, while a plain `arr[1]` read did.
10135///
10136/// An array with no accessors pays one lookup returning an empty list, so the
10137/// ordinary case is unchanged. The getters are invoked OUTSIDE the host borrow,
10138/// since calling one re-enters.
10139/// Walk `recv` the way an `Array.prototype` iteration method does: the LENGTH
10140/// is captured once at entry (LengthOfArrayLike, step 3), but each element is
10141/// read LIVE at its index, and an index that no longer exists is skipped.
10142///
10143/// Snapshotting the whole array instead meant a callback that mutated it was
10144/// not observed: `[1,2,3].forEach(v => a.shift())` visited 1, 2, 3 where node
10145/// visits 1 and 3, and `filter` kept elements the callback had already removed.
10146///
10147/// `f` returns `Some(x)` to stop early with `x`.
10148fn array_walk<T>(
10149 recv: &Value,
10150 mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10151) -> Result<Option<T>, String> {
10152 let len = array_len(recv);
10153 for i in 0..len {
10154 // A HOLE — and an index a shrinking mutation has dropped — is skipped
10155 // without calling the callback.
10156 if index_absent(recv, i) || i >= array_len(recv) {
10157 continue;
10158 }
10159 let v = get_property(recv, &i.to_string())?;
10160 if let Some(out) = f(i, v)? {
10161 return Ok(Some(out));
10162 }
10163 }
10164 Ok(None)
10165}
10166
10167/// `array_walk`'s descending twin, for `reduceRight`/`findLast*`: the same
10168/// capture-length-once, read-each-element-live rule walked from the end. A
10169/// callback that SHRINKS the array is observed by every later step, so the
10170/// indices it drops are skipped rather than served from a stale copy.
10171fn array_walk_rev<T>(
10172 recv: &Value,
10173 from: usize,
10174 mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10175) -> Result<Option<T>, String> {
10176 for i in (0..from).rev() {
10177 if index_absent(recv, i) || i >= array_len(recv) {
10178 continue;
10179 }
10180 let v = get_property(recv, &i.to_string())?;
10181 if let Some(out) = f(i, v)? {
10182 return Ok(Some(out));
10183 }
10184 }
10185 Ok(None)
10186}
10187
10188/// The live read behind `indexOf`/`includes`/`join`: the element at `i`, or
10189/// `undefined` once a mutation has shrunk the array past it.
10190fn array_elem_live(recv: &Value, i: usize) -> Result<Value, String> {
10191 if i >= array_len(recv) {
10192 return Ok(Value::Undef);
10193 }
10194 get_property(recv, &i.to_string())
10195}
10196
10197fn array_items(recv: &Value) -> Vec<Value> {
10198 let mut items = with_host(|h| match h.get(recv) {
10199 Some(JsObj::Array(items)) => items.clone(),
10200 _ => Vec::new(),
10201 });
10202 resolve_index_accessors(recv, &mut items);
10203 items
10204}
10205
10206/// Replace each slot that has an own accessor with what its getter returns.
10207pub(crate) fn resolve_index_accessors_pub(recv: &Value, items: &mut [Value]) {
10208 resolve_index_accessors(recv, items);
10209}
10210
10211/// Returns whether any slot was replaced, which the JSON walk needs: it keeps
10212/// the ORIGINAL array when nothing changed, and the original still holds the
10213/// stale slots.
10214fn resolve_index_accessors(recv: &Value, items: &mut [Value]) -> bool {
10215 let mut indices: Vec<usize> = with_host(|h| h.own_accessor_keys(recv))
10216 .into_iter()
10217 .filter_map(|k| k.parse::<usize>().ok())
10218 .filter(|i| *i < items.len())
10219 .collect();
10220 // An ELIDED index the prototype chain supplies is stale in the backing
10221 // vector too — it holds `undefined` where `[[Get]]` answers the inherited
10222 // value. Spread and `JSON.stringify` both read through here, and both
10223 // rendered the hole rather than what `a[i]` reads.
10224 let inherited: Vec<usize> = with_host(|h| h.hole_indices(recv))
10225 .into_iter()
10226 .filter(|i| *i < items.len() && !indices.contains(i))
10227 .filter(|i| has_property(recv, &i.to_string()).unwrap_or(false))
10228 .collect();
10229 indices.extend(inherited);
10230 let mut replaced = false;
10231 for i in indices {
10232 if let Ok(v) = get_property(recv, &i.to_string()) {
10233 items[i] = v;
10234 replaced = true;
10235 }
10236 }
10237 replaced
10238}
10239
10240/// The ELIDED positions of array `recv` as a membership set. A dense array —
10241/// which is nearly every array — answers with an empty set after a single
10242/// negative hash probe and allocates nothing.
10243///
10244/// The iteration methods split into two groups, and the split is not a matter of
10245/// taste: the ones spec'd through `HasProperty` (`forEach`, `map`, `filter`,
10246/// `some`, `every`, `reduce`, `indexOf`, `flat`, `sort`) SKIP a hole, while the
10247/// ones spec'd through a bare `Get` (`for…of`, spread, `find`, `includes`,
10248/// `join`, `entries`, `Array.from`) see the `undefined` a hole reads back as.
10249fn hole_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10250 with_host(|h| h.hole_indices(recv)).into_iter().collect()
10251}
10252
10253/// The indices `recv` genuinely has NO property at — the elided ones the
10254/// prototype chain does not supply either.
10255///
10256/// Every array method tests `HasProperty` before deciding to skip a position
10257/// (23.1.3.x, uniformly), and `HasProperty` walks the chain. Testing elision
10258/// alone made an inherited element invisible to all of them: with
10259/// `Array.prototype[1] = 'p'`, `[1,,3].map(v => v)` produced a hole where node
10260/// produces `'p'`, and `flat`/`concat`/`slice`/`sort`/`indexOf` each dropped
10261/// the same position.
10262///
10263/// `hole_set` remains the elision record itself, which is what `splice` moves
10264/// around — that bookkeeping is about the array's OWN storage and must not
10265/// consult the chain.
10266fn absent_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10267 hole_set(recv)
10268 .into_iter()
10269 .filter(|i| !has_property(recv, &i.to_string()).unwrap_or(false))
10270 .collect()
10271}
10272
10273/// The single-index form of [`absent_set`], for the walkers that test one
10274/// position at a time.
10275fn index_absent(recv: &Value, i: usize) -> bool {
10276 with_host(|h| h.is_hole(recv, i)) && !has_property(recv, &i.to_string()).unwrap_or(false)
10277}
10278
10279/// The element count, without copying the elements.
10280fn array_len(recv: &Value) -> usize {
10281 peek(recv, |o| match o {
10282 JsObj::Array(items) => Some(items.len()),
10283 _ => None,
10284 })
10285 .unwrap_or(0)
10286}
10287
10288/// `ArraySpeciesCreate(originalArray, length)` (23.1.3.4) — the constructor an
10289/// array method builds its RESULT with.
10290///
10291/// `map`, `filter`, `slice`, `concat`, `splice`, `flat` and `flatMap` all
10292/// produce an array of the receiver's own species, so on a `class A extends
10293/// Array` the result is an `A`. Every one of them allocated a plain array
10294/// instead, so `A.from([1]).map(x => x) instanceof A` was false.
10295///
10296/// The default `get [Symbol.species]() { return this }` is what makes the
10297/// subclass the species; a class overriding it with `Array` gets a plain array
10298/// back, which is the documented way to opt out.
10299/// Build an array-shaped result through `ctor`, or a plain array when there is
10300/// none to build through.
10301///
10302/// The constructor is called with the LENGTH and the elements written after, as
10303/// 23.1.2.1 and 23.1.3.4 both specify — which is what lets a subclass
10304/// constructor observe the allocation.
10305fn construct_array_like(ctor: Option<Value>, items: Vec<Value>) -> Result<Value, String> {
10306 let Some(ctor) = ctor.filter(|c| {
10307 matches!(
10308 with_host(|h| h.kind_of(c)),
10309 Some(ObjKind::Class) | Some(ObjKind::Func)
10310 )
10311 }) else {
10312 return Ok(with_host(|h| h.new_array(items)));
10313 };
10314 let out = host::construct(&ctor, vec![Value::Float(items.len() as f64)])?;
10315 write_elements(&out, items);
10316 Ok(out)
10317}
10318
10319/// Write `items` into a freshly constructed array-shaped `out`, clearing the
10320/// hole marks the length-only construction left behind.
10321///
10322/// `new A(3)` on `class A extends Array` really does produce three HOLES, and
10323/// the elements written over them stayed marked — so every subclass result of
10324/// `map`/`filter`/`flat` read back as holes: `A.from([1,2,3]).map(x => x * 2)`
10325/// had length 3 and printed `[null,null,null]`, and `0 in` it was false.
10326fn write_elements(out: &Value, items: Vec<Value>) {
10327 with_host(|h| {
10328 h.clear_holes(out);
10329 if let Some(JsObj::Array(dst)) = h.get_mut(out) {
10330 *dst = items;
10331 }
10332 });
10333}
10334
10335fn array_species_create(recv: &Value, items: Vec<Value>) -> Result<Value, String> {
10336 let plain = || with_host(|h| h.new_array(items.clone()));
10337 // Only a subclass instance can have a species of its own: a plain array's
10338 // `constructor` is the `Array` builtin, whose species is `Array`.
10339 // A chain lookup, not `get_property`: an Array receiver resolves its
10340 // properties through the stdlib funnel, which has no `constructor` entry,
10341 // so the read alone reports `undefined` for every subclass instance. A
10342 // Proxy is the exception — it has no property map to walk, and its
10343 // `constructor` comes from the `get` trap, so a proxied subclass array
10344 // produced plain arrays.
10345 let ctor = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
10346 get_property(recv, "constructor").unwrap_or(Value::Undef)
10347 } else {
10348 with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef)
10349 };
10350 if !matches!(
10351 with_host(|h| h.kind_of(&ctor)),
10352 Some(ObjKind::Class) | Some(ObjKind::Func)
10353 ) {
10354 return Ok(plain());
10355 }
10356 // An explicit `@@species` wins; absent one, the constructor itself is the
10357 // species, as the inherited accessor returns `this`.
10358 let species = match get_property(&ctor, "@@species") {
10359 Ok(Value::Undef) => ctor,
10360 Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(plain()),
10361 Ok(s) => s,
10362 Err(_) => ctor,
10363 };
10364 if !matches!(
10365 with_host(|h| h.kind_of(&species)),
10366 Some(ObjKind::Class) | Some(ObjKind::Func)
10367 ) {
10368 return Ok(plain());
10369 }
10370 let out = host::construct(&species, vec![Value::Float(items.len() as f64)])?;
10371 // The constructor is called with the LENGTH, so the elements are written
10372 // afterwards — which is also what lets a subclass constructor observe the
10373 // allocation, as node's does.
10374 write_elements(&out, items);
10375 Ok(out)
10376}
10377
10378fn array_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
10379 array_method_on(recv, recv, name, args)
10380}
10381
10382/// The `Array.prototype` methods that WRITE to their receiver, and so need the
10383/// generic path to copy the result back onto the array-like.
10384const ARRAY_MUTATORS: &[&str] = &[
10385 "push",
10386 "pop",
10387 "shift",
10388 "unshift",
10389 "splice",
10390 "sort",
10391 "reverse",
10392 "fill",
10393 "copyWithin",
10394];
10395
10396/// Run `Array.prototype.<method>` against an array-LIKE (`{0: 'a', length: 1}`,
10397/// a DOM-ish collection, `arguments`).
10398///
10399/// 23.1.3 defines every one of these over `LengthOfArrayLike(O)` and `Get(O, k)`
10400/// rather than over an Array's element vector, so the receiver only has to have
10401/// a `length`. The elements are read out into a temporary Array, the ordinary
10402/// implementation runs on that, and a MUTATING method writes the result back —
10403/// which keeps one implementation of each method rather than a second, generic
10404/// one that could drift from it.
10405///
10406/// An index the receiver does not own is a HOLE in the temporary, so the
10407/// methods that skip holes skip it here too, exactly as `HasProperty` makes them.
10408fn array_generic(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
10409 let len = match get_property(recv, "length") {
10410 Ok(v) => host::to_array_length(&v).unwrap_or(0),
10411 Err(_) => 0,
10412 };
10413 // A STRING receiver owns every index of its length; `has_property` answers
10414 // for objects and reports none of them, which made `[].map.call('abc', f)`
10415 // an array of three holes.
10416 let dense = with_host(|h| h.as_str(recv)).is_some();
10417 let mut items = Vec::with_capacity(len);
10418 let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
10419 for i in 0..len {
10420 let k = i.to_string();
10421 if dense || has_property(recv, &k)? {
10422 items.push(get_property(recv, &k)?);
10423 } else {
10424 holes.insert(i);
10425 items.push(Value::Undef);
10426 }
10427 }
10428 let tmp = with_host(|h| {
10429 let a = h.new_array(items);
10430 h.install_holes(&a, holes);
10431 a
10432 });
10433 let out = array_method_on(&tmp, recv, method, args)?;
10434 if ARRAY_MUTATORS.contains(&method) {
10435 let result = with_host(|h| match h.get(&tmp) {
10436 Some(JsObj::Array(items)) => items.clone(),
10437 _ => Vec::new(),
10438 });
10439 for (i, v) in result.iter().enumerate() {
10440 set_property(recv, &i.to_string(), v.clone())?;
10441 }
10442 set_property(recv, "length", Value::Float(result.len() as f64))?;
10443 }
10444 Ok(out)
10445}
10446
10447/// `Array.prototype.<name>` on `recv`.
10448///
10449/// `this_value` is what a callback receives as its third argument and what a
10450/// mutating method returns — the same object as `recv` for an ordinary array
10451/// call, but the ORIGINAL array-like when `array_generic` runs a method against
10452/// a temporary copy (`Array.prototype.slice.call(arguments)`).
10453fn array_method_on(
10454 recv: &Value,
10455 this_value: &Value,
10456 name: &str,
10457 args: Vec<Value>,
10458) -> Result<Value, String> {
10459 let args = coerce_numeric_args(ARRAY_METHOD_NUMERIC_ARGS, name, args)?;
10460 match name {
10461 "push" => {
10462 // 23.1.3.23 step 4 defines each new element through
10463 // `CreateDataPropertyOrThrow`, so a NON-EXTENSIBLE array refuses it:
10464 // `Object.seal(a)` / `preventExtensions(a)` then `a.push(x)` is a
10465 // TypeError. The elements were appended to the backing vector
10466 // regardless, so sealing an array did not seal it.
10467 if !args.is_empty() && !with_host(|h| h.is_extensible(recv)) {
10468 let at = array_len(recv);
10469 return Err(host::type_error(&format!(
10470 "Cannot add property {at}, object is not extensible"
10471 )));
10472 }
10473 // Step 5 then SETS `length`, so a non-writable one refuses the push
10474 // too — `defineProperty(a, 'length', {writable: false})` makes an
10475 // array append-proof without sealing it.
10476 if !args.is_empty() && !with_host(|h| h.prop_attrs(recv, "length").writable) {
10477 return Err(host::type_error(
10478 "Cannot assign to read only property 'length' of object '[object Array]'",
10479 ));
10480 }
10481 // `push` returns the new length; take it from the same mutable
10482 // borrow rather than copying the array back out to count it.
10483 let len = with_host(|h| {
10484 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10485 items.extend(args.iter().cloned());
10486 items.len()
10487 } else {
10488 0
10489 }
10490 });
10491 Ok(Value::Float(len as f64))
10492 }
10493 "pop" => Ok(with_host(|h| {
10494 let popped = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10495 items.pop().unwrap_or(Value::Undef)
10496 } else {
10497 Value::Undef
10498 };
10499 let len = match h.get(recv) {
10500 Some(JsObj::Array(items)) => items.len(),
10501 _ => 0,
10502 };
10503 h.truncate_holes(recv, len);
10504 popped
10505 })),
10506 "shift" => Ok(with_host(|h| {
10507 let shifted = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10508 if items.is_empty() {
10509 Value::Undef
10510 } else {
10511 items.remove(0)
10512 }
10513 } else {
10514 Value::Undef
10515 };
10516 h.remap_holes(recv, |i| i.checked_sub(1));
10517 shifted
10518 })),
10519 "unshift" => {
10520 with_host(|h| {
10521 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10522 for (i, a) in args.iter().enumerate() {
10523 items.insert(i, a.clone());
10524 }
10525 }
10526 let n = args.len();
10527 h.remap_holes(recv, |i| Some(i + n));
10528 });
10529 Ok(Value::Float(array_len(recv) as f64))
10530 }
10531 "join" => {
10532 let sep = if args.is_empty() || matches!(args[0], Value::Undef) {
10533 ",".to_string()
10534 } else {
10535 arg_to_string(&args, 0)?
10536 };
10537 join_array(recv, &sep)
10538 }
10539 // `Array.prototype.toLocaleString` (23.1.3.32): comma-join the elements'
10540 // OWN `toLocaleString` results, with `null`/`undefined` contributing the
10541 // empty string. It threw `is not a function` — the whole method was
10542 // missing — so `[1234.5, 'x'].toLocaleString()` was unreachable.
10543 "toLocaleString" => {
10544 // Shares `join`'s JoinStack: measured on node v26.7.0, `h=[1]`
10545 // `h.push(h)` makes `h.toLocaleString()` `"1,"`, not a stack overflow.
10546 if !host::join_stack_push(recv) {
10547 return Ok(with_host(|h| h.new_str(String::new())));
10548 }
10549 let items = array_items(recv);
10550 let mut parts: Vec<String> = Vec::with_capacity(items.len());
10551 for it in &items {
10552 if with_host(|h| h.is_nullish(it)) {
10553 parts.push(String::new());
10554 continue;
10555 }
10556 let v = match host::call_method(it, "toLocaleString", Vec::new()) {
10557 Ok(v) => v,
10558 Err(e) => {
10559 host::join_stack_pop();
10560 return Err(e);
10561 }
10562 };
10563 parts.push(with_host(|h| h.str_of(&v)));
10564 }
10565 host::join_stack_pop();
10566 Ok(with_host(|h| h.new_str(parts.join(","))))
10567 }
10568 // `indexOf`/`lastIndexOf` are spec'd through `HasProperty`, so a hole is
10569 // never a match: `[1,,3].indexOf(undefined)` is `-1`, while the
10570 // `Get`-based `includes` reports `true` for the same array.
10571 "indexOf" => {
10572 let target = arg0(&args);
10573 let len = array_len(recv);
10574 let start = search_start(arg_num(&args, 1), len);
10575 let mut idx = None;
10576 for i in start..len {
10577 // 23.1.3.17 steps 8a-8b: HasProperty first, so a hole — and an
10578 // index a mutation has since dropped — is skipped, not compared.
10579 if index_absent(recv, i) || i >= array_len(recv) {
10580 continue;
10581 }
10582 let x = get_property(recv, &i.to_string())?;
10583 if with_host(|h| h.strict_eq(&x, &target)) {
10584 idx = Some(i);
10585 break;
10586 }
10587 }
10588 Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10589 }
10590 "lastIndexOf" => {
10591 let items = array_items(recv);
10592 let holes = absent_set(recv);
10593 let target = arg0(&args);
10594 let from = (args.len() > 1).then(|| arg_num(&args, 1));
10595 let idx = match search_start_last(from, items.len()) {
10596 None => None,
10597 Some(start) => with_host(|h| {
10598 items[..=start]
10599 .iter()
10600 .enumerate()
10601 .rev()
10602 .find(|(i, x)| !holes.contains(i) && h.strict_eq(x, &target))
10603 .map(|(i, _)| i)
10604 }),
10605 };
10606 Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10607 }
10608 "includes" => {
10609 // Array.includes uses SameValueZero: unlike `===`, NaN matches NaN.
10610 // Unlike `indexOf` it has no HasProperty step (23.1.3.16 step 5b), so
10611 // a hole reads as `undefined` and `[,].includes(undefined)` is true.
10612 let target = arg0(&args);
10613 let tnan = matches!(target, Value::Float(f) if f.is_nan());
10614 let len = array_len(recv);
10615 let start = search_start(arg_num(&args, 1), len);
10616 let mut found = false;
10617 for i in start..len {
10618 let x = array_elem_live(recv, i)?;
10619 if (tnan && matches!(x, Value::Float(f) if f.is_nan()))
10620 || with_host(|h| h.strict_eq(&x, &target))
10621 {
10622 found = true;
10623 break;
10624 }
10625 }
10626 Ok(Value::Bool(found))
10627 }
10628 "slice" => {
10629 let items = array_items(recv);
10630 let (lo, hi) = slice_bounds(&args, items.len());
10631 let out = array_species_create(this_value, items[lo..hi].to_vec())?;
10632 with_host(|h| h.copy_holes(recv, &out, |i| (i >= lo && i < hi).then(|| i - lo)));
10633 Ok(out)
10634 }
10635 "concat" => {
10636 // `Symbol.isConcatSpreadable` (23.1.3.1) decides whether a value
10637 // is spread, overriding `IsArray` in BOTH directions: a plain
10638 // array-like opts IN, and an array opts OUT.
10639 let spreadable = |a: &Value| -> bool {
10640 let flag = get_property(a, "@@isConcatSpreadable").unwrap_or(Value::Undef);
10641 if matches!(flag, Value::Undef) {
10642 matches!(with_host(|h| h.get(a).cloned()), Some(JsObj::Array(_)))
10643 && !is_arguments(a)
10644 } else {
10645 with_host(|h| h.truthy(&flag))
10646 }
10647 };
10648 // Step 5 iterates `« O » ++ items`, so the receiver takes the same
10649 // test: a non-spreadable `this` (`concat.call("ab", 1)`) is ONE
10650 // element, its `ToObject` box, not the characters `array_generic`
10651 // read out of it. A hole in a spread receiver or argument stays a
10652 // hole in the result, at its shifted position.
10653 let (mut out, mut holes) = if spreadable(this_value) {
10654 (array_items(recv), absent_set(recv))
10655 } else {
10656 (vec![to_object(this_value)], Default::default())
10657 };
10658 let mut sources: Vec<(Value, usize)> = Vec::new();
10659 for a in &args {
10660 if !spreadable(a) {
10661 out.push(a.clone());
10662 continue;
10663 }
10664 match with_host(|h| h.get(a).cloned()) {
10665 // Read off the backing vector rather than through
10666 // `array_items`, so the resolve that does for the receiver
10667 // has to be done here too: 23.1.3.1 step 5.c.iv is a
10668 // `[[Get]]`, and an index with a getter — or an elided one
10669 // the chain supplies — is stale in that vector.
10670 Some(JsObj::Array(mut items)) => {
10671 resolve_index_accessors(a, &mut items);
10672 sources.push((a.clone(), out.len()));
10673 out.extend(items);
10674 }
10675 // An opted-in array-LIKE spreads by its `length` and index
10676 // properties rather than by a backing vector it has none of.
10677 _ => {
10678 let len = get_property(a, "length").unwrap_or(Value::Undef);
10679 let n = with_host(|h| h.to_number(&len));
10680 let n = if n.is_finite() {
10681 n.max(0.0) as usize
10682 } else {
10683 0
10684 };
10685 for i in 0..n {
10686 out.push(get_property(a, &i.to_string()).unwrap_or(Value::Undef));
10687 }
10688 }
10689 }
10690 }
10691
10692 for (src, base) in sources {
10693 holes.extend(
10694 with_host(|h| h.hole_indices(&src))
10695 .into_iter()
10696 .map(|i| i + base),
10697 );
10698 }
10699 let arr = array_species_create(this_value, out)?;
10700 with_host(|h| h.install_holes(&arr, holes));
10701 Ok(arr)
10702 }
10703 "reverse" => {
10704 let len = array_len(recv);
10705 with_host(|h| {
10706 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10707 items.reverse();
10708 }
10709 h.remap_holes(recv, |i| Some(len - 1 - i));
10710 });
10711 Ok(this_value.clone())
10712 }
10713 "fill" => {
10714 // fill(value[, start[, end]]) — negative indices count from the end.
10715 let val = arg0(&args);
10716 let len = array_len(recv) as i64;
10717 let norm =
10718 |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10719 let start = if args.len() >= 2 {
10720 norm(arg_num(&args, 1) as i64)
10721 } else {
10722 0
10723 };
10724 let end = if args.len() >= 3 {
10725 norm(arg_num(&args, 2) as i64)
10726 } else {
10727 len as usize
10728 };
10729 with_host(|h| {
10730 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10731 for it in items.iter_mut().take(end).skip(start) {
10732 *it = val.clone();
10733 }
10734 }
10735 // Every filled position now holds a real value.
10736 h.remap_holes(recv, |i| (i < start || i >= end).then_some(i));
10737 });
10738 Ok(this_value.clone())
10739 }
10740 "copyWithin" => {
10741 // copyWithin(target, start[, end]) — copy a slice within the array.
10742 let items = array_items(recv);
10743 let len = items.len() as i64;
10744 let norm =
10745 |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10746 let target = norm(arg_num(&args, 0) as i64);
10747 let start = if args.len() >= 2 {
10748 norm(arg_num(&args, 1) as i64)
10749 } else {
10750 0
10751 };
10752 let end = if args.len() >= 3 {
10753 norm(arg_num(&args, 2) as i64)
10754 } else {
10755 len as usize
10756 };
10757 let slice: Vec<Value> = items[start..end.max(start)].to_vec();
10758 let copied = slice.len();
10759 // A copied position takes its SOURCE's hole-ness (10.4.2 copyWithin
10760 // deletes the target when the source has no such property);
10761 // everything outside the written range keeps its own.
10762 let src_holes = absent_set(recv);
10763 with_host(|h| {
10764 if let Some(JsObj::Array(a)) = h.get_mut(recv) {
10765 for (k, v) in slice.into_iter().enumerate() {
10766 if target + k < a.len() {
10767 a[target + k] = v;
10768 }
10769 }
10770 }
10771 let len = len as usize;
10772 let mut holes: rustc_hash::FxHashSet<usize> = src_holes
10773 .iter()
10774 .copied()
10775 .filter(|i| *i < target || *i >= (target + copied).min(len))
10776 .collect();
10777 for k in 0..copied {
10778 if target + k < len && src_holes.contains(&(start + k)) {
10779 holes.insert(target + k);
10780 }
10781 }
10782 h.install_holes(recv, holes);
10783 });
10784 Ok(this_value.clone())
10785 }
10786 "at" => {
10787 let items = array_items(recv);
10788 let mut i = arg_num(&args, 0) as i64;
10789 if i < 0 {
10790 i += items.len() as i64;
10791 }
10792 Ok(if i >= 0 && (i as usize) < items.len() {
10793 items[i as usize].clone()
10794 } else {
10795 Value::Undef
10796 })
10797 }
10798 // 23.1.3.21: the callback runs only where `HasProperty` holds, and the
10799 // result array is created with the SAME holes — `[1,,3].map(f)` calls `f`
10800 // twice and yields `[2, <1 empty item>, 6]`.
10801 "map" => {
10802 let holes = absent_set(recv);
10803 let cb = arg0(&args);
10804 // The result keeps the source's LENGTH, so a skipped index still
10805 // occupies a slot; `array_walk` only tells us which ones ran.
10806 let mut out = vec![Value::Undef; array_len(recv)];
10807 array_walk(recv, |i, it| {
10808 let v = host::invoke(
10809 &cb,
10810 vec![it, Value::Float(i as f64), this_value.clone()],
10811 this_arg(&args, 1),
10812 )?;
10813 if i < out.len() {
10814 out[i] = v;
10815 }
10816 Ok(None::<()>)
10817 })?;
10818 let arr = array_species_create(this_value, out)?;
10819 with_host(|h| h.install_holes(&arr, holes));
10820 Ok(arr)
10821 }
10822 "flatMap" => {
10823 let cb = arg0(&args);
10824 let thisarg = this_arg(&args, 1);
10825 let mut out = Vec::new();
10826 array_walk(recv, |i, v| {
10827 let r = host::invoke(
10828 &cb,
10829 vec![v, Value::Float(i as f64), this_value.clone()],
10830 thisarg.clone(),
10831 )?;
10832 match with_host(|h| h.get(&r).cloned()) {
10833 Some(JsObj::Array(inner)) => out.extend(inner),
10834 _ => out.push(r),
10835 }
10836 Ok(None::<()>)
10837 })?;
10838 array_species_create(this_value, out)
10839 }
10840 "filter" => {
10841 let cb = arg0(&args);
10842 let mut out = Vec::new();
10843 array_walk(recv, |i, it| {
10844 let keep = host::invoke(
10845 &cb,
10846 vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10847 this_arg(&args, 1),
10848 )?;
10849 if with_host(|h| h.truthy(&keep)) {
10850 out.push(it);
10851 }
10852 Ok(None::<()>)
10853 })?;
10854 array_species_create(this_value, out)
10855 }
10856 "forEach" => {
10857 let cb = arg0(&args);
10858 array_walk(recv, |i, it| {
10859 host::invoke(
10860 &cb,
10861 vec![it, Value::Float(i as f64), this_value.clone()],
10862 this_arg(&args, 1),
10863 )?;
10864 Ok(None::<()>)
10865 })?;
10866 Ok(Value::Undef)
10867 }
10868 "find" => {
10869 let items = array_items(recv);
10870 let cb = arg0(&args);
10871 for (i, it) in items.iter().enumerate() {
10872 let m = host::invoke(
10873 &cb,
10874 vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10875 this_arg(&args, 1),
10876 )?;
10877 if with_host(|h| h.truthy(&m)) {
10878 return Ok(it.clone());
10879 }
10880 }
10881 Ok(Value::Undef)
10882 }
10883 "findIndex" => {
10884 let items = array_items(recv);
10885 let cb = arg0(&args);
10886 for (i, it) in items.iter().enumerate() {
10887 let m = host::invoke(
10888 &cb,
10889 vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10890 this_arg(&args, 1),
10891 )?;
10892 if with_host(|h| h.truthy(&m)) {
10893 return Ok(Value::Float(i as f64));
10894 }
10895 }
10896 Ok(Value::Float(-1.0))
10897 }
10898 "some" => {
10899 let cb = arg0(&args);
10900 let thisarg = this_arg(&args, 1);
10901 let hit = array_walk(recv, |i, v| {
10902 let m = host::invoke(
10903 &cb,
10904 vec![v, Value::Float(i as f64), this_value.clone()],
10905 thisarg.clone(),
10906 )?;
10907 Ok(with_host(|h| h.truthy(&m)).then_some(()))
10908 })?;
10909 Ok(Value::Bool(hit.is_some()))
10910 }
10911 "every" => {
10912 let cb = arg0(&args);
10913 let failed = array_walk(recv, |i, it| {
10914 let m = host::invoke(
10915 &cb,
10916 vec![it, Value::Float(i as f64), this_value.clone()],
10917 this_arg(&args, 1),
10918 )?;
10919 Ok((!with_host(|h| h.truthy(&m))).then_some(()))
10920 })?;
10921 Ok(Value::Bool(failed.is_none()))
10922 }
10923 "reduce" => {
10924 let items = array_items(recv);
10925 let holes = absent_set(recv);
10926 let cb = arg0(&args);
10927 let acc;
10928 let mut start = 0;
10929 if args.len() >= 2 {
10930 acc = args[1].clone();
10931 } else {
10932 // With no seed the accumulator is the first PRESENT element, so a
10933 // leading run of holes is skipped rather than seeding `undefined`.
10934 match (0..items.len()).find(|i| !holes.contains(i)) {
10935 Some(i) => {
10936 acc = items[i].clone();
10937 start = i + 1;
10938 }
10939 None => {
10940 return Err(host::type_error(
10941 "Reduce of empty array with no initial value",
10942 ))
10943 }
10944 }
10945 }
10946 // Each element is read LIVE at its index, so a callback that
10947 // shrinks the array is observed — the tail is skipped rather than
10948 // folded from a stale snapshot.
10949 let mut cur = acc;
10950 array_walk(recv, |i, it| {
10951 if i < start {
10952 return Ok(None::<()>);
10953 }
10954 cur = host::invoke(
10955 &cb,
10956 vec![
10957 std::mem::replace(&mut cur, Value::Undef),
10958 it,
10959 Value::Float(i as f64),
10960 this_value.clone(),
10961 ],
10962 this_arg(&args, 1),
10963 )?;
10964 Ok(None::<()>)
10965 })?;
10966 Ok(cur)
10967 }
10968 "reduceRight" => {
10969 let cb = arg0(&args);
10970 let n = array_len(recv);
10971 let mut acc;
10972 let mut from = n; // one past the next index to process (walking down)
10973 if args.len() >= 2 {
10974 acc = args[1].clone();
10975 } else {
10976 let holes = absent_set(recv);
10977 match (0..n).rev().find(|i| !holes.contains(i)) {
10978 Some(k) => {
10979 acc = get_property(recv, &k.to_string())?;
10980 from = k;
10981 }
10982 None => {
10983 return Err(host::type_error(
10984 "Reduce of empty array with no initial value",
10985 ))
10986 }
10987 }
10988 }
10989 // `acc` moves into the closure and back out on every step, so it
10990 // lives in an Option the closure can take from and refill.
10991 let mut slot = Some(acc);
10992 array_walk_rev(recv, from, |i, v| {
10993 let prev = slot.take().expect("accumulator is refilled each step");
10994 slot = Some(host::invoke(
10995 &cb,
10996 vec![prev, v, Value::Float(i as f64), this_value.clone()],
10997 None,
10998 )?);
10999 Ok(None::<()>)
11000 })?;
11001 acc = slot.expect("accumulator is refilled each step");
11002 Ok(acc)
11003 }
11004 "findLast" => {
11005 let items = array_items(recv);
11006 let cb = arg0(&args);
11007 for i in (0..items.len()).rev() {
11008 let m = host::invoke(
11009 &cb,
11010 vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
11011 this_arg(&args, 1),
11012 )?;
11013 if with_host(|h| h.truthy(&m)) {
11014 return Ok(items[i].clone());
11015 }
11016 }
11017 Ok(Value::Undef)
11018 }
11019 "findLastIndex" => {
11020 let items = array_items(recv);
11021 let cb = arg0(&args);
11022 for i in (0..items.len()).rev() {
11023 let m = host::invoke(
11024 &cb,
11025 vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
11026 this_arg(&args, 1),
11027 )?;
11028 if with_host(|h| h.truthy(&m)) {
11029 return Ok(Value::Float(i as f64));
11030 }
11031 }
11032 Ok(Value::Float(-1.0))
11033 }
11034 // 23.1.3.30: `SortIndexedProperties` collects only the PRESENT elements,
11035 // and the holes are re-created at the tail — `[3,,1].sort()` is
11036 // `[1, 3, <1 empty item>]` with own keys `['0','1']`.
11037 "sort" => {
11038 let all = array_items(recv);
11039 let holes = absent_set(recv);
11040 let mut items: Vec<Value> = all
11041 .iter()
11042 .enumerate()
11043 .filter(|(i, _)| !holes.contains(i))
11044 .map(|(_, v)| v.clone())
11045 .collect();
11046 sort_values(&mut items, args.first())?;
11047 let present = items.len();
11048 // 23.1.3.30 steps 4-5 write back only the indices BELOW the length
11049 // captured at step 1: `Set` for each sorted element, then `Delete`
11050 // for the holes that followed them. Replacing the whole backing
11051 // vector instead discarded anything the COMPARATOR appended —
11052 // `a.sort((x, y) => { a.push(0); return x - y })` came back at its
11053 // original length with every pushed element gone.
11054 with_host(|h| {
11055 let len = all.len();
11056 if let Some(JsObj::Array(a)) = h.get_mut(recv) {
11057 if a.len() < len {
11058 a.resize(len, Value::Undef);
11059 }
11060 for (i, v) in items.into_iter().enumerate() {
11061 a[i] = v;
11062 }
11063 for slot in a[present..len].iter_mut() {
11064 *slot = Value::Undef;
11065 }
11066 }
11067 h.install_holes(recv, (present..len).collect());
11068 });
11069 Ok(this_value.clone())
11070 }
11071 // ES2023 change-by-copy: sort a fresh copy, leaving the receiver untouched.
11072 "toSorted" => {
11073 let mut items = array_items(recv);
11074 sort_values(&mut items, args.first())?;
11075 Ok(with_host(|h| h.new_array(items)))
11076 }
11077 "toReversed" => {
11078 let mut items = array_items(recv);
11079 items.reverse();
11080 Ok(with_host(|h| h.new_array(items)))
11081 }
11082 "toSpliced" => {
11083 let mut items = array_items(recv);
11084 let len = items.len();
11085 let start = {
11086 let s = arg_num(&args, 0);
11087 if s < 0.0 {
11088 ((len as f64 + s).max(0.0)) as usize
11089 } else {
11090 (s as usize).min(len)
11091 }
11092 };
11093 let delete = if args.len() >= 2 {
11094 (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11095 } else if args.is_empty() {
11096 0
11097 } else {
11098 len - start
11099 };
11100 let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11101 items.splice(start..start + delete, inserts);
11102 Ok(with_host(|h| h.new_array(items)))
11103 }
11104 "with" => {
11105 let mut items = array_items(recv);
11106 let len = items.len() as i64;
11107 let rel = arg_num(&args, 0) as i64;
11108 let idx = if rel < 0 { len + rel } else { rel };
11109 if idx < 0 || idx >= len {
11110 return Err(host::range_error(&format!("Invalid index : {rel}")));
11111 }
11112 items[idx as usize] = args.get(1).cloned().unwrap_or(Value::Undef);
11113 Ok(with_host(|h| h.new_array(items)))
11114 }
11115 "flat" => {
11116 // depth defaults to 1; `Infinity` flattens fully. ToIntegerOrInfinity:
11117 // NaN → 0, otherwise truncate toward zero (negatives act as 0).
11118 let raw = if args.is_empty() {
11119 1.0
11120 } else {
11121 arg_num(&args, 0)
11122 };
11123 let depth = if raw.is_nan() {
11124 0.0
11125 } else if raw.is_infinite() {
11126 raw
11127 } else {
11128 raw.trunc()
11129 };
11130 let mut out = Vec::new();
11131 flatten_into(recv, depth, &mut out)?;
11132 array_species_create(this_value, out)
11133 }
11134 // Live over the array (23.1.5.1): each step reads it as it is then.
11135 "keys" => Ok(array_iterator(recv, host::ArrayIterKind::Keys)),
11136 "values" | "@@iterator" => Ok(array_iterator(recv, host::ArrayIterKind::Values)),
11137 "entries" => Ok(array_iterator(recv, host::ArrayIterKind::Entries)),
11138 "splice" => array_splice(recv, args),
11139 // `Array.prototype.toString` IS `join()` with the default separator
11140 // (23.1.3.36), so it converts each element with `ToString` too — and
11141 // shares its cycle cut, which is the whole reason it must not call
11142 // `join_parts` directly: `ToString` of a nested array lands back here.
11143 "toString" => join_array(recv, ","),
11144 // An Array inherits from `Object.prototype` too, so the methods it does
11145 // not override resolve there. `[].hasOwnProperty` already read back as a
11146 // function through the property path, but CALLING it landed here and
11147 // threw `is not a function`.
11148 _ if is_object_builtin_method(name) => object_builtin_method(recv, name, args),
11149 _ => Err(host::type_error(&format!("{name} is not a function"))),
11150 }
11151}
11152
11153/// `Array.prototype.join` (23.1.3.18) and, with the default separator,
11154/// `Array.prototype.toString` (23.1.3.36) — one body so both share the cycle
11155/// cut, which is not optional here: `ToString` of an element that is itself an
11156/// array re-enters through `toString`, so guarding only `join` left
11157/// `a=[]; a.push(a); a.join('-')` recursing until the native stack aborted the
11158/// process. On node v26.7.0 that expression is `""`.
11159fn join_array(recv: &Value, sep: &str) -> Result<Value, String> {
11160 if !host::join_stack_push(recv) {
11161 return Ok(with_host(|h| h.new_str(String::new())));
11162 }
11163 // 23.1.3.18 step 6: the length is captured once, then each element is read
11164 // and STRINGIFIED before the next is read. Both halves are observable —
11165 // a getter or a `toString` that shrinks the array is seen by every later
11166 // element, which a read-all-then-convert pass misses.
11167 let parts = (|| -> Result<Vec<String>, String> {
11168 let len = array_len(recv);
11169 let mut out = Vec::with_capacity(len);
11170 for i in 0..len {
11171 let v = array_elem_live(recv, i)?;
11172 out.push(join_parts(std::slice::from_ref(&v))?.remove(0));
11173 }
11174 Ok(out)
11175 })();
11176 host::join_stack_pop();
11177 let s = parts?.join(sep);
11178 Ok(with_host(|h| h.new_str(s)))
11179}
11180
11181/// `Array.prototype.join`'s per-element conversion (23.1.3.18 step 4): a
11182/// `null`/`undefined` element contributes the empty string, every other element
11183/// is `ToString(element)` — which for an object means invoking its `toString`,
11184/// so `[{ toString() { return 'x' } }].join()` is `"x"` and not
11185/// `"[object Object]"`.
11186///
11187/// The all-primitive array — the overwhelmingly common one — is rendered under
11188/// a single host borrow; only an array actually holding an object pays for the
11189/// re-entrant per-element conversion.
11190fn join_parts(items: &[Value]) -> Result<Vec<String>, String> {
11191 let fast = with_host(|h| {
11192 items
11193 .iter()
11194 .map(|x| match x {
11195 Value::Undef => Some(String::new()),
11196 _ if h.is_null(x) => Some(String::new()),
11197 // A SYMBOL element is primitive but has no `ToString`, so it must
11198 // fall through to the fallible path and throw there:
11199 // `[Symbol()].join()` is a TypeError on node v26.7.0.
11200 _ if matches!(h.get(x), Some(JsObj::Symbol { .. })) => None,
11201 _ if host::is_primitive(h, x) => Some(h.str_of(x)),
11202 _ => None,
11203 })
11204 .collect::<Vec<_>>()
11205 });
11206 if fast.iter().all(Option::is_some) {
11207 return Ok(fast.into_iter().flatten().collect());
11208 }
11209 let mut out = Vec::with_capacity(items.len());
11210 for (x, p) in items.iter().zip(fast) {
11211 match p {
11212 Some(s) => out.push(s),
11213 None => {
11214 let s = host::to_string_value(x)?;
11215 out.push(with_host(|h| h.str_of(&s)));
11216 }
11217 }
11218 }
11219 Ok(out)
11220}
11221
11222/// In-place sort of `items` (shared by `sort` and `toSorted`). Stable merge
11223/// sort — O(n log n) comparisons — with the fallible JS comparator called from
11224/// the merge step; default order is by the string form of each element.
11225/// Propagates a comparator error.
11226///
11227/// This was an insertion sort, which is O(n²): sorting 200k numbers with a
11228/// comparator did not finish inside 120s (node v26.7.0: 70ms), and each
11229/// doubling of the input quadrupled the time — 1k/2k/4k/8k/16k measured at
11230/// 0.21/0.81/3.39/12.94/51.36s. The comparator contract is unchanged; only the
11231/// number of times it is called is.
11232pub(crate) fn sort_values(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11233 // 23.1.3.30 step 1: a comparator that is neither `undefined` nor callable is
11234 // rejected BEFORE any comparison runs. `[2,1].sort(null)` was reaching the
11235 // invoke path and reporting the generic `null is not a function`.
11236 let cmp = match cmp {
11237 Some(Value::Undef) => None,
11238 Some(v) if !with_host(|h| host::is_callable(h, v)) => {
11239 // V8 renders the offending value with `NoSideEffectsToString`, not
11240 // with `util.inspect`: a string appears bare (`: x`) rather than
11241 // quoted, and an array is `[object Array]` rather than `[ 1, 2 ]`.
11242 let shown = no_side_effects_string(v);
11243 return Err(host::type_error(&format!(
11244 "The comparison function must be either a function or undefined: {shown}"
11245 )));
11246 }
11247 other => other,
11248 };
11249 // 23.1.3.30.1 SortIndexedProperties: `undefined` is never handed to the
11250 // comparator — it sorts to the end after the defined values are ordered.
11251 // `[3,undefined,1].sort((x,y)=>x-y)` is `[1,3,undefined]` with ONE call on
11252 // node v26.7.0; the insertion sort called the comparator twice, on
11253 // `undefined`, and left `[3,undefined,1]`. Every element passed over here
11254 // is `undefined`, so swapping keeps the defined values in input order.
11255 let mut defined = 0;
11256 for i in 0..items.len() {
11257 if !matches!(items[i], Value::Undef) {
11258 items.swap(defined, i);
11259 defined += 1;
11260 }
11261 }
11262 merge_sort(&mut items[..defined], cmp)
11263}
11264
11265/// One SortCompare: `> 0` means `b` sorts before `a`. A comparator result runs
11266/// through ToNumber, so a NaN (or a comparator returning `undefined`) is not
11267/// `> 0` and the pair keeps its input order.
11268fn sort_compare(a: &Value, b: &Value, cmp: Option<&Value>) -> Result<f64, String> {
11269 match cmp {
11270 Some(cb) => {
11271 let v = host::invoke(cb, vec![a.clone(), b.clone()], None)?;
11272 Ok(with_host(|h| h.to_number(&v)))
11273 }
11274 None => {
11275 // 23.1.3.30.2 SortCompare with no comparator: compare the ToString
11276 // of each element by CODE UNIT (`utf16::cmp_units`), which differs
11277 // from Rust's `String` order off the BMP.
11278 let x = with_host(|h| h.str_of(a));
11279 let y = with_host(|h| h.str_of(b));
11280 if crate::utf16::cmp_units(&x, &y) == std::cmp::Ordering::Greater {
11281 Ok(1.0)
11282 } else {
11283 Ok(-1.0)
11284 }
11285 }
11286 }
11287}
11288
11289/// Bottom-up stable merge sort. Bottom-up rather than recursive so a large
11290/// array cannot walk the native stack the JS comparator also runs on, and the
11291/// two buffers are swapped each pass instead of copied back.
11292fn merge_sort(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11293 let n = items.len();
11294 if n < 2 {
11295 return Ok(());
11296 }
11297 let mut src = items.to_vec();
11298 let mut dst = src.clone();
11299 let mut width = 1;
11300 while width < n {
11301 let mut lo = 0;
11302 while lo < n {
11303 let mid = (lo + width).min(n);
11304 let hi = (lo + 2 * width).min(n);
11305 merge(&src[lo..mid], &src[mid..hi], &mut dst[lo..hi], cmp)?;
11306 lo = hi;
11307 }
11308 std::mem::swap(&mut src, &mut dst);
11309 width *= 2;
11310 }
11311 items.clone_from_slice(&src);
11312 Ok(())
11313}
11314
11315/// Merge two sorted runs into `out`. Ties take from `left` first, which is what
11316/// makes the sort stable — `[{k:1},{k:0},{k:1},{k:0}].sort((x,y)=>x.k-y.k)`
11317/// keeps the two `k:0` entries in input order, as node does.
11318fn merge(
11319 left: &[Value],
11320 right: &[Value],
11321 out: &mut [Value],
11322 cmp: Option<&Value>,
11323) -> Result<(), String> {
11324 let (mut i, mut j, mut k) = (0, 0, 0);
11325 while i < left.len() && j < right.len() {
11326 if sort_compare(&left[i], &right[j], cmp)? > 0.0 {
11327 out[k] = right[j].clone();
11328 j += 1;
11329 } else {
11330 out[k] = left[i].clone();
11331 i += 1;
11332 }
11333 k += 1;
11334 }
11335 for v in left[i..].iter().chain(&right[j..]) {
11336 out[k] = v.clone();
11337 k += 1;
11338 }
11339 Ok(())
11340}
11341
11342/// Recursively flatten `items` up to `depth` levels into `out`. `depth` is an
11343/// f64 so `Infinity` (full flatten) and finite counts share one path.
11344///
11345/// `flat` has NO cycle cut — unlike `join`, V8 lets it run out of stack, and
11346/// `a=[1]; a.push(a); a.flat(Infinity)` is `RangeError: Maximum call stack size
11347/// exceeded` on node v26.7.0. That is reproduced by checking the same native
11348/// stack floor the VM does, so the answer is a catchable error rather than the
11349/// `fatal runtime error: stack overflow` abort this used to produce.
11350/// `FlattenIntoArray` (23.1.3.13.1). Takes the source ARRAY rather than its
11351/// elements because each level tests `HasProperty` before recursing, so a hole
11352/// contributes nothing at any depth: `[1,,3].flat()` is the dense `[1, 3]`.
11353fn flatten_into(src: &Value, depth: f64, out: &mut Vec<Value>) -> Result<(), String> {
11354 if host::stack_exhausted() {
11355 return Err(host::stack_overflow_error());
11356 }
11357 let items = array_items(src);
11358 let holes = absent_set(src);
11359 for (i, it) in items.into_iter().enumerate() {
11360 if holes.contains(&i) {
11361 continue;
11362 }
11363 let nested = depth > 0.0 && with_host(|h| h.kind_of(&it)) == Some(ObjKind::Array);
11364 if nested {
11365 flatten_into(&it, depth - 1.0, out)?;
11366 } else {
11367 out.push(it);
11368 }
11369 }
11370 Ok(())
11371}
11372
11373fn array_splice(recv: &Value, args: Vec<Value>) -> Result<Value, String> {
11374 let len = array_len(recv);
11375 let start = {
11376 let s = arg_num(&args, 0);
11377 if s < 0.0 {
11378 ((len as f64 + s).max(0.0)) as usize
11379 } else {
11380 (s as usize).min(len)
11381 }
11382 };
11383 let delete = if args.len() >= 2 {
11384 (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11385 } else {
11386 len - start
11387 };
11388 let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11389 let inserted = inserts.len();
11390 // The receiver's holes shift by (inserted - deleted) past the cut, and the
11391 // ones inside the cut move into the RETURNED array at their offset there.
11392 let holes = hole_set(recv);
11393 let removed = with_host(|h| {
11394 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
11395 let removed: Vec<Value> = items.splice(start..start + delete, inserts).collect();
11396 removed
11397 } else {
11398 Vec::new()
11399 }
11400 });
11401 let spliced = with_host(|h| {
11402 h.install_holes(
11403 recv,
11404 holes
11405 .iter()
11406 .filter_map(|&i| {
11407 if i < start {
11408 Some(i)
11409 } else if i < start + delete {
11410 None
11411 } else {
11412 Some(i - delete + inserted)
11413 }
11414 })
11415 .collect(),
11416 );
11417 (removed, holes.clone())
11418 });
11419 // The REMOVED elements come back as an array of the receiver's species
11420 // (23.1.3.31 step 8), so a subclass gets one of its own kind.
11421 let (removed, holes) = spliced;
11422 let out = array_species_create(recv, removed)?;
11423 with_host(|h| {
11424 h.install_holes(
11425 &out,
11426 holes
11427 .iter()
11428 .filter(|&&i| i >= start && i < start + delete)
11429 .map(|&i| i - start)
11430 .collect(),
11431 );
11432 });
11433 Ok(out)
11434}
11435
11436fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
11437 let norm = |v: f64| -> usize {
11438 if v < 0.0 {
11439 ((len as f64 + v).max(0.0)) as usize
11440 } else {
11441 (v as usize).min(len)
11442 }
11443 };
11444 let lo = if args.is_empty() || matches!(args[0], Value::Undef) {
11445 0
11446 } else {
11447 norm(arg_num(args, 0))
11448 };
11449 let hi = if args.len() < 2 || matches!(args[1], Value::Undef) {
11450 len
11451 } else {
11452 norm(arg_num(args, 1))
11453 };
11454 // A start at or past the end (`'World'.slice(2, 1)`) yields the empty range,
11455 // never a reversed one: JS `slice` clamps `end` up to `start`.
11456 (lo, hi.max(lo))
11457}
11458
11459/// The argument positions each `String.prototype` method coerces with
11460/// `ToNumber` rather than `ToString`. Everything not listed is a string
11461/// position — which matters only for a SYMBOL argument, the one value both
11462/// conversions refuse, and refuse with different wording.
11463///
11464/// Measured per method and per position: `'x'.indexOf(sym)` reports the STRING
11465/// message and `'x'.indexOf('a', sym)` the NUMBER one, and `padStart` is the
11466/// pair the other way round (a length then a pad string).
11467const STRING_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11468 ("at", &[0]),
11469 ("charAt", &[0]),
11470 ("charCodeAt", &[0]),
11471 ("codePointAt", &[0]),
11472 ("endsWith", &[1]),
11473 ("includes", &[1]),
11474 ("indexOf", &[1]),
11475 ("lastIndexOf", &[1]),
11476 ("padEnd", &[0]),
11477 ("padStart", &[0]),
11478 ("repeat", &[0]),
11479 ("slice", &[0, 1]),
11480 ("split", &[1]),
11481 ("startsWith", &[1]),
11482 ("substr", &[0, 1]),
11483 ("substring", &[0, 1]),
11484];
11485
11486/// Reject a SYMBOL argument before any string method coerces it. 7.1.17 and
11487/// 7.1.4 both refuse one, so `'x'.padStart(3, sym)` is a TypeError where this
11488/// rendered `Symbol(d)` into the result — silently, which is the shape of
11489/// mistake that makes a symbol key leak into text.
11490fn reject_symbol_args(name: &str, args: &[Value]) -> Result<(), String> {
11491 let numeric = STRING_METHOD_NUMERIC_ARGS
11492 .iter()
11493 .find(|(m, _)| *m == name)
11494 .map(|(_, ps)| *ps)
11495 .unwrap_or(&[]);
11496 for (i, a) in args.iter().enumerate() {
11497 if with_host(|h| matches!(h.get(a), Some(JsObj::Symbol { .. }))) {
11498 let kind = if numeric.contains(&i) {
11499 "number"
11500 } else {
11501 "string"
11502 };
11503 return Err(host::type_error(&format!(
11504 "Cannot convert a Symbol value to a {kind}"
11505 )));
11506 }
11507 }
11508 Ok(())
11509}
11510
11511/// Coerce a string method's arguments the way 22.1.3.x does, BEFORE any arm
11512/// reads them: a numeric position through `ToNumber`, every other through
11513/// `ToString`. Both run a user `valueOf`/`toString`, and none of them ran —
11514/// `'x'.padStart({valueOf: () => 3})` produced `"x"` and
11515/// `'x'.concat({toString: () => 'y'})` produced `"x[object Object]"`.
11516///
11517/// The positions that must NOT be coerced are the ones with their own protocol:
11518/// a RegExp or a `Symbol.replace`/`split`/`match`/`search` carrier at position
11519/// 0 of the method that honours it, and a callable REPLACEMENT at position 1 of
11520/// `replace`/`replaceAll`. Each of those already has a path that handles the
11521/// value as an object, and stringifying it first would take that path away.
11522/// The argument positions each `Array.prototype` method coerces with
11523/// `ToNumber` (23.1.3.x). Everything not listed is a VALUE position and must be
11524/// left alone: `fill`'s first argument, `with`'s second and `splice`'s items
11525/// are stored as given, and `indexOf`/`includes` compare their first argument
11526/// without converting it.
11527const ARRAY_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11528 ("at", &[0]),
11529 ("copyWithin", &[0, 1, 2]),
11530 ("fill", &[1, 2]),
11531 ("flat", &[0]),
11532 ("includes", &[1]),
11533 ("indexOf", &[1]),
11534 ("lastIndexOf", &[1]),
11535 ("slice", &[0, 1]),
11536 ("splice", &[0, 1]),
11537 ("toSpliced", &[0, 1]),
11538 ("with", &[0]),
11539];
11540
11541/// The same for `Number.prototype`. `toLocaleString` takes a LOCALE, not a
11542/// number, and is deliberately absent.
11543const NUMBER_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11544 ("toExponential", &[0]),
11545 ("toFixed", &[0]),
11546 ("toPrecision", &[0]),
11547 ("toString", &[0]),
11548];
11549
11550/// Replace the listed argument positions with their `ToNumber` value, running a
11551/// user `valueOf` and propagating a throw from it. Every one of these read the
11552/// argument with an INFALLIBLE conversion that does no `ToPrimitive` at all, so
11553/// `[1,2,3].slice({valueOf: () => 1})` sliced from 0 and `(1.234).toFixed(obj)`
11554/// was a RangeError.
11555/// `ToNumber(args[i])`, running a user `valueOf` and propagating its throw.
11556fn to_number_arg(args: &[Value], i: usize) -> Result<f64, String> {
11557 let v = args.get(i).cloned().unwrap_or(Value::Undef);
11558 let p = host::to_primitive(&v, "number")?;
11559 Ok(with_host(|h| h.to_number(&p)))
11560}
11561
11562fn coerce_numeric_args(
11563 table: &[(&str, &[usize])],
11564 name: &str,
11565 mut args: Vec<Value>,
11566) -> Result<Vec<Value>, String> {
11567 let Some((_, positions)) = table.iter().find(|(m, _)| *m == name) else {
11568 return Ok(args);
11569 };
11570 for &i in *positions {
11571 let Some(a) = args.get(i) else { continue };
11572 if matches!(a, Value::Undef) {
11573 continue;
11574 }
11575 let p = host::to_primitive(a, "number")?;
11576 args[i] = Value::Float(with_host(|h| h.to_number(&p)));
11577 }
11578 Ok(args)
11579}
11580
11581/// `RegExpCreate(v, flags)` — the regexp a string method builds from a
11582/// non-RegExp argument. An empty/absent argument makes the empty pattern, which
11583/// matches at position 0.
11584fn regexp_from_arg(v: &Value, flags: &str) -> Result<Value, String> {
11585 let src = if matches!(v, Value::Undef) {
11586 String::new()
11587 } else {
11588 with_host(|h| h.str_of(v))
11589 };
11590 let fv = with_host(|h| h.new_str(flags.to_string()));
11591 let sv = with_host(|h| h.new_str(src));
11592 regexp_ctor(&[sv, fv])
11593}
11594
11595fn coerce_string_args(name: &str, args: Vec<Value>) -> Result<Vec<Value>, String> {
11596 let numeric = STRING_METHOD_NUMERIC_ARGS
11597 .iter()
11598 .find(|(m, _)| *m == name)
11599 .map(|(_, ps)| *ps)
11600 .unwrap_or(&[]);
11601 let protocol = match name {
11602 "replace" | "replaceAll" => Some("@@replace"),
11603 "split" => Some("@@split"),
11604 "match" => Some("@@match"),
11605 "matchAll" => Some("@@matchAll"),
11606 "search" => Some("@@search"),
11607 // These three do not CONSUME `Symbol.match`, they reject a value that
11608 // carries it (22.1.3.7/23/24 step 3 — `IsRegExp`). Exempting it keeps
11609 // the object intact so that check still sees one; stringifying first
11610 // turned the TypeError into an ordinary search.
11611 "startsWith" | "endsWith" | "includes" => Some("@@match"),
11612 _ => None,
11613 };
11614 let mut out = Vec::with_capacity(args.len());
11615 for (i, a) in args.into_iter().enumerate() {
11616 if matches!(a, Value::Undef) {
11617 out.push(a);
11618 continue;
11619 }
11620 if numeric.contains(&i) {
11621 let p = host::to_primitive(&a, "number")?;
11622 out.push(Value::Float(with_host(|h| h.to_number(&p))));
11623 continue;
11624 }
11625 // The IsRegExp trio tests `Symbol.match` for TRUTHINESS (7.2.8 step 2),
11626 // not for presence: an object carrying `[Symbol.match]: false` is NOT a
11627 // regexp and coerces like anything else. The consuming protocols use
11628 // `GetMethod`, which additionally requires a callable.
11629 let is_regexp_like = matches!(name, "startsWith" | "endsWith" | "includes");
11630 let carries = |p: &str| match host::protocol_lookup(&a, p) {
11631 Ok(Some(m)) => {
11632 if is_regexp_like {
11633 with_host(|h| h.truthy(&m))
11634 } else {
11635 with_host(|h| host::is_callable(h, &m))
11636 }
11637 }
11638 _ => false,
11639 };
11640 let exempt = with_host(|h| matches!(h.get(&a), Some(JsObj::RegExp(_))))
11641 || (i == 0 && protocol.is_some_and(carries))
11642 || (i == 1
11643 && matches!(name, "replace" | "replaceAll")
11644 && with_host(|h| host::is_callable(h, &a)));
11645 if exempt {
11646 out.push(a);
11647 continue;
11648 }
11649 out.push(host::to_string_value(&a)?);
11650 }
11651 Ok(out)
11652}
11653
11654fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
11655 reject_symbol_args(name, &args)?;
11656 let args = coerce_string_args(name, args)?;
11657 // Every index-bearing method below counts UTF-16 code units, so they all
11658 // work off this one decoding rather than off `s.chars()` (code points),
11659 // which agrees only on the BMP. `@@iterator` is the deliberate exception.
11660 let u = crate::utf16::Units::of(s);
11661 match name {
11662 // `for…of` / spread over a string iterates CODE POINTS, not code units:
11663 // `[..."𝒳"]` is one element in node even though `"𝒳".length` is 2. This
11664 // is the one string operation that is specified in chars, so it stays
11665 // on `s.chars()` on purpose — do not "fix" it to match the others.
11666 "@@iterator" => {
11667 let items: Vec<Value> = s.chars().map(|c| new_s(c.to_string())).collect();
11668 Ok(with_host(|h| {
11669 h.alloc(JsObj::Iter {
11670 items,
11671 idx: 0,
11672 array: None,
11673 })
11674 }))
11675 }
11676 "toUpperCase" => Ok(new_s(s.to_uppercase())),
11677 "toLowerCase" => Ok(new_s(s.to_lowercase())),
11678 // `toLocaleUpperCase`/`toLocaleLowerCase` (22.1.3.26/22.1.3.24) differ
11679 // from the plain forms only for the locale-specific mappings (Turkish
11680 // dotless i, Lithuanian accents); with no locale argument they are the
11681 // Unicode Default Case Conversion, which is exactly `to_uppercase`/
11682 // `to_lowercase`. They threw `is not a function` before, so the common
11683 // no-argument call — the only form this runtime can answer, since it
11684 // carries no ICU — failed outright rather than agreeing with node.
11685 // A locale ARGUMENT is accepted and ignored; `'I'.toLocaleLowerCase('tr')`
11686 // is `'i'` here and `'ı'` in node.
11687 // `String.prototype.toLocaleString` (22.1.3.27) is `toString` — a string
11688 // has no locale rendering. Missing it made an ARRAY of strings fail too,
11689 // since `Array.prototype.toLocaleString` invokes it per element.
11690 "toLocaleString" => Ok(new_s(s.to_string())),
11691 "toLocaleUpperCase" => Ok(new_s(s.to_uppercase())),
11692 "toLocaleLowerCase" => Ok(new_s(s.to_lowercase())),
11693 // Locale comparison (ASCII approximation of ICU collation): primary by
11694 // case-folded order, then lowercase sorts before uppercase at a tie.
11695 "localeCompare" => {
11696 let other = with_host(|h| h.str_of(&arg0(&args)));
11697 let (la, lb) = (s.to_lowercase(), other.to_lowercase());
11698 let r = match la.cmp(&lb) {
11699 std::cmp::Ordering::Less => -1.0,
11700 std::cmp::Ordering::Greater => 1.0,
11701 std::cmp::Ordering::Equal => {
11702 let mut t = 0.0;
11703 for (ca, cb) in s.chars().zip(other.chars()) {
11704 if ca != cb {
11705 t = if ca.is_lowercase() { -1.0 } else { 1.0 };
11706 break;
11707 }
11708 }
11709 t
11710 }
11711 };
11712 Ok(Value::Float(r))
11713 }
11714 // `String.prototype.normalize` (22.1.3.15) — real UAX-15 normalization.
11715 //
11716 // This used to return the receiver unchanged and only validate the FORM
11717 // argument, which made every one of the four forms a no-op: `"Å"` (NFC,
11718 // one code point) and `"Å"` (NFD, two) stayed distinct under
11719 // `.normalize()`, so the standard way to compare Unicode text for
11720 // canonical equivalence silently answered `false`, and `NFKC` never
11721 // folded a compatibility character (`"fi"` stayed one code point instead
11722 // of becoming `"fi"`). The tables come from `unicode-normalization`.
11723 "normalize" => {
11724 use unicode_normalization::UnicodeNormalization;
11725 let form = match args.first() {
11726 Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
11727 _ => "NFC".to_string(),
11728 };
11729 let out = match form.as_str() {
11730 "NFC" => s.nfc().collect::<String>(),
11731 "NFD" => s.nfd().collect::<String>(),
11732 "NFKC" => s.nfkc().collect::<String>(),
11733 "NFKD" => s.nfkd().collect::<String>(),
11734 _ => {
11735 return Err(host::range_error(
11736 "The normalization form should be one of NFC, NFD, NFKC, NFKD.",
11737 ))
11738 }
11739 };
11740 Ok(new_s(out))
11741 }
11742 // ES2024 well-formedness (22.1.3.9 / 22.1.3.29). A `String` here is a
11743 // Rust `String`, whose `char` type EXCLUDES `U+D800..=U+DFFF`, so every
11744 // value this runtime can hold is well-formed by construction and
11745 // `toWellFormed` has nothing to replace. Both answers are therefore
11746 // exact for every string that survives storage; the one case node
11747 // answers differently is a surrogate half extracted by `charAt`/`slice`,
11748 // which is already `U+FFFD` here — the documented lone-surrogate
11749 // boundary in `utf16`, not a separate gap.
11750 "isWellFormed" => Ok(Value::Bool(true)),
11751 "toWellFormed" => Ok(new_s(s.to_string())),
11752 // The JS `WhiteSpace` set, not Rust's — they differ on `U+FEFF`.
11753 "trim" => Ok(new_s(crate::utf16::js_trim(s).to_string())),
11754 "trimStart" => Ok(new_s(crate::utf16::js_trim_start(s).to_string())),
11755 "trimEnd" => Ok(new_s(crate::utf16::js_trim_end(s).to_string())),
11756 "toString" | "valueOf" => Ok(new_s(s.to_string())),
11757 "charAt" => {
11758 let at = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit_str(i));
11759 Ok(new_s(at.unwrap_or_default()))
11760 }
11761 "at" => {
11762 let n = arg_num(&args, 0);
11763 // A negative position counts back from the end; `NaN` is 0. An
11764 // infinite position is out of range in either direction.
11765 let i = if n.is_nan() {
11766 Some(0i64)
11767 } else if n.is_finite() {
11768 let i = n.trunc() as i64;
11769 Some(if i < 0 { i + u.len() as i64 } else { i })
11770 } else {
11771 None
11772 };
11773 match i
11774 .and_then(|i| usize::try_from(i).ok())
11775 .and_then(|i| u.unit_str(i))
11776 {
11777 Some(c) => Ok(new_s(c)),
11778 None => Ok(Value::Undef),
11779 }
11780 }
11781 // `charCodeAt` reports the bare code UNIT — the high surrogate of an
11782 // astral character, not the character. `codePointAt` looks ahead one
11783 // unit and reports the whole scalar when the pair is well formed. They
11784 // agree everywhere on the BMP, which is why they used to share an arm.
11785 // They also disagree OUT of range: `charCodeAt` yields `NaN` while
11786 // `codePointAt` yields `undefined` (measured on node v26.7.0).
11787 "charCodeAt" => {
11788 let unit = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit(i));
11789 Ok(Value::Float(unit.map(f64::from).unwrap_or(f64::NAN)))
11790 }
11791 "codePointAt" => match unit_pos(arg_num(&args, 0)).and_then(|i| u.code_point(i)) {
11792 Some(cp) => Ok(Value::Float(f64::from(cp))),
11793 None => Ok(Value::Undef),
11794 },
11795 // The search quartet all honor their optional position argument.
11796 // `"a&b&c".indexOf("&", 2)` must be 3, not 1 — body-parser's
11797 // parameterCount walks a query string with exactly that call.
11798 "indexOf" => {
11799 let needle = needle_units(&args);
11800 let from = clamp_pos(arg_num(&args, 1), u.len());
11801 Ok(Value::Float(
11802 search_from(u.as_slice(), needle.as_slice(), from)
11803 .map(|i| i as f64)
11804 .unwrap_or(-1.0),
11805 ))
11806 }
11807 "lastIndexOf" => {
11808 let needle = needle_units(&args);
11809 // An absent or NaN position means "search the whole string".
11810 let n = arg_num(&args, 1);
11811 let upto = if n.is_nan() {
11812 u.len()
11813 } else {
11814 clamp_pos(n, u.len())
11815 };
11816 Ok(Value::Float(
11817 search_last(u.as_slice(), needle.as_slice(), upto)
11818 .map(|i| i as f64)
11819 .unwrap_or(-1.0),
11820 ))
11821 }
11822 // 22.1.3.7/22.1.3.23/22.1.3.14 step 2: these three reject a REGEXP
11823 // argument outright, and `IsRegExp` is what decides — so an object
11824 // advertising `Symbol.match` is rejected too. None of them checked.
11825 "startsWith" | "endsWith" | "includes" if is_regexp_arg(&arg0(&args)) => {
11826 Err(host::type_error(&format!(
11827 "First argument to String.prototype.{name} must not be a regular expression"
11828 )))
11829 }
11830 "includes" => {
11831 let needle = needle_units(&args);
11832 let from = clamp_pos(arg_num(&args, 1), u.len());
11833 Ok(Value::Bool(
11834 search_from(u.as_slice(), needle.as_slice(), from).is_some(),
11835 ))
11836 }
11837 "startsWith" => {
11838 let needle = needle_units(&args);
11839 let from = clamp_pos(arg_num(&args, 1), u.len());
11840 Ok(Value::Bool(
11841 u.as_slice()[from..].starts_with(needle.as_slice()),
11842 ))
11843 }
11844 "endsWith" => {
11845 let needle = needle_units(&args);
11846 // The 2nd argument is where the string is treated as ENDING.
11847 let end = if args.len() < 2 || matches!(args[1], Value::Undef) {
11848 u.len()
11849 } else {
11850 clamp_pos(arg_num(&args, 1), u.len())
11851 };
11852 Ok(Value::Bool(
11853 u.as_slice()[..end].ends_with(needle.as_slice()),
11854 ))
11855 }
11856 "slice" => {
11857 let (lo, hi) = slice_bounds(&args, u.len());
11858 Ok(new_s(u.slice(lo, hi)))
11859 }
11860 "substring" => {
11861 let mut a = arg_num(&args, 0).max(0.0) as usize;
11862 let mut b = if args.len() < 2 || matches!(args[1], Value::Undef) {
11863 u.len()
11864 } else {
11865 (arg_num(&args, 1).max(0.0) as usize).min(u.len())
11866 };
11867 a = a.min(u.len());
11868 if a > b {
11869 std::mem::swap(&mut a, &mut b);
11870 }
11871 Ok(new_s(u.slice(a, b)))
11872 }
11873 "substr" => {
11874 // A negative start counts from the end: max(len + start, 0).
11875 let len = u.len() as i64;
11876 let mut start = arg_num(&args, 0) as i64;
11877 if start < 0 {
11878 start = (len + start).max(0);
11879 }
11880 let start = (start as usize).min(u.len());
11881 let count = if args.len() >= 2 {
11882 arg_num(&args, 1).max(0.0) as usize
11883 } else {
11884 u.len()
11885 };
11886 let end = start.saturating_add(count).min(u.len());
11887 Ok(new_s(u.slice(start, end)))
11888 }
11889 "repeat" => {
11890 let n = arg_num(&args, 0);
11891 // `RangeError`, not `TypeError`, and the count is named:
11892 // `"x".repeat(-1)` is `RangeError: Invalid count value: -1`.
11893 if n < 0.0 || !n.is_finite() {
11894 return Err(host::range_error(&format!(
11895 "Invalid count value: {}",
11896 host::fmt_number(n)
11897 )));
11898 }
11899 // The PRODUCT is what V8 bounds, so `''.repeat(2**53)` is legal (and
11900 // `''`) while `'ab'.repeat(268435445)` is not: measured on node
11901 // v26.7.0, `'ab'.repeat(268435444).length` is 536870888 and one more
11902 // is `RangeError: Invalid string length`.
11903 if n * crate::utf16::len(s) as f64 > host::MAX_STRING_LENGTH as f64 {
11904 return Err(host::invalid_string_length());
11905 }
11906 Ok(new_s(s.repeat(n as usize)))
11907 }
11908 "concat" => {
11909 let mut out = s.to_string();
11910 for a in &args {
11911 out.push_str(&with_host(|h| h.str_of(a)));
11912 }
11913 Ok(new_s(out))
11914 }
11915 "padStart" => Ok(new_s(pad(s, &args, true)?)),
11916 "padEnd" => Ok(new_s(pad(s, &args, false)?)),
11917 // Regex-taking string methods: dispatch to the regexp module when the
11918 // argument is a RegExp; otherwise keep the plain-string behavior.
11919 // 22.1.3.20 step 2.a: `replaceAll` validates the `g` flag BEFORE it
11920 // consults `Symbol.replace`, so a non-global regexp is a TypeError even
11921 // though a RegExp does define that method. Delegating first skipped the
11922 // check and silently did a single replacement.
11923 "replaceAll"
11924 if is_regexp_arg(&arg0(&args))
11925 && !with_host(
11926 |h| matches!(h.get(&arg0(&args)), Some(JsObj::RegExp(r)) if r.global),
11927 ) =>
11928 {
11929 Err(host::type_error(
11930 "String.prototype.replaceAll called with a non-global RegExp argument",
11931 ))
11932 }
11933 "match" | "matchAll" | "search" | "split" | "replace" | "replaceAll"
11934 if symbol_protocol(
11935 &arg0(&args),
11936 match name {
11937 "match" => "@@match",
11938 "matchAll" => "@@matchAll",
11939 "search" => "@@search",
11940 "split" => "@@split",
11941 _ => "@@replace",
11942 },
11943 )
11944 .is_some() =>
11945 {
11946 let sym = match name {
11947 "match" => "@@match",
11948 "matchAll" => "@@matchAll",
11949 "search" => "@@search",
11950 "split" => "@@split",
11951 _ => "@@replace",
11952 };
11953 let f = symbol_protocol(&arg0(&args), sym).expect("guard checked");
11954 let sv = with_host(|h| h.new_str(s.to_string()));
11955 let mut rest = vec![sv];
11956 rest.extend(args.iter().skip(1).cloned());
11957 host::invoke(&f, rest, Some(arg0(&args)))
11958 }
11959 // 22.1.3.13/14: a non-RegExp argument is turned INTO one
11960 // (`RegExpCreate(regexp, …)`), so `'abc'.match('b')` matches. It
11961 // answered `null` for every string argument, which reads as "no match"
11962 // — the one answer a caller cannot tell from a real failure.
11963 // `matchAll` builds its with `g`, which 22.1.3.14 requires.
11964 "match" => {
11965 let a = arg0(&args);
11966 let re = if is_regexp_arg(&a) {
11967 a
11968 } else {
11969 regexp_from_arg(&a, "")?
11970 };
11971 crate::regexp::str_match(s, &re)
11972 }
11973 "matchAll" => {
11974 let a = arg0(&args);
11975 let re = if is_regexp_arg(&a) {
11976 a
11977 } else {
11978 regexp_from_arg(&a, "g")?
11979 };
11980 crate::regexp::str_match_all(s, &re)
11981 }
11982 "search" => {
11983 if is_regexp_arg(&arg0(&args)) {
11984 crate::regexp::str_search(s, &arg0(&args))
11985 } else {
11986 // 22.1.3.17 builds a RegExp from the argument, so a
11987 // METACHARACTER matches as one: `'a.c'.search('.')` is 0, not
11988 // 1. The substring approximation this replaces agreed only for
11989 // a literal needle, and answered -1 for an absent argument
11990 // where the empty pattern matches at 0.
11991 let re = regexp_from_arg(&arg0(&args), "")?;
11992 crate::regexp::str_search(s, &re)
11993 }
11994 }
11995 "replace" => {
11996 let pat = arg0(&args);
11997 let repl = args.get(1).cloned().unwrap_or(Value::Undef);
11998 if is_regexp_arg(&pat) {
11999 crate::regexp::str_replace_regex(s, &pat, &repl, false)
12000 } else if with_host(|h| host::is_callable(h, &repl)) {
12001 Ok(new_s(replace_str_fn(
12002 s,
12003 &with_host(|h| h.str_of(&pat)),
12004 &repl,
12005 false,
12006 )?))
12007 } else {
12008 let from = with_host(|h| h.str_of(&pat));
12009 let to = with_host(|h| h.str_of(&repl));
12010 Ok(new_s(replace_str_plain(s, &from, &to, false)))
12011 }
12012 }
12013 "replaceAll" => {
12014 let pat = arg0(&args);
12015 let repl = args.get(1).cloned().unwrap_or(Value::Undef);
12016 if is_regexp_arg(&pat) {
12017 // 22.1.3.20 step 2: a non-global regexp is a TypeError here,
12018 // because `replaceAll` cannot honour "all" without `g`. This
12019 // used to replace only the first match and say nothing.
12020 let global = with_host(|h| match h.get(&pat) {
12021 Some(JsObj::RegExp(r)) => r.global,
12022 _ => true,
12023 });
12024 if !global {
12025 return Err(host::type_error(
12026 "String.prototype.replaceAll called with a non-global RegExp argument",
12027 ));
12028 }
12029 crate::regexp::str_replace_regex(s, &pat, &repl, true)
12030 } else if with_host(|h| host::is_callable(h, &repl)) {
12031 Ok(new_s(replace_str_fn(
12032 s,
12033 &with_host(|h| h.str_of(&pat)),
12034 &repl,
12035 true,
12036 )?))
12037 } else {
12038 let from = with_host(|h| h.str_of(&pat));
12039 let to = with_host(|h| h.str_of(&repl));
12040 Ok(new_s(replace_str_plain(s, &from, &to, true)))
12041 }
12042 }
12043 "split" => {
12044 if is_regexp_arg(&arg0(&args)) {
12045 let limit = args
12046 .get(1)
12047 .filter(|v| !matches!(v, Value::Undef))
12048 .map(|v| with_host(|h| h.to_number(v)) as usize);
12049 return crate::regexp::str_split_regex(s, &arg0(&args), limit);
12050 }
12051 let mut parts: Vec<Value> = if args.is_empty() || matches!(args[0], Value::Undef) {
12052 vec![new_s(s.to_string())]
12053 } else {
12054 let sep = with_host(|h| h.str_of(&args[0]));
12055 if sep.is_empty() {
12056 // `split('')` yields one element per code UNIT, so an astral
12057 // character becomes its two surrogate halves.
12058 (0..u.len())
12059 .filter_map(|i| u.unit_str(i))
12060 .map(new_s)
12061 .collect()
12062 } else {
12063 s.split(&sep as &str)
12064 .map(|p| new_s(p.to_string()))
12065 .collect()
12066 }
12067 };
12068 // Optional limit: keep at most `limit` substrings.
12069 if let Some(lim) = args.get(1).filter(|v| !matches!(v, Value::Undef)) {
12070 let n = with_host(|h| h.to_number(lim));
12071 if n.is_finite() && n >= 0.0 {
12072 parts.truncate(n as usize);
12073 }
12074 }
12075 Ok(with_host(|h| h.new_array(parts)))
12076 }
12077 _ => Err(host::type_error(&format!("{name} is not a function"))),
12078 }
12079}
12080
12081/// GetSubstitution (22.1.3.19) for a STRING search value.
12082///
12083/// `String.prototype.replace`/`replaceAll` expand the same `$` patterns whether
12084/// the pattern is a regexp or a plain string, but the string path here did a
12085/// raw `str::replace` and passed the template through verbatim — so
12086/// `'abc'.replace('b', '[$&]')` produced `a[$&]c` instead of `a[b]c`. The
12087/// regexp path has always expanded them.
12088///
12089/// A string search captures nothing, so only `$$`, `$&`, `` $` `` and `$'`
12090/// apply; `$1` and `$<name>` have no referent and stay literal, which is also
12091/// what node does.
12092fn substitute_plain(templ: &str, matched: &str, position: usize, subject: &str) -> String {
12093 let chars: Vec<char> = templ.chars().collect();
12094 let mut out = String::new();
12095 let mut i = 0;
12096 while i < chars.len() {
12097 if chars[i] == '$' && i + 1 < chars.len() {
12098 match chars[i + 1] {
12099 '$' => {
12100 out.push('$');
12101 i += 2;
12102 continue;
12103 }
12104 '&' => {
12105 out.push_str(matched);
12106 i += 2;
12107 continue;
12108 }
12109 '`' => {
12110 out.push_str(&subject[..position]);
12111 i += 2;
12112 continue;
12113 }
12114 '\'' => {
12115 out.push_str(&subject[position + matched.len()..]);
12116 i += 2;
12117 continue;
12118 }
12119 _ => {}
12120 }
12121 }
12122 out.push(chars[i]);
12123 i += 1;
12124 }
12125 out
12126}
12127
12128/// `replace`/`replaceAll` with a string pattern and a string replacement,
12129/// expanding each match's `$` patterns against its own position.
12130fn replace_str_plain(s: &str, from: &str, to: &str, all: bool) -> String {
12131 if from.is_empty() && !all {
12132 return format!("{}{s}", substitute_plain(to, "", 0, s));
12133 }
12134 let mut out = String::new();
12135 let mut rest = 0usize;
12136 while let Some(rel) = s[rest..].find(from) {
12137 let at = rest + rel;
12138 out.push_str(&s[rest..at]);
12139 out.push_str(&substitute_plain(to, from, at, s));
12140 rest = at + from.len();
12141 if !all {
12142 break;
12143 }
12144 // An empty pattern matches between every character; step one along so
12145 // the scan terminates.
12146 if from.is_empty() {
12147 if rest >= s.len() {
12148 break;
12149 }
12150 let step = s[rest..].chars().next().map(|c| c.len_utf8()).unwrap_or(1);
12151 out.push_str(&s[rest..rest + step]);
12152 rest += step;
12153 }
12154 }
12155 out.push_str(&s[rest..]);
12156 out
12157}
12158
12159fn new_s(s: String) -> Value {
12160 with_host(|h| h.new_str(s))
12161}
12162
12163/// Where a forward `indexOf`/`includes` search starts, given the optional
12164/// `fromIndex` (23.1.3.17 steps 4-6, 23.1.3.16 steps 5-7). A negative value
12165/// counts back from the end and clamps at 0; absent or `NaN` is 0. A start at
12166/// or past the end finds nothing, which callers report as `-1` / `false`.
12167pub(crate) fn search_start(n: f64, len: usize) -> usize {
12168 if n.is_nan() {
12169 return 0;
12170 }
12171 let n = n.trunc();
12172 if n >= 0.0 {
12173 if n >= len as f64 {
12174 len
12175 } else {
12176 n as usize
12177 }
12178 } else {
12179 let from_end = len as f64 + n;
12180 if from_end <= 0.0 {
12181 0
12182 } else {
12183 from_end as usize
12184 }
12185 }
12186}
12187
12188/// The INCLUSIVE index a backward `lastIndexOf` starts at (23.1.3.20 steps
12189/// 4-6), or `None` when `fromIndex` places it before the array. Absent means
12190/// the last element — which is why this takes an `Option` rather than reading
12191/// `NaN` as "absent" the way the forward form can: an explicit `NaN` is
12192/// `ToIntegerOrInfinity`'d to 0 and searches only index 0.
12193pub(crate) fn search_start_last(from: Option<f64>, len: usize) -> Option<usize> {
12194 if len == 0 {
12195 return None;
12196 }
12197 let n = match from {
12198 None => return Some(len - 1),
12199 Some(v) if v.is_nan() => 0.0,
12200 Some(v) => v.trunc(),
12201 };
12202 if n >= 0.0 {
12203 Some(if n >= len as f64 { len - 1 } else { n as usize })
12204 } else {
12205 let k = len as f64 + n;
12206 if k < 0.0 {
12207 None
12208 } else {
12209 Some(k as usize)
12210 }
12211 }
12212}
12213
12214/// `ToIntegerOrInfinity(n)` clamped into `0..=len` — the position argument of
12215/// the `String.prototype` search methods. `NaN` (an absent argument) is `0`.
12216fn clamp_pos(n: f64, len: usize) -> usize {
12217 if n.is_nan() || n <= 0.0 {
12218 0
12219 } else if n >= len as f64 {
12220 len
12221 } else {
12222 n.trunc() as usize
12223 }
12224}
12225
12226/// `ToIntegerOrInfinity(n)` as a code-unit position, or `None` when there can be
12227/// no such unit. `NaN` (an absent argument) is 0; a negative or infinite
12228/// position is out of range — `"abc".charCodeAt(-1)` is `NaN`, not `'a'`.
12229fn unit_pos(n: f64) -> Option<usize> {
12230 if n.is_nan() {
12231 Some(0)
12232 } else if n < 0.0 || !n.is_finite() {
12233 None
12234 } else {
12235 Some(n.trunc() as usize)
12236 }
12237}
12238
12239/// The search argument of `indexOf`/`includes`/`startsWith`/… as code units, so
12240/// the needle is compared in the same alphabet the haystack is indexed by.
12241fn needle_units(args: &[Value]) -> crate::utf16::Units {
12242 crate::utf16::Units::of(&with_host(|h| h.str_of(&arg0(args))))
12243}
12244
12245/// The lowest index `>= from` at which `needle` occurs in `hay`. An empty
12246/// needle matches at `from` itself, as JS specifies.
12247fn search_from(hay: &[u16], needle: &[u16], from: usize) -> Option<usize> {
12248 if needle.is_empty() {
12249 return Some(from.min(hay.len()));
12250 }
12251 if needle.len() > hay.len() {
12252 return None;
12253 }
12254 (from..=hay.len().saturating_sub(needle.len())).find(|&i| &hay[i..i + needle.len()] == needle)
12255}
12256
12257/// The highest index `<= upto` at which `needle` occurs in `hay`.
12258fn search_last(hay: &[u16], needle: &[u16], upto: usize) -> Option<usize> {
12259 if needle.is_empty() {
12260 return Some(upto.min(hay.len()));
12261 }
12262 if needle.len() > hay.len() {
12263 return None;
12264 }
12265 let last = hay.len() - needle.len();
12266 (0..=upto.min(last))
12267 .rev()
12268 .find(|&i| &hay[i..i + needle.len()] == needle)
12269}
12270
12271fn pad(s: &str, args: &[Value], start: bool) -> Result<String, String> {
12272 let target_f = arg_num(args, 0);
12273 let target = if target_f.is_finite() && target_f > 0.0 {
12274 target_f as usize
12275 } else {
12276 0
12277 };
12278 // `targetLength` and the padding both count code units: `'𝒳'.padStart(3,'-')`
12279 // is `'-𝒳'` in node, not `'--𝒳'`.
12280 let cur = crate::utf16::len(s);
12281 if cur >= target {
12282 return Ok(s.to_string());
12283 }
12284 let filler = if args.len() >= 2 {
12285 with_host(|h| h.str_of(&args[1]))
12286 } else {
12287 " ".to_string()
12288 };
12289 if filler.is_empty() {
12290 return Ok(s.to_string());
12291 }
12292 // Checked only AFTER the two short-circuits, which is the order V8 uses:
12293 // measured on node v26.7.0, `'ab'.padStart(2**40, '')` is `'ab'` while
12294 // `'ab'.padStart(536870889, 'x')` is `RangeError: Invalid string length`.
12295 if target_f > host::MAX_STRING_LENGTH as f64 {
12296 return Err(host::invalid_string_length());
12297 }
12298 let need = target - cur;
12299 let fill = crate::utf16::Units::of(&filler);
12300 // The filler repeats and is TRUNCATED to the exact unit count, which can cut
12301 // a surrogate pair — node yields a lone surrogate there, we yield U+FFFD
12302 // (see src/utf16.rs).
12303 let units: Vec<u16> = (0..need)
12304 .filter_map(|i| fill.unit(i % fill.len()))
12305 .collect();
12306 let padding = crate::utf16::to_string_lossy(&units);
12307 Ok(if start {
12308 format!("{padding}{s}")
12309 } else {
12310 format!("{s}{padding}")
12311 })
12312}
12313
12314/// V8's radix rejection, shared by `Number.prototype.toString` and
12315/// `BigInt.prototype.toString` — one string, because they are one message and
12316/// the two sites had drifted apart ("radix must be" vs V8's "radix argument
12317/// must be").
12318const RADIX_RANGE: &str = "toString() radix argument must be between 2 and 36";
12319
12320/// `BigInt.prototype` methods: `toString([radix])`, `valueOf`, `toLocaleString`.
12321fn bigint_method(b: &num_bigint::BigInt, name: &str, args: Vec<Value>) -> Result<Value, String> {
12322 match name {
12323 "toString" => {
12324 let radix = match args.first() {
12325 None | Some(Value::Undef) => 10,
12326 Some(_) => {
12327 let t = arg_num(&args, 0).trunc();
12328 if !(2.0..=36.0).contains(&t) {
12329 return Err(host::range_error(RADIX_RANGE));
12330 }
12331 t as u32
12332 }
12333 };
12334 Ok(new_s(b.to_str_radix(radix)))
12335 }
12336 // `BigInt.prototype.toLocaleString` groups thousands like the Number
12337 // one does — `(1234567n).toLocaleString()` is `1,234,567` in node, and
12338 // returning the bare digits made it the only numeric type that skipped
12339 // grouping. Same en-US-shaped output as `Number.prototype`; the
12340 // `locales`/`options` arguments are ignored (no ICU here).
12341 "toLocaleString" => {
12342 let digits = b.magnitude().to_string();
12343 let sign = if b.sign() == num_bigint::Sign::Minus {
12344 "-"
12345 } else {
12346 ""
12347 };
12348 Ok(new_s(format!("{sign}{}", group_thousands(&digits))))
12349 }
12350 "valueOf" => Ok(with_host(|h| h.new_bigint(b.clone()))),
12351 _ => Err(host::type_error(&format!("{name} is not a function"))),
12352 }
12353}
12354
12355fn number_method(n: f64, name: &str, args: Vec<Value>) -> Result<Value, String> {
12356 let args = coerce_numeric_args(NUMBER_METHOD_NUMERIC_ARGS, name, args)?;
12357 match name {
12358 "toFixed" => {
12359 let digits = arg_num(&args, 0);
12360 if !(0.0..=100.0).contains(&digits.trunc()) {
12361 return Err(host::range_error(
12362 "toFixed() digits argument must be between 0 and 100",
12363 ));
12364 }
12365 Ok(new_s(to_fixed(n, digits as usize)))
12366 }
12367 "toExponential" => {
12368 // `undefined` (or a missing argument) selects the shortest form.
12369 let f = match args.first() {
12370 None | Some(Value::Undef) => None,
12371 Some(_) => {
12372 let d = arg_num(&args, 0).trunc();
12373 if !(0.0..=100.0).contains(&d) {
12374 return Err(host::range_error(
12375 "toExponential() argument must be between 0 and 100",
12376 ));
12377 }
12378 Some(d as usize)
12379 }
12380 };
12381 Ok(new_s(to_exponential(n, f)))
12382 }
12383 "toString" => {
12384 // An out-of-range radix THROWS; it does not silently fall back to
12385 // base 10. `(1).toString(37)` returned "1" here, so a support probe
12386 // was told every radix worked.
12387 let radix = match args.first() {
12388 None | Some(Value::Undef) => 10,
12389 Some(_) => {
12390 let r = arg_num(&args, 0);
12391 let t = r.trunc();
12392 if !(2.0..=36.0).contains(&t) {
12393 return Err(host::range_error(RADIX_RANGE));
12394 }
12395 t as u32
12396 }
12397 };
12398 if radix == 10 {
12399 Ok(new_s(host::fmt_number(n)))
12400 } else {
12401 Ok(new_s(to_radix(n, radix)))
12402 }
12403 }
12404 "toPrecision" => {
12405 // `undefined` (or a missing argument) behaves like `toString()`.
12406 match args.first() {
12407 None | Some(Value::Undef) => Ok(new_s(host::fmt_number(n))),
12408 Some(_) => {
12409 let p = arg_num(&args, 0).trunc();
12410 if !(1.0..=100.0).contains(&p) {
12411 return Err(host::range_error(
12412 "toPrecision() argument must be between 1 and 100",
12413 ));
12414 }
12415 Ok(new_s(to_precision(n, p as usize)))
12416 }
12417 }
12418 }
12419 "toLocaleString" => Ok(new_s(to_locale_string(n))),
12420 "valueOf" => Ok(Value::Float(n)),
12421 _ => Err(host::type_error(&format!("{name} is not a function"))),
12422 }
12423}
12424
12425/// `Number.prototype.toLocaleString()` with the default locale and options:
12426/// integer part grouped in threes with `,`, up to 3 fraction digits (rounded
12427/// half away from zero), trailing fractional zeros dropped. Mirrors V8's default
12428/// `Intl.NumberFormat().format` output (`(12345.678).toLocaleString()` ⇒
12429/// `"12,345.678"`; `(1234.5678)` ⇒ `"1,234.568"`). `NaN`, `±Infinity`, and `-0`
12430/// render as `"NaN"`, `"∞"`/`"-∞"`, and `"-0"`.
12431fn to_locale_string(n: f64) -> String {
12432 if n.is_nan() {
12433 return "NaN".to_string();
12434 }
12435 if n.is_infinite() {
12436 return if n < 0.0 { "-∞" } else { "∞" }.to_string();
12437 }
12438 let neg = n.is_sign_negative();
12439 // Round the magnitude to at most 3 fraction digits, then drop trailing zeros
12440 // (and a bare trailing point). `to_fixed` rounds half away from zero.
12441 // `to_fixed` falls back to `ToString` at |x| ≥ 1e21 (spec 21.1.3.3 step 6),
12442 // which is exponential — and the grouping below then chopped up the
12443 // exponent, so `(1e21).toLocaleString()` was `1e,+21` instead of node's
12444 // `1,000,000,000,000,000,000,000`. Expanding the SHORTEST repr is the right
12445 // source: node groups the shortest decimal form, so `(1e100)
12446 // .toLocaleString()` is 1 followed by a hundred zeros rather than the exact
12447 // binary value `1000…159028911…`. (`BigInt(1e100)` is the exact value, a
12448 // deliberately different rule — see `bigint_ctor`.)
12449 let fixed = expand_exponential(&to_fixed(n.abs(), 3));
12450 let trimmed = match fixed.split_once('.') {
12451 Some(_) => fixed.trim_end_matches('0').trim_end_matches('.'),
12452 None => fixed.as_str(),
12453 };
12454 let (int_part, frac_part) = match trimmed.split_once('.') {
12455 Some((i, f)) => (i, Some(f)),
12456 None => (trimmed, None),
12457 };
12458 let mut out = String::new();
12459 if neg {
12460 out.push('-'); // Intl keeps the sign even for -0.
12461 }
12462 out.push_str(&group_thousands(int_part));
12463 if let Some(f) = frac_part {
12464 out.push('.');
12465 out.push_str(f);
12466 }
12467 out
12468}
12469
12470/// Write a nonnegative decimal string in plain positional form, expanding an
12471/// `e+NN` exponent into zeros. `"1e+21"` → `"1000000000000000000000"`,
12472/// `"1.5e+21"` → `"1500000000000000000000"`. A string with no exponent, or a
12473/// negative exponent (a magnitude below 1, which the caller has already rounded
12474/// to zero), is returned unchanged.
12475fn expand_exponential(s: &str) -> String {
12476 let Some((mantissa, exp)) = s.split_once(['e', 'E']) else {
12477 return s.to_string();
12478 };
12479 let Ok(exp) = exp.trim_start_matches('+').parse::<i32>() else {
12480 return s.to_string();
12481 };
12482 if exp <= 0 {
12483 return s.to_string();
12484 }
12485 let (int_digits, frac_digits) = match mantissa.split_once('.') {
12486 Some((i, f)) => (i.to_string(), f.to_string()),
12487 None => (mantissa.to_string(), String::new()),
12488 };
12489 let mut digits = int_digits;
12490 digits.push_str(&frac_digits);
12491 // The exponent consumes the fractional digits first; whatever is left
12492 // becomes trailing zeros.
12493 let zeros = exp as usize - frac_digits.len().min(exp as usize);
12494 digits.push_str(&"0".repeat(zeros));
12495 digits
12496}
12497
12498/// Insert `,` as a thousands separator into a nonnegative integer digit string.
12499fn group_thousands(int_part: &str) -> String {
12500 let bytes = int_part.as_bytes();
12501 let n = bytes.len();
12502 let mut out = String::with_capacity(n + n / 3);
12503 for (i, &b) in bytes.iter().enumerate() {
12504 if i > 0 && (n - i) % 3 == 0 {
12505 out.push(',');
12506 }
12507 out.push(b as char);
12508 }
12509 out
12510}
12511
12512/// `Number.prototype.toFixed(f)`: fixed-point with `f` fractional digits, rounding
12513/// half away from zero on the actual IEEE-754 value (so `(1.005).toFixed(2)` is
12514/// `"1.00"` because 1.005 is really 1.00499…). The sign of a negative input is
12515/// preserved even when the rounded magnitude is zero: `(-0.4).toFixed(0) === "-0"`.
12516///
12517/// The rounding is done on the value's EXACT decimal expansion (Rust's fixed
12518/// formatting is exact), not on `x * 10^f` — the latter loses precision for large
12519/// magnitudes (`(9.999999e20).toFixed(4)` must keep every integer digit).
12520fn to_fixed(n: f64, f: usize) -> String {
12521 if !n.is_finite() {
12522 return host::fmt_number(n);
12523 }
12524 // Spec: for |x| ≥ 10^21, toFixed falls back to ToString(x).
12525 if n.abs() >= 1e21 {
12526 return host::fmt_number(n);
12527 }
12528 let neg = n < 0.0;
12529 // Exact decimal with guard digits past the rounding position; then round the
12530 // digit string half-away-from-zero (nonneg operand ⇒ round-half-up).
12531 let full = format!("{:.*}", f + 25, n.abs());
12532 let mut body = round_decimal_string(&full, f);
12533 if neg {
12534 body.insert(0, '-'); // JS keeps the sign even for "-0" / "-0.00".
12535 }
12536 body
12537}
12538
12539/// Round the exact decimal string `s` (`"int.frac"`, nonnegative) to `f`
12540/// fractional digits, half away from zero, propagating carry across the point.
12541fn round_decimal_string(s: &str, f: usize) -> String {
12542 let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
12543 let mut digits: Vec<u8> = int_part
12544 .bytes()
12545 .chain(frac_part.bytes())
12546 .map(|b| b - b'0')
12547 .collect();
12548 let point = int_part.len(); // digits before the decimal point
12549 let keep = point + f; // number of leading digits to keep
12550
12551 // Round up if the first dropped digit is ≥ 5 (exact-half ⇒ up).
12552 if digits.get(keep).map(|&d| d >= 5).unwrap_or(false) {
12553 let mut i = keep;
12554 loop {
12555 if i == 0 {
12556 digits.insert(0, 1);
12557 // A new leading digit shifts the decimal point right by one.
12558 return assemble_decimal(&digits, point + 1, f);
12559 }
12560 i -= 1;
12561 if digits[i] == 9 {
12562 digits[i] = 0;
12563 } else {
12564 digits[i] += 1;
12565 break;
12566 }
12567 }
12568 }
12569 assemble_decimal(&digits, point, f)
12570}
12571
12572/// Reassemble `digits` into `"int.frac"` keeping `f` fractional digits, given that
12573/// `point` digits precede the decimal point.
12574fn assemble_decimal(digits: &[u8], point: usize, f: usize) -> String {
12575 let int_str: String = digits[..point].iter().map(|d| (d + b'0') as char).collect();
12576 let int_str = int_str.trim_start_matches('0');
12577 let int_str = if int_str.is_empty() { "0" } else { int_str };
12578 if f == 0 {
12579 return int_str.to_string();
12580 }
12581 let frac: String = digits[point..point + f]
12582 .iter()
12583 .map(|d| (d + b'0') as char)
12584 .collect();
12585 format!("{int_str}.{frac}")
12586}
12587
12588/// Round the nonnegative finite `a` to `p` significant decimal digits, half away
12589/// from zero, returning the `p` digits and the decimal exponent `e` such that the
12590/// value is `0.d…d × 10^(e+1)` (i.e. `d.d…d e±e`). Rust's `{:.*e}` rounds half to
12591/// EVEN (`(2.5)` at 1 digit would give "2"), but JS rounds half up ("3"), so the
12592/// exact digits are taken with guard positions and rounded here.
12593fn round_significant(a: f64, p: usize) -> (String, i32) {
12594 let sci = format!("{a:.*e}", p - 1 + 25);
12595 let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12596 let mut e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
12597 let all: Vec<u8> = mant
12598 .chars()
12599 .filter(|c| c.is_ascii_digit())
12600 .map(|c| c as u8 - b'0')
12601 .collect();
12602 let mut s: String = all[..p].iter().map(|d| (d + b'0') as char).collect();
12603 if all.get(p).map(|&d| d >= 5).unwrap_or(false) {
12604 // Round the p-digit mantissa up, propagating carry; a carry out of the
12605 // leading digit (`9.99 → 10`) bumps the decimal exponent by one.
12606 let mut d: Vec<u8> = all[..p].to_vec();
12607 let mut i = p;
12608 loop {
12609 if i == 0 {
12610 d.insert(0, 1);
12611 d.truncate(p);
12612 e += 1;
12613 break;
12614 }
12615 i -= 1;
12616 if d[i] == 9 {
12617 d[i] = 0;
12618 } else {
12619 d[i] += 1;
12620 break;
12621 }
12622 }
12623 s = d.iter().map(|x| (x + b'0') as char).collect();
12624 }
12625 (s, e)
12626}
12627
12628/// `Number.prototype.toExponential(f)`: one digit before the point and `f` after,
12629/// with a signed decimal exponent (`(100).toExponential(2) === "1.00e+2"`). With
12630/// `f` omitted, as many digits as uniquely identify the value are used
12631/// (`(123456).toExponential() === "1.23456e+5"`). Rounding is half away from zero
12632/// on the exact value, matching `toPrecision`.
12633fn to_exponential(n: f64, f: Option<usize>) -> String {
12634 if !n.is_finite() {
12635 return host::fmt_number(n);
12636 }
12637 let neg = n < 0.0;
12638 let a = n.abs();
12639 let (s, e) = if a == 0.0 {
12640 // Zero has no significant digits: emit "0" padded to the requested width.
12641 ("0".repeat(f.unwrap_or(0) + 1), 0)
12642 } else {
12643 match f {
12644 Some(f) => round_significant(a, f + 1),
12645 None => {
12646 // Shortest round-tripping digits (Rust's `{:e}` is shortest).
12647 let sci = format!("{a:e}");
12648 let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12649 let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
12650 let trimmed = digits.trim_end_matches('0');
12651 let digits = if trimmed.is_empty() { "0" } else { trimmed };
12652 (digits.to_string(), exp_str.parse().unwrap_or(0))
12653 }
12654 }
12655 };
12656 let sign = if e >= 0 { '+' } else { '-' };
12657 let mag = e.abs();
12658 let body = if s.len() == 1 {
12659 format!("{s}e{sign}{mag}")
12660 } else {
12661 format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12662 };
12663 if neg {
12664 format!("-{body}")
12665 } else {
12666 body
12667 }
12668}
12669
12670/// `Number.prototype.toPrecision(p)`: `p` significant digits, switching to
12671/// exponential form when the decimal exponent `e` satisfies `e < -6` or `e ≥ p`
12672/// (ECMAScript Number.prototype.toPrecision). Trailing zeros are significant and
12673/// retained (`(100).toPrecision(5) === "100.00"`).
12674fn to_precision(n: f64, p: usize) -> String {
12675 if !n.is_finite() {
12676 return host::fmt_number(n);
12677 }
12678 if n == 0.0 {
12679 return if p == 1 {
12680 "0".into()
12681 } else {
12682 format!("0.{}", "0".repeat(p - 1))
12683 };
12684 }
12685 let neg = n < 0.0;
12686 let (s, e) = round_significant(n.abs(), p);
12687 let pp = p as i32;
12688
12689 let body = if e < -6 || e >= pp {
12690 // Exponential: first digit, optional '.rest', signed exponent.
12691 let sign = if e >= 0 { '+' } else { '-' };
12692 let mag = e.abs();
12693 if p == 1 {
12694 format!("{s}e{sign}{mag}")
12695 } else {
12696 format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12697 }
12698 } else if e >= 0 {
12699 // e in 0..p-1: (e+1) integer digits, then any remaining as fraction.
12700 let ip = (e + 1) as usize;
12701 if ip == p {
12702 s
12703 } else {
12704 format!("{}.{}", &s[..ip], &s[ip..])
12705 }
12706 } else {
12707 // -6 ≤ e < 0: "0." then (−e−1) zeros then all p digits.
12708 format!("0.{}{}", "0".repeat((-e - 1) as usize), s)
12709 };
12710 if neg {
12711 format!("-{body}")
12712 } else {
12713 body
12714 }
12715}
12716
12717/// `Number.prototype.toString(radix)` for radix 2..=36 (radix 10 goes through
12718/// `fmt_number`). Faithful port of V8's `DoubleToRadixCString`: the integer part
12719/// is emitted exact, and fractional digits are produced up to the input double's
12720/// precision (terminating via a ULP-sized `delta`), with round-half-to-even and
12721/// carry-over back into already-written digits (and into the integer part).
12722fn to_radix(n: f64, radix: u32) -> String {
12723 if !n.is_finite() {
12724 return host::fmt_number(n);
12725 }
12726 let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
12727 let rf = radix as f64;
12728 let neg = n < 0.0;
12729 let value = n.abs();
12730
12731 let mut integer = value.floor();
12732 let mut fraction = value - integer;
12733
12734 // Fraction digits, most-significant first.
12735 let mut frac: Vec<u8> = Vec::new();
12736 // Only compute fractional digits down to the input double's precision.
12737 let mut delta = 0.5 * (next_up(value) - value);
12738 delta = delta.max(next_up(0.0));
12739 if fraction >= delta {
12740 loop {
12741 // Shift up by one digit.
12742 fraction *= rf;
12743 delta *= rf;
12744 let digit = fraction as usize;
12745 frac.push(digits[digit]);
12746 fraction -= digit as f64;
12747 // Round to even.
12748 if (fraction > 0.5 || (fraction == 0.5 && (digit & 1) == 1)) && fraction + delta > 1.0 {
12749 // Carry-over: back-trace already-written fraction digits.
12750 loop {
12751 match frac.pop() {
12752 None => {
12753 // Carried past the point into the integer part.
12754 integer += 1.0;
12755 break;
12756 }
12757 Some(c) => {
12758 let d = if c > b'9' {
12759 (c - b'a' + 10) as u32
12760 } else {
12761 (c - b'0') as u32
12762 };
12763 if d + 1 < radix {
12764 frac.push(digits[(d + 1) as usize]);
12765 break;
12766 }
12767 // digit was radix-1: drop it and keep carrying.
12768 }
12769 }
12770 }
12771 break;
12772 }
12773 if fraction < delta {
12774 break;
12775 }
12776 }
12777 }
12778
12779 // Integer digits, least-significant first (reversed at the end).
12780 let mut int_out: Vec<u8> = Vec::new();
12781 // For magnitudes ≥ 2^53, `fmod` loses low bits: pre-fill trailing zeros.
12782 while v8_exponent(integer / rf) > 0 {
12783 integer /= rf;
12784 int_out.push(b'0');
12785 }
12786 loop {
12787 let remainder = integer % rf;
12788 int_out.push(digits[remainder as usize]);
12789 integer = (integer - remainder) / rf;
12790 if integer <= 0.0 {
12791 break;
12792 }
12793 }
12794 int_out.reverse();
12795
12796 let mut out: Vec<u8> = Vec::new();
12797 if neg {
12798 out.push(b'-');
12799 }
12800 out.extend_from_slice(&int_out);
12801 if !frac.is_empty() {
12802 out.push(b'.');
12803 out.extend_from_slice(&frac);
12804 }
12805 String::from_utf8(out).unwrap()
12806}
12807
12808/// Next representable f64 above `x` (`x` finite, `x ≥ 0`) — V8's `NextDouble`.
12809fn next_up(x: f64) -> f64 {
12810 f64::from_bits(x.to_bits() + 1)
12811}
12812
12813/// V8's `Double::Exponent`: the binary exponent of the significand-scaled value
12814/// (`> 0` iff |x| ≥ 2^53). Used to detect integers past `fmod`'s exact range.
12815fn v8_exponent(x: f64) -> i32 {
12816 let biased = ((x.to_bits() >> 52) & 0x7ff) as i32;
12817 if biased == 0 {
12818 -1074 // denormal
12819 } else {
12820 biased - 1075
12821 }
12822}
12823
12824// ══ Map / Set / Symbol / generator methods ═══════════════════════════════════
12825
12826/// `Map.prototype.set` step 6 and `Set.prototype.add` step 4: a key of `-0` is
12827/// STORED as `+0`. `map_key` already treats the two as one key (SameValueZero),
12828/// but the value kept alongside it is what iteration and `console.log` report,
12829/// and node shows `0` there — `new Map().set(-0, 1)` renders `Map(1) { 0 => 1 }`.
12830fn normalize_zero_key(v: Value) -> Value {
12831 match v {
12832 Value::Float(f) if f == 0.0 && f.is_sign_negative() => Value::Float(0.0),
12833 other => other,
12834 }
12835}
12836
12837fn map_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
12838 match name {
12839 "get" => {
12840 let key = with_host(|h| host::map_key(h, &arg0(&args)));
12841 Ok(with_host(|h| match h.get(recv) {
12842 Some(JsObj::Map { entries, .. }) => entries
12843 .get(&key)
12844 .map(|(_, v)| v.clone())
12845 .unwrap_or(Value::Undef),
12846 _ => Value::Undef,
12847 }))
12848 }
12849 "set" => {
12850 let kv = normalize_zero_key(arg0(&args));
12851 let vv = args.get(1).cloned().unwrap_or(Value::Undef);
12852 reject_non_object_weak_key(recv, &kv, "WeakMap")?;
12853 let key = with_host(|h| host::map_key(h, &kv));
12854 with_host(|h| {
12855 if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12856 entries.insert(key, (kv, vv));
12857 }
12858 });
12859 Ok(recv.clone())
12860 }
12861 "has" => {
12862 let key = with_host(|h| host::map_key(h, &arg0(&args)));
12863 Ok(Value::Bool(with_host(
12864 |h| matches!(h.get(recv), Some(JsObj::Map { entries, .. }) if entries.contains_key(&key)),
12865 )))
12866 }
12867 "delete" => {
12868 let key = with_host(|h| host::map_key(h, &arg0(&args)));
12869 Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
12870 Some(JsObj::Map { entries, .. }) => entries.shift_remove(&key).is_some(),
12871 _ => false,
12872 })))
12873 }
12874 "clear" => {
12875 with_host(|h| {
12876 if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12877 entries.clear();
12878 }
12879 });
12880 Ok(Value::Undef)
12881 }
12882 "forEach" => {
12883 let cb = arg0(&args);
12884 let pairs: Vec<(Value, Value)> = with_host(|h| match h.get(recv) {
12885 Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
12886 _ => Vec::new(),
12887 });
12888 for (k, v) in pairs {
12889 host::invoke(&cb, vec![v, k, recv.clone()], this_arg(&args, 1))?;
12890 }
12891 Ok(Value::Undef)
12892 }
12893 // LIVE, not a snapshot: an entry added during iteration is visited and
12894 // one deleted before it is reached is not.
12895 "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
12896 recv,
12897 if name == "@@iterator" {
12898 "entries"
12899 } else {
12900 name
12901 },
12902 )),
12903 _ => Err(host::type_error(&format!("map.{name} is not a function"))),
12904 }
12905}
12906
12907/// A weak collection can only hold objects (and unregistered symbols) — a
12908/// primitive key is a `TypeError`, which is how packages probe for weak support.
12909fn reject_non_object_weak_key(recv: &Value, key: &Value, kind: &str) -> Result<(), String> {
12910 let weak = with_host(|h| {
12911 matches!(
12912 h.get(recv),
12913 Some(JsObj::Map { weak: true, .. }) | Some(JsObj::Set { weak: true, .. })
12914 )
12915 });
12916 if !weak {
12917 return Ok(());
12918 }
12919 let is_object = with_host(|h| match key {
12920 Value::Obj(_) => !h.is_null(key) && h.as_str(key).is_none() && h.as_bigint(key).is_none(),
12921 _ => false,
12922 });
12923 if is_object {
12924 return Ok(());
12925 }
12926 Err(host::type_error(if kind == "WeakMap" {
12927 "Invalid value used as weak map key"
12928 } else {
12929 "Invalid value used in weak set"
12930 }))
12931}
12932
12933/// A `Set`-like operand of the ES2025 set methods — 24.2.1.2 `GetSetRecord`.
12934///
12935/// The seven set operations do NOT require a real `Set` on the right-hand side:
12936/// anything with a numeric `size` and callable `has`/`keys` participates, which
12937/// is what lets a `Map`'s key view or a user-written set stand in. The reads
12938/// happen in this order (`size`, `has`, `keys`) and each failure has its own
12939/// diagnostic, so a bad operand reports which field was wrong rather than
12940/// failing later inside the iteration.
12941struct SetRecord {
12942 obj: Value,
12943 /// `size` truncated toward zero, as the spec's `intSize` is; the fractional
12944 /// part is dropped BEFORE the negative check, so `size: -0.5` truncates to
12945 /// `-0` and is accepted while `-1.5` reports `'-1' is an invalid size`.
12946 size: f64,
12947 has: Value,
12948 keys: Value,
12949}
12950
12951fn get_set_record(other: &Value, method: &str) -> Result<SetRecord, String> {
12952 if !with_host(|h| is_object_like(h, other)) {
12953 return Err(host::type_error(&format!(
12954 "Set.prototype.{method} argument must be an object"
12955 )));
12956 }
12957 let raw = get_property(other, "size")?;
12958 let num = host::to_number_value(&raw)?;
12959 if num.is_nan() {
12960 return Err(host::type_error("The .size property is NaN"));
12961 }
12962 let size = num.trunc();
12963 if size < 0.0 {
12964 return Err(host::range_error(&format!("'{size}' is an invalid size")));
12965 }
12966 let has = get_property(other, "has")?;
12967 if !with_host(|h| host::is_callable(h, &has)) {
12968 return Err(host::type_error("string \"has\" is not a function"));
12969 }
12970 let keys = get_property(other, "keys")?;
12971 if !with_host(|h| host::is_callable(h, &keys)) {
12972 return Err(host::type_error("string \"keys\" is not a function"));
12973 }
12974 Ok(SetRecord {
12975 obj: other.clone(),
12976 size,
12977 has,
12978 keys,
12979 })
12980}
12981
12982impl SetRecord {
12983 /// `Call(has, obj, [v])`, coerced to a boolean the way the spec's
12984 /// `ToBoolean(Call(...))` is — a set-like may answer with anything truthy.
12985 fn has(&self, v: &Value) -> Result<bool, String> {
12986 let r = host::invoke(&self.has, vec![v.clone()], Some(self.obj.clone()))?;
12987 Ok(with_host(|h| h.truthy(&r)))
12988 }
12989
12990 /// The operand's elements, drained from the iterator its `keys` method
12991 /// returns. A non-object result is the spec's `Result of the keys method is
12992 /// not an object`, reported before anything is iterated.
12993 fn keys(&self) -> Result<Vec<Value>, String> {
12994 let it = host::invoke(&self.keys, Vec::new(), Some(self.obj.clone()))?;
12995 if !with_host(|h| is_object_like(h, &it)) {
12996 return Err(host::type_error(
12997 "Result of the keys method is not an object",
12998 ));
12999 }
13000 host::drain_iterator(&it)
13001 }
13002}
13003
13004/// The receiver of a set operation must be a real (non-weak) `Set`: these seven
13005/// methods read `[[SetData]]` directly, so a look-alike cannot stand in on the
13006/// LEFT even though it can on the right.
13007fn require_set_receiver(recv: &Value, method: &str) -> Result<(), String> {
13008 if with_host(|h| matches!(h.get(recv), Some(JsObj::Set { weak: false, .. }))) {
13009 return Ok(());
13010 }
13011 Err(host::type_error(&format!(
13012 "Method Set.prototype.{method} called on incompatible receiver {}",
13013 with_host(|h| object_tag(h, recv))
13014 )))
13015}
13016
13017/// The receiver's elements, READ AT THE POINT THE SPEC READS THEM.
13018///
13019/// Every one of these operations copies `[[SetData]]` *after* it has touched
13020/// the operand — `union` and `symmetricDifference` call the operand's `keys`
13021/// first — so a `keys` (or a `has`) that mutates the receiver is visible in the
13022/// result. Snapshotting the receiver up front instead dropped such an element:
13023/// node's `s.union({ keys(){ s.add(99); … } })` contains `99`.
13024fn set_values(recv: &Value) -> Vec<Value> {
13025 with_host(|h| match h.get(recv) {
13026 Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13027 _ => Vec::new(),
13028 })
13029}
13030
13031fn set_size(recv: &Value) -> f64 {
13032 with_host(|h| match h.get(recv) {
13033 Some(JsObj::Set { entries, .. }) => entries.len() as f64,
13034 _ => 0.0,
13035 })
13036}
13037
13038/// A fresh, ordinary `Set`. The set operations are NOT species-aware: on node
13039/// `class S extends Set {}`, `new S([1]).union(other).constructor` is `Set`.
13040fn new_set(items: Vec<Value>) -> Result<Value, String> {
13041 let s = with_host(|h| {
13042 h.alloc(JsObj::Set {
13043 entries: IndexMap::new(),
13044 weak: false,
13045 })
13046 });
13047 for v in items {
13048 set_method(&s, "add", vec![v])?;
13049 }
13050 Ok(s)
13051}
13052
13053fn set_contains(s: &Value, v: &Value) -> bool {
13054 let key = with_host(|h| host::map_key(h, v));
13055 with_host(
13056 |h| matches!(h.get(s), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13057 )
13058}
13059
13060/// The seven ES2025 set operations (24.2.4.3, .8, .5, .16, .10, .12, .7).
13061///
13062/// Each one branches on the two sizes and iterates the SMALLER side — not an
13063/// optimization but observable behaviour: which side is walked decides the
13064/// result's order and whether the operand's `has` or its `keys` is the method
13065/// that runs. `intersection` of a 3-element receiver with a 2-element operand
13066/// yields the operand's order, and its `keys` (never its `has`) is called.
13067fn set_operation(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13068 require_set_receiver(recv, name)?;
13069 let other = get_set_record(&arg0(&args), name)?;
13070 let my_size = set_size(recv);
13071 match name {
13072 "union" => {
13073 let keys = other.keys()?;
13074 let mut out = set_values(recv);
13075 out.extend(keys);
13076 new_set(out)
13077 }
13078 "intersection" => {
13079 let mut out = Vec::new();
13080 if my_size <= other.size {
13081 for v in set_values(recv) {
13082 if other.has(&v)? {
13083 out.push(v);
13084 }
13085 }
13086 } else {
13087 for k in other.keys()? {
13088 if set_contains(recv, &k) {
13089 out.push(k);
13090 }
13091 }
13092 }
13093 new_set(out)
13094 }
13095 "difference" => {
13096 if my_size <= other.size {
13097 let mut out = Vec::new();
13098 for v in set_values(recv) {
13099 if !other.has(&v)? {
13100 out.push(v);
13101 }
13102 }
13103 return new_set(out);
13104 }
13105 let out = new_set(set_values(recv))?;
13106 for k in other.keys()? {
13107 set_method(&out, "delete", vec![k])?;
13108 }
13109 Ok(out)
13110 }
13111 "symmetricDifference" => {
13112 // The operand is drained FIRST — the spec takes the iterator before
13113 // it copies `[[SetData]]`, so a `keys` that mutates the receiver is
13114 // reflected in the result.
13115 let keys = other.keys()?;
13116 let out = new_set(set_values(recv))?;
13117 for k in keys {
13118 if set_contains(recv, &k) {
13119 set_method(&out, "delete", vec![k])?;
13120 } else {
13121 set_method(&out, "add", vec![k])?;
13122 }
13123 }
13124 Ok(out)
13125 }
13126 "isSubsetOf" => {
13127 if my_size > other.size {
13128 return Ok(Value::Bool(false));
13129 }
13130 for v in set_values(recv) {
13131 if !other.has(&v)? {
13132 return Ok(Value::Bool(false));
13133 }
13134 }
13135 Ok(Value::Bool(true))
13136 }
13137 "isSupersetOf" => {
13138 if my_size < other.size {
13139 return Ok(Value::Bool(false));
13140 }
13141 for k in other.keys()? {
13142 if !set_contains(recv, &k) {
13143 return Ok(Value::Bool(false));
13144 }
13145 }
13146 Ok(Value::Bool(true))
13147 }
13148 "isDisjointFrom" => {
13149 if my_size <= other.size {
13150 for v in set_values(recv) {
13151 if other.has(&v)? {
13152 return Ok(Value::Bool(false));
13153 }
13154 }
13155 } else {
13156 for k in other.keys()? {
13157 if set_contains(recv, &k) {
13158 return Ok(Value::Bool(false));
13159 }
13160 }
13161 }
13162 Ok(Value::Bool(true))
13163 }
13164 _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13165 }
13166}
13167
13168fn set_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13169 match name {
13170 "add" => {
13171 let vv = normalize_zero_key(arg0(&args));
13172 reject_non_object_weak_key(recv, &vv, "WeakSet")?;
13173 let key = with_host(|h| host::map_key(h, &vv));
13174 with_host(|h| {
13175 if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13176 entries.insert(key, vv);
13177 }
13178 });
13179 Ok(recv.clone())
13180 }
13181 "has" => {
13182 let key = with_host(|h| host::map_key(h, &arg0(&args)));
13183 Ok(Value::Bool(with_host(
13184 |h| matches!(h.get(recv), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13185 )))
13186 }
13187 "delete" => {
13188 let key = with_host(|h| host::map_key(h, &arg0(&args)));
13189 Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
13190 Some(JsObj::Set { entries, .. }) => entries.shift_remove(&key).is_some(),
13191 _ => false,
13192 })))
13193 }
13194 "clear" => {
13195 with_host(|h| {
13196 if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13197 entries.clear();
13198 }
13199 });
13200 Ok(Value::Undef)
13201 }
13202 "forEach" => {
13203 let cb = arg0(&args);
13204 let vals: Vec<Value> = with_host(|h| match h.get(recv) {
13205 Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13206 _ => Vec::new(),
13207 });
13208 for v in vals {
13209 host::invoke(&cb, vec![v.clone(), v, recv.clone()], this_arg(&args, 1))?;
13210 }
13211 Ok(Value::Undef)
13212 }
13213 "union"
13214 | "intersection"
13215 | "difference"
13216 | "symmetricDifference"
13217 | "isSubsetOf"
13218 | "isSupersetOf"
13219 | "isDisjointFrom" => set_operation(recv, name, args),
13220 // LIVE, as for `Map`. A Set's `keys` and `values` are the same thing.
13221 "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
13222 recv,
13223 if name == "entries" {
13224 "entries"
13225 } else {
13226 "values"
13227 },
13228 )),
13229 _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13230 }
13231}
13232
13233fn generator_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13234 // A generator IS its own iterator: both symbol forms return the receiver.
13235 if matches!(name, "@@iterator" | "@@asyncIterator") {
13236 return Ok(recv.clone());
13237 }
13238 // An `async function*` object's methods return PROMISES of the record, and
13239 // its body has to be driven through the await-aware stepper (a plain
13240 // `gen_resume` would surface an internal `await` suspension as a bogus yield).
13241 if host::is_async_generator(recv) {
13242 // All three go through `[[AsyncGeneratorQueue]]` (ECMA-262 27.6.3.6):
13243 // `.return`/`.throw` must wait behind a `.next()` that is still
13244 // suspended on an internal `await`, or that `.next()` would report
13245 // `{done: true}` for a value the body had not yet reached. An uncaught
13246 // `.throw(e)` rejects the returned promise; it does not throw here.
13247 return match name {
13248 "next" => Ok(host::async_gen_enqueue(
13249 recv,
13250 host::GenReq::Next(arg0(&args)),
13251 )),
13252 "return" => Ok(host::async_gen_enqueue(
13253 recv,
13254 host::GenReq::Return(arg0(&args)),
13255 )),
13256 "throw" => Ok(host::async_gen_enqueue(
13257 recv,
13258 host::GenReq::Throw(arg0(&args)),
13259 )),
13260 "@@asyncIterator" => Ok(recv.clone()),
13261 _ => Err(host::type_error(&format!(
13262 "asyncGenerator.{name} is not a function"
13263 ))),
13264 };
13265 }
13266 match name {
13267 "next" => {
13268 let send = arg0(&args);
13269 match host::gen_resume(recv, send)? {
13270 host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13271 host::GenStep::Done(v) => Ok(iter_result(v, true)),
13272 }
13273 }
13274 "return" => {
13275 // Resume with an injected return so any pending `finally` runs; the
13276 // completion may itself be a `finally` yield (not-done) or the value.
13277 match host::gen_return(recv, arg0(&args))? {
13278 host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13279 host::GenStep::Done(v) => Ok(iter_result(v, true)),
13280 }
13281 }
13282 "throw" => {
13283 // Inject a throw at the suspension point: an enclosing `try/catch` in
13284 // the body can handle it (and any `finally` runs); otherwise it
13285 // propagates to the caller.
13286 match host::gen_throw(recv, arg0(&args))? {
13287 host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13288 host::GenStep::Done(v) => Ok(iter_result(v, true)),
13289 }
13290 }
13291 _ => Err(host::type_error(&format!(
13292 "generator.{name} is not a function"
13293 ))),
13294 }
13295}
13296
13297/// A `{ value, done }` iterator-result object.
13298fn iter_result(value: Value, done: bool) -> Value {
13299 with_host(|h| {
13300 let mut m: IndexMap<String, Value> = IndexMap::new();
13301 m.insert("value".into(), value);
13302 m.insert("done".into(), Value::Bool(done));
13303 h.new_object(m)
13304 })
13305}
13306
13307/// A live iterator over array `arr` — what `keys()`, `values()`, `entries()`
13308/// and `Symbol.iterator` return, and what a `for-of` over an array steps.
13309pub(crate) fn array_iterator(arr: &Value, kind: host::ArrayIterKind) -> Value {
13310 with_host(|h| {
13311 h.alloc(JsObj::Iter {
13312 items: Vec::new(),
13313 idx: 0,
13314 array: Some((arr.clone(), kind)),
13315 })
13316 })
13317}
13318
13319/// One step of a `JsObj::Iter`: `None` when `it` is not one, `Some(None)` once
13320/// it is exhausted, otherwise the next value.
13321///
13322/// An array iterator reads the array at every step (23.1.5.1
13323/// `%ArrayIteratorPrototype%.next`): the length is re-read, so an element
13324/// pushed during a `for-of` is visited and one popped is not, and the element
13325/// is read as `a[i]` reads it — an accessor runs, a hole reads through the
13326/// prototype. Once it reports done it stays done, even if the array grows.
13327pub(crate) fn iter_step(it: &Value) -> Option<Option<Value>> {
13328 use host::ArrayIterKind;
13329 // One host borrow for the common case — a snapshot, or an array slot that
13330 // is neither a hole nor an accessor. `Err` carries what only `[[Get]]` can
13331 // read: the array, the kind and the index.
13332 let step = with_host(|h| {
13333 let (arr, kind, i) = match h.get_mut(it) {
13334 Some(JsObj::Iter {
13335 items,
13336 idx,
13337 array: None,
13338 }) => {
13339 let v = items.get(*idx).cloned();
13340 if v.is_some() {
13341 *idx += 1;
13342 }
13343 return Some(Ok(v));
13344 }
13345 Some(JsObj::Iter {
13346 idx,
13347 array: Some((arr, kind)),
13348 ..
13349 }) => (arr.clone(), *kind, *idx),
13350 _ => return None,
13351 };
13352 // `usize::MAX` marks an iterator that has already reported done, and
13353 // it stays done even if the array grows.
13354 let len = match h.get(&arr) {
13355 Some(JsObj::Array(items)) => items.len(),
13356 _ => 0,
13357 };
13358 let done = i == usize::MAX || i >= len;
13359 if let Some(JsObj::Iter { idx, .. }) = h.get_mut(it) {
13360 *idx = if done { usize::MAX } else { i + 1 };
13361 }
13362 if done {
13363 return Some(Ok(None));
13364 }
13365 let key = Value::Float(i as f64);
13366 let slot = match (kind, h.get(&arr)) {
13367 (ArrayIterKind::Keys, _) => return Some(Ok(Some(key))),
13368 (_, Some(JsObj::Array(items)))
13369 if !h.is_hole(&arr, i) && h.own_accessor_keys(&arr).is_empty() =>
13370 {
13371 items[i].clone()
13372 }
13373 _ => return Some(Err((arr, kind, i))),
13374 };
13375 Some(Ok(Some(match kind {
13376 ArrayIterKind::Entries => h.new_array(vec![key, slot]),
13377 _ => slot,
13378 })))
13379 })?;
13380 let (arr, kind, i) = match step {
13381 Ok(step) => return Some(step),
13382 Err(slow) => slow,
13383 };
13384 let value = get_property(&arr, &i.to_string()).unwrap_or(Value::Undef);
13385 Some(Some(match kind {
13386 ArrayIterKind::Entries => with_host(|h| h.new_array(vec![Value::Float(i as f64), value])),
13387 _ => value,
13388 }))
13389}
13390
13391/// Built-in iterator object (`arr.values()`, `arr[Symbol.iterator]()`): a
13392/// cursor over a snapshot, or live over an array ([`iter_step`]).
13393fn iter_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13394 match name {
13395 "next" => Ok(match iter_step(recv).flatten() {
13396 Some(v) => iter_result(v, false),
13397 None => iter_result(Value::Undef, true),
13398 }),
13399 "return" => {
13400 // Exhaust the cursor and report done.
13401 with_host(|h| {
13402 if let Some(JsObj::Iter { items, idx, array }) = h.get_mut(recv) {
13403 *idx = if array.is_some() {
13404 usize::MAX
13405 } else {
13406 items.len()
13407 };
13408 }
13409 });
13410 Ok(iter_result(arg0(&args), true))
13411 }
13412 // An iterator is its own iterable.
13413 "@@iterator" => Ok(recv.clone()),
13414 _ => Err(host::type_error(&format!(
13415 "iterator.{name} is not a function"
13416 ))),
13417 }
13418}
13419
13420fn symbol_method(recv: &Value, name: &str, _args: Vec<Value>) -> Result<Value, String> {
13421 match name {
13422 "toString" => Ok(with_host(|h| {
13423 let s = h.str_of(recv);
13424 h.new_str(s)
13425 })),
13426 // 20.4.3.5: `Symbol.prototype[@@toPrimitive]` returns the symbol
13427 // itself for EVERY hint — it ignores its argument. That is what makes
13428 // `sym + ''` a TypeError rather than a concatenation: the conversion
13429 // succeeds and hands back a symbol, and it is `+` that then rejects it.
13430 "@@toPrimitive" | "valueOf" => Ok(recv.clone()),
13431 _ => Err(host::type_error(&format!(
13432 "symbol.{name} is not a function"
13433 ))),
13434 }
13435}
13436
13437// ══ Object.* prototype helpers, `in`, deep clone ═════════════════════════════
13438
13439fn object_create(args: Vec<Value>) -> Result<Value, String> {
13440 let proto = arg0(&args);
13441 // 20.1.2.2 step 1: the prototype must be an Object or exactly `null`.
13442 // `undefined` is NOT accepted — measured on node v26.7.0,
13443 // `Object.create(undefined)` is
13444 // `TypeError: Object prototype may only be an Object or null: undefined`,
13445 // where node-js quietly built a normal object.
13446 reject_bad_prototype(&proto)?;
13447 let obj = with_host(|h| h.new_object(IndexMap::new()));
13448 // `set_proto` records a null proto as an explicit null-prototype object.
13449 with_host(|h| h.set_proto(&obj, proto));
13450 // Optional second arg: a property-descriptor map.
13451 if let Some(descs) = args.get(1).filter(|d| !matches!(d, Value::Undef)) {
13452 let entries: Vec<(String, Value)> = with_host(|h| match h.get(descs) {
13453 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
13454 _ => Vec::new(),
13455 });
13456 for (k, d) in entries {
13457 apply_descriptor(&obj, &k, &d)?;
13458 }
13459 }
13460 Ok(obj)
13461}
13462
13463/// The enumerable method names of a builtin `<Ctor>.prototype` namespace that
13464/// supports being copied via `mixin`/`getOwnPropertyNames`. Currently only
13465/// `EventEmitter.prototype` (the one express mixes onto its app function).
13466/// The own property names of `<Ctor>.prototype`, and whether each is
13467/// enumerable, from the generated [`crate::arity::PROTO_MEMBERS`] table.
13468///
13469/// `Object.getOwnPropertyNames(Map.prototype)` answered `[]` for every
13470/// intrinsic — the members are reachable by NAME through the `@proto:` thunks
13471/// but were not enumerable, so feature detection that walks a prototype found
13472/// nothing there. The table is read from the reference engine rather than
13473/// derived from the arity table because the arity table holds functions only:
13474/// `Map.prototype.size`, `RegExp.prototype.source` and the twelve
13475/// `URL.prototype` components are accessors.
13476fn intrinsic_proto_members(ns: &str) -> Option<&'static [&'static str]> {
13477 let ctor = ns.strip_suffix(".prototype")?;
13478 crate::arity::PROTO_MEMBERS
13479 .binary_search_by(|(k, _)| (*k).cmp(ctor))
13480 .ok()
13481 .map(|i| crate::arity::PROTO_MEMBERS[i].1)
13482}
13483
13484fn builtin_proto_method_names(ns: &str) -> Option<&'static [&'static str]> {
13485 match ns {
13486 "EventEmitter.prototype" => Some(crate::stdlib::events::METHODS),
13487 _ => None,
13488 }
13489}
13490
13491/// The own SYMBOL-keyed property keys of `v` as symbol values. A Proxy's come
13492/// from its `ownKeys` trap (the symbol half of the same list the string keys are
13493/// filtered out of); every other receiver answers from its property map.
13494fn proxy_or_own_symbol_keys(v: &Value) -> Result<Vec<Value>, String> {
13495 if let Some(keys) = crate::proxy::own_keys(v)? {
13496 return Ok(keys
13497 .iter()
13498 .filter(|k| host::is_symbol_key(k))
13499 .map(|k| crate::proxy::key_value(k))
13500 .collect());
13501 }
13502 // An intrinsic prototype's symbol-keyed members come from the generated
13503 // table, which is the only record of them: they own no map entry, so
13504 // `Object.getOwnPropertySymbols(Array.prototype)` was `[]` where node
13505 // reports `Symbol.iterator` and `Symbol.unscopables`.
13506 if let Some(ns) = intrinsic_proto_of(v).map(|c| format!("{c}.prototype")) {
13507 if let Some(members) = intrinsic_proto_members(&ns) {
13508 return Ok(with_host(|h| {
13509 members
13510 .iter()
13511 .filter_map(|m| m.strip_prefix('+').unwrap_or(m).strip_prefix("@@"))
13512 .map(|name| h.well_known_symbol(name))
13513 .collect()
13514 }));
13515 }
13516 }
13517 Ok(with_host(|h| h.own_symbol_keys(v)))
13518}
13519
13520/// `[[DefineOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13521pub fn define_property_pub(obj: &Value, key: Value, desc: Value) -> Result<Value, String> {
13522 object_define_property(vec![obj.clone(), key, desc])
13523}
13524
13525/// `[[GetOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13526pub fn own_descriptor_pub(obj: &Value, key: Value) -> Result<Value, String> {
13527 object_get_own_descriptor(vec![obj.clone(), key])
13528}
13529
13530fn object_define_property(args: Vec<Value>) -> Result<Value, String> {
13531 let obj = arg0(&args);
13532 // A Proxy defines through its `defineProperty` trap; the target it forwards
13533 // to is where the ordinary path below finally runs.
13534 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
13535 let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13536 let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13537 if !with_host(|h| is_object_like(h, &desc)) {
13538 return Err(host::type_error(&format!(
13539 "Property description must be an object: {}",
13540 with_host(|h| h.str_of(&desc))
13541 )));
13542 }
13543 // `Object.defineProperty` THROWS on a refusing trap — in sloppy code
13544 // too. `Reflect.defineProperty` is the form that reports `false`.
13545 if !crate::proxy::define_property(&obj, &key, &desc)? {
13546 return Err(host::type_error(&format!(
13547 "'defineProperty' on proxy: trap returned falsish for property '{key}'"
13548 )));
13549 }
13550 return Ok(obj);
13551 }
13552 // 20.1.2.4 steps 1-3, both of which node-js skipped entirely: a non-object
13553 // target and a non-object descriptor each throw before anything is written.
13554 if !with_host(|h| is_object_like(h, &obj)) {
13555 return Err(host::type_error(
13556 "Object.defineProperty called on non-object",
13557 ));
13558 }
13559 let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13560 if !with_host(|h| is_object_like(h, &desc)) {
13561 return Err(host::type_error(&format!(
13562 "Property description must be an object: {}",
13563 with_host(|h| h.str_of(&desc))
13564 )));
13565 }
13566 let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13567 apply_descriptor(&obj, &key, &desc)?;
13568 Ok(obj)
13569}
13570
13571/// Every `Reflect` method requires an OBJECT target and reports a `TypeError`
13572/// for anything else (28.1). A primitive was being accepted and silently
13573/// producing nothing.
13574/// `CreateListFromArrayLike` (7.3.18) — the argument list `Reflect.apply` and
13575/// `Reflect.construct` take.
13576///
13577/// An ARRAY-LIKE counts: `{length: 2, 0: 1, 1: 5}` is a two-element list. The
13578/// iterator was being used instead, so an array-like produced nothing and a
13579/// primitive produced nothing rather than the TypeError node raises.
13580/// Whether `p` is ALREADY `obj`'s prototype — the one case a non-extensible
13581/// object still accepts, because it changes nothing.
13582///
13583/// The observable prototype, not the stored link: an ordinary object has no
13584/// explicit link and inherits `Object.prototype`, so comparing the raw slot
13585/// reported "different" for `setPrototypeOf(frozen, Object.prototype)`.
13586/// Whether making `p` the prototype of `obj` would create a CYCLE — 10.1.2.1
13587/// step 8 walks up from `p` looking for `obj`.
13588///
13589/// Without the check `Object.setPrototypeOf(a, b)` followed by the reverse
13590/// built a ring. Nothing hung, because every chain walk in this host carries a
13591/// hop limit, but a lookup then silently gave up instead of finding a property
13592/// that really was there.
13593fn would_cycle(obj: &Value, p: &Value) -> bool {
13594 let mut cur = Some(p.clone());
13595 for _ in 0..1000 {
13596 let Some(c) = cur else { return false };
13597 if with_host(|h| h.strict_eq(&c, obj)) {
13598 return true;
13599 }
13600 // A PROXY's prototype is its handler's business; the spec skips the
13601 // walk entirely when one is in the chain.
13602 if with_host(|h| h.kind_of(&c)) == Some(ObjKind::Proxy) {
13603 return false;
13604 }
13605 cur = with_host(|h| h.proto_of(&c));
13606 }
13607 false
13608}
13609
13610fn same_prototype(obj: &Value, p: &Value) -> bool {
13611 let cur = prototype_of(obj);
13612 with_host(|h| h.strict_eq(&cur, p) || (h.is_null(&cur) && h.is_null(p)))
13613}
13614
13615fn create_list_from_array_like(v: &Value) -> Result<Vec<Value>, String> {
13616 if !with_host(|h| is_object_like(h, v)) {
13617 return Err(host::type_error(
13618 "CreateListFromArrayLike called on non-object",
13619 ));
13620 }
13621 let len = get_property(v, "length")?;
13622 let n = with_host(|h| h.to_number(&len));
13623 let n = if n.is_finite() && n > 0.0 {
13624 n as usize
13625 } else {
13626 0
13627 };
13628 (0..n).map(|i| get_property(v, &i.to_string())).collect()
13629}
13630
13631fn reflect_require_object(v: &Value, method: &str) -> Result<(), String> {
13632 if with_host(|h| is_object_like(h, v)) {
13633 return Ok(());
13634 }
13635 Err(host::type_error(&format!(
13636 "Reflect.{method} called on non-object"
13637 )))
13638}
13639
13640/// Whether `v` is an Object in the language sense — anything `typeof` calls
13641/// `"object"` (bar `null`) or `"function"`. Used by the argument checks that
13642/// distinguish "an object" from a primitive.
13643fn is_object_like(h: &host::JsHost, v: &Value) -> bool {
13644 matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v)
13645}
13646
13647/// `RequireObjectCoercible(v)` — 7.2.1. The check in front of every `ToObject`,
13648/// which node-js was missing on the whole `Object.keys`/`values`/`entries`/
13649/// `getOwnPropertyNames`/`getOwnPropertySymbols`/`getOwnPropertyDescriptor`/
13650/// `assign` family: each returned an empty result for `null` where node v26.7.0
13651/// throws `TypeError: Cannot convert undefined or null to object`. A PRIMITIVE
13652/// is coercible and keeps working (`Object.keys(1)` is `[]`).
13653fn require_object_coercible(v: &Value) -> Result<(), String> {
13654 if with_host(|h| matches!(v, Value::Undef) || h.is_null(v)) {
13655 return Err(host::type_error(
13656 "Cannot convert undefined or null to object",
13657 ));
13658 }
13659 Ok(())
13660}
13661
13662/// 10.1.2 / 20.1.2.2 step 1: reject a `[[Prototype]]` that is neither an Object
13663/// nor `null`, with V8's wording. Measured on node v26.7.0:
13664/// `Object.create("s")` is
13665/// `TypeError: Object prototype may only be an Object or null: s`.
13666fn reject_bad_prototype(proto: &Value) -> Result<(), String> {
13667 if with_host(|h| h.is_null(proto) || is_object_like(h, proto)) {
13668 return Ok(());
13669 }
13670 Err(host::type_error(&format!(
13671 "Object prototype may only be an Object or null: {}",
13672 with_host(|h| h.str_of(proto))
13673 )))
13674}
13675
13676/// Apply a `{ value | get | set }` descriptor object to `obj[key]`.
13677///
13678/// Per ECMAScript `ToPropertyDescriptor`, an omitted `writable`/`enumerable`/
13679/// `configurable` field defaults to **false** — which is why a `defineProperty`
13680/// data property is invisible to `Object.keys` unless the caller opts in. That
13681/// asymmetry against plain assignment is the whole reason the attribute table
13682/// exists.
13683/// The requested fields of a property descriptor — 10.1.6.2
13684/// `ToPropertyDescriptor`. Each is `None` when the descriptor omits it, which
13685/// is the distinction the merge below turns on: an omitted field LEAVES an
13686/// existing attribute alone rather than resetting it.
13687struct Requested {
13688 value: Option<Value>,
13689 get: Option<Option<Value>>,
13690 set: Option<Option<Value>>,
13691 writable: Option<bool>,
13692 enumerable: Option<bool>,
13693 configurable: Option<bool>,
13694}
13695
13696impl Requested {
13697 /// Reads through the prototype chain, as `ToPropertyDescriptor`'s
13698 /// `HasProperty`/`Get` pairs do — a descriptor built with
13699 /// `Object.create({ value: 1 })` is legal.
13700 fn read(desc: &Value) -> Self {
13701 let has = |k: &str| {
13702 with_host(|h| {
13703 host::lookup_chain(h, desc, k).is_some()
13704 || host::lookup_accessor(h, desc, k).is_some()
13705 })
13706 };
13707 let val = |k: &str| get_property(desc, k).unwrap_or(Value::Undef);
13708 // Resolve the value BEFORE the borrow: `val` re-enters the host, and
13709 // doing it inside the `with_host` closure aborts on the double borrow.
13710 let flag = |k: &str| {
13711 has(k).then(|| {
13712 let v = val(k);
13713 with_host(|h| h.truthy(&v))
13714 })
13715 };
13716 Requested {
13717 value: has("value").then(|| val("value")),
13718 get: has("get").then(|| match val("get") {
13719 Value::Undef => None,
13720 g => Some(g),
13721 }),
13722 set: has("set").then(|| match val("set") {
13723 Value::Undef => None,
13724 st => Some(st),
13725 }),
13726 writable: flag("writable"),
13727 enumerable: flag("enumerable"),
13728 configurable: flag("configurable"),
13729 }
13730 }
13731
13732 fn is_accessor(&self) -> bool {
13733 self.get.is_some() || self.set.is_some()
13734 }
13735
13736 fn is_data(&self) -> bool {
13737 self.value.is_some() || self.writable.is_some()
13738 }
13739}
13740
13741/// The own property already at `key`, if any, read back through
13742/// `Object.getOwnPropertyDescriptor` so every object kind (array indices, the
13743/// fn-prop side table, Buffer bytes) is covered by one code path.
13744struct Existing {
13745 accessor: bool,
13746 value: Value,
13747 get: Option<Value>,
13748 set: Option<Value>,
13749 writable: bool,
13750 enumerable: bool,
13751 configurable: bool,
13752}
13753
13754fn existing_property(obj: &Value, key: &str) -> Option<Existing> {
13755 let k = with_host(|h| h.new_str(key.to_string()));
13756 let d = own_descriptor_pub(obj, k).ok()?;
13757 if matches!(d, Value::Undef) {
13758 return None;
13759 }
13760 let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
13761 let truthy = |n: &str| {
13762 let v = field(n);
13763 with_host(|h| h.truthy(&v))
13764 };
13765 let accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
13766 Some(Existing {
13767 accessor,
13768 value: field("value"),
13769 get: match field("get") {
13770 Value::Undef => None,
13771 g => Some(g),
13772 },
13773 set: match field("set") {
13774 Value::Undef => None,
13775 st => Some(st),
13776 },
13777 writable: truthy("writable"),
13778 enumerable: truthy("enumerable"),
13779 configurable: truthy("configurable"),
13780 })
13781}
13782
13783/// SameValue (7.2.11) — `===` except that `NaN` equals itself and `+0` and
13784/// `-0` are distinct. 10.1.6.3 compares a redefined value against the current
13785/// one with this, not with strict equality.
13786pub(crate) fn same_value(a: &Value, b: &Value) -> bool {
13787 let num = |v: &Value| match v {
13788 Value::Int(n) => Some(*n as f64),
13789 Value::Float(f) => Some(*f),
13790 _ => None,
13791 };
13792 match (num(a), num(b)) {
13793 (Some(x), Some(y)) => {
13794 if x.is_nan() && y.is_nan() {
13795 true
13796 } else if x == 0.0 && y == 0.0 {
13797 x.is_sign_negative() == y.is_sign_negative()
13798 } else {
13799 x == y
13800 }
13801 }
13802 _ => with_host(|h| h.strict_eq(a, b)),
13803 }
13804}
13805
13806/// 10.1.6.3 `ValidateAndApplyPropertyDescriptor`.
13807///
13808/// None of the validation existed: every `Object.defineProperty` was applied
13809/// unconditionally, so redefining a non-configurable property silently
13810/// succeeded where node throws. Worse in practice, an OMITTED field was read as
13811/// `false` rather than "leave alone", so the ordinary
13812/// `Object.defineProperty(o, 'k', { enumerable: false })` also stripped
13813/// `writable` and `configurable` from a property that had both.
13814///
13815/// Converting an accessor to a data property did not take effect at all: the
13816/// value was written but the accessor stayed in its side table, and accessors
13817/// win on read, so the getter kept answering.
13818fn apply_descriptor(obj: &Value, key: &str, desc: &Value) -> Result<(), String> {
13819 let req = Requested::read(desc);
13820 let cur = existing_property(obj, key);
13821
13822 // An array's `length` is the exotic own property whose write resizes the
13823 // array (10.4.2.1); routing it through the ordinary path stored a shadowing
13824 // key and left the elements untouched.
13825 if key == "length" && with_host(|h| h.kind_of(obj)) == Some(ObjKind::Array) {
13826 if let Some(v) = req.value.clone() {
13827 return set_property_pub(obj, "length", v);
13828 }
13829 }
13830
13831 // The other exotics whose own properties are SYNTHESIZED rather than stored
13832 // in a property map: a typed array's elements and a RegExp's `lastIndex`.
13833 // The ordinary path below writes a shadowing map entry the read never
13834 // consults, so `Object.defineProperty(u8, '0', {value: 9})` left `u8[0]`
13835 // unchanged.
13836 // A builtin namespace/prototype has no property map either, so a data
13837 // descriptor has to reach the same side table an assignment does.
13838 // `Object.defineProperty(Array.prototype, 'at', {value: impl})` — how a
13839 // careful polyfill installs itself, precisely to avoid the enumerable
13840 // property a bare assignment creates — wrote a map entry nothing read.
13841 if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Builtin) {
13842 if let Some(v) = req.value.clone() {
13843 return set_property_pub(obj, key, v);
13844 }
13845 }
13846 let exotic_own = (crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray")
13847 && key.parse::<usize>().is_ok())
13848 || (key == "lastIndex" && with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))));
13849 if exotic_own {
13850 if let Some(v) = req.value.clone() {
13851 return set_property_pub(obj, key, v);
13852 }
13853 }
13854
13855 // 10.1.6.3 step 2: a NEW property cannot be added to a non-extensible
13856 // object. Only an existing property's attributes were being validated, so
13857 // `defineProperty(Object.freeze({}), 'z', …)` silently added one.
13858 if cur.is_none() && !with_host(|h| h.is_extensible(obj)) {
13859 return Err(host::type_error(&format!(
13860 "Cannot define property {key}, object is not extensible"
13861 )));
13862 }
13863 if let Some(c) = &cur {
13864 if !c.configurable {
13865 let rejected = req.configurable == Some(true)
13866 || req.enumerable.is_some_and(|e| e != c.enumerable)
13867 || (req.is_accessor() && !c.accessor)
13868 || (req.is_data() && c.accessor)
13869 || (c.accessor
13870 && ((req.get.is_some() && req.get.clone().flatten() != c.get)
13871 || (req.set.is_some() && req.set.clone().flatten() != c.set)))
13872 || (!c.accessor
13873 && !c.writable
13874 && (req.writable == Some(true)
13875 || req.value.as_ref().is_some_and(|v| !same_value(v, &c.value))));
13876 if rejected {
13877 return Err(host::type_error(&format!(
13878 "Cannot redefine property: {key}"
13879 )));
13880 }
13881 }
13882 }
13883
13884 // An omitted field keeps what the property already had; a brand-new
13885 // property defaults every one of them to false.
13886 let attrs = host::PropAttrs {
13887 writable: req
13888 .writable
13889 .unwrap_or(cur.as_ref().is_some_and(|c| c.writable)),
13890 enumerable: req
13891 .enumerable
13892 .unwrap_or(cur.as_ref().is_some_and(|c| c.enumerable)),
13893 configurable: req
13894 .configurable
13895 .unwrap_or(cur.as_ref().is_some_and(|c| c.configurable)),
13896 };
13897 with_host(|h| h.set_prop_attrs(obj, key, attrs));
13898
13899 if req.is_accessor() {
13900 let get = req
13901 .get
13902 .clone()
13903 .unwrap_or_else(|| cur.as_ref().and_then(|c| c.get.clone()));
13904 let set = req
13905 .set
13906 .clone()
13907 .unwrap_or_else(|| cur.as_ref().and_then(|c| c.set.clone()));
13908 // An ACCESSOR at an index past the end still extends the array
13909 // (10.4.2.1): `Object.defineProperty([1], '4', {get})` gives
13910 // `length === 5` with holes between. Only the DATA path grew it, so
13911 // the accessor landed in the side table while `length` stayed put —
13912 // and with it out of range, `Object.keys` and `JSON.stringify` never
13913 // saw the index at all.
13914 if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>())
13915 {
13916 with_host(|h| {
13917 let old_len = match h.get(obj) {
13918 Some(JsObj::Array(items)) => items.len(),
13919 _ => 0,
13920 };
13921 if i >= old_len {
13922 if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13923 items.resize(i + 1, Value::Undef);
13924 }
13925 h.mark_hole_range(obj, old_len..i + 1);
13926 }
13927 });
13928 }
13929 with_host(|h| h.set_accessor(obj, key, get, set));
13930 return Ok(());
13931 }
13932
13933 if let Some(c) = &cur {
13934 if c.accessor {
13935 if !req.is_data() {
13936 // A generic descriptor — flags only — leaves an accessor an
13937 // accessor. They were already applied above.
13938 return Ok(());
13939 }
13940 let v = req.value.clone().unwrap_or(Value::Undef);
13941 with_host(|h| h.accessor_to_data(obj, key, v));
13942 return Ok(());
13943 }
13944 }
13945
13946 let Some(v) = req.value else {
13947 // Nothing to write: a flags-only redefinition of a data property.
13948 return Ok(());
13949 };
13950 write_data_slot(obj, key, v);
13951 Ok(())
13952}
13953
13954/// Store `v` as an own data property, in whichever slot the object kind keeps
13955/// its own properties.
13956fn write_data_slot(obj: &Value, key: &str, v: Value) {
13957 // A function/class receiver stores its own props in the fn-prop side table
13958 // (express `mixin(app, proto)` defines methods onto the `app` *function*).
13959 if matches!(
13960 with_host(|h| h.get(obj).cloned()),
13961 Some(JsObj::Func(_)) | Some(JsObj::Class(_))
13962 ) || uses_side_table(obj)
13963 {
13964 with_host(|h| h.set_fn_prop(obj, key, v));
13965 return;
13966 }
13967 if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>()) {
13968 // An array's index keys ARE its elements, and defining one past the end
13969 // grows the array with holes in between (10.4.2.1). This whole branch
13970 // used to be missing: `Object.defineProperty(arr, 1, {value})` wrote
13971 // into the ordinary property map an array does not have, so it was a
13972 // silent no-op.
13973 with_host(|h| {
13974 let old = match h.get(obj) {
13975 Some(JsObj::Array(items)) => items.len(),
13976 _ => 0,
13977 };
13978 if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13979 if i >= old {
13980 items.resize(i + 1, Value::Undef);
13981 }
13982 items[i] = v;
13983 }
13984 if i > old {
13985 h.mark_hole_range(obj, old..i);
13986 }
13987 h.clear_hole(obj, i);
13988 });
13989 return;
13990 }
13991 with_host(|h| {
13992 if let Some(JsObj::Object(p)) = h.get_mut(obj) {
13993 p.insert(key.to_string(), v);
13994 host::canonicalize_own_keys(p);
13995 }
13996 });
13997}
13998
13999/// `Object.defineProperties(obj, descriptorMap)`.
14000fn object_define_properties(args: Vec<Value>) -> Result<Value, String> {
14001 let obj = arg0(&args);
14002 let descs = args.get(1).cloned().unwrap_or(Value::Undef);
14003 let entries: Vec<(String, Value)> = with_host(|h| match h.get(&descs) {
14004 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
14005 _ => Vec::new(),
14006 });
14007 for (k, d) in entries {
14008 apply_descriptor(&obj, &k, &d)?;
14009 }
14010 Ok(obj)
14011}
14012
14013/// The descriptor of an own property a function, a typed array or a RegExp
14014/// SYNTHESIZES rather than keeping in a property map.
14015///
14016/// These read back through the ordinary path but owned no descriptor and did
14017/// not appear under `hasOwnProperty` or `getOwnPropertyNames`, so the five
14018/// views of "does this property exist" disagreed — a read said yes while
14019/// `Object.getOwnPropertyDescriptor(f, 'name')` said no such property, which is
14020/// what a shim checks before patching.
14021fn synthesized_own_descriptor(obj: &Value, key: &str) -> Option<(Value, host::PropAttrs)> {
14022 let ro_configurable = host::PropAttrs {
14023 writable: false,
14024 enumerable: false,
14025 configurable: true,
14026 };
14027 // A callable's `length`/`name` are read-only but configurable; its
14028 // `prototype` is writable and NOT configurable, and a class's is neither.
14029 // An arrow, a method and a bound function own no `prototype` at all.
14030 if with_host(|h| host::is_callable(h, obj)) && !matches!(key, "length" | "name" | "prototype") {
14031 return None;
14032 }
14033 if with_host(|h| host::is_callable(h, obj)) {
14034 if key == "prototype" {
14035 let p = get_property(obj, "prototype").ok()?;
14036 if matches!(p, Value::Undef) {
14037 return None;
14038 }
14039 return Some((
14040 p,
14041 host::PropAttrs {
14042 writable: with_host(|h| h.kind_of(obj)) != Some(ObjKind::Class),
14043 enumerable: false,
14044 configurable: false,
14045 },
14046 ));
14047 }
14048 return Some((get_property(obj, key).ok()?, ro_configurable));
14049 }
14050 // A typed array's elements are own, enumerable, writable, configurable
14051 // properties; an index past the end owns nothing.
14052 if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") {
14053 let v = crate::stdlib::typedarray::elem_get(obj, key)?;
14054 return Some((
14055 v,
14056 host::PropAttrs {
14057 writable: true,
14058 enumerable: true,
14059 configurable: true,
14060 },
14061 ));
14062 }
14063 // A RegExp's `lastIndex` is its own, writable, non-configurable cursor.
14064 if with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))) && key == "lastIndex" {
14065 return Some((
14066 get_property(obj, "lastIndex").ok()?,
14067 host::PropAttrs {
14068 writable: true,
14069 enumerable: false,
14070 configurable: false,
14071 },
14072 ));
14073 }
14074 None
14075}
14076
14077fn object_get_own_descriptor(args: Vec<Value>) -> Result<Value, String> {
14078 let obj = arg0(&args);
14079 require_object_coercible(&obj)?;
14080 let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
14081 // A string primitive's boxed own properties: each code-unit index is an
14082 // enumerable, non-writable, non-configurable data property, and `length` is
14083 // the same minus enumerable.
14084 if let Some(units) = string_primitive_units(&obj) {
14085 let entry = match key.parse::<usize>() {
14086 Ok(i) => units
14087 .get(i)
14088 .map(|c| (with_host(|h| h.new_str(c.clone())), true)),
14089 Err(_) if key == "length" => Some((Value::Float(units.len() as f64), false)),
14090 Err(_) => None,
14091 };
14092 return Ok(match entry {
14093 Some((value, enumerable)) => with_host(|h| {
14094 let mut m: IndexMap<String, Value> = IndexMap::new();
14095 m.insert("value".into(), value);
14096 m.insert("writable".into(), Value::Bool(false));
14097 m.insert("enumerable".into(), Value::Bool(enumerable));
14098 m.insert("configurable".into(), Value::Bool(false));
14099 h.new_object(m)
14100 }),
14101 None => Value::Undef,
14102 });
14103 }
14104 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
14105 return Ok(crate::proxy::get_own_descriptor(&obj, &key)?.unwrap_or(Value::Undef));
14106 }
14107 // A method read off an enumerable builtin prototype (`EventEmitter.prototype`)
14108 // yields a `{ value: <method thunk> }` data descriptor so `mixin` can copy it.
14109 if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&obj).cloned()) {
14110 if let Some(names) = builtin_proto_method_names(&ns) {
14111 if names.contains(&key.as_str()) {
14112 return Ok(with_host(|h| {
14113 let thunk = h.alloc(JsObj::Builtin(format!(
14114 "@proto:{}:{key}",
14115 ns.trim_end_matches(".prototype")
14116 )));
14117 let mut m: IndexMap<String, Value> = IndexMap::new();
14118 m.insert("value".into(), thunk);
14119 m.insert("writable".into(), Value::Bool(true));
14120 m.insert("enumerable".into(), Value::Bool(true));
14121 m.insert("configurable".into(), Value::Bool(true));
14122 h.new_object(m)
14123 }));
14124 }
14125 }
14126 }
14127 // A global the object does not own outright is still an own property of the
14128 // global object — the same lazy binding the bare identifier resolves to.
14129 // Every one of them reported `undefined`, so a feature probe written as
14130 // `getOwnPropertyDescriptor(globalThis, 'structuredClone')` concluded the
14131 // global was absent. The immutable trio (11.1.1 / 19.1.1-3) is frozen; the
14132 // rest are ordinary writable, non-enumerable, configurable bindings.
14133 if with_host(|h| h.is_global_object(&obj)) {
14134 let owned = with_host(|h| match h.get(&obj) {
14135 Some(JsObj::Object(p)) => p.contains_key(&key),
14136 _ => false,
14137 });
14138 if !owned && !CJS_WRAPPER_LOCALS.contains(&key.as_str()) {
14139 // A global a SCRIPT created — `x = 1` with no declaration — is an
14140 // ordinary enumerable property, unlike the builtins.
14141 let script_made = with_host(|h| h.read_global(&key).is_some());
14142 if let Some(v) = global_object_binding(&key) {
14143 let frozen = matches!(key.as_str(), "undefined" | "NaN" | "Infinity");
14144 return Ok(with_host(|h| {
14145 let mut m: IndexMap<String, Value> = IndexMap::new();
14146 m.insert("value".into(), v);
14147 m.insert("writable".into(), Value::Bool(!frozen));
14148 m.insert(
14149 "enumerable".into(),
14150 Value::Bool(script_made || ENUMERABLE_GLOBALS.contains(&key.as_str())),
14151 );
14152 m.insert("configurable".into(), Value::Bool(!frozen));
14153 h.new_object(m)
14154 }));
14155 }
14156 }
14157 }
14158 // Any other member of a builtin namespace (`Math.PI`, `Math.floor`,
14159 // `Array.prototype.slice`, a builtin function's own `name`/`length`). Every
14160 // one of these reads back a value, but none owned a DESCRIPTOR:
14161 // `Object.getOwnPropertyDescriptor(Math, 'PI')` was `undefined`, which reads
14162 // as "no such property" to the shim/polyfill family that probes a namespace
14163 // before patching it.
14164 // An ACCESSOR member describes itself with a `get`, never a `value` — and
14165 // it must do so without READING the property, since running the getter
14166 // against the prototype is exactly what throws. Both prototype
14167 // representations are covered, so `Symbol.prototype.description` and
14168 // `Map.prototype.size` answer alike; both were `undefined`, which reads as
14169 // "no such property" to anything that probes before patching.
14170 if let Some(ctor) = intrinsic_proto_of(&obj) {
14171 if is_proto_accessor(&ctor, &key) {
14172 let getter = proto_getter(&ctor, &key);
14173 // The poison pair is the only ECMAScript accessor here with a
14174 // SETTER, but a WebIDL class has plenty: `URL.prototype.href`,
14175 // `hostname` and the rest are all writable, and reporting them as
14176 // read-only made `Object.getOwnPropertyDescriptor(URL.prototype,
14177 // 'href').set` read `undefined` for a setter that runs.
14178 let writable = (ctor == "Function" && matches!(key.as_str(), "arguments" | "caller"))
14179 || crate::stdlib::instance_accessors(&ctor)
14180 .0
14181 .iter()
14182 .any(|(k, settable)| *k == key && *settable);
14183 let setter = writable
14184 .then(|| with_host(|h| h.alloc(JsObj::Builtin(format!("@protoset:{ctor}:{key}")))));
14185 return Ok(with_host(|h| {
14186 let mut m: IndexMap<String, Value> = IndexMap::new();
14187 m.insert("get".into(), getter);
14188 // `undefined`, not null: a read-only accessor has no setter at
14189 // all, and `JSON.stringify` of the descriptor must drop the key
14190 // rather than report `"set": null`.
14191 m.insert("set".into(), setter.unwrap_or(Value::Undef));
14192 m.insert("enumerable".into(), Value::Bool(is_webidl_proto(&ctor)));
14193 m.insert("configurable".into(), Value::Bool(true));
14194 h.new_object(m)
14195 }));
14196 }
14197 }
14198 if let Some(ns) = with_host(|h| match h.get(&obj) {
14199 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
14200 _ => None,
14201 }) {
14202 let value = namespace_property(&ns, &key);
14203 if !matches!(value, Value::Undef) {
14204 return Ok(builtin_member_descriptor(&ns, &key, value));
14205 }
14206 }
14207 if let Some((value, attrs)) = synthesized_own_descriptor(&obj, &key) {
14208 return Ok(with_host(|h| {
14209 let mut m: IndexMap<String, Value> = IndexMap::new();
14210 m.insert("value".into(), value);
14211 m.insert("writable".into(), Value::Bool(attrs.writable));
14212 m.insert("enumerable".into(), Value::Bool(attrs.enumerable));
14213 m.insert("configurable".into(), Value::Bool(attrs.configurable));
14214 h.new_object(m)
14215 }));
14216 }
14217 // Accessor descriptor?
14218 if let Some((get, set)) = with_host(|h| h.own_accessor(&obj, &key)) {
14219 return Ok(with_host(|h| {
14220 let a = h.prop_attrs(&obj, &key);
14221 let mut m: IndexMap<String, Value> = IndexMap::new();
14222 m.insert("get".into(), get.unwrap_or(Value::Undef));
14223 m.insert("set".into(), set.unwrap_or(Value::Undef));
14224 m.insert("enumerable".into(), Value::Bool(a.enumerable));
14225 m.insert("configurable".into(), Value::Bool(a.configurable));
14226 h.new_object(m)
14227 }));
14228 }
14229 let val = with_host(|h| match h.get(&obj) {
14230 // A Buffer's own properties are exactly its byte indices, read out of the
14231 // hidden `@@bytes` slot; `length`/`byteLength` are internal bookkeeping
14232 // that V8 keeps on the prototype, so they own no descriptor.
14233 Some(JsObj::Object(p))
14234 if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("Buffer") =>
14235 {
14236 match (
14237 p.get("@@bytes").and_then(|b| h.get(b)),
14238 key.parse::<usize>(),
14239 ) {
14240 (Some(JsObj::Array(items)), Ok(i)) => items.get(i).cloned(),
14241 _ => None,
14242 }
14243 }
14244 Some(JsObj::Object(p)) => p.get(&key).cloned(),
14245 // An array's index keys read the elements; `length` is the exotic own
14246 // property; anything else is an ordinary own key in the side table.
14247 Some(JsObj::Array(items)) => match key.parse::<usize>() {
14248 // An ELIDED index owns no property at all, so it has no descriptor.
14249 Ok(i) if h.is_hole(&obj, i) => None,
14250 Ok(i) => items.get(i).cloned(),
14251 Err(_) if key == "length" => Some(Value::Float(items.len() as f64)),
14252 Err(_) => h.fn_prop(&obj, &key),
14253 },
14254 // A function/class own prop lives in the fn-prop side table.
14255 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(&obj, &key),
14256 _ => None,
14257 });
14258 match val {
14259 Some(v) => Ok(with_host(|h| {
14260 let a = h.prop_attrs(&obj, &key);
14261 let mut m: IndexMap<String, Value> = IndexMap::new();
14262 m.insert("value".into(), v);
14263 m.insert("writable".into(), Value::Bool(a.writable));
14264 m.insert("enumerable".into(), Value::Bool(a.enumerable));
14265 m.insert("configurable".into(), Value::Bool(a.configurable));
14266 h.new_object(m)
14267 })),
14268 None => Ok(Value::Undef),
14269 }
14270}
14271
14272/// `Object.getOwnPropertyDescriptors(obj)` — the descriptor of every own string
14273/// key, keyed by name. `Object.create(proto, getOwnPropertyDescriptors(src))` is
14274/// the standard "clone with accessors intact" idiom, so this must agree
14275/// key-for-key with `getOwnPropertyNames`.
14276fn object_get_own_descriptors(args: Vec<Value>) -> Result<Value, String> {
14277 let obj = arg0(&args);
14278 let names = object_keys(vec![obj.clone()], 3)?;
14279 let keys: Vec<String> = with_host(|h| match h.get(&names) {
14280 Some(JsObj::Array(items)) => items.iter().map(|k| h.str_of(k)).collect(),
14281 _ => Vec::new(),
14282 });
14283 let mut out: IndexMap<String, Value> = IndexMap::new();
14284 for k in keys {
14285 let ks = with_host(|h| h.new_str(k.clone()));
14286 let d = object_get_own_descriptor(vec![obj.clone(), ks])?;
14287 if !matches!(d, Value::Undef) {
14288 out.insert(k, d);
14289 }
14290 }
14291 Ok(with_host(|h| h.new_object(out)))
14292}
14293
14294/// `key in obj` respecting the prototype chain. Reports a `Result` because a
14295/// Proxy's `has` trap is user code and may throw.
14296pub fn has_property(obj: &Value, key: &str) -> Result<bool, String> {
14297 if let Some(b) = crate::proxy::has(obj, key)? {
14298 return Ok(b);
14299 }
14300 Ok(has_property_ordinary(obj, key))
14301}
14302
14303/// `[[HasProperty]]` for every non-Proxy receiver.
14304fn has_property_ordinary(obj: &Value, key: &str) -> bool {
14305 // `key in globalThis`: membership matches what the READ answers, which for
14306 // the global object includes every lazily-bound builtin and every global a
14307 // script created. `'Math' in globalThis` and `'x' in globalThis` after
14308 // `x = 1` both answered FALSE while `globalThis.Math` and `globalThis.x`
14309 // read back fine.
14310 if with_host(|h| h.is_global_object(obj))
14311 && !CJS_WRAPPER_LOCALS.contains(&key)
14312 && global_object_binding(key).is_some()
14313 {
14314 return true;
14315 }
14316 // `key in <builtin namespace/prototype>`: membership matches what a property
14317 // read would yield. `String.prototype.indexOf` (and the rest of the builtin
14318 // prototype methods) resolve as callable thunks via `namespace_property`, so
14319 // `'indexOf' in String.prototype` must report true (get-intrinsic probes this
14320 // with the `in` operator before reading the intrinsic).
14321 if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(obj).cloned()) {
14322 return !matches!(namespace_property(&ns, key), Value::Undef);
14323 }
14324 // An integer index of a typed array / Buffer is an own property, and lives
14325 // in the hidden element array rather than the property map — the same
14326 // question `hasOwnProperty` answers, through the same helper. Only a hit
14327 // short-circuits: a non-index key like `'length'` must still fall through
14328 // to the ordinary chain lookup below.
14329 if crate::stdlib::typedarray::has_index(obj, key) == Some(true) {
14330 return true;
14331 }
14332 if with_host(|h| host::lookup_chain(h, obj, key)).is_some() {
14333 return true;
14334 }
14335 if with_host(|h| host::lookup_accessor(h, obj, key)).is_some() {
14336 return true;
14337 }
14338 // A member patched onto the receiver's intrinsic prototype. The READ
14339 // resolves it, so without this `Array.prototype.at = f` made `[].at` a
14340 // function while `'at' in []` stayed false.
14341 if !key.starts_with('#') && inherited_builtin_static(obj, key).is_some() {
14342 return true;
14343 }
14344 if with_host(|h| match h.get(obj) {
14345 Some(JsObj::Object(p)) => p.contains_key(key),
14346 Some(JsObj::Array(items)) => {
14347 key == "length"
14348 || key
14349 .parse::<usize>()
14350 .map(|i| i < items.len() && !h.is_hole(obj, i))
14351 .unwrap_or(false)
14352 // A non-index own property (`arr.foo`, `arr[sym]`) lives in the
14353 // side table, and `in` must see it.
14354 || h.fn_prop(obj, key).is_some()
14355 }
14356 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(obj, key).is_some(),
14357 // A RegExp's `lastIndex` is an OWN property in node. Here it lives in
14358 // the `RegExpObj` struct rather than a property map, so nothing above
14359 // can see it.
14360 Some(JsObj::RegExp(_)) => key == "lastIndex" || h.fn_prop(obj, key).is_some(),
14361 _ => false,
14362 }) {
14363 return true;
14364 }
14365 // An INHERITED builtin prototype method. These are not objects on the
14366 // prototype chain — they are synthesized by the read path from the
14367 // intrinsic table — so neither `lookup_chain` nor the property map above
14368 // can see them, and `'toString' in {}`, `'push' in []` and `'then' in
14369 // Promise.resolve()` all answered false. That last one is the standard
14370 // thenable test, so the `in` operator disagreed with what a read gives for
14371 // every builtin method of every builtin kind.
14372 inherited_builtin_method(obj, key)
14373}
14374
14375/// Whether a READ of `key` on `obj` would resolve to an inherited builtin
14376/// prototype method. Asked by `in` and `hasOwnProperty`'s negative case; it
14377/// performs no read, so a getter cannot fire.
14378/// A property a script MONKEY-PATCHED onto the intrinsic prototype `obj`
14379/// inherits from (`Array.prototype.at = impl`, `Object.prototype.foo = 1`), or
14380/// `None`.
14381///
14382/// The intrinsic prototypes are namespace handles rather than real objects on
14383/// the chain, so an assignment onto one lands in `builtin_statics` and no
14384/// ordinary chain walk can see it. This is the read side: the receiver's own
14385/// constructor's prototype first, then `Object.prototype`, mirroring
14386/// `inherited_method_owner`'s two-step.
14387pub(crate) fn inherited_builtin_static(obj: &Value, key: &str) -> Option<Value> {
14388 if with_host(|h| h.has_null_proto(obj)) {
14389 return None;
14390 }
14391 let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14392 Some(c) => Some(c),
14393 None if is_arguments(obj) => Some("Object"),
14394 None => with_host(|h| default_ctor_name(h, obj)),
14395 };
14396 // Only the side table is consulted, never the real prototype OBJECT's map:
14397 // `String.prototype` and friends are materialized with their intrinsic
14398 // members present, so reading their maps here would re-route every ordinary
14399 // `"a".toString()` through this path — which recursed until the stack blew.
14400 // `set_property` mirrors a write onto a real intrinsic prototype INTO this
14401 // table precisely so the read side can stay this narrow.
14402 let on = |c: &str| with_host(|h| h.builtin_static(&format!("{c}.prototype"), key));
14403 let found = ctor.and_then(on).or_else(|| on("Object"))?;
14404 // Restoring a saved intrinsic (`const orig = Array.prototype.join; …;
14405 // Array.prototype.join = orig`) stores the SYNTHESIZED thunk for this very
14406 // name back into the table. Dispatching to it would re-enter this lookup
14407 // and recurse until the stack blew, so a thunk that is already this key's
14408 // own intrinsic reports nothing and the ordinary builtin path answers.
14409 let self_thunk = with_host(
14410 |h| matches!(h.get(&found), Some(JsObj::Builtin(s)) if s.starts_with("@proto:") && s.ends_with(&format!(":{key}"))),
14411 );
14412 (!self_thunk).then_some(found)
14413}
14414
14415/// Whether `recv` carries `key` as an OWN property — the guard on
14416/// [`inherited_builtin_static`], since an own property shadows anything
14417/// patched onto a prototype.
14418fn has_own_for_shadow(recv: &Value, key: &str) -> bool {
14419 with_host(|h| {
14420 if h.fn_prop(recv, key).is_some() || h.own_accessor(recv, key).is_some() {
14421 return true;
14422 }
14423 match h.get(recv) {
14424 Some(JsObj::Object(p)) => p.contains_key(key),
14425 // An ELIDED index owns nothing — the whole point of a hole is that
14426 // the lookup continues up the chain — so it must not count as a
14427 // shadow here or an inherited value at that index stays invisible.
14428 Some(JsObj::Array(items)) => {
14429 key == "length" || {
14430 key.parse::<usize>()
14431 .is_ok_and(|i| i < items.len() && !h.is_hole(recv, i))
14432 }
14433 }
14434 _ => false,
14435 }
14436 })
14437}
14438
14439fn inherited_builtin_method(obj: &Value, key: &str) -> bool {
14440 if with_host(|h| h.has_null_proto(obj)) {
14441 return false;
14442 }
14443 if let Some(tag) = crate::stdlib::native_tag(obj) {
14444 if crate::stdlib::instance_has_method(&tag, key) {
14445 return true;
14446 }
14447 }
14448 inherited_method_owner(obj, key).is_some()
14449}
14450
14451/// Whether `recv`'s intrinsic prototype is still on its chain — that is,
14452/// whether `Array.prototype`'s methods are still reachable from an array.
14453///
14454/// A builtin's methods are synthesized from the receiver's KIND rather than
14455/// found on a chain, so replacing the prototype could not take them away:
14456/// `Object.setPrototypeOf(a, {})` left `a.join` a function where node reports
14457/// `undefined`, and `Object.setPrototypeOf(a, null)` did too. The exotic
14458/// storage is unaffected either way — `Array.isArray`, `a.length` and `a[0]`
14459/// all still answer, as they do in node.
14460///
14461/// The overwhelmingly common case is the DEFAULT link, which is recorded as no
14462/// link at all, so this answers true after one map probe and allocates nothing.
14463pub(crate) fn own_intrinsic_reachable_pub(recv: &Value) -> bool {
14464 own_intrinsic_reachable(recv)
14465}
14466
14467fn own_intrinsic_reachable(recv: &Value) -> bool {
14468 // A BOXED primitive needs no special case here: its methods resolve through
14469 // `inherited_method_owner`, which applies the wrapper rule itself.
14470 with_host(|h| default_ctor_name(h, recv)).map_or(true, |c| intrinsic_reachable(recv, c))
14471}
14472
14473/// Whether the intrinsic prototype for `ctor` is still on `recv`'s chain.
14474fn intrinsic_reachable(recv: &Value, ctor: &str) -> bool {
14475 let own = Some(ctor);
14476 let mut cur = recv.clone();
14477 for _ in 0..100 {
14478 let explicit = with_host(|h| h.proto_of(&cur));
14479 let Some(p) = explicit else {
14480 // No explicit link: the implicit prototype is this object's own
14481 // kind's, which is what `recv` is asking about only while `cur` is
14482 // still `recv` itself.
14483 if with_host(|h| h.has_null_proto(&cur)) {
14484 return false;
14485 }
14486 let implicit = with_host(|h| default_ctor_name(h, &cur));
14487 // Every implicit prototype chain ends at `Object.prototype`, so a
14488 // question about `Object` is answered yes by any of them.
14489 return implicit == own || ctor == "Object";
14490 };
14491 if with_host(|h| h.is_null(&p)) {
14492 return false;
14493 }
14494 let hit = with_host(|h| {
14495 own.is_some_and(|c| {
14496 matches!(h.get(&p), Some(JsObj::Builtin(ns)) if *ns == format!("{c}.prototype"))
14497 || h.intrinsic_proto_ctor(&p) == Some(c)
14498 || (c == "Object" && h.object_proto() == p)
14499 })
14500 });
14501 if hit {
14502 return true;
14503 }
14504 // A CLASS prototype object is not linked to the builtin its class
14505 // extends — the `extends` relationship is recorded on the class value —
14506 // so the walk has to cross over there or `class D extends Array {}` ends
14507 // it, and every inherited method of every subclass instance vanishes.
14508 if let Some(builtin) = with_host(|h| {
14509 h.class_owning_proto(&p)
14510 .and_then(|c| h.class_builtin_ancestor(&c))
14511 .map(|b| h.callable_name(&b))
14512 }) {
14513 if own == Some(builtin.as_str()) || ctor == "Object" {
14514 return true;
14515 }
14516 }
14517 cur = p;
14518 }
14519 false
14520}
14521
14522/// The intrinsic prototypes actually ON `recv`'s explicit chain, nearest first
14523/// — the complement of [`intrinsic_reachable`], which asks about one known
14524/// constructor.
14525///
14526/// `Object.create(Array.prototype)` is an ordinary object whose chain reaches
14527/// `Array.prototype`, and node resolves the whole of `Array.prototype` through
14528/// it: `o.push(1)` works, because those methods are generic over their receiver
14529/// (which is also why `Array.prototype.push.call({length: 0}, 1)` already
14530/// worked here). Deciding the owner from the receiver's KIND alone made every
14531/// one of them `undefined` — the same "methods come from the kind, not the
14532/// chain" mistake as the detachment case, in the opposite direction.
14533pub(crate) fn chain_intrinsic_ctors_pub(recv: &Value) -> Vec<&'static str> {
14534 chain_intrinsic_ctors(recv)
14535}
14536
14537fn chain_intrinsic_ctors(recv: &Value) -> Vec<&'static str> {
14538 with_host(|h| chain_intrinsic_ctors_h(h, recv))
14539}
14540
14541/// [`chain_intrinsic_ctors`] against an already-held host borrow, for the
14542/// callers that are inside one — `can_write_prop` takes `&JsHost`, so going
14543/// back through `with_host` there aborts the process on a double borrow.
14544pub(crate) fn chain_intrinsic_ctors_h(h: &host::JsHost, recv: &Value) -> Vec<&'static str> {
14545 let mut out: Vec<&'static str> = Vec::new();
14546 let mut cur = recv.clone();
14547 for _ in 0..100 {
14548 let Some(p) = h.proto_of(&cur) else {
14549 break;
14550 };
14551 if h.is_null(&p) {
14552 break;
14553 }
14554 let name = match h.get(&p) {
14555 Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
14556 _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
14557 };
14558 if let Some(n) = name {
14559 if let Some(c) = crate::arity::PROTO_MEMBERS
14560 .iter()
14561 .map(|(k, _)| *k)
14562 .find(|k| *k == n)
14563 {
14564 if !out.contains(&c) {
14565 out.push(c);
14566 }
14567 }
14568 }
14569 cur = p;
14570 }
14571 out
14572}
14573
14574/// The constructor whose prototype defines `key` for `obj` — its own if that
14575/// prototype has it, otherwise `Object` — or `None` when neither does.
14576///
14577/// Used both by `in` and by the READ, so the two cannot disagree about which
14578/// prototype a name comes from. `new Map().toString` is `Map.prototype`'s and
14579/// `new Map().hasOwnProperty` is `Object.prototype`'s.
14580pub(crate) fn inherited_method_owner_pub(obj: &Value, key: &str) -> Option<&'static str> {
14581 inherited_method_owner(obj, key)
14582}
14583
14584fn inherited_method_owner(obj: &Value, key: &str) -> Option<&'static str> {
14585 if with_host(|h| h.has_null_proto(obj)) {
14586 return None;
14587 }
14588 // The generated prototype-member table, which unlike the arity table knows
14589 // about the ACCESSORS — `size` on a Map, `source` on a RegExp, `description`
14590 // on a Symbol are members but not functions — and about `constructor`.
14591 // A BOXED primitive reports its wrapper's constructor, not `Object` —
14592 // `'description' in Object(Symbol())` is true. The box is an ordinary
14593 // object carrying the primitive in a slot, so the ctor comes from what it
14594 // holds rather than from the box itself.
14595 let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14596 Some(c) => Some(c),
14597 // An `arguments` object is ARRAY-BACKED here so that indices, `length`,
14598 // spread and `for-of` work, but node's is an exotic that inherits from
14599 // `Object.prototype` — `typeof arguments.map` is `undefined`. Reporting
14600 // its backing kind would hand it the whole `Array.prototype`.
14601 None if is_arguments(obj) => Some("Object"),
14602 None => with_host(|h| default_ctor_name(h, obj)),
14603 };
14604 let on_proto = |c: &str| {
14605 crate::arity::PROTO_MEMBERS
14606 .binary_search_by(|(k, _)| (*k).cmp(c))
14607 .ok()
14608 .is_some_and(|i| {
14609 crate::arity::PROTO_MEMBERS[i]
14610 .1
14611 .iter()
14612 .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
14613 })
14614 };
14615 // `PROTO_MEMBERS` is generated from the prototypes' STRING keys, so a
14616 // well-known symbol member is absent from it. For an object whose CHAIN
14617 // reaches an intrinsic prototype the intrinsic table has to be consulted as
14618 // well, or `[...Object.create(Array.prototype)]` finds no `Symbol.iterator`
14619 // at all. It is deliberately NOT consulted for the receiver's own kind:
14620 // there a thunk would be minted for every `@@` member the table names,
14621 // including ones whose dispatch has no implementation for that receiver,
14622 // and `[...buffer]` then failed with `@@iterator is not a function`.
14623 //
14624 // It is narrowed further to an ORDINARY object: a natively-tagged receiver
14625 // (a typed array, a Buffer) is linked to a real intrinsic prototype too,
14626 // and minting a thunk there produced `@@iterator is not a function` for
14627 // `[...new Uint8Array(ab)]` — those kinds reach their iterator by their own
14628 // fast path, which the table entry would shadow.
14629 let plain = with_host(|h| h.kind_of(obj)) == Some(ObjKind::Object)
14630 && crate::stdlib::native_tag(obj).is_none();
14631 let on_proto_or_symbol =
14632 |c: &str| on_proto(c) || (plain && builtin_meta(&format!("@proto:{c}:{key}")).is_some());
14633 // Each candidate is only an answer while ITS prototype is still on the
14634 // receiver's chain. The two are asked separately: replacing an array's
14635 // prototype with a plain object takes `Array.prototype`'s methods away and
14636 // leaves `Object.prototype`'s, since the replacement inherits from it.
14637 if let Some(c) = ctor.filter(|c| on_proto(c) && intrinsic_reachable(obj, c)) {
14638 return Some(c);
14639 }
14640 // An intrinsic prototype the receiver's chain passes THROUGH, which its own
14641 // kind does not account for.
14642 if let Some(c) = chain_intrinsic_ctors(obj)
14643 .into_iter()
14644 .find(|c| on_proto_or_symbol(c))
14645 {
14646 return Some(c);
14647 }
14648 // Everything else inherits `Object.prototype`'s.
14649 if on_proto("Object") && intrinsic_reachable(obj, "Object") {
14650 return Some("Object");
14651 }
14652 None
14653}
14654
14655/// `structuredClone` — a deep copy of plain data (objects/arrays/primitives).
14656/// `structuredClone` — the HTML structured-clone algorithm's shape: a deep copy
14657/// that preserves the *reference graph*. Two properties pointing at the same
14658/// object clone to two properties pointing at the same clone, and a cycle clones
14659/// to a cycle instead of recursing forever. `seen` maps each source heap index
14660/// to its clone, which is what buys both.
14661/// The rendering node puts in a `DataCloneError` for a value the structured
14662/// clone algorithm refuses, or `None` when the value IS cloneable.
14663///
14664/// Refusing at all is the point: these used to be copied through by reference,
14665/// so `structuredClone({f: () => 1})` handed back an object sharing the
14666/// original's function and `structuredClone(new WeakMap())` returned the very
14667/// same WeakMap. Node throws on every one of them.
14668fn clone_refusal(v: &Value) -> Option<String> {
14669 let kind = with_host(|h| h.kind_of(v))?;
14670 let render = |ctor: &str| Some(format!("#<{ctor}>"));
14671 match kind {
14672 // A function renders as its SOURCE TEXT here, which each FuncDef
14673 // keeps as a span into its script (`JsHost::func_source`).
14674 ObjKind::Func | ObjKind::Class | ObjKind::BoundFunc | ObjKind::BoundMethod => {
14675 Some(with_host(|h| h.str_of(v)))
14676 }
14677 ObjKind::Builtin if with_host(|h| host::is_callable(h, v)) => {
14678 Some(with_host(|h| h.str_of(v)))
14679 }
14680 ObjKind::Symbol => Some(with_host(|h| h.str_of(v))),
14681 ObjKind::Promise => render("Promise"),
14682 ObjKind::Generator => Some("[object Generator]".to_string()),
14683 // A proxy is refused by its TARGET's shape: a callable one renders like
14684 // the function it wraps, everything else as a plain object.
14685 ObjKind::Proxy => Some(if with_host(|h| host::is_callable(h, v)) {
14686 with_host(|h| h.str_of(v))
14687 } else {
14688 "#<Object>".to_string()
14689 }),
14690 ObjKind::Map if with_host(|h| matches!(h.get(v), Some(JsObj::Map { weak: true, .. }))) => {
14691 render("WeakMap")
14692 }
14693 ObjKind::Set if with_host(|h| matches!(h.get(v), Some(JsObj::Set { weak: true, .. }))) => {
14694 render("WeakSet")
14695 }
14696 _ => match crate::stdlib::native_tag(v).as_deref() {
14697 Some(t @ ("WeakRef" | "FinalizationRegistry")) => render(t),
14698 _ => None,
14699 },
14700 }
14701}
14702
14703/// `structuredClone(value[, { transfer }])`.
14704///
14705/// Everything in `transfer` must be an `ArrayBuffer`, and each one is DETACHED
14706/// after the clone — its bytes belong to the copy. The option used to be
14707/// ignored entirely, so the source buffer stayed usable where node leaves it
14708/// with zero length.
14709fn structured_clone(args: Vec<Value>) -> Result<Value, String> {
14710 let list: Vec<Value> = match args.get(1).filter(|v| !matches!(v, Value::Undef)) {
14711 Some(opts) => {
14712 let t = get_property(opts, "transfer")?;
14713 if matches!(t, Value::Undef) {
14714 Vec::new()
14715 } else {
14716 host::iter_all(&t)?
14717 }
14718 }
14719 None => Vec::new(),
14720 };
14721 for item in &list {
14722 if crate::stdlib::native_tag(item).as_deref() != Some("ArrayBuffer") {
14723 return Err(host::dom_error(
14724 "DataCloneError",
14725 "Found invalid value in transferList.",
14726 ));
14727 }
14728 }
14729 let out = deep_clone(&arg0(&args))?;
14730 for item in &list {
14731 crate::stdlib::typedarray::detach_buffer(item);
14732 }
14733 Ok(out)
14734}
14735
14736pub(crate) fn deep_clone(v: &Value) -> Result<Value, String> {
14737 deep_clone_seen(v, &mut std::collections::HashMap::new())
14738}
14739
14740fn deep_clone_seen(
14741 v: &Value,
14742 seen: &mut std::collections::HashMap<u32, Value>,
14743) -> Result<Value, String> {
14744 let idx = match v {
14745 Value::Obj(i) => *i,
14746 _ => return Ok(v.clone()),
14747 };
14748 if let Some(done) = seen.get(&idx) {
14749 return Ok(done.clone());
14750 }
14751 if crate::stdlib::typedarray::is_detached(v) {
14752 return Err(host::dom_error(
14753 "DataCloneError",
14754 "An ArrayBuffer is detached and could not be cloned.",
14755 ));
14756 }
14757 if let Some(render) = clone_refusal(v) {
14758 return Err(host::dom_error(
14759 "DataCloneError",
14760 &format!("{render} could not be cloned."),
14761 ));
14762 }
14763 // A REGEXP is cloned, not shared: it carries a mutable `lastIndex`, so
14764 // handing back the same object let a write through the clone move the
14765 // original's match cursor.
14766 if let Some((src, flags)) = with_host(|h| match h.get(v) {
14767 Some(JsObj::RegExp(r)) => Some((r.source.clone(), r.flags.clone())),
14768 _ => None,
14769 }) {
14770 let args = with_host(|h| vec![h.new_str(src), h.new_str(flags)]);
14771 let out = regexp_ctor(&args)?;
14772 seen.insert(idx, out.clone());
14773 return Ok(out);
14774 }
14775 Ok(match with_host(|h| h.get(v).cloned()) {
14776 Some(JsObj::Array(items)) => {
14777 // Register the (empty) clone BEFORE recursing so a self-reference
14778 // resolves to it.
14779 let out = with_host(|h| h.new_array(Vec::new()));
14780 seen.insert(idx, out.clone());
14781 let mut cloned: Vec<Value> = Vec::with_capacity(items.len());
14782 for x in &items {
14783 cloned.push(deep_clone_seen(x, seen)?);
14784 }
14785 with_host(|h| {
14786 if let Some(JsObj::Array(a)) = h.get_mut(&out) {
14787 *a = cloned;
14788 }
14789 // A sparse source clones to an equally sparse array: the clone
14790 // walks own properties, so a hole is nothing to copy.
14791 h.copy_holes(v, &out, Some);
14792 });
14793 out
14794 }
14795 Some(JsObj::Object(_)) => {
14796 let out = with_host(|h| h.new_object(IndexMap::new()));
14797 seen.insert(idx, out.clone());
14798 // Own ENUMERABLE string keys, read THROUGH any accessor: the clone
14799 // walked the property map, where an accessor stores nothing, so
14800 // `structuredClone({get p(){return 1}})` silently lost `p`. A symbol
14801 // key and a non-enumerable one are dropped, as node drops them.
14802 let is_error = with_host(|h| h.error_to_string(v)).is_some();
14803 let proto = clone_proto(v);
14804 let keeps_proto = !matches!(proto, CloneProto::Plain);
14805 // An ERROR clones its name, message and stack and NOTHING else —
14806 // node drops any other own property, even an enumerable one.
14807 let keys: Vec<String> = if is_error {
14808 // An ERROR clones its name, message and stack and NOTHING else —
14809 // node drops any other own property, even an enumerable one.
14810 ["name", "message", "stack"]
14811 .iter()
14812 .filter(|k| has_property(v, k).unwrap_or(false))
14813 .map(|k| (*k).to_string())
14814 .collect()
14815 } else if keeps_proto {
14816 // A preserved exotic keeps EVERY own property, including the
14817 // non-enumerable ones and the internal slots — a Date's time
14818 // value, an ArrayBuffer's `byteLength` and byte store, a typed
14819 // array's view. The enumerable-only walk dropped all of those,
14820 // so a cloned Date read `Invalid Date` and a cloned
14821 // ArrayBuffer had no `byteLength`.
14822 with_host(|h| match h.get(v) {
14823 Some(JsObj::Object(p)) => p.keys().cloned().collect(),
14824 _ => Vec::new(),
14825 })
14826 } else {
14827 with_host(|h| h.own_enum_key_names(v))
14828 };
14829 let mut cloned: IndexMap<String, Value> = IndexMap::new();
14830 for k in keys {
14831 // An internal slot is read straight out of the map: it is not a
14832 // property, so a `[[Get]]` would not find it.
14833 let val = if k.starts_with("@@") {
14834 match with_host(|h| match h.get(v) {
14835 Some(JsObj::Object(p)) => p.get(&k).cloned(),
14836 _ => None,
14837 }) {
14838 Some(val) => val,
14839 None => continue,
14840 }
14841 } else {
14842 get_property(v, &k)?
14843 };
14844 cloned.insert(k, deep_clone_seen(&val, seen)?);
14845 }
14846 // The prototype survives only for the exotics the algorithm knows —
14847 // a Date, an Error, a typed array, a boxed primitive. A USER class
14848 // instance becomes a plain object, which is what node produces;
14849 // keeping every prototype made `structuredClone(new K())
14850 // instanceof K` true.
14851 with_host(|h| {
14852 if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14853 *p = cloned;
14854 }
14855 match &proto {
14856 CloneProto::Same => {
14857 if let Some(p) = h.proto_of(v) {
14858 h.set_proto(&out, p);
14859 }
14860 }
14861 CloneProto::Ctor(c) => {
14862 h.ensure_error_protos();
14863 let p = h.error_proto(c).or_else(|| h.ensure_ctor_proto(c));
14864 if let Some(p) = p {
14865 h.set_proto(&out, p);
14866 }
14867 // A Buffer clones to a plain `Uint8Array`, so the native
14868 // tag has to change with the prototype — left alone,
14869 // `Buffer.isBuffer` still answered true for the clone.
14870 // A Buffer clones to a plain `Uint8Array`, so the native
14871 // tag has to change with the prototype — left alone,
14872 // `Buffer.isBuffer` answered true for the clone and the
14873 // brand stayed `[object Object]`. A typed array is
14874 // tagged `TypedArray` and names its element type in
14875 // `@@kind`; `@@native = "Uint8Array"` matches no arm.
14876 if c == "Uint8Array" {
14877 let tag = h.new_str("TypedArray");
14878 let kind = h.new_str("Uint8Array");
14879 if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14880 p.insert("@@native".into(), tag);
14881 p.insert("@@kind".into(), kind);
14882 }
14883 }
14884 }
14885 CloneProto::Plain => {}
14886 }
14887 h.copy_prop_attrs(v, &out);
14888 });
14889 out
14890 }
14891 // Map/Set are structured types: clone the entries, keep the kind.
14892 Some(JsObj::Map { entries, weak }) => {
14893 let out = with_host(|h| {
14894 h.alloc(JsObj::Map {
14895 entries: IndexMap::new(),
14896 weak,
14897 })
14898 });
14899 seen.insert(idx, out.clone());
14900 let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
14901 for (k, val) in pairs {
14902 let ck = deep_clone_seen(&k, seen)?;
14903 let cv = deep_clone_seen(&val, seen)?;
14904 let _ = map_method(&out, "set", vec![ck, cv]);
14905 }
14906 out
14907 }
14908 Some(JsObj::Set { entries, weak }) => {
14909 let out = with_host(|h| {
14910 h.alloc(JsObj::Set {
14911 entries: IndexMap::new(),
14912 weak,
14913 })
14914 });
14915 seen.insert(idx, out.clone());
14916 let vals: Vec<Value> = entries.values().cloned().collect();
14917 for x in vals {
14918 let cx = deep_clone_seen(&x, seen)?;
14919 let _ = set_method(&out, "add", vec![cx]);
14920 }
14921 out
14922 }
14923 // A string, a BigInt and a boxed primitive are immutable enough to
14924 // share; anything left is a value type.
14925 _ => v.clone(),
14926 })
14927}
14928
14929/// Whether a cloned object keeps the source's prototype.
14930///
14931/// The structured clone algorithm reproduces the exotics it knows and turns
14932/// everything else into a plain object — so a `Date` clones to a `Date` and a
14933/// user class instance clones to an `Object`.
14934fn clone_proto(v: &Value) -> CloneProto {
14935 // An ERROR clones to the BUILT-IN class its `name` selects, so a subclass
14936 // flattens: `structuredClone(new (class E extends Error{})('m'))` reports
14937 // `Error`, not `E`.
14938 if with_host(|h| h.error_to_string(v)).is_some() {
14939 let name = get_property(v, "name")
14940 .map(|n| with_host(|h| h.str_of(&n)))
14941 .unwrap_or_else(|_| "Error".into());
14942 let class = if host::ERROR_NAMES.contains(&name.as_str()) {
14943 name
14944 } else {
14945 "Error".to_string()
14946 };
14947 return CloneProto::Ctor(class);
14948 }
14949 match crate::stdlib::native_tag(v).as_deref() {
14950 // A Buffer is not reproduced as a Buffer: node hands back a plain
14951 // `Uint8Array` over the same bytes.
14952 Some("Buffer") => CloneProto::Ctor("Uint8Array".into()),
14953 Some(_) => CloneProto::Same,
14954 // A boxed primitive keeps its wrapper; anything else — a user class
14955 // instance included — becomes a plain object.
14956 None if wrapped_primitive(v).is_some() => CloneProto::Same,
14957 None => CloneProto::Plain,
14958 }
14959}
14960
14961/// Which prototype a clone gets: the source's, a named builtin's, or none.
14962enum CloneProto {
14963 Same,
14964 Ctor(String),
14965 Plain,
14966}
14967
14968// ══ Promises, timers, microtasks (event-loop-driven) ═════════════════════════
14969
14970/// A short `Name: message` string for an error value (used when an await
14971/// rejection unwinds as a thrown error).
14972pub fn error_string(h: &host::JsHost, v: &Value) -> String {
14973 if let Some(JsObj::Object(props)) = h.get(v) {
14974 let name = props
14975 .get("name")
14976 .map(|x| h.str_of(x))
14977 .or_else(|| host::lookup_chain(h, v, "name").map(|x| h.str_of(&x)))
14978 .unwrap_or_else(|| "Error".into());
14979 if let Some(m) = props.get("message") {
14980 return format!("{name}: {}", h.str_of(m));
14981 }
14982 return name;
14983 }
14984 h.str_of(v)
14985}
14986
14987/// 27.2.5.3 `thenFinally`/`catchFinally`: `PromiseResolve(result).then(() =>
14988/// value)`, or `() => { throw reason }` on the reject path.
14989///
14990/// Returning the carried value directly — what this used to do — skipped both
14991/// halves. A promise returned by the callback was never awaited, so the
14992/// ordinary async-cleanup shape
14993///
14994/// ```text
14995/// work().finally(() => closeConnection()).then(next)
14996/// ```
14997///
14998/// ran `next` before the connection had closed. And the chain settled three
14999/// microtask ticks early, which is observable in ordering against any other
15000/// chain, not just against a timer.
15001///
15002/// A rejection from the callback's own promise wins over the carried value, so
15003/// no reject handler is attached here: it propagates on its own.
15004fn finally_chain(result: Value, carried: Value, rethrow: bool) -> Value {
15005 // PromiseResolve (27.2.4.7) returns an argument that is already a promise
15006 // UNCHANGED. Wrapping it anyway costs the extra tick that resolving with a
15007 // thenable takes to adopt it, which showed up as a callback returning a
15008 // rejected promise settling one tick late against every other chain.
15009 let p = match with_host(|h| h.promise_id(&result)) {
15010 Some(_) => result,
15011 None => {
15012 let fresh = with_host(|h| h.new_promise());
15013 if let Some(pid) = with_host(|h| h.promise_id(&fresh)) {
15014 host::resolve_promise_val(pid, result);
15015 }
15016 fresh
15017 }
15018 };
15019 let cell = with_host(|h| h.new_array(vec![carried]));
15020 let idx = match cell {
15021 Value::Obj(i) => i,
15022 _ => 0,
15023 };
15024 let tag = if rethrow { "finrethrow" } else { "finret" };
15025 let thunk = make_builtin(format!("@@{tag}:{idx}"));
15026 host::promise_then(&p, thunk, Value::Undef)
15027}
15028
15029fn make_builtin(name: String) -> Value {
15030 with_host(|h| h.alloc(JsObj::Builtin(name)))
15031}
15032
15033/// `[[GetPrototypeOf]]` (10.1.1) — the answer `Object.getPrototypeOf`,
15034/// `Reflect.getPrototypeOf` and a `__proto__` READ all have to agree on.
15035///
15036/// `__proto__` used to answer from `JsHost::proto_of` alone, which records only
15037/// an EXPLICIT link, so an object on the default prototype reported `null`:
15038/// `({}).__proto__ === Object.prototype` was false while
15039/// `Object.getPrototypeOf({}) === Object.prototype` was true. One function, so
15040/// the three cannot drift apart again.
15041pub fn prototype_of(v: &Value) -> Value {
15042 // Constructor-side inheritance: `Buffer extends Uint8Array`, so
15043 // `Object.getPrototypeOf(Buffer)` is the `Uint8Array` constructor itself,
15044 // not `Function.prototype`. This is the class-side half of the subclass
15045 // link — the instance-side half is `Buffer.prototype`'s `[[Prototype]]`.
15046 if matches!(with_host(|h| h.get(v).cloned()), Some(JsObj::Builtin(ref n)) if n == "Buffer") {
15047 return with_host(|h| h.alloc(JsObj::Builtin("Uint8Array".into())));
15048 }
15049 // Constructor-side inheritance for a `class B extends A` (ClassDefinition
15050 // 15.7.14 step 6.d: the constructor's `[[Prototype]]` is the parent
15051 // CONSTRUCTOR, not `Function.prototype`). Statics already resolved through
15052 // `ClassVal.parent`, but the link itself was invisible, so
15053 // `Object.getPrototypeOf(B) === A` read false and any library walking the
15054 // constructor chain — rather than calling a static — saw a base class.
15055 // A base class keeps the default answer below (`Function.prototype`).
15056 if let Some(JsObj::Class(c)) = with_host(|h| h.get(v).cloned()) {
15057 if let Some(parent) = c.parent {
15058 return parent;
15059 }
15060 }
15061 // `Object.create(null)` and friends really do have a null prototype.
15062 if with_host(|h| h.has_null_proto(v)) {
15063 return with_host(|h| h.null());
15064 }
15065 // `Object.prototype` is the CHAIN ROOT, so its own prototype is `null`. It
15066 // reported itself, because the fallback below answers by constructor name
15067 // and a plain object's is `Object` — an infinite chain to anything walking
15068 // it.
15069 if with_host(|h| h.strict_eq(v, &h.object_proto())) {
15070 return with_host(|h| h.null());
15071 }
15072 // Every OTHER builtin prototype namespace (`Array.prototype`,
15073 // `Function.prototype`, …) inherits from `Object.prototype`; the fallback
15074 // would send it back to a namespace handle for its own constructor.
15075 if matches!(
15076 with_host(|h| h.get(v).cloned()),
15077 Some(JsObj::Builtin(ref n)) if n.ends_with(".prototype")
15078 ) {
15079 return with_host(|h| h.object_proto());
15080 }
15081 if let Some(p) = with_host(|h| h.proto_of(v)) {
15082 return p;
15083 }
15084 // A builtin exotic with no explicit `[[Prototype]]` link reports its
15085 // constructor's prototype namespace (`Object.getPrototypeOf([]) ===
15086 // Array.prototype`), which `strict_eq` compares by name. A plain object
15087 // reports the one real `Object.prototype` object.
15088 with_host(|h| {
15089 h.ensure_native_protos();
15090 match default_ctor_name(h, v) {
15091 Some("Object") => h.object_proto(),
15092 // `String`/`Number`/`Boolean` own REAL prototype objects, so a
15093 // primitive must report that object and not a fresh namespace
15094 // thunk — otherwise `Object.getPrototypeOf(1) === Number.prototype`
15095 // compares a thunk against the real object and reads false.
15096 Some(c) => h
15097 .native_proto(c)
15098 .unwrap_or_else(|| h.alloc(JsObj::Builtin(format!("{c}.prototype")))),
15099 None => h.null(),
15100 }
15101 })
15102}
15103
15104/// `new Promise((resolve, reject) => …)` — run the executor synchronously with
15105/// internal resolve/reject functions.
15106/// A fresh promise built through the SPECIES constructor, when a `Promise`
15107/// static was reached through a subclass.
15108///
15109/// `class P extends Promise {}` makes `P.resolve(1)` a `P`, because every
15110/// combinator builds its result with `this` (27.2.4.x). They all allocated a
15111/// plain promise, so nothing a subclass produced was an instance of it. The
15112/// executor is a no-op: the result is settled through its promise id, which is
15113/// what the ordinary path does too.
15114fn promise_species_create() -> Result<Option<Value>, String> {
15115 let Some(ctor) = host::current_static_this() else {
15116 return Ok(None);
15117 };
15118 if !matches!(
15119 with_host(|h| h.kind_of(&ctor)),
15120 Some(ObjKind::Class) | Some(ObjKind::Func)
15121 ) {
15122 return Ok(None);
15123 }
15124 let species = match get_property(&ctor, "@@species") {
15125 Ok(Value::Undef) => ctor,
15126 Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15127 Ok(s) => s,
15128 Err(_) => ctor,
15129 };
15130 if !matches!(
15131 with_host(|h| h.kind_of(&species)),
15132 Some(ObjKind::Class) | Some(ObjKind::Func)
15133 ) {
15134 return Ok(None);
15135 }
15136 let noop = make_builtin("@@pnoop".to_string());
15137 let p = host::construct(&species, vec![noop])?;
15138 // Only usable if the subclass really produced a promise; a constructor that
15139 // returned something else has no id to settle.
15140 Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15141}
15142
15143/// The species constructor of a promise RECEIVER — what `then`/`catch`/`finally`
15144/// build their result with (`SpeciesConstructor(p, %Promise%)`, 27.2.5.4 step 3).
15145///
15146/// Distinct from `promise_species_create`, which answers for a STATIC reached
15147/// through a subclass. Here the subclass comes from the receiver itself, so
15148/// `P.resolve(1).then(f)` is also a `P`.
15149pub fn promise_species_from(recv: &Value) -> Result<Option<Value>, String> {
15150 // A chain lookup: a Promise receiver resolves through the stdlib funnel,
15151 // which has no `constructor` entry of its own.
15152 let ctor = with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef);
15153 if !matches!(
15154 with_host(|h| h.kind_of(&ctor)),
15155 Some(ObjKind::Class) | Some(ObjKind::Func)
15156 ) {
15157 return Ok(None);
15158 }
15159 let species = match get_property(&ctor, "@@species") {
15160 Ok(Value::Undef) => ctor,
15161 Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15162 Ok(s) => s,
15163 Err(_) => ctor,
15164 };
15165 if !matches!(
15166 with_host(|h| h.kind_of(&species)),
15167 Some(ObjKind::Class) | Some(ObjKind::Func)
15168 ) {
15169 return Ok(None);
15170 }
15171 let noop = make_builtin("@@pnoop".to_string());
15172 let p = host::construct(&species, vec![noop])?;
15173 Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15174}
15175
15176fn new_promise(executor: Value) -> Result<Value, String> {
15177 let p = with_host(|h| h.new_promise());
15178 let id = with_host(|h| h.promise_id(&p).unwrap());
15179 let res = make_builtin(format!("@@presolve:{id}"));
15180 let rej = make_builtin(format!("@@preject:{id}"));
15181 if let Err(e) = host::invoke(&executor, vec![res, rej], None) {
15182 // A throw in the executor rejects the promise.
15183 let ev = host::take_exc_or_error(&e);
15184 host::reject_promise_val(id, ev);
15185 }
15186 Ok(p)
15187}
15188
15189/// `Promise.resolve(v)` for stdlib callers that need to hand back an
15190/// already-settled promise.
15191pub fn promise_resolve_pub(v: Value) -> Result<Value, String> {
15192 promise_resolve(v)
15193}
15194
15195fn promise_resolve(v: Value) -> Result<Value, String> {
15196 if let Some(p) = promise_species_create()? {
15197 let id = with_host(|h| h.promise_id(&p).unwrap());
15198 host::resolve_promise_val(id, v);
15199 return Ok(p);
15200 }
15201 Ok(host::promise_of(&v))
15202}
15203fn promise_reject(v: Value) -> Result<Value, String> {
15204 let p = match promise_species_create()? {
15205 Some(p) => p,
15206 None => with_host(|h| h.new_promise()),
15207 };
15208 let id = with_host(|h| h.promise_id(&p).unwrap());
15209 host::reject_promise_val(id, v);
15210 Ok(p)
15211}
15212
15213/// `Promise.withResolvers()` — a fresh pending promise paired with its own
15214/// resolve/reject continuations (the same `@@presolve`/`@@preject` thunks the
15215/// executor receives), returned as a plain `{ promise, resolve, reject }` object.
15216/// A fresh pending promise paired with the thunk that resolves it, for stdlib
15217/// callers that hand the resolver to an event listener.
15218pub fn pending_promise_with_resolver() -> (Value, Value) {
15219 let p = with_host(|h| h.new_promise());
15220 let id = with_host(|h| h.promise_id(&p).unwrap());
15221 let resolve = make_builtin(format!("@@presolve:{id}"));
15222 (p, resolve)
15223}
15224
15225/// `RegExp.escape(s)` (22.2.4.2) — a string that matches `s` literally.
15226///
15227/// The rule is not "backslash the syntax characters": it also escapes a LEADING
15228/// ASCII alphanumeric, so the result can be concatenated after a `\` or a `{`
15229/// without the two running together, and it escapes the punctuation that is
15230/// meaningful inside a character class or a group name.
15231fn regexp_escape(args: Vec<Value>) -> Result<Value, String> {
15232 let v = arg0(&args);
15233 if !matches!(v, Value::Str(_)) && !with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_)))) {
15234 return Err(host::type_error("input argument must be a string"));
15235 }
15236 let s = with_host(|h| h.str_of(&v));
15237 // Punctuation that is escaped by CODE POINT rather than with a backslash.
15238 // Measured against node over the whole ASCII range, not taken from a list:
15239 // `-` and `=` are here, `$` and `*` are syntax characters and are not.
15240 const OTHER_PUNCTUATORS: &str = " !\"#%&',-:;<=>@`~";
15241 const SYNTAX: &str = "^$\\.*+?()[]{}|/";
15242 let mut out = String::with_capacity(s.len());
15243 for (i, c) in s.chars().enumerate() {
15244 // A leading ASCII alphanumeric, and only a leading one.
15245 if i == 0 && c.is_ascii_alphanumeric() {
15246 out.push_str(&format!("\\x{:02x}", c as u32));
15247 continue;
15248 }
15249 if SYNTAX.contains(c) {
15250 out.push('\\');
15251 out.push(c);
15252 continue;
15253 }
15254 match c {
15255 '\t' => out.push_str("\\t"),
15256 '\n' => out.push_str("\\n"),
15257 '\u{b}' => out.push_str("\\v"),
15258 '\u{c}' => out.push_str("\\f"),
15259 '\r' => out.push_str("\\r"),
15260 _ if OTHER_PUNCTUATORS.contains(c) || is_regex_escape_space(c) => {
15261 let n = c as u32;
15262 if n <= 0xff {
15263 out.push_str(&format!("\\x{n:02x}"));
15264 } else {
15265 out.push_str(&format!("\\u{n:04x}"));
15266 }
15267 }
15268 _ => out.push(c),
15269 }
15270 }
15271 Ok(with_host(|h| h.new_str(out)))
15272}
15273
15274/// The WhiteSpace and LineTerminator code points `RegExp.escape` spells out.
15275/// Deliberately NOT `char::is_whitespace`: U+180E and U+200B are whitespace to
15276/// Unicode but not to ECMAScript, and node leaves both alone.
15277fn is_regex_escape_space(c: char) -> bool {
15278 matches!(
15279 c,
15280 '\u{a0}' | '\u{1680}' | '\u{2000}'
15281 ..='\u{200a}'
15282 | '\u{2028}'
15283 | '\u{2029}'
15284 | '\u{202f}'
15285 | '\u{205f}'
15286 | '\u{3000}'
15287 | '\u{feff}'
15288 )
15289}
15290
15291/// `Error.isError(v)` (20.5.2.1) — a brand check for `[[ErrorData]]`, so an
15292/// object that merely INHERITS from `Error.prototype` is not one.
15293fn error_is_error(args: Vec<Value>) -> Result<Value, String> {
15294 let v = arg0(&args);
15295 Ok(Value::Bool(with_host(|h| has_error_data(h, &v))))
15296}
15297
15298/// Whether `v` carries `[[ErrorData]]` — the slot `Error.isError` (20.5.2.1)
15299/// and `Object.prototype.toString`'s step 9 both test.
15300///
15301/// The brand is the OWN `stack` an error is built with (a `DOMException`
15302/// carries `@@domName` instead); a plain `Object.create(Error.prototype)` has
15303/// neither, which is why inheriting from an error prototype does not make a
15304/// value an error. Shared so the two cannot disagree — branding by a chain
15305/// lookup for `name`/`message` made `Object.create(Error.prototype)` report
15306/// `[object Error]` where node says `[object Object]`, while `Error.isError`
15307/// on the same value already said false.
15308pub(crate) fn has_error_data(h: &host::JsHost, v: &Value) -> bool {
15309 match h.get(v) {
15310 Some(JsObj::Object(p)) => {
15311 p.contains_key("stack") || p.contains_key("@@stackRaw") || p.contains_key("@@domName")
15312 }
15313 _ => false,
15314 }
15315}
15316
15317/// `Promise.try(fn, ...args)` (27.2.4.6) — call `fn` and settle the promise with
15318/// what it does, so a SYNCHRONOUS throw becomes a rejection instead of
15319/// propagating. `Promise.resolve().then(fn)` is the shape it replaces, and it
15320/// costs a tick that this does not.
15321fn promise_try(args: Vec<Value>) -> Result<Value, String> {
15322 let f = arg0(&args);
15323 // A non-callable argument REJECTS, it does not throw: `Promise.try(5)`
15324 // returns a rejected promise, so the surrounding `try` never sees it.
15325 if !with_host(|h| host::is_callable(h, &f)) {
15326 // Node names the TYPE alongside the value — `number 5 is not a
15327 // function` — which the ordinary call-site message does not. A plain
15328 // object and a symbol name only the type; `null` names both.
15329 let shown = with_host(|h| {
15330 let kind = h.type_of(&f);
15331 match kind {
15332 "undefined" => "undefined".to_string(),
15333 "symbol" | "bigint" => kind.to_string(),
15334 "object" if h.is_null(&f) => "object null".to_string(),
15335 "object" => "object".to_string(),
15336 "string" => format!("string \"{}\"", h.str_of(&f)),
15337 _ => format!("{kind} {}", h.str_of(&f)),
15338 }
15339 });
15340 let p = with_host(|h| h.new_promise());
15341 let id = with_host(|h| h.promise_id(&p).unwrap());
15342 let reject = make_builtin(format!("@@preject:{id}"));
15343 let err =
15344 with_host(|h| synth_error(h, &host::type_error(&format!("{shown} is not a function"))));
15345 host::invoke(&reject, vec![err], None)?;
15346 return Ok(p);
15347 }
15348 let rest: Vec<Value> = args.iter().skip(1).cloned().collect();
15349 let p = with_host(|h| h.new_promise());
15350 let id = with_host(|h| h.promise_id(&p).unwrap());
15351 let resolve = make_builtin(format!("@@presolve:{id}"));
15352 let reject = make_builtin(format!("@@preject:{id}"));
15353 let promise = p;
15354 match host::invoke(&f, rest, None) {
15355 Ok(v) => {
15356 host::invoke(&resolve, vec![v], None)?;
15357 }
15358 Err(e) => {
15359 // The thrown VALUE, not a re-synthesis of its rendering: a callback
15360 // that throws a `TypeError` must reject with that object, and
15361 // rebuilding it from the message string flattened it to a plain
15362 // `Error` whose message was the rendered `Uncaught TypeError: t`.
15363 let err =
15364 with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15365 with_host(|h| {
15366 h.error = None;
15367 h.exc = None;
15368 });
15369 host::invoke(&reject, vec![err], None)?;
15370 }
15371 }
15372 Ok(promise)
15373}
15374
15375fn promise_with_resolvers() -> Result<Value, String> {
15376 let p = with_host(|h| h.new_promise());
15377 let id = with_host(|h| h.promise_id(&p).unwrap());
15378 let resolve = make_builtin(format!("@@presolve:{id}"));
15379 let reject = make_builtin(format!("@@preject:{id}"));
15380 let mut props: IndexMap<String, Value> = IndexMap::new();
15381 props.insert("promise".into(), p);
15382 props.insert("resolve".into(), resolve);
15383 props.insert("reject".into(), reject);
15384 Ok(with_host(|h| h.new_object(props)))
15385}
15386
15387/// A promise already rejected with `e` — what every combinator hands back when
15388/// the ITERABLE misbehaves.
15389///
15390/// 27.2.4.1 step 4 catches an abrupt completion from the iteration and rejects
15391/// rather than letting it propagate, so `Promise.all(badIterable)` returns a
15392/// rejected promise. Throwing synchronously meant a `.catch()` never attached
15393/// and the caller saw the error at the call site instead.
15394fn rejected_promise(e: String) -> Value {
15395 let p = with_host(|h| h.new_promise());
15396 let id = with_host(|h| h.promise_id(&p).unwrap());
15397 let reject = make_builtin(format!("@@preject:{id}"));
15398 let err = with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15399 with_host(|h| {
15400 h.error = None;
15401 h.exc = None;
15402 });
15403 let _ = host::invoke(&reject, vec![err], None);
15404 p
15405}
15406
15407#[derive(Clone, Copy)]
15408enum AllMode {
15409 All,
15410 AllSettled,
15411}
15412
15413/// `Promise.all` / `Promise.allSettled`.
15414fn promise_all(args: Vec<Value>, mode: AllMode) -> Result<Value, String> {
15415 let items = match host::iter_all(&arg0(&args)) {
15416 Ok(v) => v,
15417 Err(e) => return Ok(rejected_promise(e)),
15418 };
15419 // 27.2.4.1 step 3: the combinator builds its result with `this`, so on a
15420 // subclass the promise it hands back is an instance of that subclass.
15421 let result = match promise_species_create()? {
15422 Some(p) => p,
15423 None => with_host(|h| h.new_promise()),
15424 };
15425 let rid = with_host(|h| h.promise_id(&result).unwrap());
15426 let n = items.len();
15427 if n == 0 {
15428 let empty = with_host(|h| h.new_array(Vec::new()));
15429 host::resolve_promise_val(rid, empty);
15430 return Ok(result);
15431 }
15432 // Shared mutable accumulator via Rc<RefCell<…>>.
15433 let slots = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15434 let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15435 for (i, it) in items.into_iter().enumerate() {
15436 let ap = host::promise_of(&it);
15437 let aid = with_host(|h| h.promise_id(&ap).unwrap());
15438 let slots = slots.clone();
15439 let remaining = remaining.clone();
15440 host::subscribe_native(
15441 aid,
15442 Box::new(move |state, val| {
15443 let settled = match mode {
15444 AllMode::All => {
15445 if state == host::PromiseState::Rejected {
15446 host::reject_promise_val(rid, val);
15447 return Ok(());
15448 }
15449 val
15450 }
15451 AllMode::AllSettled => with_host(|h| {
15452 let mut m: IndexMap<String, Value> = IndexMap::new();
15453 if state == host::PromiseState::Rejected {
15454 m.insert("status".into(), h.new_str("rejected"));
15455 m.insert("reason".into(), val);
15456 } else {
15457 m.insert("status".into(), h.new_str("fulfilled"));
15458 m.insert("value".into(), val);
15459 }
15460 h.new_object(m)
15461 }),
15462 };
15463 slots.borrow_mut()[i] = settled;
15464 let mut r = remaining.borrow_mut();
15465 *r -= 1;
15466 if *r == 0 {
15467 let arr = with_host(|h| h.new_array(slots.borrow().clone()));
15468 host::resolve_promise_val(rid, arr);
15469 }
15470 Ok(())
15471 }),
15472 );
15473 }
15474 Ok(result)
15475}
15476
15477/// `Promise.race` (first to settle wins) / `Promise.any` (first to fulfill wins).
15478fn promise_race(args: Vec<Value>, any: bool) -> Result<Value, String> {
15479 let items = match host::iter_all(&arg0(&args)) {
15480 Ok(v) => v,
15481 Err(e) => return Ok(rejected_promise(e)),
15482 };
15483 // Built with `this`, as every combinator is (27.2.4.5 / 27.2.4.3).
15484 let result = match promise_species_create()? {
15485 Some(p) => p,
15486 None => with_host(|h| h.new_promise()),
15487 };
15488 let rid = with_host(|h| h.promise_id(&result).unwrap());
15489 let n = items.len();
15490 let errors = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15491 let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15492 for (i, it) in items.into_iter().enumerate() {
15493 let ap = host::promise_of(&it);
15494 let aid = with_host(|h| h.promise_id(&ap).unwrap());
15495 let errors = errors.clone();
15496 let remaining = remaining.clone();
15497 host::subscribe_native(
15498 aid,
15499 Box::new(move |state, val| {
15500 if any {
15501 if state == host::PromiseState::Fulfilled {
15502 host::resolve_promise_val(rid, val);
15503 } else {
15504 errors.borrow_mut()[i] = val;
15505 let mut r = remaining.borrow_mut();
15506 *r -= 1;
15507 if *r == 0 {
15508 // All rejected → AggregateError carrying every reason.
15509 let reasons = with_host(|h| h.new_array(errors.borrow().clone()));
15510 let msg = with_host(|h| h.new_str("All promises were rejected"));
15511 let agg = make_error_inner("AggregateError", &[reasons, msg]);
15512 host::reject_promise_val(rid, agg);
15513 }
15514 }
15515 } else if state == host::PromiseState::Rejected {
15516 host::reject_promise_val(rid, val);
15517 } else {
15518 host::resolve_promise_val(rid, val);
15519 }
15520 Ok(())
15521 }),
15522 );
15523 }
15524 Ok(result)
15525}
15526
15527/// `.then` / `.catch` / `.finally` on a promise.
15528fn promise_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
15529 match name {
15530 "then" => Ok(host::promise_then(
15531 recv,
15532 args.first().cloned().unwrap_or(Value::Undef),
15533 args.get(1).cloned().unwrap_or(Value::Undef),
15534 )),
15535 "catch" => Ok(host::promise_then(
15536 recv,
15537 Value::Undef,
15538 args.first().cloned().unwrap_or(Value::Undef),
15539 )),
15540 "finally" => {
15541 let cb = arg0(&args);
15542 // 27.2.5.3 step 3: a non-callable `onFinally` is handed to `then`
15543 // as BOTH handlers, and `then` ignores a non-callable one — so the
15544 // value or reason simply passes through. Building the thunks
15545 // regardless meant `p.finally(null)` tried to call `null`.
15546 if !with_host(|h| host::is_callable(h, &cb)) {
15547 return Ok(host::promise_then(recv, cb.clone(), cb));
15548 }
15549 let i = match cb {
15550 Value::Obj(i) => i,
15551 _ => 0,
15552 };
15553 let pass = make_builtin(format!("@@finpass:{i}"));
15554 let throw = make_builtin(format!("@@finthrow:{i}"));
15555 Ok(host::promise_then(recv, pass, throw))
15556 }
15557 _ => Err(host::type_error(&format!(
15558 "promise.{name} is not a function"
15559 ))),
15560 }
15561}
15562
15563fn enqueue_microtask(next_tick: bool, cb: Value, args: Vec<Value>) {
15564 with_host(|h| {
15565 if next_tick {
15566 h.queue_nexttick(cb, args);
15567 } else {
15568 h.queue_micro(cb, args);
15569 }
15570 });
15571}
15572
15573/// `setTimeout`/`setInterval`/`setImmediate` — register a macrotask and return
15574/// the handle object Node returns (`Timeout` for the first two, `Immediate` for
15575/// the third), carrying `ref`/`unref`/`hasRef`/`refresh`.
15576///
15577/// `setInterval` schedules a *repeating* timer: the loop re-arms it each time it
15578/// fires, so it runs until cleared and — being referenced — holds the process
15579/// open exactly as in Node.
15580fn schedule_timer(name: &str, args: Vec<Value>) -> Value {
15581 let cb = arg0(&args);
15582 let delay = if name == "setImmediate" {
15583 -1.0 // before any 0ms timeout
15584 } else {
15585 args.get(1)
15586 .map(|d| with_host(|h| h.to_number(d)))
15587 .unwrap_or(0.0)
15588 .max(0.0)
15589 };
15590 let extra = if name == "setImmediate" {
15591 args.get(1..).map(|s| s.to_vec()).unwrap_or_default()
15592 } else {
15593 args.get(2..).map(|s| s.to_vec()).unwrap_or_default()
15594 };
15595 // Node clamps a sub-1ms interval to 1ms, so `setInterval(fn, 0)` yields a
15596 // ~1000Hz timer rather than a busy loop that starves the rest of the queue.
15597 let interval = (name == "setInterval").then(|| delay.max(1.0));
15598 let id = with_host(|h| h.add_timer(delay, cb, extra, interval));
15599 let tag = if name == "setImmediate" {
15600 "Immediate"
15601 } else {
15602 "Timeout"
15603 };
15604 crate::stdlib::timers::new_handle(id, tag)
15605}
15606
15607/// `clearTimeout`/`clearInterval`/`clearImmediate` — cancel by handle object or
15608/// by the bare id it coerces to (code that stored `+timer` still works).
15609fn clear_timer(v: &Value) {
15610 let id =
15611 crate::stdlib::timers::handle_id(v).unwrap_or_else(|| with_host(|h| h.to_number(v)) as u64);
15612 with_host(|h| h.cancel_timer(id));
15613}