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, _: u8) -> Value {
327 let ctor = vm.pop();
328 let parent = vm.pop();
329 let name = sval(&vm.pop());
330 host::build_class(&name, parent, ctor)
331}
332
333fn b_def_member(vm: &mut VM, _: u8) -> Value {
334 let func = vm.pop();
335 let is_static = matches!(vm.pop(), Value::Bool(true));
336 let kind = match vm.pop() {
337 Value::Int(n) => n,
338 _ => 0,
339 };
340 let name = sval(&vm.pop());
341 let class_val = vm.pop();
342 host::define_member(&class_val, &name, kind, is_static, func);
343 class_val
344}
345
346fn b_def_field(vm: &mut VM, _: u8) -> Value {
347 // `name_anon`: the initializer was an anonymous function definition, so
348 // 15.7.10 NamedEvaluation names its result after the field. Syntactic —
349 // decided by the compiler, not re-derived from the produced value.
350 let name_anon = matches!(vm.pop(), Value::Bool(true));
351 let thunk = vm.pop();
352 let name = sval(&vm.pop());
353 let class_val = vm.pop();
354 host::define_field(&class_val, &name, thunk, name_anon);
355 class_val
356}
357
358/// `super(...args)` in a derived constructor: run the parent constructor on the
359/// current `this`, then this class's field initializers.
360/// `SUPER_CALL_SPREAD` — `super(...xs)`, where the argument list is built at
361/// run time. Shares everything below with the fixed-arity form; only where the
362/// arguments come from differs.
363fn b_super_call_spread(vm: &mut VM, _: u8) -> Value {
364 let arr = vm.pop();
365 let args = host::iter_all(&arr).unwrap_or_default();
366 super_call_with(vm, args)
367}
368
369fn b_super_call(vm: &mut VM, argc: u8) -> Value {
370 let args = pop_n(vm, argc as usize);
371 super_call_with(vm, args)
372}
373
374fn super_call_with(vm: &mut VM, args: Vec<Value>) -> Value {
375 let this = with_host(|h| h.current_this());
376 let this = match this {
377 Some(t) => t,
378 None => return abort(vm, host::type_error("'super' keyword unexpected here")),
379 };
380 // The class whose constructor is running = the running method's home class.
381 let (parent, fields) = with_host(|h| h.super_context());
382 let (parent, fields) = match parent {
383 Some(p) => (p, fields),
384 None => return abort(vm, host::type_error("'super' keyword unexpected here")),
385 };
386 let nt = with_host(|h| h.current_new_target()).unwrap_or_else(|| this.clone());
387 let this = match host::super_construct(&parent, args, &this, &nt) {
388 Err(e) => return abort(vm, e),
389 // The parent returned an object of its own: 15.7.15 makes THAT the
390 // instance, so `this` is rebound to it for the rest of the constructor
391 // and it is what `new` hands back.
392 Ok(Some(replacement)) => {
393 with_host(|h| h.set_current_this(replacement.clone()));
394 replacement
395 }
396 Ok(None) => this,
397 };
398 if !with_host(|h| h.bind_super_this()) {
399 return abort(
400 vm,
401 "ReferenceError: Super constructor may only be called once".to_string(),
402 );
403 }
404 // Run this (derived) class's own instance-field initializers after super.
405 for (name, thunk, name_anon) in fields {
406 if let Err(e) = host::init_one_field(&this, &name, &thunk, name_anon) {
407 return abort(vm, e);
408 }
409 }
410 Value::Undef
411}
412
413/// `super.name` — a method from the parent's prototype, or a getter's result.
414fn b_super_get(vm: &mut VM, _: u8) -> Value {
415 let name = sval(&vm.pop());
416 match with_host(|h| h.super_resolve(&name)) {
417 host::SuperRef::Data(v) => v,
418 host::SuperRef::Getter(getter) => {
419 let this = with_host(|h| h.current_this());
420 match host::invoke(&getter, Vec::new(), this) {
421 Ok(v) => v,
422 Err(e) => abort(vm, e),
423 }
424 }
425 }
426}
427
428/// Close every loop iterator parked on `vm`'s stack at the op now executing,
429/// innermost first. Called where a chunk is about to be halted abruptly, since
430/// the code that would ordinarily close them is being jumped over.
431///
432/// A close runs user code (a generator's `finally`), which can itself throw; the
433/// error is deliberately dropped, because it must not replace the completion
434/// that caused the unwind.
435fn close_parked_iters(vm: &mut VM) {
436 let n = host::parked_iters(vm);
437 if n == 0 {
438 return;
439 }
440 // The completion that caused the unwind is already pending on the host.
441 // Closing an iterator resumes ANOTHER generator, which settles its own
442 // signal/error state, so the pending one is saved across the close and put
443 // back — otherwise the outer `.return()` would be lost.
444 let saved = with_host(|h| (h.signal.take(), h.error.take()));
445 for _ in 0..n {
446 let it = vm.pop();
447 let _ = close_iterator(&it);
448 }
449 with_host(|h| {
450 h.signal = saved.0;
451 h.error = saved.1;
452 });
453}
454
455fn b_yield(vm: &mut VM, _: u8) -> Value {
456 let v = vm.pop();
457 match host::gen_yield(v) {
458 Ok(sent) => {
459 // A `.return()`/`.throw()` injected on resume sets a pending Return
460 // signal (or error); halt the chunk so the body unwinds through any
461 // enclosing `try/finally`, exactly like a source `return`/`throw`.
462 if with_host(|h| h.error.is_some() || h.signal.is_some()) {
463 // Halting jumps past the loop exits, so the `for…of` / `yield*`
464 // iterators parked on this chunk's stack would be abandoned
465 // still-suspended. They sit directly beneath the yielded value
466 // (innermost last), and the compiler recorded how many are
467 // there for this exact op.
468 close_parked_iters(vm);
469 vm.ip = vm.chunk.ops.len();
470 }
471 sent
472 }
473 // An injected `.throw()` comes back as an error rather than a signal,
474 // and abandons the parked iterators the same way. The thrown value is
475 // already on the host as `exc`; `close_parked_iters` puts back whatever
476 // it saves, so the close cannot swallow it.
477 Err(e) => {
478 close_parked_iters(vm);
479 abort(vm, e)
480 }
481 }
482}
483
484/// `PROPKEY` — ToPropertyKey (7.1.19) for an object literal's COMPUTED key.
485///
486/// It called `JsHost::property_key` directly, which is the primitive-only half
487/// of the conversion, so an object key never ran `ToPrimitive`:
488/// `{ [{toString(){return "TS"}}]: 1 }` keyed on `"[object Object]"` while the
489/// member form `a[o] = 1` — which does go through `host::to_property_key` —
490/// keyed on `"TS"`. The two forms are the same abstract operation and now share
491/// the same implementation.
492fn b_propkey(vm: &mut VM, _: u8) -> Value {
493 let v = vm.pop();
494 match host::to_property_key(&v) {
495 Ok(k) => with_host(|h| h.new_str(k)),
496 Err(e) => abort(vm, e),
497 }
498}
499
500fn b_new_target(_vm: &mut VM, _: u8) -> Value {
501 with_host(|h| h.current_new_target().unwrap_or(Value::Undef))
502}
503
504/// `a / b` with JS/IEEE-754 semantics. fusevm's native `Op::Div` returns `Undef`
505/// for a zero divisor (so a frontend whose `/` differs must lower to a builtin —
506/// its own documented guidance), but JavaScript requires `x/0 === ±Infinity` and
507/// `0/0 === NaN`, so `/` is lowered here instead.
508///
509/// Being a builtin rather than a native op means it does NOT reach the numeric
510/// hook, so `/` was the one arithmetic operator that never ran `ToPrimitive`:
511/// `({valueOf(){return 7}}) / 2` was `NaN` where every other operator gave
512/// `3.5`, and `new Date(2) / 1` was `NaN` instead of `2`. It goes through the
513/// hook now, so `/` coerces exactly as `*` and `-` do.
514fn b_div(vm: &mut VM, _: u8) -> Value {
515 let b = vm.pop();
516 let a = vm.pop();
517 let r = numeric_hook(NumOp::Div, &a, &b);
518 finish(vm, r)
519}
520
521/// `a ** b`. Same reason `/` is a builtin: fusevm's native `Op::Pow` is IEEE-754
522/// `pow`, which returns 1 for `(-1) ** Infinity` and for `1 ** NaN` where the
523/// spec says NaN. Routing through the numeric hook also keeps BigInt `**` on the
524/// one code path that already handles it.
525fn b_pow(vm: &mut VM, _: u8) -> Value {
526 let b = vm.pop();
527 let a = vm.pop();
528 let r = numeric_hook(NumOp::Pow, &a, &b);
529 finish(vm, r)
530}
531
532/// `{ ...rest } = obj`: a new object of `obj`'s own keys minus the excluded set.
533fn b_obj_rest(vm: &mut VM, _: u8) -> Value {
534 let excluded = vm.pop();
535 let obj = vm.pop();
536 // The excluded keys are normalized exactly as a property READ normalizes
537 // them, not merely stringified: a symbol key lives on the object under its
538 // internal `@@sym:<id>` spelling, and `str_of` renders it `Symbol(k)`, which
539 // matches no key at all — so `const { [sym]: v, ...rest } = o` left the
540 // symbol-keyed property in `rest`.
541 let excl: Vec<String> = with_host(|h| h.iter_vec(&excluded))
542 .unwrap_or_default()
543 .iter()
544 .filter_map(|v| host::to_property_key(v).ok())
545 .collect();
546 // CopyDataProperties (ECMA-262 7.3.25) copies the own ENUMERABLE keys,
547 // symbol-keyed ones included. `own_enum_key_names` is what `Object.keys`
548 // uses, so an ACCESSOR is in the list — reading the property map directly
549 // missed one entirely, and `const { ...r } = { get g() {…} }` produced an
550 // object with no `g` and never ran the getter.
551 // A PROXY answers from its traps — `ownKeys`, then a
552 // `getOwnPropertyDescriptor` per key to test enumerability — which
553 // `own_enum_key_names` cannot see. Rest over one produced an empty object
554 // and ran no traps at all.
555 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
556 let keys = match crate::proxy::own_keys(&obj) {
557 Ok(k) => k.unwrap_or_default(),
558 Err(e) => return abort(vm, e),
559 };
560 let mut pairs: Vec<(String, Value)> = Vec::new();
561 for k in keys {
562 if excl.contains(&k) {
563 continue;
564 }
565 // The enumerability test and the READ interleave per key, as node's
566 // trap log shows — testing every key first and then reading them
567 // all produced the right object through the wrong trap sequence.
568 match crate::proxy::own_enumerable(&obj, &k) {
569 Ok(false) => continue,
570 Ok(true) => {}
571 Err(e) => return abort(vm, e),
572 }
573 match get_property(&obj, &k) {
574 Ok(v) => pairs.push((k, v)),
575 Err(e) => return abort(vm, e),
576 }
577 }
578 return with_host(|h| h.new_object(pairs.into_iter().collect()));
579 }
580 let keys: Vec<String> = with_host(|h| {
581 // `own_enum_key_names` is the STRING half — the same list `Object.keys`
582 // gives, so an accessor is in it. The symbol-keyed half lives in the
583 // property map under the internal `@@sym:` spelling and has to be
584 // collected separately, since `Object.keys` deliberately omits it.
585 let mut ks = h.own_enum_key_names(&obj);
586 if let Some(JsObj::Object(m)) = h.get(&obj) {
587 for k in m.keys() {
588 if host::is_symbol_key(k) && h.prop_attrs(&obj, k).enumerable {
589 ks.push(k.clone());
590 }
591 }
592 }
593 ks
594 })
595 .into_iter()
596 .filter(|k| {
597 // An internal slot (`@@native`, `@@bytes`, …) or a private class field
598 // is not a property; a SYMBOL key shares the `@@` prefix but is one, so
599 // the two cases cannot be told apart by the prefix alone.
600 !excl.contains(k)
601 && (host::is_symbol_key(k) || !(k.starts_with("@@") || k.starts_with('#')))
602 })
603 .collect();
604 // Each value is read through `[[Get]]`, OUTSIDE the host borrow: a getter is
605 // user code and re-entering the VM under the borrow aborts the process.
606 let mut pairs: Vec<(String, Value)> = Vec::with_capacity(keys.len());
607 for k in keys {
608 match get_property(&obj, &k) {
609 Ok(v) => pairs.push((k, v)),
610 Err(e) => return abort(vm, e),
611 }
612 }
613 with_host(|h| {
614 let props: IndexMap<String, Value> = pairs.into_iter().collect();
615 h.new_object(props)
616 })
617}
618
619// ── helpers ──────────────────────────────────────────────────────────────────
620
621fn pop_n(vm: &mut VM, n: usize) -> Vec<Value> {
622 let mut v = Vec::with_capacity(n);
623 for _ in 0..n {
624 v.push(vm.pop());
625 }
626 v.reverse();
627 v
628}
629
630/// Read a compiler-internal name string (native `Value::Str` or heap `str`).
631fn sval(v: &Value) -> String {
632 if let Value::Str(s) = v {
633 return (**s).clone();
634 }
635 with_host(|h| h.as_str(v)).unwrap_or_default()
636}
637
638/// The same string, without `sval`'s deep copy. Every identifier the compiler
639/// emits is a `Value::Str` constant, so a variable read or write that went
640/// through `sval` heap-allocated and memcpy'd the NAME once per access — on the
641/// hot path of every loop. `Value::Str` is an `Arc<String>`, so cloning the
642/// handle is a refcount bump instead.
643fn sname(v: &Value) -> std::sync::Arc<String> {
644 match v {
645 Value::Str(s) => s.clone(),
646 _ => std::sync::Arc::new(sval(v)),
647 }
648}
649
650fn abort(vm: &mut VM, e: String) -> Value {
651 with_host(|h| h.error = Some(e));
652 vm.ip = vm.chunk.ops.len();
653 Value::Undef
654}
655
656/// Halt the chunk if a call left an error or non-local signal pending.
657fn finish(vm: &mut VM, r: Result<Value, String>) -> Value {
658 match r {
659 Ok(v) => {
660 if with_host(|h| h.error.is_some() || h.signal.is_some()) {
661 vm.ip = vm.chunk.ops.len();
662 }
663 v
664 }
665 Err(e) => abort(vm, e),
666 }
667}
668
669// ── name handlers ─────────────────────────────────────────────────────────────
670
671/// The value a bare global identifier resolves to, or `None` if unbound.
672///
673/// Shared by `b_getlocal` (the `x` form) and the `globalThis.x` property read,
674/// which must agree: a name reachable one way and not the other is exactly the
675/// discrepancy that left `globalThis.process` undefined while `process` worked.
676pub(crate) fn global_binding(name: &str) -> Option<Value> {
677 global_binding_from(name, false)
678}
679
680/// [`global_binding`] restricted to what the GLOBAL OBJECT really holds.
681///
682/// A `globalThis.x` read falls back to the same lazy binding a bare `x` gets,
683/// which is what makes `globalThis.Math` and `globalThis.process` work — but
684/// the bare-identifier lookup walks the SCOPE CHAIN, so while any function was
685/// running its locals were readable off `globalThis`: `function f() { let zzq =
686/// 2; return typeof globalThis.zzq }` answered for a name the global object has
687/// never heard of. Only the globals map and the lazy builtins below may answer
688/// here.
689pub(crate) fn global_object_binding(name: &str) -> Option<Value> {
690 global_binding_from(name, true)
691}
692
693fn global_binding_from(name: &str, object_only: bool) -> Option<Value> {
694 let bound = with_host(|h| {
695 if object_only {
696 h.read_global(name)
697 } else {
698 h.read_name(name)
699 }
700 });
701 if let Some(v) = bound {
702 return Some(v);
703 }
704 // Globals bound lazily: numeric sentinels + builtin namespaces.
705 match name {
706 "undefined" => return Some(Value::Undef),
707 "NaN" => return Some(Value::Float(f64::NAN)),
708 "Infinity" => return Some(Value::Float(f64::INFINITY)),
709 // One object, not a fresh one per read: `globalThis === globalThis` is
710 // `true` in JS, and `globalThis.x = 1` is readable back as
711 // `globalThis.x`. Both were false while each read minted a new object.
712 // `global` is Node's alias for the same object.
713 "globalThis" | "global" => return Some(with_host(|h| h.global_object())),
714 // The WHATWG `crypto` global IS `require('crypto').webcrypto`, not the
715 // node-flavoured module: `globalThis.crypto.randomUUID` exists while
716 // `globalThis.crypto.createHash` does not.
717 "crypto" => return Some(with_host(|h| h.alloc(JsObj::Builtin("webcrypto".into())))),
718 _ => {}
719 }
720 if is_namespace(name) || is_known_builtin(name) {
721 return Some(with_host(|h| h.alloc(JsObj::Builtin(name.to_string()))));
722 }
723 None
724}
725
726fn b_getlocal(vm: &mut VM, _: u8) -> Value {
727 let name = sname(&vm.pop());
728 // A module-top-level dead zone is tracked by NAME rather than by a parked
729 // marker, so that the marker is never reachable as `globalThis.<name>`. It
730 // only applies when nothing on the scope chain SHADOWS the name — a class's
731 // own inner binding for its name does exactly that while its static
732 // initializers run.
733 if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
734 return abort(vm, host::tdz_error(&name));
735 }
736 match global_binding(&name) {
737 // The binding EXISTS but has not reached its declaration yet.
738 Some(v) if with_host(|h| h.is_tdz(&v)) => abort(vm, host::tdz_error(&name)),
739 Some(v) => v,
740 None => abort(vm, host::ref_error(&name)),
741 }
742}
743
744/// `HOIST_TDZ` — declare one `let`/`const`/`class` name as uninitialized at the
745/// top of the scope that declares it.
746fn b_hoist_tdz(vm: &mut VM, _: u8) -> Value {
747 let name = sname(&vm.pop());
748 with_host(|h| h.hoist_tdz(&name));
749 Value::Undef
750}
751
752/// The three global VALUE properties that are `{writable: false}` (19.1.1-19.1.3).
753/// Assigning to one is a silent no-op in sloppy code and a `TypeError` in strict
754/// code — and, either way, never rebinds the name.
755const READONLY_GLOBALS: [&str; 3] = ["undefined", "NaN", "Infinity"];
756
757fn readonly_global_error(name: &str) -> String {
758 host::type_error(&format!(
759 "Cannot assign to read only property '{name}' of object '#<Object>'"
760 ))
761}
762
763fn b_setlocal(vm: &mut VM, _: u8) -> Value {
764 let val = vm.pop();
765 let name = sname(&vm.pop());
766 // Sloppy assignment to a non-writable global is DISCARDED, not applied:
767 // `undefined = 1` used to rebind the name and make every later `undefined`
768 // read back as `1`.
769 if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
770 return val;
771 }
772 // Assigning to a binding still in its temporal dead zone throws too —
773 // `{ x = 1; let x }` is a ReferenceError, not an initialization.
774 if with_host(|h| match h.read_name(&name) {
775 Some(v) => h.is_tdz(&v),
776 None => h.is_tdz_global(&name),
777 }) {
778 return abort(vm, host::tdz_error(&name));
779 }
780 // An assignment to a `const` binding throws (8.5.2 SetMutableBinding on an
781 // immutable binding). This used to succeed silently.
782 if !with_host(|h| h.set_name(&name, val.clone())) {
783 return abort(vm, host::type_error("Assignment to constant variable."));
784 }
785 val
786}
787
788/// Strict-mode `x = v` (6.2.5.6 `PutValue` with an unresolvable reference):
789/// where sloppy code silently creates a global, strict code throws
790/// `ReferenceError: x is not defined`.
791///
792/// A separate opcode rather than a runtime flag: strictness is a static property
793/// of the code, so the compiler already knows which of the two an assignment is
794/// and sloppy code — everything in a CommonJS module without the directive —
795/// keeps the exact instruction it had.
796fn b_setlocal_strict(vm: &mut VM, _: u8) -> Value {
797 let val = vm.pop();
798 let name = sname(&vm.pop());
799 if !binding_exists(&name) {
800 return abort(vm, host::ref_error(&name));
801 }
802 if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
803 return abort(vm, readonly_global_error(&name));
804 }
805 if !with_host(|h| h.set_name(&name, val.clone())) {
806 return abort(vm, host::type_error("Assignment to constant variable."));
807 }
808 val
809}
810
811/// Whether `name` resolves to anything — a scope binding, a global, or a lazily
812/// materialised builtin namespace. `global_binding` answers the same question
813/// but ALLOCATES the namespace object to do it, which an assignment then throws
814/// away.
815fn binding_exists(name: &str) -> bool {
816 if with_host(|h| h.has_name(name)) {
817 return true;
818 }
819 matches!(
820 name,
821 "undefined" | "NaN" | "Infinity" | "globalThis" | "global"
822 ) || is_namespace(name)
823 || is_known_builtin(name)
824}
825
826fn b_declare(vm: &mut VM, _: u8) -> Value {
827 let val = vm.pop();
828 let name = sname(&vm.pop());
829 with_host(|h| h.declare_name(&name, val.clone()));
830 val
831}
832
833/// `const x = …`: like `DECLARE`, but the binding is immutable, so a later
834/// assignment to the name throws instead of overwriting it.
835fn b_declare_const(vm: &mut VM, _: u8) -> Value {
836 let val = vm.pop();
837 let name = sname(&vm.pop());
838 with_host(|h| h.declare_const_name(&name, val.clone()));
839 val
840}
841
842/// `var x = …` / a hoisted `function f(){}`: bind at function scope, skipping any
843/// open block scopes, so the name outlives the block it was written in.
844/// `var` hoisting: create the binding as `undefined` unless it already exists.
845fn b_hoist_var(vm: &mut VM, _: u8) -> Value {
846 let name = sname(&vm.pop());
847 with_host(|h| h.hoist_var_name(&name));
848 Value::Undef
849}
850
851fn b_declare_var(vm: &mut VM, _: u8) -> Value {
852 let val = vm.pop();
853 let name = sname(&vm.pop());
854 with_host(|h| h.declare_var_name(&name, val.clone()));
855 val
856}
857
858fn b_push_scope(_: &mut VM, _: u8) -> Value {
859 with_host(|h| h.push_scope());
860 Value::Undef
861}
862
863fn b_pop_scope(_: &mut VM, _: u8) -> Value {
864 with_host(|h| h.pop_scope());
865 Value::Undef
866}
867
868fn b_copy_scope(_: &mut VM, _: u8) -> Value {
869 with_host(|h| h.copy_scope());
870 Value::Undef
871}
872
873fn b_delname(vm: &mut VM, _: u8) -> Value {
874 let name = sval(&vm.pop());
875 with_host(|h| h.del_name(&name));
876 Value::Bool(true)
877}
878
879fn b_this(vm: &mut VM, _: u8) -> Value {
880 if with_host(|h| h.this_state()) == host::ThisState::Pending {
881 return abort(vm, host::this_before_super_error());
882 }
883 with_host(|h| h.current_this().unwrap_or(Value::Undef))
884}
885
886fn b_load_null(_vm: &mut VM, _: u8) -> Value {
887 with_host(|h| h.null())
888}
889
890// ── attribute / item handlers ─────────────────────────────────────────────────
891
892fn b_getattr(vm: &mut VM, _: u8) -> Value {
893 let name = sval(&vm.pop());
894 let recv = vm.pop();
895 match get_property(&recv, &name) {
896 Ok(v) => v,
897 Err(e) => abort(vm, e),
898 }
899}
900
901/// Read `recv.name` (also the computed-key path for string keys). Walks own
902/// properties, accessors, and the prototype chain (class methods / getters).
903/// Read one small piece out of `recv`'s heap cell under a short borrow.
904///
905/// The closure must not call back into the host (`with_host` is a `RefCell`
906/// borrow and re-entering panics) — which is exactly why it hands back only the
907/// value needed: the caller re-enters freely afterwards. This replaces the old
908/// `h.get(recv).cloned()` habit, which deep-copied a whole `Vec`/`IndexMap`/
909/// `String` just to look at it.
910fn peek<R>(recv: &Value, f: impl FnOnce(&JsObj) -> Option<R>) -> Option<R> {
911 with_host(|h| h.get(recv).and_then(f))
912}
913
914/// The nearest `[[Prototype]]` link of `recv` that is a Proxy, when the chain
915/// reaches it without a closer link already owning `name`.
916///
917/// A proxy prototype answers only from the position it occupies in the chain: a
918/// nearer prototype that owns the key (as a data property or an accessor) still
919/// wins, exactly as `OrdinaryGet` walks one link at a time.
920pub(crate) fn proxy_proto_link(recv: &Value, name: &str) -> Option<Value> {
921 with_host(|h| {
922 let mut cur = h.proto_of(recv);
923 for _ in 0..100 {
924 let p = cur?;
925 match h.get(&p) {
926 Some(JsObj::Proxy { .. }) => return Some(p),
927 Some(JsObj::Object(props)) if props.contains_key(name) => return None,
928 _ => {}
929 }
930 if h.own_accessor(&p, name).is_some() {
931 return None;
932 }
933 cur = h.proto_of(&p);
934 }
935 None
936 })
937}
938
939/// The CommonJS wrapper's parameters. They are function locals in Node, not
940/// global-object properties, so `globalThis.require` is `undefined` and
941/// `Object.getOwnPropertyDescriptor(globalThis, 'module')` reports no property —
942/// even though the bare `require` and `module` both work.
943const CJS_WRAPPER_LOCALS: &[&str] = &[
944 "require",
945 "module",
946 "exports",
947 "__filename",
948 "__dirname",
949 "__cjs_require",
950 "__cjs_resolve",
951];
952
953/// The globals node exposes as ENUMERABLE own properties of the global object —
954/// the timer family and the WHATWG additions, measured on v26.8.1. Everything
955/// else (`Math`, `parseInt`, the constructors) is non-enumerable.
956const ENUMERABLE_GLOBALS: &[&str] = &[
957 "global",
958 "clearImmediate",
959 "setImmediate",
960 "clearInterval",
961 "clearTimeout",
962 "setInterval",
963 "setTimeout",
964 "queueMicrotask",
965 "structuredClone",
966 "atob",
967 "btoa",
968 "performance",
969 "fetch",
970 "crypto",
971 "navigator",
972 "sessionStorage",
973];
974
975pub fn get_property(recv: &Value, name: &str) -> Result<Value, String> {
976 // A `#`-prefixed key is a PRIVATE name. `[[PrivateGet]]` (7.3.31) throws
977 // when the receiver carries no such private element — it does NOT read back
978 // as `undefined`, which is what `C.prototype.method.call({})` used to do.
979 if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
980 return Err(private_brand_message(name, false));
981 }
982 get_property_recv(recv, name, recv)
983}
984
985/// The `TypeError` a failed private brand check raises. Node words it two ways:
986/// a private METHOD or accessor names the class the receiver should have been an
987/// instance of, while a private FIELD names the member.
988pub fn private_brand_message(name: &str, writing: bool) -> String {
989 if with_host(|h| h.is_private_method(name)) {
990 if let Some(class) = with_host(|h| h.current_home_class_name()) {
991 return host::type_error(&format!("Receiver must be an instance of class {class}"));
992 }
993 }
994 let verb = if writing { "write" } else { "read" };
995 let prep = if writing { "to" } else { "from" };
996 host::type_error(&format!(
997 "Cannot {verb} private member {name} {prep} an object whose class did not declare it"
998 ))
999}
1000
1001/// `[[Get]](name, receiver)` — 10.1.8. `receiver` is the object the read STARTED
1002/// from and is what a getter sees as `this`; it differs from `recv` only when the
1003/// read was forwarded down a prototype chain, which is why `Reflect.get(t, k, r)`
1004/// and a Proxy `get` trap's third argument both need it. Every ordinary read
1005/// passes `recv` itself.
1006/// Re-format an error's `.stack` header on its first read, the way V8 does.
1007///
1008/// The constructor could only stamp the name it was called with, so a subclass
1009/// that sets `this.name` after `super()` — or any `e.name = …` / `e.message = …`
1010/// before the first read — left a stale header. Node re-reads both properties at
1011/// format time, including one inherited from the prototype (`E.prototype.name`).
1012///
1013/// It is formatted ONCE: node caches the string, so renaming AFTER a read does
1014/// not change what later reads return. `@@stackRaw` is the not-yet-formatted
1015/// marker and is dropped here; an explicit `e.stack = …` drops it too, so an
1016/// assignment is never clobbered by a later read.
1017/// The key of node's DEFAULT `Error.prepareStackTrace`. Recognised by name so
1018/// the ordinary stack path can skip the hook round-trip when nothing custom is
1019/// installed.
1020pub const DEFAULT_PREPARE: &str = "ErrorPrepareStackTrace";
1021
1022pub fn materialize_stack(recv: &Value) {
1023 let Some(frames) = with_host(|h| match h.get(recv) {
1024 Some(JsObj::Object(p)) => p.get("@@stackRaw").cloned(),
1025 _ => None,
1026 }) else {
1027 return;
1028 };
1029 // A custom `Error.prepareStackTrace` replaces the string entirely (V8's
1030 // stack-introspection hook, which every source-map library installs). It was
1031 // honoured only by `Error.captureStackTrace`, so an ordinary `err.stack`
1032 // read bypassed it and handed back the default text.
1033 let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
1034 if let Some(f) = prep.filter(|f| {
1035 // The default hook produces exactly what the fast path below produces,
1036 // so it is skipped rather than called.
1037 !matches!(
1038 with_host(|h| h.get(f).cloned()),
1039 Some(JsObj::Builtin(ref n)) if n == DEFAULT_PREPARE
1040 ) && matches!(
1041 with_host(|h| h.get(f).cloned()),
1042 Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
1043 )
1044 }) {
1045 // Clear the raw marker FIRST: the hook may read `.stack` itself, and a
1046 // second materialization would re-enter this path forever.
1047 with_host(|h| {
1048 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1049 p.shift_remove("@@stackRaw");
1050 }
1051 });
1052 let limit = with_host(|h| h.stack_trace_limit());
1053 if let Ok(sites) = crate::module::callsite_stack(limit) {
1054 if let Ok(out) = host::invoke(&f, vec![recv.clone(), sites], None) {
1055 with_host(|h| {
1056 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1057 p.insert("stack".into(), out);
1058 }
1059 });
1060 return;
1061 }
1062 }
1063 }
1064 with_host(|h| {
1065 let frames = h.str_of(&frames);
1066 let name = host::lookup_chain(h, recv, "name")
1067 .map(|v| h.str_of(&v))
1068 .unwrap_or_else(|| "Error".to_string());
1069 let message = host::lookup_chain(h, recv, "message")
1070 .map(|v| h.str_of(&v))
1071 .unwrap_or_default();
1072 let header = if message.is_empty() {
1073 name
1074 } else {
1075 format!("{name}: {message}")
1076 };
1077 let sv = h.new_str(format!("{header}{frames}"));
1078 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1079 p.insert("stack".into(), sv);
1080 p.shift_remove("@@stackRaw");
1081 }
1082 });
1083}
1084
1085pub fn get_property_recv(recv: &Value, name: &str, receiver: &Value) -> Result<Value, String> {
1086 // `[[Get]]` on a Proxy: the handler's `get` trap, or a forward to the
1087 // target. Checked before anything else so no ordinary-object shortcut can
1088 // read past the handler.
1089 if let Some(v) = crate::proxy::get(recv, name, receiver)? {
1090 return Ok(v);
1091 }
1092 if with_host(|h| h.is_nullish(recv)) {
1093 return Err(host::type_error(&format!(
1094 "Cannot read properties of {} (reading '{name}')",
1095 with_host(|h| h.str_of(recv))
1096 )));
1097 }
1098 if name == "stack" {
1099 materialize_stack(recv);
1100 }
1101 // A `DOMException`'s `name`/`message`/`code` are prototype accessors over
1102 // internal slots, so they resolve here rather than out of a property map.
1103 if let Some(v) = dom_exception_slot(recv, name) {
1104 return Ok(v);
1105 }
1106 // A read off `globalThis` for a name the object does not own falls back to
1107 // the same lazy global binding the bare identifier gets. Without it the
1108 // global object was an empty bag: `globalThis.process`, `.console`, `.Math`
1109 // and `.JSON` were all `undefined`, so `process === globalThis.process` was
1110 // `false` and any `globalThis.X` feature probe reported the feature missing.
1111 if with_host(|h| h.is_global_object(recv)) {
1112 let own = with_host(|h| match h.get(recv) {
1113 Some(JsObj::Object(p)) => p.contains_key(name),
1114 _ => false,
1115 });
1116 // The CommonJS wrapper's parameters are function locals in Node, not
1117 // global-object properties: `typeof globalThis.require` is `undefined`
1118 // there even though the bare `require` works.
1119 if !own && !CJS_WRAPPER_LOCALS.contains(&name) {
1120 if let Some(v) = global_object_binding(name) {
1121 return Ok(v);
1122 }
1123 }
1124 }
1125 // Accessor (own or inherited getter) takes precedence over the chain walk.
1126 // The getter runs with the RECEIVER as `this`, not the object that owns it.
1127 if let Some((getter, _)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
1128 return match getter {
1129 Some(g) => host::invoke(&g, Vec::new(), Some(receiver.clone())),
1130 None => Ok(Value::Undef), // set-only property reads as undefined
1131 };
1132 }
1133 // `Symbol.toStringTag` read as an ordinary property. The builtins that carry
1134 // one expose it to a plain read, not just to `Object.prototype.toString` —
1135 // `new Uint8Array(1)[Symbol.toStringTag]` is `'Uint8Array'`, and a `Buffer`
1136 // inherits `'Uint8Array'` from the typed-array prototype it now really has.
1137 // Anything the receiver's own chain provides wins (a class may define its
1138 // own getter), so this is only the fallback.
1139 if name == "@@toStringTag" && with_host(|h| host::lookup_chain(h, recv, name)).is_none() {
1140 if let Some(tag) = with_host(|h| well_known_tag(h, recv)) {
1141 return Ok(with_host(|h| h.new_str(tag)));
1142 }
1143 }
1144 // `constructor`: a user class/function sets it on the prototype chain, and
1145 // that wins; otherwise every builtin instance reports its native
1146 // constructor (so `[].constructor`, `new Map().constructor`,
1147 // `Promise.resolve(1).constructor`, `(5).constructor` match Node).
1148 if name == "constructor" {
1149 if let Some(v) = with_host(|h| {
1150 match h.get(recv) {
1151 Some(JsObj::Object(p)) => p.get("constructor").cloned(),
1152 _ => None,
1153 }
1154 .or_else(|| host::lookup_chain(h, recv, "constructor"))
1155 }) {
1156 return Ok(v);
1157 }
1158 // An intrinsic prototype the receiver's CHAIN reaches owns a
1159 // `constructor` too, and it wins over the receiver's own kind:
1160 // `Object.create(Map.prototype).constructor` is `Map`, not `Object`.
1161 // Deciding from the kind alone also mis-named the receiver in every
1162 // message that renders one — the brand-check errors say `#<Map>`.
1163 if let Some(c) = chain_intrinsic_ctors(recv)
1164 .into_iter()
1165 .find(|c| is_builtin_ctor(c))
1166 {
1167 return Ok(with_host(|h| h.alloc(JsObj::Builtin(c.to_string()))));
1168 }
1169 if let Some(cn) = with_host(|h| default_ctor_name(h, recv)) {
1170 return Ok(with_host(|h| h.alloc(JsObj::Builtin(cn.to_string()))));
1171 }
1172 }
1173 // `__proto__` (Annex B B.2.2.1) is an accessor on `Object.prototype`, so it
1174 // answers for EVERY object that inherits from it, not only plain ones —
1175 // `[].__proto__` is `Array.prototype`. Only the plain-object arm handled it,
1176 // so an array, function or builtin instance read `undefined`. An object with
1177 // a null prototype inherits no such accessor and reads `undefined`, which is
1178 // why this is skipped there rather than answering `null`.
1179 if name == "__proto__"
1180 && !with_host(|h| h.has_null_proto(recv))
1181 && peek(recv, |o| match o {
1182 JsObj::Object(p) => Some(p.contains_key("__proto__")),
1183 _ => Some(false),
1184 }) != Some(true)
1185 {
1186 return Ok(prototype_of(recv));
1187 }
1188 // An ACCESSOR member read off the intrinsic prototype ITSELF is not a
1189 // method: it RUNS the getter with that prototype as `this`, and all but two
1190 // of `RegExp.prototype`'s then fail their brand check and throw. Every one
1191 // answered `undefined`, so both the value and the failure were invisible.
1192 // Both representations of a prototype reach here — the namespace handles
1193 // and the real objects (`Symbol.prototype`, `String.prototype`).
1194 if let Some(ctor) = intrinsic_proto_of(recv) {
1195 if is_proto_accessor(&ctor, name) {
1196 return proto_getter_call(&ctor, name, recv);
1197 }
1198 }
1199 let kind = with_host(|h| h.kind_of(recv));
1200 #[allow(unused_mut)]
1201 let mut out = match kind {
1202 Some(ObjKind::Object) => {
1203 let numeric = !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit());
1204 // A view over a DETACHED buffer reports zero extent. Its own
1205 // `length`/`byteLength`/`byteOffset` properties still hold the old
1206 // numbers — the buffer does not know its views, so it cannot rewrite
1207 // them — and reading them straight back made a detached view still
1208 // look eight bytes long.
1209 if matches!(name, "length" | "byteLength" | "byteOffset")
1210 && crate::stdlib::typedarray::view_detached(recv)
1211 {
1212 match crate::stdlib::native_tag(recv).as_deref() {
1213 Some("TypedArray") => return Ok(Value::Float(0.0)),
1214 // A DataView THROWS where a typed array answers zero — its
1215 // extent accessors are brand-checked and node reports the
1216 // getter by name.
1217 Some("DataView") => {
1218 return Err(crate::stdlib::typedarray::detached_error(
1219 "get DataView.prototype",
1220 name,
1221 false,
1222 ))
1223 }
1224 _ => {}
1225 }
1226 }
1227 // Typed-array element read (`ta[i]`): elements live in a hidden
1228 // `@@elems`, not as own numeric props, so intercept integer keys.
1229 if numeric && crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray") {
1230 if let Some(v) = crate::stdlib::typedarray::elem_get(recv, name) {
1231 return Ok(v);
1232 }
1233 }
1234 // `buf[i]`: a Buffer's bytes live in a hidden `@@bytes` array, not as
1235 // own numeric props, so integer keys read through to it.
1236 if numeric
1237 && peek(recv, |o| match o {
1238 JsObj::Object(p) => Some(p.contains_key("@@bytes")),
1239 _ => None,
1240 })
1241 .unwrap_or(false)
1242 {
1243 return Ok(crate::stdlib::buffer::byte_get(recv, name));
1244 }
1245 if let Some(v) = peek(recv, |o| match o {
1246 JsObj::Object(p) => p.get(name).cloned(),
1247 _ => None,
1248 }) {
1249 v
1250 } else if let Some(link) = proxy_proto_link(recv, name) {
1251 // A Proxy sitting in the prototype chain. `OrdinaryGet` (10.1.8.1
1252 // step 4) forwards to the parent's `[[Get]]` with the ORIGINAL
1253 // receiver, so the trap sees the child as `receiver` and `this`
1254 // inside a trap-served getter resolves to the child, not the
1255 // proxy. `lookup_chain` cannot do this: it reads property maps,
1256 // and a proxy has none.
1257 return Ok(crate::proxy::get(&link, name, recv)?.expect("link is a proxy"));
1258 } else if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1259 // A method / data property inherited from the prototype chain.
1260 v
1261 } else if crate::stdlib::native_tag(recv)
1262 .map(|tag| crate::stdlib::instance_has_method(&tag, name))
1263 .unwrap_or(false)
1264 {
1265 // A native instance method read as a property (`server.listen`) →
1266 // a bound method, dispatched via `instance_call` when invoked.
1267 bound_method(recv, name)
1268 } else if is_object_method(name) && !with_host(|h| h.has_null_proto(recv)) {
1269 // `Object.create(null)` inherits nothing, so `toString`/`valueOf`
1270 // read as `undefined` there — which is also what makes
1271 // `Object.create(null) + 1` the spec `TypeError` instead of a
1272 // silent `"[object Object]1"`.
1273 bound_method(recv, name)
1274 } else {
1275 Value::Undef
1276 }
1277 }
1278 Some(ObjKind::Class) | Some(ObjKind::Func) | Some(ObjKind::BoundFunc) => {
1279 function_property(recv, name)
1280 }
1281 // A method READ off an instance (`[].slice`, `new Map().get`) is a bound
1282 // thunk here. It is a function value, so it answers the function
1283 // properties: `[].slice.name` was `undefined` where node reports
1284 // `slice`, and `String([].slice)` fell through to
1285 // `Object.prototype.toString`.
1286 Some(ObjKind::BoundMethod) => bound_method_property(recv, name),
1287 Some(ObjKind::Symbol) => match name {
1288 "description" => {
1289 match peek(recv, |o| match o {
1290 JsObj::Symbol { desc, .. } => desc.clone(),
1291 _ => None,
1292 }) {
1293 Some(d) => with_host(|h| h.new_str(d)),
1294 None => Value::Undef,
1295 }
1296 }
1297 "toString" => bound_method(recv, name),
1298 // Anything else a symbol answers, it inherits from
1299 // `Symbol.prototype`. The arm used to stop at `undefined`, so
1300 // `Symbol('x')[Symbol.toPrimitive]` and `Symbol('x').valueOf` read
1301 // as absent even though the prototype defines both — a symbol is an
1302 // ordinary object for the purpose of a property LOOKUP, only its
1303 // methods are branded.
1304 _ => with_host(|h| {
1305 h.ensure_wrapper_protos();
1306 h.native_proto("Symbol")
1307 })
1308 .and_then(|p| with_host(|h| host::lookup_chain(h, &p, name)))
1309 .unwrap_or(Value::Undef),
1310 },
1311 Some(ObjKind::BigInt) => {
1312 if matches!(
1313 name,
1314 "toString" | "valueOf" | "toLocaleString" | "constructor"
1315 ) {
1316 bound_method(recv, name)
1317 } else {
1318 Value::Undef
1319 }
1320 }
1321 Some(ObjKind::RegExp) => {
1322 // A RegExp holds no collection, so cloning the compiled pattern here
1323 // does not scale with any input size; `regexp_property` re-enters the
1324 // host to allocate `source`/`flags`, so it cannot run under a borrow.
1325 let r = peek(recv, |o| match o {
1326 JsObj::RegExp(r) => Some(r.clone()),
1327 _ => None,
1328 });
1329 match r {
1330 Some(r) => crate::regexp::regexp_property(&r, name).unwrap_or_else(|| {
1331 // An OWN property beats the prototype method of the same
1332 // name, which is ordinary resolution order. It mattered once
1333 // the symbol-keyed methods existed: `re[Symbol.match] =
1334 // false` disowns the regexp label (7.2.8), and the method
1335 // was shadowing the assignment so the value never took.
1336 if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1337 return v;
1338 }
1339 if crate::regexp::is_regexp_method(name) {
1340 bound_method(recv, name)
1341 } else {
1342 Value::Undef
1343 }
1344 }),
1345 None => Value::Undef,
1346 }
1347 }
1348 // A WeakMap/WeakSet has NO `size` (its contents are not observable), so
1349 // the read must be `undefined` rather than a live count.
1350 Some(ObjKind::Map) => {
1351 let (len, weak) = peek(recv, |o| match o {
1352 JsObj::Map { entries, weak } => Some((entries.len(), *weak)),
1353 _ => None,
1354 })
1355 .unwrap_or((0, false));
1356 match name {
1357 "size" if !weak => Value::Float(len as f64),
1358 "@@iterator" => bound_method(recv, name),
1359 _ if is_map_method(name) => bound_method(recv, name),
1360 _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1361 }
1362 }
1363 Some(ObjKind::Set) => {
1364 let (len, weak) = peek(recv, |o| match o {
1365 JsObj::Set { entries, weak } => Some((entries.len(), *weak)),
1366 _ => None,
1367 })
1368 .unwrap_or((0, false));
1369 match name {
1370 "size" if !weak => Value::Float(len as f64),
1371 "@@iterator" => bound_method(recv, name),
1372 _ if is_set_method(name) => bound_method(recv, name),
1373 _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1374 }
1375 }
1376 Some(ObjKind::Generator) => {
1377 // A generator IS its own iterator, so it answers for the matching
1378 // symbol — `@@asyncIterator` for an async one, `@@iterator` for a
1379 // sync one. Neither was advertised, so `ag()[Symbol.asyncIterator]`
1380 // was `undefined` even though `for await` over it worked through a
1381 // different path.
1382 let want = if with_host(|h| h.is_async_gen_val(recv)) {
1383 "@@asyncIterator"
1384 } else {
1385 "@@iterator"
1386 };
1387 if name == want || is_generator_method(name) || crate::stdlib::iterator::is_helper(name)
1388 {
1389 bound_method(recv, name)
1390 } else {
1391 with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1392 }
1393 }
1394 Some(ObjKind::Promise) => {
1395 if matches!(name, "then" | "catch" | "finally") {
1396 bound_method(recv, name)
1397 } else {
1398 with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1399 }
1400 }
1401 Some(ObjKind::Iter) => {
1402 if matches!(name, "next" | "return" | "@@iterator")
1403 || crate::stdlib::iterator::is_helper(name)
1404 {
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::Array) => {
1411 if name == "length" {
1412 let n = peek(recv, |o| match o {
1413 JsObj::Array(items) => Some(items.len()),
1414 _ => None,
1415 })
1416 .unwrap_or(0);
1417 Value::Float(n as f64)
1418 } else if let Ok(i) = name.parse::<usize>() {
1419 peek(recv, |o| match o {
1420 JsObj::Array(items) => items.get(i).cloned(),
1421 _ => None,
1422 })
1423 // An index PAST an `arguments` object's length is an ordinary
1424 // own property in the side table, since adding one must not
1425 // move `length`. The array read alone could not see it, so the
1426 // write was invisible to every later read.
1427 .or_else(|| with_host(|h| h.fn_prop(recv, name)))
1428 .unwrap_or(Value::Undef)
1429 } else if name == "@@iterator"
1430 || is_object_method(name)
1431 // An `arguments` object is array-BACKED here but is not an
1432 // Array: node's exposes no `Array.prototype` method, which is
1433 // exactly why the idiom is `Array.prototype.slice.call(args)`.
1434 // Exposing them made `arguments.map` a function.
1435 || (is_array_method(name) && !is_arguments(recv))
1436 {
1437 bound_method(recv, name)
1438 } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1439 // Extra own props attached to an array (e.g. `RegExp.exec` result's
1440 // `.index`/`.input`/`.groups`).
1441 v
1442 } else {
1443 Value::Undef
1444 }
1445 }
1446 Some(ObjKind::Str) => {
1447 // `.length` and `s[i]` count UTF-16 code units, not code points.
1448 if name == "length" {
1449 let n = peek(recv, |o| match o {
1450 JsObj::Str(s) => Some(crate::utf16::len(s)),
1451 _ => None,
1452 })
1453 .unwrap_or(0);
1454 Value::Float(n as f64)
1455 } else if let Ok(i) = name.parse::<usize>() {
1456 match peek(recv, |o| match o {
1457 JsObj::Str(s) => crate::utf16::Units::of(s).unit_str(i),
1458 _ => None,
1459 }) {
1460 Some(c) => with_host(|h| h.new_str(c)),
1461 None => Value::Undef,
1462 }
1463 } else if name == "@@iterator" || is_string_method(name) {
1464 bound_method(recv, name)
1465 } else {
1466 Value::Undef
1467 }
1468 }
1469 Some(ObjKind::Builtin) => {
1470 let ns = peek(recv, |o| match o {
1471 JsObj::Builtin(ns) => Some(ns.clone()),
1472 _ => None,
1473 })
1474 .unwrap_or_default();
1475 let v = namespace_property(&ns, name);
1476 // `Function.prototype`'s methods READ off a builtin function. The
1477 // CALL forms (`Math.max.call(null, 1, 2)`) already dispatched, but
1478 // the read answered `undefined` — so `typeof Math.max.bind` was
1479 // `"undefined"`, and `String(Math.max)` found no `toString` to
1480 // invoke and fell back to `Object.prototype.toString`'s
1481 // `[object Function]` where node reports the native-code form.
1482 if matches!(v, Value::Undef)
1483 && is_function_method(name)
1484 && host::builtin_is_callable(&ns)
1485 {
1486 return Ok(bound_method(recv, name));
1487 }
1488 v
1489 }
1490 _ => {
1491 // Primitive numbers/booleans: method access -> bound method.
1492 if matches!(recv, Value::Float(_) | Value::Int(_)) && is_number_method(name) {
1493 bound_method(recv, name)
1494 } else {
1495 Value::Undef
1496 }
1497 }
1498 };
1499 // Every object INHERITS the `Object.prototype` methods, and each kind's
1500 // read arm above knows only its OWN. So `typeof new Map().toString`,
1501 // `typeof f.hasOwnProperty` and `typeof /a/.propertyIsEnumerable` all
1502 // answered `undefined` — for Map the CALL already worked, which is the
1503 // read and the dispatch disagreeing about the same method.
1504 //
1505 // Which prototype owns the name is decided by the same helper the `in`
1506 // operator uses, so the two cannot drift, and the result is the SHARED
1507 // intrinsic rather than a per-read thunk.
1508 // `arguments.callee` (and `.caller`) is a POISON PILL in strict code — the
1509 // accessor throws rather than answering, which is how a strict function
1510 // keeps its caller unreachable. It read back as `undefined` here, which a
1511 // feature probe reads as "not supported" rather than "forbidden".
1512 // Measured: on an ARGUMENTS object only `callee` is poisoned (`caller` is
1513 // simply absent and reads `undefined`); on a strict FUNCTION both `caller`
1514 // and `arguments` are.
1515 if name == "callee" && is_arguments(recv) && with_host(|h| h.current_strict()) {
1516 return Err(host::type_error(POISON_PILL));
1517 }
1518 if matches!(name, "caller" | "arguments")
1519 && matches!(
1520 with_host(|h| h.kind_of(recv)),
1521 Some(ObjKind::Func) | Some(ObjKind::Class)
1522 )
1523 {
1524 return poison_pill_read(recv);
1525 }
1526 // `arguments.callee` in SLOPPY code is the running function — the
1527 // pre-`class` self-reference idiom. It read back `undefined`.
1528 if name == "callee" && is_arguments(recv) {
1529 if let Some(f) = with_host(|h| h.fn_prop(recv, "@@callee")) {
1530 return Ok(f);
1531 }
1532 }
1533 // A method SYNTHESIZED from the receiver's kind is only reachable while the
1534 // receiver's intrinsic prototype is still on its chain. `Object
1535 // .setPrototypeOf(a, {})` must make `a.join` `undefined`; the kind arm
1536 // above answers from the kind alone and cannot know the link changed. Only
1537 // a synthesized value is dropped — the two shapes a method read produces —
1538 // and only when the receiver does not own the name itself.
1539 if matches!(
1540 with_host(|h| h.get(&out).cloned()),
1541 Some(JsObj::BoundMethod { .. })
1542 ) || matches!(
1543 with_host(|h| h.get(&out).cloned()),
1544 Some(JsObj::Builtin(ns)) if ns.starts_with("@proto:")
1545 ) {
1546 // The kind arms synthesize their OWN kind's methods, so that is the
1547 // prototype whose reachability decides. Clearing the value here lets
1548 // the `inherited_method_owner` fallback below re-supply the
1549 // `Object.prototype` form where one exists — which is why
1550 // `a.toString` stays a function after the link is replaced while
1551 // `a.join` does not.
1552 if !own_intrinsic_reachable(recv) && !has_own_for_shadow(recv, name) {
1553 out = Value::Undef;
1554 }
1555 }
1556 // A key the receiver does not OWN is looked up on its prototype chain. The
1557 // exotic arms above answer from their own storage and stop, so an array
1558 // given a prototype inherited nothing through a read: with
1559 // `Object.setPrototypeOf(a, {1: 'q'})`, `a[1]` was `undefined` at an elided
1560 // index and at one past the end, while `1 in a` already answered true —
1561 // the two views of the same question disagreeing. An accessor was found
1562 // (`lookup_accessor` walks), so only DATA properties went missing.
1563 //
1564 // A plain object's arm already consults the chain, and an array with no
1565 // explicit prototype has no links to walk, so this changes neither.
1566 if !name.starts_with('#') && !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
1567 if matches!(out, Value::Undef) {
1568 if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1569 return Ok(v);
1570 }
1571 }
1572 // Then a monkey-patched intrinsic prototype member, which shadows the
1573 // synthesized one: after `Array.prototype.join = f`, `[1, 2].join` must
1574 // BE `f`. An explicitly-set prototype above wins over it, as the chain
1575 // order requires.
1576 if let Some(v) = inherited_builtin_static(recv, name) {
1577 return Ok(v);
1578 }
1579 }
1580 if matches!(out, Value::Undef) && !name.starts_with('#') {
1581 if let Some(owner) = inherited_method_owner(recv, name) {
1582 // An INHERITED accessor runs, it does not hand back a thunk, and
1583 // its brand check is about the receiver's internal slot rather than
1584 // its chain — `Object.create(Map.prototype).size` throws in node
1585 // even though `Map.prototype` is right there above it. This
1586 // answered `undefined`, which is the value a real Map would never
1587 // give and a plain object should never reach.
1588 if is_proto_accessor(owner, name) && !getter_in_flight(owner, name) {
1589 return proto_getter_call(owner, name, recv);
1590 }
1591 let key = format!("@proto:{owner}:{name}");
1592 if builtin_meta(&key).is_some() {
1593 return Ok(with_host(|h| h.alloc(JsObj::Builtin(key))));
1594 }
1595 // A DATA member of the prototype — `Array.prototype[Symbol
1596 // .unscopables]` is an object, not a method, so it is in neither
1597 // function table. Read it off the prototype itself rather than
1598 // answering `undefined`: an instance inherits it.
1599 let v = namespace_property(&format!("{owner}.prototype"), name);
1600 if !matches!(v, Value::Undef) {
1601 return Ok(v);
1602 }
1603 }
1604 }
1605 Ok(out)
1606}
1607
1608/// The namespace name of the `require.cache` view. A `Builtin` rather than an
1609/// object literal because the module cache is the single source of truth: a
1610/// populated copy would answer reads correctly and silently ignore a `delete`,
1611/// which is the operation the property exists for.
1612pub const REQUIRE_CACHE: &str = "__cjs_cache";
1613
1614/// The builtin constructor name for a value with no own/inherited `constructor`
1615/// property, so `x.constructor` (and thus `x.constructor.name`) matches Node for
1616/// arrays, plain objects, Map/Set, promises, iterators, functions, and boxed
1617/// primitives. `None` ⇒ leave `.constructor` as `undefined` (e.g. generators,
1618/// whose `.constructor.name` is `""` in Node — not worth modelling).
1619fn default_ctor_name(h: &host::JsHost, recv: &Value) -> Option<&'static str> {
1620 match h.get(recv) {
1621 Some(JsObj::Array(_)) => Some("Array"),
1622 Some(JsObj::Object(props)) => {
1623 // A native instance reports its own constructor, not Object — e.g.
1624 // `qs` does `buf.constructor.isBuffer(buf)`, so a Buffer's
1625 // `.constructor` must be `Buffer` (which carries `isBuffer`). Read
1626 // the `@@native` tag off the already-borrowed host (calling
1627 // `native_tag`, which re-enters `with_host`, would double-borrow).
1628 match props.get("@@native").map(|t| h.str_of(t)).as_deref() {
1629 Some("Buffer") => Some("Buffer"),
1630 Some("URL") => Some("URL"),
1631 Some("Date") => Some("Date"),
1632 Some("WeakRef") => Some("WeakRef"),
1633 Some("FinalizationRegistry") => Some("FinalizationRegistry"),
1634 Some("TextEncoder") => Some("TextEncoder"),
1635 Some("TextDecoder") => Some("TextDecoder"),
1636 Some("EventEmitter") => Some("EventEmitter"),
1637 Some("Timeout") => Some("Timeout"),
1638 Some("Immediate") => Some("Immediate"),
1639 _ => Some("Object"),
1640 }
1641 }
1642 Some(JsObj::Map { weak, .. }) => Some(if *weak { "WeakMap" } else { "Map" }),
1643 Some(JsObj::Set { weak, .. }) => Some(if *weak { "WeakSet" } else { "Set" }),
1644 Some(JsObj::Promise { .. }) => Some("Promise"),
1645 Some(JsObj::Str(_)) => Some("String"),
1646 Some(JsObj::Symbol { .. }) => Some("Symbol"),
1647 Some(JsObj::BigInt(_)) => Some("BigInt"),
1648 Some(JsObj::RegExp(_)) => Some("RegExp"),
1649 Some(JsObj::Iter { .. }) => Some("Iterator"),
1650 Some(JsObj::Func(f)) => {
1651 // A generator or async function is NOT an ordinary function: its
1652 // `[[Prototype]]` is `GeneratorFunction.prototype` (or the async
1653 // variants'), and so is its `constructor`. All three reported plain
1654 // `Function`, so `g.constructor.name` was `Function` where node
1655 // says `GeneratorFunction`.
1656 Some(match h.funcs.get(f.def_id) {
1657 Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction",
1658 Some(d) if d.is_generator => "GeneratorFunction",
1659 Some(d) if d.is_async => "AsyncFunction",
1660 _ => "Function",
1661 })
1662 }
1663 Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => Some("Function"),
1664 _ => match recv {
1665 Value::Float(_) | Value::Int(_) => Some("Number"),
1666 Value::Bool(_) => Some("Boolean"),
1667 _ => None,
1668 },
1669 }
1670}
1671
1672/// The builtin constructor *functions*, so `Ctor.name` is the constructor name.
1673/// Excludes the non-callable namespaces (`Math`, `JSON`, `console`, `Reflect`,
1674/// `process`), whose `.name` is `undefined` in Node.
1675///
1676/// Most are also globals, but not all: `Timeout`/`Immediate` are unexposed in
1677/// Node (`typeof Timeout === 'undefined'`) yet still name themselves through a
1678/// handle's `.constructor.name`, so they belong here and not in `GLOBALS`.
1679/// The builtins that expose a `Symbol.species` accessor. Each returns `this`,
1680/// so a subclass is its own species unless it overrides the getter.
1681fn has_species(name: &str) -> bool {
1682 matches!(
1683 name,
1684 "Array" | "Map" | "Set" | "WeakMap" | "WeakSet" | "Promise" | "RegExp" | "ArrayBuffer"
1685 ) || crate::stdlib::typedarray::is_ctor(name)
1686}
1687
1688fn is_builtin_ctor(name: &str) -> bool {
1689 matches!(
1690 name,
1691 "Array"
1692 | "Object"
1693 | "Number"
1694 | "String"
1695 | "Boolean"
1696 | "Symbol"
1697 | "Function"
1698 | "Map"
1699 | "Set"
1700 | "WeakMap"
1701 | "WeakSet"
1702 | "Promise"
1703 | "BigInt"
1704 | "Iterator"
1705 | "RegExp"
1706 | "Date"
1707 | "ArrayBuffer"
1708 | "DataView"
1709 | "Uint8Array"
1710 | "Int8Array"
1711 | "Uint8ClampedArray"
1712 | "Int16Array"
1713 | "Uint16Array"
1714 | "Int32Array"
1715 | "Uint32Array"
1716 | "Float32Array"
1717 | "Float64Array"
1718 | "BigInt64Array"
1719 | "BigUint64Array"
1720 | "WeakRef"
1721 | "FinalizationRegistry"
1722 | "TextEncoder"
1723 | "TextDecoder"
1724 | "IncomingMessage"
1725 | "ServerResponse"
1726 | "EventEmitter"
1727 | "Buffer"
1728 | "URL"
1729 | "URLSearchParams"
1730 | "Timeout"
1731 | "Immediate"
1732 ) || host::ERROR_NAMES.contains(&name)
1733 // The stream base classes are constructors too, and `require('stream')`
1734 // IS `Stream`, so `require('stream').name` has to answer.
1735 || crate::stdlib::stream::is_class(name)
1736}
1737
1738/// The intrinsic key of the method `<instance>.<method>` resolves to, so a bound
1739/// thunk can look its `name`/`length` up in the same table a
1740/// `<Ctor>.prototype.<method>` thunk uses. `None` when the receiver has no
1741/// builtin constructor to name (a native stdlib instance, whose methods are
1742/// node's own JS and have no specified arity).
1743fn bound_method_key(recv: &Value, method: &str) -> Option<String> {
1744 let ctor = with_host(|h| default_ctor_name(h, recv))?;
1745 Some(format!("@proto:{ctor}:{method}"))
1746}
1747
1748/// `[[Get]]` on a bound method thunk. It is a function, so `name`, `length` and
1749/// the `Function.prototype` methods all answer; `length` only when the intrinsic
1750/// table knows the method, because inventing an arity is worse than the
1751/// `undefined` a caller can test for.
1752fn bound_method_property(recv: &Value, name: &str) -> Value {
1753 let method = peek(recv, |o| match o {
1754 JsObj::BoundMethod { name, .. } => Some(name.clone()),
1755 _ => None,
1756 })
1757 .unwrap_or_default();
1758 let key = peek(recv, |o| match o {
1759 JsObj::BoundMethod { recv, .. } => Some(recv.clone()),
1760 _ => None,
1761 })
1762 .and_then(|inner| bound_method_key(&inner, &method));
1763 let meta = key.as_deref().and_then(builtin_meta);
1764 match name {
1765 "name" => {
1766 let n = meta.map(|(n, _)| n.to_string()).unwrap_or(method);
1767 with_host(|h| h.new_str(n))
1768 }
1769 "length" => match meta {
1770 Some((_, len)) => Value::Float(len as f64),
1771 None => Value::Undef,
1772 },
1773 _ if is_function_method(name) => bound_method(recv, name),
1774 _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1775 }
1776}
1777
1778fn bound_method(recv: &Value, name: &str) -> Value {
1779 // An ECMAScript intrinsic is ONE function object shared by every instance:
1780 // `[1].push === Array.prototype.push` and `[1].push === [2].push` are both
1781 // true. Reading one off an instance used to mint a fresh thunk bound to that
1782 // instance, so every such comparison answered false — and a detached method
1783 // kept working on the receiver it was read off, where node throws because it
1784 // has no `this` at all.
1785 if let Some(key) = bound_method_key(recv, name) {
1786 if builtin_meta(&key).is_some() {
1787 return with_host(|h| h.alloc(JsObj::Builtin(key)));
1788 }
1789 }
1790 with_host(|h| {
1791 h.alloc(JsObj::BoundMethod {
1792 recv: recv.clone(),
1793 name: name.to_string(),
1794 })
1795 })
1796}
1797
1798/// `Object.prototype` methods reachable on any object.
1799fn is_object_method(name: &str) -> bool {
1800 matches!(
1801 name,
1802 "hasOwnProperty"
1803 | "isPrototypeOf"
1804 | "propertyIsEnumerable"
1805 | "toString"
1806 | "toLocaleString"
1807 | "valueOf"
1808 | "constructor"
1809 | "__defineGetter__"
1810 | "__defineSetter__"
1811 | "__lookupGetter__"
1812 | "__lookupSetter__"
1813 )
1814}
1815
1816/// The `Object.prototype` methods installed as thunks on the real
1817/// `Object.prototype` object, so `Object.prototype.toString.call(x)` and a class
1818/// prototype's inherited `hasOwnProperty` both resolve through the chain.
1819pub const OBJECT_PROTO_METHODS: &[&str] = &[
1820 "hasOwnProperty",
1821 "isPrototypeOf",
1822 "propertyIsEnumerable",
1823 "toString",
1824 "toLocaleString",
1825 "valueOf",
1826 "__defineGetter__",
1827 "__defineSetter__",
1828 "__lookupGetter__",
1829 "__lookupSetter__",
1830];
1831
1832/// A typed array with elements cannot be frozen or sealed: its indices are
1833/// non-configurable by construction, so making them non-writable would violate
1834/// the invariant, and node refuses outright rather than half-applying it. An
1835/// EMPTY view and a `DataView` are both fine.
1836/// `TestIntegrityLevel` (7.3.16) — `Object.isFrozen` / `Object.isSealed`.
1837///
1838/// Over a PROXY it is a sequence of traps (`isExtensible`, `ownKeys`, then a
1839/// `getOwnPropertyDescriptor` per key), not a question for the host: the proxy
1840/// OBJECT was being inspected, so a frozen proxy answered false and the handler
1841/// never saw the query.
1842fn integrity_level(v: &Value, freeze: bool) -> Result<Value, String> {
1843 if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
1844 return Ok(Value::Bool(with_host(|h| h.is_sealed(v, freeze))));
1845 }
1846 // An EXTENSIBLE object is neither sealed nor frozen, whatever its keys say.
1847 if crate::proxy::is_extensible(v)?.unwrap_or(true) {
1848 return Ok(Value::Bool(false));
1849 }
1850 for key in crate::proxy::own_keys(v)?.unwrap_or_default() {
1851 let Some(d) = crate::proxy::get_own_descriptor(v, &key)? else {
1852 continue;
1853 };
1854 let flag = |name: &str| {
1855 with_host(|h| match h.get(&d) {
1856 Some(JsObj::Object(p)) => p.get(name).map(|x| h.truthy(x)).unwrap_or(false),
1857 _ => false,
1858 })
1859 };
1860 let is_data = with_host(
1861 |h| matches!(h.get(&d), Some(JsObj::Object(p)) if !p.contains_key("get") && !p.contains_key("set")),
1862 );
1863 if flag("configurable") || (freeze && is_data && flag("writable")) {
1864 return Ok(Value::Bool(false));
1865 }
1866 }
1867 Ok(Value::Bool(true))
1868}
1869
1870/// `SetIntegrityLevel` (7.3.15) over a PROXY, which is a sequence of TRAPS —
1871/// `preventExtensions`, then `ownKeys`, then a `getOwnPropertyDescriptor` and a
1872/// `defineProperty` per key. It ran none of them: the host sealed the proxy
1873/// OBJECT, so the handler never saw the operation and the target was untouched.
1874///
1875/// Returns false for a non-proxy, which takes the ordinary path.
1876fn seal_proxy(v: &Value, freeze: bool) -> Result<bool, String> {
1877 if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
1878 return Ok(false);
1879 }
1880 if !crate::proxy::prevent_extensions(v)? {
1881 return Err(host::type_error("Object.freeze called on non-object"));
1882 }
1883 let keys = crate::proxy::own_keys(v)?.unwrap_or_default();
1884 for key in keys {
1885 // SEALING asks for no descriptor at all — it only strips
1886 // `configurable`, which is the same for a data property and an
1887 // accessor. FREEZING has to know which it is, because only a data
1888 // property has a `writable` to strip, and that is the one extra trap
1889 // call node makes.
1890 let accessor = if freeze {
1891 let Some(cur) = crate::proxy::get_own_descriptor(v, &key)? else {
1892 continue;
1893 };
1894 with_host(
1895 |h| matches!(h.get(&cur), Some(JsObj::Object(p)) if p.contains_key("get") || p.contains_key("set")),
1896 )
1897 } else {
1898 false
1899 };
1900 let desc = with_host(|h| {
1901 let mut m: IndexMap<String, Value> = IndexMap::new();
1902 m.insert("configurable".into(), Value::Bool(false));
1903 if freeze && !accessor {
1904 m.insert("writable".into(), Value::Bool(false));
1905 }
1906 h.new_object(m)
1907 });
1908 if !crate::proxy::define_property(v, &key, &desc)? {
1909 return Err(host::type_error(&format!(
1910 "'defineProperty' on proxy: trap returned falsish for property '{key}'"
1911 )));
1912 }
1913 }
1914 Ok(true)
1915}
1916
1917fn reject_sealing_a_view(v: &Value, verb: &str) -> Result<(), String> {
1918 let has_elements = matches!(
1919 crate::stdlib::native_tag(v).as_deref(),
1920 Some("TypedArray") | Some("Buffer")
1921 ) && !crate::stdlib::typedarray::elem_values(v).is_empty();
1922 if has_elements {
1923 return Err(host::type_error(&format!(
1924 "Cannot {verb} array buffer views with elements"
1925 )));
1926 }
1927 Ok(())
1928}
1929
1930pub fn is_object_builtin_method(name: &str) -> bool {
1931 matches!(
1932 name,
1933 "hasOwnProperty"
1934 | "isPrototypeOf"
1935 | "propertyIsEnumerable"
1936 | "toString"
1937 | "toLocaleString"
1938 | "valueOf"
1939 | "__defineGetter__"
1940 | "__defineSetter__"
1941 | "__lookupGetter__"
1942 | "__lookupSetter__"
1943 )
1944}
1945
1946/// The `Symbol.toStringTag` STRING on `recv`'s chain, if any — steps 16-17 of
1947/// 20.1.3.6, the hook by which a class names its own brand.
1948///
1949/// A Proxy has no chain to probe: the step is an unconditional
1950/// `Get(O, @@toStringTag)`, so its `get` trap decides. Probing first (as an
1951/// ordinary receiver does, to keep the read off objects that carry no tag)
1952/// would always miss and brand every tagged proxy `[object Object]`.
1953///
1954/// The read runs OUTSIDE the host borrow so a getter-valued tag can be invoked.
1955fn to_string_tag(recv: &Value) -> Result<Option<String>, String> {
1956 let tagged = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
1957 || with_host(|h| {
1958 host::lookup_chain(h, recv, "@@toStringTag").is_some()
1959 || host::lookup_accessor(h, recv, "@@toStringTag").is_some()
1960 });
1961 if !tagged {
1962 return Ok(None);
1963 }
1964 let t = get_property(recv, "@@toStringTag")?;
1965 Ok(with_host(|h| h.as_str(&t)))
1966}
1967
1968/// Dispatch an `Object.prototype` builtin method on an object/instance.
1969pub fn object_builtin_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
1970 match name {
1971 // Annex B B.2.2.2-B.2.2.5. Legacy, but still present in node and still
1972 // reached by pre-`defineProperty` libraries; all four were missing, so
1973 // `o.__defineGetter__` threw "is not a function".
1974 "__defineGetter__" | "__defineSetter__" => {
1975 let getter = name == "__defineGetter__";
1976 let f = args.get(1).cloned().unwrap_or(Value::Undef);
1977 if !with_host(|h| host::is_callable(h, &f)) {
1978 return Err(host::type_error(&format!(
1979 "Object.prototype.{name}: Expecting function"
1980 )));
1981 }
1982 let key = host::to_property_key(&arg0(&args))?;
1983 let desc = with_host(|h| {
1984 let mut m: IndexMap<String, Value> = IndexMap::new();
1985 m.insert(if getter { "get" } else { "set" }.into(), f);
1986 m.insert("enumerable".into(), Value::Bool(true));
1987 m.insert("configurable".into(), Value::Bool(true));
1988 h.new_object(m)
1989 });
1990 apply_descriptor(recv, &key, &desc)?;
1991 Ok(Value::Undef)
1992 }
1993 "__lookupGetter__" | "__lookupSetter__" => {
1994 let want_get = name == "__lookupGetter__";
1995 let key = host::to_property_key(&arg0(&args))?;
1996 // Walks the prototype chain, unlike `getOwnPropertyDescriptor`.
1997 let found = with_host(|h| host::lookup_accessor(h, recv, &key));
1998 Ok(match found {
1999 Some((g, st)) => {
2000 let side = if want_get { g } else { st };
2001 side.unwrap_or(Value::Undef)
2002 }
2003 None => Value::Undef,
2004 })
2005 }
2006 "hasOwnProperty" => {
2007 let k = host::to_property_key(&arg0(&args))?;
2008 // The global object OWNS its lazily-bound builtins and every global
2009 // a script created; neither lives in its property map.
2010 if with_host(|h| h.is_global_object(recv))
2011 && !CJS_WRAPPER_LOCALS.contains(&k.as_str())
2012 && global_object_binding(&k).is_some()
2013 {
2014 return Ok(Value::Bool(true));
2015 }
2016 // A builtin namespace/prototype receiver (`Map.prototype`) reports
2017 // ownership via `has_property` (its methods resolve as thunks).
2018 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin) {
2019 return Ok(Value::Bool(has_property(recv, &k)?));
2020 }
2021 // `HasOwnProperty` (7.3.12) is `[[GetOwnProperty]]`, so on a Proxy it
2022 // is the `getOwnPropertyDescriptor` trap — NOT the `has` trap and not
2023 // the target's property map.
2024 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
2025 let d = crate::proxy::get_own_descriptor(recv, &k)?.unwrap_or(Value::Undef);
2026 return Ok(Value::Bool(!matches!(d, Value::Undef)));
2027 }
2028 // A Buffer's / typed array's own keys are its element indices: the
2029 // `length`/`byteLength` slots are internal bookkeeping, and V8
2030 // reports `hasOwnProperty('length')` as false for a typed array.
2031 // Shared with the `in` operator so the two cannot drift apart.
2032 if let Some(hit) = crate::stdlib::typedarray::has_index(recv, &k) {
2033 return Ok(Value::Bool(hit));
2034 }
2035 // A function's `length`/`name`/`prototype` and a RegExp's
2036 // `lastIndex` are SYNTHESIZED own properties: they read back but
2037 // own no map entry, so this answered false where node says true.
2038 if synthesized_own_descriptor(recv, &k).is_some() {
2039 return Ok(Value::Bool(true));
2040 }
2041 if uses_side_table(recv) {
2042 return Ok(Value::Bool(with_host(|h| h.fn_prop(recv, &k).is_some())));
2043 }
2044 let has = with_host(|h| match h.get(recv) {
2045 Some(JsObj::Object(p)) => p.contains_key(&k) || h.own_accessor(recv, &k).is_some(),
2046 Some(JsObj::Array(items)) => {
2047 k == "length"
2048 || k.parse::<usize>()
2049 .map(|i| i < items.len() && !h.is_hole(recv, i))
2050 .unwrap_or(false)
2051 }
2052 _ => false,
2053 });
2054 Ok(Value::Bool(has))
2055 }
2056 "isPrototypeOf" => {
2057 let target = arg0(&args);
2058 // The ARGUMENT is what gets walked, so a proxy there needs its
2059 // `getPrototypeOf` trap for the FIRST hop: `proto_of` reads a link a
2060 // proxy does not hold, which reported `false` for every proxy. From
2061 // the second hop on the chain is ordinary objects again, walked by
2062 // the recorded link exactly as before.
2063 let mut cur = match crate::proxy::get_prototype_of(&target)? {
2064 Some(p) => Some(p).filter(|p| !with_host(|h| h.is_null(p))),
2065 None => with_host(|h| h.proto_of(&target)),
2066 };
2067 while let Some(p) = cur {
2068 if with_host(|h| h.strict_eq(&p, recv)) {
2069 return Ok(Value::Bool(true));
2070 }
2071 cur = with_host(|h| h.proto_of(&p));
2072 }
2073 Ok(Value::Bool(false))
2074 }
2075 "propertyIsEnumerable" => {
2076 let k = with_host(|h| h.str_of(&arg0(&args)));
2077 // Own *and* enumerable — a non-enumerable own slot reads false. On a
2078 // Proxy that question is `[[GetOwnProperty]]`, i.e. the descriptor
2079 // trap, since there is no property map to enumerate.
2080 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
2081 let has = crate::proxy::own_enum_string_keys(recv)?.contains(&k);
2082 return Ok(Value::Bool(has));
2083 }
2084 let has = with_host(|h| h.own_enum_key_names(recv).contains(&k));
2085 Ok(Value::Bool(has))
2086 }
2087 "toString" => {
2088 // An instance with a custom `toString` up the chain is handled by
2089 // call_method before reaching here; this is the default — and the
2090 // default consults `Symbol.toStringTag` (20.1.3.6 steps 16-17).
2091 // Only the EXPLICIT `Object.prototype.toString.call(o)` did, so a
2092 // tagged object branded itself `[object T]` when asked one way and
2093 // `[object Object]` when converted the other (`String(o)`, `${o}`,
2094 // `o + ''`, `o.toString()`), which is the path ordinary code takes.
2095 if let Some(t) = to_string_tag(recv)? {
2096 return Ok(with_host(|h| h.new_str(format!("[object {t}]"))));
2097 }
2098 Ok(with_host(|h| {
2099 let s = h.str_of(recv);
2100 h.new_str(s)
2101 }))
2102 }
2103 // `Object.prototype.toLocaleString` (20.1.3.5) is defined as
2104 // `Invoke(this, "toString")` — no locale behavior of its own. It was
2105 // installed as a thunk on `Object.prototype` but had no dispatch arm, so
2106 // calling it threw `is not a function` on every plain object.
2107 "toLocaleString" => {
2108 let v = host::call_method(recv, "toString", Vec::new())?;
2109 Ok(v)
2110 }
2111 "valueOf" => Ok(recv.clone()),
2112 _ => Err(host::type_error(&format!("{name} is not a function"))),
2113 }
2114}
2115
2116/// `Function.prototype` methods (`call`/`apply`/`bind`) plus `Symbol.prototype`/
2117/// generator handling done elsewhere. Returns `Ok(None)` if `name` is not one of
2118/// these (so the caller can try statics).
2119pub fn function_builtin_method(
2120 recv: &Value,
2121 name: &str,
2122 args: &[Value],
2123) -> Result<Option<Value>, String> {
2124 match name {
2125 "call" => {
2126 let this = args.first().cloned();
2127 let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2128 Ok(Some(host::invoke(recv, rest, this)?))
2129 }
2130 "apply" => {
2131 let this = args.first().cloned();
2132 let arr = args.get(1).cloned().unwrap_or(Value::Undef);
2133 // `Function.prototype.apply` takes an ARRAY-LIKE, not an iterable
2134 // (10.2.4.3 → CreateListFromArrayLike): `f.apply(null, arguments)`
2135 // and `f.apply(null, {length: 2, 0: 'x', 1: 'y'})` are the shapes
2136 // this is written for, and both produced an empty list. A nullish
2137 // second argument means no arguments at all.
2138 let call_args = if matches!(arr, Value::Undef) || with_host(|h| h.is_null(&arr)) {
2139 Vec::new()
2140 } else {
2141 create_list_from_array_like(&arr)?
2142 };
2143 Ok(Some(host::invoke(recv, call_args, this)?))
2144 }
2145 "bind" => {
2146 let this = args.first().cloned().unwrap_or(Value::Undef);
2147 let pre = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2148 Ok(Some(with_host(|h| {
2149 h.alloc(JsObj::BoundFunc {
2150 target: recv.clone(),
2151 this,
2152 args: pre,
2153 })
2154 })))
2155 }
2156 "toString" => Ok(Some(with_host(|h| {
2157 let s = h.str_of(recv);
2158 h.new_str(s)
2159 }))),
2160 _ => Ok(None),
2161 }
2162}
2163
2164fn is_function_method(name: &str) -> bool {
2165 matches!(name, "call" | "apply" | "bind" | "toString")
2166}
2167fn is_map_method(name: &str) -> bool {
2168 matches!(
2169 name,
2170 "get" | "set" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
2171 )
2172}
2173fn is_set_method(name: &str) -> bool {
2174 matches!(
2175 name,
2176 "add"
2177 | "has"
2178 | "delete"
2179 | "clear"
2180 | "forEach"
2181 | "keys"
2182 | "values"
2183 | "entries"
2184 | "union"
2185 | "intersection"
2186 | "difference"
2187 | "symmetricDifference"
2188 | "isSubsetOf"
2189 | "isSupersetOf"
2190 | "isDisjointFrom"
2191 )
2192}
2193fn is_generator_method(name: &str) -> bool {
2194 matches!(name, "next" | "return" | "throw")
2195}
2196
2197/// A property read on a function/class value: own fn-props (statics, name,
2198/// prototype, length) plus inherited statics and `call`/`apply`/`bind`.
2199fn function_property(recv: &Value, name: &str) -> Value {
2200 // A class static, inherited down the constructor chain.
2201 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
2202 if let Some(v) = with_host(|h| h.class_static(recv, name)) {
2203 return v;
2204 }
2205 // A class's own `name` and `length` are its own, not the builtin
2206 // ancestor's: `class A extends Array {}` has `A.name === "A"` and
2207 // `A.length === 0`, but both were read off `Array`. Only a class that
2208 // WOULD fall through to an ancestor takes this path; a plain class keeps
2209 // the ordinary computation below.
2210 if matches!(name, "name" | "length")
2211 && with_host(|h| h.class_static(recv, name)).is_none()
2212 && with_host(|h| h.class_builtin_ancestor(recv))
2213 .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2214 {
2215 if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2216 return v;
2217 }
2218 if name == "name" {
2219 let n = with_host(|h| h.callable_name(recv));
2220 return with_host(|h| h.new_str(n));
2221 }
2222 // The class's own constructor decides its arity; with no explicit
2223 // one the implicit `constructor(...args)` has length 0.
2224 let ctor = with_host(|h| match h.get(recv) {
2225 Some(JsObj::Class(c)) => c.ctor.clone(),
2226 _ => None,
2227 });
2228 return match ctor {
2229 Some(c) => get_property(&c, "length").unwrap_or(Value::Float(0.0)),
2230 None => Value::Float(0.0),
2231 };
2232 }
2233 // `Symbol.species` is an accessor returning `this`, so a subclass that
2234 // does not override it IS its own species. Reading it off the builtin
2235 // ancestor below would answer with the ancestor — `A[Symbol.species]`
2236 // came back as `Array`, which sent every derived result to a plain
2237 // array.
2238 if name == "@@species"
2239 && with_host(|h| h.class_static(recv, "@@species")).is_none()
2240 && with_host(|h| h.class_builtin_ancestor(recv))
2241 .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2242 {
2243 return recv.clone();
2244 }
2245 // The chain may bottom out in a BUILTIN constructor (`class D extends
2246 // Array {}`), whose statics `class_static` cannot see — it only walks
2247 // `ClassVal.parent` links between user classes. Finish the lookup with an
2248 // ordinary read on that ancestor so `D.from` inherits `Array.from`.
2249 if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
2250 if let Ok(v) = get_property(&anc, name) {
2251 if !matches!(v, Value::Undef) {
2252 return v;
2253 }
2254 }
2255 }
2256 } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2257 return v;
2258 }
2259 // A method inherited via the function's [[Prototype]] chain (set with
2260 // `Object.setPrototypeOf(fn, proto)` — the `router` package makes each router
2261 // *function* inherit `route`/`use`/`get`/… from `Router.prototype` this way).
2262 if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
2263 return v;
2264 }
2265 match name {
2266 "name" => with_host(|h| {
2267 let n = h.callable_name(recv);
2268 h.new_str(n)
2269 }),
2270 "length" => Value::Float(with_host(|h| h.func_arity(recv)) as f64),
2271 "prototype" => ensure_fn_prototype(recv),
2272 _ if is_function_method(name) => bound_method(recv, name),
2273 _ => Value::Undef,
2274 }
2275}
2276
2277/// The `.prototype` of a function value, auto-created on first access (as Node
2278/// does for every non-arrow function) with `.constructor` linking back. Arrow
2279/// functions have no `prototype`.
2280fn ensure_fn_prototype(recv: &Value) -> Value {
2281 if let Some(p) = with_host(|h| h.fn_prop(recv, "prototype")) {
2282 return p;
2283 }
2284 // Only a constructor gets one: an arrow, a method definition and an async
2285 // function are not constructors, and a class sets its own (10.2.5).
2286 if with_host(|h| h.kind_of(recv)) != Some(ObjKind::Func) {
2287 return Value::Undef;
2288 }
2289 if !with_host(|h| h.owns_prototype(recv)) {
2290 return Value::Undef;
2291 }
2292 with_host(|h| {
2293 let proto = h.new_object(IndexMap::new());
2294 if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
2295 p.insert("constructor".to_string(), recv.clone());
2296 }
2297 h.hide_prop(&proto, "constructor");
2298 h.set_fn_prop(recv, "prototype", proto.clone());
2299 proto
2300 })
2301}
2302
2303/// The numeric constants a core namespace owns, in the order node reports them
2304/// under `getOwnPropertyNames`. ONE table rather than a value match plus a name
2305/// list: the enumeration and the read have to agree, and they did not — every
2306/// one of these read correctly while `Object.getOwnPropertyNames(Math)` omitted
2307/// all eight of Math's, so a member that plainly exists was invisible to any
2308/// reflective copy of the namespace.
2309///
2310/// Each is `{ writable: false, enumerable: false, configurable: false }`, which
2311/// is what separates them from the methods alongside them.
2312pub fn namespace_constants(ns: &str) -> &'static [(&'static str, f64)] {
2313 const MATH: &[(&str, f64)] = &[
2314 ("E", std::f64::consts::E),
2315 ("LN10", std::f64::consts::LN_10),
2316 ("LN2", std::f64::consts::LN_2),
2317 ("LOG10E", std::f64::consts::LOG10_E),
2318 ("LOG2E", std::f64::consts::LOG2_E),
2319 ("PI", std::f64::consts::PI),
2320 ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2),
2321 ("SQRT2", std::f64::consts::SQRT_2),
2322 ];
2323 const NUMBER: &[(&str, f64)] = &[
2324 ("MAX_VALUE", f64::MAX),
2325 // The smallest positive value a Number can hold, which is the
2326 // smallest SUBNORMAL double (`5e-324`), not Rust's
2327 // `f64::MIN_POSITIVE` — that is the smallest *normal* double,
2328 // `2.2250738585072014e-308`, ~256 binary orders of magnitude too
2329 // large.
2330 // The literal, not `f64::from_bits(1)`: that is only const-callable from
2331 // Rust 1.83 and this crate's MSRV is 1.80. It parses to the same
2332 // bit pattern — the smallest positive subnormal.
2333 ("MIN_VALUE", 5e-324),
2334 ("NaN", f64::NAN),
2335 ("NEGATIVE_INFINITY", f64::NEG_INFINITY),
2336 ("POSITIVE_INFINITY", f64::INFINITY),
2337 ("MAX_SAFE_INTEGER", 9007199254740991.0),
2338 ("MIN_SAFE_INTEGER", -9007199254740991.0),
2339 ("EPSILON", f64::EPSILON),
2340 ];
2341 match ns {
2342 "Math" => MATH,
2343 "Number" => NUMBER,
2344 _ => &[],
2345 }
2346}
2347
2348/// The descriptor of `<ns>.<key>`, whose attributes fall into four groups —
2349/// measured on node v26.8.1:
2350///
2351/// ```text
2352/// Math.PI, Number.MAX_SAFE_INTEGER, Number.prototype w=false e=false c=false
2353/// Math.max.name, Math.max.length w=false e=false c=true
2354/// Math.floor, Array.from, Array.prototype.slice w=true e=false c=true
2355/// require('path').join w=true e=true c=true
2356/// ```
2357///
2358/// So: a constant (and a constructor's `prototype`) is frozen, a function's own
2359/// `name`/`length` is read-only but configurable, and everything else is an
2360/// ordinary method — enumerable exactly when the namespace enumerates it, which
2361/// is what separates a core module's exports from an ECMAScript namespace's.
2362fn builtin_member_descriptor(ns: &str, key: &str, value: Value) -> Value {
2363 let frozen = namespace_constants(ns).iter().any(|(k, _)| *k == key)
2364 || key == "prototype"
2365 || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key));
2366 let own_fn_meta = matches!(key, "name" | "length") && host::builtin_is_callable(ns);
2367 // A key a SCRIPT assigned is an ordinary writable/enumerable/configurable
2368 // data property, whatever the namespace's built-in members look like — the
2369 // synthesized answer reported it non-enumerable, so a monkey-patched member
2370 // described itself as one of the intrinsics.
2371 let assigned = !intrinsic_proto_member(ns, key)
2372 && !crate::stdlib::namespace_keys(ns).iter().any(|k| k == key)
2373 && with_host(|h| h.builtin_static(ns, key).is_some());
2374 let enumerable = assigned
2375 || (!frozen && !own_fn_meta && crate::stdlib::namespace_keys(ns).iter().any(|k| k == key));
2376 with_host(|h| {
2377 let mut m: IndexMap<String, Value> = IndexMap::new();
2378 m.insert("value".into(), value);
2379 m.insert(
2380 "writable".into(),
2381 Value::Bool(assigned || (!frozen && !own_fn_meta)),
2382 );
2383 m.insert("enumerable".into(), Value::Bool(enumerable));
2384 m.insert("configurable".into(), Value::Bool(assigned || !frozen));
2385 h.new_object(m)
2386 })
2387}
2388
2389/// Whether `<ns>.<key>` may be deleted — the `configurable` half of
2390/// [`builtin_member_descriptor`], split out so `delete` can ask without
2391/// building a descriptor object.
2392/// Whether `key` is one of the members the intrinsic prototype namespace `ns`
2393/// really defines — as opposed to a name a script added. An assignment over one
2394/// of these is a `[[Set]]` and leaves its attributes alone.
2395fn intrinsic_proto_member(ns: &str, key: &str) -> bool {
2396 intrinsic_proto_members(ns).is_some_and(|members| {
2397 members
2398 .iter()
2399 .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
2400 })
2401}
2402
2403fn builtin_member_configurable(ns: &str, key: &str) -> bool {
2404 !(namespace_constants(ns).iter().any(|(k, _)| *k == key)
2405 || key == "prototype"
2406 || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key)))
2407}
2408
2409/// The value of `<ns>.<name>` when it is one of those constants.
2410fn namespace_constant(ns: &str, name: &str) -> Option<f64> {
2411 namespace_constants(ns)
2412 .iter()
2413 .find(|(k, _)| *k == name)
2414 .map(|(_, v)| *v)
2415}
2416
2417/// Whether `ctor` is a WebIDL interface, whose prototype members are plain
2418/// assigned — and so ENUMERABLE — rather than the non-enumerable ones an
2419/// ECMAScript builtin defines. The generated member table records the same
2420/// distinction with its `+` prefix.
2421fn is_webidl_proto(ctor: &str) -> bool {
2422 intrinsic_proto_members(&format!("{ctor}.prototype"))
2423 .is_some_and(|ms| ms.iter().any(|m| m.starts_with('+')))
2424}
2425
2426/// The intrinsic constructor a value's own kind implies — the prototype it
2427/// inherits with no explicit link.
2428pub(crate) fn own_ctor_name(h: &host::JsHost, v: &Value) -> Option<&'static str> {
2429 default_ctor_name(h, v)
2430}
2431
2432/// Whether `ctor.prototype` defines `key` as a NON-WRITABLE data property, so
2433/// an object inheriting it refuses an assignment to that name.
2434pub(crate) fn is_proto_readonly(ctor: &str, key: &str) -> bool {
2435 crate::arity::PROTO_READONLY
2436 .binary_search_by(|(k, _)| (*k).cmp(ctor))
2437 .ok()
2438 .is_some_and(|i| crate::arity::PROTO_READONLY[i].1.contains(&key))
2439}
2440
2441/// Whether `ctor.prototype` defines `key` as an ACCESSOR rather than a data
2442/// property or a method.
2443pub(crate) fn is_proto_accessor(ctor: &str, key: &str) -> bool {
2444 crate::arity::PROTO_ACCESSORS
2445 .binary_search_by(|(k, _)| (*k).cmp(ctor))
2446 .ok()
2447 .is_some_and(|i| crate::arity::PROTO_ACCESSORS[i].1.contains(&key))
2448}
2449
2450/// The constructor whose `.prototype` IS `recv`, whichever of the two
2451/// representations it uses — a `Builtin` namespace handle or a real object.
2452pub(crate) fn intrinsic_proto_of(recv: &Value) -> Option<String> {
2453 with_host(|h| match h.get(recv) {
2454 Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
2455 _ => h.intrinsic_proto_ctor(recv).map(str::to_string),
2456 })
2457}
2458
2459/// The getter function of an intrinsic prototype accessor, as a first-class
2460/// value — what `Object.getOwnPropertyDescriptor(Map.prototype, 'size').get`
2461/// hands back, and the form a library uses to borrow one.
2462fn proto_getter(ctor: &str, key: &str) -> Value {
2463 with_host(|h| h.alloc(JsObj::Builtin(format!("@protoget:{ctor}:{key}"))))
2464}
2465
2466/// Whether `recv` carries the internal slot `ctor`'s accessor demands. This is
2467/// a BRAND check, not a chain walk: `Object.create(Map.prototype).size` throws
2468/// in node even though `Map.prototype` is right there on the chain.
2469fn brand_matches(recv: &Value, ctor: &str) -> bool {
2470 if let Some(tag) = crate::stdlib::native_tag(recv) {
2471 if tag == ctor || (ctor == "TypedArray" && tag == "TypedArray") {
2472 return true;
2473 }
2474 }
2475 match ctor {
2476 "TypedArray" => crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray"),
2477 "ArrayBuffer" => with_host(
2478 |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@bytes")),
2479 ),
2480 _ => {
2481 let own = match wrapped_primitive(recv).as_ref().and_then(wrapper_ctor_of) {
2482 Some(c) => Some(c),
2483 None => with_host(|h| default_ctor_name(h, recv)),
2484 };
2485 own == Some(ctor)
2486 }
2487 }
2488}
2489
2490thread_local! {
2491 /// The `(ctor, key)` prototype accessors whose tail read is in flight.
2492 ///
2493 /// A getter's last step reads the value off the receiver, and when the
2494 /// receiver does not STORE it that read walks the chain, finds the same
2495 /// accessor and runs it again: `new TextDecoder().fatal` recursed until the
2496 /// stack overflowed and aborted the process. An accessor already in flight
2497 /// answers `undefined` for its own key rather than re-entering — the value
2498 /// a missing internal slot has, and what node reports for one.
2499 static GETTERS_IN_FLIGHT: std::cell::RefCell<Vec<(String, String)>> =
2500 const { std::cell::RefCell::new(Vec::new()) };
2501}
2502
2503/// Whether `ctor`'s `key` getter is already running further down the stack.
2504fn getter_in_flight(ctor: &str, key: &str) -> bool {
2505 GETTERS_IN_FLIGHT.with(|g| g.borrow().iter().any(|(c, k)| c == ctor && k == key))
2506}
2507
2508/// Invoke an intrinsic prototype's getter against `recv` — the body behind the
2509/// `@protoget:` thunks.
2510///
2511/// Reading one OFF THE PROTOTYPE (`Map.prototype.size`) is the case that was
2512/// wrong: it answered `undefined` where node runs the getter, fails the brand
2513/// check and throws. `RegExp.prototype` is the documented exception — 22.2.6.10
2514/// and .13 return `"(?:)"` and `""` for it specifically, so the one receiver
2515/// that would otherwise throw for every flag reads two of them back.
2516pub(crate) fn proto_getter_call(ctor: &str, key: &str, recv: &Value) -> Result<Value, String> {
2517 let is_the_prototype = with_host(
2518 |h| matches!(h.get(recv), Some(JsObj::Builtin(ns)) if *ns == format!("{ctor}.prototype")),
2519 );
2520 if is_the_prototype && ctor == "RegExp" {
2521 // 22.2.6.x each carry the same step: when `this` IS `%RegExp.prototype%`
2522 // the getter returns rather than throwing. `source` and `flags` have
2523 // their own values there; every flag getter answers `undefined`.
2524 return Ok(match key {
2525 "source" => with_host(|h| h.new_str("(?:)".to_string())),
2526 "flags" => with_host(|h| h.new_str(String::new())),
2527 _ => Value::Undef,
2528 });
2529 }
2530 // `RegExp.prototype.flags` (22.2.6.5) is the one that is GENERIC: it reads
2531 // the individual flag properties off whatever object it is handed and
2532 // concatenates their letters, so a plain object answers `""` rather than
2533 // throwing, and one carrying `global`/`ignoreCase` answers `"gi"`.
2534 if ctor == "RegExp" && key == "flags" && !brand_matches(recv, ctor) {
2535 if !with_host(|h| is_object_like(h, recv)) {
2536 return Err(regexp_brand_error(key, recv));
2537 }
2538 let mut out = String::new();
2539 for (prop, letter) in REGEXP_FLAG_LETTERS {
2540 let v = get_property(recv, prop)?;
2541 if with_host(|h| h.truthy(&v)) {
2542 out.push(*letter);
2543 }
2544 }
2545 return Ok(with_host(|h| h.new_str(out)));
2546 }
2547 // `Function.prototype.arguments`/`caller` are POISON PILLS (10.2.4.1): both
2548 // the getter and the setter throw for every receiver, which is how a strict
2549 // function keeps its caller unreachable. They are not brand checks and do
2550 // not name the receiver.
2551 if ctor == "Function" && matches!(key, "arguments" | "caller") {
2552 return poison_pill_read(recv);
2553 }
2554 if !brand_matches(recv, ctor) {
2555 return Err(match ctor {
2556 "RegExp" => regexp_brand_error(key, recv),
2557 "Symbol" => {
2558 host::type_error("Symbol.prototype.description requires that 'this' be a Symbol")
2559 }
2560 _ => host::type_error(&format!(
2561 "Method get {ctor}.prototype.{key} called on incompatible receiver {}",
2562 brand_receiver_string(recv)
2563 )),
2564 });
2565 }
2566 // A native instance keeps an accessor's value in the hidden `@@<key>` slot,
2567 // so that the public name can be a getter on the prototype rather than an
2568 // own enumerable property. Read it straight: the chain walk below would
2569 // find this same accessor and run it again.
2570 if let Some(v) = with_host(|h| match h.get(recv) {
2571 Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
2572 _ => None,
2573 }) {
2574 return Ok(v);
2575 }
2576 GETTERS_IN_FLIGHT.with(|g| g.borrow_mut().push((ctor.to_string(), key.to_string())));
2577 let out = get_property(recv, key);
2578 GETTERS_IN_FLIGHT.with(|g| {
2579 g.borrow_mut().pop();
2580 });
2581 out
2582}
2583
2584/// `Function.prototype.arguments`/`caller` read against `recv`.
2585///
2586/// The pill is conditional and the condition is the RECEIVER, not the reading
2587/// code: a sloppy non-arrow function answers `null` (node stopped populating
2588/// these long ago but kept them readable), and everything else — an arrow, a
2589/// strict function, a non-function — throws. Keying it on the READER's
2590/// strictness, which is what this did, made `strictFn.arguments` answer
2591/// `undefined` from sloppy code and a sloppy function throw from strict code:
2592/// wrong in both directions.
2593pub(crate) fn poison_pill_read(recv: &Value) -> Result<Value, String> {
2594 if with_host(|h| h.fn_is_sloppy(recv)) {
2595 return Ok(with_host(|h| h.null()));
2596 }
2597 Err(host::type_error(POISON_PILL))
2598}
2599
2600/// The message both halves of the `arguments`/`caller` poison pill throw.
2601pub(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";
2602
2603/// How a REJECTED receiver is rendered in a brand-check message.
2604///
2605/// `no_side_effects_string` answers for most of them, but two kinds differ:
2606/// an intrinsic PROTOTYPE renders `#<Map>` rather than `[object Map]`, and so
2607/// does an `ArrayBuffer`/`DataView` instance, which this host tags natively and
2608/// that function therefore brands. Node draws the line at whether the value is
2609/// one of the ES5-era classes (`Array`, `Date`, `RegExp` are `[object X]`); the
2610/// two cases here are the ones that fall on the other side of it.
2611fn brand_receiver_string(recv: &Value) -> String {
2612 if let Some(ctor) = intrinsic_proto_of(recv) {
2613 return format!("#<{ctor}>");
2614 }
2615 match crate::stdlib::native_tag(recv).as_deref() {
2616 Some(tag @ ("ArrayBuffer" | "DataView")) => format!("#<{tag}>"),
2617 _ => no_side_effects_string(recv),
2618 }
2619}
2620
2621/// The flag properties `RegExp.prototype.flags` reads, in the order 22.2.6.5
2622/// concatenates their letters.
2623const REGEXP_FLAG_LETTERS: &[(&str, char)] = &[
2624 ("hasIndices", 'd'),
2625 ("global", 'g'),
2626 ("ignoreCase", 'i'),
2627 ("multiline", 'm'),
2628 ("dotAll", 's'),
2629 ("unicode", 'u'),
2630 ("unicodeSets", 'v'),
2631 ("sticky", 'y'),
2632];
2633
2634/// `RegExp.prototype`'s flag getters word their brand failure their own way,
2635/// and `flags` distinguishes a non-object receiver from a non-RegExp one
2636/// because 22.2.6.5 reads the individual flags off any object it is given.
2637fn regexp_brand_error(key: &str, recv: &Value) -> String {
2638 if key == "flags" && !with_host(|h| matches!(recv, Value::Obj(_)) && !h.is_null(recv)) {
2639 return host::type_error(&format!(
2640 "RegExp.prototype.flags getter called on non-object {}",
2641 no_side_effects_string(recv)
2642 ));
2643 }
2644 host::type_error(&format!(
2645 "RegExp.prototype.{key} getter called on non-RegExp object"
2646 ))
2647}
2648
2649/// A property on a builtin namespace object (`Math.PI`, `Number.MAX_SAFE_INTEGER`,
2650/// `console.log`).
2651pub fn namespace_property(ns: &str, name: &str) -> Value {
2652 // `require.cache[id]` — a LIVE view of the module cache, not a copy, so a
2653 // read sees whatever is loaded now and `delete` (see `delete_property`)
2654 // actually invalidates.
2655 if ns == REQUIRE_CACHE {
2656 return crate::module::cache_get(name).unwrap_or(Value::Undef);
2657 }
2658 // A property a SCRIPT assigned onto this namespace wins over everything
2659 // synthesized below, including a member the namespace really has. That is
2660 // what monkey-patching an intrinsic is: `Array.prototype.join = f` must make
2661 // `[1, 2].join()` call `f`, and a polyfill's `Array.prototype.at = impl` has
2662 // to read back at all. Only the two `Error` hooks consulted this table, so
2663 // every other assignment onto a builtin — the whole polyfill idiom — was
2664 // stored by `set_property` and then never read: the write appeared to
2665 // succeed, `Object.isExtensible` said true, and the value came back
2666 // `undefined`.
2667 if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2668 return v;
2669 }
2670 // The ENTRY script's `require` is this builtin rather than the per-module
2671 // closure, so its `cache` has to be handed out here too.
2672 // `require.extensions` — the legacy loader map. Deprecated but still read
2673 // (and sometimes written) by tooling that hooks module loading, and it was
2674 // absent entirely. The three keys node ships are present; installing a
2675 // custom loader through them is NOT honoured by this runtime's loader, so
2676 // the map reports what it can serve rather than pretending otherwise.
2677 // `util.promisify.custom` — the registered symbol a module attaches to a
2678 // callback function to supply its own promisified form. It was `undefined`,
2679 // so the lookup that decides whether to use one always missed.
2680 if ns == "util.promisify" && name == "custom" {
2681 return with_host(|h| h.symbol_for("nodejs.util.promisify.custom"));
2682 }
2683 // `process.memoryUsage.rss()` — node's fast path for the one figure that
2684 // does not need the whole object built.
2685 if ns == "process.memoryUsage" && name == "rss" {
2686 return with_host(|h| h.alloc(JsObj::Builtin("process.memoryUsage.rss".to_string())));
2687 }
2688 if ns == "require" && name == "extensions" {
2689 return with_host(|h| {
2690 let mut m: IndexMap<String, Value> = IndexMap::new();
2691 for ext in [".js", ".json", ".node"] {
2692 let f = h.alloc(JsObj::Builtin(format!("@@extension:{ext}")));
2693 m.insert(ext.to_string(), f);
2694 }
2695 h.new_object(m)
2696 });
2697 }
2698 // `require.resolve.paths(spec)` — the directories a lookup would search:
2699 // `null` for a core module, the `node_modules` chain otherwise.
2700 if ns == "require.resolve" && name == "paths" {
2701 return with_host(|h| h.alloc(JsObj::Builtin("require.resolve.paths".to_string())));
2702 }
2703 if ns == "require" && name == "cache" {
2704 return with_host(|h| h.alloc(JsObj::Builtin(REQUIRE_CACHE.to_string())));
2705 }
2706 // The legacy numeric codes `DOMException` carries as statics
2707 // (`DOMException.ABORT_ERR` is 20), named by uppercasing the error name.
2708 if ns == "DOMException" {
2709 if let Some((_, code)) = DOM_EXCEPTION_CODES
2710 .iter()
2711 .find(|(n, _)| legacy_code_name(n) == name)
2712 {
2713 return Value::Float(*code);
2714 }
2715 }
2716 // Numeric constants.
2717 if let Some(k) = namespace_constant(ns, name) {
2718 return Value::Float(k);
2719 }
2720 // `Ctor.name` on a builtin constructor is the constructor name (`Array.name`
2721 // === "Array"); non-callable namespaces (`Math`/`JSON`) fall through to
2722 // `undefined`.
2723 // `GeneratorFunction.prototype` and the two async variants are REAL objects
2724 // in `native_protos`, not `Builtin("X.prototype")` namespace handles — they
2725 // sit on the prototype chain of every generator/async function, which a
2726 // handle cannot do. Without this the read fell through to `undefined`.
2727 if name == "prototype"
2728 && matches!(
2729 ns,
2730 "GeneratorFunction" | "AsyncFunction" | "AsyncGeneratorFunction"
2731 )
2732 {
2733 return with_host(|h| {
2734 h.ensure_native_protos();
2735 h.native_proto(ns).unwrap_or(Value::Undef)
2736 });
2737 }
2738 // `Error.prepareStackTrace` has a DEFAULT hook in node
2739 // (`ErrorPrepareStackTrace`), so a library probing `if
2740 // (Error.prepareStackTrace)` finds one. Reading `undefined` sent that probe
2741 // down the wrong branch. The default renders the header plus the frames,
2742 // which is what the fast path in `materialize_stack` already produces — it
2743 // recognises this exact builtin and skips the round trip.
2744 if ns == "Error" && name == "prepareStackTrace" {
2745 return with_host(|h| h.builtin_static("Error", "prepareStackTrace")).unwrap_or_else(
2746 || with_host(|h| h.alloc(JsObj::Builtin(DEFAULT_PREPARE.to_string()))),
2747 );
2748 }
2749 // `Error.stackTraceLimit` defaults to 10 and is settable; an assignment
2750 // lands in the builtin-static side table, which the read below consults
2751 // first. Without a default the READ was `undefined`, so a library doing
2752 // `const old = Error.stackTraceLimit` and restoring it later installed
2753 // `undefined` and disabled the limit permanently.
2754 if ns == "Error" && name == "stackTraceLimit" {
2755 return with_host(|h| h.builtin_static("Error", "stackTraceLimit"))
2756 .unwrap_or(Value::Float(10.0));
2757 }
2758 // `Ctor[Symbol.species]` is an accessor returning `this` on every builtin
2759 // that has one (23.1.2.5, 27.2.4.7, …). It was absent, so the species
2760 // protocol had nothing to read and every derived result came back a plain
2761 // builtin.
2762 if name == "@@species" && has_species(ns) {
2763 return with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())));
2764 }
2765 if name == "name" && is_builtin_ctor(ns) {
2766 return with_host(|h| h.new_str(ns.to_string()));
2767 }
2768 // A well-known symbol (`Symbol.iterator`, `Symbol.toPrimitive`, …) used as a
2769 // computed property/method key.
2770 if ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&name) {
2771 return with_host(|h| h.well_known_symbol(name));
2772 }
2773 // Non-function constants on a stdlib namespace (`path.sep`, `os.EOL`,
2774 // `buffer.Buffer`, `url.URL`).
2775 if let Some(v) = crate::stdlib::constant(ns, name) {
2776 return v;
2777 }
2778 // `Ctor.prototype` on a builtin constructor (`Object.prototype`,
2779 // `Array.prototype`, …): a prototype namespace whose methods are callable
2780 // thunks (`Object.prototype.toString.call(x)` is a load-time idiom in the
2781 // `get-intrinsic`/`function-bind` family).
2782 if name == "prototype" && is_builtin_ctor(ns) {
2783 // Same reasoning as the native prototypes below, for the error
2784 // hierarchy: `new Error(...)` links its `[[Prototype]]` to the REAL
2785 // `error_protos` object, so `Error.prototype` has to read back that same
2786 // object. It resolved to a fresh `Builtin("Error.prototype")` thunk
2787 // instead, which is a FUNCTION — so `Object.getPrototypeOf(new
2788 // Error("x")) === Error.prototype` was false, and `typeof
2789 // Error.prototype` was `"function"` where node says `"object"`.
2790 if host::ERROR_NAMES.contains(&ns) {
2791 if let Some(p) = with_host(|h| {
2792 h.ensure_error_protos();
2793 host::error_proto_of(h, ns)
2794 }) {
2795 return p;
2796 }
2797 }
2798 // `Buffer`/`Uint8Array` have real prototype *objects* — a Buffer's
2799 // `[[Prototype]]` points at one, so `Object.getPrototypeOf(buf) ===
2800 // Buffer.prototype` must compare equal, which a freshly-allocated
2801 // `Builtin` handle never can.
2802 if let Some(p) = with_host(|h| {
2803 h.ensure_native_protos();
2804 h.native_proto(ns)
2805 }) {
2806 return p;
2807 }
2808 let _ = ns;
2809 return with_host(|h| h.alloc(JsObj::Builtin(format!("{ns}.prototype"))));
2810 }
2811 // A NATIVE stdlib constructor's `.prototype` (`StringDecoder`, `Hash`,
2812 // `URLSearchParams`, …). These are absent from `is_builtin_ctor`, so the arm
2813 // above never fired and the read produced `undefined` — which broke the ES5
2814 // subclassing pattern libraries still ship. `iconv-lite`'s internal codec
2815 // reads `StringDecoder.prototype.end` at load, and threw
2816 // `Cannot read properties of undefined (reading 'end')`. Built from the same
2817 // instance-method table a method read consults, so the two cannot disagree.
2818 if name == "prototype" {
2819 if let Some(p) = with_host(|h| h.ensure_ctor_proto(ns)) {
2820 return p;
2821 }
2822 }
2823 // A method read off a builtin prototype namespace (`Array.prototype.slice`):
2824 // a `@proto:<Ctor>:<method>` thunk that, when invoked (typically via
2825 // `.call`/`.apply`), dispatches `method` against the invoke-time `this`.
2826 //
2827 // The thunk is minted only for a name the prototype REALLY carries. Minting
2828 // one unconditionally made every absent name answer with a function:
2829 // `Array.prototype.totallyBogus` was `[Function: totallyBogus]` where node
2830 // says `undefined`, and so was every well-known symbol a prototype does not
2831 // define — `Array.prototype[Symbol.toStringTag]` came back a function
2832 // instead of `undefined`, which is a value `Object.prototype.toString` and
2833 // every `typeof`/truthiness test downstream then read wrong.
2834 //
2835 // Existence is decided by the generated intrinsic table, which is read out
2836 // of the reference engine, so this cannot drift from what node defines.
2837 // A name the prototype does not define but `Object.prototype` does is
2838 // INHERITED, and node hands back Object.prototype's own function object
2839 // (`Map.prototype.toString === Object.prototype.toString` is `true`), so it
2840 // resolves to the `Object` thunk rather than a per-ctor one. That is also
2841 // what makes `String(Map.prototype)` print `[object Map]`: `Map.prototype`
2842 // has no own `toString`, and the inherited one is the generic tag reader,
2843 // not a Map method that rejects a non-Map `this`.
2844 if let Some(ctor) = ns.strip_suffix(".prototype") {
2845 // `Array.prototype[Symbol.unscopables]` (23.1.3.38) is a DATA property,
2846 // not an intrinsic function, so it is not in the arity table the lookup
2847 // above consults. It lists the methods a `with` block must NOT bring
2848 // into scope — the ones added after `with` existed, so old code using a
2849 // variable of the same name keeps working.
2850 if name == "@@unscopables" && ctor == "Array" {
2851 return with_host(|h| {
2852 let mut m: IndexMap<String, Value> = IndexMap::new();
2853 for k in [
2854 "at",
2855 "copyWithin",
2856 "entries",
2857 "fill",
2858 "find",
2859 "findIndex",
2860 "findLast",
2861 "findLastIndex",
2862 "flat",
2863 "flatMap",
2864 "includes",
2865 "keys",
2866 "toReversed",
2867 "toSorted",
2868 "toSpliced",
2869 "values",
2870 ] {
2871 m.insert(k.to_string(), Value::Bool(true));
2872 }
2873 let o = h.new_object(m);
2874 let null = h.null();
2875 h.set_proto(&o, null);
2876 o
2877 });
2878 }
2879 if builtin_meta(&format!("@proto:{ctor}:{name}")).is_some() {
2880 return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:{ctor}:{name}"))));
2881 }
2882 if ctor != "Object" && builtin_meta(&format!("@proto:Object:{name}")).is_some() {
2883 return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:Object:{name}"))));
2884 }
2885 // `constructor` is excluded from the table because it is not a method:
2886 // it is the constructor function itself, and node compares equal
2887 // (`Array.prototype.constructor === Array`). It used to resolve to a
2888 // `@proto:Array:constructor` thunk, which is a different object every
2889 // read and so never compared equal to anything.
2890 if name == "constructor" && is_builtin_ctor(ctor) {
2891 return with_host(|h| h.alloc(JsObj::Builtin(ctor.to_string())));
2892 }
2893 return Value::Undef;
2894 }
2895 let qualified = format!("{ns}.{name}");
2896 if is_known_builtin(&qualified) {
2897 return with_host(|h| h.alloc(JsObj::Builtin(qualified)));
2898 }
2899 // A property the user stuck on this builtin namespace (`Error.prepareStackTrace`).
2900 if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2901 return v;
2902 }
2903 // A builtin FUNCTION's own `name` and `length` (10.3.3-4: every one has
2904 // both). `Math.max.name` was `undefined` — as was every `.name` a library
2905 // reads to identify a callback it was handed. The non-callable namespaces
2906 // fall through: `Math.name` and `require('fs').length` really are undefined.
2907 if host::builtin_is_callable(ns) {
2908 match name {
2909 "name" => {
2910 if let Some(n) = proto_getter_name(ns) {
2911 return with_host(|h| h.new_str(n));
2912 }
2913 return with_host(|h| h.new_str(builtin_name(ns).to_string()));
2914 }
2915 // Only the intrinsics have a specified arity; a core-module
2916 // function's is a property of node's own JS source, so it stays
2917 // `undefined` rather than being invented here.
2918 "length" => {
2919 // A getter takes no argument (10.2.9 / the accessor grammar),
2920 // so its `length` is 0 — it is not in the intrinsic table,
2921 // which holds only named functions.
2922 if proto_getter_name(ns).is_some() {
2923 return Value::Float(0.0);
2924 }
2925 if let Some((_, len)) = builtin_meta(ns) {
2926 return Value::Float(len as f64);
2927 }
2928 }
2929 _ => {}
2930 }
2931 }
2932 Value::Undef
2933}
2934
2935/// Dispatch a `@proto:<Ctor>:<method>` thunk (a method read off a builtin
2936/// prototype, e.g. `Object.prototype.toString`) against `recv` (its invoke-time
2937/// `this`). `Object.prototype.toString` yields the `[object Tag]` brand string
2938/// libraries type-check on; every other method routes through normal method
2939/// dispatch on `recv`.
2940/// The TypeError a `<Ctor>.prototype.<method>` thunk throws when it is invoked
2941/// with NO receiver — `const f = [].push; f(1)`.
2942///
2943/// Reading a method off an instance used to mint a thunk bound to that
2944/// instance, so a detached method silently kept working on the object it came
2945/// from. Now that it is the shared intrinsic, a bare call has no `this` and has
2946/// to say so. Node words it four ways, and which one a method gets is not
2947/// something that can be derived — the split was measured across every method
2948/// of each prototype:
2949///
2950/// ```text
2951/// ToObject(this) "Cannot convert undefined or null to object"
2952/// RequireObjectCoercible "<Ctor>.prototype.<m> called on null or undefined"
2953/// brand check "<Ctor>.prototype.<m> requires that 'this' be a <X>"
2954/// everything else the generic incompatible-receiver message
2955/// ```
2956fn nullish_receiver_error(ctor: &str, method: &str, recv: &str) -> Option<String> {
2957 // `Array.prototype` splits: the CALLBACK-taking methods plus `concat` and
2958 // the two `indexOf` family members name themselves, the rest go through
2959 // `ToObject` and report its message.
2960 const ARRAY_NAMED: &[&str] = &[
2961 "concat",
2962 "every",
2963 "filter",
2964 "find",
2965 "findIndex",
2966 "findLast",
2967 "findLastIndex",
2968 "forEach",
2969 "indexOf",
2970 "map",
2971 "reduce",
2972 "reduceRight",
2973 "some",
2974 ];
2975 const TO_OBJECT: &str = "Cannot convert undefined or null to object";
2976 let named = |c: &str| format!("{c}.prototype.{method} called on null or undefined");
2977 let branded =
2978 |c: &str, want: &str| format!("{c}.prototype.{method} requires that 'this' be a {want}");
2979 // The generic form names the receiver, so a `null` one must not be reported
2980 // as `undefined`.
2981 let generic = |c: &str, m: &str| {
2982 format!("Method {c}.prototype.{m} called on incompatible receiver {recv}")
2983 };
2984 Some(match ctor {
2985 "Array" if ARRAY_NAMED.contains(&method) => named("Array"),
2986 "Array" => TO_OBJECT.to_string(),
2987 // `Object.prototype.toString` is the one method that ACCEPTS a nullish
2988 // receiver — it answers `[object Undefined]`.
2989 "Object" if method == "toString" => return None,
2990 "Object" if method == "toLocaleString" => named("Object"),
2991 "Object" => TO_OBJECT.to_string(),
2992 // Both aliases report the LEGACY name in the message, which is the one
2993 // place `name` and the message disagree.
2994 "String" if method == "trimStart" => named("String").replace("trimStart", "trimLeft"),
2995 "String" if method == "trimEnd" => named("String").replace("trimEnd", "trimRight"),
2996 "String" if matches!(method, "toString" | "valueOf") => branded("String", "String"),
2997 "String" => named("String"),
2998 "Number" => branded("Number", "Number"),
2999 "Boolean" => branded("Boolean", "Boolean"),
3000 "Symbol" => branded("Symbol", "Symbol"),
3001 "Function" if method == "bind" => "Bind must be called on a function".to_string(),
3002 "Function" if matches!(method, "call" | "apply") => format!(
3003 "Function.prototype.{method} was called on undefined, which is undefined and not a function"
3004 ),
3005 "Function" => branded("Function", "Function"),
3006 // `Promise.prototype.catch`/`finally` are written in terms of `then`, so
3007 // a nullish receiver fails inside them and reports that instead.
3008 "Promise" if method == "catch" => {
3009 "Cannot read properties of undefined (reading 'then')".to_string()
3010 }
3011 "Promise" if method == "finally" => {
3012 "Promise.prototype.finally called on non-object".to_string()
3013 }
3014 "Date" if method == "toJSON" => TO_OBJECT.to_string(),
3015 // The plain GETTERS and `valueOf` read `[[DateValue]]` directly and
3016 // report that slot check; every setter, every `to*String` and the two
3017 // legacy year methods go through the generic receiver check first.
3018 "Date"
3019 if method == "valueOf"
3020 || (method.starts_with("get") && method != "getYear") =>
3021 {
3022 "this is not a Date object.".to_string()
3023 }
3024 // An ALIAS reports the method it aliases: `toGMTString` IS `toUTCString`
3025 // and `Set.prototype.keys` IS `values`, one function object each.
3026 "Date" if method == "toGMTString" => generic("Date", "toUTCString"),
3027 "Set" if method == "keys" => generic("Set", "values"),
3028 // Everything else that is brand-checked names itself. Node reaches this
3029 // wording from a `[[GetOwnProperty]]`-style slot check; here the check
3030 // is the receiver's kind, and only the message has to agree.
3031 "ArrayBuffer" | "DataView" | "RegExp" | "WeakRef" | "Map" | "Set" | "WeakMap"
3032 | "WeakSet" | "Promise" | "Date" => generic(ctor, method),
3033 "URLSearchParams" => "Value of \"this\" must be of type URLSearchParams".to_string(),
3034 // Node's `URL` methods fail while reaching for their internal state, and
3035 // report the read that failed rather than the method.
3036 "URL" => "Cannot read properties of undefined (reading 'URL')".to_string(),
3037 _ => return None,
3038 })
3039}
3040
3041/// Whether `<ctor>.prototype.<method>` begins with a `this<Type>Value` brand
3042/// check (21.1.3, 20.3.3, 22.1.3.29/.35, 21.2.3). Every `Number.prototype`
3043/// method does; of `String.prototype` only `toString`/`valueOf` do — the rest
3044/// are generic and coerce their receiver with `ToString`.
3045fn is_brand_checked_primitive_method(ctor: &str, method: &str) -> bool {
3046 match ctor {
3047 "Number" => matches!(
3048 method,
3049 "toString" | "toLocaleString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision"
3050 ),
3051 "BigInt" => matches!(method, "toString" | "toLocaleString" | "valueOf"),
3052 "String" | "Boolean" => matches!(method, "toString" | "valueOf"),
3053 _ => false,
3054 }
3055}
3056
3057/// `this<Type>Value(recv)` for `ctor` ∈ Number/String/Boolean/BigInt: the
3058/// primitive itself, the primitive a wrapper boxes, or — for the three
3059/// prototypes that are themselves wrappers (21.1.3, 22.1.3, 20.3.3) — the
3060/// prototype's own `+0` / `""` / `false`. `None` is the TypeError case.
3061fn this_primitive_value(ctor: &str, recv: &Value) -> Option<Value> {
3062 let expected = match ctor {
3063 "Number" => "number",
3064 "String" => "string",
3065 "Boolean" => "boolean",
3066 "BigInt" => "bigint",
3067 _ => return None,
3068 };
3069 let is_expected = |v: &Value| with_host(|h| h.type_of(v)) == expected;
3070 if is_expected(recv) {
3071 return Some(recv.clone());
3072 }
3073 if let Some(prim) = wrapped_primitive(recv).filter(is_expected) {
3074 return Some(prim);
3075 }
3076 if with_host(|h| h.intrinsic_proto_ctor(recv) == Some(ctor)) {
3077 return match ctor {
3078 "Number" => Some(Value::Float(0.0)),
3079 "String" => Some(with_host(|h| h.new_str(""))),
3080 "Boolean" => Some(Value::Bool(false)),
3081 _ => None,
3082 };
3083 }
3084 None
3085}
3086
3087pub fn proto_method(recv: &Value, ctor_method: &str, args: Vec<Value>) -> Result<Value, String> {
3088 let (ctor, method) = ctor_method.split_once(':').unwrap_or(("", ctor_method));
3089 // A prototype ACCESSOR installed by `ensure_ctor_proto`: it reads or writes
3090 // the instance's hidden `@@<name>` slot, which is where the value lives now
3091 // that the public name is a getter rather than an own property.
3092 if let Some(key) = method.strip_prefix("@get@") {
3093 if let Some(v) = with_host(|h| match h.get(recv) {
3094 Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
3095 _ => None,
3096 }) {
3097 return Ok(v);
3098 }
3099 // No stored slot: the value is COMPUTED, so ask the class. `KeyObject`'s
3100 // `symmetricKeySize` is the secret's byte length, which nothing stores.
3101 let tag = crate::stdlib::native_tag(recv).unwrap_or_default();
3102 return crate::stdlib::instance_call(&tag, recv, method, args);
3103 }
3104 if let Some(key) = method.strip_prefix("@set@") {
3105 let v = args.first().cloned().unwrap_or(Value::Undef);
3106 with_host(|h| {
3107 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
3108 p.insert(format!("@@{key}"), v);
3109 }
3110 });
3111 crate::stdlib::instance_accessor_written(ctor, key, recv);
3112 return Ok(Value::Undef);
3113 }
3114 if with_host(|h| h.is_nullish(recv)) {
3115 let shown = if with_host(|h| h.is_null(recv)) {
3116 "null"
3117 } else {
3118 "undefined"
3119 };
3120 if let Some(msg) = nullish_receiver_error(ctor, method, shown) {
3121 return Err(format!("TypeError: {msg}"));
3122 }
3123 }
3124 // `Error.prototype.toString` (20.5.3.4): `name`, `message`, or `name:
3125 // message`, read off the chain so a subclass's `this.name = 'E'` is honored.
3126 if ctor == "Error" && method == "toString" {
3127 // A `DOMException` keeps its `name`/`message` in internal slots, so the
3128 // chain read below would find the class name on the prototype instead.
3129 if let Some(n) = dom_exception_slot(recv, "name") {
3130 let name = with_host(|h| h.str_of(&n));
3131 let msg = dom_exception_slot(recv, "message")
3132 .map(|m| with_host(|h| h.str_of(&m)))
3133 .unwrap_or_default();
3134 let s = if msg.is_empty() {
3135 name
3136 } else {
3137 format!("{name}: {msg}")
3138 };
3139 return Ok(with_host(|h| h.new_str(s)));
3140 }
3141 // `name` and `message` are read with `[[Get]]` (20.5.3.4 steps 3 and 5),
3142 // so a PROXY supplies them through its `get` trap. Reading the stored
3143 // ones first made `String(new Proxy(err, handler))` ignore the handler.
3144 let via_proxy = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy);
3145 let stored = (!via_proxy).then(|| with_host(|h| h.error_to_string(recv)));
3146 let s = match stored.flatten() {
3147 Some(s) => s,
3148 None => {
3149 let read = |k: &str| -> Result<Option<String>, String> {
3150 Ok(host::protocol_lookup(recv, k)?.map(|v| with_host(|h| h.str_of(&v))))
3151 };
3152 let name = read("name")?.unwrap_or_else(|| "Error".into());
3153 let msg = read("message")?.unwrap_or_default();
3154 if msg.is_empty() {
3155 name
3156 } else {
3157 format!("{name}: {msg}")
3158 }
3159 }
3160 };
3161 return Ok(with_host(|h| h.new_str(s)));
3162 }
3163 // The methods that read their receiver through `thisNumberValue` /
3164 // `thisBooleanValue` / `thisStringValue` / `thisBigIntValue` accept only the
3165 // primitive, its wrapper, or the prototype object (which carries the zero
3166 // value) — anything else is a TypeError naming the method. Unchecked,
3167 // `Number.prototype.valueOf.call({})` answered `{}`, `toFixed.call({})`
3168 // reported "toFixed is not a function", and `Number.prototype.valueOf()`
3169 // recursed through the generic conversion until the stack overflowed.
3170 if is_brand_checked_primitive_method(ctor, method) {
3171 let Some(prim) = this_primitive_value(ctor, recv) else {
3172 return Err(format!(
3173 "TypeError: {ctor}.prototype.{method} requires that 'this' be a {ctor}"
3174 ));
3175 };
3176 return host::call_method(&prim, method, args);
3177 }
3178 // A primitive wrapper's `toString`/`valueOf`/`toLocaleString`: unwrap and
3179 // answer as the boxed primitive does. `Number.prototype.toString.call(5)`
3180 // arrives with an already-primitive receiver and needs no unwrapping.
3181 if matches!(ctor, "String" | "Number" | "Boolean") {
3182 let prim = wrapped_primitive(recv).unwrap_or_else(|| recv.clone());
3183 return host::call_method(&prim, method, args);
3184 }
3185 // `thisSymbolValue`/`thisBigIntValue` (20.4.3, 21.2.3) accept a WRAPPER as
3186 // readily as the primitive, and neither was unwrapped here. A BigInt
3187 // wrapper's `valueOf` therefore re-entered the generic conversion, which
3188 // looked `valueOf` up again and called it again: `+Object(9n)` recursed
3189 // until the stack overflowed and ABORTED the process, which no try/catch can
3190 // see. A Symbol wrapper failed the brand check below instead and reported
3191 // that `this` was not a Symbol, when it is one. Only a real wrapper is
3192 // unwrapped — `Symbol.prototype` itself boxes nothing and still has to reach
3193 // the brand check.
3194 if matches!(ctor, "Symbol" | "BigInt") {
3195 if let Some(prim) = wrapped_primitive(recv) {
3196 return host::call_method(&prim, method, args);
3197 }
3198 }
3199 if ctor == "Object" && method == "toString" {
3200 // Steps 16-17 of 20.1.3.6: a `Symbol.toStringTag` STRING on the receiver
3201 // (own or inherited, data property or getter) replaces the builtin brand,
3202 // which is how a class advertises its own (`class C { get
3203 // [Symbol.toStringTag]() { return 'Cee' } }` → `[object Cee]`). The read
3204 // runs outside the host borrow so an accessor can be invoked.
3205 // A Proxy has no chain to probe: 20.1.3.6 step 15 is an unconditional
3206 // `Get(O, @@toStringTag)`, so the `get` trap decides. Probing first (as
3207 // the ordinary receiver does, to keep the read off objects that have no
3208 // tag) would always miss and brand every tagged proxy `[object Object]`.
3209 if let Some(s) = to_string_tag(recv)? {
3210 return Ok(with_host(|h| h.new_str(format!("[object {s}]"))));
3211 }
3212 return Ok(with_host(|h| h.new_str(object_tag(h, recv))));
3213 }
3214 // These thunks now live on the real `Object.prototype` object, i.e. on the
3215 // receiver's own chain — routing back through `call_method` would re-resolve
3216 // this very thunk and recurse.
3217 if ctor == "Object" && is_object_builtin_method(method) {
3218 return object_builtin_method(recv, method, args);
3219 }
3220 // `EventEmitter.prototype.<m>` mixed onto a receiver (express's `app`): run the
3221 // emitter method directly against `recv` (routing back through `call_method`
3222 // would re-resolve the mixed-in thunk and recurse).
3223 if ctor == "EventEmitter" {
3224 return crate::stdlib::events::instance_call(recv, method, args);
3225 }
3226 // Same recursion hazard for the exotics with a real prototype object: the
3227 // thunk now lives ON the receiver's prototype chain, so `call_method` would
3228 // re-resolve this very thunk. Dispatch straight to the native instance
3229 // implementation when the receiver is in fact an instance of `ctor`.
3230 if ctor == "Buffer" && crate::stdlib::native_tag(recv).as_deref() == Some("Buffer") {
3231 return crate::stdlib::buffer::instance_call(recv, method, &args);
3232 }
3233 // The shared typed-array methods now live on the `%TypedArray%.prototype`
3234 // intermediate, so their thunks are tagged `TypedArray`; `Uint8Array` still
3235 // appears for anything read directly off `Uint8Array.prototype`. Both
3236 // dispatch the same way, and both must bypass `call_method` or the thunk
3237 // would re-resolve itself off the receiver's chain and recurse.
3238 if ctor == "Uint8Array" || ctor == "TypedArray" {
3239 match crate::stdlib::native_tag(recv).as_deref() {
3240 Some("Buffer") => return crate::stdlib::buffer::instance_call(recv, method, &args),
3241 Some("TypedArray") => {
3242 return crate::stdlib::typedarray::instance_call(recv, method, &args)
3243 }
3244 _ => {}
3245 }
3246 }
3247 // `Array.prototype.<m>.call(arrayLike)` — every `Array.prototype` method is
3248 // GENERIC over `this` (23.1.3: each starts with `ToObject(this)` and
3249 // `LengthOfArrayLike`), which is what makes
3250 // `Array.prototype.slice.call(arguments)` the idiom it is. The receiver here
3251 // is not an Array, so `call_method` would report the method missing.
3252 if ctor == "Array" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Array) {
3253 return array_generic(recv, method, args);
3254 }
3255 // The general form of the two special cases above: a thunk taken off a native
3256 // constructor's real prototype, invoked with a receiver that IS an instance of
3257 // that constructor. Routing back through `call_method` would re-resolve this
3258 // very thunk off the receiver's own chain and recurse forever, which is why
3259 // each such prototype needed a hand-written bypass; now they all have one.
3260 // A SUBCLASS counts: `SecretKeyObject` reaches `KeyObject.prototype.equals`
3261 // through its chain, and requiring an exact tag match sent that call back
3262 // into `call_method`, which re-resolved this same thunk and recursed until
3263 // the stack overflowed.
3264 if let Some(tag) = crate::stdlib::native_tag(recv) {
3265 let mut c = Some(tag.as_str());
3266 while let Some(t) = c {
3267 if t == ctor {
3268 return crate::stdlib::instance_call(&tag, recv, method, args);
3269 }
3270 c = crate::stdlib::native_parent(t);
3271 }
3272 }
3273 // A BRANDED method reached with a receiver that has no such internal slot.
3274 // Every arm above dispatches a receiver that IS an instance, so arriving
3275 // here with one of these constructors means the brand check failed — the
3276 // spec's very first step for each of them (24.2.3.x reads `[[SetData]]`,
3277 // 24.1.3.x `[[MapData]]`, 27.2.5.4 `[[PromiseState]]`, 23.2.3.x
3278 // `ValidateTypedArray`). Falling through to ordinary dispatch reported
3279 // `union is not a function`, which says the method does not exist rather
3280 // than that the receiver is the wrong kind of object.
3281 // `Date.prototype`'s methods split in two: the ones that read the time value
3282 // (`ThisTimeValue`, 21.4.4.x) report `this is not a Date object.`, and the
3283 // rest take the ordinary branded form. Measured on node v26.8.1:
3284 // `Date.prototype.getTime.call({})` is the first, `.toISOString.call({})`
3285 // and `.setHours.call({})` the second.
3286 if ctor == "Date" && crate::stdlib::native_tag(recv).as_deref() != Some("Date") {
3287 const THIS_TIME_VALUE: &[&str] = &[
3288 "getTime",
3289 "valueOf",
3290 "getYear",
3291 "getFullYear",
3292 "getMonth",
3293 "getDate",
3294 "getDay",
3295 "getHours",
3296 "getMinutes",
3297 "getSeconds",
3298 "getMilliseconds",
3299 "getUTCFullYear",
3300 "getUTCMonth",
3301 "getUTCDate",
3302 "getUTCDay",
3303 "getUTCHours",
3304 "getUTCMinutes",
3305 "getUTCSeconds",
3306 "getUTCMilliseconds",
3307 "getTimezoneOffset",
3308 ];
3309 if THIS_TIME_VALUE.contains(&method) {
3310 return Err(host::type_error("this is not a Date object."));
3311 }
3312 // `toJSON` (21.4.4.37) is deliberately generic — it converts the
3313 // receiver and INVOKES `toISOString` on it, so it fails on the missing
3314 // method rather than on a brand.
3315 if method != "toJSON" {
3316 return Err(host::type_error(&format!(
3317 "Method Date.prototype.{method} called on incompatible receiver {}",
3318 no_side_effects_string(recv)
3319 )));
3320 }
3321 }
3322 // `%TypedArray%.prototype`'s methods split the same way: `ValidateTypedArray`
3323 // (23.2.4.4) reports `this is not a typed array.`, while the handful that
3324 // check the receiver at the call boundary take the branded form. Measured
3325 // over all 27 shared methods on node v26.8.1; `toString` is the one that is
3326 // genuinely generic (it is `Array.prototype.toString`) and never brands.
3327 if matches!(ctor, "TypedArray" | "Uint8Array")
3328 && !matches!(
3329 crate::stdlib::native_tag(recv).as_deref(),
3330 Some("TypedArray") | Some("Buffer")
3331 )
3332 {
3333 const BRANDED: &[&str] = &[
3334 "slice",
3335 "subarray",
3336 "join",
3337 "sort",
3338 "at",
3339 "toReversed",
3340 "toSorted",
3341 "toLocaleString",
3342 ];
3343 if BRANDED.contains(&method) {
3344 return Err(host::type_error(&format!(
3345 "Method %TypedArray%.prototype.{method} called on incompatible receiver {}",
3346 no_side_effects_string(recv)
3347 )));
3348 }
3349 // The four base64/hex methods brand themselves against `Uint8Array`
3350 // specifically — a WRONG view is as incompatible as a plain object, and
3351 // the generic guard here cannot tell those apart.
3352 if crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS.contains(&method) {
3353 return Err(host::type_error(&format!(
3354 "Method Uint8Array.prototype.{method} called on incompatible receiver {}",
3355 no_side_effects_string(recv)
3356 )));
3357 }
3358 if method != "toString" {
3359 return Err(host::type_error("this is not a typed array."));
3360 }
3361 }
3362 // `Function.prototype.call`/`apply`/`bind` with a callable PROXY as `this`
3363 // (`pf.call(null, 4, 5)`, reached through the target's chain). Handing
3364 // that back to `call_method` read `call` off the proxy again, which
3365 // resolved to this same thunk, and recursed until the stack overflowed and
3366 // aborted the process. The three are defined on the callee alone, so they
3367 // run here: the proxy's `apply` trap (or its target) gets the call.
3368 // `toString` recursed the same way.
3369 if ctor == "Function"
3370 && matches!(method, "call" | "apply" | "bind" | "toString")
3371 && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
3372 {
3373 let mut rest = args.into_iter();
3374 let this_arg = rest.next().unwrap_or(Value::Undef);
3375 match method {
3376 "call" => return host::invoke(recv, rest.collect(), Some(this_arg)),
3377 "apply" => {
3378 let list = match rest.next() {
3379 None | Some(Value::Undef) => Vec::new(),
3380 Some(v) if with_host(|h| h.is_null(&v)) => Vec::new(),
3381 Some(v) => create_list_from_array_like(&v)?,
3382 };
3383 return host::invoke(recv, list, Some(this_arg));
3384 }
3385 "bind" => {
3386 let target = recv.clone();
3387 let pre: Vec<Value> = rest.collect();
3388 return Ok(with_host(|h| {
3389 h.alloc(JsObj::BoundFunc {
3390 target,
3391 this: this_arg,
3392 args: pre,
3393 })
3394 }));
3395 }
3396 // A proxy has no source text; V8 prints the native form for it.
3397 _ => return Ok(with_host(|h| h.new_str("function () { [native code] }"))),
3398 }
3399 }
3400 // `Symbol.prototype`'s methods are branded, and the receiver that reaches
3401 // them is very often NOT a symbol: `Symbol.prototype` itself is an ordinary
3402 // object. Without this check `Symbol.prototype.toString()` re-entered the
3403 // generic string conversion, which looked `toString` up again and called it
3404 // again — an infinite recursion that overflowed the stack and ABORTED the
3405 // process, which no `try`/`catch` can see. Node throws a plain TypeError.
3406 // The wording is Symbol's own, not the "incompatible receiver" form the
3407 // collections use.
3408 if ctor == "Symbol" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Symbol) {
3409 // A symbol-KEYED method is named in brackets rather than after a dot:
3410 // node's wording is `Symbol.prototype [ @@toPrimitive ] requires …`.
3411 // That is the message `String(Symbol.prototype)` produces, since the
3412 // conversion reaches `@@toPrimitive` before it would reach `toString`.
3413 let named = match method.strip_prefix("@@") {
3414 Some(sym) => format!("Symbol.prototype [ @@{sym} ]"),
3415 None => format!("Symbol.prototype.{method}"),
3416 };
3417 return Err(host::type_error(&format!(
3418 "{named} requires that 'this' be a Symbol"
3419 )));
3420 }
3421 if let Some(label) = branded_method_label(ctor, recv) {
3422 return Err(host::type_error(&format!(
3423 "Method {label}.prototype.{method} called on incompatible receiver {}",
3424 no_side_effects_string(recv)
3425 )));
3426 }
3427 host::call_method(recv, method, args)
3428}
3429
3430/// The name a branded prototype method reports itself under when its receiver
3431/// fails the brand check, or `None` when `ctor`'s methods are generic over
3432/// `this` (every `Array.prototype` and `Object.prototype` method is) or the
3433/// receiver really is an instance.
3434///
3435fn branded_method_label(ctor: &str, recv: &Value) -> Option<&'static str> {
3436 let kind = with_host(|h| h.kind_of(recv));
3437 // `weak` is part of the brand: a `WeakSet` has `[[WeakSetData]]`, not
3438 // `[[SetData]]`, so `Set.prototype.has.call(new WeakSet())` is incompatible
3439 // even though both are `JsObj::Set` here.
3440 let weak = peek(recv, |o| match o {
3441 JsObj::Set { weak, .. } | JsObj::Map { weak, .. } => Some(*weak),
3442 _ => None,
3443 })
3444 .unwrap_or(false);
3445 let ok = match ctor {
3446 "Set" => kind == Some(ObjKind::Set) && !weak,
3447 "WeakSet" => kind == Some(ObjKind::Set) && weak,
3448 "Map" => kind == Some(ObjKind::Map) && !weak,
3449 "WeakMap" => kind == Some(ObjKind::Map) && weak,
3450 "Promise" => kind == Some(ObjKind::Promise),
3451 _ => return None,
3452 };
3453 if ok {
3454 return None;
3455 }
3456 Some(match ctor {
3457 "Set" => "Set",
3458 "WeakSet" => "WeakSet",
3459 "Map" => "Map",
3460 "WeakMap" => "WeakMap",
3461 _ => "Promise",
3462 })
3463}
3464
3465/// V8's `Object::NoSideEffectsToString`, the rendering an engine-thrown message
3466/// uses for a value it must not run user code on. Measured on node v26.8.1
3467/// through `Map.prototype.get.call(x)`:
3468///
3469/// ```text
3470/// 5 / 'str' / true / null / undefined / 9n the value's own ToString
3471/// Symbol('s') Symbol(s)
3472/// function f(){} its source text
3473/// new Error('e') Error: e
3474/// {} / new (class A {}) #<Object> / #<A>
3475/// new Map() / Promise.resolve() #<Map> / #<Promise>
3476/// [] / new Date() / /re/ / new Uint8Array() [object Array] / [object Date] / …
3477/// { toString() {} } / Object.create(null) [object Object]
3478/// ```
3479///
3480/// The split is one test: a receiver whose `toString` is still
3481/// `Object.prototype.toString` prints `#<Constructor>`, and any other receiver
3482/// prints what the BUILTIN brand would be — V8 never calls the user's method,
3483/// which is why an object with its own `toString` prints `[object Object]` and
3484/// not what that method returns.
3485fn no_side_effects_string(recv: &Value) -> String {
3486 if with_host(|h| host::is_primitive(h, recv)) || with_host(|h| host::is_callable(h, recv)) {
3487 return with_host(|h| h.str_of(recv));
3488 }
3489 if let Some(s) = with_host(|h| h.error_to_string(recv)) {
3490 return s;
3491 }
3492 // `native_tag` re-enters the host, so it is read BEFORE the borrow below
3493 // rather than inside it.
3494 let native = crate::stdlib::native_tag(recv).is_some();
3495 let brands_itself = with_host(|h| {
3496 // `Object.prototype.toString` reaches every object as a thunk on the
3497 // real prototype object, so its presence proves nothing; only a
3498 // toString the receiver's chain OVERRIDES it with counts.
3499 let overridden = host::lookup_chain(h, recv, "toString")
3500 .map(|f| !matches!(h.get(&f), Some(JsObj::Builtin(n)) if n == "@proto:Object:toString"))
3501 .unwrap_or(false);
3502 native
3503 || overridden
3504 || h.has_null_proto(recv)
3505 || !matches!(
3506 h.kind_of(recv),
3507 Some(ObjKind::Object)
3508 | Some(ObjKind::Map)
3509 | Some(ObjKind::Set)
3510 | Some(ObjKind::Promise)
3511 )
3512 });
3513 if brands_itself {
3514 return with_host(|h| object_tag(h, recv));
3515 }
3516 let ctor = get_property(recv, "constructor")
3517 .ok()
3518 .map(|c| with_host(|h| h.callable_name(&c)))
3519 .filter(|n| !n.is_empty())
3520 .unwrap_or_else(|| "Object".to_string());
3521 format!("#<{ctor}>")
3522}
3523
3524/// The value of `v[Symbol.toStringTag]` for a builtin that genuinely carries
3525/// one, or `None` when reading that symbol must yield `undefined`.
3526///
3527/// Every builtin brand is already computed in exactly one place (`object_tag`),
3528/// so this reuses it and subtracts the legacy builtins, which brand for
3529/// `Object.prototype.toString` but expose no `Symbol.toStringTag` property.
3530/// The subtracted list is measured against node v26.7.0, not assumed: `[]`,
3531/// `function(){}`, `{}`, `new Date()`, `/x/` and `new Error()` all read
3532/// `undefined`, while `Map`/`Set`/`Promise`/typed arrays/`ArrayBuffer`/
3533/// `DataView`/`WeakRef`/`FinalizationRegistry`/`BigInt`/`Symbol`/generators/
3534/// async+generator functions/`Math`/`JSON`/`Reflect`/`URL`/`URLSearchParams`/
3535/// `TextEncoder`/`TextDecoder` all read their brand.
3536pub(crate) fn well_known_tag(h: &host::JsHost, v: &Value) -> Option<String> {
3537 // A primitive never carries the symbol except a BigInt/Symbol wrapper, both
3538 // of which `object_tag` already brands.
3539 let tag = object_brand(h, v);
3540 const NO_TAG: &[&str] = &[
3541 "Undefined",
3542 "Null",
3543 "Boolean",
3544 "Number",
3545 "String",
3546 "Array",
3547 "Function",
3548 "Object",
3549 "Date",
3550 "RegExp",
3551 "Error",
3552 ];
3553 if NO_TAG.contains(&tag.as_str()) {
3554 return None;
3555 }
3556 Some(tag)
3557}
3558
3559/// The constructor name of the nearest intrinsic prototype on `v`'s chain that
3560/// carries an own `Symbol.toStringTag`, if any.
3561fn chain_tag_ctor(h: &host::JsHost, v: &Value) -> Option<String> {
3562 let mut cur = h.proto_of(v);
3563 for _ in 0..100 {
3564 let p = cur?;
3565 if h.is_null(&p) {
3566 return None;
3567 }
3568 let name = match h.get(&p) {
3569 Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
3570 _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
3571 }
3572 // A CLASS prototype is not linked to the builtin its class extends —
3573 // the relationship lives on the class value — so the walk crosses over
3574 // there, or `Object.create(D.prototype)` for `class D extends Map`
3575 // finds nothing.
3576 .or_else(|| {
3577 h.class_owning_proto(&p)
3578 .and_then(|c| h.class_builtin_ancestor(&c))
3579 .map(|b| h.callable_name(&b))
3580 .filter(|n| !n.is_empty())
3581 });
3582 if let Some(n) = name {
3583 if intrinsic_proto_members(&format!("{n}.prototype"))
3584 .is_some_and(|ms| ms.contains(&"@@toStringTag"))
3585 {
3586 return Some(n);
3587 }
3588 }
3589 cur = h.proto_of(&p);
3590 }
3591 None
3592}
3593
3594/// The `Object.prototype.toString` brand tag for `v` (`[object Array]` etc.).
3595/// Every builtin exotic object reports its own brand, which is how packages
3596/// type-test values they did not construct (`toString.call(x) ===
3597/// '[object Uint8Array]'`). A `Buffer` reports `Uint8Array` because in Node it
3598/// IS a `Uint8Array` subclass and inherits that `Symbol.toStringTag`.
3599pub(crate) fn object_tag(h: &host::JsHost, v: &Value) -> String {
3600 format!("[object {}]", object_brand(h, v))
3601}
3602
3603/// The bare brand name behind `Object.prototype.toString` (`Array`, `Uint8Array`
3604/// …), without the `[object …]` wrapper. Split out so the brand and the
3605/// `Symbol.toStringTag` property read cannot disagree about what a value is.
3606/// Whether `v` is a function's `arguments` object.
3607///
3608/// Backed by an Array so indices, `length`, spread and `for-of` all work, but
3609/// marked so it does not pass for one: node's is an exotic, and `isArray`, the
3610/// brand and `util.types.isArgumentsObject` all have to tell them apart.
3611pub fn is_arguments(v: &Value) -> bool {
3612 with_host(|h| is_arguments_h(h, v))
3613}
3614
3615/// `is_arguments` for a caller that already holds the host borrow — `object_brand`
3616/// runs under one, and re-entering through `with_host` aborts the process.
3617pub fn is_arguments_h(h: &host::JsHost, v: &Value) -> bool {
3618 h.fn_prop(v, "@@arguments").is_some()
3619}
3620
3621fn object_brand(h: &host::JsHost, v: &Value) -> String {
3622 // A `<C>.prototype` this host built as a real object is an ORDINARY object:
3623 // it holds no instance slot, so only the branded few report anything but
3624 // `[object Object]`. Checked before the match because those prototypes are
3625 // plain `JsObj::Object`s and would otherwise be branded by whatever their
3626 // own properties happen to look like — `TypeError.prototype` has `name` and
3627 // `message`, which read as an Error instance.
3628 if let Some(ctor) = h.intrinsic_proto_ctor(v) {
3629 return if BRANDED_PROTOS.contains(&ctor) {
3630 ctor.to_string()
3631 } else {
3632 "Object".to_string()
3633 };
3634 }
3635 let tag: String = match v {
3636 Value::Undef => "Undefined".into(),
3637 Value::Bool(_) => "Boolean".into(),
3638 Value::Int(_) | Value::Float(_) => "Number".into(),
3639 Value::Str(_) => "String".into(),
3640 Value::Obj(_) => match h.get(v) {
3641 Some(JsObj::Null) => "Null".into(),
3642 Some(JsObj::Str(_)) => "String".into(),
3643 Some(JsObj::Array(_)) if is_arguments_h(h, v) => "Arguments".into(),
3644 Some(JsObj::Array(_)) => "Array".into(),
3645 // A lazy iterator helper brands as node does.
3646 Some(JsObj::Object(p))
3647 if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("IteratorHelper") =>
3648 {
3649 "Iterator Helper".into()
3650 }
3651 // A `DOMException` brands by its class, not as a plain `Error`.
3652 Some(JsObj::Object(p)) if p.contains_key("@@domName") => "DOMException".into(),
3653 // 20.1.3.6 steps 5-8 brand a wrapper by its internal slot, so
3654 // `Object.prototype.toString.call(new Number(1))` is
3655 // `[object Number]` rather than `[object Object]`.
3656 Some(JsObj::Object(p)) if p.contains_key("@@primitive") => match p["@@primitive"] {
3657 Value::Bool(_) => "Boolean".into(),
3658 Value::Int(_) | Value::Float(_) => "Number".into(),
3659 _ => "String".into(),
3660 },
3661 // 20.1.3.6 step 3 brands by `IsArray`, which follows a Proxy to its
3662 // `[[ProxyTarget]]` — `Object.prototype.toString.call(new Proxy([],
3663 // {}))` is `'[object Array]'`. Everything else about a proxy brands
3664 // as a plain Object (a `Symbol.toStringTag` read through the `get`
3665 // trap is handled by the caller, before this).
3666 Some(JsObj::Proxy { target, .. }) => {
3667 let mut cur = target;
3668 for _ in 0..100 {
3669 match h.get(cur) {
3670 Some(JsObj::Proxy { target: t, .. }) => cur = t,
3671 _ => break,
3672 }
3673 }
3674 match h.get(cur) {
3675 Some(JsObj::Array(_)) => "Array".into(),
3676 _ => "Object".into(),
3677 }
3678 }
3679 // `function*` / `async function` / `async function*` carry their own
3680 // `Symbol.toStringTag` in V8 (27.3.3.2, 27.7.3.2, 27.4.3.2).
3681 Some(JsObj::Func(f)) => match h.funcs.get(f.def_id) {
3682 Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction".into(),
3683 Some(d) if d.is_generator => "GeneratorFunction".into(),
3684 Some(d) if d.is_async => "AsyncFunction".into(),
3685 _ => "Function".into(),
3686 },
3687 // `Math`/`JSON`/`Reflect` are namespace OBJECTS, not callables, and
3688 // brand by name (21.3.1.9, 25.5.3, 28.1.14).
3689 Some(JsObj::Builtin(n)) if matches!(n.as_str(), "Math" | "JSON" | "Reflect") => {
3690 n.clone()
3691 }
3692 // A `<Ctor>.prototype` object brands as the constructor it belongs
3693 // to — `Object.prototype.toString.call(Set.prototype)` is
3694 // `[object Set]` — and a `require()`d module namespace is a plain
3695 // object. Neither is a function, so neither brands as one.
3696 Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
3697 match n.strip_suffix(".prototype") {
3698 Some(ctor) if BRANDED_PROTOS.contains(&ctor) => ctor.to_string(),
3699 _ => "Object".into(),
3700 }
3701 }
3702 Some(JsObj::Class(_))
3703 | Some(JsObj::Builtin(_))
3704 | Some(JsObj::BoundFunc { .. })
3705 | Some(JsObj::BoundMethod { .. }) => "Function".into(),
3706 // A suspended generator object is `[object Generator]`; an async one
3707 // `[object AsyncGenerator]`.
3708 Some(JsObj::Generator { .. }) if h.is_async_gen_val(v) => "AsyncGenerator".into(),
3709 Some(JsObj::Generator { .. }) => "Generator".into(),
3710 Some(JsObj::RegExp(_)) => "RegExp".into(),
3711 Some(JsObj::Map { weak, .. }) => if *weak { "WeakMap" } else { "Map" }.into(),
3712 Some(JsObj::Set { weak, .. }) => if *weak { "WeakSet" } else { "Set" }.into(),
3713 Some(JsObj::Promise { .. }) => "Promise".into(),
3714 Some(JsObj::Symbol { .. }) => "Symbol".into(),
3715 Some(JsObj::BigInt(_)) => "BigInt".into(),
3716 // Native-tagged instances brand by their tag; a typed array brands by
3717 // its element kind (`@@kind`), and every Error subclass is `Error`.
3718 Some(JsObj::Object(p)) => match p.get("@@native").map(|t| h.str_of(t)).as_deref() {
3719 Some("TypedArray") => p
3720 .get("@@kind")
3721 .map(|k| h.str_of(k))
3722 .unwrap_or_else(|| "Uint8Array".into()),
3723 Some("Buffer") => "Uint8Array".into(),
3724 // Every native class that really carries a `Symbol.toStringTag`
3725 // in Node brands by its own name. Verified against node v26:
3726 // `Object.prototype.toString.call(new WeakRef({}))` is
3727 // `[object WeakRef]`. The rest of the `@@native` tags
3728 // (`EventEmitter`, `Server`, `Hash`, `Readable`, …) are plain
3729 // classes with NO tag, so they stay `[object Object]` — listing
3730 // them here would invent a brand Node does not have.
3731 Some(
3732 t @ ("ArrayBuffer"
3733 | "DataView"
3734 | "Date"
3735 | "WeakRef"
3736 | "FinalizationRegistry"
3737 | "TextEncoder"
3738 | "TextDecoder"
3739 | "URL"
3740 | "URLSearchParams"),
3741 ) => t.into(),
3742 _ if has_error_data(h, v) => "Error".into(),
3743 _ => "Object".into(),
3744 },
3745 _ => "Object".into(),
3746 },
3747 // node-js only produces the Value variants above; fusevm's shell-oriented
3748 // variants never arise here.
3749 _ => "Object".into(),
3750 };
3751 // Nothing about the value itself brands it. An ordinary object whose CHAIN
3752 // reaches an intrinsic prototype carrying an own `Symbol.toStringTag`
3753 // borrows that one: 20.1.3.6 step 15 is a `Get`, which walks.
3754 // `Object.prototype.toString.call(Object.create(Map.prototype))` is
3755 // `[object Map]` and was `[object Object]`.
3756 //
3757 // Only as a FALLBACK, and only for the prototypes that REALLY carry the
3758 // symbol. A typed array reaches `%TypedArray%.prototype`, whose tag is an
3759 // ACCESSOR returning the specific kind, so consulting the chain FIRST
3760 // branded every view `[object TypedArray]` instead of `[object Uint8Array]`
3761 // — three records caught it. `Error.prototype` carries no tag at all, so
3762 // inheriting from it borrows nothing.
3763 if tag == "Object" && !has_error_data(h, v) {
3764 if let Some(ctor) = chain_tag_ctor(h, v) {
3765 return ctor;
3766 }
3767 }
3768 tag
3769}
3770
3771fn b_setattr(vm: &mut VM, _: u8) -> Value {
3772 let val = vm.pop();
3773 let name = sval(&vm.pop());
3774 let recv = vm.pop();
3775 if let Err(e) = set_property(&recv, &name, val.clone()) {
3776 return abort(vm, e);
3777 }
3778 val
3779}
3780
3781/// `NAMED_EVAL` — SetFunctionName (10.2.9) for a function whose name is only
3782/// known at run time, i.e. one defined under a COMPUTED key: `{ [k]: () => {} }`,
3783/// `class C { static [k] = function(){} }`.
3784///
3785/// The compiler emits this ONLY where the grammar says NamedEvaluation applies
3786/// (`IsAnonymousFunctionDefinition` is a syntactic predicate, not a runtime one:
3787/// `{ m: someAlreadyAnonymousFn }` must NOT be renamed), so the name is set
3788/// unconditionally here.
3789///
3790/// A symbol key becomes `[description]` per step 2 of SetFunctionName; `kind`
3791/// contributes the accessor prefix, so `{ get [k](){} }` is `get <key>`.
3792fn b_named_eval(vm: &mut VM, _: u8) -> Value {
3793 let func = vm.pop();
3794 let kind = vm.pop().to_int();
3795 let key = vm.pop();
3796 let key = sval(&key);
3797 // `@@sym:<id>` / `@@iterator` — an internal symbol key. Step 2: an empty
3798 // description gives the empty name, not `[undefined]`.
3799 let base = match with_host(|h| h.symbol_of_key(&key)) {
3800 Some(sym) => match with_host(|h| h.get(&sym).cloned()) {
3801 Some(JsObj::Symbol {
3802 desc: Some(desc), ..
3803 }) => format!("[{desc}]"),
3804 _ => String::new(),
3805 },
3806 None => key,
3807 };
3808 let name = match kind {
3809 host::member::GET => format!("get {base}"),
3810 host::member::SET => format!("set {base}"),
3811 _ => base,
3812 };
3813 with_host(|h| {
3814 let s = h.new_str(name);
3815 h.set_fn_prop(&func, "name", s);
3816 });
3817 func
3818}
3819
3820/// `[[Set]]` reachable from `crate::proxy`'s no-trap forward, which has to land
3821/// on the same path a plain `o.k = v` takes.
3822pub fn set_property_pub(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3823 set_property(recv, name, val)
3824}
3825
3826/// An object's OWN property as `(value, writable, configurable, is_accessor)`,
3827/// or `None` when it has none. Reads through a Proxy's
3828/// `getOwnPropertyDescriptor` trap, so it answers for any object.
3829pub fn own_prop_facts(obj: &Value, key: &str) -> Option<(Value, bool, bool, bool)> {
3830 let k = with_host(|h| h.new_str(key.to_string()));
3831 let d = own_descriptor_pub(obj, k).ok()?;
3832 if matches!(d, Value::Undef) {
3833 return None;
3834 }
3835 let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
3836 // Each read is hoisted out of the `with_host` borrow: `get_property` takes
3837 // the host itself, so reading inside the closure double-borrows.
3838 let value = field("value");
3839 let writable = field("writable");
3840 let configurable = field("configurable");
3841 let truthy = |v: &Value| with_host(|h| h.truthy(v));
3842 let is_accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
3843 Some((value, truthy(&writable), truthy(&configurable), is_accessor))
3844}
3845
3846/// `OrdinarySetWithOwnDescriptor` (10.1.9.2) with a receiver distinct from the
3847/// object the lookup started on — what `Reflect.set(t, k, v, receiver)` and a
3848/// proxy `set` trap forwarding to it both need.
3849///
3850/// The distinction that matters: an accessor found on `target`'s chain RUNS,
3851/// with `receiver` as `this`; a data property does not write to `target` at all
3852/// but is CREATED on `receiver` through its `[[DefineOwnProperty]]`. Routing
3853/// that second case back through `[[Set]]` made a proxy receiver re-enter its
3854/// own `set` trap forever — the trap body `Reflect.set(t, k, v, recv)` is the
3855/// documented way to forward a write, so the recursion hit every faithful
3856/// handler.
3857pub fn set_with_receiver(
3858 target: &Value,
3859 key: &str,
3860 val: Value,
3861 receiver: &Value,
3862) -> Result<bool, String> {
3863 // A proxy target answers through its own trap, which re-enters here with
3864 // whatever receiver the handler passes on.
3865 if crate::proxy::parts(target).is_some() {
3866 return crate::proxy::set(target, key, &val, receiver);
3867 }
3868 // An accessor anywhere on the target's chain wins, and sees `receiver`.
3869 if let Some((_, setter)) = with_host(|h| host::lookup_accessor(h, target, key)) {
3870 return match setter {
3871 Some(s) => {
3872 host::invoke(&s, vec![val], Some(receiver.clone()))?;
3873 Ok(true)
3874 }
3875 // A getter with no setter refuses the write rather than shadowing it.
3876 None => Ok(false),
3877 };
3878 }
3879 if !with_host(|h| h.can_write_prop(target, key)) {
3880 return Ok(false);
3881 }
3882 // Steps 3.b-3.d: only an object can receive the property, and its OWN
3883 // property decides — an accessor or a read-only slot refuses, and every
3884 // other case defines a plain data property.
3885 //
3886 // `is_object_like`, not a shape test: a string, a symbol and a bigint are
3887 // PRIMITIVES that ride as `Value::Obj` handles here, so the shape check
3888 // passed them through to `defineProperty`, which then threw `called on
3889 // non-object` where 10.1.9.2 step 3.b simply reports `false`.
3890 if !with_host(|h| is_object_like(h, receiver)) {
3891 return Ok(false);
3892 }
3893 if let Some((_, writable, _, is_accessor)) = own_prop_facts(receiver, key) {
3894 if is_accessor || !writable {
3895 return Ok(false);
3896 }
3897 }
3898 // Steps 3.d.iii and 3.e both DEFINE, they do not assign: a setter inherited
3899 // by the receiver must not run, and a proxy receiver must reach its
3900 // `defineProperty` trap rather than its `set` trap.
3901 let desc = with_host(|h| {
3902 let mut m: IndexMap<String, Value> = IndexMap::new();
3903 m.insert("value".into(), val);
3904 m.insert("writable".into(), Value::Bool(true));
3905 m.insert("enumerable".into(), Value::Bool(true));
3906 m.insert("configurable".into(), Value::Bool(true));
3907 h.new_object(m)
3908 });
3909 if crate::proxy::parts(receiver).is_some() {
3910 return crate::proxy::define_property(receiver, key, &desc);
3911 }
3912 let k = with_host(|h| h.new_str(key.to_string()));
3913 define_property_pub(receiver, k, desc)?;
3914 Ok(true)
3915}
3916
3917/// Whether the first argument is a PRIMITIVE — including the three that ride as
3918/// heap handles, which a shape test misses.
3919fn is_primitive_arg(args: &[Value]) -> bool {
3920 let v = arg0(args);
3921 with_host(|h| host::is_primitive(h, &v))
3922}
3923
3924/// The `TypeError` a refused write raises in strict code, worded as V8 does.
3925///
3926/// Adding a key to a non-extensible object reports differently from assigning
3927/// to a read-only one, and the object is named by its brand — `#<Object>` for a
3928/// plain object, `[object Array]` for an array.
3929fn write_refused(recv: &Value, name: &str) -> String {
3930 let extensible = with_host(|h| h.is_extensible(recv));
3931 // Which of the two messages applies turns on whether the key already
3932 // EXISTS. Every shape that keeps its own properties in the fn-prop side
3933 // table answered a blanket `true` here, so adding a key to a frozen
3934 // function reported "read only" where node reports "not extensible".
3935 let has_own = with_host(|h| match h.get(recv) {
3936 Some(JsObj::Object(p)) => p.contains_key(name),
3937 Some(JsObj::Array(items)) => {
3938 name.parse::<usize>()
3939 .map(|i| i < items.len())
3940 .unwrap_or(false)
3941 || h.fn_prop(recv, name).is_some()
3942 }
3943 Some(JsObj::RegExp(_)) => name == "lastIndex" || h.fn_prop(recv, name).is_some(),
3944 _ => h.fn_prop(recv, name).is_some(),
3945 });
3946 if !extensible && !has_own {
3947 return host::type_error(&format!(
3948 "Cannot add property {name}, object is not extensible"
3949 ));
3950 }
3951 // The receiver renders the way every other brand-check message renders one
3952 // — `#<Object>`, `[object Array]`, `[object RegExp]`, `#<Map>`, `#<C>` for a
3953 // class instance, `Error: m` for an error. Only Array was special-cased, so
3954 // every other exotic reported `#<Object>`.
3955 host::type_error(&format!(
3956 "Cannot assign to read only property '{name}' of object '{}'",
3957 no_side_effects_string(recv)
3958 ))
3959}
3960
3961fn set_property(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3962 // 6.2.5.6 `PutValue` begins with `RequireObjectCoercible`: writing any
3963 // property of `undefined` or `null` throws, naming the key. Every such
3964 // write was silently discarded, so `u.x = 1` — the mirror of the single
3965 // most common runtime fault in JS, which the READ side already reports —
3966 // looked like it had succeeded.
3967 if with_host(|h| h.is_nullish(recv)) {
3968 return Err(host::type_error(&format!(
3969 "Cannot set properties of {} (setting '{name}')",
3970 with_host(|h| h.str_of(recv))
3971 )));
3972 }
3973 // A write to a PRIMITIVE receiver has no target — `ToObject` makes a
3974 // throwaway wrapper — so it is discarded in sloppy code and throws in
3975 // strict (10.1.9.2 / 6.2.5.6 again). The refusal was silent in both.
3976 // `is_primitive` rather than a shape test: a string, a symbol and a bigint
3977 // ride as `Value::Obj` handles in this host, so a check for a non-`Obj`
3978 // value caught only numbers and booleans.
3979 if with_host(|h| host::is_primitive(h, recv)) && with_host(|h| h.current_strict()) {
3980 return Err(host::type_error(&format!(
3981 "Cannot create property '{name}' on {} '{}'",
3982 with_host(|h| h.type_of(recv)),
3983 with_host(|h| h.str_of(recv))
3984 )));
3985 }
3986 // `[[PrivateSet]]` (7.3.32) refuses a receiver that carries no such private
3987 // element. The class's own field initializers install theirs directly
3988 // (`host::init_one_field`), so a declaration never reaches this check.
3989 if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
3990 return Err(private_brand_message(name, true));
3991 }
3992 // `[[Set]]` on a Proxy: the handler's `set` trap, or a forward to the target.
3993 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
3994 // A `set` trap that returns falsish REFUSED the write: silent in sloppy
3995 // code, a TypeError in strict, exactly as an ordinary refused write is.
3996 if crate::proxy::set(recv, name, &val, recv)? {
3997 return Ok(());
3998 }
3999 if with_host(|h| h.current_strict()) {
4000 return Err(host::type_error(&format!(
4001 "'set' on proxy: trap returned falsish for property '{name}'"
4002 )));
4003 }
4004 return Ok(());
4005 }
4006 // `globalThis.x = 1` creates a real global binding, so the bare `x` reads it
4007 // back. Writing only the own property left the two views disagreeing:
4008 // `globalThis.zz` was 7 while `zz` was still a `ReferenceError`.
4009 if with_host(|h| h.is_global_object(recv)) && !name.starts_with("@@") {
4010 with_host(|h| h.set_name(name, val.clone()));
4011 }
4012 // `obj.__proto__ = p` re-links the prototype — but only for the two values
4013 // the Annex B setter accepts, an Object or `null`. Everything else is a
4014 // silent no-op in Node (`o.__proto__ = 5` leaves `Object.getPrototypeOf(o)`
4015 // untouched and creates no own key), and a null-prototype object inherits
4016 // no such setter at all, so there the assignment is an ORDINARY own
4017 // property write. Re-linking unconditionally made `o.__proto__ = 5` set the
4018 // prototype to the number 5.
4019 if name == "__proto__" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
4020 if with_host(|h| h.has_null_proto(recv)) {
4021 // falls through to the ordinary own-property write below
4022 } else {
4023 let assignable =
4024 with_host(|h| h.is_null(&val) || matches!(h.kind_of(&val), Some(ObjKind::Object)));
4025 if assignable {
4026 // The `__proto__` setter runs `[[SetPrototypeOf]]`, which a
4027 // NON-EXTENSIBLE object refuses — and unlike an ordinary
4028 // refused write, the setter throws in sloppy code too. It was
4029 // rewriting the link of a frozen object.
4030 if would_cycle(recv, &val) {
4031 return Err(host::type_error("Cyclic __proto__ value"));
4032 }
4033 if !with_host(|h| h.is_extensible(recv)) && !same_prototype(recv, &val) {
4034 return Err(host::type_error(&format!(
4035 "{} is not extensible",
4036 no_side_effects_string(recv)
4037 )));
4038 }
4039 with_host(|h| h.set_proto(recv, val));
4040 }
4041 return Ok(());
4042 }
4043 }
4044 // Every environment value is a STRING. `process.env.PORT = 8080` stores
4045 // "8080", so `process.env.PORT + 1` concatenates the way it does in a real
4046 // process; storing the number made it add instead.
4047 if !name.starts_with("@@")
4048 && with_host(
4049 |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4050 )
4051 {
4052 let text = with_host(|h| h.str_of(&val));
4053 // Write THROUGH to the real environment as well. `process.env` is not a
4054 // private map: node applies the change to the process, so a child
4055 // spawned afterwards inherits it. Keeping it only in the JS object meant
4056 // `process.env.NODE_ENV = 'production'` was invisible to every
4057 // `spawnSync`/`execSync` that followed.
4058 std::env::set_var(name, &text);
4059 let sv = with_host(|h| h.new_str(text));
4060 with_host(|h| {
4061 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4062 p.insert(name.to_string(), sv);
4063 }
4064 });
4065 return Ok(());
4066 }
4067 // Assigning `e.stack` wins permanently: drop the not-yet-formatted marker so
4068 // no later read re-derives a header over the top of the assigned value.
4069 if name == "stack" {
4070 with_host(|h| {
4071 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4072 p.shift_remove("@@stackRaw");
4073 }
4074 });
4075 }
4076 // An inherited/own setter accessor intercepts the write. This is checked
4077 // BEFORE the writable test because 10.1.9.2 branches on the descriptor
4078 // kind first: `writable` is a data-property attribute and means nothing on
4079 // an accessor, where the setter alone decides. Testing it first meant an
4080 // accessor defined through `Object.defineProperty` — which leaves
4081 // `writable` false, having no such field — silently swallowed every write
4082 // instead of calling its setter, so the standard clone idiom
4083 // `Object.create(proto, Object.getOwnPropertyDescriptors(src))` produced an
4084 // object whose setters did nothing. An accessor from an object literal
4085 // carries all-true attributes, which is why only the former broke.
4086 if let Some((getter, setter)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
4087 if let Some(setter) = setter {
4088 let _ = host::invoke(&setter, vec![val], Some(recv.clone()));
4089 return Ok(());
4090 }
4091 // Only a getter: the write is refused — silent in sloppy mode, a
4092 // TypeError in strict code. The `return` above matters, since a
4093 // successful setter call must not fall into this.
4094 let _ = getter;
4095 if with_host(|h| h.current_strict()) {
4096 return Err(host::type_error(&format!(
4097 "Cannot set property {name} of #<Object> which has only a getter"
4098 )));
4099 }
4100 return Ok(());
4101 }
4102 // A non-writable property, or a new key on a non-extensible object, refuses
4103 // the write. In SLOPPY mode that is silent; in strict code it is a
4104 // TypeError, and the ASSIGNMENT SITE decides which — not the object. Every
4105 // refusal used to be silent, so `'use strict'` did not catch a write to a
4106 // frozen object, which is most of the reason to freeze one.
4107 if !with_host(|h| h.can_write_prop(recv, name)) {
4108 if with_host(|h| h.current_strict()) {
4109 return Err(write_refused(recv, name));
4110 }
4111 return Ok(());
4112 }
4113 // Writing `name`/`prototype`/statics on a function value.
4114 if matches!(
4115 with_host(|h| h.kind_of(recv)),
4116 Some(ObjKind::Func) | Some(ObjKind::Class)
4117 ) {
4118 with_host(|h| h.set_fn_prop(recv, name, val));
4119 return Ok(());
4120 }
4121 // Writing a static onto a builtin namespace/ctor (`Error.prepareStackTrace`).
4122 // Each bare reference is a fresh `Builtin` handle, so route to the stable
4123 // per-namespace side table rather than the per-index `fn_props`.
4124 if let Some(ns) = peek(recv, |o| match o {
4125 JsObj::Builtin(ns) => Some(ns.clone()),
4126 _ => None,
4127 }) {
4128 // `process.exitCode` is an accessor in Node, not a data property: the
4129 // setter validates and stores the code the process will finally exit
4130 // with. Landing it in the generic static table made it a write-only
4131 // decoration — `process.exitCode = 3` read back as 3 and the process
4132 // still exited 0.
4133 if ns == "process" && name == "exitCode" {
4134 return crate::stdlib::process::set_exit_code(&val);
4135 }
4136 with_host(|h| h.set_builtin_static(&ns, name, val));
4137 return Ok(());
4138 }
4139 // A write onto a REAL intrinsic prototype object (`Object.prototype`,
4140 // `String.prototype`, `TypeError.prototype`) is mirrored into the
4141 // per-namespace side table as well as the object's own map. Instances are
4142 // not linked to these objects by `proto_of` — the chain walk never reaches
4143 // them — so the mirror is what makes `String.prototype.pad = f` visible as
4144 // `"x".pad`. The own-map write below still happens, so reading the
4145 // prototype itself and enumerating it keep working unchanged.
4146 if let Some(ns) = with_host(|h| {
4147 h.intrinsic_proto_ctor(recv)
4148 .map(str::to_string)
4149 .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4150 }) {
4151 with_host(|h| h.set_builtin_static(&format!("{ns}.prototype"), name, val.clone()));
4152 }
4153 // `re.lastIndex = n` on a RegExp advances/resets its match cursor. The
4154 // writability check above already refused it on a FROZEN regexp, which it
4155 // could only do once `integrity_keys` learned that `lastIndex` is an own
4156 // property.
4157 if name == "lastIndex" {
4158 if let Some(n) = with_host(|h| match h.get(recv) {
4159 Some(JsObj::RegExp(_)) => Some(h.to_number(&val)),
4160 _ => None,
4161 }) {
4162 with_host(|h| {
4163 if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
4164 r.last_index = if n.is_finite() && n >= 0.0 {
4165 crate::utf16::U16Index::new(n as usize)
4166 } else {
4167 crate::utf16::U16Index::ZERO
4168 };
4169 }
4170 });
4171 return Ok(());
4172 }
4173 }
4174 // An `arguments` object is an ORDINARY object with a `length` data property,
4175 // not an array: a write PAST the end adds an index and leaves `length`
4176 // alone. The array backing grew it instead, so `f(1)` followed by
4177 // `arguments[1] = 9` reported `arguments.length` as 2.
4178 if let Ok(i) = name.parse::<usize>() {
4179 if is_arguments(recv) && i >= array_len(recv) {
4180 with_host(|h| h.set_fn_prop(recv, name, val));
4181 return Ok(());
4182 }
4183 }
4184 // Typed-array element write (`ta[i] = v`): coerce + store into `@@elems`.
4185 if !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
4186 let is_ta = crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray");
4187 if is_ta && crate::stdlib::typedarray::elem_set(recv, name, &val)? {
4188 return Ok(());
4189 }
4190 // An index write to a view over a DETACHED buffer is DROPPED. Falling
4191 // through would store it as an ordinary own property, which then showed
4192 // up in `getOwnPropertyDescriptor` over a buffer with no bytes.
4193 if is_ta && crate::stdlib::typedarray::view_detached(recv) {
4194 return Ok(());
4195 }
4196 // `buf[i] = n` writes through to the Buffer's hidden byte array.
4197 if crate::stdlib::buffer::byte_set(recv, name, &val) {
4198 return Ok(());
4199 }
4200 }
4201 // Any own property on an exotic with no property map of its own. This sits
4202 // BELOW the exotic-specific writes above, so a RegExp's `lastIndex` still
4203 // moves its match cursor rather than being shadowed by a side-table entry.
4204 if uses_side_table(recv) {
4205 with_host(|h| h.set_fn_prop(recv, name, val));
4206 return Ok(());
4207 }
4208 // An arbitrary own prop on an array (e.g. exec-result `.index`/`.input`).
4209 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array)
4210 && name != "length"
4211 && name.parse::<usize>().is_err()
4212 {
4213 with_host(|h| h.set_fn_prop(recv, name, val));
4214 return Ok(());
4215 }
4216 // `arr.length = n` (10.4.2.4 `ArraySetLength`) validates BEFORE it resizes,
4217 // and does so outside the host borrow because `ToNumber` may run a user
4218 // `valueOf`. An invalid length throws instead of being silently coerced to 0.
4219 let new_len = if name == "length" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array) {
4220 let want = host::to_array_length(&val)?;
4221 // 10.4.2.4 steps 15-17: shrinking deletes from the END downwards and
4222 // STOPS at the first element that cannot be deleted, leaving the length
4223 // just past it. Truncating regardless discarded a non-configurable
4224 // element and reported a length node would not have accepted.
4225 let floor = with_host(|h| {
4226 let old = match h.get(recv) {
4227 Some(JsObj::Array(items)) => items.len(),
4228 _ => 0,
4229 };
4230 let mut stop = want;
4231 for i in (want..old).rev() {
4232 if !h.prop_attrs(recv, &i.to_string()).configurable {
4233 stop = i + 1;
4234 break;
4235 }
4236 }
4237 stop
4238 });
4239 Some(floor.max(want))
4240 } else {
4241 None
4242 };
4243 with_host(|h| match h.get_mut(recv) {
4244 Some(JsObj::Object(props)) => {
4245 // Adding a *new* array-index key must re-place it into ascending
4246 // integer-key order (updating an existing key keeps its position).
4247 let is_new = !props.contains_key(name);
4248 props.insert(name.to_string(), val);
4249 if is_new && host::array_index(name).is_some() {
4250 host::canonicalize_own_keys(props);
4251 }
4252 }
4253 Some(JsObj::Array(items)) => {
4254 if let Some(n) = new_len {
4255 // Growing `length` appends HOLES (`a=[1]; a.length=3` still has
4256 // just the one own key); shrinking drops any hole past the end.
4257 let old = items.len();
4258 items.resize(n, Value::Undef);
4259 if n > old {
4260 h.mark_hole_range(recv, old..n);
4261 } else {
4262 h.truncate_holes(recv, n);
4263 }
4264 } else if let Ok(i) = name.parse::<usize>() {
4265 // A write PAST the end leaves the skipped positions elided.
4266 let old = items.len();
4267 if i >= old {
4268 items.resize(i + 1, Value::Undef);
4269 }
4270 items[i] = val;
4271 if i > old {
4272 h.mark_hole_range(recv, old..i);
4273 }
4274 // …and the written index itself is no longer one. This is the
4275 // single site that keeps a hole record from outliving the
4276 // elision it describes: every array element write in the
4277 // language reaches it.
4278 h.clear_hole(recv, i);
4279 }
4280 }
4281 _ => {}
4282 });
4283 Ok(())
4284}
4285
4286fn b_getitem(vm: &mut VM, _: u8) -> Value {
4287 let idx = vm.pop();
4288 let recv = vm.pop();
4289 let key = match host::to_property_key(&idx) {
4290 Ok(k) => k,
4291 Err(e) => return abort(vm, e),
4292 };
4293 match get_property(&recv, &key) {
4294 Ok(v) => v,
4295 Err(e) => abort(vm, e),
4296 }
4297}
4298
4299fn b_setitem(vm: &mut VM, _: u8) -> Value {
4300 let val = vm.pop();
4301 let idx = vm.pop();
4302 let recv = vm.pop();
4303 let key = match host::to_property_key(&idx) {
4304 Ok(k) => k,
4305 Err(e) => return abort(vm, e),
4306 };
4307 if let Err(e) = set_property(&recv, &key, val.clone()) {
4308 return abort(vm, e);
4309 }
4310 val
4311}
4312
4313/// `[[Delete]]` (10.1.10) for an already-resolved property key: the one place
4314/// `delete o[k]`, `delete o.k` and `Reflect.deleteProperty` all go through, so
4315/// the three cannot drift. Reports `false` for a non-configurable property
4316/// (sloppy mode ignores the failure rather than throwing) and `true` otherwise,
4317/// which is also what deleting an absent key reports.
4318pub fn delete_property(recv: &Value, key: &str) -> Result<bool, String> {
4319 // 13.5.1.2 step 5 runs `ToObject` on the base, which a nullish one refuses.
4320 // `delete u.x` reported success instead.
4321 if with_host(|h| h.is_nullish(recv)) {
4322 return Err(host::type_error(
4323 "Cannot convert undefined or null to object",
4324 ));
4325 }
4326 // `[[Delete]]` on a Proxy runs the handler's `deleteProperty` trap, which may
4327 // throw — the reason this reports a `Result` rather than a bare `bool`.
4328 if let Some(b) = crate::proxy::delete(recv, key)? {
4329 return Ok(b);
4330 }
4331 // `delete globalThis.x` removes a global a script created. It lives in the
4332 // globals map, not the object's property map, so the ordinary path reported
4333 // success and removed nothing — the binding stayed readable afterwards.
4334 if with_host(|h| h.is_global_object(recv)) && with_host(|h| h.remove_global(key)) {
4335 return Ok(true);
4336 }
4337 // `delete require.cache[id]` drops the module so the next `require` of that
4338 // file runs it again — the whole point of exposing the cache.
4339 if peek(recv, |o| match o {
4340 JsObj::Builtin(ns) => Some(ns == REQUIRE_CACHE),
4341 _ => None,
4342 }) == Some(true)
4343 {
4344 return Ok(crate::module::cache_delete(key));
4345 }
4346 // `delete process.env.X` unsets the variable in the PROCESS, not just in the
4347 // JS view, so a child spawned afterwards no longer sees it.
4348 if !key.starts_with("@@")
4349 && with_host(
4350 |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4351 )
4352 {
4353 std::env::remove_var(key);
4354 }
4355 // A member of a builtin NAMESPACE (`Math.PI`, `Number.MAX_VALUE`,
4356 // `Object.prototype`) is non-configurable when it is a constant or a
4357 // constructor's `prototype`, and `delete` of one answers false without
4358 // removing anything. There is no property map behind a namespace, so the
4359 // ordinary attribute lookup below cannot tell — it reported success for
4360 // every one of them.
4361 // A REAL intrinsic prototype object carries the write in its own map AND in
4362 // the side table the instance read consults, so the delete has to clear
4363 // both. Clearing only the map left `Object.prototype.patch` deleted as far
4364 // as the prototype was concerned and still inherited by every object.
4365 if let Some(ns) = with_host(|h| {
4366 h.intrinsic_proto_ctor(recv)
4367 .map(str::to_string)
4368 .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4369 }) {
4370 with_host(|h| h.remove_builtin_static(&format!("{ns}.prototype"), key));
4371 }
4372 if let Some(ns) = peek(recv, |o| match o {
4373 JsObj::Builtin(ns) => Some(ns.clone()),
4374 _ => None,
4375 }) {
4376 // A script-assigned static is an ordinary configurable property and is
4377 // removed from the side table the assignment landed in. Falling through
4378 // to the attribute check below answered true and deleted nothing, so a
4379 // patch survived its own `delete`.
4380 if with_host(|h| h.remove_builtin_static(&ns, key)) {
4381 return Ok(true);
4382 }
4383 if ns != REQUIRE_CACHE && !builtin_member_configurable(&ns, key) {
4384 return Ok(false);
4385 }
4386 }
4387 if !with_host(|h| h.prop_attrs(recv, key).configurable) {
4388 return Ok(false);
4389 }
4390 // An accessor lives in its own table, not the property map, so removing it
4391 // has to be explicit — otherwise `delete` reported success while the getter
4392 // kept answering and `in` kept reporting the key.
4393 if with_host(|h| h.own_accessor(recv, key).is_some()) {
4394 with_host(|h| h.remove_accessor(recv, key));
4395 return Ok(true);
4396 }
4397 with_host(|h| {
4398 let index = key.parse::<usize>();
4399 match h.get_mut(recv) {
4400 Some(JsObj::Object(props)) => {
4401 props.shift_remove(key);
4402 return;
4403 }
4404 Some(JsObj::Array(items)) => {
4405 if let Ok(i) = index {
4406 if i < items.len() {
4407 // `delete a[i]` punches a HOLE: the length is unchanged
4408 // but the index stops being an own property.
4409 items[i] = Value::Undef;
4410 h.mark_hole(recv, i);
4411 }
4412 return;
4413 }
4414 }
4415 _ => {}
4416 }
4417 // A non-index key on an array (`arr.foo`, `arr[sym]`), or any own key on
4418 // a function/class, is an ordinary own property kept in the side table.
4419 h.remove_fn_prop(recv, key);
4420 });
4421 Ok(true)
4422}
4423
4424fn b_delitem(vm: &mut VM, _: u8) -> Value {
4425 let strict = vm.pop();
4426 let idx = vm.pop();
4427 let recv = vm.pop();
4428 // `delete o[k]` keys through ToPropertyKey (7.1.19), exactly as the read and
4429 // the write do: `String(k)` would turn a Symbol into its `Symbol(desc)`
4430 // description and delete a key nothing ever wrote.
4431 let key = match host::to_property_key(&idx) {
4432 Ok(k) => k,
4433 Err(e) => return abort(vm, e),
4434 };
4435 match delete_property(&recv, &key) {
4436 Ok(false) if with_host(|h| h.truthy(&strict)) => {
4437 abort(vm, refused_delete_error(&recv, &key))
4438 }
4439 Ok(b) => Value::Bool(b),
4440 Err(e) => abort(vm, e),
4441 }
4442}
4443
4444fn b_delprop_name(vm: &mut VM, _: u8) -> Value {
4445 let strict = vm.pop();
4446 let name = sval(&vm.pop());
4447 let recv = vm.pop();
4448 match delete_property(&recv, &name) {
4449 Ok(false) if with_host(|h| h.truthy(&strict)) => {
4450 abort(vm, refused_delete_error(&recv, &name))
4451 }
4452 Ok(b) => Value::Bool(b),
4453 Err(e) => abort(vm, e),
4454 }
4455}
4456
4457/// The TypeError a STRICT `delete` of a non-configurable property raises. The
4458/// receiver renders the way every other brand-check message renders one.
4459fn refused_delete_error(recv: &Value, key: &str) -> String {
4460 // A PROXY names the trap that refused. Only the `delete` OPERATOR reports
4461 // it; `Reflect.deleteProperty` answers `false`, which is why this lives
4462 // here rather than in the shared `[[Delete]]`.
4463 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
4464 return host::type_error(&format!(
4465 "'deleteProperty' on proxy: trap returned falsish for property '{key}'"
4466 ));
4467 }
4468 // A non-callable builtin NAMESPACE renders as a plain object here — node
4469 // reports `#<Object>` for `Math`, not its `[object Math]` brand.
4470 let shown = match peek(recv, |o| match o {
4471 JsObj::Builtin(ns) => Some(ns.clone()),
4472 _ => None,
4473 }) {
4474 Some(ns) if !host::builtin_is_callable(&ns) => "#<Object>".to_string(),
4475 _ => no_side_effects_string(recv),
4476 };
4477 host::type_error(&format!("Cannot delete property '{key}' of {shown}"))
4478}
4479
4480// ── constructors ──────────────────────────────────────────────────────────────
4481
4482fn b_mkstr(vm: &mut VM, argc: u8) -> Value {
4483 let parts = pop_n(vm, argc as usize);
4484 let s: String = with_host(|h| parts.iter().map(|p| h.str_of(p)).collect());
4485 with_host(|h| h.new_str(s))
4486}
4487
4488fn b_mkarr(vm: &mut VM, argc: u8) -> Value {
4489 let items = pop_n(vm, argc as usize);
4490 with_host(|h| h.new_array(items))
4491}
4492
4493/// `MARK_HOLE [arr, index]`: record `arr[index]` as an ELIDED element. Emitted
4494/// only for an array literal that actually contains an elision, so a dense
4495/// literal costs nothing. Returns `undefined`; the array stays on the stack
4496/// underneath (the compiler `Dup`s it).
4497fn b_mark_hole(vm: &mut VM, _: u8) -> Value {
4498 let idx = vm.pop();
4499 let arr = vm.pop();
4500 let i = match idx {
4501 Value::Int(i) if i >= 0 => i as usize,
4502 _ => return Value::Undef,
4503 };
4504 with_host(|h| h.mark_hole(&arr, i));
4505 Value::Undef
4506}
4507
4508fn b_mkobj(vm: &mut VM, argc: u8) -> Value {
4509 let flat = pop_n(vm, argc as usize);
4510 let mut props: IndexMap<String, Value> = IndexMap::new();
4511 // A literal `__proto__: x` key sets the object's prototype (not an own prop).
4512 let mut proto_override: Option<Value> = None;
4513 let mut method_keys: Vec<String> = Vec::new();
4514 let mut i = 0;
4515 while i + 2 < flat.len() || (i + 2 == flat.len() && flat.len() % 3 == 0 && i < flat.len()) {
4516 if i + 2 >= flat.len() {
4517 break;
4518 }
4519 // Tag 2: an ACCESSOR's position. An accessor lives in its own table, so
4520 // the literal reserves its slot here with the `@@ord:` marker key that
4521 // `own_enum_data_keys` resolves back — otherwise `{ get g(){}, d: 2 }`
4522 // enumerated `d, g`, because `DEF_ACCESSOR` runs after `MKOBJ` and its
4523 // marker landed at the end.
4524 if matches!(flat[i], Value::Int(2)) {
4525 let key = with_host(|h| h.str_of(&flat[i + 1]));
4526 props
4527 .entry(format!("{}{key}", host::ORD_MARKER))
4528 .or_insert(Value::Undef);
4529 i += 3;
4530 continue;
4531 }
4532 // Tag 3: a METHOD DEFINITION — an ordinary property whose key is also
4533 // recorded so the literal can become its `[[HomeObject]]` below.
4534 if matches!(flat[i], Value::Int(3)) {
4535 let key = with_host(|h| h.str_of(&flat[i + 1]));
4536 method_keys.push(key.clone());
4537 props.insert(key, flat[i + 2].clone());
4538 i += 3;
4539 continue;
4540 }
4541 let spread = matches!(flat[i], Value::Int(1));
4542 if spread {
4543 let src = flat[i + 1].clone();
4544 // A STRING source spreads its index properties (`{..."ab"}` is
4545 // `{0:'a',1:'b'}`): CopyDataProperties (7.3.25) calls ToObject, and a
4546 // String exotic object owns one enumerable property per UTF-16 code
4547 // UNIT (10.4.3). `own_enum_entries_deep` only walks heap objects, so
4548 // a string source contributed nothing and `{..."ab"}` was `{}`.
4549 // Every other primitive (number/boolean/symbol) boxes to an object
4550 // with no own enumerable properties, and null/undefined are ignored,
4551 // so those correctly stay no-ops on the path below.
4552 if let Some(s) = with_host(|h| h.as_str(&src)) {
4553 for idx in 0..crate::utf16::len(&s) {
4554 if let Ok(ch) = get_property(&src, &idx.to_string()) {
4555 props.insert(idx.to_string(), ch);
4556 }
4557 }
4558 i += 3;
4559 continue;
4560 }
4561 // Object spread copies own *enumerable* properties only — never the
4562 // hidden `@@…` slots (copying `@@native` used to turn `{...buf}`
4563 // into something that still claimed to be a Buffer) and never a
4564 // property a descriptor marked non-enumerable.
4565 // A getter that throws during spread propagates as a thrown value,
4566 // which in the VM means aborting the frame.
4567 let entries = match host::own_enum_entries_deep(&src) {
4568 Ok(e) => e,
4569 Err(e) => return abort(vm, e),
4570 };
4571 for (k, v) in entries {
4572 props.insert(k, v);
4573 }
4574 // `CopyDataProperties` (7.3.25) copies own enumerable SYMBOL keys
4575 // too — only `Object.keys`/`for-in`/`JSON.stringify` skip them.
4576 for (k, v) in with_host(|h| h.own_symbol_entries(&src)) {
4577 props.insert(k, v);
4578 }
4579 } else {
4580 let key = with_host(|h| h.str_of(&flat[i + 1]));
4581 if key == "__proto__" {
4582 proto_override = Some(flat[i + 2].clone());
4583 } else {
4584 props.insert(key, flat[i + 2].clone());
4585 }
4586 }
4587 i += 3;
4588 }
4589 with_host(|h| {
4590 let o = h.new_object(props);
4591 if let Some(p) = proto_override {
4592 if matches!(p, Value::Obj(_)) {
4593 h.set_proto(&o, p);
4594 }
4595 }
4596 // A method DEFINED here takes the literal as its `[[HomeObject]]`, which
4597 // is what `super` inside it resolves through. The home object is fixed
4598 // at definition, so a method that merely arrives as a value
4599 // (`{ m: other.m }`) keeps the one it was defined with — stamping every
4600 // method-valued property instead rebound the original and changed what
4601 // IT resolved.
4602 for key in &method_keys {
4603 let m = match h.get(&o) {
4604 Some(JsObj::Object(p)) => p.get(key).cloned(),
4605 _ => None,
4606 };
4607 if let Some(m) = m {
4608 if let Some(JsObj::Func(f)) = h.get_mut(&m) {
4609 f.home_object = Some(o.clone());
4610 }
4611 }
4612 }
4613 o
4614 })
4615}
4616
4617fn b_mkfunc(vm: &mut VM, _: u8) -> Value {
4618 let def_id = match vm.pop() {
4619 Value::Int(n) => n as usize,
4620 Value::Float(f) => f as usize,
4621 _ => return abort(vm, "internal: MKFUNC id".into()),
4622 };
4623 let (is_arrow, self_name) = with_host(|h| match h.funcs.get(def_id) {
4624 Some(d) => (
4625 d.is_arrow,
4626 (d.self_name && !d.name.is_empty()).then(|| d.name.clone()),
4627 ),
4628 None => (false, None),
4629 });
4630 with_host(|h| {
4631 let mut env = h.current_env_capture();
4632 let this = h.current_this();
4633 // An arrow has no `super` of its own: it uses the enclosing METHOD's,
4634 // exactly as it uses the enclosing `this`. Nothing was captured, so
4635 // `super.m()` inside an arrow reported the method missing — in a class
4636 // method as well as an object literal.
4637 let (home_class, home_static, home_object) = if is_arrow {
4638 h.current_home()
4639 } else {
4640 (None, false, None)
4641 };
4642 // A named function expression closes over an extra scope holding its own
4643 // name, so the body can recurse through it (`function f(){ … f() … }`)
4644 // independently of whatever the outer binding is later set to.
4645 if self_name.is_some() {
4646 env = host::child_env(env);
4647 }
4648 let f = h.alloc(JsObj::Func(FuncVal {
4649 def_id,
4650 env: Some(env.clone()),
4651 this,
4652 is_arrow,
4653 home_class,
4654 home_static,
4655 home_object,
4656 }));
4657 if let Some(n) = self_name {
4658 env.borrow_mut().vars.insert(n, f.clone());
4659 }
4660 f
4661 })
4662}
4663
4664// ── truthiness / coercion / equality ──────────────────────────────────────────
4665
4666fn b_truthy(vm: &mut VM, _: u8) -> Value {
4667 let v = vm.pop();
4668 Value::Bool(with_host(|h| h.truthy(&v)))
4669}
4670
4671fn b_nullish(vm: &mut VM, _: u8) -> Value {
4672 let v = vm.pop();
4673 Value::Bool(with_host(|h| h.is_nullish(&v)))
4674}
4675
4676fn b_tostr(vm: &mut VM, _: u8) -> Value {
4677 let v = vm.pop();
4678 // ToString with user-`toString`/`valueOf` dispatch (template interpolation,
4679 // `String(x)`, object keys).
4680 match host::to_string_value(&v) {
4681 Ok(s) => s,
4682 Err(e) => abort(vm, e),
4683 }
4684}
4685
4686fn b_typeof(vm: &mut VM, _: u8) -> Value {
4687 let v = vm.pop();
4688 with_host(|h| {
4689 let t = h.type_of(&v);
4690 h.new_str(t)
4691 })
4692}
4693
4694/// `typeof <bare ident>`: read the name like `b_getlocal` but return "undefined"
4695/// (never a ReferenceError) when the name is unbound — JS `typeof` semantics.
4696fn b_typeof_name(vm: &mut VM, _: u8) -> Value {
4697 let name = sval(&vm.pop());
4698 // `typeof` does NOT excuse the temporal dead zone: it answers "undefined"
4699 // for an UNBOUND name, but a `let` above its declaration is bound and
4700 // throws. Reading the marker's type answered "function".
4701 if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
4702 return abort(vm, host::tdz_error(&name));
4703 }
4704 if let Some(v) = with_host(|h| h.read_name(&name)) {
4705 if with_host(|h| h.is_tdz(&v)) {
4706 return abort(vm, host::tdz_error(&name));
4707 }
4708 }
4709 // Bound name (user variable) → typeof its value.
4710 if let Some(v) = with_host(|h| h.read_name(&name)) {
4711 return with_host(|h| {
4712 let t = h.type_of(&v);
4713 h.new_str(t)
4714 });
4715 }
4716 // Lazily-bound globals mirror `b_getlocal`: resolve to the same value it
4717 // would produce, then take its type (so object-namespaces like `console`/
4718 // `Math`/`JSON`/`process` report "object", constructors report "function").
4719 let t = match name.as_str() {
4720 "undefined" => "undefined".to_string(),
4721 "NaN" | "Infinity" => "number".to_string(),
4722 "globalThis" | "global" => "object".to_string(),
4723 n if is_namespace(n) || is_known_builtin(n) => {
4724 let v = with_host(|h| h.alloc(JsObj::Builtin(name.clone())));
4725 with_host(|h| h.type_of(&v)).to_string()
4726 }
4727 _ => "undefined".to_string(), // genuinely unbound → JS returns "undefined"
4728 };
4729 with_host(|h| h.new_str(t))
4730}
4731
4732fn b_strict_eq(vm: &mut VM, _: u8) -> Value {
4733 let b = vm.pop();
4734 let a = vm.pop();
4735 Value::Bool(with_host(|h| h.strict_eq(&a, &b)))
4736}
4737
4738fn b_loose_eq(vm: &mut VM, _: u8) -> Value {
4739 let b = vm.pop();
4740 let a = vm.pop();
4741 // Abstract Equality steps 10-11 (7.2.15): object ⇄ primitive converts the
4742 // object with `ToPrimitive` — a JS `valueOf`/`Symbol.toPrimitive` call, so it
4743 // runs before the host borrow. Object ⇄ object stays a reference check.
4744 let (a, b) = match with_host(|h| (host::is_primitive(h, &a), host::is_primitive(h, &b))) {
4745 (false, true) if coerces_against_object(&b) => match host::to_primitive(&a, "default") {
4746 Ok(p) => (p, b),
4747 Err(e) => return abort(vm, e),
4748 },
4749 (true, false) if coerces_against_object(&a) => match host::to_primitive(&b, "default") {
4750 Ok(p) => (a, p),
4751 Err(e) => return abort(vm, e),
4752 },
4753 _ => (a, b),
4754 };
4755 Value::Bool(with_host(|h| h.loose_eq(&a, &b)))
4756}
4757
4758fn b_instanceof(vm: &mut VM, _: u8) -> Value {
4759 let ctor = vm.pop();
4760 let obj = vm.pop();
4761 match host::instance_of(&obj, &ctor) {
4762 Ok(b) => Value::Bool(b),
4763 Err(e) => abort(vm, e),
4764 }
4765}
4766
4767// ── bitwise / unary ───────────────────────────────────────────────────────────
4768
4769fn b_binop(vm: &mut VM, _: u8) -> Value {
4770 let b = vm.pop();
4771 let a = vm.pop();
4772 let tag = match vm.pop() {
4773 Value::Int(n) => n,
4774 _ => 0,
4775 };
4776 // Both operands are ToPrimitive-d with the number hint before ToInt32
4777 // (ECMA-262 13.12.1), which has to happen outside the host borrow.
4778 let r = host::to_primitive(&a, "number")
4779 .and_then(|a| host::to_primitive(&b, "number").map(|b| (a, b)))
4780 .and_then(|(a, b)| with_host(|h| h.bitwise(tag, &a, &b)));
4781 finish(vm, r)
4782}
4783
4784fn b_unary(vm: &mut VM, _: u8) -> Value {
4785 let v = vm.pop();
4786 let tag = match vm.pop() {
4787 Value::Int(n) => n,
4788 _ => 0,
4789 };
4790 // Unary `+`/`~` on a BigInt: `+` is a hard TypeError in JS; `~x` is `-x - 1`
4791 // computed in arbitrary precision.
4792 if with_host(|h| h.is_bigint_val(&v)) {
4793 return match tag {
4794 host::unop::POS => abort(
4795 vm,
4796 host::type_error("Cannot convert a BigInt value to a number"),
4797 ),
4798 host::unop::BITNOT => {
4799 let b = with_host(|h| h.as_bigint(&v)).unwrap();
4800 let r = -(b + num_bigint::BigInt::from(1));
4801 with_host(|h| h.new_bigint(r))
4802 }
4803 _ => Value::Undef,
4804 };
4805 }
4806 // `ToNumber` outside the host borrow: an object operand's `valueOf` /
4807 // `Symbol.toPrimitive` is a JS call, so it cannot run under `with_host`.
4808 let n = match host::to_number_value(&v) {
4809 Ok(n) => n,
4810 Err(e) => return abort(vm, e),
4811 };
4812 match tag {
4813 host::unop::POS => Value::Float(n),
4814 host::unop::BITNOT => {
4815 let i = if n.is_finite() {
4816 n.trunc() as i64 as i32
4817 } else {
4818 0
4819 };
4820 Value::Float(!i as f64)
4821 }
4822 _ => Value::Undef,
4823 }
4824}
4825
4826// ── membership ────────────────────────────────────────────────────────────────
4827
4828fn b_contains(vm: &mut VM, _: u8) -> Value {
4829 let container = vm.pop();
4830 let key = vm.pop();
4831 // `x in y` requires y to be an object. V8 names both operands:
4832 // `Cannot use 'in' operator to search for 'a' in 5`.
4833 // A heap-backed PRIMITIVE — a string, a symbol, a bigint — is a
4834 // `Value::Obj` in this host but is not an object, so the shape test alone
4835 // let `'length' in 'ab'` and `'description' in Symbol('x')` answer `true`
4836 // where node throws. `is_primitive` is the same predicate `ToObject` and
4837 // `typeof` use, so the three cannot disagree about what an object is.
4838 if !matches!(container, Value::Obj(_)) || with_host(|h| host::is_primitive(h, &container)) {
4839 let (k, c) = with_host(|h| (h.property_key(&key), h.str_of(&container)));
4840 return abort(
4841 vm,
4842 host::type_error(&format!(
4843 "Cannot use 'in' operator to search for '{k}' in {c}"
4844 )),
4845 );
4846 }
4847 let k = match host::to_property_key(&key) {
4848 Ok(k) => k,
4849 Err(e) => return abort(vm, e),
4850 };
4851 match has_property(&container, &k) {
4852 Ok(b) => Value::Bool(b),
4853 Err(e) => abort(vm, e),
4854 }
4855}
4856
4857// ── control ───────────────────────────────────────────────────────────────────
4858
4859fn b_sig_return(vm: &mut VM, _: u8) -> Value {
4860 let v = vm.pop();
4861 with_host(|h| h.signal = Some(host::Signal::Return(v.clone())));
4862 vm.ip = vm.chunk.ops.len();
4863 v
4864}
4865
4866/// `break [label]` whose target loop lives in an enclosing chunk (the statement is
4867/// inside a `try` block, which the host runs as its own chunk). Raise the signal
4868/// and halt this chunk; `SIG_UNWIND` after the `TRY` op re-dispatches it.
4869fn b_sig_break(vm: &mut VM, _: u8) -> Value {
4870 let label = sval(&vm.pop());
4871 let label = (!label.is_empty()).then_some(label);
4872 with_host(|h| h.signal = Some(host::Signal::Break(label)));
4873 vm.ip = vm.chunk.ops.len();
4874 Value::Undef
4875}
4876
4877/// `continue [label]` out of a `try` block — see [`b_sig_break`].
4878fn b_sig_continue(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::Continue(label)));
4882 vm.ip = vm.chunk.ops.len();
4883 Value::Undef
4884}
4885
4886/// Dispatch a pending control signal at the instruction after a `TRY`. `tag`
4887/// describes what the `try` is nested in (see [`host::unwind`]):
4888///
4889/// * no signal → `NONE`, execution continues normally;
4890/// * `Return`, or no enclosing loop in this chunk → halt the chunk so the signal
4891/// keeps travelling outward;
4892/// * `break`/`continue` targeting the enclosing loop → consume it and report
4893/// `BREAK`/`CONTINUE` so the compiler-emitted jump lands on the loop's exit /
4894/// continue target;
4895/// * a LABELED `break`/`continue` for some outer loop → report `BREAK` but leave
4896/// the signal pending, so leaving this loop re-dispatches it one level out.
4897fn b_sig_unwind(vm: &mut VM, _: u8) -> Value {
4898 let cont_tag = sval(&vm.pop());
4899 let brk_tag = sval(&vm.pop());
4900 let sig = match with_host(|h| h.signal.clone()) {
4901 Some(s) => s,
4902 None => return Value::Int(host::unwind::NONE),
4903 };
4904 // Nothing in this chunk can catch a `break`: halt so the signal keeps going.
4905 let propagate = |vm: &mut VM| {
4906 vm.ip = vm.chunk.ops.len();
4907 Value::Int(host::unwind::NONE)
4908 };
4909 match &sig {
4910 host::Signal::Return(_) => propagate(vm),
4911 host::Signal::Break(label) => {
4912 if brk_tag == host::unwind::NO_LOOP {
4913 return propagate(vm);
4914 }
4915 let mine = match label {
4916 None => true, // unlabeled: always the innermost enclosing context
4917 Some(l) => brk_tag == *l,
4918 };
4919 if mine {
4920 with_host(|h| h.signal = None);
4921 }
4922 // Not ours: still leave this context by its break exit, keeping the
4923 // signal pending for the next dispatch point one level out.
4924 Value::Int(host::unwind::BREAK)
4925 }
4926 host::Signal::Continue(label) => {
4927 let mine = match label {
4928 // Unlabeled `continue` binds to the innermost continue-catching
4929 // loop — which a `switch` between here and it is NOT.
4930 None => cont_tag != host::unwind::NO_LOOP,
4931 Some(l) => cont_tag == *l,
4932 };
4933 if mine {
4934 with_host(|h| h.signal = None);
4935 return Value::Int(host::unwind::CONTINUE);
4936 }
4937 if brk_tag == host::unwind::NO_LOOP {
4938 return propagate(vm);
4939 }
4940 // The target loop is further out: exit the innermost context here and
4941 // re-dispatch there.
4942 Value::Int(host::unwind::BREAK)
4943 }
4944 }
4945}
4946
4947fn b_throw(vm: &mut VM, _: u8) -> Value {
4948 let v = vm.pop();
4949 let msg = with_host(|h| {
4950 h.exc = Some(v.clone());
4951 // Prefer an error object's message for the top-level report.
4952 error_display(h, &v)
4953 });
4954 abort(vm, msg)
4955}
4956
4957fn error_display(h: &host::JsHost, v: &Value) -> String {
4958 if let Some(JsObj::Object(props)) = h.get(v) {
4959 let name = props
4960 .get("name")
4961 .map(|x| h.str_of(x))
4962 .unwrap_or_else(|| "Error".into());
4963 if let Some(m) = props.get("message") {
4964 return format!("Uncaught {name}: {}", h.str_of(m));
4965 }
4966 }
4967 format!("Uncaught {}", h.str_of(v))
4968}
4969
4970fn b_try(vm: &mut VM, _: u8) -> Value {
4971 let id = match vm.pop() {
4972 Value::Int(n) => n as usize,
4973 _ => return abort(vm, "internal: TRY id".into()),
4974 };
4975 // Shape only. Running a `try` used to clone the whole `TryDef` — its block,
4976 // its handler and its finalizer bytecode — every time control entered it,
4977 // which for a `try` inside a loop is once per iteration.
4978 let (has_handler, catch_bind, has_finalizer) = match with_host(|h| h.try_shape(id)) {
4979 Some(t) => t,
4980 None => return abort(vm, "internal: unknown try id".into()),
4981 };
4982 let mut pending: Option<String> = None;
4983 // Each sub-block runs as its own chunk on THIS frame, so a throw part-way
4984 // through can leave block scopes open. Snapshot the scope and restore it
4985 // before the handler and after the whole statement.
4986 let scope = with_host(|h| h.scope_snapshot());
4987
4988 with_host(|h| h.push_scope()); // the try block is its own block scope
4989 let body_res = host::run_chunk_keyed(host::try_key(id, 0), || {
4990 with_host(|h| h.try_chunk(id, 0)).expect("try block exists")
4991 });
4992 with_host(|h| h.restore_scope(scope.clone()));
4993 let signal_after = with_host(|h| h.signal.is_some());
4994 if let Err(e) = body_res {
4995 if signal_after {
4996 pending = Some(e);
4997 } else if has_handler {
4998 // Bind the thrown value (or a synthesized error) to the catch param.
4999 let thrown =
5000 with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
5001 with_host(|h| {
5002 h.error = None;
5003 h.exc = None;
5004 });
5005 // The catch parameter is block-scoped to the handler.
5006 with_host(|h| h.push_scope());
5007 if let Some(name) = &catch_bind {
5008 with_host(|h| h.declare_name(name, thrown));
5009 }
5010 let hres = host::run_chunk_keyed(host::try_key(id, 1), || {
5011 with_host(|h| h.try_chunk(id, 1)).expect("handler exists")
5012 });
5013 with_host(|h| h.restore_scope(scope.clone()));
5014 if let Err(e2) = hres {
5015 pending = Some(e2);
5016 }
5017 } else {
5018 pending = Some(e);
5019 }
5020 }
5021
5022 // finally always runs; a finally error/signal supersedes.
5023 if has_finalizer {
5024 let sig_before = with_host(|h| h.signal.take());
5025 with_host(|h| h.push_scope()); // ditto for `finally`
5026 let fres = host::run_chunk_keyed(host::try_key(id, 2), || {
5027 with_host(|h| h.try_chunk(id, 2)).expect("finalizer exists")
5028 });
5029 with_host(|h| h.restore_scope(scope.clone()));
5030 match fres {
5031 Ok(_) => {
5032 if with_host(|h| h.signal.is_none()) {
5033 // The finalizer completed normally: the try/catch block's own
5034 // abrupt completion resumes.
5035 with_host(|h| h.signal = sig_before);
5036 } else {
5037 // ECMA-262 14.15.3 TryStatement evaluation: when the finalizer's
5038 // completion is abrupt (`return`/`break`/`continue` inside
5039 // `finally`), that completion REPLACES the try/catch block's —
5040 // including a pending throw, which is discarded, not rethrown.
5041 pending = None;
5042 with_host(|h| {
5043 h.error = None;
5044 h.exc = None;
5045 });
5046 }
5047 }
5048 Err(e) => pending = Some(e),
5049 }
5050 }
5051
5052 if let Some(e) = pending {
5053 return abort(vm, e);
5054 }
5055 Value::Undef
5056}
5057
5058/// Synthesize an `Error`-shaped object from an internal error string, linked to
5059/// the matching builtin error prototype so `instanceof`/`.constructor` work.
5060pub(crate) fn synth_error(h: &mut host::JsHost, e: &str) -> Value {
5061 h.ensure_error_protos();
5062 // A `DOMException` marker: the WHATWG error NAME rides in the string, since
5063 // it is not one of the ECMAScript error classes below.
5064 if let Some(rest) = e.strip_prefix(host::DOM_MARK) {
5065 if let Some((name, msg)) = rest.split_once('\u{1}') {
5066 return dom_exception_with(h, name, msg);
5067 }
5068 }
5069 // A `Name [ERR_CODE]: message` head carries a Node error `code` next to the
5070 // error class, exactly as Node's internal errors render it in `.stack`.
5071 let (head, rest) = match e.split_once(": ") {
5072 Some((n, m)) => (n, m.to_string()),
5073 None => ("", e.to_string()),
5074 };
5075 let (base, code) = match head.split_once(" [") {
5076 Some((n, c)) if c.ends_with(']') => (n, Some(c[..c.len() - 1].to_string())),
5077 _ => (head, None),
5078 };
5079 let (name, mut message) = if host::ERROR_NAMES.contains(&base) {
5080 (base.to_string(), rest)
5081 } else {
5082 ("Error".to_string(), e.to_string())
5083 };
5084 // A `host::plain_coded_error` marker: the code rides at the head of the
5085 // MESSAGE rather than in the class, because Node's native-layer errors set
5086 // `.code` while leaving `String(err)` unbracketed (`TypeError: Invalid URL`
5087 // with `code === 'ERR_INVALID_URL'`). Strip it back off here — the marker is
5088 // internal and must never reach a user-visible `.message`.
5089 let mut code = code;
5090 // Whether `String(err)`/`err.stack` show `Name [CODE]:` — true for the
5091 // bracketed head, false for the marker form.
5092 let mut bracketed = code.is_some();
5093 // Extra own properties (`input`, `base`) from `host::plain_coded_error_with`.
5094 let mut fields: Vec<(String, String)> = Vec::new();
5095 if let Some(rest) = message.strip_prefix(host::CODE_MARK) {
5096 if let Some((c, m)) = rest.split_once('\u{1}') {
5097 code = Some(c.to_string());
5098 bracketed = false;
5099 let (m, fs) = host::split_error_fields(m);
5100 fields = fs
5101 .into_iter()
5102 .map(|(k, v)| (k.to_string(), v.to_string()))
5103 .collect();
5104 message = m.to_string();
5105 }
5106 }
5107 let mut props: IndexMap<String, Value> = IndexMap::new();
5108 let mv = h.new_str(message.clone());
5109 props.insert("message".into(), mv);
5110 if let Some(c) = &code {
5111 let cv = h.new_str(c.clone());
5112 props.insert("code".into(), cv);
5113 for (k, v) in fields {
5114 let fv = h.new_str(v);
5115 props.insert(k, fv);
5116 }
5117 if bracketed {
5118 // Marks this as a Node JS-layer error, whose `toString` brackets the
5119 // code. A native-layer error has the same `.code` and does not.
5120 props.insert("@@nodeError".into(), Value::Bool(true));
5121 }
5122 }
5123 let label = match (&code, bracketed) {
5124 (Some(c), true) => format!("{name} [{c}]"),
5125 _ => name.clone(),
5126 };
5127 let frames = h.stack_frames();
5128 let stack = if message.is_empty() {
5129 format!("{label}{frames}")
5130 } else {
5131 format!("{label}: {message}{frames}")
5132 };
5133 let sv = h.new_str(stack);
5134 props.insert("stack".into(), sv);
5135 // A libuv system-error message is itself the canonical encoding of the
5136 // error's metadata — `ENOENT: no such file or directory, open '/x'` — so a
5137 // filesystem/network failure recovers the enumerable `code`/`errno`/
5138 // `syscall`/`path` own properties that `err.code === 'ENOENT'` checks (the
5139 // single most common error-handling idiom in Node packages) depend on.
5140 for (k, v) in syscall_error_fields(&message) {
5141 let sv = match v {
5142 SysField::Str(s) => h.new_str(s),
5143 SysField::Num(n) => Value::Float(n),
5144 };
5145 props.insert(k.into(), sv);
5146 }
5147 let obj = h.new_object(props);
5148 if let Some(p) = host::error_proto_of(h, &name) {
5149 h.set_proto(&obj, p);
5150 }
5151 // `message`/`stack` are non-enumerable; a Node `ERR_*` error's `code` is not
5152 // (`Object.keys(e)` on an `ERR_INVALID_ARG_TYPE` reads `["code"]`).
5153 h.hide_prop(&obj, "message");
5154 h.hide_prop(&obj, "stack");
5155 obj
5156}
5157
5158enum SysField {
5159 Str(String),
5160 Num(f64),
5161}
5162
5163/// Decompose a libuv-shaped message (`ECODE: reason, syscall 'path'`) into the
5164/// own properties Node hangs off a system error. Returns empty for any message
5165/// that is not in that shape.
5166fn syscall_error_fields(message: &str) -> Vec<(&'static str, SysField)> {
5167 let (code, rest) = match message.split_once(": ") {
5168 Some((c, r))
5169 if c.len() >= 2
5170 && c.starts_with('E')
5171 && c.bytes()
5172 .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()) =>
5173 {
5174 (c, r)
5175 }
5176 _ => return Vec::new(),
5177 };
5178 let mut out: Vec<(&'static str, SysField)> = vec![
5179 ("errno", SysField::Num(errno_for(code))),
5180 ("code", SysField::Str(code.to_string())),
5181 ];
5182 // `reason, syscall 'path'` — the path is optional (`EPIPE: …, write`).
5183 if let Some((_, tail)) = rest.split_once(", ") {
5184 let (syscall, path) = match tail.split_once(" '") {
5185 // A two-path message ends `'from' -> 'to'`; `err.path` is the FIRST
5186 // one, so the scan stops at its closing quote rather than at the
5187 // end of the line — which had been swallowing `' -> 'dest` into the
5188 // path for every `rename` and `copyFile` failure.
5189 Some((s, p)) => (s, p.split_once('\'').map(|(first, _)| first)),
5190 None => (tail, None),
5191 };
5192 out.push(("syscall", SysField::Str(syscall.to_string())));
5193 if let Some(p) = path {
5194 out.push(("path", SysField::Str(p.to_string())));
5195 }
5196 }
5197 out
5198}
5199
5200/// The negative `errno` Node reports for a libuv error code on this platform.
5201/// Only the codes `err_str` can produce are mapped; anything else reports the
5202/// generic `EIO` number rather than inventing a value.
5203fn errno_for(code: &str) -> f64 {
5204 let n: i32 = match code {
5205 "ENOENT" => 2,
5206 "EACCES" => 13,
5207 "EEXIST" => 17,
5208 "ENOTDIR" => 20,
5209 "EISDIR" => 21,
5210 "EINVAL" => 22,
5211 "EPIPE" => 32,
5212 "ENOTEMPTY" => 66,
5213 _ => 5, // EIO
5214 };
5215 -f64::from(n)
5216}
5217
5218// ── iteration ─────────────────────────────────────────────────────────────────
5219
5220fn b_getiter(vm: &mut VM, _: u8) -> Value {
5221 let v = vm.pop();
5222 // A generator is its own iterator (resumed lazily by FORITER).
5223 if with_host(|h| h.is_generator_val(&v)) {
5224 return v;
5225 }
5226 // A Proxy's iterator comes from its traps, materialized eagerly: the
5227 // `lookup_chain` probe below reads the property map a proxy does not have.
5228 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5229 return match crate::proxy::iterate(&v) {
5230 Ok(Some(items)) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
5231 Ok(None) => abort(vm, "internal: kind_of said Proxy".into()),
5232 Err(e) => abort(vm, e),
5233 };
5234 }
5235 // Arrays and strings take the direct path below: they have no iterator
5236 // state to preserve and are the hot case, so they must not pay a property
5237 // lookup and a call per loop.
5238 let direct = matches!(
5239 with_host(|h| h.kind_of(&v)),
5240 Some(ObjKind::Array) | Some(ObjKind::Str)
5241 );
5242 // …but only while their `Symbol.iterator` is still reachable. It comes from
5243 // the intrinsic prototype, so replacing the link takes it away: node reports
5244 // `a is not iterable` for an array whose prototype is a plain object, where
5245 // the fast path below iterated the backing vector regardless.
5246 if !own_intrinsic_reachable(&v)
5247 && !matches!(
5248 get_property(&v, "@@iterator"),
5249 Ok(ref f) if with_host(|h| host::is_callable(h, f))
5250 )
5251 {
5252 let shown = with_host(|h| h.inspect(&v));
5253 let msg = host::type_error(&format!("{shown} is not iterable"));
5254 return abort(vm, host::name_call_site(vm, &shown, msg));
5255 }
5256 // Anything else with a `Symbol.iterator`: call it for the iterator object.
5257 //
5258 // Resolved as a full property READ, not a stored-property lookup. A
5259 // NATIVE-tagged object (`URLSearchParams`, `Headers`, `Map`, `Set`)
5260 // dispatches its methods through the stdlib table rather than a property
5261 // map, so a `lookup_chain` probe found nothing and the loop fell through to
5262 // materializing the value — which threw for `URLSearchParams` and
5263 // snapshotted for `Map`. Spreading the same object already worked, because
5264 // that path had been fixed and this one had not.
5265 if !direct {
5266 if let Ok(iter_fn) = get_property(&v, "@@iterator") {
5267 if with_host(|h| host::is_callable(h, &iter_fn)) {
5268 return match host::invoke(&iter_fn, Vec::new(), Some(v.clone())) {
5269 Ok(it) => it,
5270 Err(e) => abort(vm, e),
5271 };
5272 }
5273 }
5274 }
5275 match with_host(|h| h.iter_vec(&v)) {
5276 Ok(items) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
5277 // V8 names the SOURCE EXPRESSION, not the value: `for (const x of a)`
5278 // reports `a is not iterable`. The text was recorded for this op.
5279 Err(e) => {
5280 let shown = with_host(|h| h.inspect(&v));
5281 let named = host::name_call_site(vm, &shown, e);
5282 abort(vm, named)
5283 }
5284 }
5285}
5286
5287fn b_forin_keys(vm: &mut VM, _: u8) -> Value {
5288 let v = vm.pop();
5289 // `for-in` over a Proxy is 14.7.5.9 `EnumerateObjectProperties`: the
5290 // `ownKeys` trap filtered by `[[GetOwnProperty]]`'s `enumerable`. Both traps
5291 // are user code, so this cannot run inside `enum_keys`'s `&mut` host borrow.
5292 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5293 // `ownKeys` ONLY. The `enumerable` filter is 14.7.5.10's per-key
5294 // `[[GetOwnProperty]]`, which `FORIN_ALIVE` runs at the moment each key
5295 // is visited — so the `getOwnPropertyDescriptor` traps interleave with
5296 // the body the way node's do, instead of all firing up front.
5297 return match crate::proxy::own_keys(&v) {
5298 Ok(keys) => with_host(|h| {
5299 let out: Vec<Value> = keys
5300 .unwrap_or_default()
5301 .into_iter()
5302 .filter(|k| !host::is_symbol_key(k))
5303 .map(|k| h.new_str(k))
5304 .collect();
5305 h.new_array(out)
5306 }),
5307 Err(e) => abort(vm, e),
5308 };
5309 }
5310 let mut keys = with_host(|h| h.enum_keys(&v));
5311 // A member patched onto the receiver's INTRINSIC prototype is enumerable
5312 // and inherited, so `for-in` visits it after the own keys — but the
5313 // intrinsic prototypes are not links `enum_keys` can walk, so its chain
5314 // pass never reaches them.
5315 if !with_host(|h| h.has_null_proto(&v)) {
5316 let seen: Vec<String> = keys.iter().map(|k| with_host(|h| h.str_of(k))).collect();
5317 for ns in intrinsic_proto_namespaces(&v) {
5318 for k in with_host(|h| h.builtin_static_keys(&ns)) {
5319 if !seen.contains(&k) && !intrinsic_proto_member(&ns, &k) {
5320 keys.push(with_host(|h| h.new_str(k)));
5321 }
5322 }
5323 }
5324 }
5325 with_host(|h| h.new_array(keys))
5326}
5327
5328/// The intrinsic prototype namespaces `v` inherits from, nearest first — its
5329/// own constructor's and then `Object`'s, the same two steps
5330/// `inherited_builtin_static` looks a value up in.
5331fn intrinsic_proto_namespaces(v: &Value) -> Vec<String> {
5332 let ctor = match wrapped_primitive(v).as_ref().and_then(wrapper_ctor_of) {
5333 Some(c) => Some(c),
5334 None if is_arguments(v) => Some("Object"),
5335 None => with_host(|h| default_ctor_name(h, v)),
5336 };
5337 let mut out: Vec<String> = ctor
5338 .filter(|c| *c != "Object")
5339 .map(|c| format!("{c}.prototype"))
5340 .into_iter()
5341 .collect();
5342 out.push("Object.prototype".to_string());
5343 out
5344}
5345
5346/// `FORIN_ALIVE` — is `key` STILL an enumerable property of `obj`?
5347///
5348/// `for-in` takes its key list once (14.7.5.10 builds it lazily, but a snapshot
5349/// of the enumerable keys is observationally the same for everything except
5350/// this), and the body can delete a key before the loop reaches it. Node does
5351/// not visit a key deleted that way; without this check `delete d.z` inside the
5352/// loop still produced `x,y,z`.
5353///
5354/// The check is `[[GetOwnProperty]]`-shaped rather than `in`: on a Proxy it runs
5355/// the `getOwnPropertyDescriptor` trap, which is what node runs, and NOT the
5356/// `has` trap, which node never fires for `for-in`. That also puts each trap
5357/// call immediately before its visit, matching node's interleaving — the trap
5358/// log used to show every `gopd` up front because the key list was filtered
5359/// eagerly.
5360fn b_forin_alive(vm: &mut VM, _: u8) -> Value {
5361 let key = vm.pop();
5362 let obj = vm.pop();
5363 let name = with_host(|h| h.str_of(&key));
5364 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
5365 return match crate::proxy::own_enumerable(&obj, &name) {
5366 Ok(b) => Value::Bool(b),
5367 Err(e) => abort(vm, e),
5368 };
5369 }
5370 // A STRING's keys are its character indices. `in` is not defined on a string
5371 // primitive at all, so the ordinary path below has no answer for one and
5372 // `for (const i in 'abc')` came back empty.
5373 if let Some(s) = with_host(|h| h.as_str(&obj)) {
5374 let len = crate::utf16::len(&s);
5375 return Value::Bool(name.parse::<usize>().is_ok_and(|i| i < len));
5376 }
5377 // Any other receiver: EXISTENCE only. Node re-checks that the key is still
5378 // there and does NOT re-check enumerability — making one non-enumerable
5379 // mid-loop still visits it, where re-filtering on `enumerable` dropped it.
5380 // (The Proxy branch above does re-check, because there the answer comes from
5381 // the trap node itself calls.)
5382 Value::Bool(has_property_ordinary(&obj, &name))
5383}
5384
5385fn b_foriter(vm: &mut VM, _: u8) -> Value {
5386 let it = match vm.stack.last() {
5387 Some(v) => v.clone(),
5388 None => return abort(vm, "internal: FORITER with empty stack".into()),
5389 };
5390 // Eager array-backed iterator (arrays/strings/Map/Set).
5391 let eager = with_host(|h| {
5392 if let Some(JsObj::Iter { items, idx }) = h.get_mut(&it) {
5393 if *idx < items.len() {
5394 let v = items[*idx].clone();
5395 *idx += 1;
5396 return Some(Some(v));
5397 }
5398 return Some(None);
5399 }
5400 None
5401 });
5402 if let Some(step) = eager {
5403 return match step {
5404 Some(v) => {
5405 vm.push(v);
5406 Value::Bool(true)
5407 }
5408 None => Value::Bool(false),
5409 };
5410 }
5411 // Generator: resume one step.
5412 if with_host(|h| h.is_generator_val(&it)) {
5413 return match host::gen_resume(&it, Value::Undef) {
5414 Ok(host::GenStep::Yield(v)) => {
5415 vm.push(v);
5416 Value::Bool(true)
5417 }
5418 Ok(host::GenStep::Done(_)) => Value::Bool(false),
5419 Err(e) => abort(vm, e),
5420 };
5421 }
5422 // A user iterator object with a `.next()` returning `{ value, done }`.
5423 match host::call_method(&it, "next", Vec::new()) {
5424 Ok(step) => {
5425 let done = get_property(&step, "done")
5426 .map(|d| with_host(|h| h.truthy(&d)))
5427 .unwrap_or(true);
5428 if done {
5429 Value::Bool(false)
5430 } else {
5431 match get_property(&step, "value") {
5432 Ok(v) => {
5433 vm.push(v);
5434 Value::Bool(true)
5435 }
5436 Err(e) => abort(vm, e),
5437 }
5438 }
5439 }
5440 Err(e) => abort(vm, e),
5441 }
5442}
5443
5444fn b_unpack(vm: &mut VM, _: u8) -> Value {
5445 let star = match vm.pop() {
5446 Value::Int(n) => n,
5447 _ => -1,
5448 };
5449 let count = match vm.pop() {
5450 Value::Int(n) => n as usize,
5451 _ => 0,
5452 };
5453 let iterable = vm.pop();
5454 // Without a `...rest` element the pattern needs exactly `count` values and
5455 // must then close the iterator; draining hung on an unbounded source.
5456 let items = match if star < 0 {
5457 host::iter_take(&iterable, count)
5458 } else {
5459 host::iter_all(&iterable)
5460 } {
5461 Ok(v) => v,
5462 // Destructuring a non-iterable names the SOURCE EXPRESSION, the way
5463 // `for-of` does: `const [x] = o` reports `o is not iterable`. The text
5464 // was recorded for this op at compile time.
5465 Err(e) => {
5466 // Node names the source only when the pattern's right-hand side is
5467 // a plain IDENTIFIER — `const [x] = o` is `o is not iterable`.
5468 // Anything else (a member, a call, a nested pattern, a parameter)
5469 // reports the TYPE instead, with the property note. Measured across
5470 // twelve shapes rather than guessed.
5471 let msg = match host::call_site_text(vm) {
5472 Some(text) => host::type_error(&format!("{text} is not iterable")),
5473 None if e.ends_with(" is not iterable") => {
5474 host::type_error(¬_iterable_typed(&iterable))
5475 }
5476 None => e,
5477 };
5478 return abort(vm, msg);
5479 }
5480 };
5481 let ordered: Vec<Value> = if star < 0 {
5482 (0..count)
5483 .map(|i| items.get(i).cloned().unwrap_or(Value::Undef))
5484 .collect()
5485 } else {
5486 let si = star as usize;
5487 let after = count.saturating_sub(si + 1);
5488 let rest_end = items.len().saturating_sub(after).max(si);
5489 let mut out: Vec<Value> = Vec::with_capacity(count);
5490 for i in 0..si {
5491 out.push(items.get(i).cloned().unwrap_or(Value::Undef));
5492 }
5493 let rest: Vec<Value> = items
5494 .get(si..rest_end)
5495 .map(|s| s.to_vec())
5496 .unwrap_or_default();
5497 out.push(with_host(|h| h.new_array(rest)));
5498 for j in 0..after {
5499 out.push(items.get(rest_end + j).cloned().unwrap_or(Value::Undef));
5500 }
5501 out
5502 };
5503 if ordered.is_empty() {
5504 return Value::Undef;
5505 }
5506 for it in ordered[1..].iter().rev().cloned() {
5507 vm.push(it);
5508 }
5509 ordered[0].clone()
5510}
5511
5512fn b_build_args(vm: &mut VM, argc: u8) -> Value {
5513 let flat = pop_n(vm, argc as usize);
5514 let mut out = Vec::new();
5515 // Elided positions of an array literal (tag 2), recorded as the run-time
5516 // index each lands on — which only this walk knows, because a preceding
5517 // spread contributes an unknown number of elements. Call-argument lists,
5518 // the other `BUILD_ARGS` caller, cannot contain an elision, so this stays
5519 // empty for them.
5520 let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
5521 let mut i = 0;
5522 while i + 1 < flat.len() {
5523 let val = flat[i + 1].clone();
5524 match flat[i] {
5525 // Tag 1 is an ARRAY-LITERAL spread, tag 3 a CALL-ARGUMENT one. They
5526 // report a non-iterable differently, which is the only reason the
5527 // two are told apart here.
5528 Value::Int(1) => match host::iter_all(&val).map_err(|e| {
5529 let shown = with_host(|h| h.inspect(&val));
5530 host::name_call_site(vm, &shown, e)
5531 }) {
5532 Ok(items) => out.extend(items),
5533 Err(e) => return abort(vm, e),
5534 },
5535 Value::Int(3) => match host::iter_all(&val) {
5536 Ok(items) => out.extend(items),
5537 Err(e) => {
5538 // A NULLISH spread names the value and what could not be
5539 // read off it; anything else names the missing protocol.
5540 let shown = with_host(|h| h.is_nullish(&val).then(|| h.str_of(&val)));
5541 return abort(
5542 vm,
5543 match shown {
5544 Some(s) => host::type_error(&format!(
5545 "{s} is not iterable (cannot read property {s})"
5546 )),
5547 None if e.ends_with(" is not iterable") => host::type_error(
5548 "Spread syntax requires ...iterable[Symbol.iterator] to be a function",
5549 ),
5550 None => e,
5551 },
5552 );
5553 }
5554 },
5555 Value::Int(2) => {
5556 holes.insert(out.len());
5557 out.push(Value::Undef);
5558 }
5559 _ => out.push(val),
5560 }
5561 i += 2;
5562 }
5563 with_host(|h| {
5564 let arr = h.new_array(out);
5565 h.install_holes(&arr, holes);
5566 arr
5567 })
5568}
5569
5570// ── calls ──────────────────────────────────────────────────────────────────────
5571
5572fn b_call(vm: &mut VM, argc: u8) -> Value {
5573 let mut args = pop_n(vm, argc as usize);
5574 let name = sval(&args.remove(0));
5575 let r = host::call_named(&name, args);
5576 // A bare name that resolved to a non-callable reports the VALUE
5577 // (`undefined is not a function`); node names the identifier. Resolving it
5578 // again to learn what the message said costs nothing off the error path.
5579 let r = r.map_err(|e| {
5580 let shown = global_binding(&name)
5581 .map(|v| with_host(|h| h.str_of(&v)))
5582 .unwrap_or_default();
5583 host::name_call_site(vm, &shown, e)
5584 });
5585 finish(vm, r)
5586}
5587
5588/// `recv[0](…)` — a computed call whose key is an ARRAY INDEX rather than a
5589/// method name. `call_method` resolves by name and bottoms out in
5590/// `call_type_method`, which knows `sort`/`slice` and not `"0"`, so an element
5591/// that happens to be a function reported "is not a function". Read the element
5592/// and invoke it with `recv` as `this`, which is the receiver 13.3.6 gives it.
5593/// A computed call's key is a property key, so it goes through ToPropertyKey:
5594/// `arr[0](…)` looks up `"0"`. `sval` only unwraps an existing `Value::Str` and
5595/// answers "" for a number, which turned `arr[0]()` into a call to the method
5596/// named "" — so the key is stringified here instead.
5597fn call_key_of(v: &Value) -> String {
5598 if let Value::Str(s) = v {
5599 return (**s).clone();
5600 }
5601 // `ToPropertyKey`, not `ToString`. A SYMBOL key has an internal `@@name`
5602 // spelling that `str_of` does not produce — it renders
5603 // `Symbol(Symbol.iterator)` — so `obj[Symbol.iterator]()` dispatched a
5604 // method by that display text and reported it was not a function, for every
5605 // object including a plain literal with a computed symbol method. Reading
5606 // the same property without calling it worked, which is what hid this.
5607 with_host(|h| h.property_key(v))
5608}
5609
5610fn index_element_call(recv: &Value, name: &str, args: &[Value]) -> Option<Result<Value, String>> {
5611 if name.is_empty() || !name.bytes().all(|b| b.is_ascii_digit()) {
5612 return None;
5613 }
5614 let f = get_property(recv, name).ok()?;
5615 with_host(|h| host::is_callable(h, &f))
5616 .then(|| host::invoke(&f, args.to_vec(), Some(recv.clone())))
5617}
5618
5619fn b_call_method(vm: &mut VM, argc: u8) -> Value {
5620 let mut args = pop_n(vm, argc as usize);
5621 let recv = args.remove(0);
5622 let name = call_key_of(&args.remove(0));
5623 if let Some(r) = index_element_call(&recv, &name, &args) {
5624 return finish(vm, r);
5625 }
5626 let r = host::call_method(&recv, &name, args);
5627 // `z.f()` on a missing method is `z.f is not a function` in node, not
5628 // `f is not a function`: V8 names the callee as the source wrote it. The
5629 // text was recorded for this op at compile time.
5630 let r = r.map_err(|e| host::name_call_site(vm, &name, e));
5631 finish(vm, r)
5632}
5633
5634fn b_call_value(vm: &mut VM, argc: u8) -> Value {
5635 let mut args = pop_n(vm, argc as usize);
5636 let callable = args.remove(0);
5637 let r = host::invoke(&callable, args, None);
5638 // The callee here is an expression, not a name, so the message it produced
5639 // describes the VALUE (`undefined is not a function`); node names the
5640 // expression. Same site table, keyed on that rendering.
5641 let r = r.map_err(|e| {
5642 let shown = with_host(|h| h.str_of(&callable));
5643 host::name_call_site(vm, &shown, e)
5644 });
5645 finish(vm, r)
5646}
5647
5648/// `NEW_SPREAD` — `new C(...xs)`, where the argument list is a run-time array
5649/// rather than a fixed count of stack slots.
5650///
5651/// `compile_new` used to compile each argument with `compile_expr`, and a
5652/// spread there evaluates to the SPREAD OBJECT itself — so `new C(...[1, 2])`
5653/// passed the array as one argument and `new Date(...[2020, 0, 1])` built an
5654/// Invalid Date.
5655fn b_new_spread(vm: &mut VM, _: u8) -> Value {
5656 let args_arr = vm.pop();
5657 let ctor = vm.pop();
5658 let args = host::iter_all(&args_arr).unwrap_or_default();
5659 let r = host::construct(&ctor, args).map_err(|e| {
5660 let shown = with_host(|h| h.str_of(&ctor));
5661 host::name_call_site(vm, &shown, e)
5662 });
5663 finish(vm, r)
5664}
5665
5666fn b_new(vm: &mut VM, argc: u8) -> Value {
5667 let mut args = pop_n(vm, argc as usize);
5668 let ctor = args.remove(0);
5669 let r = host::construct(&ctor, args);
5670 // `new (o.a.b.c)()` on a non-constructor names the expression, as a failed
5671 // call does.
5672 let r = r.map_err(|e| {
5673 let shown = with_host(|h| h.str_of(&ctor));
5674 host::name_call_site(vm, &shown, e)
5675 });
5676 finish(vm, r)
5677}
5678
5679fn b_apply(vm: &mut VM, _: u8) -> Value {
5680 let args_arr = vm.pop();
5681 let callable = vm.pop();
5682 let args = host::iter_all(&args_arr).unwrap_or_default();
5683 let r = host::invoke(&callable, args, None);
5684 finish(vm, r)
5685}
5686
5687fn b_apply_method(vm: &mut VM, _: u8) -> Value {
5688 let args_arr = vm.pop();
5689 let name = call_key_of(&vm.pop());
5690 let recv = vm.pop();
5691 let args = host::iter_all(&args_arr).unwrap_or_default();
5692 if let Some(r) = index_element_call(&recv, &name, &args) {
5693 return finish(vm, r);
5694 }
5695 let r = host::call_method(&recv, &name, args);
5696 finish(vm, r)
5697}
5698
5699// ── numeric hook ──────────────────────────────────────────────────────────────
5700
5701/// Host callback for arithmetic fusevm cannot complete natively (a non-`Int`/
5702/// non-`Float` operand). Supplies JavaScript `+` concatenation and coercion.
5703///
5704/// Every operand is run through `ToPrimitive` FIRST (ECMA-262 13.15.3 for `+`,
5705/// 13.6.3 for the other arithmetic ops, 13.10.1 for the relational ones), which
5706/// is what invokes a user `valueOf`/`Symbol.toPrimitive`. It has to happen here
5707/// rather than inside `JsHost::arith`, because calling back into JS re-enters
5708/// the VM and `arith` runs under the host's `RefCell` borrow.
5709pub fn numeric_hook(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5710 use NumOp::*;
5711 let (a, b) = match op {
5712 // `==`/`!=` only convert when the OTHER side is a primitive that can be
5713 // compared numerically or textually; `{} == {}` stays a reference check.
5714 Eq | Ne => {
5715 let (pa, pb) = with_host(|h| (host::is_primitive(h, a), host::is_primitive(h, b)));
5716 match (pa, pb) {
5717 (false, true) if coerces_against_object(b) => {
5718 (host::to_primitive(a, "default")?, b.clone())
5719 }
5720 (true, false) if coerces_against_object(a) => {
5721 (a.clone(), host::to_primitive(b, "default")?)
5722 }
5723 _ => (a.clone(), b.clone()),
5724 }
5725 }
5726 // `+` uses the default hint (`valueOf` first, but a string result still
5727 // selects concatenation); everything else uses the number hint.
5728 Add => (
5729 host::to_primitive(a, "default")?,
5730 host::to_primitive(b, "default")?,
5731 ),
5732 _ => (
5733 host::to_primitive(a, "number")?,
5734 host::to_primitive(b, "number")?,
5735 ),
5736 };
5737 reject_symbol_operand(op, &a, &b)?;
5738 with_host(|h| h.arith(op, &a, &b))
5739}
5740
5741/// A symbol has no `ToNumber` and no `ToString`, so every operator except the
5742/// equality family rejects it (7.1.4 step 2, 7.1.17 step 2). node-js instead
5743/// concatenated `Symbol(desc)` into the result.
5744///
5745/// Which of the two messages V8 uses is decided by whether the operation is
5746/// STRING concatenation — measured on node v26.7.0, `Symbol() + ''` is
5747/// `Cannot convert a Symbol value to a string` while `Symbol() + 1`,
5748/// `Symbol() + Symbol()` and `Symbol() * 1` are all
5749/// `Cannot convert a Symbol value to a number`. `==`/`===` never convert
5750/// (`Symbol() == 1` is `false`), so they are left alone.
5751fn reject_symbol_operand(op: NumOp, a: &Value, b: &Value) -> Result<(), String> {
5752 use NumOp::*;
5753 if matches!(op, Eq | Ne) {
5754 return Ok(());
5755 }
5756 let (sym, concat) = with_host(|h| {
5757 let is_sym = |v: &Value| matches!(h.get(v), Some(JsObj::Symbol { .. }));
5758 let is_str =
5759 |v: &Value| matches!(v, Value::Str(_)) || matches!(h.get(v), Some(JsObj::Str(_)));
5760 (is_sym(a) || is_sym(b), is_str(a) || is_str(b))
5761 });
5762 if !sym {
5763 return Ok(());
5764 }
5765 Err(host::type_error(if matches!(op, Add) && concat {
5766 "Cannot convert a Symbol value to a string"
5767 } else {
5768 "Cannot convert a Symbol value to a number"
5769 }))
5770}
5771
5772/// Whether a primitive `v` makes `==` against an object convert that object
5773/// (7.2.15 steps 10-11): numbers, strings, bigints and symbols do; `null`,
5774/// `undefined` and booleans are settled without a `ToPrimitive` call
5775/// (a boolean is coerced to a number first, and then it does).
5776fn coerces_against_object(v: &Value) -> bool {
5777 match v {
5778 Value::Undef => false,
5779 Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_) => true,
5780 _ => with_host(|h| !h.is_null(v)),
5781 }
5782}
5783
5784// ══ standard library ═══════════════════════════════════════════════════════════
5785
5786/// Namespaces reachable as bare globals.
5787fn is_namespace(name: &str) -> bool {
5788 matches!(
5789 name,
5790 "console"
5791 | "Math"
5792 | "JSON"
5793 | "Object"
5794 | "Array"
5795 | "Number"
5796 | "String"
5797 | "Boolean"
5798 | "Symbol"
5799 | "Reflect"
5800 | "Promise"
5801 | "process"
5802 | "Buffer"
5803 | "URL"
5804 | "URLSearchParams"
5805 )
5806}
5807
5808const GLOBAL_FUNCS: &[&str] = &[
5809 "parseInt",
5810 "parseFloat",
5811 "isNaN",
5812 "isFinite",
5813 "encodeURIComponent",
5814 "decodeURIComponent",
5815 "encodeURI",
5816 "decodeURI",
5817 // Annex B legacy encoders. Still globals on every engine, and still called
5818 // by pre-`encodeURIComponent` library code.
5819 "escape",
5820 "unescape",
5821 "eval",
5822 "String",
5823 "Number",
5824 "Boolean",
5825 "Array",
5826 "Object",
5827 "Function",
5828 "Symbol",
5829 "Map",
5830 "Set",
5831 "WeakMap",
5832 "WeakSet",
5833 "Promise",
5834 "Error",
5835 "TypeError",
5836 "RangeError",
5837 "SyntaxError",
5838 "ReferenceError",
5839 "EvalError",
5840 "URIError",
5841 "AggregateError",
5842 "DOMException",
5843 "Iterator",
5844 "BigInt",
5845 "RegExp",
5846 "Date",
5847 "ArrayBuffer",
5848 "DataView",
5849 "Uint8Array",
5850 "Int8Array",
5851 "Uint8ClampedArray",
5852 "Int16Array",
5853 "Uint16Array",
5854 "Int32Array",
5855 "Uint32Array",
5856 "Float32Array",
5857 "Float64Array",
5858 "BigInt64Array",
5859 "BigUint64Array",
5860 "WeakRef",
5861 "FinalizationRegistry",
5862 "TextEncoder",
5863 "TextDecoder",
5864 // WHATWG Fetch globals (see `stdlib::fetch`).
5865 "fetch",
5866 "Headers",
5867 "Request",
5868 "Response",
5869 "Blob",
5870 "File",
5871 "FormData",
5872 "AbortController",
5873 "AbortSignal",
5874 "queueMicrotask",
5875 "setTimeout",
5876 "setInterval",
5877 "setImmediate",
5878 "clearTimeout",
5879 "clearInterval",
5880 "clearImmediate",
5881 "structuredClone",
5882 // Base64 helpers. They existed only as `require('buffer').btoa`, but node
5883 // exposes both as globals, so `btoa('abc')` was a ReferenceError.
5884 "btoa",
5885 "atob",
5886 "Proxy",
5887 "require",
5888 // CommonJS loader dispatch targets referenced by per-module `require`
5889 // closures (see `module.rs`); never written by user code.
5890 "__cjs_require",
5891 "__cjs_resolve",
5892 "__cjs_cache",
5893];
5894
5895const NS_METHODS: &[&str] = &[
5896 "console.log",
5897 "console.error",
5898 "console.warn",
5899 "console.info",
5900 "console.debug",
5901 "Math.abs",
5902 "Math.acos",
5903 "Math.acosh",
5904 "Math.asin",
5905 "Math.asinh",
5906 "Math.atan",
5907 "Math.atanh",
5908 "Math.atan2",
5909 "Math.ceil",
5910 "Math.cbrt",
5911 "Math.expm1",
5912 "Math.clz32",
5913 "Math.cos",
5914 "Math.cosh",
5915 "Math.exp",
5916 "Math.floor",
5917 "Math.fround",
5918 "Math.hypot",
5919 "Math.imul",
5920 "Math.log",
5921 "Math.log1p",
5922 "Math.log2",
5923 "Math.log10",
5924 "Math.max",
5925 "Math.min",
5926 "Math.pow",
5927 "Math.random",
5928 "Math.round",
5929 "Math.sign",
5930 "Math.sin",
5931 "Math.sinh",
5932 "Math.sqrt",
5933 "Math.tan",
5934 "Math.tanh",
5935 "Math.trunc",
5936 "JSON.stringify",
5937 "JSON.parse",
5938 "JSON.rawJSON",
5939 "JSON.isRawJSON",
5940 "Object.keys",
5941 "Object.values",
5942 "Object.entries",
5943 "Object.assign",
5944 "Object.freeze",
5945 "Object.is",
5946 "Object.fromEntries",
5947 "Object.getPrototypeOf",
5948 "Object.setPrototypeOf",
5949 "Object.create",
5950 "Object.getOwnPropertyNames",
5951 "Object.getOwnPropertySymbols",
5952 "Object.defineProperty",
5953 "Object.getOwnPropertyDescriptor",
5954 "Object.getOwnPropertyDescriptors",
5955 "Object.defineProperties",
5956 "Object.isFrozen",
5957 "Object.isSealed",
5958 "Object.seal",
5959 "Object.preventExtensions",
5960 "Object.isExtensible",
5961 "Object.hasOwn",
5962 "Object.groupBy",
5963 "Array.isArray",
5964 "Array.from",
5965 "Array.fromAsync",
5966 "Array.of",
5967 "Number.isFinite",
5968 "Number.isInteger",
5969 "Number.isNaN",
5970 "Number.isSafeInteger",
5971 "Number.parseFloat",
5972 "Number.parseInt",
5973 "String.fromCharCode",
5974 "String.fromCodePoint",
5975 "String.raw",
5976 "Symbol.for",
5977 "Symbol.keyFor",
5978 "BigInt.asIntN",
5979 "BigInt.asUintN",
5980 "Proxy.revocable",
5981 "Reflect.defineProperty",
5982 "Reflect.deleteProperty",
5983 "Reflect.apply",
5984 "Reflect.construct",
5985 "Reflect.get",
5986 "Reflect.getOwnPropertyDescriptor",
5987 "Reflect.getPrototypeOf",
5988 "Reflect.has",
5989 "Reflect.isExtensible",
5990 "Reflect.ownKeys",
5991 "Reflect.preventExtensions",
5992 "Reflect.set",
5993 "Reflect.setPrototypeOf",
5994 "Promise.resolve",
5995 "Promise.reject",
5996 "Promise.all",
5997 "Promise.allSettled",
5998 "Promise.race",
5999 "Promise.any",
6000 "Promise.withResolvers",
6001 "Promise.try",
6002 "RegExp.escape",
6003 "Error.isError",
6004 "Map.groupBy",
6005 "Response.json",
6006 "Response.error",
6007 "Response.redirect",
6008 "AbortSignal.abort",
6009 "AbortSignal.timeout",
6010 "process.nextTick",
6011 "Error.captureStackTrace",
6012 "require.resolve",
6013 "require.resolve.paths",
6014 "process.memoryUsage.rss",
6015];
6016
6017/// The `name` and `length` a builtin function reports, from the generated
6018/// intrinsic table ([`crate::arity::BUILTIN_ARITY`]). `None` for a key the table
6019/// does not cover — every non-function namespace (`Math`, `require('fs')`),
6020/// and the core-module functions, whose arity is not specified anywhere.
6021pub fn builtin_meta(key: &str) -> Option<(&'static str, u32)> {
6022 crate::arity::BUILTIN_ARITY
6023 .binary_search_by(|(k, _, _)| (*k).cmp(key))
6024 .ok()
6025 .map(|i| {
6026 let (_, name, len) = crate::arity::BUILTIN_ARITY[i];
6027 (name, len)
6028 })
6029}
6030
6031/// The `name` a builtin function reports. The table answers for an intrinsic;
6032/// anything else falls back to the last segment of the key, which is what the
6033/// name is for every builtin this frontend synthesizes: `@proto:TypedArray:set`
6034/// is `set` and `fs.readFileSync` is `readFileSync`. Reporting the whole key was
6035/// how `[Function: @proto:TypedArray:set]` reached `console.log`.
6036pub fn builtin_name(key: &str) -> &str {
6037 if let Some((name, _)) = builtin_meta(key) {
6038 return name;
6039 }
6040 match key.strip_prefix("@proto:") {
6041 Some(rest) => rest.rsplit(':').next().unwrap_or(rest),
6042 // An accessor's getter is named `get <member>` (10.2.9 SetFunctionName
6043 // with a `get` prefix), which is what `util.inspect` prints for it and
6044 // what a library reads to identify one.
6045 None => key.rsplit('.').next().unwrap_or(key),
6046 }
6047}
6048
6049/// The `name` of an intrinsic accessor's getter thunk, or `None` for anything
6050/// else. Kept out of `builtin_name`'s `&str` return, which cannot own the
6051/// `"get size"` it has to build.
6052pub fn proto_getter_name(key: &str) -> Option<String> {
6053 let (verb, rest) = match key.strip_prefix("@protoget:") {
6054 Some(rest) => ("get", rest),
6055 None => ("set", key.strip_prefix("@protoset:")?),
6056 };
6057 let (_, member) = rest.split_once(':')?;
6058 Some(format!("{verb} {member}"))
6059}
6060
6061pub fn is_known_builtin(name: &str) -> bool {
6062 // Binary search over a sorted INDEX of the two tables rather than a scan of
6063 // both. This runs on every call whose callee is a builtin — `call_method`
6064 // asks it before dispatching `Math.max(…)` or `JSON.parse(…)` — and the
6065 // answer came only after a full scan of `GLOBAL_FUNCS` (77) plus a scan of
6066 // `NS_METHODS` up to the entry — 106 string comparisons for `Math.max`, 120
6067 // for `Object.keys` — because those tables are ordered for ENUMERATION (V8's
6068 // own order for `Math`/`Number`/`Reflect`), not for lookup. Eight probes
6069 // now. The index is built once per process and derived FROM those tables, so
6070 // it cannot drift from them.
6071 //
6072 // That is an operation count, not a measured time, and NO wall-clock win is
6073 // claimed. Re-measured in isolation (this hunk alone applied to the previous
6074 // commit, interleaved against it, minimums over ten rounds each): the A/B
6075 // ratio came out 0.753, 1.072, 0.994 and 0.744 across four repeats, while
6076 // the A/A control — the SAME binary under both labels — came out 1.084,
6077 // 1.072, 0.787 and 1.093. The A/B spread lies inside the A/A spread, so on
6078 // this machine the change is not distinguishable from noise. It is kept for
6079 // the comparison count and because it cannot drift from the tables it is
6080 // derived from, not because anything got faster.
6081 static SORTED: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
6082 let sorted = SORTED.get_or_init(|| {
6083 let mut v: Vec<&'static str> = GLOBAL_FUNCS
6084 .iter()
6085 .chain(NS_METHODS.iter())
6086 .copied()
6087 .collect();
6088 v.sort_unstable();
6089 v
6090 });
6091 sorted.binary_search(&name).is_ok() || is_namespace(name) || crate::stdlib::is_method(name)
6092}
6093
6094// ── dynamic functions (runtime source → callable) ────────────────────────────
6095
6096/// Build a callable from a complete function-expression source text — the ONE
6097/// dynamic-function generator on this frontend.
6098///
6099/// `src` is the exact source V8 synthesizes for the construct, WITHOUT the
6100/// wrapping parentheses needed to parse it as an expression: those are added
6101/// here, and `src` itself is retained so `Function.prototype.toString` reports
6102/// what V8 reports. The two callers synthesize different text and both shapes
6103/// are observable — see `stdlib::vm::compile_function` for the measured diff.
6104///
6105/// The body runs in the MODULE scope, never the constructing function's scope
6106/// (20.2.1.1.1 step 26 instantiates a dynamic function's body against the
6107/// *global* environment). That also makes a `var` inside the body a function
6108/// local: measured on node v26.7.0, `new Function('a','var zz = 5; return zz + a')`
6109/// returns 6 and leaves `globalThis.zz` `undefined`.
6110pub fn dynamic_function(src: &str) -> Result<Value, String> {
6111 let f = crate::eval_in_global_scope(&format!("({src})"))?;
6112 with_host(|h| {
6113 let s = h.new_str(src.to_string());
6114 h.set_fn_prop(&f, "@@source", s);
6115 });
6116 Ok(f)
6117}
6118
6119/// `new Function(p1, …, pN, body)` / `Function(p1, …, pN, body)`.
6120///
6121/// Argument convention (20.2.1.1.1): the LAST argument is the body and the rest
6122/// are parameter-list fragments joined with `,` — so a fragment may itself hold
6123/// several parameters (`new Function('a,b', 'c', …)` takes three). With no
6124/// arguments at all, both the parameter list and the body are empty.
6125///
6126/// Measured on node v26.7.0:
6127///
6128/// ```text
6129/// new Function('a','b','return a+b').toString() === 'function anonymous(a,b\n) {\nreturn a+b\n}'
6130/// new Function().toString() === 'function anonymous(\n) {\n\n}'
6131/// new Function('a,b','c','return [a,b,c]').length === 3
6132/// new Function('a','b','return a+b').name === 'anonymous'
6133/// ```
6134pub fn function_ctor(args: &[Value]) -> Result<Value, String> {
6135 let parts: Vec<String> = args.iter().map(|a| with_host(|h| h.str_of(a))).collect();
6136 let (params, body) = match parts.split_last() {
6137 Some((body, params)) => (params.join(","), body.clone()),
6138 None => (String::new(), String::new()),
6139 };
6140 dynamic_function(&format!("function anonymous({params}\n) {{\n{body}\n}}"))
6141}
6142
6143/// `eval(src)`. `direct` selects the scope the source runs in: a DIRECT eval —
6144/// the literal `eval(...)` call form — evaluates in the CALLER's scope, every
6145/// other route to the same function value is an INDIRECT eval and evaluates in
6146/// the global scope (ECMA-262 19.2.1.1 `PerformEval`). The two are told apart in
6147/// `host::call_named`, which `ops::CALL` reaches and `ops::CALL_VALUE`/`APPLY`
6148/// do not.
6149///
6150/// A non-string argument is returned unchanged (19.2.1.1 step 2).
6151pub fn eval_source(arg: Option<&Value>, direct: bool) -> Result<Value, String> {
6152 let v = arg.cloned().unwrap_or(Value::Undef);
6153 let is_string =
6154 matches!(v, Value::Str(_)) || with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_))));
6155 if !is_string {
6156 return Ok(v);
6157 }
6158 let src = with_host(|h| h.str_of(&v));
6159 // A DIRECT eval inherits the caller's strictness (19.2.1.1 step 10), which
6160 // decides both the early errors the COMPILE raises and the variable
6161 // environment below. An INDIRECT one is global-scope sloppy code.
6162 let caller_strict = direct && with_host(|h| h.current_strict());
6163 let chunk = crate::load_merged(crate::compile_completion_strict(&src, caller_strict)?);
6164 if !direct {
6165 return host::run_chunk_in_global_scope(chunk);
6166 }
6167 // A STRICT direct eval gets its OWN variable environment (19.2.1.1 step 12),
6168 // so its `var`s and function declarations die with it. Only a SLOPPY one
6169 // shares the caller's, which is the form that can inject a binding — and
6170 // sharing it unconditionally meant `eval('var x=1')` inside strict code
6171 // left `x` behind.
6172 let strict = caller_strict
6173 || src.trim_start().starts_with("'use strict'")
6174 || src.trim_start().starts_with("\"use strict\"");
6175 if !strict {
6176 // 19.2.1.1 steps 12-13: a SLOPPY direct eval shares the caller's
6177 // VARIABLE environment — which is what lets `eval('var x=1')` inject a
6178 // binding — but gets a fresh LEXICAL one of its own. A `let`, `const`
6179 // or `class` declared inside therefore dies with the eval; every one of
6180 // them was landing in the caller's scope, so `eval('let a=1')` left `a`
6181 // behind and `let a=1; eval('let a=2')` overwrote it.
6182 //
6183 // `push_scope` is exactly that split: `var` and a hoisted function
6184 // declaration bind to `base_env`, which this does not touch.
6185 with_host(|h| h.push_scope());
6186 let out = host::run_chunk_on(chunk);
6187 with_host(|h| h.pop_scope());
6188 return out;
6189 }
6190 let prev = with_host(|h| h.push_var_scope());
6191 let out = host::run_chunk_on(chunk);
6192 with_host(|h| h.pop_var_scope(prev));
6193 out
6194}
6195
6196/// Call a resolved builtin function (global or `namespace.method`).
6197pub fn call_builtin_function(name: &str, args: Vec<Value>) -> Result<Value, String> {
6198 // `require(spec)`: the ENTRY script's top-level require — core module first,
6199 // else the CommonJS loader resolving from the entry file's directory.
6200 if name == "require" {
6201 let spec = with_host(|h| h.str_of(&arg0(&args)));
6202 return crate::module::require(&spec, &crate::module::entry_dir());
6203 }
6204 // `__cjs_require(spec, fromDir)`: a per-module `require` closure's dispatch
6205 // into the loader, resolving `spec` against the module's own directory.
6206 if name == "__cjs_require" {
6207 let spec = with_host(|h| h.str_of(&arg0(&args)));
6208 let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6209 return crate::module::require(&spec, std::path::Path::new(&from));
6210 }
6211 if name == "process.memoryUsage.rss" {
6212 return Ok(crate::stdlib::process::memory_usage_rss());
6213 }
6214 if name == "require.resolve.paths" {
6215 let spec = with_host(|h| h.str_of(&arg0(&args)));
6216 // A core module is not looked up on disk at all.
6217 if crate::stdlib::is_core(&spec) {
6218 return Ok(with_host(|h| h.null()));
6219 }
6220 let dirs = crate::module::resolve_paths(&spec, &crate::module::entry_dir());
6221 return Ok(with_host(|h| {
6222 let items: Vec<Value> = dirs.into_iter().map(|d| h.new_str(d)).collect();
6223 h.new_array(items)
6224 }));
6225 }
6226 // A `require.extensions` entry. This runtime's loader does not dispatch
6227 // through the map, so calling one is the loader's own behaviour for that
6228 // extension rather than a hook point.
6229 if let Some(ext) = name.strip_prefix("@@extension:") {
6230 let _ = ext;
6231 return Ok(Value::Undef);
6232 }
6233 // `require.resolve(spec)` at the ENTRY level: resolve from the entry dir.
6234 if name == "require.resolve" {
6235 let spec = with_host(|h| h.str_of(&arg0(&args)));
6236 if crate::stdlib::is_core(&spec) {
6237 return Ok(with_host(|h| h.new_str(spec)));
6238 }
6239 return match crate::module::resolve(&spec, &crate::module::entry_dir()) {
6240 Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6241 None => Err(crate::host::plain_coded_error(
6242 "Error",
6243 "MODULE_NOT_FOUND",
6244 &format!("Cannot find module '{spec}'"),
6245 )),
6246 };
6247 }
6248 // `__cjs_resolve(spec, fromDir)`: `require.resolve` — the resolved absolute
6249 // path (core modules resolve to the bare specifier, as in Node).
6250 if name == "__cjs_resolve" {
6251 let spec = with_host(|h| h.str_of(&arg0(&args)));
6252 let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
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, std::path::Path::new(&from)) {
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 // `Error.captureStackTrace(target[, ctor])`: V8's stack capture. Sets
6266 // `target.stack`; when a custom `Error.prepareStackTrace` is installed (the
6267 // stack-introspection pattern used by `depd`), it is called with a synthetic
6268 // CallSite array and its result becomes `.stack`, else `.stack` is a string.
6269 if name == "Error.captureStackTrace" {
6270 let target = arg0(&args);
6271 let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
6272 let stack = match prep {
6273 Some(f)
6274 if matches!(
6275 with_host(|h| h.get(&f).cloned()),
6276 Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
6277 ) =>
6278 {
6279 let sites = crate::module::callsite_stack(10)?;
6280 host::invoke(&f, vec![target.clone(), sites], None)?
6281 }
6282 _ => with_host(|h| h.new_str("")),
6283 };
6284 let _ = set_property(&target, "stack", stack);
6285 return Ok(Value::Undef);
6286 }
6287 // Native stdlib module methods (path/os/fs/util/assert/crypto/buffer/url).
6288 if let Some(r) = crate::stdlib::call(name, &args) {
6289 return r;
6290 }
6291 match name {
6292 // Node's DEFAULT `Error.prepareStackTrace`: the `Name: message` header
6293 // followed by one ` at <site>` line per call site. Reachable because
6294 // the read hands the hook out, and a library may call it directly to
6295 // render a stack it captured.
6296 DEFAULT_PREPARE => {
6297 let err = arg0(&args);
6298 let header = with_host(|h| {
6299 let name = host::lookup_chain(h, &err, "name")
6300 .map(|v| h.str_of(&v))
6301 .unwrap_or_else(|| "Error".to_string());
6302 let msg = host::lookup_chain(h, &err, "message")
6303 .map(|v| h.str_of(&v))
6304 .unwrap_or_default();
6305 if msg.is_empty() {
6306 name
6307 } else {
6308 format!("{name}: {msg}")
6309 }
6310 });
6311 let sites = args.get(1).cloned().unwrap_or(Value::Undef);
6312 let lines = with_host(|h| match h.get(&sites) {
6313 Some(JsObj::Array(items)) => items.clone(),
6314 _ => Vec::new(),
6315 });
6316 let mut out = header;
6317 for s in lines {
6318 let rendered = host::to_string_value(&s)
6319 .map(|v| with_host(|h| h.str_of(&v)))
6320 .unwrap_or_default();
6321 out.push_str("\n at ");
6322 out.push_str(&rendered);
6323 }
6324 Ok(with_host(|h| h.new_str(out)))
6325 }
6326 "console.log" | "console.info" | "console.debug" => {
6327 print_line(&args, false)?;
6328 Ok(Value::Undef)
6329 }
6330 "console.error" | "console.warn" => {
6331 print_line(&args, true)?;
6332 Ok(Value::Undef)
6333 }
6334 "parseInt" | "Number.parseInt" => Ok(Value::Float(parse_int(&args)?)),
6335 "parseFloat" | "Number.parseFloat" => Ok(Value::Float(parse_float(&args)?)),
6336 // `isNaN`/`isFinite` are `ToNumber(x)` too (19.2.3/4).
6337 "isNaN" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_nan())),
6338 "isFinite" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_finite())),
6339 "encodeURIComponent" => uri_encode(&arg_to_string(&args, 0)?, false),
6340 "encodeURI" => uri_encode(&arg_to_string(&args, 0)?, true),
6341 "decodeURIComponent" => uri_decode(&arg_to_string(&args, 0)?, false),
6342 "decodeURI" => uri_decode(&arg_to_string(&args, 0)?, true),
6343 "escape" => legacy_escape(&with_host(|h| h.str_of(&arg0(&args)))),
6344 "unescape" => legacy_unescape(&with_host(|h| h.str_of(&arg0(&args)))),
6345 // Reaching `eval` through this table means the eval FUNCTION VALUE was
6346 // called — `(0, eval)(src)`, `const e = eval; e(src)`, `[eval][0](src)`.
6347 // Those are INDIRECT evals and run in the global scope. A literal
6348 // `eval(src)` is intercepted earlier, in `host::call_named`.
6349 "eval" => eval_source(args.first(), false),
6350 // `new Function(...)` and `Function(...)` are the same operation
6351 // (20.2.1.1 `CreateDynamicFunction` is reached from both [[Call]] and
6352 // [[Construct]]), so both route to the one generator.
6353 "Function" => function_ctor(&args),
6354 // `Buffer(arg[, encodingOrOffset[, length]])` — the deprecated call form
6355 // (DEP0005). Node still supports it and still routes it to the same place
6356 // `new Buffer` goes, which is why `safe-buffer`'s legacy `SafeBuffer`
6357 // wrapper is just `return Buffer(arg, encodingOrOffset, length)`. Measured
6358 // on node v26.7.0: `Buffer('abc').toString() === 'abc'`,
6359 // `Buffer([1,2]).toString('hex') === '0102'`, `Buffer(3).length === 3`.
6360 // Node emits DEP0005 once, on stderr, through the same one-shot machinery
6361 // `url.parse`'s DEP0169 uses, so this does too rather than staying silent
6362 // where Node warns.
6363 "Buffer" => {
6364 crate::stdlib::process::emit_deprecation_warning(
6365 "DEP0005",
6366 "Buffer() is deprecated due to security and usability issues. \
6367 Please use the Buffer.alloc(), Buffer.allocUnsafe(), or \
6368 Buffer.from() methods instead.",
6369 );
6370 crate::stdlib::construct("Buffer", &args)
6371 .unwrap_or_else(|| Err(host::type_error("Buffer is not a function")))
6372 }
6373 "Number.isInteger" => Ok(Value::Bool(is_integer(arg0(&args)))),
6374 "Number.isSafeInteger" => Ok(Value::Bool(is_safe_integer(arg0(&args)))),
6375 "Number.isNaN" => Ok(Value::Bool(
6376 matches!(arg0(&args), Value::Float(f) if f.is_nan()),
6377 )),
6378 "Number.isFinite" => Ok(Value::Bool(
6379 matches!(arg0(&args), Value::Float(f) if f.is_finite())
6380 || matches!(arg0(&args), Value::Int(_)),
6381 )),
6382 "String" => {
6383 if args.is_empty() {
6384 Ok(with_host(|h| h.new_str("")))
6385 } else {
6386 // A symbol argument stringifies to `Symbol(desc)` (explicit String()
6387 // is allowed); everything else via ToString method dispatch.
6388 host::string_ctor_value(&args[0])
6389 }
6390 }
6391 // `Number(v)` is NOT plain ToNumber: 21.1.1.1 step 2 converts the object
6392 // first and then explicitly ACCEPTS a BigInt, returning its mathematical
6393 // value as a Number. Only `Number` does — `+v` and `Math.abs(v)` reject
6394 // one — which is why this cannot just call `to_number_value`.
6395 "Number" => Ok(Value::Float(if args.is_empty() {
6396 0.0
6397 } else {
6398 let prim = host::to_primitive(&args[0], "number")?;
6399 match with_host(|h| h.as_bigint(&prim)) {
6400 Some(b) => host::bigint_to_f64(&b),
6401 None => host::to_number_value(&prim)?,
6402 }
6403 })),
6404 "BigInt" => bigint_ctor(&arg0(&args)),
6405 "RegExp" => regexp_ctor(&args),
6406 "BigInt.asIntN" | "BigInt.asUintN" => bigint_as_n(name.ends_with("asUintN"), &args),
6407 "Boolean" => Ok(Value::Bool(with_host(|h| h.truthy(&arg0(&args))))),
6408 // Each argument is truncated to a uint16 and taken as one code UNIT, so
6409 // `String.fromCharCode(0x1D4B3)` is U+D4B3, NOT the astral U+1D4B3, and
6410 // a surrogate PAIR of arguments composes into one character.
6411 "String.fromCharCode" => Ok(with_host(|h| {
6412 let units: Vec<u16> = args
6413 .iter()
6414 .map(|a| crate::utf16::to_uint16(h.to_number(a)))
6415 .collect();
6416 let s = crate::utf16::to_string_lossy(&units);
6417 h.new_str(s)
6418 })),
6419 // `fromCodePoint` takes whole code POINTS and rejects anything that is
6420 // not one — including a lone surrogate, which `fromCharCode` accepts.
6421 "String.fromCodePoint" => {
6422 let mut s = String::new();
6423 for a in &args {
6424 let n = with_host(|h| h.to_number(a));
6425 let cp = if n.is_finite() && n.trunc() == n && (0.0..=0x10FFFF as f64).contains(&n)
6426 {
6427 char::from_u32(n as u32)
6428 } else {
6429 None
6430 };
6431 match cp {
6432 Some(c) => s.push(c),
6433 None => {
6434 return Err(format!(
6435 "RangeError: Invalid code point {}",
6436 with_host(|h| h.str_of(a))
6437 ))
6438 }
6439 }
6440 }
6441 Ok(new_s(s))
6442 }
6443 "String.raw" => string_raw(&args),
6444 // `Array(5)` === `new Array(5)` (length-5 empty), but `Array.of(5)` is `[5]`.
6445 "Array" => construct_builtin("Array", args),
6446 "Array.of" => construct_array_like(host::current_static_this(), args),
6447 // 23.1.2.2 `IsArray` follows a Proxy to its `[[ProxyTarget]]` rather than
6448 // consulting any trap, so `Array.isArray(new Proxy([], {}))` is `true`.
6449 "Array.isArray" => {
6450 let v = arg0(&args);
6451 let subject = crate::proxy::ultimate_target(&v).unwrap_or(v);
6452 Ok(Value::Bool(
6453 matches!(
6454 with_host(|h| h.get(&subject).cloned()),
6455 Some(JsObj::Array(_))
6456 ) && !is_arguments(&subject),
6457 ))
6458 }
6459 "Array.from" => array_from(args),
6460 "Array.fromAsync" => array_from_async(args),
6461 "Object" => Ok(object_call(args)),
6462 "Object.keys" => object_keys(args, 0),
6463 "Object.values" => object_keys(args, 1),
6464 "Object.entries" => object_keys(args, 2),
6465 "Object.assign" => object_assign(args),
6466 "Object.freeze" => {
6467 let v = arg0(&args);
6468 reject_sealing_a_view(&v, "freeze")?;
6469 if seal_proxy(&v, true)? {
6470 return Ok(v);
6471 }
6472 with_host(|h| h.seal_object(&v, true));
6473 Ok(v)
6474 }
6475 "Object.seal" => {
6476 let v = arg0(&args);
6477 reject_sealing_a_view(&v, "seal")?;
6478 if seal_proxy(&v, false)? {
6479 return Ok(v);
6480 }
6481 with_host(|h| h.seal_object(&v, false));
6482 Ok(v)
6483 }
6484 "Object.preventExtensions" => {
6485 let v = arg0(&args);
6486 if crate::proxy::prevent_extensions(&v)? {
6487 return Ok(v);
6488 }
6489 with_host(|h| h.prevent_extensions(&v));
6490 Ok(v)
6491 }
6492 // A PRIMITIVE has no integrity to speak of and 7.3.15/16 answer for it
6493 // without coercion: it is not extensible, and vacuously frozen and
6494 // sealed. Reporting it extensible and unfrozen was the opposite of
6495 // every one of the three.
6496 "Object.isFrozen" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6497 "Object.isSealed" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6498 "Object.isExtensible" if is_primitive_arg(&args) => Ok(Value::Bool(false)),
6499 "Object.isFrozen" => integrity_level(&arg0(&args), true),
6500 "Object.isSealed" => integrity_level(&arg0(&args), false),
6501 "Object.isExtensible" => {
6502 let v = arg0(&args);
6503 match crate::proxy::is_extensible(&v)? {
6504 Some(b) => Ok(Value::Bool(b)),
6505 None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6506 }
6507 }
6508 // Object.is — SameValue: like `===` but NaN is equal to NaN and +0 is
6509 // distinct from -0.
6510 "Object.is" => {
6511 let a = arg0(&args);
6512 let b = args.get(1).cloned().unwrap_or(Value::Undef);
6513 let num = |v: &Value| match v {
6514 Value::Int(n) => Some(*n as f64),
6515 Value::Float(f) => Some(*f),
6516 _ => None,
6517 };
6518 let r = match (num(&a), num(&b)) {
6519 (Some(x), Some(y)) => {
6520 if x.is_nan() && y.is_nan() {
6521 true
6522 } else if x == 0.0 && y == 0.0 {
6523 x.is_sign_negative() == y.is_sign_negative()
6524 } else {
6525 x == y
6526 }
6527 }
6528 _ => with_host(|h| h.strict_eq(&a, &b)),
6529 };
6530 Ok(Value::Bool(r))
6531 }
6532 "Object.fromEntries" => object_from_entries(args),
6533 // `[[GetPrototypeOf]]`: a Proxy answers from its trap (which may throw),
6534 // so the proxy form cannot share `prototype_of`'s infallible signature.
6535 // `Object.getPrototypeOf` coerces a primitive to its wrapper and
6536 // answers; `Reflect.getPrototypeOf` requires an object (28.1.8).
6537 "Object.getPrototypeOf" | "Reflect.getPrototypeOf" => {
6538 if name == "Reflect.getPrototypeOf" {
6539 reflect_require_object(&arg0(&args), "getPrototypeOf")?;
6540 }
6541 let v = arg0(&args);
6542 match crate::proxy::get_prototype_of(&v)? {
6543 Some(p) => Ok(p),
6544 None => Ok(prototype_of(&v)),
6545 }
6546 }
6547 "Object.setPrototypeOf" => {
6548 let obj = arg0(&args);
6549 let proto = args.get(1).cloned().unwrap_or(Value::Undef);
6550 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6551 reject_bad_prototype(&proto)?;
6552 crate::proxy::set_prototype_of(&obj, &proto)?;
6553 return Ok(obj);
6554 }
6555 // 20.1.2.23: `RequireObjectCoercible` on the target, then the
6556 // prototype type check, then — only for an actual object target —
6557 // the extensibility check. A PRIMITIVE target is returned untouched
6558 // (`Object.setPrototypeOf(1, {})` is `1`), which is why the
6559 // extensibility test cannot come first.
6560 if with_host(|h| matches!(obj, Value::Undef) || h.is_null(&obj)) {
6561 return Err(host::type_error(
6562 "Object.setPrototypeOf called on null or undefined",
6563 ));
6564 }
6565 reject_bad_prototype(&proto)?;
6566 if with_host(|h| is_object_like(h, &obj)) {
6567 // Setting the SAME prototype is a no-op and stays legal even on a
6568 // frozen object: node v26.7.0 accepts
6569 // `Object.setPrototypeOf(Object.freeze({}), Object.prototype)`.
6570 // `prototype_of`, not `proto_of`: an object with no EXPLICIT
6571 // link still has `Object.prototype`, and comparing against the
6572 // absent link would call that a change.
6573 if would_cycle(&obj, &proto) {
6574 return Err(host::type_error("Cyclic __proto__ value"));
6575 }
6576 if !same_prototype(&obj, &proto) && !with_host(|h| h.is_extensible(&obj)) {
6577 // The receiver is named by its brand, as every other
6578 // refusal names it — a NULL-PROTOTYPE object is
6579 // `[object Object]`, not `#<Object>`, because it has no
6580 // constructor to name.
6581 return Err(host::type_error(&format!(
6582 "{} is not extensible",
6583 no_side_effects_string(&obj)
6584 )));
6585 }
6586 with_host(|h| h.set_proto(&obj, proto));
6587 }
6588 Ok(obj)
6589 }
6590 "Object.create" => object_create(args),
6591 "Object.getOwnPropertyNames" => object_keys(args, 3),
6592 "Object.getOwnPropertySymbols" => {
6593 let v = arg0(&args);
6594 require_object_coercible(&v)?;
6595 let syms = proxy_or_own_symbol_keys(&v)?;
6596 Ok(with_host(|h| h.new_array(syms)))
6597 }
6598 // `Object.hasOwn(obj, key)` — the static form of `hasOwnProperty`.
6599 "Object.hasOwn" => {
6600 let obj = arg0(&args);
6601 let key = args.get(1).cloned().unwrap_or(Value::Undef);
6602 object_builtin_method(&obj, "hasOwnProperty", vec![key])
6603 }
6604 "Object.defineProperty" => object_define_property(args),
6605 "Object.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6606 "Object.getOwnPropertyDescriptors" => object_get_own_descriptors(args),
6607 "Object.defineProperties" => object_define_properties(args),
6608 // `Object.groupBy(items, cb)` (ES2024): group into a null-prototype object
6609 // keyed by `ToPropertyKey(cb(item, i))`, each value an array of members.
6610 "Object.groupBy" => object_group_by(args),
6611 "Symbol" => Ok(with_host(|h| {
6612 let desc = args
6613 .first()
6614 .filter(|a| !matches!(a, Value::Undef))
6615 .map(|a| h.str_of(a));
6616 h.new_symbol(desc)
6617 })),
6618 "Symbol.for" => Ok(with_host(|h| {
6619 let key = h.str_of(&arg0(&args));
6620 h.symbol_for(&key)
6621 })),
6622 // `Symbol.keyFor(sym)` (20.4.2.6) is a REGISTRY lookup, not a
6623 // description read: it answers only for symbols `Symbol.for` created.
6624 // Returning the description made every symbol look registered —
6625 // `Symbol.keyFor(Symbol("k"))` was `"k"` where node says `undefined`.
6626 "Symbol.keyFor" => Ok(with_host(|h| h.symbol_registry_key(&arg0(&args)))),
6627 "Map" | "WeakMap" | "Set" | "WeakSet" | "Promise" => construct_builtin(name, args),
6628 // `Proxy` has no `[[Call]]` slot: it is constructor-only (28.2.1).
6629 "Proxy" => Err(host::type_error("Constructor Proxy requires 'new'")),
6630 "Proxy.revocable" => crate::proxy::revocable(&args),
6631 // `Reflect.ownKeys` reports EVERY own key, non-enumerable included —
6632 // the same set as `getOwnPropertyNames` (node-js has no symbol-keyed
6633 // own properties, so there is no second half to append).
6634 // `Reflect.ownKeys` is `OwnPropertyKeys` (7.3.23): every own key,
6635 // non-enumerable included, strings first and then the SYMBOLS.
6636 "Reflect.ownKeys" => {
6637 let v = arg0(&args);
6638 reflect_require_object(&v, "ownKeys")?;
6639 let names = object_keys(args, 3)?;
6640 let syms = proxy_or_own_symbol_keys(&v)?;
6641 if syms.is_empty() {
6642 return Ok(names);
6643 }
6644 let mut all = with_host(|h| h.iter_vec(&names)).unwrap_or_default();
6645 all.extend(syms);
6646 Ok(with_host(|h| h.new_array(all)))
6647 }
6648 "Reflect.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6649 // `Reflect.defineProperty` REPORTS success as a boolean where
6650 // `Object.defineProperty` throws (28.1.3). It was propagating the
6651 // throw, so the whole point of the reflective form was lost.
6652 "Reflect.defineProperty" => {
6653 reflect_require_object(&arg0(&args), "defineProperty")?;
6654 Ok(Value::Bool(object_define_property(args).is_ok()))
6655 }
6656 "Reflect.deleteProperty" => {
6657 let obj = arg0(&args);
6658 reflect_require_object(&obj, "deleteProperty")?;
6659 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6660 Ok(Value::Bool(delete_property(&obj, &k)?))
6661 }
6662 "Reflect.setPrototypeOf" => {
6663 let obj = arg0(&args);
6664 let p = args.get(1).cloned().unwrap_or(Value::Undef);
6665 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6666 crate::proxy::set_prototype_of(&obj, &p)?;
6667 return Ok(Value::Bool(true));
6668 }
6669 // 10.1.2.1: a NON-EXTENSIBLE object refuses a prototype change —
6670 // unless the new one is what it already has, which is a no-op. It
6671 // reported success and rewrote the link.
6672 // `Reflect` reports a refusal rather than throwing, for a cycle as
6673 // for a non-extensible receiver.
6674 if would_cycle(&obj, &p) {
6675 return Ok(Value::Bool(false));
6676 }
6677 if !with_host(|h| h.is_extensible(&obj)) {
6678 return Ok(Value::Bool(same_prototype(&obj, &p)));
6679 }
6680 with_host(|h| h.set_proto(&obj, p));
6681 Ok(Value::Bool(true))
6682 }
6683 "Reflect.isExtensible" => {
6684 let v = arg0(&args);
6685 match crate::proxy::is_extensible(&v)? {
6686 Some(b) => Ok(Value::Bool(b)),
6687 None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6688 }
6689 }
6690 "Reflect.preventExtensions" => {
6691 let v = arg0(&args);
6692 if crate::proxy::prevent_extensions(&v)? {
6693 return Ok(Value::Bool(true));
6694 }
6695 with_host(|h| h.prevent_extensions(&v));
6696 Ok(Value::Bool(true))
6697 }
6698 // `Reflect.apply(target, thisArg, argsList)` / `Reflect.construct(t, a)`.
6699 "Reflect.apply" => {
6700 let f = arg0(&args);
6701 let this = args.get(1).cloned();
6702 let list = create_list_from_array_like(&args.get(2).cloned().unwrap_or(Value::Undef))?;
6703 host::invoke(&f, list, this.filter(|t| !with_host(|h| h.is_nullish(t))))
6704 }
6705 // `Reflect.construct(target, args, newTarget)` — the optional third
6706 // argument decides which constructor's `prototype` the instance gets
6707 // (28.1.2). It was ignored, so the result always inherited from
6708 // `target` and `instanceof newTarget` was false.
6709 "Reflect.construct" => {
6710 let f = arg0(&args);
6711 let list = create_list_from_array_like(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6712 let new_target = args.get(2).cloned().unwrap_or_else(|| f.clone());
6713 host::construct_nt(&f, list, new_target)
6714 }
6715 "Reflect.has" => {
6716 let obj = arg0(&args);
6717 reflect_require_object(&obj, "has")?;
6718 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6719 Ok(Value::Bool(has_property(&obj, &k)?))
6720 }
6721 // `Reflect.get(target, key, receiver)` — the optional third argument is
6722 // what a getter sees as `this` (28.1.6). Defaults to the target.
6723 "Reflect.get" => {
6724 let obj = arg0(&args);
6725 reflect_require_object(&obj, "get")?;
6726 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6727 let receiver = args.get(2).cloned().unwrap_or_else(|| obj.clone());
6728 get_property_recv(&obj, &k, &receiver)
6729 }
6730 // `Reflect.set(target, key, value, receiver)` — the optional fourth
6731 // argument is what a setter sees as `this`, and where a DATA property
6732 // lands (28.1.13). It was ignored: the setter ran against the target
6733 // and the property was written there.
6734 "Reflect.set" => {
6735 let obj = arg0(&args);
6736 reflect_require_object(&obj, "set")?;
6737 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6738 let v = args.get(2).cloned().unwrap_or(Value::Undef);
6739 let receiver = args.get(3).cloned().unwrap_or_else(|| obj.clone());
6740 Ok(Value::Bool(set_with_receiver(&obj, &k, v, &receiver)?))
6741 }
6742 "JSON.stringify" => json_stringify(args),
6743 "JSON.parse" => json_parse(args),
6744 "JSON.rawJSON" => json_raw(args),
6745 "JSON.isRawJSON" => json_is_raw(args),
6746 "structuredClone" => structured_clone(args),
6747 // The deferred drain a `Readable.from` schedules; the suffix is the
6748 // stream's heap index.
6749 _ if name.starts_with("@@transformCb:") => {
6750 let idx: u32 = name["@@transformCb:".len()..].parse().unwrap_or(0);
6751 crate::stdlib::stream::transform_callback(&Value::Obj(idx), &args)?;
6752 Ok(Value::Undef)
6753 }
6754 _ if name.starts_with("@@streamFlush:") => {
6755 let idx: u32 = name["@@streamFlush:".len()..].parse().unwrap_or(0);
6756 crate::stdlib::stream::flush_from(&Value::Obj(idx))?;
6757 Ok(Value::Undef)
6758 }
6759 // Same implementation the `buffer` module exposes; only the binding was
6760 // missing.
6761 "btoa" | "atob" => crate::stdlib::buffer::module_call(name, &args)
6762 .unwrap_or_else(|| Err(host::type_error(&format!("{name} is not a function")))),
6763 "fetch" => crate::stdlib::fetch::fetch(&args),
6764 // An `AbortSignal.timeout` deadline reached its macrotask: the thunk's
6765 // suffix is the signal's heap index.
6766 _ if name.starts_with("@@aborttimeout:") => {
6767 let idx: u32 = name["@@aborttimeout:".len()..].parse().unwrap_or(0);
6768 crate::stdlib::fetch::fire_timeout_abort(idx)
6769 }
6770 // The `callback` handed to a `new Writable({ write(chunk, enc, cb) })`
6771 // implementation. Nothing here waits on backpressure, so it only has to
6772 // BE callable — an implementation that ends with `cb()`, which the
6773 // stream contract requires, would otherwise throw.
6774 "@@streamWriteCallback" => Ok(Value::Undef),
6775 "queueMicrotask" | "process.nextTick" => {
6776 let cb = arg0(&args);
6777 require_callback(&cb)?;
6778 let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
6779 enqueue_microtask(name == "process.nextTick", cb, rest);
6780 Ok(Value::Undef)
6781 }
6782 "setTimeout" | "setInterval" | "setImmediate" => {
6783 require_callback(&arg0(&args))?;
6784 Ok(schedule_timer(name, args))
6785 }
6786 "clearTimeout" | "clearInterval" | "clearImmediate" => {
6787 clear_timer(&arg0(&args));
6788 Ok(Value::Undef)
6789 }
6790 "Promise.resolve" => promise_resolve(arg0(&args)),
6791 "Promise.reject" => promise_reject(arg0(&args)),
6792 "Promise.all" => promise_all(args, AllMode::All),
6793 "Promise.allSettled" => promise_all(args, AllMode::AllSettled),
6794 "Promise.race" => promise_race(args, false),
6795 "Promise.any" => promise_race(args, true),
6796 // `Promise.withResolvers()` (ES2024): a new pending promise plus its own
6797 // resolve/reject functions, returned as `{ promise, resolve, reject }`.
6798 "Promise.withResolvers" => promise_with_resolvers(),
6799 "Promise.try" => promise_try(args),
6800 "RegExp.escape" => regexp_escape(args),
6801 "Error.isError" => error_is_error(args),
6802 // `Map.groupBy(items, cb)` (ES2024): group into a `Map` keyed by the raw
6803 // `cb(item, i)` result (SameValueZero), each value an array of members.
6804 "Map.groupBy" => map_group_by(args),
6805 n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
6806 _ if name.starts_with("Math.") => math_fn(&name[5..], &args),
6807 // Internal continuations (Promise resolve/reject fns, `.finally` wrappers).
6808 // The executor a species-constructed promise is built with: it does
6809 // nothing, because the caller settles the result through its id.
6810 "@@pnoop" => Ok(Value::Undef),
6811 _ if name.starts_with("@@presolve:") => {
6812 let id: u32 = name[11..].parse().unwrap_or(0);
6813 host::resolve_promise_val(id, arg0(&args));
6814 Ok(Value::Undef)
6815 }
6816 _ if name.starts_with("@@preject:") => {
6817 let id: u32 = name[10..].parse().unwrap_or(0);
6818 host::reject_promise_val(id, arg0(&args));
6819 Ok(Value::Undef)
6820 }
6821 // The revoker `Proxy.revocable` hands back, keyed by the proxy's heap
6822 // index so calling it twice is the spec's no-op rather than a re-tear.
6823 _ if name.starts_with("@@prevoke:") => {
6824 let i: u32 = name[10..].parse().unwrap_or(0);
6825 Ok(crate::proxy::revoke(i))
6826 }
6827 _ if name.starts_with("@@finpass:") => {
6828 // finally(cb) on fulfill: run cb, await whatever it returned, then
6829 // pass the original value through.
6830 let i: u32 = name["@@finpass:".len()..].parse().unwrap_or(0);
6831 let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6832 Ok(finally_chain(result, arg0(&args), false))
6833 }
6834 _ if name.starts_with("@@finthrow:") => {
6835 // finally(cb) on reject: same, then re-throw the original reason.
6836 let i: u32 = name["@@finthrow:".len()..].parse().unwrap_or(0);
6837 let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6838 Ok(finally_chain(result, arg0(&args), true))
6839 }
6840 // The two thunks `finally_chain` hangs off that awaited promise. Each
6841 // carries the value it must reinstate in a one-slot cell, since a
6842 // builtin is identified only by its name and cannot close over one.
6843 _ if name.starts_with("@@finret:") => {
6844 let i: u32 = name["@@finret:".len()..].parse().unwrap_or(0);
6845 get_property(&Value::Obj(i), "0")
6846 }
6847 _ if name.starts_with("@@finrethrow:") => {
6848 let i: u32 = name["@@finrethrow:".len()..].parse().unwrap_or(0);
6849 let reason = get_property(&Value::Obj(i), "0")?;
6850 with_host(|h| h.exc = Some(reason.clone()));
6851 Err(with_host(|h| error_string(h, &reason)))
6852 }
6853 _ => Err(host::type_error(&format!("{name} is not a function"))),
6854 }
6855}
6856
6857/// `BigInt(x)`: convert a boolean/number/string/bigint to a BigInt. A
6858/// non-integer number is a `RangeError`; an unparseable string a `SyntaxError`
6859/// (matching Node's messages).
6860/// V8 names the offending value: `BigInt(undefined)` is `Cannot convert
6861/// undefined to a BigInt`, `BigInt({})` is `Cannot convert [object Object] to a
6862/// BigInt`. The old text said "value" literally, for every input.
6863fn bigint_convert_error(v: &Value) -> String {
6864 let shown = with_host(|h| h.str_of(v));
6865 host::type_error(&format!("Cannot convert {shown} to a BigInt"))
6866}
6867
6868/// `ToBigInt(v)` — 7.1.13. The conversion every BigInt-typed SINK performs: a
6869/// 64-bit typed array's element write, `DataView.prototype.setBigInt64`, and
6870/// BigInt arithmetic's operand check.
6871///
6872/// It is NOT `BigInt(v)`: a Number is a `TypeError` here (`BigInt(1)` is `1n`,
6873/// but `new BigInt64Array(1)[0] = 1` throws), which is the whole point of the
6874/// separate abstract op. Everything else follows `ToPrimitive(v, number)` then
6875/// the type table — booleans convert (`true` → `1n`), strings parse with a
6876/// `SyntaxError` on failure, and `undefined`/`null`/symbols throw.
6877///
6878/// Measured on node v26.8.1, receiver `new BigInt64Array(1)`:
6879///
6880/// ```text
6881/// a[0] = true → 1n
6882/// a[0] = '12' → 12n
6883/// a[0] = [] → 0n (ToPrimitive → "" → 0n)
6884/// a[0] = ['3'] → 3n
6885/// a[0] = 1 → TypeError: Cannot convert 1 to a BigInt
6886/// a[0] = new Number(3) → TypeError: Cannot convert 3 to a BigInt
6887/// a[0] = 'a' → SyntaxError: Cannot convert a to a BigInt
6888/// a[0] = {} → SyntaxError: Cannot convert [object Object] to a BigInt
6889/// ```
6890pub fn to_bigint(v: &Value) -> Result<num_bigint::BigInt, String> {
6891 let prim = host::to_primitive(v, "number")?;
6892 if let Some(b) = with_host(|h| match h.get(&prim) {
6893 Some(JsObj::BigInt(b)) => Some(b.clone()),
6894 _ => None,
6895 }) {
6896 return Ok(b);
6897 }
6898 match &prim {
6899 Value::Bool(b) => Ok(num_bigint::BigInt::from(*b as i64)),
6900 Value::Str(s) => host::parse_bigint_str(s)
6901 .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt")),
6902 _ if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) => {
6903 let s = with_host(|h| h.str_of(&prim));
6904 host::parse_bigint_str(&s)
6905 .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt"))
6906 }
6907 _ => Err(bigint_convert_error(&prim)),
6908 }
6909}
6910
6911fn bigint_ctor(v: &Value) -> Result<Value, String> {
6912 use num_bigint::BigInt;
6913 let big = match v {
6914 Value::Bool(b) => BigInt::from(*b as i64),
6915 Value::Int(n) => BigInt::from(*n),
6916 Value::Float(f) => {
6917 if !f.is_finite() || f.fract() != 0.0 {
6918 let disp = with_host(|h| h.str_of(v));
6919 return Err(format!(
6920 "RangeError: The number {disp} cannot be converted to a BigInt because it is not an integer"
6921 ));
6922 }
6923 // The decimal EXPANSION, not `fmt_number`: `Number.prototype
6924 // .toString` switches to exponential notation at 1e21, and
6925 // `BigInt::parse_bytes` cannot read `"1e+21"` — so `BigInt(1e21)`
6926 // threw `Cannot convert value to a BigInt` where node returns
6927 // `1000000000000000000000n`. `{:.0}` prints an integral f64's exact
6928 // value, which is also what node reports for a magnitude past the
6929 // exactly-representable range (`BigInt(1e30)` is
6930 // `1000000000000000019884624838656n` in both).
6931 match BigInt::parse_bytes(format!("{f:.0}").as_bytes(), 10) {
6932 Some(b) => b,
6933 None => return Err(bigint_convert_error(v)),
6934 }
6935 }
6936 Value::Str(s) => match host::parse_bigint_str(s) {
6937 Some(b) => b,
6938 None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
6939 },
6940 Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
6941 Some(JsObj::BigInt(b)) => b,
6942 Some(JsObj::Str(s)) => match host::parse_bigint_str(&s) {
6943 Some(b) => b,
6944 None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
6945 },
6946 _ => return Err(bigint_convert_error(v)),
6947 },
6948 _ => return Err(bigint_convert_error(v)),
6949 };
6950 Ok(with_host(|h| h.new_bigint(big)))
6951}
6952
6953/// `new RegExp(source[, flags])` / `RegExp(...)`. A first `RegExp` argument copies
6954/// its source (and flags, unless new ones are given).
6955fn regexp_ctor(args: &[Value]) -> Result<Value, String> {
6956 let (source, existing_flags) = match with_host(|h| h.get(&arg0(args)).cloned()) {
6957 Some(JsObj::RegExp(r)) => (r.source.clone(), Some(r.flags.clone())),
6958 _ => {
6959 let a0 = arg0(args);
6960 // 22.2.4.1 step 9 is `ToString(pattern)`, which a SYMBOL refuses —
6961 // `new RegExp(sym)` was compiling the text `Symbol(d)` into a
6962 // pattern instead of throwing.
6963 let src = if matches!(a0, Value::Undef) {
6964 String::new()
6965 } else {
6966 arg_to_string(args, 0)?
6967 };
6968 (src, None)
6969 }
6970 };
6971 let flags = match args.get(1) {
6972 Some(v) if !matches!(v, Value::Undef) => arg_to_string(args, 1)?,
6973 _ => existing_flags.unwrap_or_default(),
6974 };
6975 // An empty source compiles as the JS canonical `(?:)`.
6976 let src = if source.is_empty() {
6977 "(?:)".to_string()
6978 } else {
6979 source
6980 };
6981 crate::regexp::build_regexp(&src, &flags)
6982}
6983
6984/// `BigInt.asIntN(bits, x)` / `BigInt.asUintN(bits, x)`: wrap `x` to a `bits`-wide
6985/// two's-complement (signed) or unsigned integer.
6986fn bigint_as_n(unsigned: bool, args: &[Value]) -> Result<Value, String> {
6987 use num_bigint::BigInt;
6988 use num_traits::Signed;
6989 let bits = with_host(|h| h.to_number(&arg0(args))) as i64;
6990 if bits < 0 {
6991 return Err("RangeError: Invalid value: not (convertible to) a safe integer".into());
6992 }
6993 let x = match with_host(|h| h.as_bigint(&args.get(1).cloned().unwrap_or(Value::Undef))) {
6994 Some(b) => b,
6995 None => return Err(host::type_error("Cannot convert to a BigInt")),
6996 };
6997 let bits = bits as u32;
6998 if bits == 0 {
6999 return Ok(with_host(|h| h.new_bigint(BigInt::from(0))));
7000 }
7001 let modulus = BigInt::from(1) << bits; // 2^bits
7002 // Reduce into [0, 2^bits); for the signed form fold the top half negative.
7003 let mut r = &x % &modulus;
7004 if r.is_negative() {
7005 r += &modulus;
7006 }
7007 if !unsigned {
7008 let half = BigInt::from(1) << (bits - 1);
7009 if r >= half {
7010 r -= &modulus;
7011 }
7012 }
7013 Ok(with_host(|h| h.new_bigint(r)))
7014}
7015
7016/// `String.raw(callSite, ...subs)`: concatenate the raw quasis (`callSite.raw`)
7017/// interleaved with the substitutions.
7018fn string_raw(args: &[Value]) -> Result<Value, String> {
7019 let call_site = arg0(args);
7020 let raw = get_property(&call_site, "raw")?;
7021 let raws = with_host(|h| h.iter_vec(&raw)).unwrap_or_default();
7022 let mut out = String::new();
7023 for (i, r) in raws.iter().enumerate() {
7024 out.push_str(&with_host(|h| h.str_of(r)));
7025 if i + 1 < raws.len() {
7026 if let Some(sub) = args.get(i + 1) {
7027 out.push_str(&with_host(|h| h.str_of(sub)));
7028 }
7029 }
7030 }
7031 Ok(with_host(|h| h.new_str(out)))
7032}
7033
7034/// `Object(x)`: box/pass-through — for our model, non-object args just return a
7035/// fresh object; objects pass through.
7036/// Whether `v`'s own properties live in the fn-prop SIDE TABLE rather than in a
7037/// property map. A `Map`/`Set`/`Promise`/`RegExp`/generator/symbol/bigint is an
7038/// ordinary object that also has internal slots, so it can carry own properties
7039/// like anything else — but its heap variant holds only those slots, so a write
7040/// had nowhere to go and vanished: `m.x = 5` left `m.x` undefined.
7041pub fn uses_side_table(v: &Value) -> bool {
7042 matches!(
7043 with_host(|h| h.kind_of(v)),
7044 Some(
7045 ObjKind::Map
7046 | ObjKind::Set
7047 | ObjKind::Promise
7048 | ObjKind::RegExp
7049 | ObjKind::Generator
7050 | ObjKind::Symbol
7051 | ObjKind::BigInt
7052 | ObjKind::Iter
7053 )
7054 )
7055}
7056
7057fn object_call(args: Vec<Value>) -> Value {
7058 let a = arg0(&args);
7059 // `Object(v)` is `ToObject(v)` (20.1.1.1): a primitive comes back BOXED,
7060 // not replaced by an empty object. `Object(1).valueOf()` was `undefined`.
7061 if matches!(a, Value::Undef) || with_host(|h| h.is_null(&a)) {
7062 return with_host(|h| h.new_object(IndexMap::new()));
7063 }
7064 to_object(&a)
7065}
7066
7067/// The name of the wrapper a primitive boxes into, or `None` when the value is
7068/// already an object.
7069fn wrapper_ctor_of(v: &Value) -> Option<&'static str> {
7070 match v {
7071 Value::Int(_) | Value::Float(_) => Some("Number"),
7072 Value::Bool(_) => Some("Boolean"),
7073 Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
7074 Some(JsObj::Str(_)) => Some("String"),
7075 Some(JsObj::Symbol { .. }) => Some("Symbol"),
7076 Some(JsObj::BigInt(_)) => Some("BigInt"),
7077 _ => None,
7078 },
7079 _ => None,
7080 }
7081}
7082
7083/// The primitive a wrapper object boxes (`new String("a")` → `"a"`), or `None`
7084/// for every other value. The slot is a hidden `@@primitive` own property —
7085/// the same `@@` marker convention the engine already uses for internal state,
7086/// so it stays out of `Object.keys` and `JSON.stringify` on its own.
7087pub fn wrapped_primitive(v: &Value) -> Option<Value> {
7088 with_host(|h| match h.get(v) {
7089 Some(JsObj::Object(p)) => p.get("@@primitive").cloned(),
7090 _ => None,
7091 })
7092}
7093
7094/// `ToObject(v)` (7.1.18) for a primitive: the wrapper object with the matching
7095/// prototype and a `[[StringData]]`/`[[NumberData]]`/`[[BooleanData]]` slot.
7096///
7097/// A String wrapper also owns its index properties and `length`, which is what
7098/// makes `w[0]`, `w.length` and `Object.keys(w)` answer; all of them are
7099/// non-writable and non-configurable, as the exotic `String` object's are.
7100pub fn to_object(v: &Value) -> Value {
7101 let Some(ctor) = wrapper_ctor_of(v) else {
7102 return v.clone();
7103 };
7104 with_host(|h| h.ensure_wrapper_protos());
7105 let chars: Vec<String> = if ctor == "String" {
7106 with_host(|h| h.str_of(v))
7107 .chars()
7108 .map(|c| c.to_string())
7109 .collect()
7110 } else {
7111 Vec::new()
7112 };
7113 with_host(|h| {
7114 let mut m: IndexMap<String, Value> = IndexMap::new();
7115 for (i, c) in chars.iter().enumerate() {
7116 let s = h.new_str(c.clone());
7117 m.insert(i.to_string(), s);
7118 }
7119 let w = h.new_object(m);
7120 if ctor == "String" {
7121 for i in 0..chars.len() {
7122 h.set_prop_attrs(
7123 &w,
7124 &i.to_string(),
7125 host::PropAttrs {
7126 writable: false,
7127 enumerable: true,
7128 configurable: false,
7129 },
7130 );
7131 }
7132 let len = Value::Float(chars.len() as f64);
7133 if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7134 p.insert("length".into(), len);
7135 }
7136 h.set_prop_attrs(
7137 &w,
7138 "length",
7139 host::PropAttrs {
7140 writable: false,
7141 enumerable: false,
7142 configurable: false,
7143 },
7144 );
7145 }
7146 if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7147 p.insert("@@primitive".into(), v.clone());
7148 }
7149 if let Some(proto) = h.native_proto(ctor) {
7150 h.set_proto(&w, proto);
7151 }
7152 w
7153 })
7154}
7155
7156/// Construct via `new` for the builtin constructors.
7157pub fn construct_builtin(name: &str, args: Vec<Value>) -> Result<Value, String> {
7158 // Native stdlib constructors (`new URL(...)`, `new EventEmitter()`, `new Buffer(...)`).
7159 if let Some(r) = crate::stdlib::construct(name, &args) {
7160 return r;
7161 }
7162 match name {
7163 "Array" => {
7164 // `new Array(n)` -> length-n array; `new Array(a, b)` -> [a, b].
7165 // A single NUMBER argument is a length and is validated as one
7166 // (23.1.1.1 step 6), so `new Array(-1)` / `new Array(1.5)` /
7167 // `new Array(2**32)` are all `RangeError: Invalid array length` on
7168 // node v26.7.0; only a non-number single argument is an element.
7169 if args.len() == 1 {
7170 if let Value::Float(_) | Value::Int(_) = args[0] {
7171 let n = host::to_array_length(&args[0])?;
7172 // Every element of `new Array(n)` is a HOLE, not a stored
7173 // `undefined`: `Object.keys(Array(3))` is `[]`.
7174 return Ok(with_host(|h| {
7175 let a = h.new_array(vec![Value::Undef; n]);
7176 h.mark_hole_range(&a, 0..n);
7177 a
7178 }));
7179 }
7180 }
7181 Ok(with_host(|h| h.new_array(args)))
7182 }
7183 "Object" => Ok(object_call(args)),
7184 // `new String(v)` / `new Number(v)` / `new Boolean(v)` — the wrapper
7185 // form. These were not constructors at all, so every one threw.
7186 "String" => Ok(to_object(&host::to_string_value(
7187 &args
7188 .first()
7189 .cloned()
7190 .unwrap_or_else(|| with_host(|h| h.new_str(String::new()))),
7191 )?)),
7192 "Number" => Ok(to_object(&Value::Float(match args.first() {
7193 Some(a) => host::to_number_value(a)?,
7194 None => 0.0,
7195 }))),
7196 "Boolean" => Ok(to_object(&Value::Bool(with_host(|h| {
7197 h.truthy(&arg0(&args))
7198 })))),
7199 "Map" | "WeakMap" => {
7200 let weak = name == "WeakMap";
7201 let m = with_host(|h| {
7202 h.alloc(JsObj::Map {
7203 entries: indexmap::IndexMap::new(),
7204 weak,
7205 })
7206 });
7207 if let Some(init) = args
7208 .first()
7209 .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7210 {
7211 // Stepped, not drained: an entry that is not a pair has to
7212 // stop the construction at that element and CLOSE the iterator
7213 // (24.1.1.2 step 8). Materializing first meant a bad entry in an
7214 // infinite source was never reached and the constructor HUNG.
7215 host::iter_for_each(init, |p, _| {
7216 // 24.1.1.2 step 8.d: each entry must be an OBJECT. A string
7217 // is iterable, so without this check `new Map(["ab"])`
7218 // happily stored `'a' => 'b'` instead of throwing — and over
7219 // an infinite source it never stopped.
7220 if !with_host(|h| is_object_like(h, &p)) {
7221 let shown = with_host(|h| h.str_of(&p));
7222 return Err(host::type_error(&format!(
7223 "Iterator value {shown} is not an entry object"
7224 )));
7225 }
7226 // The entry is read by INDEX with `[[Get]]` (step 8.e), not
7227 // iterated: an object with a `Symbol.iterator` but no `0`/`1`
7228 // gives `undefined => undefined`, and an array-LIKE entry
7229 // works. Iterating it instead accepted a string as a pair
7230 // and rejected the array-like.
7231 let k = get_property(&p, "0")?;
7232 let v = get_property(&p, "1")?;
7233 map_method(&m, "set", vec![k, v])?;
7234 Ok(())
7235 })?;
7236 }
7237 Ok(m)
7238 }
7239 "Set" | "WeakSet" => {
7240 let weak = name == "WeakSet";
7241 let s = with_host(|h| {
7242 h.alloc(JsObj::Set {
7243 entries: indexmap::IndexMap::new(),
7244 weak,
7245 })
7246 });
7247 if let Some(init) = args
7248 .first()
7249 .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7250 {
7251 host::iter_for_each(init, |v, _| {
7252 set_method(&s, "add", vec![v])?;
7253 Ok(())
7254 })?;
7255 }
7256 Ok(s)
7257 }
7258 "Promise" => new_promise(arg0(&args)),
7259 "Proxy" => crate::proxy::create(&args),
7260 // `new Function(p…, body)` — the same `CreateDynamicFunction` the plain
7261 // call form runs (20.2.1.1). `depd`'s `wrapfunction` builds its
7262 // deprecation wrapper this way, so `require('body-parser')` — and with it
7263 // `require('express')` — dies at load without it.
7264 "Function" => function_ctor(&args),
7265 "RegExp" => regexp_ctor(&args),
7266 "BigInt" => Err(host::type_error("BigInt is not a constructor")),
7267 "Error" => make_error_checked(name, &args),
7268 // `new DOMException(message, name)` — the name is an ARGUMENT, and the
7269 // legacy numeric `code` follows from it.
7270 "DOMException" => Ok(dom_exception(&args)),
7271 n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
7272 _ => Err(host::type_error(&format!("{name} is not a constructor"))),
7273 }
7274}
7275
7276/// The legacy numeric `DOMException.code` a WHATWG error name maps to. A name
7277/// outside the table — including the default `"Error"` — reports 0.
7278pub const DOM_EXCEPTION_CODES: &[(&str, f64)] = &[
7279 ("IndexSizeError", 1.0),
7280 ("DOMStringSizeError", 2.0),
7281 ("HierarchyRequestError", 3.0),
7282 ("WrongDocumentError", 4.0),
7283 ("InvalidCharacterError", 5.0),
7284 ("NoDataAllowedError", 6.0),
7285 ("NoModificationAllowedError", 7.0),
7286 ("NotFoundError", 8.0),
7287 ("NotSupportedError", 9.0),
7288 ("InUseAttributeError", 10.0),
7289 ("InvalidStateError", 11.0),
7290 ("SyntaxError", 12.0),
7291 ("InvalidModificationError", 13.0),
7292 ("NamespaceError", 14.0),
7293 ("InvalidAccessError", 15.0),
7294 ("ValidationError", 16.0),
7295 ("TypeMismatchError", 17.0),
7296 ("SecurityError", 18.0),
7297 ("NetworkError", 19.0),
7298 ("AbortError", 20.0),
7299 ("URLMismatchError", 21.0),
7300 ("QuotaExceededError", 22.0),
7301 ("TimeoutError", 23.0),
7302 ("InvalidNodeTypeError", 24.0),
7303 ("DataCloneError", 25.0),
7304];
7305
7306/// The static name a `DOMException` code is exposed under: the error name minus
7307/// its `Error` suffix, upper-snake-cased, plus `_ERR` — `AbortError` becomes
7308/// `ABORT_ERR`, `IndexSizeError` becomes `INDEX_SIZE_ERR`.
7309fn legacy_code_name(error_name: &str) -> String {
7310 let stem = error_name.strip_suffix("Error").unwrap_or(error_name);
7311 let mut out = String::new();
7312 for (i, c) in stem.chars().enumerate() {
7313 if c.is_ascii_uppercase() && i > 0 {
7314 out.push('_');
7315 }
7316 out.push(c.to_ascii_uppercase());
7317 }
7318 out.push_str("_ERR");
7319 out
7320}
7321
7322/// `new DOMException(message, name)`.
7323///
7324/// The class node's `AbortSignal.reason` rejects with. Its `name` is the second
7325/// ARGUMENT (defaulting to `"Error"`), not the class name, and its `code` is the
7326/// legacy number that name maps to.
7327pub fn dom_exception(args: &[Value]) -> Value {
7328 let message = match args.first() {
7329 None | Some(Value::Undef) => String::new(),
7330 Some(v) => with_host(|h| h.str_of(v)),
7331 };
7332 let name = match args.get(1) {
7333 None | Some(Value::Undef) => "Error".to_string(),
7334 Some(v) => with_host(|h| h.str_of(v)),
7335 };
7336 with_host(|h| dom_exception_with(h, &name, &message))
7337}
7338
7339/// `dom_exception` for a caller that already holds the host borrow.
7340pub(crate) fn dom_exception_with(h: &mut host::JsHost, name: &str, message: &str) -> Value {
7341 let name = name.to_string();
7342 let message = message.to_string();
7343 let code = DOM_EXCEPTION_CODES
7344 .iter()
7345 .find(|(n, _)| *n == name)
7346 .map(|(_, c)| *c)
7347 .unwrap_or(0.0);
7348 let head = if message.is_empty() {
7349 name.clone()
7350 } else {
7351 format!("{name}: {message}")
7352 };
7353 let e = synth_error(h, &head);
7354 {
7355 let nv = h.new_str(name);
7356 let mv = h.new_str(message);
7357 let sv = h.new_str(head);
7358 if let Some(JsObj::Object(p)) = h.get_mut(&e) {
7359 // `name`, `message` and `code` are PROTOTYPE accessors over internal
7360 // slots in node, so `stack` is the instance's only own property.
7361 // Storing them as own keys would show up in
7362 // `Object.getOwnPropertyNames`, which reports just `['stack']`.
7363 p.shift_remove("message");
7364 p.insert("@@domName".into(), nv);
7365 p.insert("@@domMessage".into(), mv);
7366 p.insert("@@domCode".into(), Value::Float(code));
7367 p.insert("stack".into(), sv);
7368 }
7369 h.ensure_error_protos();
7370 if let Some(proto) = host::error_proto_of(h, "DOMException") {
7371 h.set_proto(&e, proto);
7372 }
7373 }
7374 e
7375}
7376
7377/// A `DOMException`'s `name`/`message`/`code`, which live in internal slots
7378/// rather than as own properties. `None` for anything else.
7379pub fn dom_exception_slot(recv: &Value, name: &str) -> Option<Value> {
7380 let slot = match name {
7381 "name" => "@@domName",
7382 "message" => "@@domMessage",
7383 "code" => "@@domCode",
7384 _ => return None,
7385 };
7386 with_host(|h| match h.get(recv) {
7387 Some(JsObj::Object(p)) if p.contains_key("@@domName") => p.get(slot).cloned(),
7388 _ => None,
7389 })
7390}
7391
7392/// Build an `Error` object carrying `msg`, for stdlib callers that need to
7393/// throw a value with extra own properties on it.
7394pub(crate) fn make_error_pub(name: &str, msg: &str) -> Value {
7395 let m = with_host(|h| h.new_str(msg.to_string()));
7396 make_error_inner(name, &[m])
7397}
7398
7399/// [`make_error`] with the message's `ToString` allowed to FAIL. A symbol
7400/// refuses it (20.5.1.1 step 3), so `new Error(sym)` is a TypeError where this
7401/// rendered `Symbol(desc)` into `.message`.
7402fn make_error_checked(name: &str, args: &[Value]) -> Result<Value, String> {
7403 if let Some(m) = args.first().filter(|m| !matches!(m, Value::Undef)) {
7404 // AggregateError's message is its SECOND argument.
7405 let idx = usize::from(name == "AggregateError");
7406 if idx == 0 {
7407 host::to_string_value(m)?;
7408 } else if let Some(m2) = args.get(idx).filter(|m| !matches!(m, Value::Undef)) {
7409 host::to_string_value(m2)?;
7410 }
7411 }
7412 Ok(make_error_inner(name, args))
7413}
7414
7415fn make_error_inner(name: &str, args: &[Value]) -> Value {
7416 // `new AggregateError(errors, message)` takes the causes FIRST; every other
7417 // error constructor takes the message first.
7418 let agg = name == "AggregateError";
7419 let (errors, args) = if agg {
7420 (
7421 Some(args.first().cloned().unwrap_or(Value::Undef)),
7422 args.get(1..).unwrap_or(&[]),
7423 )
7424 } else {
7425 (None, args)
7426 };
7427 with_host(|h| {
7428 h.ensure_error_protos();
7429 let mut props: IndexMap<String, Value> = IndexMap::new();
7430 let msg = args
7431 .first()
7432 .filter(|a| !matches!(a, Value::Undef))
7433 .map(|a| h.str_of(a));
7434 if let Some(m) = &msg {
7435 let mv = h.new_str(m.clone());
7436 props.insert("message".into(), mv);
7437 }
7438 // `.stack` is engine-specific; a simple `Name: message` header line
7439 // suffices for parity (the fuzzer never prints raw stacks).
7440 //
7441 // V8 formats that header LAZILY, on the first read, from whatever `name`
7442 // and `message` the error carries at that moment — which is why the
7443 // near-universal
7444 //
7445 // class MyErr extends Error { constructor(m) { super(m); this.name = 'MyErr'; } }
7446 //
7447 // reports `MyErr: boom` and not the `Error: boom` this built eagerly,
7448 // inside `super()`, before the subclass had renamed anything. `@@stackRaw`
7449 // carries the frames so the read can redo it; see `materialize_stack`.
7450 let frames = h.stack_frames();
7451 let stack = match &msg {
7452 Some(m) if !m.is_empty() => format!("{name}: {m}{frames}"),
7453 _ => format!("{name}{frames}"),
7454 };
7455 let sv = h.new_str(stack);
7456 props.insert("stack".into(), sv);
7457 let raw = h.new_str(frames);
7458 props.insert("@@stackRaw".into(), raw);
7459 if let Some(errs) = errors {
7460 // Materialize the iterable into the own `errors` array property.
7461 let items = h.iter_vec(&errs).unwrap_or_default();
7462 let arr = h.new_array(items);
7463 props.insert("errors".into(), arr);
7464 }
7465 // `new Error(msg, { cause })` (ES2022): installed only when the options
7466 // bag actually has a `cause` key, so `new Error(m, {})` leaves none.
7467 let opts = args.get(1);
7468 if let Some(cause) = opts.and_then(|o| match h.get(o) {
7469 Some(JsObj::Object(p)) => p.get("cause").cloned(),
7470 _ => None,
7471 }) {
7472 props.insert("cause".into(), cause);
7473 }
7474 let e = h.new_object(props);
7475 if let Some(p) = host::error_proto_of(h, name) {
7476 h.set_proto(&e, p);
7477 }
7478 // Every own slot an error constructor installs is non-enumerable in V8,
7479 // which is why `Object.keys(err)` is `[]` and `JSON.stringify(err)` is
7480 // `{}` — properties a *script* later assigns stay enumerable.
7481 for k in ["message", "stack", "errors", "cause", "@@stackRaw"] {
7482 h.hide_prop(&e, k);
7483 }
7484 e
7485 })
7486}
7487
7488fn print_line(args: &[Value], stderr: bool) -> Result<(), String> {
7489 // Node's console.log(...args) === util.format(...args): printf-style
7490 // substitution when the first arg is a format string, else inspect-and-join.
7491 // A directive can THROW (`console.log('%j', 1n)`), and node lets that reach
7492 // the caller instead of printing a line — so nothing is written on failure.
7493 let line: String = crate::stdlib::util::format(args)?;
7494 with_host(|h| h.write_out(&format!("{line}\n"), stderr));
7495 Ok(())
7496}
7497
7498fn arg0(args: &[Value]) -> Value {
7499 args.first().cloned().unwrap_or(Value::Undef)
7500}
7501/// `ToString(arg)` for a builtin's argument — fallible, because a SYMBOL
7502/// refuses the conversion (7.1.17). Every site that reached for `str_of`
7503/// instead rendered `Symbol(desc)` into its result and reported nothing.
7504fn arg_to_string(args: &[Value], i: usize) -> Result<String, String> {
7505 let v = args.get(i).cloned().unwrap_or(Value::Undef);
7506 let sv = host::to_string_value(&v)?;
7507 Ok(with_host(|h| h.str_of(&sv)))
7508}
7509
7510fn arg_num(args: &[Value], i: usize) -> f64 {
7511 with_host(|h| h.to_number(&args.get(i).cloned().unwrap_or(Value::Undef)))
7512}
7513
7514fn is_integer(v: Value) -> bool {
7515 match v {
7516 Value::Int(_) => true,
7517 Value::Float(f) => f.is_finite() && f.fract() == 0.0,
7518 _ => false,
7519 }
7520}
7521fn is_safe_integer(v: Value) -> bool {
7522 match v {
7523 Value::Float(f) => f.is_finite() && f.fract() == 0.0 && f.abs() <= 9007199254740991.0,
7524 Value::Int(_) => true,
7525 _ => false,
7526 }
7527}
7528
7529/// `encodeURI`/`encodeURIComponent`: percent-encode `s`'s UTF-8 bytes, leaving
7530/// the unreserved set unescaped. `encodeURI` additionally preserves the reserved
7531/// URI characters (`;,/?:@&=+$#`) that delimit a URI's structure.
7532fn uri_encode(s: &str, uri: bool) -> Result<Value, String> {
7533 // Always-unescaped (`encodeURIComponent`'s unreserved set), per the spec.
7534 const UNRESERVED: &[u8] =
7535 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
7536 // Reserved characters `encodeURI` leaves intact on top of the unreserved set.
7537 const RESERVED: &[u8] = b";,/?:@&=+$#";
7538 let mut out = String::with_capacity(s.len());
7539 for &b in s.as_bytes() {
7540 if UNRESERVED.contains(&b) || (uri && RESERVED.contains(&b)) {
7541 out.push(b as char);
7542 } else {
7543 out.push('%');
7544 out.push(
7545 char::from_digit((b >> 4) as u32, 16)
7546 .unwrap()
7547 .to_ascii_uppercase(),
7548 );
7549 out.push(
7550 char::from_digit((b & 0xf) as u32, 16)
7551 .unwrap()
7552 .to_ascii_uppercase(),
7553 );
7554 }
7555 }
7556 Ok(with_host(|h| h.new_str(out)))
7557}
7558
7559/// `decodeURI`/`decodeURIComponent`: reverse `%XX` escapes back to UTF-8 text.
7560/// For `decodeURI`, escapes of the reserved delimiters are left as-is (the spec's
7561/// asymmetry with `encodeURI`). Throws `URIError` on a malformed escape.
7562fn uri_decode(s: &str, uri: bool) -> Result<Value, String> {
7563 const RESERVED: &[u8] = b";,/?:@&=+$#";
7564 let bytes = s.as_bytes();
7565 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
7566 let mut i = 0;
7567 while i < bytes.len() {
7568 if bytes[i] == b'%' {
7569 if i + 2 >= bytes.len() {
7570 return Err("URIError: URI malformed".into());
7571 }
7572 let hi = (bytes[i + 1] as char).to_digit(16);
7573 let lo = (bytes[i + 2] as char).to_digit(16);
7574 match (hi, lo) {
7575 (Some(h), Some(l)) => {
7576 let byte = (h * 16 + l) as u8;
7577 // decodeURI keeps reserved-delimiter escapes literal.
7578 if uri && RESERVED.contains(&byte) {
7579 out.extend_from_slice(&bytes[i..i + 3]);
7580 } else {
7581 out.push(byte);
7582 }
7583 i += 3;
7584 }
7585 _ => return Err("URIError: URI malformed".into()),
7586 }
7587 } else {
7588 out.push(bytes[i]);
7589 i += 1;
7590 }
7591 }
7592 match String::from_utf8(out) {
7593 Ok(decoded) => Ok(with_host(|h| h.new_str(decoded))),
7594 Err(_) => Err("URIError: URI malformed".into()),
7595 }
7596}
7597
7598/// `escape` (Annex B.2.1.1) — the pre-`encodeURIComponent` legacy encoder, still
7599/// present in every engine and still reached by old libraries (jQuery's cookie
7600/// plugin, `querystring`-era code). It works on UTF-16 CODE UNITS, not UTF-8
7601/// bytes, which is what separates it from `encodeURIComponent`: a unit below
7602/// `0x100` becomes `%XX`, anything above becomes `%uXXXX`, so an astral
7603/// character yields the two escapes of its surrogate pair
7604/// (`escape("\u{1D4B3}")` is `"%uD835%uDCB3"` on node v26.7.0).
7605///
7606/// The unescaped set is frozen by the spec and is NOT the URI unreserved set —
7607/// it keeps `@*_+-./` and drops `!~'()`.
7608fn legacy_escape(s: &str) -> Result<Value, String> {
7609 const KEEP: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";
7610 let mut out = String::with_capacity(s.len());
7611 for u in s.encode_utf16() {
7612 if u < 0x100 {
7613 if KEEP.contains(&(u as u8)) {
7614 out.push(u as u8 as char);
7615 } else {
7616 out.push_str(&format!("%{u:02X}"));
7617 }
7618 } else {
7619 out.push_str(&format!("%u{u:04X}"));
7620 }
7621 }
7622 Ok(with_host(|h| h.new_str(out)))
7623}
7624
7625/// `unescape` (Annex B.2.1.2) — the inverse of [`legacy_escape`]. Unlike
7626/// `decodeURIComponent` it never throws: a `%` that does not begin a well-formed
7627/// `%XX` or `%uXXXX` escape is passed through literally
7628/// (`unescape("%u0041%42%zz%2")` is `"AB%zz%2"` on node v26.7.0).
7629///
7630/// Decoding is done in code-unit space and re-joined at the end so a
7631/// `%uD835%uDCB3` pair recomposes into the one astral character it came from.
7632fn legacy_unescape(s: &str) -> Result<Value, String> {
7633 let b = s.as_bytes();
7634 let hex = |i: usize, n: usize| -> Option<u16> {
7635 if i + n > b.len() {
7636 return None;
7637 }
7638 let mut v: u16 = 0;
7639 for &c in &b[i..i + n] {
7640 v = v.checked_mul(16)? + (c as char).to_digit(16)? as u16;
7641 }
7642 Some(v)
7643 };
7644 let units: Vec<u16> = s.encode_utf16().collect();
7645 let mut out: Vec<u16> = Vec::with_capacity(units.len());
7646 let mut i = 0;
7647 while i < b.len() {
7648 // Escapes are pure ASCII, so a byte index is a unit index up to here —
7649 // but the tail may not be, so non-`%` bytes are re-decoded as chars.
7650 if b[i] == b'%' {
7651 if let Some(u) = hex(i + 1, 2) {
7652 out.push(u);
7653 i += 3;
7654 continue;
7655 }
7656 if b.get(i + 1) == Some(&b'u') {
7657 if let Some(u) = hex(i + 2, 4) {
7658 out.push(u);
7659 i += 6;
7660 continue;
7661 }
7662 }
7663 }
7664 let c = s[i..].chars().next().unwrap_or('%');
7665 let mut buf = [0u16; 2];
7666 out.extend_from_slice(c.encode_utf16(&mut buf));
7667 i += c.len_utf8();
7668 }
7669 Ok(with_host(|h| {
7670 h.new_str(crate::utf16::to_string_lossy(&out))
7671 }))
7672}
7673
7674/// `parseInt` begins with `ToString(argument)` (19.2.5 step 1), and that step can
7675/// THROW — a Symbol has no string form, so `parseInt([Symbol()])` is a TypeError
7676/// rather than `NaN`. Reading the argument with `str_of` took the object's brand
7677/// instead of converting it, which both swallowed that throw and ignored any
7678/// `toString` the value defines.
7679fn parse_int(args: &[Value]) -> Result<f64, String> {
7680 // Converted BEFORE the host borrow: `to_string_value` can call back into JS.
7681 let sv = host::to_string_value(&arg0(args))?;
7682 // 19.2.5 step 2 is `ToInt32(radix)`, which runs a user `valueOf` — the
7683 // infallible read below does no `ToPrimitive`, so an object radix came out
7684 // as NaN and the parse silently fell back to auto-detection.
7685 let radix = match args.get(1) {
7686 Some(r) if !matches!(r, Value::Undef) => {
7687 vec![arg0(args), Value::Float(to_number_arg(args, 1)?)]
7688 }
7689 _ => args.to_vec(),
7690 };
7691 Ok(parse_int_str(&with_host(|h| h.str_of(&sv)), &radix))
7692}
7693
7694fn parse_int_str(s: &str, args: &[Value]) -> f64 {
7695 // 19.2.5 step 8: an EXPLICIT radix outside 2..=36 is `NaN`, it does not fall
7696 // back to auto-detection. The old `.filter()` silently discarded a bad radix,
7697 // so `parseInt("10", 37)` answered 10 where every engine says NaN.
7698 let radix_arg = args
7699 .get(1)
7700 .map(|r| with_host(|h| host::to_int32(h.to_number(r))));
7701 let radix = match radix_arg {
7702 Some(0) | None => None,
7703 Some(r) if (2..=36).contains(&r) => Some(r as u32),
7704 Some(_) => return f64::NAN,
7705 };
7706 let t = crate::utf16::js_trim_start(s);
7707 let (neg, digits) = match t.strip_prefix('-') {
7708 Some(rest) => (true, rest),
7709 None => (false, t.strip_prefix('+').unwrap_or(t)),
7710 };
7711 let (radix, digits) = match radix {
7712 Some(16) => (
7713 16u32,
7714 digits
7715 .strip_prefix("0x")
7716 .or_else(|| digits.strip_prefix("0X"))
7717 .unwrap_or(digits),
7718 ),
7719 Some(r) => (r, digits),
7720 None => {
7721 if let Some(hex) = digits
7722 .strip_prefix("0x")
7723 .or_else(|| digits.strip_prefix("0X"))
7724 {
7725 (16, hex)
7726 } else {
7727 (10, digits)
7728 }
7729 }
7730 };
7731 let valid: String = digits.chars().take_while(|c| c.is_digit(radix)).collect();
7732 if valid.is_empty() {
7733 return f64::NAN;
7734 }
7735 // Accumulate in `f64`, not `i64`. `i64::from_str_radix` OVERFLOWS past ~19
7736 // digits and the error was mapped to `NaN`, so
7737 // `parseInt("999999999999999999999999")` was NaN instead of 1e+24. The spec
7738 // asks for the mathematical value rounded to a Number, which is what
7739 // repeated multiply-accumulate in `f64` produces.
7740 let n = if radix == 10 {
7741 // Rust's decimal float parser is correctly rounded; digit-by-digit
7742 // multiply-accumulate is not, and drifted a ULP on long inputs
7743 // (`parseInt("999999999999999999999999")` came out
7744 // 1.0000000000000003e+24 rather than 1e+24).
7745 valid.parse::<f64>().unwrap_or(f64::NAN)
7746 } else {
7747 let mut n = 0.0f64;
7748 for c in valid.chars() {
7749 n = n * radix as f64 + c.to_digit(radix).unwrap_or(0) as f64;
7750 }
7751 n
7752 };
7753 if neg {
7754 -n
7755 } else {
7756 n
7757 }
7758}
7759
7760/// `parseFloat` likewise starts from `ToString(argument)`; see `parse_int`.
7761fn parse_float(args: &[Value]) -> Result<f64, String> {
7762 let sv = host::to_string_value(&arg0(args))?;
7763 Ok(parse_float_str(&with_host(|h| h.str_of(&sv))))
7764}
7765
7766fn parse_float_str(s: &str) -> f64 {
7767 let t = crate::utf16::js_trim_start(s);
7768 // `Infinity` / `+Infinity` / `-Infinity` are valid parseFloat prefixes.
7769 let inf_body = t
7770 .strip_prefix('+')
7771 .or_else(|| t.strip_prefix('-'))
7772 .unwrap_or(t);
7773 if inf_body.starts_with("Infinity") {
7774 return if t.starts_with('-') {
7775 f64::NEG_INFINITY
7776 } else {
7777 f64::INFINITY
7778 };
7779 }
7780 // The LONGEST prefix that is itself a complete `StrDecimalLiteral`, which is
7781 // not the same as the longest run of characters that could appear in one:
7782 // `"1e"` and `"1e+"` are `1` in every engine, because the exponent part is
7783 // only valid once a digit follows `e`. Tracking `end` at every character
7784 // accepted the dangling `e`, `parse::<f64>` then failed, and the whole call
7785 // came back NaN.
7786 let mut end = 0;
7787 let bytes = t.as_bytes();
7788 let mut seen_dot = false;
7789 let mut seen_e = false;
7790 let mut digits_before_dot = false;
7791 for (i, &c) in bytes.iter().enumerate() {
7792 match c {
7793 b'0'..=b'9' => {
7794 if !seen_dot && !seen_e {
7795 digits_before_dot = true;
7796 }
7797 end = i + 1;
7798 }
7799 // A sign is only meaningful leading, or straight after the exponent
7800 // marker; it never completes a literal on its own.
7801 b'+' | b'-' if i == 0 || bytes[i - 1] == b'e' || bytes[i - 1] == b'E' => {}
7802 // `1.` is a complete literal; a bare `.` is not.
7803 b'.' if !seen_dot && !seen_e => {
7804 seen_dot = true;
7805 if digits_before_dot {
7806 end = i + 1;
7807 }
7808 }
7809 b'e' | b'E' if !seen_e && end > 0 => seen_e = true,
7810 _ => break,
7811 }
7812 }
7813 if end == 0 {
7814 return f64::NAN;
7815 }
7816 t[..end].parse::<f64>().unwrap_or(f64::NAN)
7817}
7818
7819/// ECMA-262 `Number::exponentiate` (6.1.6.1.3), backing both `Math.pow` and the
7820/// `**` operator. Three clauses differ from IEEE-754 `pow`, which is what Rust's
7821/// `powf` implements: a NaN exponent is NaN even for base 1, a NaN base is NaN
7822/// for any non-zero exponent, and `|base| == 1` with an infinite exponent is NaN
7823/// rather than 1.
7824pub(crate) fn js_pow(base: f64, exp: f64) -> f64 {
7825 if exp == 0.0 {
7826 return 1.0;
7827 }
7828 if base.is_nan() || exp.is_nan() {
7829 return f64::NAN;
7830 }
7831 if base.abs() == 1.0 && exp.is_infinite() {
7832 return f64::NAN;
7833 }
7834 base.powf(exp)
7835}
7836
7837fn math_fn(fname: &str, args: &[Value]) -> Result<Value, String> {
7838 // Every `Math` function coerces its arguments with `ToNumber`, and `ToNumber`
7839 // of a BigInt is a TypeError (7.1.4 step 2) — the whole point of BigInt being
7840 // a separate numeric type. `arg_num` reads a BigInt's magnitude instead, so
7841 // `Math.max(1n)` quietly answered 1 where V8 throws. `Math.random` is the one
7842 // exception: it never reads an argument, so `Math.random(1n)` is fine.
7843 // A BigInt WRAPPER converts to a BigInt and is rejected just as the
7844 // primitive is: `Math.abs(Object(9n))` is a TypeError where it answered NaN.
7845 // The boxed value is read BEFORE the borrow — `wrapped_primitive` borrows
7846 // the host itself and cannot run inside another borrow.
7847 let is_bigint = |a: &Value| {
7848 if with_host(|h| matches!(h.get(a), Some(JsObj::BigInt(_)))) {
7849 return true;
7850 }
7851 match wrapped_primitive(a) {
7852 Some(p) => with_host(|h| matches!(h.get(&p), Some(JsObj::BigInt(_)))),
7853 None => false,
7854 }
7855 };
7856 if fname != "random" && args.iter().any(is_bigint) {
7857 return Err(host::type_error(
7858 "Cannot convert a BigInt value to a number",
7859 ));
7860 }
7861 // Every argument is `ToNumber`d (21.3.2.x), which runs a user `valueOf` and
7862 // can throw from it. `arg_num` does no `ToPrimitive` at all, so
7863 // `Math.max({valueOf: () => 1}, 0)` answered NaN.
7864 // EVERY argument, not a fixed prefix: `Math.max`/`min`/`hypot` are
7865 // variadic, and coercing only the first few silently DROPPED the rest —
7866 // `Math.max(...gen)` over five values answered for four of them.
7867 let mut coerced = Vec::with_capacity(args.len());
7868 for a in args {
7869 if matches!(a, Value::Undef) {
7870 coerced.push(a.clone());
7871 continue;
7872 }
7873 let p = host::to_primitive(a, "number")?;
7874 coerced.push(Value::Float(with_host(|h| h.to_number(&p))));
7875 }
7876 let args: &[Value] = &coerced;
7877 let x = arg_num(args, 0);
7878 let r = match fname {
7879 "floor" => x.floor(),
7880 "ceil" => x.ceil(),
7881 // ECMA-262 `Math.round` (21.3.2.28) transcribed clause by clause. The
7882 // obvious `(x + 0.5).floor()` is NOT this function: the addition rounds
7883 // before the floor sees it, so it answers 1 for the largest double below
7884 // 0.5 (`Math.round(0.49999999999999994)` is 0 in every engine) and it
7885 // perturbs integers above 2^52, where `x + 0.5` is no longer
7886 // representable (`Math.round(4503599627370497)` must be the input).
7887 // Splitting the zero-band cases out first also carries the signed zero
7888 // the spec asks for without a post-hoc patch.
7889 "round" => {
7890 if !x.is_finite() || x == 0.0 {
7891 x
7892 } else if x > 0.0 && x < 0.5 {
7893 0.0
7894 } else if (-0.5..0.0).contains(&x) {
7895 -0.0
7896 } else {
7897 // |x| >= 0.5, so `floor` and the subtraction are both exact
7898 // (every double >= 2^52 is already an integer and yields 0 here).
7899 let f = x.floor();
7900 if x - f >= 0.5 {
7901 f + 1.0
7902 } else {
7903 f
7904 }
7905 }
7906 }
7907 "trunc" => x.trunc(),
7908 "abs" => x.abs(),
7909 "sign" => {
7910 if x.is_nan() {
7911 f64::NAN
7912 } else if x > 0.0 {
7913 1.0
7914 } else if x < 0.0 {
7915 -1.0
7916 } else {
7917 x
7918 }
7919 }
7920 "sqrt" => x.sqrt(),
7921 "cbrt" => x.cbrt(),
7922 "exp" => x.exp(),
7923 "log" => x.ln(),
7924 "log2" => x.log2(),
7925 "log10" => x.log10(),
7926 "sin" => x.sin(),
7927 "cos" => x.cos(),
7928 "tan" => x.tan(),
7929 "asin" => x.asin(),
7930 "acos" => x.acos(),
7931 "atan" => x.atan(),
7932 "atan2" => x.atan2(arg_num(args, 1)),
7933 // Rust `powf` is IEEE-754 `pow`, which is NOT JS `**`/`Math.pow`: IEEE
7934 // makes `pow(x, ±0)` and `pow(±1, y)` return 1 unconditionally, so
7935 // `(-1) ** Infinity` and `1 ** NaN` come back 1 where the spec
7936 // (6.1.6.1.3 Number::exponentiate) says NaN. Only the exponent-is-zero
7937 // clause is shared.
7938 "pow" => js_pow(x, arg_num(args, 1)),
7939 // Hyperbolics and the two precision-preserving log/exp forms.
7940 "sinh" => x.sinh(),
7941 "cosh" => x.cosh(),
7942 "tanh" => x.tanh(),
7943 "asinh" => x.asinh(),
7944 "acosh" => x.acosh(),
7945 "atanh" => x.atanh(),
7946 "log1p" => x.ln_1p(),
7947 "expm1" => x.exp_m1(),
7948 // C-style 32-bit integer multiply: ToInt32 both operands, multiply with
7949 // wraparound, reinterpret as a signed 32-bit result.
7950 "imul" => (host::to_int32(x).wrapping_mul(host::to_int32(arg_num(args, 1)))) as f64,
7951 "hypot" => {
7952 // Scale by the largest magnitude before squaring — this avoids the
7953 // last-ULP error of the naive `sqrt(Σ xᵢ²)` and matches V8's result.
7954 let xs: Vec<f64> = args.iter().map(|a| with_host(|h| h.to_number(a))).collect();
7955 let mut max = 0.0f64;
7956 for x in &xs {
7957 if x.abs() > max {
7958 max = x.abs();
7959 }
7960 }
7961 if xs.iter().any(|x| x.is_infinite()) {
7962 f64::INFINITY
7963 } else if max == 0.0 || !max.is_finite() {
7964 max
7965 } else {
7966 let s: f64 = xs.iter().map(|x| (x / max) * (x / max)).sum();
7967 max * s.sqrt()
7968 }
7969 }
7970 "random" => pseudo_random(),
7971 "max" => {
7972 if args.is_empty() {
7973 f64::NEG_INFINITY
7974 } else {
7975 let mut m = f64::NEG_INFINITY;
7976 for a in args {
7977 let n = with_host(|h| h.to_number(a));
7978 if n.is_nan() {
7979 return Ok(Value::Float(f64::NAN));
7980 }
7981 // `>` cannot separate the zeroes (`0.0 > -0.0` is false), but
7982 // the spec ranks +0 above -0, so `Math.max(-0, 0)` is +0 and
7983 // must not keep the -0 the first iteration installed.
7984 if n > m || (n == m && n == 0.0 && n.is_sign_positive()) {
7985 m = n;
7986 }
7987 }
7988 m
7989 }
7990 }
7991 "min" => {
7992 if args.is_empty() {
7993 f64::INFINITY
7994 } else {
7995 let mut m = f64::INFINITY;
7996 for a in args {
7997 let n = with_host(|h| h.to_number(a));
7998 if n.is_nan() {
7999 return Ok(Value::Float(f64::NAN));
8000 }
8001 // Mirror of `max`: -0 ranks below +0 even though `<` says
8002 // they are equal, so `Math.min(0, -0)` is -0.
8003 if n < m || (n == m && n == 0.0 && n.is_sign_negative()) {
8004 m = n;
8005 }
8006 }
8007 m
8008 }
8009 }
8010 // Count leading zero bits of ToUint32(x) (Math.clz32(1) === 31).
8011 "clz32" => {
8012 let u = if x.is_finite() {
8013 x.trunc().rem_euclid(4294967296.0) as u32
8014 } else {
8015 0
8016 };
8017 u.leading_zeros() as f64
8018 }
8019 // Round to the nearest single-precision float.
8020 "fround" => (x as f32) as f64,
8021 _ => return Err(host::type_error(&format!("Math.{fname} is not a function"))),
8022 };
8023 Ok(Value::Float(r))
8024}
8025
8026/// A small deterministic PRNG for `Math.random` (output is non-reproducible vs
8027/// Node by nature; kept simple).
8028fn pseudo_random() -> f64 {
8029 use std::cell::Cell;
8030 thread_local!(static SEED: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
8031 SEED.with(|s| {
8032 let mut x = s.get();
8033 x ^= x << 13;
8034 x ^= x >> 7;
8035 x ^= x << 17;
8036 s.set(x);
8037 (x >> 11) as f64 / (1u64 << 53) as f64
8038 })
8039}
8040
8041// ── Object.* ──────────────────────────────────────────────────────────────────
8042
8043/// The characters of a string PRIMITIVE, as the `ToObject` wrapper's own index
8044/// properties (10.4.3 `StringExoticObject`).
8045///
8046/// `getOwnPropertyDescriptor` begins with `ToObject`, which boxes a string into
8047/// an exotic object whose own keys are its code-unit indices plus `length`;
8048/// this is the descriptor half of that. (The KEY half lives in
8049/// `JsHost::own_enum_data_keys`, the single source every enumeration path
8050/// reads.) Indices are UTF-16 code units, matching `.length` and `s[i]`.
8051///
8052/// A boxed `String` object is deliberately NOT routed here: it can carry
8053/// ordinary own properties too (`const s = new String('ab'); s.x = 1`), and its
8054/// existing path already reports them alongside the indices.
8055fn string_primitive_units(v: &Value) -> Option<Vec<String>> {
8056 // A JS string primitive rides as a `Value::Obj` handle to `JsObj::Str` (see
8057 // `host.rs`); a BOXED `new String(...)` is a different heap object, so this
8058 // never catches one.
8059 let s = match v {
8060 Value::Str(s) => (**s).clone(),
8061 _ => with_host(|h| match h.get(v) {
8062 Some(JsObj::Str(s)) => Some(s.clone()),
8063 _ => None,
8064 })?,
8065 };
8066 let units = crate::utf16::Units::of(&s);
8067 Some((0..units.len()).filter_map(|i| units.unit_str(i)).collect())
8068}
8069
8070fn object_keys(args: Vec<Value>, mode: u8) -> Result<Value, String> {
8071 let v = arg0(&args);
8072 require_object_coercible(&v)?;
8073 // A Proxy answers from its `ownKeys` trap. `getOwnPropertyNames` (mode 3)
8074 // reports every own STRING key the trap named; the enumerating modes
8075 // additionally filter by each key's `[[GetOwnProperty]]`, so both traps run.
8076 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8077 if mode == 3 {
8078 let keys = crate::proxy::own_keys(&v)?.unwrap_or_default();
8079 return Ok(with_host(|h| {
8080 let out: Vec<Value> = keys
8081 .into_iter()
8082 .filter(|k| !host::is_symbol_key(k))
8083 .map(|k| h.new_str(k))
8084 .collect();
8085 h.new_array(out)
8086 }));
8087 }
8088 // `Object.keys` (mode 0) must run `ownKeys` and `getOwnPropertyDescriptor`
8089 // and STOP — 7.3.23 never performs `[[Get]]` when only keys are wanted.
8090 // Going through `own_enum_entries` fired the `get` trap once per key, so
8091 // the observable trap sequence carried a trailing `get` node does not
8092 // emit, and a trap with side effects ran when it should not have.
8093 if mode == 0 {
8094 let keys = crate::proxy::own_enum_string_keys(&v)?;
8095 return Ok(with_host(|h| {
8096 let out: Vec<Value> = keys.into_iter().map(|k| h.new_str(k)).collect();
8097 h.new_array(out)
8098 }));
8099 }
8100 let entries = crate::proxy::own_enum_entries(&v)?;
8101 return Ok(with_host(|h| {
8102 let out: Vec<Value> = entries
8103 .into_iter()
8104 .map(|(k, val)| match mode {
8105 0 => h.new_str(k),
8106 1 => val,
8107 _ => {
8108 let ks = h.new_str(k);
8109 h.new_array(vec![ks, val])
8110 }
8111 })
8112 .collect();
8113 h.new_array(out)
8114 }));
8115 }
8116 // An intrinsic prototype this host built as a REAL OBJECT — `Symbol
8117 // .prototype`, `String.prototype`, the error hierarchy — answers from the
8118 // generated table too. It was answering from its own property map instead,
8119 // which carries neither the right names nor V8's order: `Symbol.prototype`
8120 // reported `toLocaleString` and omitted `description`, and
8121 // `String.prototype` omitted `length` and every Annex B HTML method.
8122 //
8123 // `ns` is the namespace SPELLING, so the arm below is shared verbatim —
8124 // the two representations of a prototype cannot answer differently.
8125 let real_proto_ns = with_host(|h| h.intrinsic_proto_ctor(&v).map(|c| format!("{c}.prototype")))
8126 .filter(|ns| intrinsic_proto_members(ns).is_some());
8127 // A builtin prototype namespace that exposes enumerable methods for copying
8128 // (`Object.getOwnPropertyNames(EventEmitter.prototype)` — express's mixin).
8129 if let Some(ns) = real_proto_ns.or_else(|| {
8130 with_host(|h| match h.get(&v) {
8131 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
8132 _ => None,
8133 })
8134 }) {
8135 // An INTRINSIC prototype (`Map.prototype`, `URL.prototype`). Members are
8136 // non-enumerable on an ECMAScript builtin and enumerable on a WebIDL
8137 // interface, which the table records per name.
8138 if let Some(members) = intrinsic_proto_members(&ns) {
8139 let ctor = ns.trim_end_matches(".prototype");
8140 let mut names: Vec<String> = members
8141 .iter()
8142 .filter(|m| mode == 3 || m.starts_with('+'))
8143 .map(|m| m.strip_prefix('+').unwrap_or(m).to_string())
8144 // `getOwnPropertyNames` reports STRING keys only; the table's
8145 // `@@` entries are symbol-keyed members and belong to
8146 // `getOwnPropertySymbols` instead.
8147 .filter(|m| !m.starts_with("@@"))
8148 .collect();
8149 // Plus whatever a script patched onto this prototype under a NEW
8150 // name — an ordinary enumerable own property, so it lists in every
8151 // mode. Without it `Object.keys(Array.prototype)` stayed `[]` after
8152 // an assignment that `Array.prototype.patch` read back happily.
8153 //
8154 // Assigning over an EXISTING member is a `[[Set]]`, which leaves
8155 // that member's attributes alone: restoring a saved `join` must not
8156 // turn it into an enumerable key.
8157 for k in with_host(|h| h.builtin_static_keys(&ns)) {
8158 if !intrinsic_proto_member(&ns, &k) && !names.contains(&k) {
8159 names.push(k);
8160 }
8161 }
8162 return Ok(with_host(|h| {
8163 let out: Vec<Value> = names
8164 .iter()
8165 .map(|name| {
8166 // An accessor member has no thunk — `Map.prototype.size`
8167 // is not a function — so a VALUE read of one answers
8168 // undefined rather than synthesizing a callable.
8169 let val = |h: &mut host::JsHost| {
8170 if let Some(v) = h.builtin_static(&ns, name) {
8171 return v;
8172 }
8173 let key = format!("@proto:{ctor}:{name}");
8174 if builtin_meta(&key).is_some() {
8175 h.alloc(JsObj::Builtin(key))
8176 } else {
8177 Value::Undef
8178 }
8179 };
8180 match mode {
8181 1 => val(h),
8182 2 => {
8183 let ks = h.new_str(name.clone());
8184 let v = val(h);
8185 h.new_array(vec![ks, v])
8186 }
8187 _ => h.new_str(name.clone()),
8188 }
8189 })
8190 .collect();
8191 h.new_array(out)
8192 }));
8193 }
8194 if let Some(names) = builtin_proto_method_names(&ns) {
8195 return Ok(with_host(|h| {
8196 let out: Vec<Value> = names
8197 .iter()
8198 .map(|name| match mode {
8199 1 => h.alloc(JsObj::Builtin(format!(
8200 "@proto:{}:{name}",
8201 ns.trim_end_matches(".prototype")
8202 ))),
8203 2 => {
8204 let ks = h.new_str(*name);
8205 let val = h.alloc(JsObj::Builtin(format!(
8206 "@proto:{}:{name}",
8207 ns.trim_end_matches(".prototype")
8208 )));
8209 h.new_array(vec![ks, val])
8210 }
8211 _ => h.new_str(*name),
8212 })
8213 .collect();
8214 h.new_array(out)
8215 }));
8216 }
8217 // A stdlib namespace (`Buffer`, `require('buffer')`): its own enumerable
8218 // keys are the members node-js implements, each resolved to the same
8219 // first-class value a property read would give.
8220 let mut names = crate::stdlib::namespace_keys(&ns);
8221 // A core namespace (`Reflect`, `Math`, `JSON`) has no stdlib key list —
8222 // its members live in the builtin dispatch table. They are
8223 // non-enumerable in V8, so they surface only under
8224 // `getOwnPropertyNames`/`Reflect.ownKeys` (mode 3), never `Object.keys`.
8225 if names.is_empty() && mode == 3 {
8226 let prefix = format!("{ns}.");
8227 // A builtin constructor's own `length`/`name`/`prototype` come
8228 // first, as they do in V8.
8229 if is_builtin_ctor(&ns) {
8230 names.extend(["length", "name", "prototype"].map(str::to_string));
8231 }
8232 names.extend(
8233 NS_METHODS
8234 .iter()
8235 .filter_map(|q| q.strip_prefix(&prefix))
8236 .map(|m| m.to_string()),
8237 );
8238 // The numeric constants are members too. Without them
8239 // `getOwnPropertyNames(Math)` reported 35 of the 43 names node-js
8240 // actually answers — the eight it dropped being `PI` and its
8241 // siblings, which read fine and now own a descriptor as well.
8242 names.extend(
8243 namespace_constants(&ns)
8244 .iter()
8245 .map(|(k, _)| (*k).to_string()),
8246 );
8247 }
8248 // A builtin FUNCTION owns exactly `length` and `name` (10.3.3-4), so
8249 // `Object.getOwnPropertyNames(Math.max)` is `[ 'length', 'name' ]` — it
8250 // was `[]`, which said the function had no properties at all while both
8251 // of them read back a value. `length` is listed only where the intrinsic
8252 // table has an arity, so the names never advertise a read that answers
8253 // `undefined`.
8254 if names.is_empty() && mode == 3 && host::builtin_is_callable(&ns) {
8255 if builtin_meta(&ns).is_some() {
8256 names.push("length".to_string());
8257 }
8258 names.push("name".to_string());
8259 }
8260 // Whatever a script assigned onto the namespace, in assignment order and
8261 // after the built-in members — an ordinary enumerable own property, so
8262 // it surfaces under `Object.keys` too and not only `ownKeys`. These were
8263 // missing from every listing, which made a patched prototype read as
8264 // unpatched to any code that enumerates rather than reads.
8265 for k in with_host(|h| h.builtin_static_keys(&ns)) {
8266 if !names.contains(&k) {
8267 names.push(k);
8268 }
8269 }
8270 if !names.is_empty() {
8271 let entries: Vec<(String, Value)> = names
8272 .into_iter()
8273 .map(|k| {
8274 let val = namespace_property(&ns, &k);
8275 (k, val)
8276 })
8277 .collect();
8278 return Ok(with_host(|h| {
8279 let out: Vec<Value> = entries
8280 .into_iter()
8281 .map(|(k, val)| match mode {
8282 1 => val,
8283 2 => {
8284 let ks = h.new_str(k);
8285 h.new_array(vec![ks, val])
8286 }
8287 _ => h.new_str(k),
8288 })
8289 .collect();
8290 h.new_array(out)
8291 }));
8292 }
8293 }
8294 // mode 3 (`getOwnPropertyNames`) reports every own string key including the
8295 // non-enumerable ones, plus the exotic `length` an array carries.
8296 let entries: Vec<(String, Value)> = with_host(|h| {
8297 if mode == 3 {
8298 // An array's exotic `length` is already placed (after the indices,
8299 // before the ordinary string keys) by `own_key_names`.
8300 return h
8301 .own_key_names(&v, false)
8302 .into_iter()
8303 .map(|k| (k, Value::Undef))
8304 .collect();
8305 }
8306 Vec::new()
8307 });
8308 // `Object.keys` (mode 0) wants NAMES. `own_enum_entries_deep` returns
8309 // key/value pairs, so asking it for them ran every enumerable getter —
8310 // 20.1.2.17 -> 7.3.23 EnumerableOwnProperties only needs `[[GetOwnProperty]]`
8311 // for the enumerable flag, never `[[Get]]`, and a getter can throw or have
8312 // side effects:
8313 //
8314 // let n = 0; const o = { get g() { n++; return 1 } };
8315 // Object.keys(o); n // was 1, node says 0
8316 //
8317 // `values`/`entries` (modes 1 and 2) do read, and still do.
8318 let entries = match mode {
8319 3 => entries,
8320 0 => with_host(|h| h.own_enum_key_names(&v))
8321 .into_iter()
8322 .map(|k| (k, Value::Undef))
8323 .collect(),
8324 _ => host::own_enum_entries_deep(&v)?,
8325 };
8326 Ok(with_host(|h| {
8327 let out: Vec<Value> = entries
8328 .into_iter()
8329 .map(|(k, val)| match mode {
8330 0 | 3 => h.new_str(k),
8331 1 => val,
8332 _ => {
8333 let ks = h.new_str(k);
8334 h.new_array(vec![ks, val])
8335 }
8336 })
8337 .collect();
8338 h.new_array(out)
8339 }))
8340}
8341
8342fn object_assign(args: Vec<Value>) -> Result<Value, String> {
8343 let target = arg0(&args);
8344 // 20.1.2.1 step 1 is `ToObject(target)`, so a nullish TARGET throws while a
8345 // nullish SOURCE is skipped (`Object.assign({}, null)` is `{}`).
8346 require_object_coercible(&target)?;
8347 for src in args.iter().skip(1) {
8348 // `Object.assign` copies own *enumerable* properties, running any getter
8349 // — symbol-keyed ones included (7.3.25).
8350 let entries = host::own_enum_entries_deep(src)?;
8351 let syms = with_host(|h| h.own_symbol_entries(src));
8352 // A plain object target is filled in place (one borrow, then a single
8353 // re-canonicalization of the integer-index keys).
8354 let filled = with_host(|h| {
8355 if let Some(JsObj::Object(p)) = h.get_mut(&target) {
8356 for (k, v) in entries.iter().cloned().chain(syms.iter().cloned()) {
8357 p.insert(k, v);
8358 }
8359 host::canonicalize_own_keys(p);
8360 return true;
8361 }
8362 false
8363 });
8364 // Any OTHER target — an array being the common one — goes through the
8365 // ordinary Set path. The in-place branch above matched `JsObj::Object`
8366 // only, so `Object.assign([1,2], {extra:9})` silently copied NOTHING and
8367 // returned the untouched array: no error, just a missing property. The
8368 // Set path is what an `arr.extra = 9` assignment already used, so index
8369 // and non-index keys land where they do for a direct write.
8370 if !filled {
8371 for (k, v) in entries.into_iter().chain(syms) {
8372 set_property(&target, &k, v)?;
8373 }
8374 }
8375 }
8376 Ok(target)
8377}
8378
8379fn object_from_entries(args: Vec<Value>) -> Result<Value, String> {
8380 let pairs = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
8381 let mut props: IndexMap<String, Value> = IndexMap::new();
8382 for p in pairs {
8383 let kv = with_host(|h| h.iter_vec(&p)).unwrap_or_default();
8384 let key = with_host(|h| h.str_of(&kv.first().cloned().unwrap_or(Value::Undef)));
8385 let val = kv.get(1).cloned().unwrap_or(Value::Undef);
8386 props.insert(key, val);
8387 }
8388 Ok(with_host(|h| h.new_object(props)))
8389}
8390
8391/// `Object.groupBy(items, cb)` — group the iterable `items` into a null-prototype
8392/// object. Keys are `ToPropertyKey(cb(item, index))`; values are arrays of the
8393/// members mapped to that key, in first-seen key order.
8394fn object_group_by(args: Vec<Value>) -> Result<Value, String> {
8395 group_by_check_iterable(&arg0(&args), "Object.groupBy")?;
8396 let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8397 let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
8398 // Stepped, not drained: the callback runs per element, so a throwing one
8399 // stops at the first. Draining first meant an infinite source never reached
8400 // the callback at all and the call HUNG.
8401 host::iter_for_each(&arg0(&args), |item, i| {
8402 let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8403 let key = with_host(|h| h.property_key(&key_v));
8404 groups.entry(key).or_default().push(item);
8405 Ok(())
8406 })?;
8407 let props: IndexMap<String, Value> = with_host(|h| {
8408 groups
8409 .into_iter()
8410 .map(|(k, v)| (k, h.new_array(v)))
8411 .collect()
8412 });
8413 let obj = with_host(|h| h.new_object(props));
8414 // A null-prototype object (as Node returns), so it has no inherited members.
8415 with_host(|h| {
8416 let nv = h.null();
8417 h.set_proto(&obj, nv);
8418 });
8419 Ok(obj)
8420}
8421
8422/// The `groupBy` family words a non-iterable argument its OWN way — a third
8423/// vocabulary, alongside the array-literal spread's and the call spread's:
8424///
8425/// ```text
8426/// null / undefined "<Name> called on null or undefined"
8427/// anything else "<typeof> [value ]is not iterable (cannot read property
8428/// Symbol(Symbol.iterator))"
8429/// ```
8430///
8431/// A plain object, a symbol and a bigint name only their TYPE; a number, a
8432/// string and a boolean name the value too.
8433fn group_by_check_iterable(v: &Value, name: &str) -> Result<(), String> {
8434 if with_host(|h| h.is_nullish(v)) {
8435 return Err(host::type_error(&format!(
8436 "{name} called on null or undefined"
8437 )));
8438 }
8439 // Asked WITHOUT consuming anything: `iter_all` would drain the iterator
8440 // here, so the stepping loop below then saw an exhausted one — the finite
8441 // case returned an empty group and the infinite case was back to hanging.
8442 let iter_fn = get_property(v, "@@iterator").unwrap_or(Value::Undef);
8443 if with_host(|h| host::is_callable(h, &iter_fn)) {
8444 return Ok(());
8445 }
8446 Err(host::type_error(¬_iterable_typed(v)))
8447}
8448
8449/// The `<type> <value> is not iterable (cannot read property
8450/// Symbol(Symbol.iterator))` wording, which node uses wherever the source has
8451/// no name to report: a plain object, a symbol and a bigint name only their
8452/// TYPE; a number, a string and a boolean name the value too.
8453pub(crate) fn not_iterable_typed(v: &Value) -> String {
8454 let shown = with_host(|h| {
8455 let kind = h.type_of(v);
8456 match kind {
8457 "object" | "symbol" | "bigint" => kind.to_string(),
8458 "string" => format!("string \"{}\"", h.str_of(v)),
8459 _ => format!("{kind} {}", h.str_of(v)),
8460 }
8461 });
8462 format!("{shown} is not iterable (cannot read property Symbol(Symbol.iterator))")
8463}
8464
8465/// `Map.groupBy(items, cb)` — like `Object.groupBy` but returns a `Map` keyed by
8466/// the raw `cb(item, index)` value under SameValueZero (so object/any keys work).
8467fn map_group_by(args: Vec<Value>) -> Result<Value, String> {
8468 group_by_check_iterable(&arg0(&args), "Map.groupBy")?;
8469 let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8470 let m = with_host(|h| {
8471 h.alloc(JsObj::Map {
8472 entries: IndexMap::new(),
8473 weak: false,
8474 })
8475 });
8476 // Stepped for the same reason `Object.groupBy` is.
8477 host::iter_for_each(&arg0(&args), |item, i| {
8478 let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8479 let existing = map_method(&m, "get", vec![key_v.clone()])?;
8480 if matches!(existing, Value::Undef) {
8481 let arr = with_host(|h| h.new_array(vec![item]));
8482 map_method(&m, "set", vec![key_v, arr])?;
8483 } else {
8484 with_host(|h| {
8485 if let Some(JsObj::Array(a)) = h.get_mut(&existing) {
8486 a.push(item);
8487 }
8488 });
8489 }
8490 Ok(())
8491 })?;
8492 Ok(m)
8493}
8494
8495/// `Array.fromAsync(items[, mapFn])` — a Promise for an array, awaiting each
8496/// element and each `mapFn` result.
8497///
8498/// Written in JavaScript and compiled once, because the operation IS an async
8499/// function: a Rust builtin runs outside any coroutine and has no way to await,
8500/// so draining a promise from there would mean running the microtask queue by
8501/// hand. Delegating to the engine's own `async`/`for await` keeps the
8502/// suspension semantics — and the ordering they imply — exactly the language's.
8503///
8504/// The source may be an async iterable, a sync iterable, a bare iterator, or an
8505/// array-like. Everything iterable goes through `for await`, which awaits a sync
8506/// source's elements individually — that is what makes
8507/// `Array.fromAsync([1, Promise.resolve(2)])` answer `[1, 2]`. A bare `.next` is
8508/// accepted because an async generator object does not expose
8509/// `Symbol.asyncIterator` on this frontend.
8510fn array_from_async(args: Vec<Value>) -> Result<Value, String> {
8511 thread_local! {
8512 static IMPL: std::cell::RefCell<Option<Value>> = const { std::cell::RefCell::new(None) };
8513 }
8514 const SRC: &str = "(async function (items, mapFn, thisArg) {\n\
8515 const out = []; let i = 0;\n\
8516 const step = async (v) => { const a = await v; out.push(mapFn ? await mapFn.call(thisArg, a, i) : a); i++; };\n\
8517 const iterable = items != null && (typeof items[Symbol.asyncIterator] === 'function'\n\
8518 || typeof items[Symbol.iterator] === 'function' || typeof items.next === 'function');\n\
8519 if (iterable) {\n\
8520 for await (const v of items) { out.push(mapFn ? await mapFn.call(thisArg, v, i) : v); i++; }\n\
8521 return out;\n\
8522 }\n\
8523 const len = items == null ? 0 : (Math.trunc(Number(items.length)) || 0);\n\
8524 while (i < len) { await step(items[i]); }\n\
8525 return out;\n\
8526 })";
8527 let f = IMPL.with(|c| c.borrow().clone());
8528 let f = match f {
8529 Some(f) => f,
8530 None => {
8531 let f = crate::eval_in_global_scope(SRC)?;
8532 IMPL.with(|c| *c.borrow_mut() = Some(f.clone()));
8533 f
8534 }
8535 };
8536 host::invoke(&f, args, None)
8537}
8538
8539fn array_from(args: Vec<Value>) -> Result<Value, String> {
8540 // `Array.from` accepts generators and user iterables, plus array-likes with a
8541 // numeric `.length`.
8542 let src = arg0(&args);
8543 if let Some(cb) = args.get(1).cloned() {
8544 // Stepped, not drained: the mapper runs per element as the iterator
8545 // yields it (23.1.2.1 step 6.e). Materializing the whole sequence first
8546 // meant `Array.from(infiniteIterator, fn)` never reached the mapper at
8547 // all and HUNG, and a throwing mapper could not close the iterator.
8548 let this = this_arg(&args, 2);
8549 let mut out = Vec::new();
8550 let mapped = host::iter_for_each(&src, |v, i| {
8551 out.push(host::invoke(
8552 &cb,
8553 vec![v, Value::Float(i as f64)],
8554 this.clone(),
8555 )?);
8556 Ok(())
8557 });
8558 match mapped {
8559 Ok(()) => {}
8560 // An array-LIKE has no iterator; fall back to its indexed items.
8561 Err(e) if host::user_iterator_fn(&src).is_none() && e.ends_with(" is not iterable") => {
8562 out.clear();
8563 for (i, it) in array_like_items(&src).into_iter().enumerate() {
8564 out.push(host::invoke(
8565 &cb,
8566 vec![it, Value::Float(i as f64)],
8567 this.clone(),
8568 )?);
8569 }
8570 }
8571 Err(e) => return Err(e),
8572 }
8573 return construct_array_like(host::current_static_this(), out);
8574 }
8575 let items = match host::iter_all(&src) {
8576 Ok(v) => v,
8577 Err(_) => array_like_items(&src),
8578 };
8579 // 23.1.2.1 step 5: `Array.from` builds through `this`, so on a subclass the
8580 // result is an instance of it. It always allocated a plain array, which is
8581 // also why `A.from([1]).map(f) instanceof A` was false — the species chain
8582 // never started.
8583 construct_array_like(host::current_static_this(), items)
8584}
8585
8586/// Items of an array-like `{ length, 0, 1, … }` object (for `Array.from`).
8587fn array_like_items(src: &Value) -> Vec<Value> {
8588 // `LengthOfArrayLike` is `ToLength(Get(O, "length"))`, and `ToNumber` runs a
8589 // user `valueOf` — `Array.from({length: {valueOf: () => 1}})` was empty
8590 // because the infallible read does no `ToPrimitive`. A throw from it is
8591 // swallowed here for the same reason the `length` read is: this helper has
8592 // no way to report one, and every caller treats an unreadable length as 0.
8593 let len = get_property(src, "length")
8594 .ok()
8595 .and_then(|l| host::to_primitive(&l, "number").ok())
8596 .map(|l| with_host(|h| h.to_number(&l)))
8597 .unwrap_or(0.0);
8598 if !len.is_finite() || len <= 0.0 {
8599 return Vec::new();
8600 }
8601 (0..len as usize)
8602 .map(|i| get_property(src, &i.to_string()).unwrap_or(Value::Undef))
8603 .collect()
8604}
8605
8606// ── JSON ──────────────────────────────────────────────────────────────────────
8607
8608fn json_stringify(args: Vec<Value>) -> Result<Value, String> {
8609 // A CALLABLE second argument is the replacer function, and it is checked
8610 // before the array form (`IsCallable` precedes `IsArray` in the spec), so a
8611 // callable never also reaches the key-filter path below.
8612 let replacer = args
8613 .get(1)
8614 .filter(|r| with_host(|h| host::is_callable(h, r)))
8615 .cloned();
8616 // `toJSON` and the replacer run BEFORE serialization and are user code, so
8617 // the tree is rewritten first — outside the host borrow `json_str` holds,
8618 // and before the BigInt walk, which has no cycle guard of its own.
8619 //
8620 // The top-level value is a property of a synthetic wrapper `{ "": value }`
8621 // under key `""`, which is exactly the holder the replacer receives as
8622 // `this` on its first call.
8623 let root = arg0(&args);
8624 let wrapper = with_host(|h| {
8625 let mut m: IndexMap<String, Value> = IndexMap::new();
8626 m.insert(String::new(), root.clone());
8627 h.new_object(m)
8628 });
8629 let v = apply_to_json(&wrapper, "", &root, &mut Vec::new(), replacer.as_ref())?;
8630 // A BigInt anywhere in a serializable position is a TypeError (JSON has no
8631 // bigint form), matching Node's exact message.
8632 if with_host(|h| json_has_bigint(h, &v)) {
8633 return Err(host::type_error("Do not know how to serialize a BigInt"));
8634 }
8635 let indent = match args.get(2) {
8636 Some(Value::Float(f)) => " ".repeat((*f as usize).min(10)),
8637 Some(other) => with_host(|h| h.as_str(other)).unwrap_or_default(),
8638 None => String::new(),
8639 };
8640 // A replacer array (args[1]) restricts which object keys are serialized.
8641 let keys: Option<Vec<String>> = args.get(1).and_then(|r| {
8642 with_host(|h| match h.get(r) {
8643 Some(JsObj::Array(items)) => {
8644 Some(items.iter().map(|k| h.str_of(k)).collect::<Vec<_>>())
8645 }
8646 _ => None,
8647 })
8648 });
8649 let s = with_host(|h| json_str(h, &v, &indent, 0, keys.as_deref()));
8650 match s {
8651 Some(s) => Ok(with_host(|h| h.new_str(s))),
8652 None => Ok(Value::Undef),
8653 }
8654}
8655
8656/// One `SerializeJSONProperty(key, holder)` step: rewrite `v` (the value read
8657/// from `holder[key]`) by calling its `toJSON(key)` and then the replacer
8658/// function as `replacer.call(holder, key, value)`, then recurse into whatever
8659/// object survives. Applies to user methods, class methods, and the native
8660/// `Date`/`Buffer`/`URL` accessors alike.
8661///
8662/// Returns a fresh tree; the input is never mutated. `path` carries the chain of
8663/// objects currently being walked so a cyclic structure is reported rather than
8664/// spinning forever.
8665///
8666/// `toJSON` is called on the value ONCE and is NOT re-applied to its own result
8667/// — `{toJSON(){ return {toJSON(){ return 1 }} }}` serializes as `{}` in Node,
8668/// because the inner method is a plain (unserializable) function property of the
8669/// returned object, not a second conversion hook.
8670fn apply_to_json(
8671 holder: &Value,
8672 key: &str,
8673 v: &Value,
8674 path: &mut Vec<Value>,
8675 rep: Option<&Value>,
8676) -> Result<Value, String> {
8677 let mut v = v.clone();
8678 if matches!(v, Value::Obj(_)) {
8679 let tag = crate::stdlib::native_tag(&v);
8680 // 25.5.2.1 step 2: `toJSON` is looked up with `[[Get]]`, so a PROXY
8681 // supplies one through its `get` trap. `lookup_chain` walks the
8682 // property map and never asks the handler, so a proxy carrying a
8683 // `toJSON` was serialized as a plain object instead of by its own
8684 // method — and node's trap log starts with that `get`.
8685 let to_json = if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8686 get_property(&v, "toJSON")?
8687 } else {
8688 with_host(|h| host::lookup_chain(h, &v, "toJSON")).unwrap_or(Value::Undef)
8689 };
8690 let has_to_json = with_host(|h| host::is_callable(h, &to_json))
8691 || tag
8692 .as_deref()
8693 .map(crate::stdlib::has_to_json)
8694 .unwrap_or(false);
8695 if has_to_json {
8696 let k = with_host(|h| h.new_str(key.to_string()));
8697 v = host::call_method(&v, "toJSON", vec![k])?;
8698 }
8699 }
8700 if let Some(rep) = rep {
8701 let k = with_host(|h| h.new_str(key.to_string()));
8702 v = host::invoke(rep, vec![k, v.clone()], Some(holder.clone()))?;
8703 }
8704 json_walk_children(&v, path, rep)
8705}
8706
8707/// Whether a raw property key of a host object is one `json_str` serializes. The
8708/// internal slots (`@@`-prefixed symbol keys, `#`-prefixed private fields) are
8709/// invisible to JSON, so the replacer must not be invoked for them either.
8710fn json_visible_key(k: &str) -> bool {
8711 !k.starts_with("@@") && !k.starts_with('#')
8712}
8713
8714/// Recurse into the elements/properties of an already-converted value, running
8715/// `apply_to_json` for each with this value as the holder.
8716fn json_walk_children(
8717 v: &Value,
8718 path: &mut Vec<Value>,
8719 rep: Option<&Value>,
8720) -> Result<Value, String> {
8721 if !matches!(v, Value::Obj(_)) {
8722 return Ok(v.clone());
8723 }
8724 // A value that contains itself has no JSON form.
8725 if with_host(|h| path.iter().any(|p| h.strict_eq(p, v))) {
8726 return Err(host::type_error("Converting circular structure to JSON"));
8727 }
8728 // A Proxy owns no property map, so it is snapshotted through its traps into
8729 // the plain array/object `SerializeJSONArray`/`SerializeJSONObject` describe
8730 // — which read every member through `[[Get]]`, exactly as the snapshot does.
8731 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8732 let snap = crate::proxy::json_snapshot(v)?;
8733 path.push(v.clone());
8734 let out = json_walk_children(&snap, path, rep);
8735 path.pop();
8736 return out;
8737 }
8738 let obj = with_host(|h| h.get(v).cloned());
8739 path.push(v.clone());
8740 let out = (|| match obj {
8741 Some(JsObj::Array(items)) => {
8742 // Read the elements through the accessor-aware funnel: an index
8743 // with a getter must be SERIALIZED as what the getter returns, and
8744 // the backing vector still holds the stale slot.
8745 let mut resolved = items;
8746 // An index with a getter must be SERIALIZED as what the getter
8747 // returns, and it also forces a rebuild below: keeping the original
8748 // array would hand the serializer back the stale backing vector.
8749 let had_accessor = resolve_index_accessors(v, &mut resolved);
8750 let items = resolved;
8751 let mut out = Vec::with_capacity(items.len());
8752 let mut changed = had_accessor;
8753 for (i, it) in items.iter().enumerate() {
8754 let nv = apply_to_json(v, &i.to_string(), it, path, rep)?;
8755 changed |= !with_host(|h| h.strict_eq(&nv, it));
8756 out.push(nv);
8757 }
8758 // Keep identity when nothing changed, so an enclosing object is not
8759 // needlessly rebuilt (which would drop its property attributes).
8760 if changed {
8761 Ok(with_host(|h| h.new_array(out)))
8762 } else {
8763 Ok(v.clone())
8764 }
8765 }
8766 Some(JsObj::Object(props)) => {
8767 // An enumerable own accessor must have its getter RUN and the result
8768 // serialized. That cannot happen inside `json_str` (which holds the
8769 // host borrow), so materialize here — the same reason `toJSON` is
8770 // applied in this pass.
8771 let has_accessor = with_host(|h| {
8772 h.own_accessor_keys(v)
8773 .iter()
8774 .any(|k| h.prop_attrs(v, k).enumerable)
8775 });
8776 if has_accessor {
8777 let mut next: IndexMap<String, Value> = IndexMap::new();
8778 for (k, val) in host::own_enum_entries_deep(v)? {
8779 let nv = if json_visible_key(&k) {
8780 apply_to_json(v, &k, &val, path, rep)?
8781 } else {
8782 val
8783 };
8784 next.insert(k, nv);
8785 }
8786 return Ok(with_host(|h| h.new_object(next)));
8787 }
8788 // Only rebuild when a descendant actually changed, so plain data keeps
8789 // its identity (and its prototype / native tag).
8790 let mut next: IndexMap<String, Value> = IndexMap::new();
8791 let mut changed = false;
8792 for (k, val) in &props {
8793 let nv = if json_visible_key(k) {
8794 apply_to_json(v, k, val, path, rep)?
8795 } else {
8796 val.clone()
8797 };
8798 changed |= !with_host(|h| h.strict_eq(&nv, val));
8799 next.insert(k.clone(), nv);
8800 }
8801 if changed {
8802 Ok(with_host(|h| {
8803 let o = h.new_object(next);
8804 h.copy_prop_attrs(v, &o);
8805 o
8806 }))
8807 } else {
8808 Ok(v.clone())
8809 }
8810 }
8811 _ => Ok(v.clone()),
8812 })();
8813 path.pop();
8814 out
8815}
8816
8817/// Whether a value tree contains a `BigInt` in a position `JSON.stringify` would
8818/// try to serialize (a value in an array/object) — such a value throws.
8819fn json_has_bigint(h: &host::JsHost, v: &Value) -> bool {
8820 match h.get(v) {
8821 Some(JsObj::BigInt(_)) => true,
8822 Some(JsObj::Array(items)) => items.iter().any(|x| json_has_bigint(h, x)),
8823 Some(JsObj::Object(props)) => props
8824 .iter()
8825 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
8826 .any(|(_, val)| json_has_bigint(h, val)),
8827 _ => false,
8828 }
8829}
8830
8831fn json_str(
8832 h: &host::JsHost,
8833 v: &Value,
8834 indent: &str,
8835 depth: usize,
8836 keys: Option<&[String]>,
8837) -> Option<String> {
8838 let sep = if indent.is_empty() { ":" } else { ": " };
8839 match v {
8840 Value::Undef => None,
8841 Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
8842 Value::Int(n) => Some(n.to_string()),
8843 Value::Float(f) => Some(if f.is_finite() {
8844 host::fmt_number(*f)
8845 } else {
8846 "null".into()
8847 }),
8848 Value::Str(s) => Some(json_quote(s)),
8849 Value::Obj(_) => match h.get(v) {
8850 Some(JsObj::Str(s)) => Some(json_quote(s)),
8851 Some(JsObj::Null) => Some("null".into()),
8852 // A `JSON.rawJSON` marker contributes its text VERBATIM — that is the
8853 // whole point of it, and it is why a number wider than a `double`
8854 // can survive a round trip.
8855 _ if h.fn_prop(v, "@@rawJSON").is_some() => match h.get(v) {
8856 Some(JsObj::Object(p)) => p.get("rawJSON").map(|r| h.str_of(r)),
8857 _ => None,
8858 },
8859 // A Map/Set has no ENTRIES to serialize (they are internal slots),
8860 // but any own property a script attached is serialized like an
8861 // ordinary object's: `JSON.stringify(Object.assign(new Map(), {a:1}))`
8862 // is `{"a":1}`.
8863 Some(JsObj::Map { .. })
8864 | Some(JsObj::Set { .. })
8865 | Some(JsObj::RegExp(_))
8866 // A Promise and a generator are ORDINARY objects to the serializer:
8867 // their state is internal slots, so they contribute no entries and
8868 // render as `{}`. They were being omitted entirely instead, so a
8869 // promise in an array became `null` and one in an object vanished.
8870 | Some(JsObj::Promise { .. })
8871 | Some(JsObj::Generator { .. }) => {
8872 let parts: Vec<String> = h
8873 .own_enum_entries(v)
8874 .into_iter()
8875 .filter(|(k, _)| !k.starts_with("@@") && !host::is_symbol_key(k))
8876 .filter_map(|(k, val)| {
8877 json_str(h, &val, indent, depth + 1, keys)
8878 .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8879 })
8880 .collect();
8881 Some(wrap(&parts, "{", "}", indent, depth))
8882 }
8883 // A NON-callable builtin is a namespace object, not a function, so
8884 // it serializes as one: `JSON.stringify(Math)` is `{}` (its members
8885 // are all non-enumerable), where omitting it made the whole property
8886 // disappear from its holder.
8887 Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
8888 let parts: Vec<String> = crate::stdlib::namespace_keys(n)
8889 .into_iter()
8890 .filter_map(|k| {
8891 let val = h.builtin_static(n, &k)?;
8892 json_str(h, &val, indent, depth + 1, keys)
8893 .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8894 })
8895 .collect();
8896 Some(wrap(&parts, "{", "}", indent, depth))
8897 }
8898 // Functions and symbols are omitted (undefined) as values.
8899 Some(JsObj::Func(_))
8900 | Some(JsObj::Builtin(_))
8901 | Some(JsObj::BoundMethod { .. })
8902 | Some(JsObj::BoundFunc { .. })
8903 | Some(JsObj::Class(_))
8904 | Some(JsObj::Symbol { .. }) => None,
8905 Some(JsObj::Array(items)) => {
8906 if items.is_empty() {
8907 return Some("[]".into());
8908 }
8909 let parts: Vec<String> = items
8910 .iter()
8911 .map(|x| {
8912 json_str(h, x, indent, depth + 1, keys).unwrap_or_else(|| "null".into())
8913 })
8914 .collect();
8915 Some(wrap(&parts, "[", "]", indent, depth))
8916 }
8917 Some(JsObj::Object(props)) if props.contains_key("@@primitive") => {
8918 // 25.5.2.2 step 4: a String/Number/Boolean wrapper serializes as
8919 // the primitive it boxes, not as the object holding it —
8920 // `JSON.stringify(new Number(1))` is `1`, not `{}`.
8921 json_str(h, &props["@@primitive"].clone(), indent, depth, keys)
8922 }
8923 Some(JsObj::Object(props)) => {
8924 // A replacer array restricts (and orders) which keys are emitted.
8925 let parts: Vec<String> = match keys {
8926 Some(allow) => allow
8927 .iter()
8928 .filter_map(|k| {
8929 props.get(k).and_then(|val| {
8930 json_str(h, val, indent, depth + 1, keys)
8931 .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
8932 })
8933 })
8934 .collect(),
8935 None => h
8936 .own_enum_entries(v)
8937 .iter()
8938 .filter_map(|(k, val)| {
8939 json_str(h, val, indent, depth + 1, keys)
8940 .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
8941 })
8942 .collect(),
8943 };
8944 if parts.is_empty() {
8945 return Some("{}".into());
8946 }
8947 Some(wrap(&parts, "{", "}", indent, depth))
8948 }
8949 _ => Some("null".into()),
8950 },
8951 _ => Some("null".into()),
8952 }
8953}
8954
8955fn wrap(parts: &[String], open: &str, close: &str, indent: &str, depth: usize) -> String {
8956 if indent.is_empty() {
8957 format!("{open}{}{close}", parts.join(","))
8958 } else {
8959 let pad = indent.repeat(depth + 1);
8960 let pad_close = indent.repeat(depth);
8961 format!(
8962 "{open}\n{pad}{}\n{pad_close}{close}",
8963 parts.join(&format!(",\n{pad}"))
8964 )
8965 }
8966}
8967
8968fn json_quote(s: &str) -> String {
8969 let mut out = String::from("\"");
8970 for c in s.chars() {
8971 match c {
8972 '"' => out.push_str("\\\""),
8973 '\\' => out.push_str("\\\\"),
8974 '\n' => out.push_str("\\n"),
8975 '\t' => out.push_str("\\t"),
8976 '\r' => out.push_str("\\r"),
8977 // QuoteJSONString (25.5.2.2) names SIX short escapes, not four.
8978 // Backspace and form feed were missing, so they fell through to the
8979 // `\uXXXX` arm below and `JSON.stringify("\b")` produced
8980 // `""` where node produces `"\b"`. Both parse back to the same
8981 // string, so the difference is invisible to a round trip and shows
8982 // up only as a byte mismatch against a fixture or a checksum.
8983 '\u{8}' => out.push_str("\\b"),
8984 '\u{c}' => out.push_str("\\f"),
8985 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
8986 _ => out.push(c),
8987 }
8988 }
8989 out.push('"');
8990 out
8991}
8992
8993fn json_parse(args: Vec<Value>) -> Result<Value, String> {
8994 let s = with_host(|h| h.str_of(&arg0(&args)));
8995 let mut p = JsonParser {
8996 chars: s.chars().collect(),
8997 pos: 0,
8998 prims: Vec::new(),
8999 record: args
9000 .get(1)
9001 .is_some_and(|r| with_host(|h| host::is_callable(h, r))),
9002 };
9003 p.skip_ws();
9004 if p.peek().is_none() {
9005 return Err("SyntaxError: Unexpected end of JSON input".into());
9006 }
9007 let v = p.parse_value()?;
9008 let value_end = p.pos;
9009 p.skip_ws();
9010 // Anything after the top-level value is an error — the parser used to accept
9011 // and silently discard it, so `JSON.parse('{"a":1}x')` succeeded.
9012 if let Some(c) = p.peek() {
9013 // V8 names the token kind only when it butts directly against the value
9014 // (`01` -> "Unexpected number at position 1"); with whitespace between
9015 // it is just a non-whitespace character (`1 2`).
9016 // Only a digit butted directly against a completed number literal —
9017 // V8's number scanner is still in number context there. `5"x"` and
9018 // `[0,1]0` exit the scanner cleanly and get the generic message.
9019 let after_number = value_end > 0
9020 && p.pos == value_end
9021 && p.chars[value_end - 1].is_ascii_digit()
9022 && c.is_ascii_digit();
9023 return Err(if after_number {
9024 p.err_at("Unexpected number", p.pos)
9025 } else {
9026 p.err_trailing(p.pos)
9027 });
9028 }
9029 // Optional reviver: walk bottom-up, transforming each (key, value).
9030 if let Some(reviver) = args
9031 .get(1)
9032 .filter(|r| with_host(|h| host::is_callable(h, r)))
9033 .cloned()
9034 {
9035 // The top-level holder is a fresh `{ "": value }` wrapper, as the spec
9036 // constructs before the walk.
9037 let root = with_host(|h| {
9038 let mut m: IndexMap<String, Value> = IndexMap::new();
9039 m.insert(String::new(), v.clone());
9040 h.new_object(m)
9041 });
9042 return json_revive("", v, &reviver, &root, &p.prims, &mut 0);
9043 }
9044 Ok(v)
9045}
9046
9047/// `JSON.parse` reviver walk: recurse into children first, then call
9048/// `reviver(key, value)`; a returned `undefined` drops the property.
9049///
9050/// The reviver runs with the HOLDER as `this` (25.5.1.1
9051/// InternalizeJSONProperty) — the object or array the key lives in, and at the
9052/// top level a wrapper `{ "": value }`. It was being called with no receiver,
9053/// so `this` was undefined and a reviver could not reach its siblings.
9054/// `JSON.rawJSON(text)` — a marker object whose text `JSON.stringify` emits
9055/// VERBATIM, so a number too large for a `double` survives a round trip
9056/// (`JSON.stringify({n: JSON.rawJSON("12345678901234567890")})`).
9057///
9058/// The validation is not "does `JSON.parse` accept it": node's rule, measured
9059/// across the whole matrix, is
9060///
9061/// ```text
9062/// "" -> SyntaxError: Invalid value for JSON.rawJSON
9063/// leading whitespace -> the parse error for that first character
9064/// a complete literal -> ok
9065/// anything left over -> SyntaxError: Invalid value for JSON.rawJSON
9066/// a broken literal -> the parse error the scanner raised
9067/// ```
9068///
9069/// so `" 1"` reports an unexpected token while `"1 "` and `"1,2"` report the
9070/// invalid-value message even though `JSON.parse` accepts the former and gives
9071/// a token error for the latter.
9072fn json_raw(args: Vec<Value>) -> Result<Value, String> {
9073 const INVALID: &str = "SyntaxError: Invalid value for JSON.rawJSON";
9074 let s = with_host(|h| h.str_of(&arg0(&args)));
9075 if s.is_empty() {
9076 return Err(INVALID.into());
9077 }
9078 let mut p = JsonParser {
9079 chars: s.chars().collect(),
9080 pos: 0,
9081 prims: Vec::new(),
9082 record: false,
9083 };
9084 // An object or an array is rejected where it starts, as leading whitespace
9085 // is — both are "not a primitive", but node reports the token.
9086 if matches!(p.peek(), Some('{') | Some('[')) || p.peek().is_some_and(|c| c.is_whitespace()) {
9087 return Err(p.err_token(0));
9088 }
9089 p.parse_value()?;
9090 if p.pos != p.chars.len() {
9091 // A digit butted against a completed number is still in the number
9092 // scanner, so `"01"` reports the scanner's error rather than leftover
9093 // input — the same distinction `json_parse` draws for trailing text.
9094 if p.chars[p.pos - 1].is_ascii_digit() && p.chars[p.pos].is_ascii_digit() {
9095 return Err(p.err_at("Unexpected number", p.pos));
9096 }
9097 return Err(INVALID.into());
9098 }
9099 // A null prototype and one own `rawJSON` property, frozen — the brand is a
9100 // hidden slot so `Object.keys` stays `["rawJSON"]`.
9101 Ok(with_host(|h| {
9102 let mut m: IndexMap<String, Value> = IndexMap::new();
9103 let text = h.new_str(s);
9104 m.insert("rawJSON".into(), text);
9105 let o = h.new_object(m);
9106 let null = h.null();
9107 h.set_proto(&o, null);
9108 h.set_fn_prop(&o, "@@rawJSON", Value::Bool(true));
9109 h.seal_object(&o, true);
9110 o
9111 }))
9112}
9113
9114/// `JSON.isRawJSON(v)` — the brand check. A hand-built `{ rawJSON: "1" }` is
9115/// NOT one, which is why the marker is a hidden slot rather than the property.
9116fn json_is_raw(args: Vec<Value>) -> Result<Value, String> {
9117 Ok(Value::Bool(is_raw_json(&arg0(&args))))
9118}
9119
9120fn is_raw_json(v: &Value) -> bool {
9121 with_host(|h| h.fn_prop(v, "@@rawJSON")).is_some()
9122}
9123
9124fn json_revive(
9125 key: &str,
9126 val: Value,
9127 reviver: &Value,
9128 holder: &Value,
9129 prims: &[String],
9130 next: &mut usize,
9131) -> Result<Value, String> {
9132 // A PRIMITIVE claims the next recorded source slice before its children
9133 // would — it has none — and a container claims nothing. The walk descends in
9134 // the same order the parse produced them, so one cursor lines the two up.
9135 let is_container =
9136 with_host(|h| matches!(h.get(&val), Some(JsObj::Array(_)) | Some(JsObj::Object(_))));
9137 let source = if !is_container {
9138 let s = prims.get(*next).cloned();
9139 if s.is_some() {
9140 *next += 1;
9141 }
9142 s
9143 } else {
9144 None
9145 };
9146 match with_host(|h| h.get(&val).cloned()) {
9147 Some(JsObj::Array(items)) => {
9148 for i in 0..items.len() {
9149 let elem = with_host(|h| match h.get(&val) {
9150 Some(JsObj::Array(it)) => it[i].clone(),
9151 _ => Value::Undef,
9152 });
9153 let nv = json_revive(&i.to_string(), elem, reviver, &val, prims, next)?;
9154 with_host(|h| {
9155 if let Some(JsObj::Array(it)) = h.get_mut(&val) {
9156 it[i] = nv;
9157 }
9158 });
9159 }
9160 }
9161 Some(JsObj::Object(props)) => {
9162 let keys: Vec<String> = props
9163 .keys()
9164 .filter(|k| !k.starts_with("@@"))
9165 .cloned()
9166 .collect();
9167 for k in keys {
9168 let elem = with_host(|h| match h.get(&val) {
9169 Some(JsObj::Object(p)) => p.get(&k).cloned().unwrap_or(Value::Undef),
9170 _ => Value::Undef,
9171 });
9172 let nv = json_revive(&k, elem, reviver, &val, prims, next)?;
9173 with_host(|h| {
9174 if let Some(JsObj::Object(p)) = h.get_mut(&val) {
9175 if matches!(nv, Value::Undef) {
9176 p.shift_remove(&k);
9177 } else {
9178 p.insert(k.clone(), nv);
9179 }
9180 }
9181 });
9182 }
9183 }
9184 _ => {}
9185 }
9186 let kv = with_host(|h| h.new_str(key.to_string()));
9187 // 25.5.1.1 step 2.b: the reviver's THIRD argument. `{ source }` for a
9188 // primitive, an empty object for an array or an object — node passes it
9189 // either way, and code reading `ctx.source` used to die on `undefined`
9190 // because only two arguments were passed.
9191 let ctx = with_host(|h| {
9192 let mut m: IndexMap<String, Value> = IndexMap::new();
9193 if let Some(s) = source {
9194 let sv = h.new_str(s);
9195 m.insert("source".into(), sv);
9196 }
9197 h.new_object(m)
9198 });
9199 host::invoke(reviver, vec![kv, val, ctx], Some(holder.clone()))
9200}
9201
9202struct JsonParser {
9203 chars: Vec<char>,
9204 pos: usize,
9205 /// Source text of each PRIMITIVE value, in parse order — what the reviver's
9206 /// third argument reports as `context.source` (25.5.1.1). Only collected
9207 /// when a reviver was supplied.
9208 ///
9209 /// A flat list rather than a parallel tree because the reviver walk visits
9210 /// primitives in the same depth-first order the parse produced them, so an
9211 /// index into this is enough to line them up.
9212 prims: Vec<String>,
9213 record: bool,
9214}
9215impl JsonParser {
9216 fn peek(&self) -> Option<char> {
9217 self.chars.get(self.pos).copied()
9218 }
9219
9220 /// `at position N (line L column C)` — the location suffix V8 appends to the
9221 /// positional JSON parse errors. Positions are in UTF-16-ish code units;
9222 /// node-js counts `char`s, which agree for the BMP.
9223 fn at(&self, pos: usize) -> String {
9224 let mut line = 1usize;
9225 let mut col = 1usize;
9226 for c in &self.chars[..pos.min(self.chars.len())] {
9227 if *c == '\n' {
9228 line += 1;
9229 col = 1;
9230 } else {
9231 col += 1;
9232 }
9233 }
9234 format!(" at position {pos} (line {line} column {col})")
9235 }
9236
9237 /// A positional error (`Expected ':' after property name in JSON at …`).
9238 fn err_at(&self, what: &str, pos: usize) -> String {
9239 format!("SyntaxError: {what} in JSON{}", self.at(pos))
9240 }
9241
9242 /// The one positional message V8 does NOT suffix with `in JSON`.
9243 fn err_trailing(&self, pos: usize) -> String {
9244 format!(
9245 "SyntaxError: Unexpected non-whitespace character after JSON{}",
9246 self.at(pos)
9247 )
9248 }
9249
9250 /// V8's default parse error: the offending character plus a window of the
9251 /// source. The whole input is quoted when it is short (<= 20 chars);
9252 /// otherwise a 10-character context window either side of `pos` is shown,
9253 /// elided with `...` on whichever side was cut.
9254 fn err_token(&self, pos: usize) -> String {
9255 const MAX_WHOLE: usize = 20;
9256 const CONTEXT: usize = 10;
9257 let len = self.chars.len();
9258 let Some(c) = self.chars.get(pos) else {
9259 return "SyntaxError: Unexpected end of JSON input".into();
9260 };
9261 // V8 reports the whole input for the JS literals that are famously not
9262 // JSON, without naming an offending character.
9263 let whole: String = self.chars.iter().collect();
9264 if matches!(
9265 whole.as_str(),
9266 "undefined" | "NaN" | "Infinity" | "-Infinity"
9267 ) {
9268 return format!("SyntaxError: \"{whole}\" is not valid JSON");
9269 }
9270 let snippet = if len <= MAX_WHOLE {
9271 format!("\"{whole}\"")
9272 } else {
9273 let start = pos.saturating_sub(CONTEXT);
9274 let end = (pos + CONTEXT).min(len);
9275 let body: String = self.chars[start..end].iter().collect();
9276 let head = if start > 0 { "..." } else { "" };
9277 let tail = if end < len { "..." } else { "" };
9278 format!("{head}\"{body}\"{tail}")
9279 };
9280 format!("SyntaxError: Unexpected token '{c}', {snippet} is not valid JSON")
9281 }
9282
9283 fn skip_ws(&mut self) {
9284 while matches!(
9285 self.peek(),
9286 Some(' ') | Some('\n') | Some('\t') | Some('\r')
9287 ) {
9288 self.pos += 1;
9289 }
9290 }
9291 fn parse_value(&mut self) -> Result<Value, String> {
9292 self.skip_ws();
9293 let start = self.pos;
9294 let prim = matches!(self.peek(), Some(c) if c != '{' && c != '[');
9295 let v = match self.peek() {
9296 Some('{') => self.parse_object(),
9297 Some('[') => self.parse_array(),
9298 Some('"') => {
9299 let s = self.parse_string()?;
9300 Ok(with_host(|h| h.new_str(s)))
9301 }
9302 Some('t') | Some('f') => self.parse_bool(),
9303 Some('n') => {
9304 self.expect_lit("null")?;
9305 Ok(with_host(|h| h.null()))
9306 }
9307 Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
9308 None => Err("SyntaxError: Unexpected end of JSON input".into()),
9309 _ => Err(self.err_token(self.pos)),
9310 }?;
9311 if prim && self.record {
9312 self.prims
9313 .push(self.chars[start..self.pos].iter().collect());
9314 }
9315 Ok(v)
9316 }
9317 fn expect_lit(&mut self, lit: &str) -> Result<(), String> {
9318 for ch in lit.chars() {
9319 match self.peek() {
9320 Some(c) if c == ch => self.pos += 1,
9321 // V8 reports the first character that broke the literal, which is
9322 // why `foo` complains about `'o'` (index 2) and not `'f'`.
9323 None => return Err("SyntaxError: Unexpected end of JSON input".into()),
9324 _ => return Err(self.err_token(self.pos)),
9325 }
9326 }
9327 Ok(())
9328 }
9329 fn parse_bool(&mut self) -> Result<Value, String> {
9330 if self.peek() == Some('t') {
9331 self.expect_lit("true")?;
9332 Ok(Value::Bool(true))
9333 } else {
9334 self.expect_lit("false")?;
9335 Ok(Value::Bool(false))
9336 }
9337 }
9338 /// JSON's number grammar: `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?`.
9339 /// A leading zero does NOT swallow the following digits — `01` parses as `0`
9340 /// and the stray `1` becomes a trailing-token error, which is how V8 reports
9341 /// it. Each way the grammar can run out has its own message.
9342 fn parse_number(&mut self) -> Result<Value, String> {
9343 let start = self.pos;
9344 if self.peek() == Some('-') {
9345 self.pos += 1;
9346 if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9347 return Err(self.err_at("No number after minus sign", self.pos));
9348 }
9349 }
9350 if self.peek() == Some('0') {
9351 self.pos += 1;
9352 } else {
9353 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9354 self.pos += 1;
9355 }
9356 }
9357 if self.peek() == Some('.') {
9358 self.pos += 1;
9359 if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9360 return Err(self.err_at("Unterminated fractional number", self.pos));
9361 }
9362 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9363 self.pos += 1;
9364 }
9365 }
9366 if matches!(self.peek(), Some('e') | Some('E')) {
9367 self.pos += 1;
9368 if matches!(self.peek(), Some('+') | Some('-')) {
9369 self.pos += 1;
9370 }
9371 if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9372 return Err(self.err_at("Exponent part is missing a number", self.pos));
9373 }
9374 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9375 self.pos += 1;
9376 }
9377 }
9378 let s: String = self.chars[start..self.pos].iter().collect();
9379 s.parse::<f64>()
9380 .map(Value::Float)
9381 .map_err(|_| self.err_at("Unexpected number", start))
9382 }
9383 fn parse_string(&mut self) -> Result<String, String> {
9384 self.pos += 1; // opening quote
9385 let mut out = String::new();
9386 loop {
9387 match self.peek() {
9388 None => return Err(self.err_at("Unterminated string", self.pos)),
9389 Some('"') => {
9390 self.pos += 1;
9391 break;
9392 }
9393 Some('\\') => {
9394 self.pos += 1;
9395 match self.peek() {
9396 Some('n') => out.push('\n'),
9397 Some('t') => out.push('\t'),
9398 Some('r') => out.push('\r'),
9399 Some('"') => out.push('"'),
9400 Some('\\') => out.push('\\'),
9401 Some('/') => out.push('/'),
9402 Some('b') => out.push('\u{08}'),
9403 Some('f') => out.push('\u{0C}'),
9404 Some('u') => {
9405 let h: String = self.chars
9406 [self.pos + 1..(self.pos + 5).min(self.chars.len())]
9407 .iter()
9408 .collect();
9409 if let Ok(n) = u32::from_str_radix(&h, 16) {
9410 if let Some(ch) = char::from_u32(n) {
9411 out.push(ch);
9412 }
9413 }
9414 self.pos += 4;
9415 }
9416 _ => {}
9417 }
9418 self.pos += 1;
9419 }
9420 // A raw control character is not legal inside a JSON string; it
9421 // has to be escaped. V8 rejects it rather than passing it through.
9422 Some(c) if (c as u32) < 0x20 => {
9423 return Err(self.err_at("Bad control character in string literal", self.pos))
9424 }
9425 Some(c) => {
9426 out.push(c);
9427 self.pos += 1;
9428 }
9429 }
9430 }
9431 Ok(out)
9432 }
9433 fn parse_array(&mut self) -> Result<Value, String> {
9434 self.pos += 1; // [
9435 let mut items = Vec::new();
9436 self.skip_ws();
9437 if self.peek() == Some(']') {
9438 self.pos += 1;
9439 return Ok(with_host(|h| h.new_array(items)));
9440 }
9441 loop {
9442 items.push(self.parse_value()?);
9443 self.skip_ws();
9444 match self.peek() {
9445 Some(',') => {
9446 self.pos += 1;
9447 }
9448 Some(']') => {
9449 self.pos += 1;
9450 break;
9451 }
9452 _ => return Err(self.err_at("Expected ',' or ']' after array element", self.pos)),
9453 }
9454 }
9455 Ok(with_host(|h| h.new_array(items)))
9456 }
9457 fn parse_object(&mut self) -> Result<Value, String> {
9458 self.pos += 1; // {
9459 let mut props: IndexMap<String, Value> = IndexMap::new();
9460 self.skip_ws();
9461 if self.peek() == Some('}') {
9462 self.pos += 1;
9463 return Ok(with_host(|h| h.new_object(props)));
9464 }
9465 loop {
9466 self.skip_ws();
9467 if self.peek() != Some('"') {
9468 // The first key uses the "or '}'" wording (an empty object is
9469 // still legal there); a key after a comma does not. End of input
9470 // reports the same expectation, at the end position.
9471 return Err(if props.is_empty() {
9472 self.err_at("Expected property name or '}'", self.pos)
9473 } else {
9474 self.err_at("Expected double-quoted property name", self.pos)
9475 });
9476 }
9477 let key = self.parse_string()?;
9478 self.skip_ws();
9479 if self.peek() != Some(':') {
9480 return Err(match self.peek() {
9481 None => "SyntaxError: Unexpected end of JSON input".into(),
9482 _ => self.err_at("Expected ':' after property name", self.pos),
9483 });
9484 }
9485 self.pos += 1;
9486 let val = self.parse_value()?;
9487 props.insert(key, val);
9488 self.skip_ws();
9489 match self.peek() {
9490 Some(',') => {
9491 self.pos += 1;
9492 }
9493 Some('}') => {
9494 self.pos += 1;
9495 break;
9496 }
9497 _ => return Err(self.err_at("Expected ',' or '}' after property value", self.pos)),
9498 }
9499 }
9500 Ok(with_host(|h| h.new_object(props)))
9501 }
9502}
9503
9504// ══ type methods (array / string / number) ═══════════════════════════════════
9505
9506fn is_array_method(name: &str) -> bool {
9507 matches!(
9508 name,
9509 "push"
9510 | "pop"
9511 | "shift"
9512 | "unshift"
9513 | "map"
9514 | "filter"
9515 | "forEach"
9516 | "join"
9517 | "slice"
9518 | "indexOf"
9519 | "lastIndexOf"
9520 | "includes"
9521 | "reduce"
9522 | "concat"
9523 | "reverse"
9524 | "sort"
9525 | "find"
9526 | "findIndex"
9527 | "some"
9528 | "every"
9529 | "flat"
9530 | "fill"
9531 | "splice"
9532 | "keys"
9533 | "values"
9534 | "entries"
9535 | "flatMap"
9536 | "at"
9537 | "toString"
9538 | "reduceRight"
9539 | "findLast"
9540 | "findLastIndex"
9541 | "copyWithin"
9542 )
9543}
9544/// Every `String.prototype` method node-js implements.
9545///
9546/// A LIST rather than a `matches!` arm because the same set has to be installed
9547/// on the real `String.prototype` object: a method read off the prototype
9548/// (`String.prototype.trim.call(s)`, the generic-borrowing idiom libraries use)
9549/// found nothing there, so the two views of "which methods exist" would drift
9550/// if they were written twice.
9551pub(crate) const STRING_PROTO_METHODS: &[&str] = &[
9552 "toUpperCase",
9553 "toLowerCase",
9554 "charAt",
9555 "charCodeAt",
9556 "codePointAt",
9557 "indexOf",
9558 "lastIndexOf",
9559 "includes",
9560 "slice",
9561 "substring",
9562 "substr",
9563 "split",
9564 "trim",
9565 "trimStart",
9566 "trimEnd",
9567 "replace",
9568 "replaceAll",
9569 "repeat",
9570 "startsWith",
9571 "endsWith",
9572 "padStart",
9573 "padEnd",
9574 "concat",
9575 "at",
9576 "toString",
9577 "toLocaleString",
9578 "valueOf",
9579 "match",
9580 "matchAll",
9581 "search",
9582 "normalize",
9583 "localeCompare",
9584 "toLocaleUpperCase",
9585 "toLocaleLowerCase",
9586 "isWellFormed",
9587 "toWellFormed",
9588];
9589
9590fn is_string_method(name: &str) -> bool {
9591 STRING_PROTO_METHODS.contains(&name)
9592}
9593
9594/// Every SYMBOL-keyed intrinsic method the generated table lists for `ctor`,
9595/// spelled the way this frontend spells the key (`@@iterator`).
9596///
9597/// A prototype built as a REAL object (`String.prototype`, `URLSearchParams
9598/// .prototype`) installs its methods from a list, and only the string-keyed
9599/// list was walked — so `String.prototype[Symbol.iterator]` read `undefined`
9600/// while `Array.prototype[Symbol.iterator]`, which resolves through the
9601/// `Builtin` namespace and its table gate, answered a function. Derived from
9602/// the table rather than written out, so it cannot name a method node does not
9603/// define nor miss one it does.
9604pub(crate) fn proto_symbol_methods(ctor: &str) -> Vec<&'static str> {
9605 let prefix = format!("@proto:{ctor}:");
9606 crate::arity::BUILTIN_ARITY
9607 .iter()
9608 .filter_map(|(k, _, _)| k.strip_prefix(prefix.as_str()))
9609 .filter(|m| m.starts_with("@@"))
9610 .collect()
9611}
9612
9613/// The builtin constructors whose `.prototype` object is BRANDED — every other
9614/// `<C>.prototype` is an ordinary object and reports `[object Object]`.
9615///
9616/// Measured on node v26.8.1 over every constructor this frontend knows:
9617///
9618/// ```text
9619/// Array/Object/Number/String/Boolean/Function the ES5 legacy slot prototypes
9620/// Symbol/BigInt/Map/Set/WeakMap/WeakSet carry an own @@toStringTag
9621/// Promise/Iterator/ArrayBuffer/DataView "
9622/// WeakRef/FinalizationRegistry/URL "
9623/// URLSearchParams/TextEncoder/TextDecoder "
9624/// Date/RegExp/Error/TypeError/Uint8Array/… [object Object]
9625/// ```
9626///
9627/// The rule this replaces branded EVERY `<C>.prototype` as `C`, so
9628/// `Object.prototype.toString.call(Date.prototype)` read `[object Date]` — and
9629/// a `Date.prototype.toString` call on a plain object named `[object Date]` in
9630/// its own failure message where node names `[object Object]`.
9631pub(crate) const BRANDED_PROTOS: &[&str] = &[
9632 "Array",
9633 "ArrayBuffer",
9634 "BigInt",
9635 "Boolean",
9636 "DataView",
9637 "FinalizationRegistry",
9638 "Function",
9639 "Iterator",
9640 "Map",
9641 "Number",
9642 "Object",
9643 "Promise",
9644 "Set",
9645 "SharedArrayBuffer",
9646 "String",
9647 "Symbol",
9648 "TextDecoder",
9649 "TextEncoder",
9650 "URL",
9651 "URLSearchParams",
9652 "WeakMap",
9653 "WeakRef",
9654 "WeakSet",
9655];
9656
9657/// Whether `v` is a `RegExp` value (drives the regex path of `match`/`replace`/…).
9658/// A user `Symbol.match`/`replace`/`search`/`split`/`matchAll` method on the
9659/// ARGUMENT, which the string method must delegate to (22.1.3.x step 2).
9660///
9661/// `"abc".match(o)` where `o` defines `Symbol.match` calls that method rather
9662/// than coercing `o` to a pattern — the protocol every regexp-like library
9663/// implements. None of the five were consulted, so a custom matcher was
9664/// silently stringified instead.
9665fn symbol_protocol(arg: &Value, sym: &str) -> Option<Value> {
9666 if matches!(arg, Value::Undef) || with_host(|h| h.is_null(arg)) {
9667 return None;
9668 }
9669 let f = get_property(arg, sym).ok()?;
9670 with_host(|h| host::is_callable(h, &f)).then_some(f)
9671}
9672
9673fn is_regexp_arg(v: &Value) -> bool {
9674 // 7.2.8 `IsRegExp` asks `Symbol.match` FIRST, so an object can declare
9675 // itself a regexp — or a real one can disown the label. Only the heap kind
9676 // was checked, so `"a".startsWith({[Symbol.match]: true})` did not throw
9677 // the TypeError the spec requires.
9678 if let Ok(m) = get_property(v, "@@match") {
9679 if !matches!(m, Value::Undef) {
9680 return with_host(|h| h.truthy(&m));
9681 }
9682 }
9683 with_host(|h| h.kind_of(v)) == Some(ObjKind::RegExp)
9684}
9685
9686/// `str.replace(strPattern, fn)` — a function replacer against a literal (string)
9687/// pattern: replace the first (or all) occurrence, calling `fn(match, offset, s)`.
9688fn replace_str_fn(s: &str, pat: &str, repl: &Value, all: bool) -> Result<String, String> {
9689 if pat.is_empty() {
9690 return Ok(s.to_string());
9691 }
9692 let mut out = String::new();
9693 let mut rest = s;
9694 let mut base = 0usize;
9695 while let Some(pos) = rest.find(pat) {
9696 out.push_str(&rest[..pos]);
9697 let offset = base + pos;
9698 let m = with_host(|h| h.new_str(pat.to_string()));
9699 let str_arg = with_host(|h| h.new_str(s.to_string()));
9700 let r = host::invoke(repl, vec![m, Value::Float(offset as f64), str_arg], None)?;
9701 out.push_str(&with_host(|h| h.str_of(&r)));
9702 let consumed = pos + pat.len();
9703 base += consumed;
9704 rest = &rest[consumed..];
9705 if !all {
9706 break;
9707 }
9708 }
9709 out.push_str(rest);
9710 Ok(out)
9711}
9712/// Every `Number.prototype` method node-js implements — a list for the same
9713/// reason [`STRING_PROTO_METHODS`] is one.
9714pub(crate) const NUMBER_PROTO_METHODS: &[&str] = &[
9715 "toFixed",
9716 "toExponential",
9717 "toString",
9718 "toPrecision",
9719 "toLocaleString",
9720 "valueOf",
9721];
9722
9723fn is_number_method(name: &str) -> bool {
9724 NUMBER_PROTO_METHODS.contains(&name)
9725}
9726
9727/// The exotic kinds whose own dispatch table does NOT already reach the
9728/// `Object.prototype` methods, so the inherited ones have to be routed to.
9729///
9730/// An allowlist rather than a catch-all: a primitive receiver also reaches this
9731/// function, and a Number's `toString` is `Number.prototype.toString` — routing
9732/// it to the object form made `(255).toString(16)` report `[object Number]`.
9733fn inherits_object_methods(recv: &Value) -> bool {
9734 matches!(
9735 with_host(|h| h.kind_of(recv)),
9736 Some(
9737 ObjKind::Map
9738 | ObjKind::Set
9739 | ObjKind::Promise
9740 | ObjKind::RegExp
9741 | ObjKind::Generator
9742 | ObjKind::Symbol
9743 | ObjKind::BigInt
9744 | ObjKind::Iter
9745 )
9746 )
9747}
9748
9749/// Whether `recv`'s own prototype defines `name`, shadowing the
9750/// `Object.prototype` method of that name — `RegExp.prototype.toString` does,
9751/// `Map.prototype` does not.
9752fn overrides_object_method(recv: &Value, name: &str) -> bool {
9753 match with_host(|h| h.kind_of(recv)) {
9754 Some(ObjKind::Map) => is_map_method(name),
9755 Some(ObjKind::Set) => is_set_method(name),
9756 Some(ObjKind::RegExp) => crate::regexp::is_regexp_method(name),
9757 // A Symbol has its own `toString`; `valueOf` is the inherited one,
9758 // which returns the receiver — exactly what a symbol needs.
9759 Some(ObjKind::Symbol) => matches!(name, "toString" | "valueOf" | "@@toPrimitive"),
9760 Some(ObjKind::BigInt) => matches!(name, "toString" | "valueOf" | "toLocaleString"),
9761 _ => false,
9762 }
9763}
9764
9765/// Dispatch `recv.name(args)` for the built-in prototype methods.
9766pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
9767 // A USER method on the receiver's prototype chain wins over the builtin of
9768 // the same name — that is how a `class X extends Array` method is reached,
9769 // since the dispatch below goes straight to the builtin table and has no
9770 // entry for it.
9771 //
9772 // Deliberately restricted to a user function: the shared `Object.prototype`
9773 // carries real `@proto:Object:*` thunks, so accepting any callable made a
9774 // bare `map.toString()` resolve to the object form instead of the builtin
9775 // one the exotic is supposed to use.
9776 if let Some(f) = with_host(|h| host::lookup_chain(h, recv, name)) {
9777 if matches!(
9778 with_host(|h| h.kind_of(&f)),
9779 Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc)
9780 ) {
9781 return host::invoke(&f, args, Some(recv.clone()));
9782 }
9783 }
9784 // A method synthesized from the receiver's KIND is unreachable once its
9785 // intrinsic prototype is off the chain. The read already answers
9786 // `undefined` for one; dispatch has its own table and would still have
9787 // called it, so `Object.setPrototypeOf(a, {}); a.join()` returned "1,2"
9788 // while `a.join` was `undefined` — the read and the call disagreeing again,
9789 // in the opposite direction from the monkey-patch case below.
9790 if !own_intrinsic_reachable(recv)
9791 && inherited_method_owner(recv, name).is_none()
9792 && !has_own_for_shadow(recv, name)
9793 && inherited_builtin_static(recv, name).is_none()
9794 && with_host(|h| host::lookup_chain(h, recv, name)).is_none()
9795 {
9796 return Err(host::type_error(&format!("{name} is not a function")));
9797 }
9798 // A method monkey-patched onto the receiver's intrinsic prototype. The READ
9799 // path resolves these, but dispatch goes straight to the builtin table and
9800 // never consults it, so `Array.prototype.last = f; [1].last()` threw "is not
9801 // a function" while `[1].last` WAS `f` — the read and the call disagreeing
9802 // about the same name, on the one path a polyfill actually uses.
9803 if !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
9804 if let Some(f) = inherited_builtin_static(recv, name) {
9805 if with_host(|h| host::is_callable(h, &f)) {
9806 return host::invoke(&f, args, Some(recv.clone()));
9807 }
9808 }
9809 }
9810 // Every object INHERITS the `Object.prototype` methods, and an exotic that
9811 // does not define its own reaches them the same way. Each kind's dispatch
9812 // table below only knows its own methods, so `new Map().toString()`,
9813 // `promise.hasOwnProperty(k)` and `sym.toLocaleString()` all reported "is
9814 // not a function" — `Object.prototype.toString.call(m)` worked while
9815 // `m.toString()` did not.
9816 // The allowlist is the kinds whose own dispatch table below would otherwise
9817 // claim the name. Every OTHER receiver reaches an `Object.prototype` method
9818 // the same way — a function, a class and a bound function included, where
9819 // `f.hasOwnProperty(k)` reported "is not a function" even though the READ
9820 // resolved it. `inherited_method_owner` decides which prototype owns the
9821 // name, so a kind that defines its own still gets its own.
9822 if is_object_builtin_method(name)
9823 && (inherited_method_owner(recv, name) == Some("Object")
9824 || (inherits_object_methods(recv) && !overrides_object_method(recv, name)))
9825 {
9826 // `toString` goes through the branded form (20.1.3.6), which reads
9827 // `Symbol.toStringTag` and falls back to the receiver's own brand —
9828 // `[object Map]`, not the generic stringification.
9829 if name == "toString" {
9830 return proto_method(recv, "Object:toString", args);
9831 }
9832 return object_builtin_method(recv, name, args);
9833 }
9834 // `Object.prototype.valueOf` is inherited by every exotic that does not
9835 // override it (an Array does not), and returns the receiver. Without this
9836 // the `ToPrimitive` probe on `[o] + ''` reached `array_method("valueOf")`
9837 // and threw `valueOf is not a function`.
9838 if name == "valueOf"
9839 && matches!(
9840 with_host(|h| h.kind_of(recv)),
9841 Some(
9842 ObjKind::Array
9843 | ObjKind::Map
9844 | ObjKind::Set
9845 | ObjKind::Generator
9846 | ObjKind::Promise
9847 | ObjKind::Iter
9848 | ObjKind::RegExp
9849 )
9850 )
9851 {
9852 return Ok(recv.clone());
9853 }
9854 // Only the tag is needed to pick the branch — cloning the receiver here made
9855 // every `arr.push(x)` copy the whole array, so a fill loop was O(n^2).
9856 match with_host(|h| h.kind_of(recv)) {
9857 Some(ObjKind::Array) => array_method(recv, name, args),
9858 Some(ObjKind::Str) => {
9859 // `string_method` consumes the text itself, so this clone is the
9860 // payload, not a tag probe.
9861 let s = peek(recv, |o| match o {
9862 JsObj::Str(s) => Some(s.clone()),
9863 _ => None,
9864 })
9865 .unwrap_or_default();
9866 string_method(&s, name, args)
9867 }
9868 Some(ObjKind::Map) => map_method(recv, name, args),
9869 Some(ObjKind::Set) => set_method(recv, name, args),
9870 Some(ObjKind::Generator) if crate::stdlib::iterator::is_helper(name) => {
9871 crate::stdlib::iterator::call(recv, name, &args)
9872 }
9873 Some(ObjKind::Generator) => generator_method(recv, name, args),
9874 Some(ObjKind::Promise) => promise_method(recv, name, args),
9875 Some(ObjKind::Iter) if crate::stdlib::iterator::is_helper(name) => {
9876 crate::stdlib::iterator::call(recv, name, &args)
9877 }
9878 Some(ObjKind::Iter) => iter_method(recv, name, args),
9879 Some(ObjKind::Symbol) => symbol_method(recv, name, args),
9880 Some(ObjKind::BigInt) => {
9881 let b = peek(recv, |o| match o {
9882 JsObj::BigInt(b) => Some(b.clone()),
9883 _ => None,
9884 })
9885 .unwrap_or_default();
9886 bigint_method(&b, name, args)
9887 }
9888 Some(ObjKind::RegExp) => crate::regexp::regexp_method(recv, name, args),
9889 Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc) => {
9890 match function_builtin_method(recv, name, &args)? {
9891 Some(v) => Ok(v),
9892 None => Err(host::type_error(&format!("{name} is not a function"))),
9893 }
9894 }
9895 Some(ObjKind::Object) => {
9896 if let Some(f) = peek(recv, |o| match o {
9897 JsObj::Object(p) => p.get(name).cloned(),
9898 _ => None,
9899 }) {
9900 host::invoke(&f, args, Some(recv.clone()))
9901 } else if name == "hasOwnProperty" {
9902 let k = with_host(|h| h.str_of(&arg0(&args)));
9903 let has = peek(recv, |o| match o {
9904 JsObj::Object(p) => Some(p.contains_key(&k)),
9905 _ => None,
9906 })
9907 .unwrap_or(false);
9908 Ok(Value::Bool(has))
9909 } else if name == "toString" {
9910 Ok(with_host(|h| h.new_str("[object Object]")))
9911 } else {
9912 Err(host::type_error(&format!("{} is not a function", name)))
9913 }
9914 }
9915 _ => {
9916 // Primitive number/bool/string coercions.
9917 if let Value::Float(_) | Value::Int(_) = recv {
9918 return number_method(with_host(|h| h.to_number(recv)), name, args);
9919 }
9920 if let Some(s) = with_host(|h| h.as_str(recv)) {
9921 return string_method(&s, name, args);
9922 }
9923 // `Boolean.prototype` (20.3.3): a boolean is not a heap object here,
9924 // so it reached no branch at all and `true.toString()` threw `is not
9925 // a function`. Its three methods are `toString`, `valueOf`, and the
9926 // inherited `Object.prototype.toLocaleString` — which
9927 // `[1,'a',true].toLocaleString()` invokes per element, so the hole
9928 // was reachable from the array form too.
9929 if let Value::Bool(b) = recv {
9930 return match name {
9931 "toString" | "toLocaleString" => {
9932 Ok(new_s(if *b { "true" } else { "false" }.to_string()))
9933 }
9934 "valueOf" => Ok(Value::Bool(*b)),
9935 _ => Err(host::type_error(&format!("{name} is not a function"))),
9936 };
9937 }
9938 Err(host::type_error(&format!("{} is not a function", name)))
9939 }
9940 }
9941}
9942
9943/// A copy of the whole backing store, for the methods that genuinely consume
9944/// every element (`map`, `filter`, `join`, …). Never call it just to read
9945/// `.len()` — use [`array_len`], or `push`/`unshift` become O(n) per call.
9946/// A LIVE iterator over a `Map` or `Set`.
9947///
9948/// Node's collection iterators see the collection as it is at each step: an
9949/// entry added during iteration IS visited, and one deleted before it is
9950/// reached is NOT. Ours materialized every entry up front, so both were wrong —
9951/// a loop that deletes as it goes still processed the entries it had removed.
9952///
9953/// The cursor is the last key yielded plus the index it was at. On each step
9954/// the key is located again in the CURRENT order: if it is still there the next
9955/// entry follows it, and if it was itself deleted the stored index now names
9956/// the entry that shifted into its place. That reproduces node for the cases
9957/// its own tests turn on — add-during, delete-ahead, delete-self,
9958/// delete-behind, delete-the-rest and clear — without giving `Map` the
9959/// tombstoned entry list node uses internally.
9960fn collection_iterator(coll: &Value, kind: &str) -> Value {
9961 with_host(|h| {
9962 let mut m = IndexMap::new();
9963 m.insert(
9964 "@@native".into(),
9965 h.new_str("CollectionIterator".to_string()),
9966 );
9967 m.insert("@@coll".into(), coll.clone());
9968 m.insert("@@kind".into(), h.new_str(kind.to_string()));
9969 m.insert("@@started".into(), Value::Bool(false));
9970 m.insert("@@lastIdx".into(), Value::Float(0.0));
9971 h.new_object(m)
9972 })
9973}
9974
9975/// One step of a live collection iterator.
9976pub(crate) fn collection_iterator_next(recv: &Value) -> Result<Value, String> {
9977 let slot = |k: &str| {
9978 with_host(|h| match h.get(recv) {
9979 Some(JsObj::Object(p)) => p.get(k).cloned(),
9980 _ => None,
9981 })
9982 };
9983 let coll = slot("@@coll").unwrap_or(Value::Undef);
9984 let kind = slot("@@kind")
9985 .map(|v| with_host(|h| h.str_of(&v)))
9986 .unwrap_or_default();
9987 let started = slot("@@started").is_some_and(|v| with_host(|h| h.truthy(&v)));
9988 let last_idx = slot("@@lastIdx")
9989 .map(|v| with_host(|h| h.to_number(&v)) as usize)
9990 .unwrap_or(0);
9991 let last_key = slot("@@lastKey");
9992
9993 let next_idx = if !started {
9994 0
9995 } else {
9996 match last_key
9997 .as_ref()
9998 .and_then(|k| with_host(|h| collection_index_of(h, &coll, k)))
9999 {
10000 // Still present: continue after it.
10001 Some(i) => i + 1,
10002 // Deleted since: whatever shifted into its slot is next.
10003 None => last_idx,
10004 }
10005 };
10006 let entry = with_host(|h| collection_entry_at(h, &coll, next_idx));
10007 let Some((k, v)) = entry else {
10008 return Ok(iter_result(Value::Undef, true));
10009 };
10010 with_host(|h| {
10011 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
10012 p.insert("@@started".into(), Value::Bool(true));
10013 p.insert("@@lastIdx".into(), Value::Float(next_idx as f64));
10014 p.insert("@@lastKey".into(), k.clone());
10015 }
10016 });
10017 let out = match kind.as_str() {
10018 "keys" => k,
10019 "values" => v,
10020 _ => with_host(|h| h.new_array(vec![k, v])),
10021 };
10022 Ok(iter_result(out, false))
10023}
10024
10025/// The (key, value) at `idx` in a Map, or (value, value) in a Set.
10026fn collection_entry_at(h: &host::JsHost, coll: &Value, idx: usize) -> Option<(Value, Value)> {
10027 match h.get(coll) {
10028 Some(JsObj::Map { entries, .. }) => entries.get_index(idx).map(|(_, kv)| kv.clone()),
10029 Some(JsObj::Set { entries, .. }) => {
10030 entries.get_index(idx).map(|(_, v)| (v.clone(), v.clone()))
10031 }
10032 _ => None,
10033 }
10034}
10035
10036/// Where `key` currently sits in the collection's order.
10037fn collection_index_of(h: &host::JsHost, coll: &Value, key: &Value) -> Option<usize> {
10038 let mk = host::map_key(h, key);
10039 match h.get(coll) {
10040 Some(JsObj::Map { entries, .. }) => entries.get_index_of(&mk),
10041 Some(JsObj::Set { entries, .. }) => entries.get_index_of(&mk),
10042 _ => None,
10043 }
10044}
10045
10046/// The `thisArg` an iteration method was given, if any.
10047///
10048/// `[1].forEach(fn, thisArg)` binds `thisArg` as the callback's `this`, and so
10049/// do `map`/`filter`/`some`/`every`/`find`/`findIndex`/`findLast`/
10050/// `findLastIndex`/`flatMap`, `Map`/`Set`/TypedArray `forEach`, and
10051/// `Array.from`'s map function. Every one of them was invoking the callback
10052/// with no receiver, so `this` inside it was undefined and the argument did
10053/// nothing.
10054fn this_arg(args: &[Value], idx: usize) -> Option<Value> {
10055 args.get(idx)
10056 .filter(|v| !matches!(v, Value::Undef))
10057 .cloned()
10058}
10059
10060/// The elements of an array, with any INDEX ACCESSOR resolved.
10061///
10062/// `Object.defineProperty(arr, 1, { get })` stores the getter in the accessor
10063/// table, and an array's elements live in a backing vector — so every method
10064/// reading that vector directly (`join`, `map`, `indexOf`, …) saw the stale
10065/// slot and never called the getter, while a plain `arr[1]` read did.
10066///
10067/// An array with no accessors pays one lookup returning an empty list, so the
10068/// ordinary case is unchanged. The getters are invoked OUTSIDE the host borrow,
10069/// since calling one re-enters.
10070/// Walk `recv` the way an `Array.prototype` iteration method does: the LENGTH
10071/// is captured once at entry (LengthOfArrayLike, step 3), but each element is
10072/// read LIVE at its index, and an index that no longer exists is skipped.
10073///
10074/// Snapshotting the whole array instead meant a callback that mutated it was
10075/// not observed: `[1,2,3].forEach(v => a.shift())` visited 1, 2, 3 where node
10076/// visits 1 and 3, and `filter` kept elements the callback had already removed.
10077///
10078/// `f` returns `Some(x)` to stop early with `x`.
10079fn array_walk<T>(
10080 recv: &Value,
10081 mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10082) -> Result<Option<T>, String> {
10083 let len = array_len(recv);
10084 for i in 0..len {
10085 // A HOLE — and an index a shrinking mutation has dropped — is skipped
10086 // without calling the callback.
10087 if index_absent(recv, i) || i >= array_len(recv) {
10088 continue;
10089 }
10090 let v = get_property(recv, &i.to_string())?;
10091 if let Some(out) = f(i, v)? {
10092 return Ok(Some(out));
10093 }
10094 }
10095 Ok(None)
10096}
10097
10098/// `array_walk`'s descending twin, for `reduceRight`/`findLast*`: the same
10099/// capture-length-once, read-each-element-live rule walked from the end. A
10100/// callback that SHRINKS the array is observed by every later step, so the
10101/// indices it drops are skipped rather than served from a stale copy.
10102fn array_walk_rev<T>(
10103 recv: &Value,
10104 from: usize,
10105 mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10106) -> Result<Option<T>, String> {
10107 for i in (0..from).rev() {
10108 if index_absent(recv, i) || i >= array_len(recv) {
10109 continue;
10110 }
10111 let v = get_property(recv, &i.to_string())?;
10112 if let Some(out) = f(i, v)? {
10113 return Ok(Some(out));
10114 }
10115 }
10116 Ok(None)
10117}
10118
10119/// The live read behind `indexOf`/`includes`/`join`: the element at `i`, or
10120/// `undefined` once a mutation has shrunk the array past it.
10121fn array_elem_live(recv: &Value, i: usize) -> Result<Value, String> {
10122 if i >= array_len(recv) {
10123 return Ok(Value::Undef);
10124 }
10125 get_property(recv, &i.to_string())
10126}
10127
10128fn array_items(recv: &Value) -> Vec<Value> {
10129 let mut items = with_host(|h| match h.get(recv) {
10130 Some(JsObj::Array(items)) => items.clone(),
10131 _ => Vec::new(),
10132 });
10133 resolve_index_accessors(recv, &mut items);
10134 items
10135}
10136
10137/// Replace each slot that has an own accessor with what its getter returns.
10138pub(crate) fn resolve_index_accessors_pub(recv: &Value, items: &mut [Value]) {
10139 resolve_index_accessors(recv, items);
10140}
10141
10142/// Returns whether any slot was replaced, which the JSON walk needs: it keeps
10143/// the ORIGINAL array when nothing changed, and the original still holds the
10144/// stale slots.
10145fn resolve_index_accessors(recv: &Value, items: &mut [Value]) -> bool {
10146 let mut indices: Vec<usize> = with_host(|h| h.own_accessor_keys(recv))
10147 .into_iter()
10148 .filter_map(|k| k.parse::<usize>().ok())
10149 .filter(|i| *i < items.len())
10150 .collect();
10151 // An ELIDED index the prototype chain supplies is stale in the backing
10152 // vector too — it holds `undefined` where `[[Get]]` answers the inherited
10153 // value. Spread and `JSON.stringify` both read through here, and both
10154 // rendered the hole rather than what `a[i]` reads.
10155 let inherited: Vec<usize> = with_host(|h| h.hole_indices(recv))
10156 .into_iter()
10157 .filter(|i| *i < items.len() && !indices.contains(i))
10158 .filter(|i| has_property(recv, &i.to_string()).unwrap_or(false))
10159 .collect();
10160 indices.extend(inherited);
10161 let mut replaced = false;
10162 for i in indices {
10163 if let Ok(v) = get_property(recv, &i.to_string()) {
10164 items[i] = v;
10165 replaced = true;
10166 }
10167 }
10168 replaced
10169}
10170
10171/// The ELIDED positions of array `recv` as a membership set. A dense array —
10172/// which is nearly every array — answers with an empty set after a single
10173/// negative hash probe and allocates nothing.
10174///
10175/// The iteration methods split into two groups, and the split is not a matter of
10176/// taste: the ones spec'd through `HasProperty` (`forEach`, `map`, `filter`,
10177/// `some`, `every`, `reduce`, `indexOf`, `flat`, `sort`) SKIP a hole, while the
10178/// ones spec'd through a bare `Get` (`for…of`, spread, `find`, `includes`,
10179/// `join`, `entries`, `Array.from`) see the `undefined` a hole reads back as.
10180fn hole_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10181 with_host(|h| h.hole_indices(recv)).into_iter().collect()
10182}
10183
10184/// The indices `recv` genuinely has NO property at — the elided ones the
10185/// prototype chain does not supply either.
10186///
10187/// Every array method tests `HasProperty` before deciding to skip a position
10188/// (23.1.3.x, uniformly), and `HasProperty` walks the chain. Testing elision
10189/// alone made an inherited element invisible to all of them: with
10190/// `Array.prototype[1] = 'p'`, `[1,,3].map(v => v)` produced a hole where node
10191/// produces `'p'`, and `flat`/`concat`/`slice`/`sort`/`indexOf` each dropped
10192/// the same position.
10193///
10194/// `hole_set` remains the elision record itself, which is what `splice` moves
10195/// around — that bookkeeping is about the array's OWN storage and must not
10196/// consult the chain.
10197fn absent_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10198 hole_set(recv)
10199 .into_iter()
10200 .filter(|i| !has_property(recv, &i.to_string()).unwrap_or(false))
10201 .collect()
10202}
10203
10204/// The single-index form of [`absent_set`], for the walkers that test one
10205/// position at a time.
10206fn index_absent(recv: &Value, i: usize) -> bool {
10207 with_host(|h| h.is_hole(recv, i)) && !has_property(recv, &i.to_string()).unwrap_or(false)
10208}
10209
10210/// The element count, without copying the elements.
10211fn array_len(recv: &Value) -> usize {
10212 peek(recv, |o| match o {
10213 JsObj::Array(items) => Some(items.len()),
10214 _ => None,
10215 })
10216 .unwrap_or(0)
10217}
10218
10219/// `ArraySpeciesCreate(originalArray, length)` (23.1.3.4) — the constructor an
10220/// array method builds its RESULT with.
10221///
10222/// `map`, `filter`, `slice`, `concat`, `splice`, `flat` and `flatMap` all
10223/// produce an array of the receiver's own species, so on a `class A extends
10224/// Array` the result is an `A`. Every one of them allocated a plain array
10225/// instead, so `A.from([1]).map(x => x) instanceof A` was false.
10226///
10227/// The default `get [Symbol.species]() { return this }` is what makes the
10228/// subclass the species; a class overriding it with `Array` gets a plain array
10229/// back, which is the documented way to opt out.
10230/// Build an array-shaped result through `ctor`, or a plain array when there is
10231/// none to build through.
10232///
10233/// The constructor is called with the LENGTH and the elements written after, as
10234/// 23.1.2.1 and 23.1.3.4 both specify — which is what lets a subclass
10235/// constructor observe the allocation.
10236fn construct_array_like(ctor: Option<Value>, items: Vec<Value>) -> Result<Value, String> {
10237 let Some(ctor) = ctor.filter(|c| {
10238 matches!(
10239 with_host(|h| h.kind_of(c)),
10240 Some(ObjKind::Class) | Some(ObjKind::Func)
10241 )
10242 }) else {
10243 return Ok(with_host(|h| h.new_array(items)));
10244 };
10245 let out = host::construct(&ctor, vec![Value::Float(items.len() as f64)])?;
10246 write_elements(&out, items);
10247 Ok(out)
10248}
10249
10250/// Write `items` into a freshly constructed array-shaped `out`, clearing the
10251/// hole marks the length-only construction left behind.
10252///
10253/// `new A(3)` on `class A extends Array` really does produce three HOLES, and
10254/// the elements written over them stayed marked — so every subclass result of
10255/// `map`/`filter`/`flat` read back as holes: `A.from([1,2,3]).map(x => x * 2)`
10256/// had length 3 and printed `[null,null,null]`, and `0 in` it was false.
10257fn write_elements(out: &Value, items: Vec<Value>) {
10258 with_host(|h| {
10259 h.clear_holes(out);
10260 if let Some(JsObj::Array(dst)) = h.get_mut(out) {
10261 *dst = items;
10262 }
10263 });
10264}
10265
10266fn array_species_create(recv: &Value, items: Vec<Value>) -> Result<Value, String> {
10267 let plain = || with_host(|h| h.new_array(items.clone()));
10268 // Only a subclass instance can have a species of its own: a plain array's
10269 // `constructor` is the `Array` builtin, whose species is `Array`.
10270 // A chain lookup, not `get_property`: an Array receiver resolves its
10271 // properties through the stdlib funnel, which has no `constructor` entry,
10272 // so the read alone reports `undefined` for every subclass instance. A
10273 // Proxy is the exception — it has no property map to walk, and its
10274 // `constructor` comes from the `get` trap, so a proxied subclass array
10275 // produced plain arrays.
10276 let ctor = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
10277 get_property(recv, "constructor").unwrap_or(Value::Undef)
10278 } else {
10279 with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef)
10280 };
10281 if !matches!(
10282 with_host(|h| h.kind_of(&ctor)),
10283 Some(ObjKind::Class) | Some(ObjKind::Func)
10284 ) {
10285 return Ok(plain());
10286 }
10287 // An explicit `@@species` wins; absent one, the constructor itself is the
10288 // species, as the inherited accessor returns `this`.
10289 let species = match get_property(&ctor, "@@species") {
10290 Ok(Value::Undef) => ctor,
10291 Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(plain()),
10292 Ok(s) => s,
10293 Err(_) => ctor,
10294 };
10295 if !matches!(
10296 with_host(|h| h.kind_of(&species)),
10297 Some(ObjKind::Class) | Some(ObjKind::Func)
10298 ) {
10299 return Ok(plain());
10300 }
10301 let out = host::construct(&species, vec![Value::Float(items.len() as f64)])?;
10302 // The constructor is called with the LENGTH, so the elements are written
10303 // afterwards — which is also what lets a subclass constructor observe the
10304 // allocation, as node's does.
10305 write_elements(&out, items);
10306 Ok(out)
10307}
10308
10309fn array_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
10310 array_method_on(recv, recv, name, args)
10311}
10312
10313/// The `Array.prototype` methods that WRITE to their receiver, and so need the
10314/// generic path to copy the result back onto the array-like.
10315const ARRAY_MUTATORS: &[&str] = &[
10316 "push",
10317 "pop",
10318 "shift",
10319 "unshift",
10320 "splice",
10321 "sort",
10322 "reverse",
10323 "fill",
10324 "copyWithin",
10325];
10326
10327/// Run `Array.prototype.<method>` against an array-LIKE (`{0: 'a', length: 1}`,
10328/// a DOM-ish collection, `arguments`).
10329///
10330/// 23.1.3 defines every one of these over `LengthOfArrayLike(O)` and `Get(O, k)`
10331/// rather than over an Array's element vector, so the receiver only has to have
10332/// a `length`. The elements are read out into a temporary Array, the ordinary
10333/// implementation runs on that, and a MUTATING method writes the result back —
10334/// which keeps one implementation of each method rather than a second, generic
10335/// one that could drift from it.
10336///
10337/// An index the receiver does not own is a HOLE in the temporary, so the
10338/// methods that skip holes skip it here too, exactly as `HasProperty` makes them.
10339fn array_generic(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
10340 let len = match get_property(recv, "length") {
10341 Ok(v) => host::to_array_length(&v).unwrap_or(0),
10342 Err(_) => 0,
10343 };
10344 // A STRING receiver owns every index of its length; `has_property` answers
10345 // for objects and reports none of them, which made `[].map.call('abc', f)`
10346 // an array of three holes.
10347 let dense = with_host(|h| h.as_str(recv)).is_some();
10348 let mut items = Vec::with_capacity(len);
10349 let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
10350 for i in 0..len {
10351 let k = i.to_string();
10352 if dense || has_property(recv, &k)? {
10353 items.push(get_property(recv, &k)?);
10354 } else {
10355 holes.insert(i);
10356 items.push(Value::Undef);
10357 }
10358 }
10359 let tmp = with_host(|h| {
10360 let a = h.new_array(items);
10361 h.install_holes(&a, holes);
10362 a
10363 });
10364 let out = array_method_on(&tmp, recv, method, args)?;
10365 if ARRAY_MUTATORS.contains(&method) {
10366 let result = with_host(|h| match h.get(&tmp) {
10367 Some(JsObj::Array(items)) => items.clone(),
10368 _ => Vec::new(),
10369 });
10370 for (i, v) in result.iter().enumerate() {
10371 set_property(recv, &i.to_string(), v.clone())?;
10372 }
10373 set_property(recv, "length", Value::Float(result.len() as f64))?;
10374 }
10375 Ok(out)
10376}
10377
10378/// `Array.prototype.<name>` on `recv`.
10379///
10380/// `this_value` is what a callback receives as its third argument and what a
10381/// mutating method returns — the same object as `recv` for an ordinary array
10382/// call, but the ORIGINAL array-like when `array_generic` runs a method against
10383/// a temporary copy (`Array.prototype.slice.call(arguments)`).
10384fn array_method_on(
10385 recv: &Value,
10386 this_value: &Value,
10387 name: &str,
10388 args: Vec<Value>,
10389) -> Result<Value, String> {
10390 let args = coerce_numeric_args(ARRAY_METHOD_NUMERIC_ARGS, name, args)?;
10391 match name {
10392 "push" => {
10393 // 23.1.3.23 step 4 defines each new element through
10394 // `CreateDataPropertyOrThrow`, so a NON-EXTENSIBLE array refuses it:
10395 // `Object.seal(a)` / `preventExtensions(a)` then `a.push(x)` is a
10396 // TypeError. The elements were appended to the backing vector
10397 // regardless, so sealing an array did not seal it.
10398 if !args.is_empty() && !with_host(|h| h.is_extensible(recv)) {
10399 let at = array_len(recv);
10400 return Err(host::type_error(&format!(
10401 "Cannot add property {at}, object is not extensible"
10402 )));
10403 }
10404 // Step 5 then SETS `length`, so a non-writable one refuses the push
10405 // too — `defineProperty(a, 'length', {writable: false})` makes an
10406 // array append-proof without sealing it.
10407 if !args.is_empty() && !with_host(|h| h.prop_attrs(recv, "length").writable) {
10408 return Err(host::type_error(
10409 "Cannot assign to read only property 'length' of object '[object Array]'",
10410 ));
10411 }
10412 // `push` returns the new length; take it from the same mutable
10413 // borrow rather than copying the array back out to count it.
10414 let len = with_host(|h| {
10415 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10416 items.extend(args.iter().cloned());
10417 items.len()
10418 } else {
10419 0
10420 }
10421 });
10422 Ok(Value::Float(len as f64))
10423 }
10424 "pop" => Ok(with_host(|h| {
10425 let popped = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10426 items.pop().unwrap_or(Value::Undef)
10427 } else {
10428 Value::Undef
10429 };
10430 let len = match h.get(recv) {
10431 Some(JsObj::Array(items)) => items.len(),
10432 _ => 0,
10433 };
10434 h.truncate_holes(recv, len);
10435 popped
10436 })),
10437 "shift" => Ok(with_host(|h| {
10438 let shifted = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10439 if items.is_empty() {
10440 Value::Undef
10441 } else {
10442 items.remove(0)
10443 }
10444 } else {
10445 Value::Undef
10446 };
10447 h.remap_holes(recv, |i| i.checked_sub(1));
10448 shifted
10449 })),
10450 "unshift" => {
10451 with_host(|h| {
10452 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10453 for (i, a) in args.iter().enumerate() {
10454 items.insert(i, a.clone());
10455 }
10456 }
10457 let n = args.len();
10458 h.remap_holes(recv, |i| Some(i + n));
10459 });
10460 Ok(Value::Float(array_len(recv) as f64))
10461 }
10462 "join" => {
10463 let sep = if args.is_empty() || matches!(args[0], Value::Undef) {
10464 ",".to_string()
10465 } else {
10466 arg_to_string(&args, 0)?
10467 };
10468 join_array(recv, &sep)
10469 }
10470 // `Array.prototype.toLocaleString` (23.1.3.32): comma-join the elements'
10471 // OWN `toLocaleString` results, with `null`/`undefined` contributing the
10472 // empty string. It threw `is not a function` — the whole method was
10473 // missing — so `[1234.5, 'x'].toLocaleString()` was unreachable.
10474 "toLocaleString" => {
10475 // Shares `join`'s JoinStack: measured on node v26.7.0, `h=[1]`
10476 // `h.push(h)` makes `h.toLocaleString()` `"1,"`, not a stack overflow.
10477 if !host::join_stack_push(recv) {
10478 return Ok(with_host(|h| h.new_str(String::new())));
10479 }
10480 let items = array_items(recv);
10481 let mut parts: Vec<String> = Vec::with_capacity(items.len());
10482 for it in &items {
10483 if with_host(|h| h.is_nullish(it)) {
10484 parts.push(String::new());
10485 continue;
10486 }
10487 let v = match host::call_method(it, "toLocaleString", Vec::new()) {
10488 Ok(v) => v,
10489 Err(e) => {
10490 host::join_stack_pop();
10491 return Err(e);
10492 }
10493 };
10494 parts.push(with_host(|h| h.str_of(&v)));
10495 }
10496 host::join_stack_pop();
10497 Ok(with_host(|h| h.new_str(parts.join(","))))
10498 }
10499 // `indexOf`/`lastIndexOf` are spec'd through `HasProperty`, so a hole is
10500 // never a match: `[1,,3].indexOf(undefined)` is `-1`, while the
10501 // `Get`-based `includes` reports `true` for the same array.
10502 "indexOf" => {
10503 let target = arg0(&args);
10504 let len = array_len(recv);
10505 let start = search_start(arg_num(&args, 1), len);
10506 let mut idx = None;
10507 for i in start..len {
10508 // 23.1.3.17 steps 8a-8b: HasProperty first, so a hole — and an
10509 // index a mutation has since dropped — is skipped, not compared.
10510 if index_absent(recv, i) || i >= array_len(recv) {
10511 continue;
10512 }
10513 let x = get_property(recv, &i.to_string())?;
10514 if with_host(|h| h.strict_eq(&x, &target)) {
10515 idx = Some(i);
10516 break;
10517 }
10518 }
10519 Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10520 }
10521 "lastIndexOf" => {
10522 let items = array_items(recv);
10523 let holes = absent_set(recv);
10524 let target = arg0(&args);
10525 let from = (args.len() > 1).then(|| arg_num(&args, 1));
10526 let idx = match search_start_last(from, items.len()) {
10527 None => None,
10528 Some(start) => with_host(|h| {
10529 items[..=start]
10530 .iter()
10531 .enumerate()
10532 .rev()
10533 .find(|(i, x)| !holes.contains(i) && h.strict_eq(x, &target))
10534 .map(|(i, _)| i)
10535 }),
10536 };
10537 Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10538 }
10539 "includes" => {
10540 // Array.includes uses SameValueZero: unlike `===`, NaN matches NaN.
10541 // Unlike `indexOf` it has no HasProperty step (23.1.3.16 step 5b), so
10542 // a hole reads as `undefined` and `[,].includes(undefined)` is true.
10543 let target = arg0(&args);
10544 let tnan = matches!(target, Value::Float(f) if f.is_nan());
10545 let len = array_len(recv);
10546 let start = search_start(arg_num(&args, 1), len);
10547 let mut found = false;
10548 for i in start..len {
10549 let x = array_elem_live(recv, i)?;
10550 if (tnan && matches!(x, Value::Float(f) if f.is_nan()))
10551 || with_host(|h| h.strict_eq(&x, &target))
10552 {
10553 found = true;
10554 break;
10555 }
10556 }
10557 Ok(Value::Bool(found))
10558 }
10559 "slice" => {
10560 let items = array_items(recv);
10561 let (lo, hi) = slice_bounds(&args, items.len());
10562 let out = array_species_create(this_value, items[lo..hi].to_vec())?;
10563 with_host(|h| h.copy_holes(recv, &out, |i| (i >= lo && i < hi).then(|| i - lo)));
10564 Ok(out)
10565 }
10566 "concat" => {
10567 let mut out = array_items(recv);
10568 // A hole in either the receiver or a spreadable argument stays a hole
10569 // in the result, at its shifted position.
10570 let mut holes = absent_set(recv);
10571 let mut sources: Vec<(Value, usize)> = Vec::new();
10572 for a in &args {
10573 // `Symbol.isConcatSpreadable` (23.1.3.1) decides whether an
10574 // argument is spread, overriding `IsArray` in BOTH directions:
10575 // a plain array-like opts IN, and an array opts OUT. It was
10576 // never consulted, so an array was always spread and an
10577 // array-like never was.
10578 let flag = get_property(a, "@@isConcatSpreadable").unwrap_or(Value::Undef);
10579 let spread = if matches!(flag, Value::Undef) {
10580 matches!(with_host(|h| h.get(a).cloned()), Some(JsObj::Array(_)))
10581 } else {
10582 with_host(|h| h.truthy(&flag))
10583 };
10584 if !spread {
10585 out.push(a.clone());
10586 continue;
10587 }
10588 match with_host(|h| h.get(a).cloned()) {
10589 // Read off the backing vector rather than through
10590 // `array_items`, so the resolve that does for the receiver
10591 // has to be done here too: 23.1.3.1 step 5.c.iv is a
10592 // `[[Get]]`, and an index with a getter — or an elided one
10593 // the chain supplies — is stale in that vector.
10594 Some(JsObj::Array(mut items)) => {
10595 resolve_index_accessors(a, &mut items);
10596 sources.push((a.clone(), out.len()));
10597 out.extend(items);
10598 }
10599 // An opted-in array-LIKE spreads by its `length` and index
10600 // properties rather than by a backing vector it has none of.
10601 _ => {
10602 let len = get_property(a, "length").unwrap_or(Value::Undef);
10603 let n = with_host(|h| h.to_number(&len));
10604 let n = if n.is_finite() {
10605 n.max(0.0) as usize
10606 } else {
10607 0
10608 };
10609 for i in 0..n {
10610 out.push(get_property(a, &i.to_string()).unwrap_or(Value::Undef));
10611 }
10612 }
10613 }
10614 }
10615
10616 for (src, base) in sources {
10617 holes.extend(
10618 with_host(|h| h.hole_indices(&src))
10619 .into_iter()
10620 .map(|i| i + base),
10621 );
10622 }
10623 let arr = array_species_create(this_value, out)?;
10624 with_host(|h| h.install_holes(&arr, holes));
10625 Ok(arr)
10626 }
10627 "reverse" => {
10628 let len = array_len(recv);
10629 with_host(|h| {
10630 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10631 items.reverse();
10632 }
10633 h.remap_holes(recv, |i| Some(len - 1 - i));
10634 });
10635 Ok(this_value.clone())
10636 }
10637 "fill" => {
10638 // fill(value[, start[, end]]) — negative indices count from the end.
10639 let val = arg0(&args);
10640 let len = array_len(recv) as i64;
10641 let norm =
10642 |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10643 let start = if args.len() >= 2 {
10644 norm(arg_num(&args, 1) as i64)
10645 } else {
10646 0
10647 };
10648 let end = if args.len() >= 3 {
10649 norm(arg_num(&args, 2) as i64)
10650 } else {
10651 len as usize
10652 };
10653 with_host(|h| {
10654 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10655 for it in items.iter_mut().take(end).skip(start) {
10656 *it = val.clone();
10657 }
10658 }
10659 // Every filled position now holds a real value.
10660 h.remap_holes(recv, |i| (i < start || i >= end).then_some(i));
10661 });
10662 Ok(this_value.clone())
10663 }
10664 "copyWithin" => {
10665 // copyWithin(target, start[, end]) — copy a slice within the array.
10666 let items = array_items(recv);
10667 let len = items.len() as i64;
10668 let norm =
10669 |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10670 let target = norm(arg_num(&args, 0) as i64);
10671 let start = if args.len() >= 2 {
10672 norm(arg_num(&args, 1) as i64)
10673 } else {
10674 0
10675 };
10676 let end = if args.len() >= 3 {
10677 norm(arg_num(&args, 2) as i64)
10678 } else {
10679 len as usize
10680 };
10681 let slice: Vec<Value> = items[start..end.max(start)].to_vec();
10682 let copied = slice.len();
10683 // A copied position takes its SOURCE's hole-ness (10.4.2 copyWithin
10684 // deletes the target when the source has no such property);
10685 // everything outside the written range keeps its own.
10686 let src_holes = absent_set(recv);
10687 with_host(|h| {
10688 if let Some(JsObj::Array(a)) = h.get_mut(recv) {
10689 for (k, v) in slice.into_iter().enumerate() {
10690 if target + k < a.len() {
10691 a[target + k] = v;
10692 }
10693 }
10694 }
10695 let len = len as usize;
10696 let mut holes: rustc_hash::FxHashSet<usize> = src_holes
10697 .iter()
10698 .copied()
10699 .filter(|i| *i < target || *i >= (target + copied).min(len))
10700 .collect();
10701 for k in 0..copied {
10702 if target + k < len && src_holes.contains(&(start + k)) {
10703 holes.insert(target + k);
10704 }
10705 }
10706 h.install_holes(recv, holes);
10707 });
10708 Ok(this_value.clone())
10709 }
10710 "at" => {
10711 let items = array_items(recv);
10712 let mut i = arg_num(&args, 0) as i64;
10713 if i < 0 {
10714 i += items.len() as i64;
10715 }
10716 Ok(if i >= 0 && (i as usize) < items.len() {
10717 items[i as usize].clone()
10718 } else {
10719 Value::Undef
10720 })
10721 }
10722 // 23.1.3.21: the callback runs only where `HasProperty` holds, and the
10723 // result array is created with the SAME holes — `[1,,3].map(f)` calls `f`
10724 // twice and yields `[2, <1 empty item>, 6]`.
10725 "map" => {
10726 let holes = absent_set(recv);
10727 let cb = arg0(&args);
10728 // The result keeps the source's LENGTH, so a skipped index still
10729 // occupies a slot; `array_walk` only tells us which ones ran.
10730 let mut out = vec![Value::Undef; array_len(recv)];
10731 array_walk(recv, |i, it| {
10732 let v = host::invoke(
10733 &cb,
10734 vec![it, Value::Float(i as f64), this_value.clone()],
10735 this_arg(&args, 1),
10736 )?;
10737 if i < out.len() {
10738 out[i] = v;
10739 }
10740 Ok(None::<()>)
10741 })?;
10742 let arr = array_species_create(this_value, out)?;
10743 with_host(|h| h.install_holes(&arr, holes));
10744 Ok(arr)
10745 }
10746 "flatMap" => {
10747 let cb = arg0(&args);
10748 let thisarg = this_arg(&args, 1);
10749 let mut out = Vec::new();
10750 array_walk(recv, |i, v| {
10751 let r = host::invoke(
10752 &cb,
10753 vec![v, Value::Float(i as f64), this_value.clone()],
10754 thisarg.clone(),
10755 )?;
10756 match with_host(|h| h.get(&r).cloned()) {
10757 Some(JsObj::Array(inner)) => out.extend(inner),
10758 _ => out.push(r),
10759 }
10760 Ok(None::<()>)
10761 })?;
10762 array_species_create(this_value, out)
10763 }
10764 "filter" => {
10765 let cb = arg0(&args);
10766 let mut out = Vec::new();
10767 array_walk(recv, |i, it| {
10768 let keep = host::invoke(
10769 &cb,
10770 vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10771 this_arg(&args, 1),
10772 )?;
10773 if with_host(|h| h.truthy(&keep)) {
10774 out.push(it);
10775 }
10776 Ok(None::<()>)
10777 })?;
10778 array_species_create(this_value, out)
10779 }
10780 "forEach" => {
10781 let cb = arg0(&args);
10782 array_walk(recv, |i, it| {
10783 host::invoke(
10784 &cb,
10785 vec![it, Value::Float(i as f64), this_value.clone()],
10786 this_arg(&args, 1),
10787 )?;
10788 Ok(None::<()>)
10789 })?;
10790 Ok(Value::Undef)
10791 }
10792 "find" => {
10793 let items = array_items(recv);
10794 let cb = arg0(&args);
10795 for (i, it) in items.iter().enumerate() {
10796 let m = host::invoke(
10797 &cb,
10798 vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10799 this_arg(&args, 1),
10800 )?;
10801 if with_host(|h| h.truthy(&m)) {
10802 return Ok(it.clone());
10803 }
10804 }
10805 Ok(Value::Undef)
10806 }
10807 "findIndex" => {
10808 let items = array_items(recv);
10809 let cb = arg0(&args);
10810 for (i, it) in items.iter().enumerate() {
10811 let m = host::invoke(
10812 &cb,
10813 vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10814 this_arg(&args, 1),
10815 )?;
10816 if with_host(|h| h.truthy(&m)) {
10817 return Ok(Value::Float(i as f64));
10818 }
10819 }
10820 Ok(Value::Float(-1.0))
10821 }
10822 "some" => {
10823 let cb = arg0(&args);
10824 let thisarg = this_arg(&args, 1);
10825 let hit = array_walk(recv, |i, v| {
10826 let m = host::invoke(
10827 &cb,
10828 vec![v, Value::Float(i as f64), this_value.clone()],
10829 thisarg.clone(),
10830 )?;
10831 Ok(with_host(|h| h.truthy(&m)).then_some(()))
10832 })?;
10833 Ok(Value::Bool(hit.is_some()))
10834 }
10835 "every" => {
10836 let cb = arg0(&args);
10837 let failed = array_walk(recv, |i, it| {
10838 let m = host::invoke(
10839 &cb,
10840 vec![it, Value::Float(i as f64), this_value.clone()],
10841 this_arg(&args, 1),
10842 )?;
10843 Ok((!with_host(|h| h.truthy(&m))).then_some(()))
10844 })?;
10845 Ok(Value::Bool(failed.is_none()))
10846 }
10847 "reduce" => {
10848 let items = array_items(recv);
10849 let holes = absent_set(recv);
10850 let cb = arg0(&args);
10851 let acc;
10852 let mut start = 0;
10853 if args.len() >= 2 {
10854 acc = args[1].clone();
10855 } else {
10856 // With no seed the accumulator is the first PRESENT element, so a
10857 // leading run of holes is skipped rather than seeding `undefined`.
10858 match (0..items.len()).find(|i| !holes.contains(i)) {
10859 Some(i) => {
10860 acc = items[i].clone();
10861 start = i + 1;
10862 }
10863 None => {
10864 return Err(host::type_error(
10865 "Reduce of empty array with no initial value",
10866 ))
10867 }
10868 }
10869 }
10870 // Each element is read LIVE at its index, so a callback that
10871 // shrinks the array is observed — the tail is skipped rather than
10872 // folded from a stale snapshot.
10873 let mut cur = acc;
10874 array_walk(recv, |i, it| {
10875 if i < start {
10876 return Ok(None::<()>);
10877 }
10878 cur = host::invoke(
10879 &cb,
10880 vec![
10881 std::mem::replace(&mut cur, Value::Undef),
10882 it,
10883 Value::Float(i as f64),
10884 this_value.clone(),
10885 ],
10886 this_arg(&args, 1),
10887 )?;
10888 Ok(None::<()>)
10889 })?;
10890 Ok(cur)
10891 }
10892 "reduceRight" => {
10893 let cb = arg0(&args);
10894 let n = array_len(recv);
10895 let mut acc;
10896 let mut from = n; // one past the next index to process (walking down)
10897 if args.len() >= 2 {
10898 acc = args[1].clone();
10899 } else {
10900 let holes = absent_set(recv);
10901 match (0..n).rev().find(|i| !holes.contains(i)) {
10902 Some(k) => {
10903 acc = get_property(recv, &k.to_string())?;
10904 from = k;
10905 }
10906 None => {
10907 return Err(host::type_error(
10908 "Reduce of empty array with no initial value",
10909 ))
10910 }
10911 }
10912 }
10913 // `acc` moves into the closure and back out on every step, so it
10914 // lives in an Option the closure can take from and refill.
10915 let mut slot = Some(acc);
10916 array_walk_rev(recv, from, |i, v| {
10917 let prev = slot.take().expect("accumulator is refilled each step");
10918 slot = Some(host::invoke(
10919 &cb,
10920 vec![prev, v, Value::Float(i as f64), this_value.clone()],
10921 None,
10922 )?);
10923 Ok(None::<()>)
10924 })?;
10925 acc = slot.expect("accumulator is refilled each step");
10926 Ok(acc)
10927 }
10928 "findLast" => {
10929 let items = array_items(recv);
10930 let cb = arg0(&args);
10931 for i in (0..items.len()).rev() {
10932 let m = host::invoke(
10933 &cb,
10934 vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
10935 this_arg(&args, 1),
10936 )?;
10937 if with_host(|h| h.truthy(&m)) {
10938 return Ok(items[i].clone());
10939 }
10940 }
10941 Ok(Value::Undef)
10942 }
10943 "findLastIndex" => {
10944 let items = array_items(recv);
10945 let cb = arg0(&args);
10946 for i in (0..items.len()).rev() {
10947 let m = host::invoke(
10948 &cb,
10949 vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
10950 this_arg(&args, 1),
10951 )?;
10952 if with_host(|h| h.truthy(&m)) {
10953 return Ok(Value::Float(i as f64));
10954 }
10955 }
10956 Ok(Value::Float(-1.0))
10957 }
10958 // 23.1.3.30: `SortIndexedProperties` collects only the PRESENT elements,
10959 // and the holes are re-created at the tail — `[3,,1].sort()` is
10960 // `[1, 3, <1 empty item>]` with own keys `['0','1']`.
10961 "sort" => {
10962 let all = array_items(recv);
10963 let holes = absent_set(recv);
10964 let mut items: Vec<Value> = all
10965 .iter()
10966 .enumerate()
10967 .filter(|(i, _)| !holes.contains(i))
10968 .map(|(_, v)| v.clone())
10969 .collect();
10970 sort_values(&mut items, args.first())?;
10971 let present = items.len();
10972 // 23.1.3.30 steps 4-5 write back only the indices BELOW the length
10973 // captured at step 1: `Set` for each sorted element, then `Delete`
10974 // for the holes that followed them. Replacing the whole backing
10975 // vector instead discarded anything the COMPARATOR appended —
10976 // `a.sort((x, y) => { a.push(0); return x - y })` came back at its
10977 // original length with every pushed element gone.
10978 with_host(|h| {
10979 let len = all.len();
10980 if let Some(JsObj::Array(a)) = h.get_mut(recv) {
10981 if a.len() < len {
10982 a.resize(len, Value::Undef);
10983 }
10984 for (i, v) in items.into_iter().enumerate() {
10985 a[i] = v;
10986 }
10987 for slot in a[present..len].iter_mut() {
10988 *slot = Value::Undef;
10989 }
10990 }
10991 h.install_holes(recv, (present..len).collect());
10992 });
10993 Ok(this_value.clone())
10994 }
10995 // ES2023 change-by-copy: sort a fresh copy, leaving the receiver untouched.
10996 "toSorted" => {
10997 let mut items = array_items(recv);
10998 sort_values(&mut items, args.first())?;
10999 Ok(with_host(|h| h.new_array(items)))
11000 }
11001 "toReversed" => {
11002 let mut items = array_items(recv);
11003 items.reverse();
11004 Ok(with_host(|h| h.new_array(items)))
11005 }
11006 "toSpliced" => {
11007 let mut items = array_items(recv);
11008 let len = items.len();
11009 let start = {
11010 let s = arg_num(&args, 0);
11011 if s < 0.0 {
11012 ((len as f64 + s).max(0.0)) as usize
11013 } else {
11014 (s as usize).min(len)
11015 }
11016 };
11017 let delete = if args.len() >= 2 {
11018 (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11019 } else if args.is_empty() {
11020 0
11021 } else {
11022 len - start
11023 };
11024 let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11025 items.splice(start..start + delete, inserts);
11026 Ok(with_host(|h| h.new_array(items)))
11027 }
11028 "with" => {
11029 let mut items = array_items(recv);
11030 let len = items.len() as i64;
11031 let rel = arg_num(&args, 0) as i64;
11032 let idx = if rel < 0 { len + rel } else { rel };
11033 if idx < 0 || idx >= len {
11034 return Err(host::range_error(&format!("Invalid index : {rel}")));
11035 }
11036 items[idx as usize] = args.get(1).cloned().unwrap_or(Value::Undef);
11037 Ok(with_host(|h| h.new_array(items)))
11038 }
11039 "flat" => {
11040 // depth defaults to 1; `Infinity` flattens fully. ToIntegerOrInfinity:
11041 // NaN → 0, otherwise truncate toward zero (negatives act as 0).
11042 let raw = if args.is_empty() {
11043 1.0
11044 } else {
11045 arg_num(&args, 0)
11046 };
11047 let depth = if raw.is_nan() {
11048 0.0
11049 } else if raw.is_infinite() {
11050 raw
11051 } else {
11052 raw.trunc()
11053 };
11054 let mut out = Vec::new();
11055 flatten_into(recv, depth, &mut out)?;
11056 array_species_create(this_value, out)
11057 }
11058 "keys" => {
11059 let n = array_len(recv);
11060 let items: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
11061 Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
11062 }
11063 "values" | "@@iterator" => {
11064 let items = array_items(recv);
11065 Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
11066 }
11067 "entries" => {
11068 let items = array_items(recv);
11069 let pairs: Vec<Value> = items
11070 .into_iter()
11071 .enumerate()
11072 .map(|(i, v)| with_host(|h| h.new_array(vec![Value::Float(i as f64), v])))
11073 .collect();
11074 Ok(with_host(|h| {
11075 h.alloc(JsObj::Iter {
11076 items: pairs,
11077 idx: 0,
11078 })
11079 }))
11080 }
11081 "splice" => array_splice(recv, args),
11082 // `Array.prototype.toString` IS `join()` with the default separator
11083 // (23.1.3.36), so it converts each element with `ToString` too — and
11084 // shares its cycle cut, which is the whole reason it must not call
11085 // `join_parts` directly: `ToString` of a nested array lands back here.
11086 "toString" => join_array(recv, ","),
11087 // An Array inherits from `Object.prototype` too, so the methods it does
11088 // not override resolve there. `[].hasOwnProperty` already read back as a
11089 // function through the property path, but CALLING it landed here and
11090 // threw `is not a function`.
11091 _ if is_object_builtin_method(name) => object_builtin_method(recv, name, args),
11092 _ => Err(host::type_error(&format!("{name} is not a function"))),
11093 }
11094}
11095
11096/// `Array.prototype.join` (23.1.3.18) and, with the default separator,
11097/// `Array.prototype.toString` (23.1.3.36) — one body so both share the cycle
11098/// cut, which is not optional here: `ToString` of an element that is itself an
11099/// array re-enters through `toString`, so guarding only `join` left
11100/// `a=[]; a.push(a); a.join('-')` recursing until the native stack aborted the
11101/// process. On node v26.7.0 that expression is `""`.
11102fn join_array(recv: &Value, sep: &str) -> Result<Value, String> {
11103 if !host::join_stack_push(recv) {
11104 return Ok(with_host(|h| h.new_str(String::new())));
11105 }
11106 // 23.1.3.18 step 6: the length is captured once, then each element is read
11107 // and STRINGIFIED before the next is read. Both halves are observable —
11108 // a getter or a `toString` that shrinks the array is seen by every later
11109 // element, which a read-all-then-convert pass misses.
11110 let parts = (|| -> Result<Vec<String>, String> {
11111 let len = array_len(recv);
11112 let mut out = Vec::with_capacity(len);
11113 for i in 0..len {
11114 let v = array_elem_live(recv, i)?;
11115 out.push(join_parts(std::slice::from_ref(&v))?.remove(0));
11116 }
11117 Ok(out)
11118 })();
11119 host::join_stack_pop();
11120 let s = parts?.join(sep);
11121 Ok(with_host(|h| h.new_str(s)))
11122}
11123
11124/// `Array.prototype.join`'s per-element conversion (23.1.3.18 step 4): a
11125/// `null`/`undefined` element contributes the empty string, every other element
11126/// is `ToString(element)` — which for an object means invoking its `toString`,
11127/// so `[{ toString() { return 'x' } }].join()` is `"x"` and not
11128/// `"[object Object]"`.
11129///
11130/// The all-primitive array — the overwhelmingly common one — is rendered under
11131/// a single host borrow; only an array actually holding an object pays for the
11132/// re-entrant per-element conversion.
11133fn join_parts(items: &[Value]) -> Result<Vec<String>, String> {
11134 let fast = with_host(|h| {
11135 items
11136 .iter()
11137 .map(|x| match x {
11138 Value::Undef => Some(String::new()),
11139 _ if h.is_null(x) => Some(String::new()),
11140 // A SYMBOL element is primitive but has no `ToString`, so it must
11141 // fall through to the fallible path and throw there:
11142 // `[Symbol()].join()` is a TypeError on node v26.7.0.
11143 _ if matches!(h.get(x), Some(JsObj::Symbol { .. })) => None,
11144 _ if host::is_primitive(h, x) => Some(h.str_of(x)),
11145 _ => None,
11146 })
11147 .collect::<Vec<_>>()
11148 });
11149 if fast.iter().all(Option::is_some) {
11150 return Ok(fast.into_iter().flatten().collect());
11151 }
11152 let mut out = Vec::with_capacity(items.len());
11153 for (x, p) in items.iter().zip(fast) {
11154 match p {
11155 Some(s) => out.push(s),
11156 None => {
11157 let s = host::to_string_value(x)?;
11158 out.push(with_host(|h| h.str_of(&s)));
11159 }
11160 }
11161 }
11162 Ok(out)
11163}
11164
11165/// In-place sort of `items` (shared by `sort` and `toSorted`). Stable merge
11166/// sort — O(n log n) comparisons — with the fallible JS comparator called from
11167/// the merge step; default order is by the string form of each element.
11168/// Propagates a comparator error.
11169///
11170/// This was an insertion sort, which is O(n²): sorting 200k numbers with a
11171/// comparator did not finish inside 120s (node v26.7.0: 70ms), and each
11172/// doubling of the input quadrupled the time — 1k/2k/4k/8k/16k measured at
11173/// 0.21/0.81/3.39/12.94/51.36s. The comparator contract is unchanged; only the
11174/// number of times it is called is.
11175pub(crate) fn sort_values(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11176 // 23.1.3.30 step 1: a comparator that is neither `undefined` nor callable is
11177 // rejected BEFORE any comparison runs. `[2,1].sort(null)` was reaching the
11178 // invoke path and reporting the generic `null is not a function`.
11179 let cmp = match cmp {
11180 Some(Value::Undef) => None,
11181 Some(v) if !with_host(|h| host::is_callable(h, v)) => {
11182 // V8 renders the offending value with `NoSideEffectsToString`, not
11183 // with `util.inspect`: a string appears bare (`: x`) rather than
11184 // quoted, and an array is `[object Array]` rather than `[ 1, 2 ]`.
11185 let shown = no_side_effects_string(v);
11186 return Err(host::type_error(&format!(
11187 "The comparison function must be either a function or undefined: {shown}"
11188 )));
11189 }
11190 other => other,
11191 };
11192 // 23.1.3.30.1 SortIndexedProperties: `undefined` is never handed to the
11193 // comparator — it sorts to the end after the defined values are ordered.
11194 // `[3,undefined,1].sort((x,y)=>x-y)` is `[1,3,undefined]` with ONE call on
11195 // node v26.7.0; the insertion sort called the comparator twice, on
11196 // `undefined`, and left `[3,undefined,1]`. Every element passed over here
11197 // is `undefined`, so swapping keeps the defined values in input order.
11198 let mut defined = 0;
11199 for i in 0..items.len() {
11200 if !matches!(items[i], Value::Undef) {
11201 items.swap(defined, i);
11202 defined += 1;
11203 }
11204 }
11205 merge_sort(&mut items[..defined], cmp)
11206}
11207
11208/// One SortCompare: `> 0` means `b` sorts before `a`. A comparator result runs
11209/// through ToNumber, so a NaN (or a comparator returning `undefined`) is not
11210/// `> 0` and the pair keeps its input order.
11211fn sort_compare(a: &Value, b: &Value, cmp: Option<&Value>) -> Result<f64, String> {
11212 match cmp {
11213 Some(cb) => {
11214 let v = host::invoke(cb, vec![a.clone(), b.clone()], None)?;
11215 Ok(with_host(|h| h.to_number(&v)))
11216 }
11217 None => {
11218 // 23.1.3.30.2 SortCompare with no comparator: compare the ToString
11219 // of each element by CODE UNIT (`utf16::cmp_units`), which differs
11220 // from Rust's `String` order off the BMP.
11221 let x = with_host(|h| h.str_of(a));
11222 let y = with_host(|h| h.str_of(b));
11223 if crate::utf16::cmp_units(&x, &y) == std::cmp::Ordering::Greater {
11224 Ok(1.0)
11225 } else {
11226 Ok(-1.0)
11227 }
11228 }
11229 }
11230}
11231
11232/// Bottom-up stable merge sort. Bottom-up rather than recursive so a large
11233/// array cannot walk the native stack the JS comparator also runs on, and the
11234/// two buffers are swapped each pass instead of copied back.
11235fn merge_sort(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11236 let n = items.len();
11237 if n < 2 {
11238 return Ok(());
11239 }
11240 let mut src = items.to_vec();
11241 let mut dst = src.clone();
11242 let mut width = 1;
11243 while width < n {
11244 let mut lo = 0;
11245 while lo < n {
11246 let mid = (lo + width).min(n);
11247 let hi = (lo + 2 * width).min(n);
11248 merge(&src[lo..mid], &src[mid..hi], &mut dst[lo..hi], cmp)?;
11249 lo = hi;
11250 }
11251 std::mem::swap(&mut src, &mut dst);
11252 width *= 2;
11253 }
11254 items.clone_from_slice(&src);
11255 Ok(())
11256}
11257
11258/// Merge two sorted runs into `out`. Ties take from `left` first, which is what
11259/// makes the sort stable — `[{k:1},{k:0},{k:1},{k:0}].sort((x,y)=>x.k-y.k)`
11260/// keeps the two `k:0` entries in input order, as node does.
11261fn merge(
11262 left: &[Value],
11263 right: &[Value],
11264 out: &mut [Value],
11265 cmp: Option<&Value>,
11266) -> Result<(), String> {
11267 let (mut i, mut j, mut k) = (0, 0, 0);
11268 while i < left.len() && j < right.len() {
11269 if sort_compare(&left[i], &right[j], cmp)? > 0.0 {
11270 out[k] = right[j].clone();
11271 j += 1;
11272 } else {
11273 out[k] = left[i].clone();
11274 i += 1;
11275 }
11276 k += 1;
11277 }
11278 for v in left[i..].iter().chain(&right[j..]) {
11279 out[k] = v.clone();
11280 k += 1;
11281 }
11282 Ok(())
11283}
11284
11285/// Recursively flatten `items` up to `depth` levels into `out`. `depth` is an
11286/// f64 so `Infinity` (full flatten) and finite counts share one path.
11287///
11288/// `flat` has NO cycle cut — unlike `join`, V8 lets it run out of stack, and
11289/// `a=[1]; a.push(a); a.flat(Infinity)` is `RangeError: Maximum call stack size
11290/// exceeded` on node v26.7.0. That is reproduced by checking the same native
11291/// stack floor the VM does, so the answer is a catchable error rather than the
11292/// `fatal runtime error: stack overflow` abort this used to produce.
11293/// `FlattenIntoArray` (23.1.3.13.1). Takes the source ARRAY rather than its
11294/// elements because each level tests `HasProperty` before recursing, so a hole
11295/// contributes nothing at any depth: `[1,,3].flat()` is the dense `[1, 3]`.
11296fn flatten_into(src: &Value, depth: f64, out: &mut Vec<Value>) -> Result<(), String> {
11297 if host::stack_exhausted() {
11298 return Err(host::stack_overflow_error());
11299 }
11300 let items = array_items(src);
11301 let holes = absent_set(src);
11302 for (i, it) in items.into_iter().enumerate() {
11303 if holes.contains(&i) {
11304 continue;
11305 }
11306 let nested = depth > 0.0 && with_host(|h| h.kind_of(&it)) == Some(ObjKind::Array);
11307 if nested {
11308 flatten_into(&it, depth - 1.0, out)?;
11309 } else {
11310 out.push(it);
11311 }
11312 }
11313 Ok(())
11314}
11315
11316fn array_splice(recv: &Value, args: Vec<Value>) -> Result<Value, String> {
11317 let len = array_len(recv);
11318 let start = {
11319 let s = arg_num(&args, 0);
11320 if s < 0.0 {
11321 ((len as f64 + s).max(0.0)) as usize
11322 } else {
11323 (s as usize).min(len)
11324 }
11325 };
11326 let delete = if args.len() >= 2 {
11327 (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11328 } else {
11329 len - start
11330 };
11331 let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11332 let inserted = inserts.len();
11333 // The receiver's holes shift by (inserted - deleted) past the cut, and the
11334 // ones inside the cut move into the RETURNED array at their offset there.
11335 let holes = hole_set(recv);
11336 let removed = with_host(|h| {
11337 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
11338 let removed: Vec<Value> = items.splice(start..start + delete, inserts).collect();
11339 removed
11340 } else {
11341 Vec::new()
11342 }
11343 });
11344 let spliced = with_host(|h| {
11345 h.install_holes(
11346 recv,
11347 holes
11348 .iter()
11349 .filter_map(|&i| {
11350 if i < start {
11351 Some(i)
11352 } else if i < start + delete {
11353 None
11354 } else {
11355 Some(i - delete + inserted)
11356 }
11357 })
11358 .collect(),
11359 );
11360 (removed, holes.clone())
11361 });
11362 // The REMOVED elements come back as an array of the receiver's species
11363 // (23.1.3.31 step 8), so a subclass gets one of its own kind.
11364 let (removed, holes) = spliced;
11365 let out = array_species_create(recv, removed)?;
11366 with_host(|h| {
11367 h.install_holes(
11368 &out,
11369 holes
11370 .iter()
11371 .filter(|&&i| i >= start && i < start + delete)
11372 .map(|&i| i - start)
11373 .collect(),
11374 );
11375 });
11376 Ok(out)
11377}
11378
11379fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
11380 let norm = |v: f64| -> usize {
11381 if v < 0.0 {
11382 ((len as f64 + v).max(0.0)) as usize
11383 } else {
11384 (v as usize).min(len)
11385 }
11386 };
11387 let lo = if args.is_empty() || matches!(args[0], Value::Undef) {
11388 0
11389 } else {
11390 norm(arg_num(args, 0))
11391 };
11392 let hi = if args.len() < 2 || matches!(args[1], Value::Undef) {
11393 len
11394 } else {
11395 norm(arg_num(args, 1))
11396 };
11397 // A start at or past the end (`'World'.slice(2, 1)`) yields the empty range,
11398 // never a reversed one: JS `slice` clamps `end` up to `start`.
11399 (lo, hi.max(lo))
11400}
11401
11402/// The argument positions each `String.prototype` method coerces with
11403/// `ToNumber` rather than `ToString`. Everything not listed is a string
11404/// position — which matters only for a SYMBOL argument, the one value both
11405/// conversions refuse, and refuse with different wording.
11406///
11407/// Measured per method and per position: `'x'.indexOf(sym)` reports the STRING
11408/// message and `'x'.indexOf('a', sym)` the NUMBER one, and `padStart` is the
11409/// pair the other way round (a length then a pad string).
11410const STRING_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11411 ("at", &[0]),
11412 ("charAt", &[0]),
11413 ("charCodeAt", &[0]),
11414 ("codePointAt", &[0]),
11415 ("endsWith", &[1]),
11416 ("includes", &[1]),
11417 ("indexOf", &[1]),
11418 ("lastIndexOf", &[1]),
11419 ("padEnd", &[0]),
11420 ("padStart", &[0]),
11421 ("repeat", &[0]),
11422 ("slice", &[0, 1]),
11423 ("split", &[1]),
11424 ("startsWith", &[1]),
11425 ("substr", &[0, 1]),
11426 ("substring", &[0, 1]),
11427];
11428
11429/// Reject a SYMBOL argument before any string method coerces it. 7.1.17 and
11430/// 7.1.4 both refuse one, so `'x'.padStart(3, sym)` is a TypeError where this
11431/// rendered `Symbol(d)` into the result — silently, which is the shape of
11432/// mistake that makes a symbol key leak into text.
11433fn reject_symbol_args(name: &str, args: &[Value]) -> Result<(), String> {
11434 let numeric = STRING_METHOD_NUMERIC_ARGS
11435 .iter()
11436 .find(|(m, _)| *m == name)
11437 .map(|(_, ps)| *ps)
11438 .unwrap_or(&[]);
11439 for (i, a) in args.iter().enumerate() {
11440 if with_host(|h| matches!(h.get(a), Some(JsObj::Symbol { .. }))) {
11441 let kind = if numeric.contains(&i) {
11442 "number"
11443 } else {
11444 "string"
11445 };
11446 return Err(host::type_error(&format!(
11447 "Cannot convert a Symbol value to a {kind}"
11448 )));
11449 }
11450 }
11451 Ok(())
11452}
11453
11454/// Coerce a string method's arguments the way 22.1.3.x does, BEFORE any arm
11455/// reads them: a numeric position through `ToNumber`, every other through
11456/// `ToString`. Both run a user `valueOf`/`toString`, and none of them ran —
11457/// `'x'.padStart({valueOf: () => 3})` produced `"x"` and
11458/// `'x'.concat({toString: () => 'y'})` produced `"x[object Object]"`.
11459///
11460/// The positions that must NOT be coerced are the ones with their own protocol:
11461/// a RegExp or a `Symbol.replace`/`split`/`match`/`search` carrier at position
11462/// 0 of the method that honours it, and a callable REPLACEMENT at position 1 of
11463/// `replace`/`replaceAll`. Each of those already has a path that handles the
11464/// value as an object, and stringifying it first would take that path away.
11465/// The argument positions each `Array.prototype` method coerces with
11466/// `ToNumber` (23.1.3.x). Everything not listed is a VALUE position and must be
11467/// left alone: `fill`'s first argument, `with`'s second and `splice`'s items
11468/// are stored as given, and `indexOf`/`includes` compare their first argument
11469/// without converting it.
11470const ARRAY_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11471 ("at", &[0]),
11472 ("copyWithin", &[0, 1, 2]),
11473 ("fill", &[1, 2]),
11474 ("flat", &[0]),
11475 ("includes", &[1]),
11476 ("indexOf", &[1]),
11477 ("lastIndexOf", &[1]),
11478 ("slice", &[0, 1]),
11479 ("splice", &[0, 1]),
11480 ("toSpliced", &[0, 1]),
11481 ("with", &[0]),
11482];
11483
11484/// The same for `Number.prototype`. `toLocaleString` takes a LOCALE, not a
11485/// number, and is deliberately absent.
11486const NUMBER_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11487 ("toExponential", &[0]),
11488 ("toFixed", &[0]),
11489 ("toPrecision", &[0]),
11490 ("toString", &[0]),
11491];
11492
11493/// Replace the listed argument positions with their `ToNumber` value, running a
11494/// user `valueOf` and propagating a throw from it. Every one of these read the
11495/// argument with an INFALLIBLE conversion that does no `ToPrimitive` at all, so
11496/// `[1,2,3].slice({valueOf: () => 1})` sliced from 0 and `(1.234).toFixed(obj)`
11497/// was a RangeError.
11498/// `ToNumber(args[i])`, running a user `valueOf` and propagating its throw.
11499fn to_number_arg(args: &[Value], i: usize) -> Result<f64, String> {
11500 let v = args.get(i).cloned().unwrap_or(Value::Undef);
11501 let p = host::to_primitive(&v, "number")?;
11502 Ok(with_host(|h| h.to_number(&p)))
11503}
11504
11505fn coerce_numeric_args(
11506 table: &[(&str, &[usize])],
11507 name: &str,
11508 mut args: Vec<Value>,
11509) -> Result<Vec<Value>, String> {
11510 let Some((_, positions)) = table.iter().find(|(m, _)| *m == name) else {
11511 return Ok(args);
11512 };
11513 for &i in *positions {
11514 let Some(a) = args.get(i) else { continue };
11515 if matches!(a, Value::Undef) {
11516 continue;
11517 }
11518 let p = host::to_primitive(a, "number")?;
11519 args[i] = Value::Float(with_host(|h| h.to_number(&p)));
11520 }
11521 Ok(args)
11522}
11523
11524/// `RegExpCreate(v, flags)` — the regexp a string method builds from a
11525/// non-RegExp argument. An empty/absent argument makes the empty pattern, which
11526/// matches at position 0.
11527fn regexp_from_arg(v: &Value, flags: &str) -> Result<Value, String> {
11528 let src = if matches!(v, Value::Undef) {
11529 String::new()
11530 } else {
11531 with_host(|h| h.str_of(v))
11532 };
11533 let fv = with_host(|h| h.new_str(flags.to_string()));
11534 let sv = with_host(|h| h.new_str(src));
11535 regexp_ctor(&[sv, fv])
11536}
11537
11538fn coerce_string_args(name: &str, args: Vec<Value>) -> Result<Vec<Value>, String> {
11539 let numeric = STRING_METHOD_NUMERIC_ARGS
11540 .iter()
11541 .find(|(m, _)| *m == name)
11542 .map(|(_, ps)| *ps)
11543 .unwrap_or(&[]);
11544 let protocol = match name {
11545 "replace" | "replaceAll" => Some("@@replace"),
11546 "split" => Some("@@split"),
11547 "match" => Some("@@match"),
11548 "matchAll" => Some("@@matchAll"),
11549 "search" => Some("@@search"),
11550 // These three do not CONSUME `Symbol.match`, they reject a value that
11551 // carries it (22.1.3.7/23/24 step 3 — `IsRegExp`). Exempting it keeps
11552 // the object intact so that check still sees one; stringifying first
11553 // turned the TypeError into an ordinary search.
11554 "startsWith" | "endsWith" | "includes" => Some("@@match"),
11555 _ => None,
11556 };
11557 let mut out = Vec::with_capacity(args.len());
11558 for (i, a) in args.into_iter().enumerate() {
11559 if matches!(a, Value::Undef) {
11560 out.push(a);
11561 continue;
11562 }
11563 if numeric.contains(&i) {
11564 let p = host::to_primitive(&a, "number")?;
11565 out.push(Value::Float(with_host(|h| h.to_number(&p))));
11566 continue;
11567 }
11568 // The IsRegExp trio tests `Symbol.match` for TRUTHINESS (7.2.8 step 2),
11569 // not for presence: an object carrying `[Symbol.match]: false` is NOT a
11570 // regexp and coerces like anything else. The consuming protocols use
11571 // `GetMethod`, which additionally requires a callable.
11572 let is_regexp_like = matches!(name, "startsWith" | "endsWith" | "includes");
11573 let carries = |p: &str| match host::protocol_lookup(&a, p) {
11574 Ok(Some(m)) => {
11575 if is_regexp_like {
11576 with_host(|h| h.truthy(&m))
11577 } else {
11578 with_host(|h| host::is_callable(h, &m))
11579 }
11580 }
11581 _ => false,
11582 };
11583 let exempt = with_host(|h| matches!(h.get(&a), Some(JsObj::RegExp(_))))
11584 || (i == 0 && protocol.is_some_and(carries))
11585 || (i == 1
11586 && matches!(name, "replace" | "replaceAll")
11587 && with_host(|h| host::is_callable(h, &a)));
11588 if exempt {
11589 out.push(a);
11590 continue;
11591 }
11592 out.push(host::to_string_value(&a)?);
11593 }
11594 Ok(out)
11595}
11596
11597fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
11598 reject_symbol_args(name, &args)?;
11599 let args = coerce_string_args(name, args)?;
11600 // Every index-bearing method below counts UTF-16 code units, so they all
11601 // work off this one decoding rather than off `s.chars()` (code points),
11602 // which agrees only on the BMP. `@@iterator` is the deliberate exception.
11603 let u = crate::utf16::Units::of(s);
11604 match name {
11605 // `for…of` / spread over a string iterates CODE POINTS, not code units:
11606 // `[..."𝒳"]` is one element in node even though `"𝒳".length` is 2. This
11607 // is the one string operation that is specified in chars, so it stays
11608 // on `s.chars()` on purpose — do not "fix" it to match the others.
11609 "@@iterator" => {
11610 let items: Vec<Value> = s.chars().map(|c| new_s(c.to_string())).collect();
11611 Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
11612 }
11613 "toUpperCase" => Ok(new_s(s.to_uppercase())),
11614 "toLowerCase" => Ok(new_s(s.to_lowercase())),
11615 // `toLocaleUpperCase`/`toLocaleLowerCase` (22.1.3.26/22.1.3.24) differ
11616 // from the plain forms only for the locale-specific mappings (Turkish
11617 // dotless i, Lithuanian accents); with no locale argument they are the
11618 // Unicode Default Case Conversion, which is exactly `to_uppercase`/
11619 // `to_lowercase`. They threw `is not a function` before, so the common
11620 // no-argument call — the only form this runtime can answer, since it
11621 // carries no ICU — failed outright rather than agreeing with node.
11622 // A locale ARGUMENT is accepted and ignored; `'I'.toLocaleLowerCase('tr')`
11623 // is `'i'` here and `'ı'` in node.
11624 // `String.prototype.toLocaleString` (22.1.3.27) is `toString` — a string
11625 // has no locale rendering. Missing it made an ARRAY of strings fail too,
11626 // since `Array.prototype.toLocaleString` invokes it per element.
11627 "toLocaleString" => Ok(new_s(s.to_string())),
11628 "toLocaleUpperCase" => Ok(new_s(s.to_uppercase())),
11629 "toLocaleLowerCase" => Ok(new_s(s.to_lowercase())),
11630 // Locale comparison (ASCII approximation of ICU collation): primary by
11631 // case-folded order, then lowercase sorts before uppercase at a tie.
11632 "localeCompare" => {
11633 let other = with_host(|h| h.str_of(&arg0(&args)));
11634 let (la, lb) = (s.to_lowercase(), other.to_lowercase());
11635 let r = match la.cmp(&lb) {
11636 std::cmp::Ordering::Less => -1.0,
11637 std::cmp::Ordering::Greater => 1.0,
11638 std::cmp::Ordering::Equal => {
11639 let mut t = 0.0;
11640 for (ca, cb) in s.chars().zip(other.chars()) {
11641 if ca != cb {
11642 t = if ca.is_lowercase() { -1.0 } else { 1.0 };
11643 break;
11644 }
11645 }
11646 t
11647 }
11648 };
11649 Ok(Value::Float(r))
11650 }
11651 // `String.prototype.normalize` (22.1.3.15) — real UAX-15 normalization.
11652 //
11653 // This used to return the receiver unchanged and only validate the FORM
11654 // argument, which made every one of the four forms a no-op: `"Å"` (NFC,
11655 // one code point) and `"Å"` (NFD, two) stayed distinct under
11656 // `.normalize()`, so the standard way to compare Unicode text for
11657 // canonical equivalence silently answered `false`, and `NFKC` never
11658 // folded a compatibility character (`"fi"` stayed one code point instead
11659 // of becoming `"fi"`). The tables come from `unicode-normalization`.
11660 "normalize" => {
11661 use unicode_normalization::UnicodeNormalization;
11662 let form = match args.first() {
11663 Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
11664 _ => "NFC".to_string(),
11665 };
11666 let out = match form.as_str() {
11667 "NFC" => s.nfc().collect::<String>(),
11668 "NFD" => s.nfd().collect::<String>(),
11669 "NFKC" => s.nfkc().collect::<String>(),
11670 "NFKD" => s.nfkd().collect::<String>(),
11671 _ => {
11672 return Err(host::range_error(
11673 "The normalization form should be one of NFC, NFD, NFKC, NFKD.",
11674 ))
11675 }
11676 };
11677 Ok(new_s(out))
11678 }
11679 // ES2024 well-formedness (22.1.3.9 / 22.1.3.29). A `String` here is a
11680 // Rust `String`, whose `char` type EXCLUDES `U+D800..=U+DFFF`, so every
11681 // value this runtime can hold is well-formed by construction and
11682 // `toWellFormed` has nothing to replace. Both answers are therefore
11683 // exact for every string that survives storage; the one case node
11684 // answers differently is a surrogate half extracted by `charAt`/`slice`,
11685 // which is already `U+FFFD` here — the documented lone-surrogate
11686 // boundary in `utf16`, not a separate gap.
11687 "isWellFormed" => Ok(Value::Bool(true)),
11688 "toWellFormed" => Ok(new_s(s.to_string())),
11689 // The JS `WhiteSpace` set, not Rust's — they differ on `U+FEFF`.
11690 "trim" => Ok(new_s(crate::utf16::js_trim(s).to_string())),
11691 "trimStart" => Ok(new_s(crate::utf16::js_trim_start(s).to_string())),
11692 "trimEnd" => Ok(new_s(crate::utf16::js_trim_end(s).to_string())),
11693 "toString" | "valueOf" => Ok(new_s(s.to_string())),
11694 "charAt" => {
11695 let at = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit_str(i));
11696 Ok(new_s(at.unwrap_or_default()))
11697 }
11698 "at" => {
11699 let n = arg_num(&args, 0);
11700 // A negative position counts back from the end; `NaN` is 0. An
11701 // infinite position is out of range in either direction.
11702 let i = if n.is_nan() {
11703 Some(0i64)
11704 } else if n.is_finite() {
11705 let i = n.trunc() as i64;
11706 Some(if i < 0 { i + u.len() as i64 } else { i })
11707 } else {
11708 None
11709 };
11710 match i
11711 .and_then(|i| usize::try_from(i).ok())
11712 .and_then(|i| u.unit_str(i))
11713 {
11714 Some(c) => Ok(new_s(c)),
11715 None => Ok(Value::Undef),
11716 }
11717 }
11718 // `charCodeAt` reports the bare code UNIT — the high surrogate of an
11719 // astral character, not the character. `codePointAt` looks ahead one
11720 // unit and reports the whole scalar when the pair is well formed. They
11721 // agree everywhere on the BMP, which is why they used to share an arm.
11722 // They also disagree OUT of range: `charCodeAt` yields `NaN` while
11723 // `codePointAt` yields `undefined` (measured on node v26.7.0).
11724 "charCodeAt" => {
11725 let unit = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit(i));
11726 Ok(Value::Float(unit.map(f64::from).unwrap_or(f64::NAN)))
11727 }
11728 "codePointAt" => match unit_pos(arg_num(&args, 0)).and_then(|i| u.code_point(i)) {
11729 Some(cp) => Ok(Value::Float(f64::from(cp))),
11730 None => Ok(Value::Undef),
11731 },
11732 // The search quartet all honor their optional position argument.
11733 // `"a&b&c".indexOf("&", 2)` must be 3, not 1 — body-parser's
11734 // parameterCount walks a query string with exactly that call.
11735 "indexOf" => {
11736 let needle = needle_units(&args);
11737 let from = clamp_pos(arg_num(&args, 1), u.len());
11738 Ok(Value::Float(
11739 search_from(u.as_slice(), needle.as_slice(), from)
11740 .map(|i| i as f64)
11741 .unwrap_or(-1.0),
11742 ))
11743 }
11744 "lastIndexOf" => {
11745 let needle = needle_units(&args);
11746 // An absent or NaN position means "search the whole string".
11747 let n = arg_num(&args, 1);
11748 let upto = if n.is_nan() {
11749 u.len()
11750 } else {
11751 clamp_pos(n, u.len())
11752 };
11753 Ok(Value::Float(
11754 search_last(u.as_slice(), needle.as_slice(), upto)
11755 .map(|i| i as f64)
11756 .unwrap_or(-1.0),
11757 ))
11758 }
11759 // 22.1.3.7/22.1.3.23/22.1.3.14 step 2: these three reject a REGEXP
11760 // argument outright, and `IsRegExp` is what decides — so an object
11761 // advertising `Symbol.match` is rejected too. None of them checked.
11762 "startsWith" | "endsWith" | "includes" if is_regexp_arg(&arg0(&args)) => {
11763 Err(host::type_error(&format!(
11764 "First argument to String.prototype.{name} must not be a regular expression"
11765 )))
11766 }
11767 "includes" => {
11768 let needle = needle_units(&args);
11769 let from = clamp_pos(arg_num(&args, 1), u.len());
11770 Ok(Value::Bool(
11771 search_from(u.as_slice(), needle.as_slice(), from).is_some(),
11772 ))
11773 }
11774 "startsWith" => {
11775 let needle = needle_units(&args);
11776 let from = clamp_pos(arg_num(&args, 1), u.len());
11777 Ok(Value::Bool(
11778 u.as_slice()[from..].starts_with(needle.as_slice()),
11779 ))
11780 }
11781 "endsWith" => {
11782 let needle = needle_units(&args);
11783 // The 2nd argument is where the string is treated as ENDING.
11784 let end = if args.len() < 2 || matches!(args[1], Value::Undef) {
11785 u.len()
11786 } else {
11787 clamp_pos(arg_num(&args, 1), u.len())
11788 };
11789 Ok(Value::Bool(
11790 u.as_slice()[..end].ends_with(needle.as_slice()),
11791 ))
11792 }
11793 "slice" => {
11794 let (lo, hi) = slice_bounds(&args, u.len());
11795 Ok(new_s(u.slice(lo, hi)))
11796 }
11797 "substring" => {
11798 let mut a = arg_num(&args, 0).max(0.0) as usize;
11799 let mut b = if args.len() < 2 || matches!(args[1], Value::Undef) {
11800 u.len()
11801 } else {
11802 (arg_num(&args, 1).max(0.0) as usize).min(u.len())
11803 };
11804 a = a.min(u.len());
11805 if a > b {
11806 std::mem::swap(&mut a, &mut b);
11807 }
11808 Ok(new_s(u.slice(a, b)))
11809 }
11810 "substr" => {
11811 // A negative start counts from the end: max(len + start, 0).
11812 let len = u.len() as i64;
11813 let mut start = arg_num(&args, 0) as i64;
11814 if start < 0 {
11815 start = (len + start).max(0);
11816 }
11817 let start = (start as usize).min(u.len());
11818 let count = if args.len() >= 2 {
11819 arg_num(&args, 1).max(0.0) as usize
11820 } else {
11821 u.len()
11822 };
11823 let end = start.saturating_add(count).min(u.len());
11824 Ok(new_s(u.slice(start, end)))
11825 }
11826 "repeat" => {
11827 let n = arg_num(&args, 0);
11828 // `RangeError`, not `TypeError`, and the count is named:
11829 // `"x".repeat(-1)` is `RangeError: Invalid count value: -1`.
11830 if n < 0.0 || !n.is_finite() {
11831 return Err(host::range_error(&format!(
11832 "Invalid count value: {}",
11833 host::fmt_number(n)
11834 )));
11835 }
11836 // The PRODUCT is what V8 bounds, so `''.repeat(2**53)` is legal (and
11837 // `''`) while `'ab'.repeat(268435445)` is not: measured on node
11838 // v26.7.0, `'ab'.repeat(268435444).length` is 536870888 and one more
11839 // is `RangeError: Invalid string length`.
11840 if n * crate::utf16::len(s) as f64 > host::MAX_STRING_LENGTH as f64 {
11841 return Err(host::invalid_string_length());
11842 }
11843 Ok(new_s(s.repeat(n as usize)))
11844 }
11845 "concat" => {
11846 let mut out = s.to_string();
11847 for a in &args {
11848 out.push_str(&with_host(|h| h.str_of(a)));
11849 }
11850 Ok(new_s(out))
11851 }
11852 "padStart" => Ok(new_s(pad(s, &args, true)?)),
11853 "padEnd" => Ok(new_s(pad(s, &args, false)?)),
11854 // Regex-taking string methods: dispatch to the regexp module when the
11855 // argument is a RegExp; otherwise keep the plain-string behavior.
11856 // 22.1.3.20 step 2.a: `replaceAll` validates the `g` flag BEFORE it
11857 // consults `Symbol.replace`, so a non-global regexp is a TypeError even
11858 // though a RegExp does define that method. Delegating first skipped the
11859 // check and silently did a single replacement.
11860 "replaceAll"
11861 if is_regexp_arg(&arg0(&args))
11862 && !with_host(
11863 |h| matches!(h.get(&arg0(&args)), Some(JsObj::RegExp(r)) if r.global),
11864 ) =>
11865 {
11866 Err(host::type_error(
11867 "String.prototype.replaceAll called with a non-global RegExp argument",
11868 ))
11869 }
11870 "match" | "matchAll" | "search" | "split" | "replace" | "replaceAll"
11871 if symbol_protocol(
11872 &arg0(&args),
11873 match name {
11874 "match" => "@@match",
11875 "matchAll" => "@@matchAll",
11876 "search" => "@@search",
11877 "split" => "@@split",
11878 _ => "@@replace",
11879 },
11880 )
11881 .is_some() =>
11882 {
11883 let sym = match name {
11884 "match" => "@@match",
11885 "matchAll" => "@@matchAll",
11886 "search" => "@@search",
11887 "split" => "@@split",
11888 _ => "@@replace",
11889 };
11890 let f = symbol_protocol(&arg0(&args), sym).expect("guard checked");
11891 let sv = with_host(|h| h.new_str(s.to_string()));
11892 let mut rest = vec![sv];
11893 rest.extend(args.iter().skip(1).cloned());
11894 host::invoke(&f, rest, Some(arg0(&args)))
11895 }
11896 // 22.1.3.13/14: a non-RegExp argument is turned INTO one
11897 // (`RegExpCreate(regexp, …)`), so `'abc'.match('b')` matches. It
11898 // answered `null` for every string argument, which reads as "no match"
11899 // — the one answer a caller cannot tell from a real failure.
11900 // `matchAll` builds its with `g`, which 22.1.3.14 requires.
11901 "match" => {
11902 let a = arg0(&args);
11903 let re = if is_regexp_arg(&a) {
11904 a
11905 } else {
11906 regexp_from_arg(&a, "")?
11907 };
11908 crate::regexp::str_match(s, &re)
11909 }
11910 "matchAll" => {
11911 let a = arg0(&args);
11912 let re = if is_regexp_arg(&a) {
11913 a
11914 } else {
11915 regexp_from_arg(&a, "g")?
11916 };
11917 crate::regexp::str_match_all(s, &re)
11918 }
11919 "search" => {
11920 if is_regexp_arg(&arg0(&args)) {
11921 crate::regexp::str_search(s, &arg0(&args))
11922 } else {
11923 // 22.1.3.17 builds a RegExp from the argument, so a
11924 // METACHARACTER matches as one: `'a.c'.search('.')` is 0, not
11925 // 1. The substring approximation this replaces agreed only for
11926 // a literal needle, and answered -1 for an absent argument
11927 // where the empty pattern matches at 0.
11928 let re = regexp_from_arg(&arg0(&args), "")?;
11929 crate::regexp::str_search(s, &re)
11930 }
11931 }
11932 "replace" => {
11933 let pat = arg0(&args);
11934 let repl = args.get(1).cloned().unwrap_or(Value::Undef);
11935 if is_regexp_arg(&pat) {
11936 crate::regexp::str_replace_regex(s, &pat, &repl, false)
11937 } else if with_host(|h| host::is_callable(h, &repl)) {
11938 Ok(new_s(replace_str_fn(
11939 s,
11940 &with_host(|h| h.str_of(&pat)),
11941 &repl,
11942 false,
11943 )?))
11944 } else {
11945 let from = with_host(|h| h.str_of(&pat));
11946 let to = with_host(|h| h.str_of(&repl));
11947 Ok(new_s(replace_str_plain(s, &from, &to, false)))
11948 }
11949 }
11950 "replaceAll" => {
11951 let pat = arg0(&args);
11952 let repl = args.get(1).cloned().unwrap_or(Value::Undef);
11953 if is_regexp_arg(&pat) {
11954 // 22.1.3.20 step 2: a non-global regexp is a TypeError here,
11955 // because `replaceAll` cannot honour "all" without `g`. This
11956 // used to replace only the first match and say nothing.
11957 let global = with_host(|h| match h.get(&pat) {
11958 Some(JsObj::RegExp(r)) => r.global,
11959 _ => true,
11960 });
11961 if !global {
11962 return Err(host::type_error(
11963 "String.prototype.replaceAll called with a non-global RegExp argument",
11964 ));
11965 }
11966 crate::regexp::str_replace_regex(s, &pat, &repl, true)
11967 } else if with_host(|h| host::is_callable(h, &repl)) {
11968 Ok(new_s(replace_str_fn(
11969 s,
11970 &with_host(|h| h.str_of(&pat)),
11971 &repl,
11972 true,
11973 )?))
11974 } else {
11975 let from = with_host(|h| h.str_of(&pat));
11976 let to = with_host(|h| h.str_of(&repl));
11977 Ok(new_s(replace_str_plain(s, &from, &to, true)))
11978 }
11979 }
11980 "split" => {
11981 if is_regexp_arg(&arg0(&args)) {
11982 let limit = args
11983 .get(1)
11984 .filter(|v| !matches!(v, Value::Undef))
11985 .map(|v| with_host(|h| h.to_number(v)) as usize);
11986 return crate::regexp::str_split_regex(s, &arg0(&args), limit);
11987 }
11988 let mut parts: Vec<Value> = if args.is_empty() || matches!(args[0], Value::Undef) {
11989 vec![new_s(s.to_string())]
11990 } else {
11991 let sep = with_host(|h| h.str_of(&args[0]));
11992 if sep.is_empty() {
11993 // `split('')` yields one element per code UNIT, so an astral
11994 // character becomes its two surrogate halves.
11995 (0..u.len())
11996 .filter_map(|i| u.unit_str(i))
11997 .map(new_s)
11998 .collect()
11999 } else {
12000 s.split(&sep as &str)
12001 .map(|p| new_s(p.to_string()))
12002 .collect()
12003 }
12004 };
12005 // Optional limit: keep at most `limit` substrings.
12006 if let Some(lim) = args.get(1).filter(|v| !matches!(v, Value::Undef)) {
12007 let n = with_host(|h| h.to_number(lim));
12008 if n.is_finite() && n >= 0.0 {
12009 parts.truncate(n as usize);
12010 }
12011 }
12012 Ok(with_host(|h| h.new_array(parts)))
12013 }
12014 _ => Err(host::type_error(&format!("{name} is not a function"))),
12015 }
12016}
12017
12018/// GetSubstitution (22.1.3.19) for a STRING search value.
12019///
12020/// `String.prototype.replace`/`replaceAll` expand the same `$` patterns whether
12021/// the pattern is a regexp or a plain string, but the string path here did a
12022/// raw `str::replace` and passed the template through verbatim — so
12023/// `'abc'.replace('b', '[$&]')` produced `a[$&]c` instead of `a[b]c`. The
12024/// regexp path has always expanded them.
12025///
12026/// A string search captures nothing, so only `$$`, `$&`, `` $` `` and `$'`
12027/// apply; `$1` and `$<name>` have no referent and stay literal, which is also
12028/// what node does.
12029fn substitute_plain(templ: &str, matched: &str, position: usize, subject: &str) -> String {
12030 let chars: Vec<char> = templ.chars().collect();
12031 let mut out = String::new();
12032 let mut i = 0;
12033 while i < chars.len() {
12034 if chars[i] == '$' && i + 1 < chars.len() {
12035 match chars[i + 1] {
12036 '$' => {
12037 out.push('$');
12038 i += 2;
12039 continue;
12040 }
12041 '&' => {
12042 out.push_str(matched);
12043 i += 2;
12044 continue;
12045 }
12046 '`' => {
12047 out.push_str(&subject[..position]);
12048 i += 2;
12049 continue;
12050 }
12051 '\'' => {
12052 out.push_str(&subject[position + matched.len()..]);
12053 i += 2;
12054 continue;
12055 }
12056 _ => {}
12057 }
12058 }
12059 out.push(chars[i]);
12060 i += 1;
12061 }
12062 out
12063}
12064
12065/// `replace`/`replaceAll` with a string pattern and a string replacement,
12066/// expanding each match's `$` patterns against its own position.
12067fn replace_str_plain(s: &str, from: &str, to: &str, all: bool) -> String {
12068 if from.is_empty() && !all {
12069 return format!("{}{s}", substitute_plain(to, "", 0, s));
12070 }
12071 let mut out = String::new();
12072 let mut rest = 0usize;
12073 while let Some(rel) = s[rest..].find(from) {
12074 let at = rest + rel;
12075 out.push_str(&s[rest..at]);
12076 out.push_str(&substitute_plain(to, from, at, s));
12077 rest = at + from.len();
12078 if !all {
12079 break;
12080 }
12081 // An empty pattern matches between every character; step one along so
12082 // the scan terminates.
12083 if from.is_empty() {
12084 if rest >= s.len() {
12085 break;
12086 }
12087 let step = s[rest..].chars().next().map(|c| c.len_utf8()).unwrap_or(1);
12088 out.push_str(&s[rest..rest + step]);
12089 rest += step;
12090 }
12091 }
12092 out.push_str(&s[rest..]);
12093 out
12094}
12095
12096fn new_s(s: String) -> Value {
12097 with_host(|h| h.new_str(s))
12098}
12099
12100/// Where a forward `indexOf`/`includes` search starts, given the optional
12101/// `fromIndex` (23.1.3.17 steps 4-6, 23.1.3.16 steps 5-7). A negative value
12102/// counts back from the end and clamps at 0; absent or `NaN` is 0. A start at
12103/// or past the end finds nothing, which callers report as `-1` / `false`.
12104pub(crate) fn search_start(n: f64, len: usize) -> usize {
12105 if n.is_nan() {
12106 return 0;
12107 }
12108 let n = n.trunc();
12109 if n >= 0.0 {
12110 if n >= len as f64 {
12111 len
12112 } else {
12113 n as usize
12114 }
12115 } else {
12116 let from_end = len as f64 + n;
12117 if from_end <= 0.0 {
12118 0
12119 } else {
12120 from_end as usize
12121 }
12122 }
12123}
12124
12125/// The INCLUSIVE index a backward `lastIndexOf` starts at (23.1.3.20 steps
12126/// 4-6), or `None` when `fromIndex` places it before the array. Absent means
12127/// the last element — which is why this takes an `Option` rather than reading
12128/// `NaN` as "absent" the way the forward form can: an explicit `NaN` is
12129/// `ToIntegerOrInfinity`'d to 0 and searches only index 0.
12130pub(crate) fn search_start_last(from: Option<f64>, len: usize) -> Option<usize> {
12131 if len == 0 {
12132 return None;
12133 }
12134 let n = match from {
12135 None => return Some(len - 1),
12136 Some(v) if v.is_nan() => 0.0,
12137 Some(v) => v.trunc(),
12138 };
12139 if n >= 0.0 {
12140 Some(if n >= len as f64 { len - 1 } else { n as usize })
12141 } else {
12142 let k = len as f64 + n;
12143 if k < 0.0 {
12144 None
12145 } else {
12146 Some(k as usize)
12147 }
12148 }
12149}
12150
12151/// `ToIntegerOrInfinity(n)` clamped into `0..=len` — the position argument of
12152/// the `String.prototype` search methods. `NaN` (an absent argument) is `0`.
12153fn clamp_pos(n: f64, len: usize) -> usize {
12154 if n.is_nan() || n <= 0.0 {
12155 0
12156 } else if n >= len as f64 {
12157 len
12158 } else {
12159 n.trunc() as usize
12160 }
12161}
12162
12163/// `ToIntegerOrInfinity(n)` as a code-unit position, or `None` when there can be
12164/// no such unit. `NaN` (an absent argument) is 0; a negative or infinite
12165/// position is out of range — `"abc".charCodeAt(-1)` is `NaN`, not `'a'`.
12166fn unit_pos(n: f64) -> Option<usize> {
12167 if n.is_nan() {
12168 Some(0)
12169 } else if n < 0.0 || !n.is_finite() {
12170 None
12171 } else {
12172 Some(n.trunc() as usize)
12173 }
12174}
12175
12176/// The search argument of `indexOf`/`includes`/`startsWith`/… as code units, so
12177/// the needle is compared in the same alphabet the haystack is indexed by.
12178fn needle_units(args: &[Value]) -> crate::utf16::Units {
12179 crate::utf16::Units::of(&with_host(|h| h.str_of(&arg0(args))))
12180}
12181
12182/// The lowest index `>= from` at which `needle` occurs in `hay`. An empty
12183/// needle matches at `from` itself, as JS specifies.
12184fn search_from(hay: &[u16], needle: &[u16], from: usize) -> Option<usize> {
12185 if needle.is_empty() {
12186 return Some(from.min(hay.len()));
12187 }
12188 if needle.len() > hay.len() {
12189 return None;
12190 }
12191 (from..=hay.len().saturating_sub(needle.len())).find(|&i| &hay[i..i + needle.len()] == needle)
12192}
12193
12194/// The highest index `<= upto` at which `needle` occurs in `hay`.
12195fn search_last(hay: &[u16], needle: &[u16], upto: usize) -> Option<usize> {
12196 if needle.is_empty() {
12197 return Some(upto.min(hay.len()));
12198 }
12199 if needle.len() > hay.len() {
12200 return None;
12201 }
12202 let last = hay.len() - needle.len();
12203 (0..=upto.min(last))
12204 .rev()
12205 .find(|&i| &hay[i..i + needle.len()] == needle)
12206}
12207
12208fn pad(s: &str, args: &[Value], start: bool) -> Result<String, String> {
12209 let target_f = arg_num(args, 0);
12210 let target = if target_f.is_finite() && target_f > 0.0 {
12211 target_f as usize
12212 } else {
12213 0
12214 };
12215 // `targetLength` and the padding both count code units: `'𝒳'.padStart(3,'-')`
12216 // is `'-𝒳'` in node, not `'--𝒳'`.
12217 let cur = crate::utf16::len(s);
12218 if cur >= target {
12219 return Ok(s.to_string());
12220 }
12221 let filler = if args.len() >= 2 {
12222 with_host(|h| h.str_of(&args[1]))
12223 } else {
12224 " ".to_string()
12225 };
12226 if filler.is_empty() {
12227 return Ok(s.to_string());
12228 }
12229 // Checked only AFTER the two short-circuits, which is the order V8 uses:
12230 // measured on node v26.7.0, `'ab'.padStart(2**40, '')` is `'ab'` while
12231 // `'ab'.padStart(536870889, 'x')` is `RangeError: Invalid string length`.
12232 if target_f > host::MAX_STRING_LENGTH as f64 {
12233 return Err(host::invalid_string_length());
12234 }
12235 let need = target - cur;
12236 let fill = crate::utf16::Units::of(&filler);
12237 // The filler repeats and is TRUNCATED to the exact unit count, which can cut
12238 // a surrogate pair — node yields a lone surrogate there, we yield U+FFFD
12239 // (see src/utf16.rs).
12240 let units: Vec<u16> = (0..need)
12241 .filter_map(|i| fill.unit(i % fill.len()))
12242 .collect();
12243 let padding = crate::utf16::to_string_lossy(&units);
12244 Ok(if start {
12245 format!("{padding}{s}")
12246 } else {
12247 format!("{s}{padding}")
12248 })
12249}
12250
12251/// V8's radix rejection, shared by `Number.prototype.toString` and
12252/// `BigInt.prototype.toString` — one string, because they are one message and
12253/// the two sites had drifted apart ("radix must be" vs V8's "radix argument
12254/// must be").
12255const RADIX_RANGE: &str = "toString() radix argument must be between 2 and 36";
12256
12257/// `BigInt.prototype` methods: `toString([radix])`, `valueOf`, `toLocaleString`.
12258fn bigint_method(b: &num_bigint::BigInt, name: &str, args: Vec<Value>) -> Result<Value, String> {
12259 match name {
12260 "toString" => {
12261 let radix = match args.first() {
12262 None | Some(Value::Undef) => 10,
12263 Some(_) => {
12264 let t = arg_num(&args, 0).trunc();
12265 if !(2.0..=36.0).contains(&t) {
12266 return Err(host::range_error(RADIX_RANGE));
12267 }
12268 t as u32
12269 }
12270 };
12271 Ok(new_s(b.to_str_radix(radix)))
12272 }
12273 // `BigInt.prototype.toLocaleString` groups thousands like the Number
12274 // one does — `(1234567n).toLocaleString()` is `1,234,567` in node, and
12275 // returning the bare digits made it the only numeric type that skipped
12276 // grouping. Same en-US-shaped output as `Number.prototype`; the
12277 // `locales`/`options` arguments are ignored (no ICU here).
12278 "toLocaleString" => {
12279 let digits = b.magnitude().to_string();
12280 let sign = if b.sign() == num_bigint::Sign::Minus {
12281 "-"
12282 } else {
12283 ""
12284 };
12285 Ok(new_s(format!("{sign}{}", group_thousands(&digits))))
12286 }
12287 "valueOf" => Ok(with_host(|h| h.new_bigint(b.clone()))),
12288 _ => Err(host::type_error(&format!("{name} is not a function"))),
12289 }
12290}
12291
12292fn number_method(n: f64, name: &str, args: Vec<Value>) -> Result<Value, String> {
12293 let args = coerce_numeric_args(NUMBER_METHOD_NUMERIC_ARGS, name, args)?;
12294 match name {
12295 "toFixed" => {
12296 let digits = arg_num(&args, 0);
12297 if !(0.0..=100.0).contains(&digits.trunc()) {
12298 return Err(host::range_error(
12299 "toFixed() digits argument must be between 0 and 100",
12300 ));
12301 }
12302 Ok(new_s(to_fixed(n, digits as usize)))
12303 }
12304 "toExponential" => {
12305 // `undefined` (or a missing argument) selects the shortest form.
12306 let f = match args.first() {
12307 None | Some(Value::Undef) => None,
12308 Some(_) => {
12309 let d = arg_num(&args, 0).trunc();
12310 if !(0.0..=100.0).contains(&d) {
12311 return Err(host::range_error(
12312 "toExponential() argument must be between 0 and 100",
12313 ));
12314 }
12315 Some(d as usize)
12316 }
12317 };
12318 Ok(new_s(to_exponential(n, f)))
12319 }
12320 "toString" => {
12321 // An out-of-range radix THROWS; it does not silently fall back to
12322 // base 10. `(1).toString(37)` returned "1" here, so a support probe
12323 // was told every radix worked.
12324 let radix = match args.first() {
12325 None | Some(Value::Undef) => 10,
12326 Some(_) => {
12327 let r = arg_num(&args, 0);
12328 let t = r.trunc();
12329 if !(2.0..=36.0).contains(&t) {
12330 return Err(host::range_error(RADIX_RANGE));
12331 }
12332 t as u32
12333 }
12334 };
12335 if radix == 10 {
12336 Ok(new_s(host::fmt_number(n)))
12337 } else {
12338 Ok(new_s(to_radix(n, radix)))
12339 }
12340 }
12341 "toPrecision" => {
12342 // `undefined` (or a missing argument) behaves like `toString()`.
12343 match args.first() {
12344 None | Some(Value::Undef) => Ok(new_s(host::fmt_number(n))),
12345 Some(_) => {
12346 let p = arg_num(&args, 0).trunc();
12347 if !(1.0..=100.0).contains(&p) {
12348 return Err(host::range_error(
12349 "toPrecision() argument must be between 1 and 100",
12350 ));
12351 }
12352 Ok(new_s(to_precision(n, p as usize)))
12353 }
12354 }
12355 }
12356 "toLocaleString" => Ok(new_s(to_locale_string(n))),
12357 "valueOf" => Ok(Value::Float(n)),
12358 _ => Err(host::type_error(&format!("{name} is not a function"))),
12359 }
12360}
12361
12362/// `Number.prototype.toLocaleString()` with the default locale and options:
12363/// integer part grouped in threes with `,`, up to 3 fraction digits (rounded
12364/// half away from zero), trailing fractional zeros dropped. Mirrors V8's default
12365/// `Intl.NumberFormat().format` output (`(12345.678).toLocaleString()` ⇒
12366/// `"12,345.678"`; `(1234.5678)` ⇒ `"1,234.568"`). `NaN`, `±Infinity`, and `-0`
12367/// render as `"NaN"`, `"∞"`/`"-∞"`, and `"-0"`.
12368fn to_locale_string(n: f64) -> String {
12369 if n.is_nan() {
12370 return "NaN".to_string();
12371 }
12372 if n.is_infinite() {
12373 return if n < 0.0 { "-∞" } else { "∞" }.to_string();
12374 }
12375 let neg = n.is_sign_negative();
12376 // Round the magnitude to at most 3 fraction digits, then drop trailing zeros
12377 // (and a bare trailing point). `to_fixed` rounds half away from zero.
12378 // `to_fixed` falls back to `ToString` at |x| ≥ 1e21 (spec 21.1.3.3 step 6),
12379 // which is exponential — and the grouping below then chopped up the
12380 // exponent, so `(1e21).toLocaleString()` was `1e,+21` instead of node's
12381 // `1,000,000,000,000,000,000,000`. Expanding the SHORTEST repr is the right
12382 // source: node groups the shortest decimal form, so `(1e100)
12383 // .toLocaleString()` is 1 followed by a hundred zeros rather than the exact
12384 // binary value `1000…159028911…`. (`BigInt(1e100)` is the exact value, a
12385 // deliberately different rule — see `bigint_ctor`.)
12386 let fixed = expand_exponential(&to_fixed(n.abs(), 3));
12387 let trimmed = match fixed.split_once('.') {
12388 Some(_) => fixed.trim_end_matches('0').trim_end_matches('.'),
12389 None => fixed.as_str(),
12390 };
12391 let (int_part, frac_part) = match trimmed.split_once('.') {
12392 Some((i, f)) => (i, Some(f)),
12393 None => (trimmed, None),
12394 };
12395 let mut out = String::new();
12396 if neg {
12397 out.push('-'); // Intl keeps the sign even for -0.
12398 }
12399 out.push_str(&group_thousands(int_part));
12400 if let Some(f) = frac_part {
12401 out.push('.');
12402 out.push_str(f);
12403 }
12404 out
12405}
12406
12407/// Write a nonnegative decimal string in plain positional form, expanding an
12408/// `e+NN` exponent into zeros. `"1e+21"` → `"1000000000000000000000"`,
12409/// `"1.5e+21"` → `"1500000000000000000000"`. A string with no exponent, or a
12410/// negative exponent (a magnitude below 1, which the caller has already rounded
12411/// to zero), is returned unchanged.
12412fn expand_exponential(s: &str) -> String {
12413 let Some((mantissa, exp)) = s.split_once(['e', 'E']) else {
12414 return s.to_string();
12415 };
12416 let Ok(exp) = exp.trim_start_matches('+').parse::<i32>() else {
12417 return s.to_string();
12418 };
12419 if exp <= 0 {
12420 return s.to_string();
12421 }
12422 let (int_digits, frac_digits) = match mantissa.split_once('.') {
12423 Some((i, f)) => (i.to_string(), f.to_string()),
12424 None => (mantissa.to_string(), String::new()),
12425 };
12426 let mut digits = int_digits;
12427 digits.push_str(&frac_digits);
12428 // The exponent consumes the fractional digits first; whatever is left
12429 // becomes trailing zeros.
12430 let zeros = exp as usize - frac_digits.len().min(exp as usize);
12431 digits.push_str(&"0".repeat(zeros));
12432 digits
12433}
12434
12435/// Insert `,` as a thousands separator into a nonnegative integer digit string.
12436fn group_thousands(int_part: &str) -> String {
12437 let bytes = int_part.as_bytes();
12438 let n = bytes.len();
12439 let mut out = String::with_capacity(n + n / 3);
12440 for (i, &b) in bytes.iter().enumerate() {
12441 if i > 0 && (n - i) % 3 == 0 {
12442 out.push(',');
12443 }
12444 out.push(b as char);
12445 }
12446 out
12447}
12448
12449/// `Number.prototype.toFixed(f)`: fixed-point with `f` fractional digits, rounding
12450/// half away from zero on the actual IEEE-754 value (so `(1.005).toFixed(2)` is
12451/// `"1.00"` because 1.005 is really 1.00499…). The sign of a negative input is
12452/// preserved even when the rounded magnitude is zero: `(-0.4).toFixed(0) === "-0"`.
12453///
12454/// The rounding is done on the value's EXACT decimal expansion (Rust's fixed
12455/// formatting is exact), not on `x * 10^f` — the latter loses precision for large
12456/// magnitudes (`(9.999999e20).toFixed(4)` must keep every integer digit).
12457fn to_fixed(n: f64, f: usize) -> String {
12458 if !n.is_finite() {
12459 return host::fmt_number(n);
12460 }
12461 // Spec: for |x| ≥ 10^21, toFixed falls back to ToString(x).
12462 if n.abs() >= 1e21 {
12463 return host::fmt_number(n);
12464 }
12465 let neg = n < 0.0;
12466 // Exact decimal with guard digits past the rounding position; then round the
12467 // digit string half-away-from-zero (nonneg operand ⇒ round-half-up).
12468 let full = format!("{:.*}", f + 25, n.abs());
12469 let mut body = round_decimal_string(&full, f);
12470 if neg {
12471 body.insert(0, '-'); // JS keeps the sign even for "-0" / "-0.00".
12472 }
12473 body
12474}
12475
12476/// Round the exact decimal string `s` (`"int.frac"`, nonnegative) to `f`
12477/// fractional digits, half away from zero, propagating carry across the point.
12478fn round_decimal_string(s: &str, f: usize) -> String {
12479 let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
12480 let mut digits: Vec<u8> = int_part
12481 .bytes()
12482 .chain(frac_part.bytes())
12483 .map(|b| b - b'0')
12484 .collect();
12485 let point = int_part.len(); // digits before the decimal point
12486 let keep = point + f; // number of leading digits to keep
12487
12488 // Round up if the first dropped digit is ≥ 5 (exact-half ⇒ up).
12489 if digits.get(keep).map(|&d| d >= 5).unwrap_or(false) {
12490 let mut i = keep;
12491 loop {
12492 if i == 0 {
12493 digits.insert(0, 1);
12494 // A new leading digit shifts the decimal point right by one.
12495 return assemble_decimal(&digits, point + 1, f);
12496 }
12497 i -= 1;
12498 if digits[i] == 9 {
12499 digits[i] = 0;
12500 } else {
12501 digits[i] += 1;
12502 break;
12503 }
12504 }
12505 }
12506 assemble_decimal(&digits, point, f)
12507}
12508
12509/// Reassemble `digits` into `"int.frac"` keeping `f` fractional digits, given that
12510/// `point` digits precede the decimal point.
12511fn assemble_decimal(digits: &[u8], point: usize, f: usize) -> String {
12512 let int_str: String = digits[..point].iter().map(|d| (d + b'0') as char).collect();
12513 let int_str = int_str.trim_start_matches('0');
12514 let int_str = if int_str.is_empty() { "0" } else { int_str };
12515 if f == 0 {
12516 return int_str.to_string();
12517 }
12518 let frac: String = digits[point..point + f]
12519 .iter()
12520 .map(|d| (d + b'0') as char)
12521 .collect();
12522 format!("{int_str}.{frac}")
12523}
12524
12525/// Round the nonnegative finite `a` to `p` significant decimal digits, half away
12526/// from zero, returning the `p` digits and the decimal exponent `e` such that the
12527/// value is `0.d…d × 10^(e+1)` (i.e. `d.d…d e±e`). Rust's `{:.*e}` rounds half to
12528/// EVEN (`(2.5)` at 1 digit would give "2"), but JS rounds half up ("3"), so the
12529/// exact digits are taken with guard positions and rounded here.
12530fn round_significant(a: f64, p: usize) -> (String, i32) {
12531 let sci = format!("{a:.*e}", p - 1 + 25);
12532 let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12533 let mut e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
12534 let all: Vec<u8> = mant
12535 .chars()
12536 .filter(|c| c.is_ascii_digit())
12537 .map(|c| c as u8 - b'0')
12538 .collect();
12539 let mut s: String = all[..p].iter().map(|d| (d + b'0') as char).collect();
12540 if all.get(p).map(|&d| d >= 5).unwrap_or(false) {
12541 // Round the p-digit mantissa up, propagating carry; a carry out of the
12542 // leading digit (`9.99 → 10`) bumps the decimal exponent by one.
12543 let mut d: Vec<u8> = all[..p].to_vec();
12544 let mut i = p;
12545 loop {
12546 if i == 0 {
12547 d.insert(0, 1);
12548 d.truncate(p);
12549 e += 1;
12550 break;
12551 }
12552 i -= 1;
12553 if d[i] == 9 {
12554 d[i] = 0;
12555 } else {
12556 d[i] += 1;
12557 break;
12558 }
12559 }
12560 s = d.iter().map(|x| (x + b'0') as char).collect();
12561 }
12562 (s, e)
12563}
12564
12565/// `Number.prototype.toExponential(f)`: one digit before the point and `f` after,
12566/// with a signed decimal exponent (`(100).toExponential(2) === "1.00e+2"`). With
12567/// `f` omitted, as many digits as uniquely identify the value are used
12568/// (`(123456).toExponential() === "1.23456e+5"`). Rounding is half away from zero
12569/// on the exact value, matching `toPrecision`.
12570fn to_exponential(n: f64, f: Option<usize>) -> String {
12571 if !n.is_finite() {
12572 return host::fmt_number(n);
12573 }
12574 let neg = n < 0.0;
12575 let a = n.abs();
12576 let (s, e) = if a == 0.0 {
12577 // Zero has no significant digits: emit "0" padded to the requested width.
12578 ("0".repeat(f.unwrap_or(0) + 1), 0)
12579 } else {
12580 match f {
12581 Some(f) => round_significant(a, f + 1),
12582 None => {
12583 // Shortest round-tripping digits (Rust's `{:e}` is shortest).
12584 let sci = format!("{a:e}");
12585 let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12586 let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
12587 let trimmed = digits.trim_end_matches('0');
12588 let digits = if trimmed.is_empty() { "0" } else { trimmed };
12589 (digits.to_string(), exp_str.parse().unwrap_or(0))
12590 }
12591 }
12592 };
12593 let sign = if e >= 0 { '+' } else { '-' };
12594 let mag = e.abs();
12595 let body = if s.len() == 1 {
12596 format!("{s}e{sign}{mag}")
12597 } else {
12598 format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12599 };
12600 if neg {
12601 format!("-{body}")
12602 } else {
12603 body
12604 }
12605}
12606
12607/// `Number.prototype.toPrecision(p)`: `p` significant digits, switching to
12608/// exponential form when the decimal exponent `e` satisfies `e < -6` or `e ≥ p`
12609/// (ECMAScript Number.prototype.toPrecision). Trailing zeros are significant and
12610/// retained (`(100).toPrecision(5) === "100.00"`).
12611fn to_precision(n: f64, p: usize) -> String {
12612 if !n.is_finite() {
12613 return host::fmt_number(n);
12614 }
12615 if n == 0.0 {
12616 return if p == 1 {
12617 "0".into()
12618 } else {
12619 format!("0.{}", "0".repeat(p - 1))
12620 };
12621 }
12622 let neg = n < 0.0;
12623 let (s, e) = round_significant(n.abs(), p);
12624 let pp = p as i32;
12625
12626 let body = if e < -6 || e >= pp {
12627 // Exponential: first digit, optional '.rest', signed exponent.
12628 let sign = if e >= 0 { '+' } else { '-' };
12629 let mag = e.abs();
12630 if p == 1 {
12631 format!("{s}e{sign}{mag}")
12632 } else {
12633 format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12634 }
12635 } else if e >= 0 {
12636 // e in 0..p-1: (e+1) integer digits, then any remaining as fraction.
12637 let ip = (e + 1) as usize;
12638 if ip == p {
12639 s
12640 } else {
12641 format!("{}.{}", &s[..ip], &s[ip..])
12642 }
12643 } else {
12644 // -6 ≤ e < 0: "0." then (−e−1) zeros then all p digits.
12645 format!("0.{}{}", "0".repeat((-e - 1) as usize), s)
12646 };
12647 if neg {
12648 format!("-{body}")
12649 } else {
12650 body
12651 }
12652}
12653
12654/// `Number.prototype.toString(radix)` for radix 2..=36 (radix 10 goes through
12655/// `fmt_number`). Faithful port of V8's `DoubleToRadixCString`: the integer part
12656/// is emitted exact, and fractional digits are produced up to the input double's
12657/// precision (terminating via a ULP-sized `delta`), with round-half-to-even and
12658/// carry-over back into already-written digits (and into the integer part).
12659fn to_radix(n: f64, radix: u32) -> String {
12660 if !n.is_finite() {
12661 return host::fmt_number(n);
12662 }
12663 let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
12664 let rf = radix as f64;
12665 let neg = n < 0.0;
12666 let value = n.abs();
12667
12668 let mut integer = value.floor();
12669 let mut fraction = value - integer;
12670
12671 // Fraction digits, most-significant first.
12672 let mut frac: Vec<u8> = Vec::new();
12673 // Only compute fractional digits down to the input double's precision.
12674 let mut delta = 0.5 * (next_up(value) - value);
12675 delta = delta.max(next_up(0.0));
12676 if fraction >= delta {
12677 loop {
12678 // Shift up by one digit.
12679 fraction *= rf;
12680 delta *= rf;
12681 let digit = fraction as usize;
12682 frac.push(digits[digit]);
12683 fraction -= digit as f64;
12684 // Round to even.
12685 if (fraction > 0.5 || (fraction == 0.5 && (digit & 1) == 1)) && fraction + delta > 1.0 {
12686 // Carry-over: back-trace already-written fraction digits.
12687 loop {
12688 match frac.pop() {
12689 None => {
12690 // Carried past the point into the integer part.
12691 integer += 1.0;
12692 break;
12693 }
12694 Some(c) => {
12695 let d = if c > b'9' {
12696 (c - b'a' + 10) as u32
12697 } else {
12698 (c - b'0') as u32
12699 };
12700 if d + 1 < radix {
12701 frac.push(digits[(d + 1) as usize]);
12702 break;
12703 }
12704 // digit was radix-1: drop it and keep carrying.
12705 }
12706 }
12707 }
12708 break;
12709 }
12710 if fraction < delta {
12711 break;
12712 }
12713 }
12714 }
12715
12716 // Integer digits, least-significant first (reversed at the end).
12717 let mut int_out: Vec<u8> = Vec::new();
12718 // For magnitudes ≥ 2^53, `fmod` loses low bits: pre-fill trailing zeros.
12719 while v8_exponent(integer / rf) > 0 {
12720 integer /= rf;
12721 int_out.push(b'0');
12722 }
12723 loop {
12724 let remainder = integer % rf;
12725 int_out.push(digits[remainder as usize]);
12726 integer = (integer - remainder) / rf;
12727 if integer <= 0.0 {
12728 break;
12729 }
12730 }
12731 int_out.reverse();
12732
12733 let mut out: Vec<u8> = Vec::new();
12734 if neg {
12735 out.push(b'-');
12736 }
12737 out.extend_from_slice(&int_out);
12738 if !frac.is_empty() {
12739 out.push(b'.');
12740 out.extend_from_slice(&frac);
12741 }
12742 String::from_utf8(out).unwrap()
12743}
12744
12745/// Next representable f64 above `x` (`x` finite, `x ≥ 0`) — V8's `NextDouble`.
12746fn next_up(x: f64) -> f64 {
12747 f64::from_bits(x.to_bits() + 1)
12748}
12749
12750/// V8's `Double::Exponent`: the binary exponent of the significand-scaled value
12751/// (`> 0` iff |x| ≥ 2^53). Used to detect integers past `fmod`'s exact range.
12752fn v8_exponent(x: f64) -> i32 {
12753 let biased = ((x.to_bits() >> 52) & 0x7ff) as i32;
12754 if biased == 0 {
12755 -1074 // denormal
12756 } else {
12757 biased - 1075
12758 }
12759}
12760
12761// ══ Map / Set / Symbol / generator methods ═══════════════════════════════════
12762
12763/// `Map.prototype.set` step 6 and `Set.prototype.add` step 4: a key of `-0` is
12764/// STORED as `+0`. `map_key` already treats the two as one key (SameValueZero),
12765/// but the value kept alongside it is what iteration and `console.log` report,
12766/// and node shows `0` there — `new Map().set(-0, 1)` renders `Map(1) { 0 => 1 }`.
12767fn normalize_zero_key(v: Value) -> Value {
12768 match v {
12769 Value::Float(f) if f == 0.0 && f.is_sign_negative() => Value::Float(0.0),
12770 other => other,
12771 }
12772}
12773
12774fn map_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
12775 match name {
12776 "get" => {
12777 let key = with_host(|h| host::map_key(h, &arg0(&args)));
12778 Ok(with_host(|h| match h.get(recv) {
12779 Some(JsObj::Map { entries, .. }) => entries
12780 .get(&key)
12781 .map(|(_, v)| v.clone())
12782 .unwrap_or(Value::Undef),
12783 _ => Value::Undef,
12784 }))
12785 }
12786 "set" => {
12787 let kv = normalize_zero_key(arg0(&args));
12788 let vv = args.get(1).cloned().unwrap_or(Value::Undef);
12789 reject_non_object_weak_key(recv, &kv, "WeakMap")?;
12790 let key = with_host(|h| host::map_key(h, &kv));
12791 with_host(|h| {
12792 if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12793 entries.insert(key, (kv, vv));
12794 }
12795 });
12796 Ok(recv.clone())
12797 }
12798 "has" => {
12799 let key = with_host(|h| host::map_key(h, &arg0(&args)));
12800 Ok(Value::Bool(with_host(
12801 |h| matches!(h.get(recv), Some(JsObj::Map { entries, .. }) if entries.contains_key(&key)),
12802 )))
12803 }
12804 "delete" => {
12805 let key = with_host(|h| host::map_key(h, &arg0(&args)));
12806 Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
12807 Some(JsObj::Map { entries, .. }) => entries.shift_remove(&key).is_some(),
12808 _ => false,
12809 })))
12810 }
12811 "clear" => {
12812 with_host(|h| {
12813 if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12814 entries.clear();
12815 }
12816 });
12817 Ok(Value::Undef)
12818 }
12819 "forEach" => {
12820 let cb = arg0(&args);
12821 let pairs: Vec<(Value, Value)> = with_host(|h| match h.get(recv) {
12822 Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
12823 _ => Vec::new(),
12824 });
12825 for (k, v) in pairs {
12826 host::invoke(&cb, vec![v, k, recv.clone()], this_arg(&args, 1))?;
12827 }
12828 Ok(Value::Undef)
12829 }
12830 // LIVE, not a snapshot: an entry added during iteration is visited and
12831 // one deleted before it is reached is not.
12832 "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
12833 recv,
12834 if name == "@@iterator" {
12835 "entries"
12836 } else {
12837 name
12838 },
12839 )),
12840 _ => Err(host::type_error(&format!("map.{name} is not a function"))),
12841 }
12842}
12843
12844/// A weak collection can only hold objects (and unregistered symbols) — a
12845/// primitive key is a `TypeError`, which is how packages probe for weak support.
12846fn reject_non_object_weak_key(recv: &Value, key: &Value, kind: &str) -> Result<(), String> {
12847 let weak = with_host(|h| {
12848 matches!(
12849 h.get(recv),
12850 Some(JsObj::Map { weak: true, .. }) | Some(JsObj::Set { weak: true, .. })
12851 )
12852 });
12853 if !weak {
12854 return Ok(());
12855 }
12856 let is_object = with_host(|h| match key {
12857 Value::Obj(_) => !h.is_null(key) && h.as_str(key).is_none() && h.as_bigint(key).is_none(),
12858 _ => false,
12859 });
12860 if is_object {
12861 return Ok(());
12862 }
12863 Err(host::type_error(if kind == "WeakMap" {
12864 "Invalid value used as weak map key"
12865 } else {
12866 "Invalid value used in weak set"
12867 }))
12868}
12869
12870/// A `Set`-like operand of the ES2025 set methods — 24.2.1.2 `GetSetRecord`.
12871///
12872/// The seven set operations do NOT require a real `Set` on the right-hand side:
12873/// anything with a numeric `size` and callable `has`/`keys` participates, which
12874/// is what lets a `Map`'s key view or a user-written set stand in. The reads
12875/// happen in this order (`size`, `has`, `keys`) and each failure has its own
12876/// diagnostic, so a bad operand reports which field was wrong rather than
12877/// failing later inside the iteration.
12878struct SetRecord {
12879 obj: Value,
12880 /// `size` truncated toward zero, as the spec's `intSize` is; the fractional
12881 /// part is dropped BEFORE the negative check, so `size: -0.5` truncates to
12882 /// `-0` and is accepted while `-1.5` reports `'-1' is an invalid size`.
12883 size: f64,
12884 has: Value,
12885 keys: Value,
12886}
12887
12888fn get_set_record(other: &Value, method: &str) -> Result<SetRecord, String> {
12889 if !with_host(|h| is_object_like(h, other)) {
12890 return Err(host::type_error(&format!(
12891 "Set.prototype.{method} argument must be an object"
12892 )));
12893 }
12894 let raw = get_property(other, "size")?;
12895 let num = host::to_number_value(&raw)?;
12896 if num.is_nan() {
12897 return Err(host::type_error("The .size property is NaN"));
12898 }
12899 let size = num.trunc();
12900 if size < 0.0 {
12901 return Err(host::range_error(&format!("'{size}' is an invalid size")));
12902 }
12903 let has = get_property(other, "has")?;
12904 if !with_host(|h| host::is_callable(h, &has)) {
12905 return Err(host::type_error("string \"has\" is not a function"));
12906 }
12907 let keys = get_property(other, "keys")?;
12908 if !with_host(|h| host::is_callable(h, &keys)) {
12909 return Err(host::type_error("string \"keys\" is not a function"));
12910 }
12911 Ok(SetRecord {
12912 obj: other.clone(),
12913 size,
12914 has,
12915 keys,
12916 })
12917}
12918
12919impl SetRecord {
12920 /// `Call(has, obj, [v])`, coerced to a boolean the way the spec's
12921 /// `ToBoolean(Call(...))` is — a set-like may answer with anything truthy.
12922 fn has(&self, v: &Value) -> Result<bool, String> {
12923 let r = host::invoke(&self.has, vec![v.clone()], Some(self.obj.clone()))?;
12924 Ok(with_host(|h| h.truthy(&r)))
12925 }
12926
12927 /// The operand's elements, drained from the iterator its `keys` method
12928 /// returns. A non-object result is the spec's `Result of the keys method is
12929 /// not an object`, reported before anything is iterated.
12930 fn keys(&self) -> Result<Vec<Value>, String> {
12931 let it = host::invoke(&self.keys, Vec::new(), Some(self.obj.clone()))?;
12932 if !with_host(|h| is_object_like(h, &it)) {
12933 return Err(host::type_error(
12934 "Result of the keys method is not an object",
12935 ));
12936 }
12937 host::drain_iterator(&it)
12938 }
12939}
12940
12941/// The receiver of a set operation must be a real (non-weak) `Set`: these seven
12942/// methods read `[[SetData]]` directly, so a look-alike cannot stand in on the
12943/// LEFT even though it can on the right.
12944fn require_set_receiver(recv: &Value, method: &str) -> Result<(), String> {
12945 if with_host(|h| matches!(h.get(recv), Some(JsObj::Set { weak: false, .. }))) {
12946 return Ok(());
12947 }
12948 Err(host::type_error(&format!(
12949 "Method Set.prototype.{method} called on incompatible receiver {}",
12950 with_host(|h| object_tag(h, recv))
12951 )))
12952}
12953
12954/// The receiver's elements, READ AT THE POINT THE SPEC READS THEM.
12955///
12956/// Every one of these operations copies `[[SetData]]` *after* it has touched
12957/// the operand — `union` and `symmetricDifference` call the operand's `keys`
12958/// first — so a `keys` (or a `has`) that mutates the receiver is visible in the
12959/// result. Snapshotting the receiver up front instead dropped such an element:
12960/// node's `s.union({ keys(){ s.add(99); … } })` contains `99`.
12961fn set_values(recv: &Value) -> Vec<Value> {
12962 with_host(|h| match h.get(recv) {
12963 Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
12964 _ => Vec::new(),
12965 })
12966}
12967
12968fn set_size(recv: &Value) -> f64 {
12969 with_host(|h| match h.get(recv) {
12970 Some(JsObj::Set { entries, .. }) => entries.len() as f64,
12971 _ => 0.0,
12972 })
12973}
12974
12975/// A fresh, ordinary `Set`. The set operations are NOT species-aware: on node
12976/// `class S extends Set {}`, `new S([1]).union(other).constructor` is `Set`.
12977fn new_set(items: Vec<Value>) -> Result<Value, String> {
12978 let s = with_host(|h| {
12979 h.alloc(JsObj::Set {
12980 entries: IndexMap::new(),
12981 weak: false,
12982 })
12983 });
12984 for v in items {
12985 set_method(&s, "add", vec![v])?;
12986 }
12987 Ok(s)
12988}
12989
12990fn set_contains(s: &Value, v: &Value) -> bool {
12991 let key = with_host(|h| host::map_key(h, v));
12992 with_host(
12993 |h| matches!(h.get(s), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
12994 )
12995}
12996
12997/// The seven ES2025 set operations (24.2.4.3, .8, .5, .16, .10, .12, .7).
12998///
12999/// Each one branches on the two sizes and iterates the SMALLER side — not an
13000/// optimization but observable behaviour: which side is walked decides the
13001/// result's order and whether the operand's `has` or its `keys` is the method
13002/// that runs. `intersection` of a 3-element receiver with a 2-element operand
13003/// yields the operand's order, and its `keys` (never its `has`) is called.
13004fn set_operation(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13005 require_set_receiver(recv, name)?;
13006 let other = get_set_record(&arg0(&args), name)?;
13007 let my_size = set_size(recv);
13008 match name {
13009 "union" => {
13010 let keys = other.keys()?;
13011 let mut out = set_values(recv);
13012 out.extend(keys);
13013 new_set(out)
13014 }
13015 "intersection" => {
13016 let mut out = Vec::new();
13017 if my_size <= other.size {
13018 for v in set_values(recv) {
13019 if other.has(&v)? {
13020 out.push(v);
13021 }
13022 }
13023 } else {
13024 for k in other.keys()? {
13025 if set_contains(recv, &k) {
13026 out.push(k);
13027 }
13028 }
13029 }
13030 new_set(out)
13031 }
13032 "difference" => {
13033 if my_size <= other.size {
13034 let mut out = Vec::new();
13035 for v in set_values(recv) {
13036 if !other.has(&v)? {
13037 out.push(v);
13038 }
13039 }
13040 return new_set(out);
13041 }
13042 let out = new_set(set_values(recv))?;
13043 for k in other.keys()? {
13044 set_method(&out, "delete", vec![k])?;
13045 }
13046 Ok(out)
13047 }
13048 "symmetricDifference" => {
13049 // The operand is drained FIRST — the spec takes the iterator before
13050 // it copies `[[SetData]]`, so a `keys` that mutates the receiver is
13051 // reflected in the result.
13052 let keys = other.keys()?;
13053 let out = new_set(set_values(recv))?;
13054 for k in keys {
13055 if set_contains(recv, &k) {
13056 set_method(&out, "delete", vec![k])?;
13057 } else {
13058 set_method(&out, "add", vec![k])?;
13059 }
13060 }
13061 Ok(out)
13062 }
13063 "isSubsetOf" => {
13064 if my_size > other.size {
13065 return Ok(Value::Bool(false));
13066 }
13067 for v in set_values(recv) {
13068 if !other.has(&v)? {
13069 return Ok(Value::Bool(false));
13070 }
13071 }
13072 Ok(Value::Bool(true))
13073 }
13074 "isSupersetOf" => {
13075 if my_size < other.size {
13076 return Ok(Value::Bool(false));
13077 }
13078 for k in other.keys()? {
13079 if !set_contains(recv, &k) {
13080 return Ok(Value::Bool(false));
13081 }
13082 }
13083 Ok(Value::Bool(true))
13084 }
13085 "isDisjointFrom" => {
13086 if my_size <= other.size {
13087 for v in set_values(recv) {
13088 if other.has(&v)? {
13089 return Ok(Value::Bool(false));
13090 }
13091 }
13092 } else {
13093 for k in other.keys()? {
13094 if set_contains(recv, &k) {
13095 return Ok(Value::Bool(false));
13096 }
13097 }
13098 }
13099 Ok(Value::Bool(true))
13100 }
13101 _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13102 }
13103}
13104
13105fn set_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13106 match name {
13107 "add" => {
13108 let vv = normalize_zero_key(arg0(&args));
13109 reject_non_object_weak_key(recv, &vv, "WeakSet")?;
13110 let key = with_host(|h| host::map_key(h, &vv));
13111 with_host(|h| {
13112 if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13113 entries.insert(key, vv);
13114 }
13115 });
13116 Ok(recv.clone())
13117 }
13118 "has" => {
13119 let key = with_host(|h| host::map_key(h, &arg0(&args)));
13120 Ok(Value::Bool(with_host(
13121 |h| matches!(h.get(recv), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13122 )))
13123 }
13124 "delete" => {
13125 let key = with_host(|h| host::map_key(h, &arg0(&args)));
13126 Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
13127 Some(JsObj::Set { entries, .. }) => entries.shift_remove(&key).is_some(),
13128 _ => false,
13129 })))
13130 }
13131 "clear" => {
13132 with_host(|h| {
13133 if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13134 entries.clear();
13135 }
13136 });
13137 Ok(Value::Undef)
13138 }
13139 "forEach" => {
13140 let cb = arg0(&args);
13141 let vals: Vec<Value> = with_host(|h| match h.get(recv) {
13142 Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13143 _ => Vec::new(),
13144 });
13145 for v in vals {
13146 host::invoke(&cb, vec![v.clone(), v, recv.clone()], this_arg(&args, 1))?;
13147 }
13148 Ok(Value::Undef)
13149 }
13150 "union"
13151 | "intersection"
13152 | "difference"
13153 | "symmetricDifference"
13154 | "isSubsetOf"
13155 | "isSupersetOf"
13156 | "isDisjointFrom" => set_operation(recv, name, args),
13157 // LIVE, as for `Map`. A Set's `keys` and `values` are the same thing.
13158 "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
13159 recv,
13160 if name == "entries" {
13161 "entries"
13162 } else {
13163 "values"
13164 },
13165 )),
13166 _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13167 }
13168}
13169
13170fn generator_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13171 // A generator IS its own iterator: both symbol forms return the receiver.
13172 if matches!(name, "@@iterator" | "@@asyncIterator") {
13173 return Ok(recv.clone());
13174 }
13175 // An `async function*` object's methods return PROMISES of the record, and
13176 // its body has to be driven through the await-aware stepper (a plain
13177 // `gen_resume` would surface an internal `await` suspension as a bogus yield).
13178 if host::is_async_generator(recv) {
13179 // All three go through `[[AsyncGeneratorQueue]]` (ECMA-262 27.6.3.6):
13180 // `.return`/`.throw` must wait behind a `.next()` that is still
13181 // suspended on an internal `await`, or that `.next()` would report
13182 // `{done: true}` for a value the body had not yet reached. An uncaught
13183 // `.throw(e)` rejects the returned promise; it does not throw here.
13184 return match name {
13185 "next" => Ok(host::async_gen_enqueue(
13186 recv,
13187 host::GenReq::Next(arg0(&args)),
13188 )),
13189 "return" => Ok(host::async_gen_enqueue(
13190 recv,
13191 host::GenReq::Return(arg0(&args)),
13192 )),
13193 "throw" => Ok(host::async_gen_enqueue(
13194 recv,
13195 host::GenReq::Throw(arg0(&args)),
13196 )),
13197 "@@asyncIterator" => Ok(recv.clone()),
13198 _ => Err(host::type_error(&format!(
13199 "asyncGenerator.{name} is not a function"
13200 ))),
13201 };
13202 }
13203 match name {
13204 "next" => {
13205 let send = arg0(&args);
13206 match host::gen_resume(recv, send)? {
13207 host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13208 host::GenStep::Done(v) => Ok(iter_result(v, true)),
13209 }
13210 }
13211 "return" => {
13212 // Resume with an injected return so any pending `finally` runs; the
13213 // completion may itself be a `finally` yield (not-done) or the value.
13214 match host::gen_return(recv, arg0(&args))? {
13215 host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13216 host::GenStep::Done(v) => Ok(iter_result(v, true)),
13217 }
13218 }
13219 "throw" => {
13220 // Inject a throw at the suspension point: an enclosing `try/catch` in
13221 // the body can handle it (and any `finally` runs); otherwise it
13222 // propagates to the caller.
13223 match host::gen_throw(recv, arg0(&args))? {
13224 host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13225 host::GenStep::Done(v) => Ok(iter_result(v, true)),
13226 }
13227 }
13228 _ => Err(host::type_error(&format!(
13229 "generator.{name} is not a function"
13230 ))),
13231 }
13232}
13233
13234/// A `{ value, done }` iterator-result object.
13235fn iter_result(value: Value, done: bool) -> Value {
13236 with_host(|h| {
13237 let mut m: IndexMap<String, Value> = IndexMap::new();
13238 m.insert("value".into(), value);
13239 m.insert("done".into(), Value::Bool(done));
13240 h.new_object(m)
13241 })
13242}
13243
13244/// Built-in iterator object (`arr.values()`, `arr[Symbol.iterator]()`): a lazy
13245/// cursor over a materialized item list.
13246fn iter_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13247 match name {
13248 "next" => {
13249 let step = with_host(|h| {
13250 if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
13251 if *idx < items.len() {
13252 let v = items[*idx].clone();
13253 *idx += 1;
13254 return Some(v);
13255 }
13256 }
13257 None
13258 });
13259 Ok(match step {
13260 Some(v) => iter_result(v, false),
13261 None => iter_result(Value::Undef, true),
13262 })
13263 }
13264 "return" => {
13265 // Exhaust the cursor and report done.
13266 with_host(|h| {
13267 if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
13268 *idx = items.len();
13269 }
13270 });
13271 Ok(iter_result(arg0(&args), true))
13272 }
13273 // An iterator is its own iterable.
13274 "@@iterator" => Ok(recv.clone()),
13275 _ => Err(host::type_error(&format!(
13276 "iterator.{name} is not a function"
13277 ))),
13278 }
13279}
13280
13281fn symbol_method(recv: &Value, name: &str, _args: Vec<Value>) -> Result<Value, String> {
13282 match name {
13283 "toString" => Ok(with_host(|h| {
13284 let s = h.str_of(recv);
13285 h.new_str(s)
13286 })),
13287 // 20.4.3.5: `Symbol.prototype[@@toPrimitive]` returns the symbol
13288 // itself for EVERY hint — it ignores its argument. That is what makes
13289 // `sym + ''` a TypeError rather than a concatenation: the conversion
13290 // succeeds and hands back a symbol, and it is `+` that then rejects it.
13291 "@@toPrimitive" | "valueOf" => Ok(recv.clone()),
13292 _ => Err(host::type_error(&format!(
13293 "symbol.{name} is not a function"
13294 ))),
13295 }
13296}
13297
13298// ══ Object.* prototype helpers, `in`, deep clone ═════════════════════════════
13299
13300fn object_create(args: Vec<Value>) -> Result<Value, String> {
13301 let proto = arg0(&args);
13302 // 20.1.2.2 step 1: the prototype must be an Object or exactly `null`.
13303 // `undefined` is NOT accepted — measured on node v26.7.0,
13304 // `Object.create(undefined)` is
13305 // `TypeError: Object prototype may only be an Object or null: undefined`,
13306 // where node-js quietly built a normal object.
13307 reject_bad_prototype(&proto)?;
13308 let obj = with_host(|h| h.new_object(IndexMap::new()));
13309 // `set_proto` records a null proto as an explicit null-prototype object.
13310 with_host(|h| h.set_proto(&obj, proto));
13311 // Optional second arg: a property-descriptor map.
13312 if let Some(descs) = args.get(1).filter(|d| !matches!(d, Value::Undef)) {
13313 let entries: Vec<(String, Value)> = with_host(|h| match h.get(descs) {
13314 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
13315 _ => Vec::new(),
13316 });
13317 for (k, d) in entries {
13318 apply_descriptor(&obj, &k, &d)?;
13319 }
13320 }
13321 Ok(obj)
13322}
13323
13324/// The enumerable method names of a builtin `<Ctor>.prototype` namespace that
13325/// supports being copied via `mixin`/`getOwnPropertyNames`. Currently only
13326/// `EventEmitter.prototype` (the one express mixes onto its app function).
13327/// The own property names of `<Ctor>.prototype`, and whether each is
13328/// enumerable, from the generated [`crate::arity::PROTO_MEMBERS`] table.
13329///
13330/// `Object.getOwnPropertyNames(Map.prototype)` answered `[]` for every
13331/// intrinsic — the members are reachable by NAME through the `@proto:` thunks
13332/// but were not enumerable, so feature detection that walks a prototype found
13333/// nothing there. The table is read from the reference engine rather than
13334/// derived from the arity table because the arity table holds functions only:
13335/// `Map.prototype.size`, `RegExp.prototype.source` and the twelve
13336/// `URL.prototype` components are accessors.
13337fn intrinsic_proto_members(ns: &str) -> Option<&'static [&'static str]> {
13338 let ctor = ns.strip_suffix(".prototype")?;
13339 crate::arity::PROTO_MEMBERS
13340 .binary_search_by(|(k, _)| (*k).cmp(ctor))
13341 .ok()
13342 .map(|i| crate::arity::PROTO_MEMBERS[i].1)
13343}
13344
13345fn builtin_proto_method_names(ns: &str) -> Option<&'static [&'static str]> {
13346 match ns {
13347 "EventEmitter.prototype" => Some(crate::stdlib::events::METHODS),
13348 _ => None,
13349 }
13350}
13351
13352/// The own SYMBOL-keyed property keys of `v` as symbol values. A Proxy's come
13353/// from its `ownKeys` trap (the symbol half of the same list the string keys are
13354/// filtered out of); every other receiver answers from its property map.
13355fn proxy_or_own_symbol_keys(v: &Value) -> Result<Vec<Value>, String> {
13356 if let Some(keys) = crate::proxy::own_keys(v)? {
13357 return Ok(keys
13358 .iter()
13359 .filter(|k| host::is_symbol_key(k))
13360 .map(|k| crate::proxy::key_value(k))
13361 .collect());
13362 }
13363 // An intrinsic prototype's symbol-keyed members come from the generated
13364 // table, which is the only record of them: they own no map entry, so
13365 // `Object.getOwnPropertySymbols(Array.prototype)` was `[]` where node
13366 // reports `Symbol.iterator` and `Symbol.unscopables`.
13367 if let Some(ns) = intrinsic_proto_of(v).map(|c| format!("{c}.prototype")) {
13368 if let Some(members) = intrinsic_proto_members(&ns) {
13369 return Ok(with_host(|h| {
13370 members
13371 .iter()
13372 .filter_map(|m| m.strip_prefix('+').unwrap_or(m).strip_prefix("@@"))
13373 .map(|name| h.well_known_symbol(name))
13374 .collect()
13375 }));
13376 }
13377 }
13378 Ok(with_host(|h| h.own_symbol_keys(v)))
13379}
13380
13381/// `[[DefineOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13382pub fn define_property_pub(obj: &Value, key: Value, desc: Value) -> Result<Value, String> {
13383 object_define_property(vec![obj.clone(), key, desc])
13384}
13385
13386/// `[[GetOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13387pub fn own_descriptor_pub(obj: &Value, key: Value) -> Result<Value, String> {
13388 object_get_own_descriptor(vec![obj.clone(), key])
13389}
13390
13391fn object_define_property(args: Vec<Value>) -> Result<Value, String> {
13392 let obj = arg0(&args);
13393 // A Proxy defines through its `defineProperty` trap; the target it forwards
13394 // to is where the ordinary path below finally runs.
13395 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
13396 let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13397 let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13398 if !with_host(|h| is_object_like(h, &desc)) {
13399 return Err(host::type_error(&format!(
13400 "Property description must be an object: {}",
13401 with_host(|h| h.str_of(&desc))
13402 )));
13403 }
13404 // `Object.defineProperty` THROWS on a refusing trap — in sloppy code
13405 // too. `Reflect.defineProperty` is the form that reports `false`.
13406 if !crate::proxy::define_property(&obj, &key, &desc)? {
13407 return Err(host::type_error(&format!(
13408 "'defineProperty' on proxy: trap returned falsish for property '{key}'"
13409 )));
13410 }
13411 return Ok(obj);
13412 }
13413 // 20.1.2.4 steps 1-3, both of which node-js skipped entirely: a non-object
13414 // target and a non-object descriptor each throw before anything is written.
13415 if !with_host(|h| is_object_like(h, &obj)) {
13416 return Err(host::type_error(
13417 "Object.defineProperty called on non-object",
13418 ));
13419 }
13420 let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13421 if !with_host(|h| is_object_like(h, &desc)) {
13422 return Err(host::type_error(&format!(
13423 "Property description must be an object: {}",
13424 with_host(|h| h.str_of(&desc))
13425 )));
13426 }
13427 let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13428 apply_descriptor(&obj, &key, &desc)?;
13429 Ok(obj)
13430}
13431
13432/// Every `Reflect` method requires an OBJECT target and reports a `TypeError`
13433/// for anything else (28.1). A primitive was being accepted and silently
13434/// producing nothing.
13435/// `CreateListFromArrayLike` (7.3.18) — the argument list `Reflect.apply` and
13436/// `Reflect.construct` take.
13437///
13438/// An ARRAY-LIKE counts: `{length: 2, 0: 1, 1: 5}` is a two-element list. The
13439/// iterator was being used instead, so an array-like produced nothing and a
13440/// primitive produced nothing rather than the TypeError node raises.
13441/// Whether `p` is ALREADY `obj`'s prototype — the one case a non-extensible
13442/// object still accepts, because it changes nothing.
13443///
13444/// The observable prototype, not the stored link: an ordinary object has no
13445/// explicit link and inherits `Object.prototype`, so comparing the raw slot
13446/// reported "different" for `setPrototypeOf(frozen, Object.prototype)`.
13447/// Whether making `p` the prototype of `obj` would create a CYCLE — 10.1.2.1
13448/// step 8 walks up from `p` looking for `obj`.
13449///
13450/// Without the check `Object.setPrototypeOf(a, b)` followed by the reverse
13451/// built a ring. Nothing hung, because every chain walk in this host carries a
13452/// hop limit, but a lookup then silently gave up instead of finding a property
13453/// that really was there.
13454fn would_cycle(obj: &Value, p: &Value) -> bool {
13455 let mut cur = Some(p.clone());
13456 for _ in 0..1000 {
13457 let Some(c) = cur else { return false };
13458 if with_host(|h| h.strict_eq(&c, obj)) {
13459 return true;
13460 }
13461 // A PROXY's prototype is its handler's business; the spec skips the
13462 // walk entirely when one is in the chain.
13463 if with_host(|h| h.kind_of(&c)) == Some(ObjKind::Proxy) {
13464 return false;
13465 }
13466 cur = with_host(|h| h.proto_of(&c));
13467 }
13468 false
13469}
13470
13471fn same_prototype(obj: &Value, p: &Value) -> bool {
13472 let cur = prototype_of(obj);
13473 with_host(|h| h.strict_eq(&cur, p) || (h.is_null(&cur) && h.is_null(p)))
13474}
13475
13476fn create_list_from_array_like(v: &Value) -> Result<Vec<Value>, String> {
13477 if !with_host(|h| is_object_like(h, v)) {
13478 return Err(host::type_error(
13479 "CreateListFromArrayLike called on non-object",
13480 ));
13481 }
13482 let len = get_property(v, "length")?;
13483 let n = with_host(|h| h.to_number(&len));
13484 let n = if n.is_finite() && n > 0.0 {
13485 n as usize
13486 } else {
13487 0
13488 };
13489 (0..n).map(|i| get_property(v, &i.to_string())).collect()
13490}
13491
13492fn reflect_require_object(v: &Value, method: &str) -> Result<(), String> {
13493 if with_host(|h| is_object_like(h, v)) {
13494 return Ok(());
13495 }
13496 Err(host::type_error(&format!(
13497 "Reflect.{method} called on non-object"
13498 )))
13499}
13500
13501/// Whether `v` is an Object in the language sense — anything `typeof` calls
13502/// `"object"` (bar `null`) or `"function"`. Used by the argument checks that
13503/// distinguish "an object" from a primitive.
13504fn is_object_like(h: &host::JsHost, v: &Value) -> bool {
13505 matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v)
13506}
13507
13508/// `RequireObjectCoercible(v)` — 7.2.1. The check in front of every `ToObject`,
13509/// which node-js was missing on the whole `Object.keys`/`values`/`entries`/
13510/// `getOwnPropertyNames`/`getOwnPropertySymbols`/`getOwnPropertyDescriptor`/
13511/// `assign` family: each returned an empty result for `null` where node v26.7.0
13512/// throws `TypeError: Cannot convert undefined or null to object`. A PRIMITIVE
13513/// is coercible and keeps working (`Object.keys(1)` is `[]`).
13514fn require_object_coercible(v: &Value) -> Result<(), String> {
13515 if with_host(|h| matches!(v, Value::Undef) || h.is_null(v)) {
13516 return Err(host::type_error(
13517 "Cannot convert undefined or null to object",
13518 ));
13519 }
13520 Ok(())
13521}
13522
13523/// 10.1.2 / 20.1.2.2 step 1: reject a `[[Prototype]]` that is neither an Object
13524/// nor `null`, with V8's wording. Measured on node v26.7.0:
13525/// `Object.create("s")` is
13526/// `TypeError: Object prototype may only be an Object or null: s`.
13527fn reject_bad_prototype(proto: &Value) -> Result<(), String> {
13528 if with_host(|h| h.is_null(proto) || is_object_like(h, proto)) {
13529 return Ok(());
13530 }
13531 Err(host::type_error(&format!(
13532 "Object prototype may only be an Object or null: {}",
13533 with_host(|h| h.str_of(proto))
13534 )))
13535}
13536
13537/// Apply a `{ value | get | set }` descriptor object to `obj[key]`.
13538///
13539/// Per ECMAScript `ToPropertyDescriptor`, an omitted `writable`/`enumerable`/
13540/// `configurable` field defaults to **false** — which is why a `defineProperty`
13541/// data property is invisible to `Object.keys` unless the caller opts in. That
13542/// asymmetry against plain assignment is the whole reason the attribute table
13543/// exists.
13544/// The requested fields of a property descriptor — 10.1.6.2
13545/// `ToPropertyDescriptor`. Each is `None` when the descriptor omits it, which
13546/// is the distinction the merge below turns on: an omitted field LEAVES an
13547/// existing attribute alone rather than resetting it.
13548struct Requested {
13549 value: Option<Value>,
13550 get: Option<Option<Value>>,
13551 set: Option<Option<Value>>,
13552 writable: Option<bool>,
13553 enumerable: Option<bool>,
13554 configurable: Option<bool>,
13555}
13556
13557impl Requested {
13558 /// Reads through the prototype chain, as `ToPropertyDescriptor`'s
13559 /// `HasProperty`/`Get` pairs do — a descriptor built with
13560 /// `Object.create({ value: 1 })` is legal.
13561 fn read(desc: &Value) -> Self {
13562 let has = |k: &str| {
13563 with_host(|h| {
13564 host::lookup_chain(h, desc, k).is_some()
13565 || host::lookup_accessor(h, desc, k).is_some()
13566 })
13567 };
13568 let val = |k: &str| get_property(desc, k).unwrap_or(Value::Undef);
13569 // Resolve the value BEFORE the borrow: `val` re-enters the host, and
13570 // doing it inside the `with_host` closure aborts on the double borrow.
13571 let flag = |k: &str| {
13572 has(k).then(|| {
13573 let v = val(k);
13574 with_host(|h| h.truthy(&v))
13575 })
13576 };
13577 Requested {
13578 value: has("value").then(|| val("value")),
13579 get: has("get").then(|| match val("get") {
13580 Value::Undef => None,
13581 g => Some(g),
13582 }),
13583 set: has("set").then(|| match val("set") {
13584 Value::Undef => None,
13585 st => Some(st),
13586 }),
13587 writable: flag("writable"),
13588 enumerable: flag("enumerable"),
13589 configurable: flag("configurable"),
13590 }
13591 }
13592
13593 fn is_accessor(&self) -> bool {
13594 self.get.is_some() || self.set.is_some()
13595 }
13596
13597 fn is_data(&self) -> bool {
13598 self.value.is_some() || self.writable.is_some()
13599 }
13600}
13601
13602/// The own property already at `key`, if any, read back through
13603/// `Object.getOwnPropertyDescriptor` so every object kind (array indices, the
13604/// fn-prop side table, Buffer bytes) is covered by one code path.
13605struct Existing {
13606 accessor: bool,
13607 value: Value,
13608 get: Option<Value>,
13609 set: Option<Value>,
13610 writable: bool,
13611 enumerable: bool,
13612 configurable: bool,
13613}
13614
13615fn existing_property(obj: &Value, key: &str) -> Option<Existing> {
13616 let k = with_host(|h| h.new_str(key.to_string()));
13617 let d = own_descriptor_pub(obj, k).ok()?;
13618 if matches!(d, Value::Undef) {
13619 return None;
13620 }
13621 let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
13622 let truthy = |n: &str| {
13623 let v = field(n);
13624 with_host(|h| h.truthy(&v))
13625 };
13626 let accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
13627 Some(Existing {
13628 accessor,
13629 value: field("value"),
13630 get: match field("get") {
13631 Value::Undef => None,
13632 g => Some(g),
13633 },
13634 set: match field("set") {
13635 Value::Undef => None,
13636 st => Some(st),
13637 },
13638 writable: truthy("writable"),
13639 enumerable: truthy("enumerable"),
13640 configurable: truthy("configurable"),
13641 })
13642}
13643
13644/// SameValue (7.2.11) — `===` except that `NaN` equals itself and `+0` and
13645/// `-0` are distinct. 10.1.6.3 compares a redefined value against the current
13646/// one with this, not with strict equality.
13647pub(crate) fn same_value(a: &Value, b: &Value) -> bool {
13648 let num = |v: &Value| match v {
13649 Value::Int(n) => Some(*n as f64),
13650 Value::Float(f) => Some(*f),
13651 _ => None,
13652 };
13653 match (num(a), num(b)) {
13654 (Some(x), Some(y)) => {
13655 if x.is_nan() && y.is_nan() {
13656 true
13657 } else if x == 0.0 && y == 0.0 {
13658 x.is_sign_negative() == y.is_sign_negative()
13659 } else {
13660 x == y
13661 }
13662 }
13663 _ => with_host(|h| h.strict_eq(a, b)),
13664 }
13665}
13666
13667/// 10.1.6.3 `ValidateAndApplyPropertyDescriptor`.
13668///
13669/// None of the validation existed: every `Object.defineProperty` was applied
13670/// unconditionally, so redefining a non-configurable property silently
13671/// succeeded where node throws. Worse in practice, an OMITTED field was read as
13672/// `false` rather than "leave alone", so the ordinary
13673/// `Object.defineProperty(o, 'k', { enumerable: false })` also stripped
13674/// `writable` and `configurable` from a property that had both.
13675///
13676/// Converting an accessor to a data property did not take effect at all: the
13677/// value was written but the accessor stayed in its side table, and accessors
13678/// win on read, so the getter kept answering.
13679fn apply_descriptor(obj: &Value, key: &str, desc: &Value) -> Result<(), String> {
13680 let req = Requested::read(desc);
13681 let cur = existing_property(obj, key);
13682
13683 // An array's `length` is the exotic own property whose write resizes the
13684 // array (10.4.2.1); routing it through the ordinary path stored a shadowing
13685 // key and left the elements untouched.
13686 if key == "length" && with_host(|h| h.kind_of(obj)) == Some(ObjKind::Array) {
13687 if let Some(v) = req.value.clone() {
13688 return set_property_pub(obj, "length", v);
13689 }
13690 }
13691
13692 // The other exotics whose own properties are SYNTHESIZED rather than stored
13693 // in a property map: a typed array's elements and a RegExp's `lastIndex`.
13694 // The ordinary path below writes a shadowing map entry the read never
13695 // consults, so `Object.defineProperty(u8, '0', {value: 9})` left `u8[0]`
13696 // unchanged.
13697 // A builtin namespace/prototype has no property map either, so a data
13698 // descriptor has to reach the same side table an assignment does.
13699 // `Object.defineProperty(Array.prototype, 'at', {value: impl})` — how a
13700 // careful polyfill installs itself, precisely to avoid the enumerable
13701 // property a bare assignment creates — wrote a map entry nothing read.
13702 if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Builtin) {
13703 if let Some(v) = req.value.clone() {
13704 return set_property_pub(obj, key, v);
13705 }
13706 }
13707 let exotic_own = (crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray")
13708 && key.parse::<usize>().is_ok())
13709 || (key == "lastIndex" && with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))));
13710 if exotic_own {
13711 if let Some(v) = req.value.clone() {
13712 return set_property_pub(obj, key, v);
13713 }
13714 }
13715
13716 // 10.1.6.3 step 2: a NEW property cannot be added to a non-extensible
13717 // object. Only an existing property's attributes were being validated, so
13718 // `defineProperty(Object.freeze({}), 'z', …)` silently added one.
13719 if cur.is_none() && !with_host(|h| h.is_extensible(obj)) {
13720 return Err(host::type_error(&format!(
13721 "Cannot define property {key}, object is not extensible"
13722 )));
13723 }
13724 if let Some(c) = &cur {
13725 if !c.configurable {
13726 let rejected = req.configurable == Some(true)
13727 || req.enumerable.is_some_and(|e| e != c.enumerable)
13728 || (req.is_accessor() && !c.accessor)
13729 || (req.is_data() && c.accessor)
13730 || (c.accessor
13731 && ((req.get.is_some() && req.get.clone().flatten() != c.get)
13732 || (req.set.is_some() && req.set.clone().flatten() != c.set)))
13733 || (!c.accessor
13734 && !c.writable
13735 && (req.writable == Some(true)
13736 || req.value.as_ref().is_some_and(|v| !same_value(v, &c.value))));
13737 if rejected {
13738 return Err(host::type_error(&format!(
13739 "Cannot redefine property: {key}"
13740 )));
13741 }
13742 }
13743 }
13744
13745 // An omitted field keeps what the property already had; a brand-new
13746 // property defaults every one of them to false.
13747 let attrs = host::PropAttrs {
13748 writable: req
13749 .writable
13750 .unwrap_or(cur.as_ref().is_some_and(|c| c.writable)),
13751 enumerable: req
13752 .enumerable
13753 .unwrap_or(cur.as_ref().is_some_and(|c| c.enumerable)),
13754 configurable: req
13755 .configurable
13756 .unwrap_or(cur.as_ref().is_some_and(|c| c.configurable)),
13757 };
13758 with_host(|h| h.set_prop_attrs(obj, key, attrs));
13759
13760 if req.is_accessor() {
13761 let get = req
13762 .get
13763 .clone()
13764 .unwrap_or_else(|| cur.as_ref().and_then(|c| c.get.clone()));
13765 let set = req
13766 .set
13767 .clone()
13768 .unwrap_or_else(|| cur.as_ref().and_then(|c| c.set.clone()));
13769 // An ACCESSOR at an index past the end still extends the array
13770 // (10.4.2.1): `Object.defineProperty([1], '4', {get})` gives
13771 // `length === 5` with holes between. Only the DATA path grew it, so
13772 // the accessor landed in the side table while `length` stayed put —
13773 // and with it out of range, `Object.keys` and `JSON.stringify` never
13774 // saw the index at all.
13775 if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>())
13776 {
13777 with_host(|h| {
13778 let old_len = match h.get(obj) {
13779 Some(JsObj::Array(items)) => items.len(),
13780 _ => 0,
13781 };
13782 if i >= old_len {
13783 if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13784 items.resize(i + 1, Value::Undef);
13785 }
13786 h.mark_hole_range(obj, old_len..i + 1);
13787 }
13788 });
13789 }
13790 with_host(|h| h.set_accessor(obj, key, get, set));
13791 return Ok(());
13792 }
13793
13794 if let Some(c) = &cur {
13795 if c.accessor {
13796 if !req.is_data() {
13797 // A generic descriptor — flags only — leaves an accessor an
13798 // accessor. They were already applied above.
13799 return Ok(());
13800 }
13801 let v = req.value.clone().unwrap_or(Value::Undef);
13802 with_host(|h| h.accessor_to_data(obj, key, v));
13803 return Ok(());
13804 }
13805 }
13806
13807 let Some(v) = req.value else {
13808 // Nothing to write: a flags-only redefinition of a data property.
13809 return Ok(());
13810 };
13811 write_data_slot(obj, key, v);
13812 Ok(())
13813}
13814
13815/// Store `v` as an own data property, in whichever slot the object kind keeps
13816/// its own properties.
13817fn write_data_slot(obj: &Value, key: &str, v: Value) {
13818 // A function/class receiver stores its own props in the fn-prop side table
13819 // (express `mixin(app, proto)` defines methods onto the `app` *function*).
13820 if matches!(
13821 with_host(|h| h.get(obj).cloned()),
13822 Some(JsObj::Func(_)) | Some(JsObj::Class(_))
13823 ) || uses_side_table(obj)
13824 {
13825 with_host(|h| h.set_fn_prop(obj, key, v));
13826 return;
13827 }
13828 if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>()) {
13829 // An array's index keys ARE its elements, and defining one past the end
13830 // grows the array with holes in between (10.4.2.1). This whole branch
13831 // used to be missing: `Object.defineProperty(arr, 1, {value})` wrote
13832 // into the ordinary property map an array does not have, so it was a
13833 // silent no-op.
13834 with_host(|h| {
13835 let old = match h.get(obj) {
13836 Some(JsObj::Array(items)) => items.len(),
13837 _ => 0,
13838 };
13839 if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13840 if i >= old {
13841 items.resize(i + 1, Value::Undef);
13842 }
13843 items[i] = v;
13844 }
13845 if i > old {
13846 h.mark_hole_range(obj, old..i);
13847 }
13848 h.clear_hole(obj, i);
13849 });
13850 return;
13851 }
13852 with_host(|h| {
13853 if let Some(JsObj::Object(p)) = h.get_mut(obj) {
13854 p.insert(key.to_string(), v);
13855 host::canonicalize_own_keys(p);
13856 }
13857 });
13858}
13859
13860/// `Object.defineProperties(obj, descriptorMap)`.
13861fn object_define_properties(args: Vec<Value>) -> Result<Value, String> {
13862 let obj = arg0(&args);
13863 let descs = args.get(1).cloned().unwrap_or(Value::Undef);
13864 let entries: Vec<(String, Value)> = with_host(|h| match h.get(&descs) {
13865 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
13866 _ => Vec::new(),
13867 });
13868 for (k, d) in entries {
13869 apply_descriptor(&obj, &k, &d)?;
13870 }
13871 Ok(obj)
13872}
13873
13874/// The descriptor of an own property a function, a typed array or a RegExp
13875/// SYNTHESIZES rather than keeping in a property map.
13876///
13877/// These read back through the ordinary path but owned no descriptor and did
13878/// not appear under `hasOwnProperty` or `getOwnPropertyNames`, so the five
13879/// views of "does this property exist" disagreed — a read said yes while
13880/// `Object.getOwnPropertyDescriptor(f, 'name')` said no such property, which is
13881/// what a shim checks before patching.
13882fn synthesized_own_descriptor(obj: &Value, key: &str) -> Option<(Value, host::PropAttrs)> {
13883 let ro_configurable = host::PropAttrs {
13884 writable: false,
13885 enumerable: false,
13886 configurable: true,
13887 };
13888 // A callable's `length`/`name` are read-only but configurable; its
13889 // `prototype` is writable and NOT configurable, and a class's is neither.
13890 // An arrow, a method and a bound function own no `prototype` at all.
13891 if with_host(|h| host::is_callable(h, obj)) && !matches!(key, "length" | "name" | "prototype") {
13892 return None;
13893 }
13894 if with_host(|h| host::is_callable(h, obj)) {
13895 if key == "prototype" {
13896 let p = get_property(obj, "prototype").ok()?;
13897 if matches!(p, Value::Undef) {
13898 return None;
13899 }
13900 return Some((
13901 p,
13902 host::PropAttrs {
13903 writable: with_host(|h| h.kind_of(obj)) != Some(ObjKind::Class),
13904 enumerable: false,
13905 configurable: false,
13906 },
13907 ));
13908 }
13909 return Some((get_property(obj, key).ok()?, ro_configurable));
13910 }
13911 // A typed array's elements are own, enumerable, writable, configurable
13912 // properties; an index past the end owns nothing.
13913 if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") {
13914 let v = crate::stdlib::typedarray::elem_get(obj, key)?;
13915 return Some((
13916 v,
13917 host::PropAttrs {
13918 writable: true,
13919 enumerable: true,
13920 configurable: true,
13921 },
13922 ));
13923 }
13924 // A RegExp's `lastIndex` is its own, writable, non-configurable cursor.
13925 if with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))) && key == "lastIndex" {
13926 return Some((
13927 get_property(obj, "lastIndex").ok()?,
13928 host::PropAttrs {
13929 writable: true,
13930 enumerable: false,
13931 configurable: false,
13932 },
13933 ));
13934 }
13935 None
13936}
13937
13938fn object_get_own_descriptor(args: Vec<Value>) -> Result<Value, String> {
13939 let obj = arg0(&args);
13940 require_object_coercible(&obj)?;
13941 let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13942 // A string primitive's boxed own properties: each code-unit index is an
13943 // enumerable, non-writable, non-configurable data property, and `length` is
13944 // the same minus enumerable.
13945 if let Some(units) = string_primitive_units(&obj) {
13946 let entry = match key.parse::<usize>() {
13947 Ok(i) => units
13948 .get(i)
13949 .map(|c| (with_host(|h| h.new_str(c.clone())), true)),
13950 Err(_) if key == "length" => Some((Value::Float(units.len() as f64), false)),
13951 Err(_) => None,
13952 };
13953 return Ok(match entry {
13954 Some((value, enumerable)) => with_host(|h| {
13955 let mut m: IndexMap<String, Value> = IndexMap::new();
13956 m.insert("value".into(), value);
13957 m.insert("writable".into(), Value::Bool(false));
13958 m.insert("enumerable".into(), Value::Bool(enumerable));
13959 m.insert("configurable".into(), Value::Bool(false));
13960 h.new_object(m)
13961 }),
13962 None => Value::Undef,
13963 });
13964 }
13965 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
13966 return Ok(crate::proxy::get_own_descriptor(&obj, &key)?.unwrap_or(Value::Undef));
13967 }
13968 // A method read off an enumerable builtin prototype (`EventEmitter.prototype`)
13969 // yields a `{ value: <method thunk> }` data descriptor so `mixin` can copy it.
13970 if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&obj).cloned()) {
13971 if let Some(names) = builtin_proto_method_names(&ns) {
13972 if names.contains(&key.as_str()) {
13973 return Ok(with_host(|h| {
13974 let thunk = h.alloc(JsObj::Builtin(format!(
13975 "@proto:{}:{key}",
13976 ns.trim_end_matches(".prototype")
13977 )));
13978 let mut m: IndexMap<String, Value> = IndexMap::new();
13979 m.insert("value".into(), thunk);
13980 m.insert("writable".into(), Value::Bool(true));
13981 m.insert("enumerable".into(), Value::Bool(true));
13982 m.insert("configurable".into(), Value::Bool(true));
13983 h.new_object(m)
13984 }));
13985 }
13986 }
13987 }
13988 // A global the object does not own outright is still an own property of the
13989 // global object — the same lazy binding the bare identifier resolves to.
13990 // Every one of them reported `undefined`, so a feature probe written as
13991 // `getOwnPropertyDescriptor(globalThis, 'structuredClone')` concluded the
13992 // global was absent. The immutable trio (11.1.1 / 19.1.1-3) is frozen; the
13993 // rest are ordinary writable, non-enumerable, configurable bindings.
13994 if with_host(|h| h.is_global_object(&obj)) {
13995 let owned = with_host(|h| match h.get(&obj) {
13996 Some(JsObj::Object(p)) => p.contains_key(&key),
13997 _ => false,
13998 });
13999 if !owned && !CJS_WRAPPER_LOCALS.contains(&key.as_str()) {
14000 // A global a SCRIPT created — `x = 1` with no declaration — is an
14001 // ordinary enumerable property, unlike the builtins.
14002 let script_made = with_host(|h| h.read_global(&key).is_some());
14003 if let Some(v) = global_object_binding(&key) {
14004 let frozen = matches!(key.as_str(), "undefined" | "NaN" | "Infinity");
14005 return Ok(with_host(|h| {
14006 let mut m: IndexMap<String, Value> = IndexMap::new();
14007 m.insert("value".into(), v);
14008 m.insert("writable".into(), Value::Bool(!frozen));
14009 m.insert(
14010 "enumerable".into(),
14011 Value::Bool(script_made || ENUMERABLE_GLOBALS.contains(&key.as_str())),
14012 );
14013 m.insert("configurable".into(), Value::Bool(!frozen));
14014 h.new_object(m)
14015 }));
14016 }
14017 }
14018 }
14019 // Any other member of a builtin namespace (`Math.PI`, `Math.floor`,
14020 // `Array.prototype.slice`, a builtin function's own `name`/`length`). Every
14021 // one of these reads back a value, but none owned a DESCRIPTOR:
14022 // `Object.getOwnPropertyDescriptor(Math, 'PI')` was `undefined`, which reads
14023 // as "no such property" to the shim/polyfill family that probes a namespace
14024 // before patching it.
14025 // An ACCESSOR member describes itself with a `get`, never a `value` — and
14026 // it must do so without READING the property, since running the getter
14027 // against the prototype is exactly what throws. Both prototype
14028 // representations are covered, so `Symbol.prototype.description` and
14029 // `Map.prototype.size` answer alike; both were `undefined`, which reads as
14030 // "no such property" to anything that probes before patching.
14031 if let Some(ctor) = intrinsic_proto_of(&obj) {
14032 if is_proto_accessor(&ctor, &key) {
14033 let getter = proto_getter(&ctor, &key);
14034 // The poison pair is the only ECMAScript accessor here with a
14035 // SETTER, but a WebIDL class has plenty: `URL.prototype.href`,
14036 // `hostname` and the rest are all writable, and reporting them as
14037 // read-only made `Object.getOwnPropertyDescriptor(URL.prototype,
14038 // 'href').set` read `undefined` for a setter that runs.
14039 let writable = (ctor == "Function" && matches!(key.as_str(), "arguments" | "caller"))
14040 || crate::stdlib::instance_accessors(&ctor)
14041 .0
14042 .iter()
14043 .any(|(k, settable)| *k == key && *settable);
14044 let setter = writable
14045 .then(|| with_host(|h| h.alloc(JsObj::Builtin(format!("@protoset:{ctor}:{key}")))));
14046 return Ok(with_host(|h| {
14047 let mut m: IndexMap<String, Value> = IndexMap::new();
14048 m.insert("get".into(), getter);
14049 // `undefined`, not null: a read-only accessor has no setter at
14050 // all, and `JSON.stringify` of the descriptor must drop the key
14051 // rather than report `"set": null`.
14052 m.insert("set".into(), setter.unwrap_or(Value::Undef));
14053 m.insert("enumerable".into(), Value::Bool(is_webidl_proto(&ctor)));
14054 m.insert("configurable".into(), Value::Bool(true));
14055 h.new_object(m)
14056 }));
14057 }
14058 }
14059 if let Some(ns) = with_host(|h| match h.get(&obj) {
14060 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
14061 _ => None,
14062 }) {
14063 let value = namespace_property(&ns, &key);
14064 if !matches!(value, Value::Undef) {
14065 return Ok(builtin_member_descriptor(&ns, &key, value));
14066 }
14067 }
14068 if let Some((value, attrs)) = synthesized_own_descriptor(&obj, &key) {
14069 return Ok(with_host(|h| {
14070 let mut m: IndexMap<String, Value> = IndexMap::new();
14071 m.insert("value".into(), value);
14072 m.insert("writable".into(), Value::Bool(attrs.writable));
14073 m.insert("enumerable".into(), Value::Bool(attrs.enumerable));
14074 m.insert("configurable".into(), Value::Bool(attrs.configurable));
14075 h.new_object(m)
14076 }));
14077 }
14078 // Accessor descriptor?
14079 if let Some((get, set)) = with_host(|h| h.own_accessor(&obj, &key)) {
14080 return Ok(with_host(|h| {
14081 let a = h.prop_attrs(&obj, &key);
14082 let mut m: IndexMap<String, Value> = IndexMap::new();
14083 m.insert("get".into(), get.unwrap_or(Value::Undef));
14084 m.insert("set".into(), set.unwrap_or(Value::Undef));
14085 m.insert("enumerable".into(), Value::Bool(a.enumerable));
14086 m.insert("configurable".into(), Value::Bool(a.configurable));
14087 h.new_object(m)
14088 }));
14089 }
14090 let val = with_host(|h| match h.get(&obj) {
14091 // A Buffer's own properties are exactly its byte indices, read out of the
14092 // hidden `@@bytes` slot; `length`/`byteLength` are internal bookkeeping
14093 // that V8 keeps on the prototype, so they own no descriptor.
14094 Some(JsObj::Object(p))
14095 if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("Buffer") =>
14096 {
14097 match (
14098 p.get("@@bytes").and_then(|b| h.get(b)),
14099 key.parse::<usize>(),
14100 ) {
14101 (Some(JsObj::Array(items)), Ok(i)) => items.get(i).cloned(),
14102 _ => None,
14103 }
14104 }
14105 Some(JsObj::Object(p)) => p.get(&key).cloned(),
14106 // An array's index keys read the elements; `length` is the exotic own
14107 // property; anything else is an ordinary own key in the side table.
14108 Some(JsObj::Array(items)) => match key.parse::<usize>() {
14109 // An ELIDED index owns no property at all, so it has no descriptor.
14110 Ok(i) if h.is_hole(&obj, i) => None,
14111 Ok(i) => items.get(i).cloned(),
14112 Err(_) if key == "length" => Some(Value::Float(items.len() as f64)),
14113 Err(_) => h.fn_prop(&obj, &key),
14114 },
14115 // A function/class own prop lives in the fn-prop side table.
14116 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(&obj, &key),
14117 _ => None,
14118 });
14119 match val {
14120 Some(v) => Ok(with_host(|h| {
14121 let a = h.prop_attrs(&obj, &key);
14122 let mut m: IndexMap<String, Value> = IndexMap::new();
14123 m.insert("value".into(), v);
14124 m.insert("writable".into(), Value::Bool(a.writable));
14125 m.insert("enumerable".into(), Value::Bool(a.enumerable));
14126 m.insert("configurable".into(), Value::Bool(a.configurable));
14127 h.new_object(m)
14128 })),
14129 None => Ok(Value::Undef),
14130 }
14131}
14132
14133/// `Object.getOwnPropertyDescriptors(obj)` — the descriptor of every own string
14134/// key, keyed by name. `Object.create(proto, getOwnPropertyDescriptors(src))` is
14135/// the standard "clone with accessors intact" idiom, so this must agree
14136/// key-for-key with `getOwnPropertyNames`.
14137fn object_get_own_descriptors(args: Vec<Value>) -> Result<Value, String> {
14138 let obj = arg0(&args);
14139 let names = object_keys(vec![obj.clone()], 3)?;
14140 let keys: Vec<String> = with_host(|h| match h.get(&names) {
14141 Some(JsObj::Array(items)) => items.iter().map(|k| h.str_of(k)).collect(),
14142 _ => Vec::new(),
14143 });
14144 let mut out: IndexMap<String, Value> = IndexMap::new();
14145 for k in keys {
14146 let ks = with_host(|h| h.new_str(k.clone()));
14147 let d = object_get_own_descriptor(vec![obj.clone(), ks])?;
14148 if !matches!(d, Value::Undef) {
14149 out.insert(k, d);
14150 }
14151 }
14152 Ok(with_host(|h| h.new_object(out)))
14153}
14154
14155/// `key in obj` respecting the prototype chain. Reports a `Result` because a
14156/// Proxy's `has` trap is user code and may throw.
14157pub fn has_property(obj: &Value, key: &str) -> Result<bool, String> {
14158 if let Some(b) = crate::proxy::has(obj, key)? {
14159 return Ok(b);
14160 }
14161 Ok(has_property_ordinary(obj, key))
14162}
14163
14164/// `[[HasProperty]]` for every non-Proxy receiver.
14165fn has_property_ordinary(obj: &Value, key: &str) -> bool {
14166 // `key in globalThis`: membership matches what the READ answers, which for
14167 // the global object includes every lazily-bound builtin and every global a
14168 // script created. `'Math' in globalThis` and `'x' in globalThis` after
14169 // `x = 1` both answered FALSE while `globalThis.Math` and `globalThis.x`
14170 // read back fine.
14171 if with_host(|h| h.is_global_object(obj))
14172 && !CJS_WRAPPER_LOCALS.contains(&key)
14173 && global_object_binding(key).is_some()
14174 {
14175 return true;
14176 }
14177 // `key in <builtin namespace/prototype>`: membership matches what a property
14178 // read would yield. `String.prototype.indexOf` (and the rest of the builtin
14179 // prototype methods) resolve as callable thunks via `namespace_property`, so
14180 // `'indexOf' in String.prototype` must report true (get-intrinsic probes this
14181 // with the `in` operator before reading the intrinsic).
14182 if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(obj).cloned()) {
14183 return !matches!(namespace_property(&ns, key), Value::Undef);
14184 }
14185 // An integer index of a typed array / Buffer is an own property, and lives
14186 // in the hidden element array rather than the property map — the same
14187 // question `hasOwnProperty` answers, through the same helper. Only a hit
14188 // short-circuits: a non-index key like `'length'` must still fall through
14189 // to the ordinary chain lookup below.
14190 if crate::stdlib::typedarray::has_index(obj, key) == Some(true) {
14191 return true;
14192 }
14193 if with_host(|h| host::lookup_chain(h, obj, key)).is_some() {
14194 return true;
14195 }
14196 if with_host(|h| host::lookup_accessor(h, obj, key)).is_some() {
14197 return true;
14198 }
14199 // A member patched onto the receiver's intrinsic prototype. The READ
14200 // resolves it, so without this `Array.prototype.at = f` made `[].at` a
14201 // function while `'at' in []` stayed false.
14202 if !key.starts_with('#') && inherited_builtin_static(obj, key).is_some() {
14203 return true;
14204 }
14205 if with_host(|h| match h.get(obj) {
14206 Some(JsObj::Object(p)) => p.contains_key(key),
14207 Some(JsObj::Array(items)) => {
14208 key == "length"
14209 || key
14210 .parse::<usize>()
14211 .map(|i| i < items.len() && !h.is_hole(obj, i))
14212 .unwrap_or(false)
14213 // A non-index own property (`arr.foo`, `arr[sym]`) lives in the
14214 // side table, and `in` must see it.
14215 || h.fn_prop(obj, key).is_some()
14216 }
14217 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(obj, key).is_some(),
14218 // A RegExp's `lastIndex` is an OWN property in node. Here it lives in
14219 // the `RegExpObj` struct rather than a property map, so nothing above
14220 // can see it.
14221 Some(JsObj::RegExp(_)) => key == "lastIndex" || h.fn_prop(obj, key).is_some(),
14222 _ => false,
14223 }) {
14224 return true;
14225 }
14226 // An INHERITED builtin prototype method. These are not objects on the
14227 // prototype chain — they are synthesized by the read path from the
14228 // intrinsic table — so neither `lookup_chain` nor the property map above
14229 // can see them, and `'toString' in {}`, `'push' in []` and `'then' in
14230 // Promise.resolve()` all answered false. That last one is the standard
14231 // thenable test, so the `in` operator disagreed with what a read gives for
14232 // every builtin method of every builtin kind.
14233 inherited_builtin_method(obj, key)
14234}
14235
14236/// Whether a READ of `key` on `obj` would resolve to an inherited builtin
14237/// prototype method. Asked by `in` and `hasOwnProperty`'s negative case; it
14238/// performs no read, so a getter cannot fire.
14239/// A property a script MONKEY-PATCHED onto the intrinsic prototype `obj`
14240/// inherits from (`Array.prototype.at = impl`, `Object.prototype.foo = 1`), or
14241/// `None`.
14242///
14243/// The intrinsic prototypes are namespace handles rather than real objects on
14244/// the chain, so an assignment onto one lands in `builtin_statics` and no
14245/// ordinary chain walk can see it. This is the read side: the receiver's own
14246/// constructor's prototype first, then `Object.prototype`, mirroring
14247/// `inherited_method_owner`'s two-step.
14248pub(crate) fn inherited_builtin_static(obj: &Value, key: &str) -> Option<Value> {
14249 if with_host(|h| h.has_null_proto(obj)) {
14250 return None;
14251 }
14252 let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14253 Some(c) => Some(c),
14254 None if is_arguments(obj) => Some("Object"),
14255 None => with_host(|h| default_ctor_name(h, obj)),
14256 };
14257 // Only the side table is consulted, never the real prototype OBJECT's map:
14258 // `String.prototype` and friends are materialized with their intrinsic
14259 // members present, so reading their maps here would re-route every ordinary
14260 // `"a".toString()` through this path — which recursed until the stack blew.
14261 // `set_property` mirrors a write onto a real intrinsic prototype INTO this
14262 // table precisely so the read side can stay this narrow.
14263 let on = |c: &str| with_host(|h| h.builtin_static(&format!("{c}.prototype"), key));
14264 let found = ctor.and_then(on).or_else(|| on("Object"))?;
14265 // Restoring a saved intrinsic (`const orig = Array.prototype.join; …;
14266 // Array.prototype.join = orig`) stores the SYNTHESIZED thunk for this very
14267 // name back into the table. Dispatching to it would re-enter this lookup
14268 // and recurse until the stack blew, so a thunk that is already this key's
14269 // own intrinsic reports nothing and the ordinary builtin path answers.
14270 let self_thunk = with_host(
14271 |h| matches!(h.get(&found), Some(JsObj::Builtin(s)) if s.starts_with("@proto:") && s.ends_with(&format!(":{key}"))),
14272 );
14273 (!self_thunk).then_some(found)
14274}
14275
14276/// Whether `recv` carries `key` as an OWN property — the guard on
14277/// [`inherited_builtin_static`], since an own property shadows anything
14278/// patched onto a prototype.
14279fn has_own_for_shadow(recv: &Value, key: &str) -> bool {
14280 with_host(|h| {
14281 if h.fn_prop(recv, key).is_some() || h.own_accessor(recv, key).is_some() {
14282 return true;
14283 }
14284 match h.get(recv) {
14285 Some(JsObj::Object(p)) => p.contains_key(key),
14286 // An ELIDED index owns nothing — the whole point of a hole is that
14287 // the lookup continues up the chain — so it must not count as a
14288 // shadow here or an inherited value at that index stays invisible.
14289 Some(JsObj::Array(items)) => {
14290 key == "length" || {
14291 key.parse::<usize>()
14292 .is_ok_and(|i| i < items.len() && !h.is_hole(recv, i))
14293 }
14294 }
14295 _ => false,
14296 }
14297 })
14298}
14299
14300fn inherited_builtin_method(obj: &Value, key: &str) -> bool {
14301 if with_host(|h| h.has_null_proto(obj)) {
14302 return false;
14303 }
14304 if let Some(tag) = crate::stdlib::native_tag(obj) {
14305 if crate::stdlib::instance_has_method(&tag, key) {
14306 return true;
14307 }
14308 }
14309 inherited_method_owner(obj, key).is_some()
14310}
14311
14312/// Whether `recv`'s intrinsic prototype is still on its chain — that is,
14313/// whether `Array.prototype`'s methods are still reachable from an array.
14314///
14315/// A builtin's methods are synthesized from the receiver's KIND rather than
14316/// found on a chain, so replacing the prototype could not take them away:
14317/// `Object.setPrototypeOf(a, {})` left `a.join` a function where node reports
14318/// `undefined`, and `Object.setPrototypeOf(a, null)` did too. The exotic
14319/// storage is unaffected either way — `Array.isArray`, `a.length` and `a[0]`
14320/// all still answer, as they do in node.
14321///
14322/// The overwhelmingly common case is the DEFAULT link, which is recorded as no
14323/// link at all, so this answers true after one map probe and allocates nothing.
14324pub(crate) fn own_intrinsic_reachable_pub(recv: &Value) -> bool {
14325 own_intrinsic_reachable(recv)
14326}
14327
14328fn own_intrinsic_reachable(recv: &Value) -> bool {
14329 // A BOXED primitive needs no special case here: its methods resolve through
14330 // `inherited_method_owner`, which applies the wrapper rule itself.
14331 with_host(|h| default_ctor_name(h, recv)).map_or(true, |c| intrinsic_reachable(recv, c))
14332}
14333
14334/// Whether the intrinsic prototype for `ctor` is still on `recv`'s chain.
14335fn intrinsic_reachable(recv: &Value, ctor: &str) -> bool {
14336 let own = Some(ctor);
14337 let mut cur = recv.clone();
14338 for _ in 0..100 {
14339 let explicit = with_host(|h| h.proto_of(&cur));
14340 let Some(p) = explicit else {
14341 // No explicit link: the implicit prototype is this object's own
14342 // kind's, which is what `recv` is asking about only while `cur` is
14343 // still `recv` itself.
14344 if with_host(|h| h.has_null_proto(&cur)) {
14345 return false;
14346 }
14347 let implicit = with_host(|h| default_ctor_name(h, &cur));
14348 // Every implicit prototype chain ends at `Object.prototype`, so a
14349 // question about `Object` is answered yes by any of them.
14350 return implicit == own || ctor == "Object";
14351 };
14352 if with_host(|h| h.is_null(&p)) {
14353 return false;
14354 }
14355 let hit = with_host(|h| {
14356 own.is_some_and(|c| {
14357 matches!(h.get(&p), Some(JsObj::Builtin(ns)) if *ns == format!("{c}.prototype"))
14358 || h.intrinsic_proto_ctor(&p) == Some(c)
14359 || (c == "Object" && h.object_proto() == p)
14360 })
14361 });
14362 if hit {
14363 return true;
14364 }
14365 // A CLASS prototype object is not linked to the builtin its class
14366 // extends — the `extends` relationship is recorded on the class value —
14367 // so the walk has to cross over there or `class D extends Array {}` ends
14368 // it, and every inherited method of every subclass instance vanishes.
14369 if let Some(builtin) = with_host(|h| {
14370 h.class_owning_proto(&p)
14371 .and_then(|c| h.class_builtin_ancestor(&c))
14372 .map(|b| h.callable_name(&b))
14373 }) {
14374 if own == Some(builtin.as_str()) || ctor == "Object" {
14375 return true;
14376 }
14377 }
14378 cur = p;
14379 }
14380 false
14381}
14382
14383/// The intrinsic prototypes actually ON `recv`'s explicit chain, nearest first
14384/// — the complement of [`intrinsic_reachable`], which asks about one known
14385/// constructor.
14386///
14387/// `Object.create(Array.prototype)` is an ordinary object whose chain reaches
14388/// `Array.prototype`, and node resolves the whole of `Array.prototype` through
14389/// it: `o.push(1)` works, because those methods are generic over their receiver
14390/// (which is also why `Array.prototype.push.call({length: 0}, 1)` already
14391/// worked here). Deciding the owner from the receiver's KIND alone made every
14392/// one of them `undefined` — the same "methods come from the kind, not the
14393/// chain" mistake as the detachment case, in the opposite direction.
14394pub(crate) fn chain_intrinsic_ctors_pub(recv: &Value) -> Vec<&'static str> {
14395 chain_intrinsic_ctors(recv)
14396}
14397
14398fn chain_intrinsic_ctors(recv: &Value) -> Vec<&'static str> {
14399 with_host(|h| chain_intrinsic_ctors_h(h, recv))
14400}
14401
14402/// [`chain_intrinsic_ctors`] against an already-held host borrow, for the
14403/// callers that are inside one — `can_write_prop` takes `&JsHost`, so going
14404/// back through `with_host` there aborts the process on a double borrow.
14405pub(crate) fn chain_intrinsic_ctors_h(h: &host::JsHost, recv: &Value) -> Vec<&'static str> {
14406 let mut out: Vec<&'static str> = Vec::new();
14407 let mut cur = recv.clone();
14408 for _ in 0..100 {
14409 let Some(p) = h.proto_of(&cur) else {
14410 break;
14411 };
14412 if h.is_null(&p) {
14413 break;
14414 }
14415 let name = match h.get(&p) {
14416 Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
14417 _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
14418 };
14419 if let Some(n) = name {
14420 if let Some(c) = crate::arity::PROTO_MEMBERS
14421 .iter()
14422 .map(|(k, _)| *k)
14423 .find(|k| *k == n)
14424 {
14425 if !out.contains(&c) {
14426 out.push(c);
14427 }
14428 }
14429 }
14430 cur = p;
14431 }
14432 out
14433}
14434
14435/// The constructor whose prototype defines `key` for `obj` — its own if that
14436/// prototype has it, otherwise `Object` — or `None` when neither does.
14437///
14438/// Used both by `in` and by the READ, so the two cannot disagree about which
14439/// prototype a name comes from. `new Map().toString` is `Map.prototype`'s and
14440/// `new Map().hasOwnProperty` is `Object.prototype`'s.
14441pub(crate) fn inherited_method_owner_pub(obj: &Value, key: &str) -> Option<&'static str> {
14442 inherited_method_owner(obj, key)
14443}
14444
14445fn inherited_method_owner(obj: &Value, key: &str) -> Option<&'static str> {
14446 if with_host(|h| h.has_null_proto(obj)) {
14447 return None;
14448 }
14449 // The generated prototype-member table, which unlike the arity table knows
14450 // about the ACCESSORS — `size` on a Map, `source` on a RegExp, `description`
14451 // on a Symbol are members but not functions — and about `constructor`.
14452 // A BOXED primitive reports its wrapper's constructor, not `Object` —
14453 // `'description' in Object(Symbol())` is true. The box is an ordinary
14454 // object carrying the primitive in a slot, so the ctor comes from what it
14455 // holds rather than from the box itself.
14456 let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14457 Some(c) => Some(c),
14458 // An `arguments` object is ARRAY-BACKED here so that indices, `length`,
14459 // spread and `for-of` work, but node's is an exotic that inherits from
14460 // `Object.prototype` — `typeof arguments.map` is `undefined`. Reporting
14461 // its backing kind would hand it the whole `Array.prototype`.
14462 None if is_arguments(obj) => Some("Object"),
14463 None => with_host(|h| default_ctor_name(h, obj)),
14464 };
14465 let on_proto = |c: &str| {
14466 crate::arity::PROTO_MEMBERS
14467 .binary_search_by(|(k, _)| (*k).cmp(c))
14468 .ok()
14469 .is_some_and(|i| {
14470 crate::arity::PROTO_MEMBERS[i]
14471 .1
14472 .iter()
14473 .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
14474 })
14475 };
14476 // `PROTO_MEMBERS` is generated from the prototypes' STRING keys, so a
14477 // well-known symbol member is absent from it. For an object whose CHAIN
14478 // reaches an intrinsic prototype the intrinsic table has to be consulted as
14479 // well, or `[...Object.create(Array.prototype)]` finds no `Symbol.iterator`
14480 // at all. It is deliberately NOT consulted for the receiver's own kind:
14481 // there a thunk would be minted for every `@@` member the table names,
14482 // including ones whose dispatch has no implementation for that receiver,
14483 // and `[...buffer]` then failed with `@@iterator is not a function`.
14484 //
14485 // It is narrowed further to an ORDINARY object: a natively-tagged receiver
14486 // (a typed array, a Buffer) is linked to a real intrinsic prototype too,
14487 // and minting a thunk there produced `@@iterator is not a function` for
14488 // `[...new Uint8Array(ab)]` — those kinds reach their iterator by their own
14489 // fast path, which the table entry would shadow.
14490 let plain = with_host(|h| h.kind_of(obj)) == Some(ObjKind::Object)
14491 && crate::stdlib::native_tag(obj).is_none();
14492 let on_proto_or_symbol =
14493 |c: &str| on_proto(c) || (plain && builtin_meta(&format!("@proto:{c}:{key}")).is_some());
14494 // Each candidate is only an answer while ITS prototype is still on the
14495 // receiver's chain. The two are asked separately: replacing an array's
14496 // prototype with a plain object takes `Array.prototype`'s methods away and
14497 // leaves `Object.prototype`'s, since the replacement inherits from it.
14498 if let Some(c) = ctor.filter(|c| on_proto(c) && intrinsic_reachable(obj, c)) {
14499 return Some(c);
14500 }
14501 // An intrinsic prototype the receiver's chain passes THROUGH, which its own
14502 // kind does not account for.
14503 if let Some(c) = chain_intrinsic_ctors(obj)
14504 .into_iter()
14505 .find(|c| on_proto_or_symbol(c))
14506 {
14507 return Some(c);
14508 }
14509 // Everything else inherits `Object.prototype`'s.
14510 if on_proto("Object") && intrinsic_reachable(obj, "Object") {
14511 return Some("Object");
14512 }
14513 None
14514}
14515
14516/// `structuredClone` — a deep copy of plain data (objects/arrays/primitives).
14517/// `structuredClone` — the HTML structured-clone algorithm's shape: a deep copy
14518/// that preserves the *reference graph*. Two properties pointing at the same
14519/// object clone to two properties pointing at the same clone, and a cycle clones
14520/// to a cycle instead of recursing forever. `seen` maps each source heap index
14521/// to its clone, which is what buys both.
14522/// The rendering node puts in a `DataCloneError` for a value the structured
14523/// clone algorithm refuses, or `None` when the value IS cloneable.
14524///
14525/// Refusing at all is the point: these used to be copied through by reference,
14526/// so `structuredClone({f: () => 1})` handed back an object sharing the
14527/// original's function and `structuredClone(new WeakMap())` returned the very
14528/// same WeakMap. Node throws on every one of them.
14529fn clone_refusal(v: &Value) -> Option<String> {
14530 let kind = with_host(|h| h.kind_of(v))?;
14531 let render = |ctor: &str| Some(format!("#<{ctor}>"));
14532 match kind {
14533 // A function renders as its SOURCE TEXT here. This frontend does not
14534 // retain function source (`FuncDef` holds a compiled chunk), so the
14535 // message says `function f() { [code] }` where node quotes the original
14536 // — the error, its name and its code are right, the text is not.
14537 ObjKind::Func | ObjKind::Class | ObjKind::BoundFunc | ObjKind::BoundMethod => {
14538 Some(with_host(|h| h.str_of(v)))
14539 }
14540 ObjKind::Builtin if with_host(|h| host::is_callable(h, v)) => {
14541 Some(with_host(|h| h.str_of(v)))
14542 }
14543 ObjKind::Symbol => Some(with_host(|h| h.str_of(v))),
14544 ObjKind::Promise => render("Promise"),
14545 ObjKind::Generator => Some("[object Generator]".to_string()),
14546 // A proxy is refused by its TARGET's shape: a callable one renders like
14547 // the function it wraps, everything else as a plain object.
14548 ObjKind::Proxy => Some(if with_host(|h| host::is_callable(h, v)) {
14549 with_host(|h| h.str_of(v))
14550 } else {
14551 "#<Object>".to_string()
14552 }),
14553 ObjKind::Map if with_host(|h| matches!(h.get(v), Some(JsObj::Map { weak: true, .. }))) => {
14554 render("WeakMap")
14555 }
14556 ObjKind::Set if with_host(|h| matches!(h.get(v), Some(JsObj::Set { weak: true, .. }))) => {
14557 render("WeakSet")
14558 }
14559 _ => match crate::stdlib::native_tag(v).as_deref() {
14560 Some(t @ ("WeakRef" | "FinalizationRegistry")) => render(t),
14561 _ => None,
14562 },
14563 }
14564}
14565
14566/// `structuredClone(value[, { transfer }])`.
14567///
14568/// Everything in `transfer` must be an `ArrayBuffer`, and each one is DETACHED
14569/// after the clone — its bytes belong to the copy. The option used to be
14570/// ignored entirely, so the source buffer stayed usable where node leaves it
14571/// with zero length.
14572fn structured_clone(args: Vec<Value>) -> Result<Value, String> {
14573 let list: Vec<Value> = match args.get(1).filter(|v| !matches!(v, Value::Undef)) {
14574 Some(opts) => {
14575 let t = get_property(opts, "transfer")?;
14576 if matches!(t, Value::Undef) {
14577 Vec::new()
14578 } else {
14579 host::iter_all(&t)?
14580 }
14581 }
14582 None => Vec::new(),
14583 };
14584 for item in &list {
14585 if crate::stdlib::native_tag(item).as_deref() != Some("ArrayBuffer") {
14586 return Err(host::dom_error(
14587 "DataCloneError",
14588 "Found invalid value in transferList.",
14589 ));
14590 }
14591 }
14592 let out = deep_clone(&arg0(&args))?;
14593 for item in &list {
14594 crate::stdlib::typedarray::detach_buffer(item);
14595 }
14596 Ok(out)
14597}
14598
14599pub(crate) fn deep_clone(v: &Value) -> Result<Value, String> {
14600 deep_clone_seen(v, &mut std::collections::HashMap::new())
14601}
14602
14603fn deep_clone_seen(
14604 v: &Value,
14605 seen: &mut std::collections::HashMap<u32, Value>,
14606) -> Result<Value, String> {
14607 let idx = match v {
14608 Value::Obj(i) => *i,
14609 _ => return Ok(v.clone()),
14610 };
14611 if let Some(done) = seen.get(&idx) {
14612 return Ok(done.clone());
14613 }
14614 if crate::stdlib::typedarray::is_detached(v) {
14615 return Err(host::dom_error(
14616 "DataCloneError",
14617 "An ArrayBuffer is detached and could not be cloned.",
14618 ));
14619 }
14620 if let Some(render) = clone_refusal(v) {
14621 return Err(host::dom_error(
14622 "DataCloneError",
14623 &format!("{render} could not be cloned."),
14624 ));
14625 }
14626 // A REGEXP is cloned, not shared: it carries a mutable `lastIndex`, so
14627 // handing back the same object let a write through the clone move the
14628 // original's match cursor.
14629 if let Some((src, flags)) = with_host(|h| match h.get(v) {
14630 Some(JsObj::RegExp(r)) => Some((r.source.clone(), r.flags.clone())),
14631 _ => None,
14632 }) {
14633 let args = with_host(|h| vec![h.new_str(src), h.new_str(flags)]);
14634 let out = regexp_ctor(&args)?;
14635 seen.insert(idx, out.clone());
14636 return Ok(out);
14637 }
14638 Ok(match with_host(|h| h.get(v).cloned()) {
14639 Some(JsObj::Array(items)) => {
14640 // Register the (empty) clone BEFORE recursing so a self-reference
14641 // resolves to it.
14642 let out = with_host(|h| h.new_array(Vec::new()));
14643 seen.insert(idx, out.clone());
14644 let mut cloned: Vec<Value> = Vec::with_capacity(items.len());
14645 for x in &items {
14646 cloned.push(deep_clone_seen(x, seen)?);
14647 }
14648 with_host(|h| {
14649 if let Some(JsObj::Array(a)) = h.get_mut(&out) {
14650 *a = cloned;
14651 }
14652 // A sparse source clones to an equally sparse array: the clone
14653 // walks own properties, so a hole is nothing to copy.
14654 h.copy_holes(v, &out, Some);
14655 });
14656 out
14657 }
14658 Some(JsObj::Object(_)) => {
14659 let out = with_host(|h| h.new_object(IndexMap::new()));
14660 seen.insert(idx, out.clone());
14661 // Own ENUMERABLE string keys, read THROUGH any accessor: the clone
14662 // walked the property map, where an accessor stores nothing, so
14663 // `structuredClone({get p(){return 1}})` silently lost `p`. A symbol
14664 // key and a non-enumerable one are dropped, as node drops them.
14665 let is_error = with_host(|h| h.error_to_string(v)).is_some();
14666 let proto = clone_proto(v);
14667 let keeps_proto = !matches!(proto, CloneProto::Plain);
14668 // An ERROR clones its name, message and stack and NOTHING else —
14669 // node drops any other own property, even an enumerable one.
14670 let keys: Vec<String> = if is_error {
14671 // An ERROR clones its name, message and stack and NOTHING else —
14672 // node drops any other own property, even an enumerable one.
14673 ["name", "message", "stack"]
14674 .iter()
14675 .filter(|k| has_property(v, k).unwrap_or(false))
14676 .map(|k| (*k).to_string())
14677 .collect()
14678 } else if keeps_proto {
14679 // A preserved exotic keeps EVERY own property, including the
14680 // non-enumerable ones and the internal slots — a Date's time
14681 // value, an ArrayBuffer's `byteLength` and byte store, a typed
14682 // array's view. The enumerable-only walk dropped all of those,
14683 // so a cloned Date read `Invalid Date` and a cloned
14684 // ArrayBuffer had no `byteLength`.
14685 with_host(|h| match h.get(v) {
14686 Some(JsObj::Object(p)) => p.keys().cloned().collect(),
14687 _ => Vec::new(),
14688 })
14689 } else {
14690 with_host(|h| h.own_enum_key_names(v))
14691 };
14692 let mut cloned: IndexMap<String, Value> = IndexMap::new();
14693 for k in keys {
14694 // An internal slot is read straight out of the map: it is not a
14695 // property, so a `[[Get]]` would not find it.
14696 let val = if k.starts_with("@@") {
14697 match with_host(|h| match h.get(v) {
14698 Some(JsObj::Object(p)) => p.get(&k).cloned(),
14699 _ => None,
14700 }) {
14701 Some(val) => val,
14702 None => continue,
14703 }
14704 } else {
14705 get_property(v, &k)?
14706 };
14707 cloned.insert(k, deep_clone_seen(&val, seen)?);
14708 }
14709 // The prototype survives only for the exotics the algorithm knows —
14710 // a Date, an Error, a typed array, a boxed primitive. A USER class
14711 // instance becomes a plain object, which is what node produces;
14712 // keeping every prototype made `structuredClone(new K())
14713 // instanceof K` true.
14714 with_host(|h| {
14715 if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14716 *p = cloned;
14717 }
14718 match &proto {
14719 CloneProto::Same => {
14720 if let Some(p) = h.proto_of(v) {
14721 h.set_proto(&out, p);
14722 }
14723 }
14724 CloneProto::Ctor(c) => {
14725 h.ensure_error_protos();
14726 let p = h.error_proto(c).or_else(|| h.ensure_ctor_proto(c));
14727 if let Some(p) = p {
14728 h.set_proto(&out, p);
14729 }
14730 // A Buffer clones to a plain `Uint8Array`, so the native
14731 // tag has to change with the prototype — left alone,
14732 // `Buffer.isBuffer` still answered true for the clone.
14733 // A Buffer clones to a plain `Uint8Array`, so the native
14734 // tag has to change with the prototype — left alone,
14735 // `Buffer.isBuffer` answered true for the clone and the
14736 // brand stayed `[object Object]`. A typed array is
14737 // tagged `TypedArray` and names its element type in
14738 // `@@kind`; `@@native = "Uint8Array"` matches no arm.
14739 if c == "Uint8Array" {
14740 let tag = h.new_str("TypedArray");
14741 let kind = h.new_str("Uint8Array");
14742 if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14743 p.insert("@@native".into(), tag);
14744 p.insert("@@kind".into(), kind);
14745 }
14746 }
14747 }
14748 CloneProto::Plain => {}
14749 }
14750 h.copy_prop_attrs(v, &out);
14751 });
14752 out
14753 }
14754 // Map/Set are structured types: clone the entries, keep the kind.
14755 Some(JsObj::Map { entries, weak }) => {
14756 let out = with_host(|h| {
14757 h.alloc(JsObj::Map {
14758 entries: IndexMap::new(),
14759 weak,
14760 })
14761 });
14762 seen.insert(idx, out.clone());
14763 let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
14764 for (k, val) in pairs {
14765 let ck = deep_clone_seen(&k, seen)?;
14766 let cv = deep_clone_seen(&val, seen)?;
14767 let _ = map_method(&out, "set", vec![ck, cv]);
14768 }
14769 out
14770 }
14771 Some(JsObj::Set { entries, weak }) => {
14772 let out = with_host(|h| {
14773 h.alloc(JsObj::Set {
14774 entries: IndexMap::new(),
14775 weak,
14776 })
14777 });
14778 seen.insert(idx, out.clone());
14779 let vals: Vec<Value> = entries.values().cloned().collect();
14780 for x in vals {
14781 let cx = deep_clone_seen(&x, seen)?;
14782 let _ = set_method(&out, "add", vec![cx]);
14783 }
14784 out
14785 }
14786 // A string, a BigInt and a boxed primitive are immutable enough to
14787 // share; anything left is a value type.
14788 _ => v.clone(),
14789 })
14790}
14791
14792/// Whether a cloned object keeps the source's prototype.
14793///
14794/// The structured clone algorithm reproduces the exotics it knows and turns
14795/// everything else into a plain object — so a `Date` clones to a `Date` and a
14796/// user class instance clones to an `Object`.
14797fn clone_proto(v: &Value) -> CloneProto {
14798 // An ERROR clones to the BUILT-IN class its `name` selects, so a subclass
14799 // flattens: `structuredClone(new (class E extends Error{})('m'))` reports
14800 // `Error`, not `E`.
14801 if with_host(|h| h.error_to_string(v)).is_some() {
14802 let name = get_property(v, "name")
14803 .map(|n| with_host(|h| h.str_of(&n)))
14804 .unwrap_or_else(|_| "Error".into());
14805 let class = if host::ERROR_NAMES.contains(&name.as_str()) {
14806 name
14807 } else {
14808 "Error".to_string()
14809 };
14810 return CloneProto::Ctor(class);
14811 }
14812 match crate::stdlib::native_tag(v).as_deref() {
14813 // A Buffer is not reproduced as a Buffer: node hands back a plain
14814 // `Uint8Array` over the same bytes.
14815 Some("Buffer") => CloneProto::Ctor("Uint8Array".into()),
14816 Some(_) => CloneProto::Same,
14817 // A boxed primitive keeps its wrapper; anything else — a user class
14818 // instance included — becomes a plain object.
14819 None if wrapped_primitive(v).is_some() => CloneProto::Same,
14820 None => CloneProto::Plain,
14821 }
14822}
14823
14824/// Which prototype a clone gets: the source's, a named builtin's, or none.
14825enum CloneProto {
14826 Same,
14827 Ctor(String),
14828 Plain,
14829}
14830
14831// ══ Promises, timers, microtasks (event-loop-driven) ═════════════════════════
14832
14833/// A short `Name: message` string for an error value (used when an await
14834/// rejection unwinds as a thrown error).
14835pub fn error_string(h: &host::JsHost, v: &Value) -> String {
14836 if let Some(JsObj::Object(props)) = h.get(v) {
14837 let name = props
14838 .get("name")
14839 .map(|x| h.str_of(x))
14840 .or_else(|| host::lookup_chain(h, v, "name").map(|x| h.str_of(&x)))
14841 .unwrap_or_else(|| "Error".into());
14842 if let Some(m) = props.get("message") {
14843 return format!("{name}: {}", h.str_of(m));
14844 }
14845 return name;
14846 }
14847 h.str_of(v)
14848}
14849
14850/// 27.2.5.3 `thenFinally`/`catchFinally`: `PromiseResolve(result).then(() =>
14851/// value)`, or `() => { throw reason }` on the reject path.
14852///
14853/// Returning the carried value directly — what this used to do — skipped both
14854/// halves. A promise returned by the callback was never awaited, so the
14855/// ordinary async-cleanup shape
14856///
14857/// ```text
14858/// work().finally(() => closeConnection()).then(next)
14859/// ```
14860///
14861/// ran `next` before the connection had closed. And the chain settled three
14862/// microtask ticks early, which is observable in ordering against any other
14863/// chain, not just against a timer.
14864///
14865/// A rejection from the callback's own promise wins over the carried value, so
14866/// no reject handler is attached here: it propagates on its own.
14867fn finally_chain(result: Value, carried: Value, rethrow: bool) -> Value {
14868 // PromiseResolve (27.2.4.7) returns an argument that is already a promise
14869 // UNCHANGED. Wrapping it anyway costs the extra tick that resolving with a
14870 // thenable takes to adopt it, which showed up as a callback returning a
14871 // rejected promise settling one tick late against every other chain.
14872 let p = match with_host(|h| h.promise_id(&result)) {
14873 Some(_) => result,
14874 None => {
14875 let fresh = with_host(|h| h.new_promise());
14876 if let Some(pid) = with_host(|h| h.promise_id(&fresh)) {
14877 host::resolve_promise_val(pid, result);
14878 }
14879 fresh
14880 }
14881 };
14882 let cell = with_host(|h| h.new_array(vec![carried]));
14883 let idx = match cell {
14884 Value::Obj(i) => i,
14885 _ => 0,
14886 };
14887 let tag = if rethrow { "finrethrow" } else { "finret" };
14888 let thunk = make_builtin(format!("@@{tag}:{idx}"));
14889 host::promise_then(&p, thunk, Value::Undef)
14890}
14891
14892fn make_builtin(name: String) -> Value {
14893 with_host(|h| h.alloc(JsObj::Builtin(name)))
14894}
14895
14896/// `[[GetPrototypeOf]]` (10.1.1) — the answer `Object.getPrototypeOf`,
14897/// `Reflect.getPrototypeOf` and a `__proto__` READ all have to agree on.
14898///
14899/// `__proto__` used to answer from `JsHost::proto_of` alone, which records only
14900/// an EXPLICIT link, so an object on the default prototype reported `null`:
14901/// `({}).__proto__ === Object.prototype` was false while
14902/// `Object.getPrototypeOf({}) === Object.prototype` was true. One function, so
14903/// the three cannot drift apart again.
14904pub fn prototype_of(v: &Value) -> Value {
14905 // Constructor-side inheritance: `Buffer extends Uint8Array`, so
14906 // `Object.getPrototypeOf(Buffer)` is the `Uint8Array` constructor itself,
14907 // not `Function.prototype`. This is the class-side half of the subclass
14908 // link — the instance-side half is `Buffer.prototype`'s `[[Prototype]]`.
14909 if matches!(with_host(|h| h.get(v).cloned()), Some(JsObj::Builtin(ref n)) if n == "Buffer") {
14910 return with_host(|h| h.alloc(JsObj::Builtin("Uint8Array".into())));
14911 }
14912 // Constructor-side inheritance for a `class B extends A` (ClassDefinition
14913 // 15.7.14 step 6.d: the constructor's `[[Prototype]]` is the parent
14914 // CONSTRUCTOR, not `Function.prototype`). Statics already resolved through
14915 // `ClassVal.parent`, but the link itself was invisible, so
14916 // `Object.getPrototypeOf(B) === A` read false and any library walking the
14917 // constructor chain — rather than calling a static — saw a base class.
14918 // A base class keeps the default answer below (`Function.prototype`).
14919 if let Some(JsObj::Class(c)) = with_host(|h| h.get(v).cloned()) {
14920 if let Some(parent) = c.parent {
14921 return parent;
14922 }
14923 }
14924 // `Object.create(null)` and friends really do have a null prototype.
14925 if with_host(|h| h.has_null_proto(v)) {
14926 return with_host(|h| h.null());
14927 }
14928 // `Object.prototype` is the CHAIN ROOT, so its own prototype is `null`. It
14929 // reported itself, because the fallback below answers by constructor name
14930 // and a plain object's is `Object` — an infinite chain to anything walking
14931 // it.
14932 if with_host(|h| h.strict_eq(v, &h.object_proto())) {
14933 return with_host(|h| h.null());
14934 }
14935 // Every OTHER builtin prototype namespace (`Array.prototype`,
14936 // `Function.prototype`, …) inherits from `Object.prototype`; the fallback
14937 // would send it back to a namespace handle for its own constructor.
14938 if matches!(
14939 with_host(|h| h.get(v).cloned()),
14940 Some(JsObj::Builtin(ref n)) if n.ends_with(".prototype")
14941 ) {
14942 return with_host(|h| h.object_proto());
14943 }
14944 if let Some(p) = with_host(|h| h.proto_of(v)) {
14945 return p;
14946 }
14947 // A builtin exotic with no explicit `[[Prototype]]` link reports its
14948 // constructor's prototype namespace (`Object.getPrototypeOf([]) ===
14949 // Array.prototype`), which `strict_eq` compares by name. A plain object
14950 // reports the one real `Object.prototype` object.
14951 with_host(|h| {
14952 h.ensure_native_protos();
14953 match default_ctor_name(h, v) {
14954 Some("Object") => h.object_proto(),
14955 // `String`/`Number`/`Boolean` own REAL prototype objects, so a
14956 // primitive must report that object and not a fresh namespace
14957 // thunk — otherwise `Object.getPrototypeOf(1) === Number.prototype`
14958 // compares a thunk against the real object and reads false.
14959 Some(c) => h
14960 .native_proto(c)
14961 .unwrap_or_else(|| h.alloc(JsObj::Builtin(format!("{c}.prototype")))),
14962 None => h.null(),
14963 }
14964 })
14965}
14966
14967/// `new Promise((resolve, reject) => …)` — run the executor synchronously with
14968/// internal resolve/reject functions.
14969/// A fresh promise built through the SPECIES constructor, when a `Promise`
14970/// static was reached through a subclass.
14971///
14972/// `class P extends Promise {}` makes `P.resolve(1)` a `P`, because every
14973/// combinator builds its result with `this` (27.2.4.x). They all allocated a
14974/// plain promise, so nothing a subclass produced was an instance of it. The
14975/// executor is a no-op: the result is settled through its promise id, which is
14976/// what the ordinary path does too.
14977fn promise_species_create() -> Result<Option<Value>, String> {
14978 let Some(ctor) = host::current_static_this() else {
14979 return Ok(None);
14980 };
14981 if !matches!(
14982 with_host(|h| h.kind_of(&ctor)),
14983 Some(ObjKind::Class) | Some(ObjKind::Func)
14984 ) {
14985 return Ok(None);
14986 }
14987 let species = match get_property(&ctor, "@@species") {
14988 Ok(Value::Undef) => ctor,
14989 Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
14990 Ok(s) => s,
14991 Err(_) => ctor,
14992 };
14993 if !matches!(
14994 with_host(|h| h.kind_of(&species)),
14995 Some(ObjKind::Class) | Some(ObjKind::Func)
14996 ) {
14997 return Ok(None);
14998 }
14999 let noop = make_builtin("@@pnoop".to_string());
15000 let p = host::construct(&species, vec![noop])?;
15001 // Only usable if the subclass really produced a promise; a constructor that
15002 // returned something else has no id to settle.
15003 Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15004}
15005
15006/// The species constructor of a promise RECEIVER — what `then`/`catch`/`finally`
15007/// build their result with (`SpeciesConstructor(p, %Promise%)`, 27.2.5.4 step 3).
15008///
15009/// Distinct from `promise_species_create`, which answers for a STATIC reached
15010/// through a subclass. Here the subclass comes from the receiver itself, so
15011/// `P.resolve(1).then(f)` is also a `P`.
15012pub fn promise_species_from(recv: &Value) -> Result<Option<Value>, String> {
15013 // A chain lookup: a Promise receiver resolves through the stdlib funnel,
15014 // which has no `constructor` entry of its own.
15015 let ctor = with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef);
15016 if !matches!(
15017 with_host(|h| h.kind_of(&ctor)),
15018 Some(ObjKind::Class) | Some(ObjKind::Func)
15019 ) {
15020 return Ok(None);
15021 }
15022 let species = match get_property(&ctor, "@@species") {
15023 Ok(Value::Undef) => ctor,
15024 Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15025 Ok(s) => s,
15026 Err(_) => ctor,
15027 };
15028 if !matches!(
15029 with_host(|h| h.kind_of(&species)),
15030 Some(ObjKind::Class) | Some(ObjKind::Func)
15031 ) {
15032 return Ok(None);
15033 }
15034 let noop = make_builtin("@@pnoop".to_string());
15035 let p = host::construct(&species, vec![noop])?;
15036 Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15037}
15038
15039fn new_promise(executor: Value) -> Result<Value, String> {
15040 let p = with_host(|h| h.new_promise());
15041 let id = with_host(|h| h.promise_id(&p).unwrap());
15042 let res = make_builtin(format!("@@presolve:{id}"));
15043 let rej = make_builtin(format!("@@preject:{id}"));
15044 if let Err(e) = host::invoke(&executor, vec![res, rej], None) {
15045 // A throw in the executor rejects the promise.
15046 let ev = host::take_exc_or_error(&e);
15047 host::reject_promise_val(id, ev);
15048 }
15049 Ok(p)
15050}
15051
15052/// `Promise.resolve(v)` for stdlib callers that need to hand back an
15053/// already-settled promise.
15054pub fn promise_resolve_pub(v: Value) -> Result<Value, String> {
15055 promise_resolve(v)
15056}
15057
15058fn promise_resolve(v: Value) -> Result<Value, String> {
15059 if let Some(p) = promise_species_create()? {
15060 let id = with_host(|h| h.promise_id(&p).unwrap());
15061 host::resolve_promise_val(id, v);
15062 return Ok(p);
15063 }
15064 Ok(host::promise_of(&v))
15065}
15066fn promise_reject(v: Value) -> Result<Value, String> {
15067 let p = match promise_species_create()? {
15068 Some(p) => p,
15069 None => with_host(|h| h.new_promise()),
15070 };
15071 let id = with_host(|h| h.promise_id(&p).unwrap());
15072 host::reject_promise_val(id, v);
15073 Ok(p)
15074}
15075
15076/// `Promise.withResolvers()` — a fresh pending promise paired with its own
15077/// resolve/reject continuations (the same `@@presolve`/`@@preject` thunks the
15078/// executor receives), returned as a plain `{ promise, resolve, reject }` object.
15079/// A fresh pending promise paired with the thunk that resolves it, for stdlib
15080/// callers that hand the resolver to an event listener.
15081pub fn pending_promise_with_resolver() -> (Value, Value) {
15082 let p = with_host(|h| h.new_promise());
15083 let id = with_host(|h| h.promise_id(&p).unwrap());
15084 let resolve = make_builtin(format!("@@presolve:{id}"));
15085 (p, resolve)
15086}
15087
15088/// `RegExp.escape(s)` (22.2.4.2) — a string that matches `s` literally.
15089///
15090/// The rule is not "backslash the syntax characters": it also escapes a LEADING
15091/// ASCII alphanumeric, so the result can be concatenated after a `\` or a `{`
15092/// without the two running together, and it escapes the punctuation that is
15093/// meaningful inside a character class or a group name.
15094fn regexp_escape(args: Vec<Value>) -> Result<Value, String> {
15095 let v = arg0(&args);
15096 if !matches!(v, Value::Str(_)) && !with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_)))) {
15097 return Err(host::type_error("input argument must be a string"));
15098 }
15099 let s = with_host(|h| h.str_of(&v));
15100 // Punctuation that is escaped by CODE POINT rather than with a backslash.
15101 // Measured against node over the whole ASCII range, not taken from a list:
15102 // `-` and `=` are here, `$` and `*` are syntax characters and are not.
15103 const OTHER_PUNCTUATORS: &str = " !\"#%&',-:;<=>@`~";
15104 const SYNTAX: &str = "^$\\.*+?()[]{}|/";
15105 let mut out = String::with_capacity(s.len());
15106 for (i, c) in s.chars().enumerate() {
15107 // A leading ASCII alphanumeric, and only a leading one.
15108 if i == 0 && c.is_ascii_alphanumeric() {
15109 out.push_str(&format!("\\x{:02x}", c as u32));
15110 continue;
15111 }
15112 if SYNTAX.contains(c) {
15113 out.push('\\');
15114 out.push(c);
15115 continue;
15116 }
15117 match c {
15118 '\t' => out.push_str("\\t"),
15119 '\n' => out.push_str("\\n"),
15120 '\u{b}' => out.push_str("\\v"),
15121 '\u{c}' => out.push_str("\\f"),
15122 '\r' => out.push_str("\\r"),
15123 _ if OTHER_PUNCTUATORS.contains(c) || is_regex_escape_space(c) => {
15124 let n = c as u32;
15125 if n <= 0xff {
15126 out.push_str(&format!("\\x{n:02x}"));
15127 } else {
15128 out.push_str(&format!("\\u{n:04x}"));
15129 }
15130 }
15131 _ => out.push(c),
15132 }
15133 }
15134 Ok(with_host(|h| h.new_str(out)))
15135}
15136
15137/// The WhiteSpace and LineTerminator code points `RegExp.escape` spells out.
15138/// Deliberately NOT `char::is_whitespace`: U+180E and U+200B are whitespace to
15139/// Unicode but not to ECMAScript, and node leaves both alone.
15140fn is_regex_escape_space(c: char) -> bool {
15141 matches!(
15142 c,
15143 '\u{a0}' | '\u{1680}' | '\u{2000}'
15144 ..='\u{200a}'
15145 | '\u{2028}'
15146 | '\u{2029}'
15147 | '\u{202f}'
15148 | '\u{205f}'
15149 | '\u{3000}'
15150 | '\u{feff}'
15151 )
15152}
15153
15154/// `Error.isError(v)` (20.5.2.1) — a brand check for `[[ErrorData]]`, so an
15155/// object that merely INHERITS from `Error.prototype` is not one.
15156fn error_is_error(args: Vec<Value>) -> Result<Value, String> {
15157 let v = arg0(&args);
15158 Ok(Value::Bool(with_host(|h| has_error_data(h, &v))))
15159}
15160
15161/// Whether `v` carries `[[ErrorData]]` — the slot `Error.isError` (20.5.2.1)
15162/// and `Object.prototype.toString`'s step 9 both test.
15163///
15164/// The brand is the OWN `stack` an error is built with (a `DOMException`
15165/// carries `@@domName` instead); a plain `Object.create(Error.prototype)` has
15166/// neither, which is why inheriting from an error prototype does not make a
15167/// value an error. Shared so the two cannot disagree — branding by a chain
15168/// lookup for `name`/`message` made `Object.create(Error.prototype)` report
15169/// `[object Error]` where node says `[object Object]`, while `Error.isError`
15170/// on the same value already said false.
15171pub(crate) fn has_error_data(h: &host::JsHost, v: &Value) -> bool {
15172 match h.get(v) {
15173 Some(JsObj::Object(p)) => {
15174 p.contains_key("stack") || p.contains_key("@@stackRaw") || p.contains_key("@@domName")
15175 }
15176 _ => false,
15177 }
15178}
15179
15180/// `Promise.try(fn, ...args)` (27.2.4.6) — call `fn` and settle the promise with
15181/// what it does, so a SYNCHRONOUS throw becomes a rejection instead of
15182/// propagating. `Promise.resolve().then(fn)` is the shape it replaces, and it
15183/// costs a tick that this does not.
15184fn promise_try(args: Vec<Value>) -> Result<Value, String> {
15185 let f = arg0(&args);
15186 // A non-callable argument REJECTS, it does not throw: `Promise.try(5)`
15187 // returns a rejected promise, so the surrounding `try` never sees it.
15188 if !with_host(|h| host::is_callable(h, &f)) {
15189 // Node names the TYPE alongside the value — `number 5 is not a
15190 // function` — which the ordinary call-site message does not. A plain
15191 // object and a symbol name only the type; `null` names both.
15192 let shown = with_host(|h| {
15193 let kind = h.type_of(&f);
15194 match kind {
15195 "undefined" => "undefined".to_string(),
15196 "symbol" | "bigint" => kind.to_string(),
15197 "object" if h.is_null(&f) => "object null".to_string(),
15198 "object" => "object".to_string(),
15199 "string" => format!("string \"{}\"", h.str_of(&f)),
15200 _ => format!("{kind} {}", h.str_of(&f)),
15201 }
15202 });
15203 let p = with_host(|h| h.new_promise());
15204 let id = with_host(|h| h.promise_id(&p).unwrap());
15205 let reject = make_builtin(format!("@@preject:{id}"));
15206 let err =
15207 with_host(|h| synth_error(h, &host::type_error(&format!("{shown} is not a function"))));
15208 host::invoke(&reject, vec![err], None)?;
15209 return Ok(p);
15210 }
15211 let rest: Vec<Value> = args.iter().skip(1).cloned().collect();
15212 let p = with_host(|h| h.new_promise());
15213 let id = with_host(|h| h.promise_id(&p).unwrap());
15214 let resolve = make_builtin(format!("@@presolve:{id}"));
15215 let reject = make_builtin(format!("@@preject:{id}"));
15216 let promise = p;
15217 match host::invoke(&f, rest, None) {
15218 Ok(v) => {
15219 host::invoke(&resolve, vec![v], None)?;
15220 }
15221 Err(e) => {
15222 // The thrown VALUE, not a re-synthesis of its rendering: a callback
15223 // that throws a `TypeError` must reject with that object, and
15224 // rebuilding it from the message string flattened it to a plain
15225 // `Error` whose message was the rendered `Uncaught TypeError: t`.
15226 let err =
15227 with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15228 with_host(|h| {
15229 h.error = None;
15230 h.exc = None;
15231 });
15232 host::invoke(&reject, vec![err], None)?;
15233 }
15234 }
15235 Ok(promise)
15236}
15237
15238fn promise_with_resolvers() -> Result<Value, String> {
15239 let p = with_host(|h| h.new_promise());
15240 let id = with_host(|h| h.promise_id(&p).unwrap());
15241 let resolve = make_builtin(format!("@@presolve:{id}"));
15242 let reject = make_builtin(format!("@@preject:{id}"));
15243 let mut props: IndexMap<String, Value> = IndexMap::new();
15244 props.insert("promise".into(), p);
15245 props.insert("resolve".into(), resolve);
15246 props.insert("reject".into(), reject);
15247 Ok(with_host(|h| h.new_object(props)))
15248}
15249
15250/// A promise already rejected with `e` — what every combinator hands back when
15251/// the ITERABLE misbehaves.
15252///
15253/// 27.2.4.1 step 4 catches an abrupt completion from the iteration and rejects
15254/// rather than letting it propagate, so `Promise.all(badIterable)` returns a
15255/// rejected promise. Throwing synchronously meant a `.catch()` never attached
15256/// and the caller saw the error at the call site instead.
15257fn rejected_promise(e: String) -> Value {
15258 let p = with_host(|h| h.new_promise());
15259 let id = with_host(|h| h.promise_id(&p).unwrap());
15260 let reject = make_builtin(format!("@@preject:{id}"));
15261 let err = with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15262 with_host(|h| {
15263 h.error = None;
15264 h.exc = None;
15265 });
15266 let _ = host::invoke(&reject, vec![err], None);
15267 p
15268}
15269
15270#[derive(Clone, Copy)]
15271enum AllMode {
15272 All,
15273 AllSettled,
15274}
15275
15276/// `Promise.all` / `Promise.allSettled`.
15277fn promise_all(args: Vec<Value>, mode: AllMode) -> Result<Value, String> {
15278 let items = match host::iter_all(&arg0(&args)) {
15279 Ok(v) => v,
15280 Err(e) => return Ok(rejected_promise(e)),
15281 };
15282 // 27.2.4.1 step 3: the combinator builds its result with `this`, so on a
15283 // subclass the promise it hands back is an instance of that subclass.
15284 let result = match promise_species_create()? {
15285 Some(p) => p,
15286 None => with_host(|h| h.new_promise()),
15287 };
15288 let rid = with_host(|h| h.promise_id(&result).unwrap());
15289 let n = items.len();
15290 if n == 0 {
15291 let empty = with_host(|h| h.new_array(Vec::new()));
15292 host::resolve_promise_val(rid, empty);
15293 return Ok(result);
15294 }
15295 // Shared mutable accumulator via Rc<RefCell<…>>.
15296 let slots = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15297 let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15298 for (i, it) in items.into_iter().enumerate() {
15299 let ap = host::promise_of(&it);
15300 let aid = with_host(|h| h.promise_id(&ap).unwrap());
15301 let slots = slots.clone();
15302 let remaining = remaining.clone();
15303 host::subscribe_native(
15304 aid,
15305 Box::new(move |state, val| {
15306 let settled = match mode {
15307 AllMode::All => {
15308 if state == host::PromiseState::Rejected {
15309 host::reject_promise_val(rid, val);
15310 return Ok(());
15311 }
15312 val
15313 }
15314 AllMode::AllSettled => with_host(|h| {
15315 let mut m: IndexMap<String, Value> = IndexMap::new();
15316 if state == host::PromiseState::Rejected {
15317 m.insert("status".into(), h.new_str("rejected"));
15318 m.insert("reason".into(), val);
15319 } else {
15320 m.insert("status".into(), h.new_str("fulfilled"));
15321 m.insert("value".into(), val);
15322 }
15323 h.new_object(m)
15324 }),
15325 };
15326 slots.borrow_mut()[i] = settled;
15327 let mut r = remaining.borrow_mut();
15328 *r -= 1;
15329 if *r == 0 {
15330 let arr = with_host(|h| h.new_array(slots.borrow().clone()));
15331 host::resolve_promise_val(rid, arr);
15332 }
15333 Ok(())
15334 }),
15335 );
15336 }
15337 Ok(result)
15338}
15339
15340/// `Promise.race` (first to settle wins) / `Promise.any` (first to fulfill wins).
15341fn promise_race(args: Vec<Value>, any: bool) -> Result<Value, String> {
15342 let items = match host::iter_all(&arg0(&args)) {
15343 Ok(v) => v,
15344 Err(e) => return Ok(rejected_promise(e)),
15345 };
15346 // Built with `this`, as every combinator is (27.2.4.5 / 27.2.4.3).
15347 let result = match promise_species_create()? {
15348 Some(p) => p,
15349 None => with_host(|h| h.new_promise()),
15350 };
15351 let rid = with_host(|h| h.promise_id(&result).unwrap());
15352 let n = items.len();
15353 let errors = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15354 let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15355 for (i, it) in items.into_iter().enumerate() {
15356 let ap = host::promise_of(&it);
15357 let aid = with_host(|h| h.promise_id(&ap).unwrap());
15358 let errors = errors.clone();
15359 let remaining = remaining.clone();
15360 host::subscribe_native(
15361 aid,
15362 Box::new(move |state, val| {
15363 if any {
15364 if state == host::PromiseState::Fulfilled {
15365 host::resolve_promise_val(rid, val);
15366 } else {
15367 errors.borrow_mut()[i] = val;
15368 let mut r = remaining.borrow_mut();
15369 *r -= 1;
15370 if *r == 0 {
15371 // All rejected → AggregateError carrying every reason.
15372 let reasons = with_host(|h| h.new_array(errors.borrow().clone()));
15373 let msg = with_host(|h| h.new_str("All promises were rejected"));
15374 let agg = make_error_inner("AggregateError", &[reasons, msg]);
15375 host::reject_promise_val(rid, agg);
15376 }
15377 }
15378 } else if state == host::PromiseState::Rejected {
15379 host::reject_promise_val(rid, val);
15380 } else {
15381 host::resolve_promise_val(rid, val);
15382 }
15383 Ok(())
15384 }),
15385 );
15386 }
15387 Ok(result)
15388}
15389
15390/// `.then` / `.catch` / `.finally` on a promise.
15391fn promise_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
15392 match name {
15393 "then" => Ok(host::promise_then(
15394 recv,
15395 args.first().cloned().unwrap_or(Value::Undef),
15396 args.get(1).cloned().unwrap_or(Value::Undef),
15397 )),
15398 "catch" => Ok(host::promise_then(
15399 recv,
15400 Value::Undef,
15401 args.first().cloned().unwrap_or(Value::Undef),
15402 )),
15403 "finally" => {
15404 let cb = arg0(&args);
15405 // 27.2.5.3 step 3: a non-callable `onFinally` is handed to `then`
15406 // as BOTH handlers, and `then` ignores a non-callable one — so the
15407 // value or reason simply passes through. Building the thunks
15408 // regardless meant `p.finally(null)` tried to call `null`.
15409 if !with_host(|h| host::is_callable(h, &cb)) {
15410 return Ok(host::promise_then(recv, cb.clone(), cb));
15411 }
15412 let i = match cb {
15413 Value::Obj(i) => i,
15414 _ => 0,
15415 };
15416 let pass = make_builtin(format!("@@finpass:{i}"));
15417 let throw = make_builtin(format!("@@finthrow:{i}"));
15418 Ok(host::promise_then(recv, pass, throw))
15419 }
15420 _ => Err(host::type_error(&format!(
15421 "promise.{name} is not a function"
15422 ))),
15423 }
15424}
15425
15426fn enqueue_microtask(next_tick: bool, cb: Value, args: Vec<Value>) {
15427 with_host(|h| {
15428 if next_tick {
15429 h.queue_nexttick(cb, args);
15430 } else {
15431 h.queue_micro(cb, args);
15432 }
15433 });
15434}
15435
15436/// `setTimeout`/`setInterval`/`setImmediate` — register a macrotask and return
15437/// the handle object Node returns (`Timeout` for the first two, `Immediate` for
15438/// the third), carrying `ref`/`unref`/`hasRef`/`refresh`.
15439///
15440/// `setInterval` schedules a *repeating* timer: the loop re-arms it each time it
15441/// fires, so it runs until cleared and — being referenced — holds the process
15442/// open exactly as in Node.
15443fn schedule_timer(name: &str, args: Vec<Value>) -> Value {
15444 let cb = arg0(&args);
15445 let delay = if name == "setImmediate" {
15446 -1.0 // before any 0ms timeout
15447 } else {
15448 args.get(1)
15449 .map(|d| with_host(|h| h.to_number(d)))
15450 .unwrap_or(0.0)
15451 .max(0.0)
15452 };
15453 let extra = if name == "setImmediate" {
15454 args.get(1..).map(|s| s.to_vec()).unwrap_or_default()
15455 } else {
15456 args.get(2..).map(|s| s.to_vec()).unwrap_or_default()
15457 };
15458 // Node clamps a sub-1ms interval to 1ms, so `setInterval(fn, 0)` yields a
15459 // ~1000Hz timer rather than a busy loop that starves the rest of the queue.
15460 let interval = (name == "setInterval").then(|| delay.max(1.0));
15461 let id = with_host(|h| h.add_timer(delay, cb, extra, interval));
15462 let tag = if name == "setImmediate" {
15463 "Immediate"
15464 } else {
15465 "Timeout"
15466 };
15467 crate::stdlib::timers::new_handle(id, tag)
15468}
15469
15470/// `clearTimeout`/`clearInterval`/`clearImmediate` — cancel by handle object or
15471/// by the bare id it coerces to (code that stored `+timer` still works).
15472fn clear_timer(v: &Value) {
15473 let id =
15474 crate::stdlib::timers::handle_id(v).unwrap_or_else(|| with_host(|h| h.to_number(v)) as u64);
15475 with_host(|h| h.cancel_timer(id));
15476}