nodejs/builtins.rs
1//! Builtin op handlers (compiler-emitted `CallBuiltin` ids) plus the JS standard
2//! library (`console`, `Math`, `JSON`, `Object`, array/string methods) reachable
3//! from the host. Handlers pop their arguments off the VM operand stack and
4//! return the result value, which the VM pushes back.
5
6use crate::host::{self, ops, with_host, FuncVal, JsObj, ObjKind};
7use fusevm::{NumOp, Value, VM};
8use indexmap::IndexMap;
9
10/// Register every node-js builtin id on a VM.
11pub fn install(vm: &mut VM) {
12 vm.register_builtin(ops::GETLOCAL, b_getlocal);
13 vm.register_builtin(ops::SETLOCAL, b_setlocal);
14 vm.register_builtin(ops::SETLOCAL_STRICT, b_setlocal_strict);
15 vm.register_builtin(ops::DECLARE, b_declare);
16 vm.register_builtin(ops::DECLARE_CONST, b_declare_const);
17 vm.register_builtin(ops::MARK_HOLE, b_mark_hole);
18 vm.register_builtin(ops::DELNAME, b_delname);
19 vm.register_builtin(ops::GETATTR, b_getattr);
20 vm.register_builtin(ops::SETATTR, b_setattr);
21 vm.register_builtin(ops::GETITEM, b_getitem);
22 vm.register_builtin(ops::SETITEM, b_setitem);
23 vm.register_builtin(ops::DELITEM, b_delitem);
24 vm.register_builtin(ops::MKSTR, b_mkstr);
25 vm.register_builtin(ops::MKARR, b_mkarr);
26 vm.register_builtin(ops::MKOBJ, b_mkobj);
27 vm.register_builtin(ops::CALL, b_call);
28 vm.register_builtin(ops::CALL_METHOD, b_call_method);
29 vm.register_builtin(ops::CALL_VALUE, b_call_value);
30 vm.register_builtin(ops::NEW, b_new);
31 vm.register_builtin(ops::TRUTHY, b_truthy);
32 vm.register_builtin(ops::TOSTR, b_tostr);
33 vm.register_builtin(ops::MKFUNC, b_mkfunc);
34 vm.register_builtin(ops::GETITER, b_getiter);
35 vm.register_builtin(ops::FORITER, b_foriter);
36 vm.register_builtin(ops::FORIN_KEYS, b_forin_keys);
37 vm.register_builtin(ops::FORIN_ALIVE, b_forin_alive);
38 vm.register_builtin(ops::HOIST_TDZ, b_hoist_tdz);
39 vm.register_builtin(ops::NEW_SPREAD, b_new_spread);
40 vm.register_builtin(ops::SUPER_CALL_SPREAD, b_super_call_spread);
41 vm.register_builtin(ops::CONTAINS, b_contains);
42 vm.register_builtin(ops::SIG_RETURN, b_sig_return);
43 vm.register_builtin(ops::BINOP, b_binop);
44 vm.register_builtin(ops::UNARY, b_unary);
45 vm.register_builtin(ops::STRICT_EQ, b_strict_eq);
46 vm.register_builtin(ops::LOOSE_EQ, b_loose_eq);
47 vm.register_builtin(ops::TYPEOF, b_typeof);
48 vm.register_builtin(ops::LOAD_NULL, b_load_null);
49 vm.register_builtin(ops::THROW, b_throw);
50 vm.register_builtin(ops::TRY, b_try);
51 vm.register_builtin(ops::NULLISH, b_nullish);
52 vm.register_builtin(ops::UNPACK, b_unpack);
53 vm.register_builtin(ops::BUILD_ARGS, b_build_args);
54 vm.register_builtin(ops::THIS, b_this);
55 vm.register_builtin(ops::INSTANCEOF, b_instanceof);
56 vm.register_builtin(ops::DELPROP_NAME, b_delprop_name);
57 vm.register_builtin(ops::APPLY, b_apply);
58 vm.register_builtin(ops::APPLY_METHOD, b_apply_method);
59 vm.register_builtin(ops::OBJ_REST, b_obj_rest);
60 vm.register_builtin(ops::DIV, b_div);
61 vm.register_builtin(ops::POW, b_pow);
62 vm.register_builtin(ops::MKCLASS, b_mkclass);
63 vm.register_builtin(ops::DEF_MEMBER, b_def_member);
64 vm.register_builtin(ops::DEF_FIELD, b_def_field);
65 vm.register_builtin(ops::SUPER_CALL, b_super_call);
66 vm.register_builtin(ops::SUPER_GET, b_super_get);
67 vm.register_builtin(ops::YIELD, b_yield);
68 vm.register_builtin(ops::PROPKEY, b_propkey);
69 vm.register_builtin(ops::NEW_TARGET, b_new_target);
70 vm.register_builtin(ops::AWAIT, b_await);
71 vm.register_builtin(ops::DEF_ACCESSOR, b_def_accessor);
72 vm.register_builtin(ops::DBG_LINE, b_dbg_line);
73 vm.register_builtin(ops::MKBIGINT, b_mkbigint);
74 vm.register_builtin(ops::MKREGEX, b_mkregex);
75 vm.register_builtin(ops::TAG_TMPL, b_tag_tmpl);
76 vm.register_builtin(ops::GET_ASYNC_ITER, b_get_async_iter);
77 vm.register_builtin(ops::ASYNC_STEP, b_async_step);
78 vm.register_builtin(ops::NUM_STEP, b_num_step);
79 vm.register_builtin(ops::ITER_CLOSE, b_iter_close);
80 vm.register_builtin(ops::TYPEOF_NAME, b_typeof_name);
81 vm.register_builtin(ops::SIG_BREAK, b_sig_break);
82 vm.register_builtin(ops::SIG_CONTINUE, b_sig_continue);
83 vm.register_builtin(ops::SIG_UNWIND, b_sig_unwind);
84 vm.register_builtin(ops::PUSH_SCOPE, b_push_scope);
85 vm.register_builtin(ops::POP_SCOPE, b_pop_scope);
86 vm.register_builtin(ops::COPY_SCOPE, b_copy_scope);
87 vm.register_builtin(ops::DECLARE_VAR, b_declare_var);
88 vm.register_builtin(ops::HOIST_VAR, b_hoist_var);
89 vm.register_builtin(ops::NAMED_EVAL, b_named_eval);
90}
91
92/// `ITER_CLOSE`: close the iterator on the stack (a for-of `break`). A generator
93/// runs its pending `finally`; a user iterator object gets its `.return()` called
94/// if present; a plain materialized iterator just drops. Returns `undefined`.
95/// `IteratorClose` (7.4.9): resume a generator with a forced return so its
96/// pending `finally` runs, or invoke a user iterator's `.return()`. A value that
97/// is neither is left alone.
98pub(crate) fn close_iterator(it: &Value) -> Result<(), String> {
99 if with_host(|h| h.is_generator_val(it)) {
100 host::gen_return(it, Value::Undef)?;
101 return Ok(());
102 }
103 if matches!(with_host(|h| h.get(it).cloned()), Some(JsObj::Object(_))) {
104 if let Some(f) = with_host(|h| host::lookup_chain(h, it, "return")) {
105 if with_host(|h| host::is_callable(h, &f)) {
106 host::invoke(&f, Vec::new(), Some(it.clone()))?;
107 }
108 }
109 }
110 Ok(())
111}
112
113fn b_iter_close(vm: &mut VM, _: u8) -> Value {
114 let it = vm.pop();
115 // A `finally` may print or yield, but the loop is done either way; an error
116 // it raises still propagates.
117 match close_iterator(&it) {
118 Ok(()) => Value::Undef,
119 Err(e) => abort(vm, e),
120 }
121}
122
123/// `NUM_STEP`: the `++`/`--` core. Pops `old` and the step `tag` (`+1`/`-1`),
124/// pushes `ToNumeric(old)` (a BigInt stays a BigInt, else a Number), and returns
125/// `old ± 1` in the SAME numeric type — so `x++` on a BigInt neither coerces to
126/// Number nor throws the mix error.
127fn b_num_step(vm: &mut VM, _: u8) -> Value {
128 let old = vm.pop();
129 let tag = match vm.pop() {
130 Value::Int(n) => n,
131 Value::Float(f) => f as i64,
132 _ => 1,
133 };
134 if with_host(|h| h.is_bigint_val(&old)) {
135 let b = with_host(|h| h.as_bigint(&old)).unwrap();
136 let old_n = with_host(|h| h.new_bigint(b.clone()));
137 let new = with_host(|h| h.new_bigint(b + num_bigint::BigInt::from(tag)));
138 vm.push(old_n);
139 new
140 } else {
141 let n = with_host(|h| h.to_number(&old));
142 vm.push(Value::Float(n));
143 Value::Float(n + tag as f64)
144 }
145}
146
147/// `ASYNC_STEP`: one step of a `for await` loop — returns a Promise of the
148/// `{value, done}` record (see `host::async_step`).
149fn b_async_step(vm: &mut VM, _: u8) -> Value {
150 let iter = vm.pop();
151 let r = host::async_step(&iter);
152 finish(vm, r)
153}
154
155/// `MKBIGINT`: pop the canonical decimal digit string constant, allocate the heap
156/// BigInt. The lexer already validated the digits, so parsing cannot fail here.
157fn b_mkbigint(vm: &mut VM, _: u8) -> Value {
158 let digits = sval(&vm.pop());
159 match digits.parse::<num_bigint::BigInt>() {
160 Ok(b) => with_host(|h| h.new_bigint(b)),
161 Err(_) => abort(vm, host::type_error("invalid BigInt literal")),
162 }
163}
164
165/// `TAG_TMPL`: invoke a tagged template. The compiler emits the operands as
166/// `[tag, n, m, cooked×n, raw×n, values×m]` (see `compile_tagged_template`).
167/// Builds the `strings` array (carrying its `.raw` array) and calls
168/// `tag(strings, ...values)`.
169/// Reject a non-callable where node's scheduling entry points demand one.
170///
171/// Every one of them validates SYNCHRONOUSLY — `try { queueMicrotask(1) }
172/// catch` catches an `ERR_INVALID_ARG_TYPE` in node. Here the value was queued
173/// unchecked and the failure surfaced from the event loop instead, as an
174/// uncaught `1 is not a function` that killed the process past any `try` around
175/// the call.
176fn require_callback(cb: &Value) -> Result<(), String> {
177 if with_host(|h| host::is_callable(h, cb)) {
178 return Ok(());
179 }
180 Err(host::invalid_arg_type(
181 "callback", "argument", "function", cb,
182 ))
183}
184
185fn b_tag_tmpl(vm: &mut VM, argc: u8) -> Value {
186 // The chunk holding this site, read before the operands are popped and
187 // before any host borrow: together with the compiler's per-site ordinal it
188 // names the Parse Node whose template object 13.2.8.4 caches.
189 let chunk = vm.chunk.op_hash;
190 let mut all = pop_n(vm, argc as usize);
191 let int_of = |v: &Value| match v {
192 Value::Int(n) => *n as usize,
193 Value::Float(f) => *f as usize,
194 _ => 0,
195 };
196 let this = all.remove(0);
197 let tag = all.remove(0);
198 let n = int_of(&all.remove(0));
199 let mcount = int_of(&all.remove(0));
200 let site = int_of(&all.remove(0)) as u64;
201 let cooked: Vec<Value> = all.drain(0..n.min(all.len())).collect();
202 let raw: Vec<Value> = all.drain(0..n.min(all.len())).collect();
203 let values: Vec<Value> = all.drain(0..mcount.min(all.len())).collect();
204 // GetTemplateObject caches by Parse Node, so a site evaluated twice hands
205 // back the SAME object — the whole point of the caching, since a tag that
206 // memoizes on the strings array (lit-html, graphql-tag) re-parses its
207 // template on every call without it.
208 let key = (chunk, site);
209 let strings = match with_host(|h| h.template_object(key)) {
210 Some(cached) => cached,
211 None => {
212 // strings = cooked array; strings.raw = raw array.
213 let strings = with_host(|h| h.new_array(cooked));
214 let raw_arr = with_host(|h| h.new_array(raw));
215 // `raw` is an own property that is neither writable, enumerable, nor
216 // configurable, so it stays out of `Object.keys(strings)` while
217 // `getOwnPropertyNames` still reports it.
218 with_host(|h| {
219 h.set_fn_prop(&strings, "raw", raw_arr.clone());
220 h.set_prop_attrs(
221 &strings,
222 "raw",
223 host::PropAttrs {
224 writable: false,
225 enumerable: false,
226 configurable: false,
227 },
228 );
229 // Steps 12-13 run SetIntegrityLevel(frozen) on the raw array and
230 // then on the template object itself. Without them a tag could
231 // write through its own strings array and corrupt every later
232 // evaluation of the site — which is exactly what caching makes
233 // reachable, so the freeze and the cache belong together.
234 h.seal_object(&raw_arr, true);
235 h.seal_object(&strings, true);
236 h.set_template_object(key, strings.clone());
237 });
238 strings
239 }
240 };
241 let mut call_args = vec![strings];
242 call_args.extend(values);
243 let this = match this {
244 Value::Undef => None,
245 v => Some(v),
246 };
247 let r = host::invoke(&tag, call_args, this);
248 finish(vm, r)
249}
250
251/// `GET_ASYNC_ITER`: obtain an async iterator for `for await (… of …)`. If the
252/// value has a `Symbol.asyncIterator`, use it; otherwise fall back to its sync
253/// iterator (each yielded value is awaited). Returns the iterator object/handle.
254fn b_get_async_iter(vm: &mut VM, _: u8) -> Value {
255 let src = vm.pop();
256 let r = host::get_async_iterator(&src).map_err(|e| {
257 // `for await` names the source AND says ASYNC: `for await (const x of
258 // o)` is `o is not async iterable`. Built here rather than through
259 // `name_call_site`, whose suffix table has no entry that composes.
260 match host::call_site_text(vm) {
261 Some(t) if e.ends_with(" is not iterable") => {
262 host::type_error(&format!("{t} is not async iterable"))
263 }
264 _ => e,
265 }
266 });
267 finish(vm, r)
268}
269
270/// `MKREGEX`: pop `(pattern, flags)`, translate the JS pattern to a Rust `regex`,
271/// and allocate a `RegExp`. A pattern using a JS feature Rust `regex` cannot
272/// express (backreference/lookaround) throws a `SyntaxError` here.
273fn b_mkregex(vm: &mut VM, _: u8) -> Value {
274 let flags = sval(&vm.pop());
275 let pattern = sval(&vm.pop());
276 match crate::regexp::build_regexp(&pattern, &flags) {
277 Ok(v) => v,
278 Err(e) => abort(vm, e),
279 }
280}
281
282/// DAP per-statement marker (`node --dap` only; the compiler emits this before
283/// each statement under `debug`). Pops the source line pushed by the preceding
284/// `LoadInt` and fires the debugger line hook, which pauses at breakpoints/step
285/// targets. Returns `undefined` (the compiler pops it). A no-op unless a debug
286/// session is active.
287fn b_dbg_line(vm: &mut VM, _: u8) -> Value {
288 let line = match vm.pop() {
289 Value::Int(n) => n as u32,
290 _ => 0,
291 };
292 crate::dap::on_debug_line(line);
293 Value::Undef
294}
295
296/// Install an object-literal getter/setter on an object (`kind` is `member::GET`
297/// or `member::SET`). Keeps the object on the stack.
298fn b_def_accessor(vm: &mut VM, _: u8) -> Value {
299 let func = vm.pop();
300 let kind = match vm.pop() {
301 Value::Int(n) => n,
302 _ => 0,
303 };
304 let name = sval(&vm.pop());
305 let obj = vm.pop();
306 with_host(|h| {
307 if kind == host::member::SET {
308 h.set_accessor(&obj, &name, None, Some(func));
309 } else {
310 h.set_accessor(&obj, &name, Some(func), None);
311 }
312 });
313 obj
314}
315
316fn b_await(vm: &mut VM, _: u8) -> Value {
317 let v = vm.pop();
318 match host::await_value(v) {
319 Ok(r) => r,
320 Err(e) => abort(vm, e),
321 }
322}
323
324// ── classes / super / generators / property keys (compiler-emitted ops) ──────
325
326fn b_mkclass(vm: &mut VM, argc: u8) -> Value {
327 // The fourth argument, when present, is the FuncDef carrying the class's
328 // source span.
329 let source_def = match argc {
330 4 => match vm.pop() {
331 Value::Int(n) => Some(n as usize),
332 _ => None,
333 },
334 _ => None,
335 };
336 let ctor = vm.pop();
337 let parent = vm.pop();
338 let name = sval(&vm.pop());
339 host::build_class(&name, parent, ctor, source_def)
340}
341
342fn b_def_member(vm: &mut VM, _: u8) -> Value {
343 let func = vm.pop();
344 let is_static = matches!(vm.pop(), Value::Bool(true));
345 let kind = match vm.pop() {
346 Value::Int(n) => n,
347 _ => 0,
348 };
349 let name = sval(&vm.pop());
350 let class_val = vm.pop();
351 host::define_member(&class_val, &name, kind, is_static, func);
352 class_val
353}
354
355fn b_def_field(vm: &mut VM, _: u8) -> Value {
356 // `name_anon`: the initializer was an anonymous function definition, so
357 // 15.7.10 NamedEvaluation names its result after the field. Syntactic —
358 // decided by the compiler, not re-derived from the produced value.
359 let name_anon = matches!(vm.pop(), Value::Bool(true));
360 let thunk = vm.pop();
361 let name = sval(&vm.pop());
362 let class_val = vm.pop();
363 host::define_field(&class_val, &name, thunk, name_anon);
364 class_val
365}
366
367/// `super(...args)` in a derived constructor: run the parent constructor on the
368/// current `this`, then this class's field initializers.
369/// `SUPER_CALL_SPREAD` — `super(...xs)`, where the argument list is built at
370/// run time. Shares everything below with the fixed-arity form; only where the
371/// arguments come from differs.
372fn b_super_call_spread(vm: &mut VM, _: u8) -> Value {
373 let arr = vm.pop();
374 let args = host::iter_all(&arr).unwrap_or_default();
375 super_call_with(vm, args)
376}
377
378fn b_super_call(vm: &mut VM, argc: u8) -> Value {
379 let args = pop_n(vm, argc as usize);
380 super_call_with(vm, args)
381}
382
383fn super_call_with(vm: &mut VM, args: Vec<Value>) -> Value {
384 let this = with_host(|h| h.current_this());
385 let this = match this {
386 Some(t) => t,
387 None => return abort(vm, host::type_error("'super' keyword unexpected here")),
388 };
389 // The class whose constructor is running = the running method's home class.
390 let (parent, fields) = with_host(|h| h.super_context());
391 let (parent, fields) = match parent {
392 Some(p) => (p, fields),
393 None => return abort(vm, host::type_error("'super' keyword unexpected here")),
394 };
395 let nt = with_host(|h| h.current_new_target()).unwrap_or_else(|| this.clone());
396 let this = match host::super_construct(&parent, args, &this, &nt) {
397 Err(e) => return abort(vm, e),
398 // The parent returned an object of its own: 15.7.15 makes THAT the
399 // instance, so `this` is rebound to it for the rest of the constructor
400 // and it is what `new` hands back.
401 Ok(Some(replacement)) => {
402 with_host(|h| h.set_current_this(replacement.clone()));
403 replacement
404 }
405 Ok(None) => this,
406 };
407 if !with_host(|h| h.bind_super_this()) {
408 return abort(
409 vm,
410 "ReferenceError: Super constructor may only be called once".to_string(),
411 );
412 }
413 // Run this (derived) class's own instance-field initializers after super.
414 for (name, thunk, name_anon) in fields {
415 if let Err(e) = host::init_one_field(&this, &name, &thunk, name_anon) {
416 return abort(vm, e);
417 }
418 }
419 Value::Undef
420}
421
422/// `super.name` — a method from the parent's prototype, or a getter's result.
423fn b_super_get(vm: &mut VM, _: u8) -> Value {
424 let name = sval(&vm.pop());
425 match with_host(|h| h.super_resolve(&name)) {
426 host::SuperRef::Data(v) => v,
427 host::SuperRef::Getter(getter) => {
428 let this = with_host(|h| h.current_this());
429 match host::invoke(&getter, Vec::new(), this) {
430 Ok(v) => v,
431 Err(e) => abort(vm, e),
432 }
433 }
434 }
435}
436
437/// Close every loop iterator parked on `vm`'s stack at the op now executing,
438/// innermost first. Called where a chunk is about to be halted abruptly, since
439/// the code that would ordinarily close them is being jumped over.
440///
441/// A close runs user code (a generator's `finally`), which can itself throw; the
442/// error is deliberately dropped, because it must not replace the completion
443/// that caused the unwind.
444fn close_parked_iters(vm: &mut VM) {
445 let n = host::parked_iters(vm);
446 if n == 0 {
447 return;
448 }
449 // The completion that caused the unwind is already pending on the host.
450 // Closing an iterator resumes ANOTHER generator, which settles its own
451 // signal/error state, so the pending one is saved across the close and put
452 // back — otherwise the outer `.return()` would be lost.
453 let saved = with_host(|h| (h.signal.take(), h.error.take()));
454 for _ in 0..n {
455 let it = vm.pop();
456 let _ = close_iterator(&it);
457 }
458 with_host(|h| {
459 h.signal = saved.0;
460 h.error = saved.1;
461 });
462}
463
464fn b_yield(vm: &mut VM, _: u8) -> Value {
465 let v = vm.pop();
466 match host::gen_yield(v) {
467 Ok(sent) => {
468 // A `.return()`/`.throw()` injected on resume sets a pending Return
469 // signal (or error); halt the chunk so the body unwinds through any
470 // enclosing `try/finally`, exactly like a source `return`/`throw`.
471 if with_host(|h| h.error.is_some() || h.signal.is_some()) {
472 // Halting jumps past the loop exits, so the `for…of` / `yield*`
473 // iterators parked on this chunk's stack would be abandoned
474 // still-suspended. They sit directly beneath the yielded value
475 // (innermost last), and the compiler recorded how many are
476 // there for this exact op.
477 close_parked_iters(vm);
478 vm.ip = vm.chunk.ops.len();
479 }
480 sent
481 }
482 // An injected `.throw()` comes back as an error rather than a signal,
483 // and abandons the parked iterators the same way. The thrown value is
484 // already on the host as `exc`; `close_parked_iters` puts back whatever
485 // it saves, so the close cannot swallow it.
486 Err(e) => {
487 close_parked_iters(vm);
488 abort(vm, e)
489 }
490 }
491}
492
493/// `PROPKEY` — ToPropertyKey (7.1.19) for an object literal's COMPUTED key.
494///
495/// It called `JsHost::property_key` directly, which is the primitive-only half
496/// of the conversion, so an object key never ran `ToPrimitive`:
497/// `{ [{toString(){return "TS"}}]: 1 }` keyed on `"[object Object]"` while the
498/// member form `a[o] = 1` — which does go through `host::to_property_key` —
499/// keyed on `"TS"`. The two forms are the same abstract operation and now share
500/// the same implementation.
501fn b_propkey(vm: &mut VM, _: u8) -> Value {
502 let v = vm.pop();
503 match host::to_property_key(&v) {
504 Ok(k) => with_host(|h| h.new_str(k)),
505 Err(e) => abort(vm, e),
506 }
507}
508
509fn b_new_target(_vm: &mut VM, _: u8) -> Value {
510 with_host(|h| h.current_new_target().unwrap_or(Value::Undef))
511}
512
513/// `a / b` with JS/IEEE-754 semantics. fusevm's native `Op::Div` returns `Undef`
514/// for a zero divisor (so a frontend whose `/` differs must lower to a builtin —
515/// its own documented guidance), but JavaScript requires `x/0 === ±Infinity` and
516/// `0/0 === NaN`, so `/` is lowered here instead.
517///
518/// Being a builtin rather than a native op means it does NOT reach the numeric
519/// hook, so `/` was the one arithmetic operator that never ran `ToPrimitive`:
520/// `({valueOf(){return 7}}) / 2` was `NaN` where every other operator gave
521/// `3.5`, and `new Date(2) / 1` was `NaN` instead of `2`. It goes through the
522/// hook now, so `/` coerces exactly as `*` and `-` do.
523fn b_div(vm: &mut VM, _: u8) -> Value {
524 let b = vm.pop();
525 let a = vm.pop();
526 let r = numeric_hook(NumOp::Div, &a, &b);
527 finish(vm, r)
528}
529
530/// `a ** b`. Same reason `/` is a builtin: fusevm's native `Op::Pow` is IEEE-754
531/// `pow`, which returns 1 for `(-1) ** Infinity` and for `1 ** NaN` where the
532/// spec says NaN. Routing through the numeric hook also keeps BigInt `**` on the
533/// one code path that already handles it.
534fn b_pow(vm: &mut VM, _: u8) -> Value {
535 let b = vm.pop();
536 let a = vm.pop();
537 let r = numeric_hook(NumOp::Pow, &a, &b);
538 finish(vm, r)
539}
540
541/// `{ ...rest } = obj`: a new object of `obj`'s own keys minus the excluded set.
542fn b_obj_rest(vm: &mut VM, _: u8) -> Value {
543 let excluded = vm.pop();
544 let obj = vm.pop();
545 // The excluded keys are normalized exactly as a property READ normalizes
546 // them, not merely stringified: a symbol key lives on the object under its
547 // internal `@@sym:<id>` spelling, and `str_of` renders it `Symbol(k)`, which
548 // matches no key at all — so `const { [sym]: v, ...rest } = o` left the
549 // symbol-keyed property in `rest`.
550 let excl: Vec<String> = with_host(|h| h.iter_vec(&excluded))
551 .unwrap_or_default()
552 .iter()
553 .filter_map(|v| host::to_property_key(v).ok())
554 .collect();
555 // CopyDataProperties (ECMA-262 7.3.25) copies the own ENUMERABLE keys,
556 // symbol-keyed ones included. `own_enum_key_names` is what `Object.keys`
557 // uses, so an ACCESSOR is in the list — reading the property map directly
558 // missed one entirely, and `const { ...r } = { get g() {…} }` produced an
559 // object with no `g` and never ran the getter.
560 // A PROXY answers from its traps — `ownKeys`, then a
561 // `getOwnPropertyDescriptor` per key to test enumerability — which
562 // `own_enum_key_names` cannot see. Rest over one produced an empty object
563 // and ran no traps at all.
564 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
565 let keys = match crate::proxy::own_keys(&obj) {
566 Ok(k) => k.unwrap_or_default(),
567 Err(e) => return abort(vm, e),
568 };
569 let mut pairs: Vec<(String, Value)> = Vec::new();
570 for k in keys {
571 if excl.contains(&k) {
572 continue;
573 }
574 // The enumerability test and the READ interleave per key, as node's
575 // trap log shows — testing every key first and then reading them
576 // all produced the right object through the wrong trap sequence.
577 match crate::proxy::own_enumerable(&obj, &k) {
578 Ok(false) => continue,
579 Ok(true) => {}
580 Err(e) => return abort(vm, e),
581 }
582 match get_property(&obj, &k) {
583 Ok(v) => pairs.push((k, v)),
584 Err(e) => return abort(vm, e),
585 }
586 }
587 return with_host(|h| h.new_object(pairs.into_iter().collect()));
588 }
589 let keys: Vec<String> = with_host(|h| {
590 // `own_enum_key_names` is the STRING half — the same list `Object.keys`
591 // gives, so an accessor is in it. The symbol-keyed half lives in the
592 // property map under the internal `@@sym:` spelling and has to be
593 // collected separately, since `Object.keys` deliberately omits it.
594 let mut ks = h.own_enum_key_names(&obj);
595 if let Some(JsObj::Object(m)) = h.get(&obj) {
596 for k in m.keys() {
597 if host::is_symbol_key(k) && h.prop_attrs(&obj, k).enumerable {
598 ks.push(k.clone());
599 }
600 }
601 }
602 ks
603 })
604 .into_iter()
605 .filter(|k| {
606 // An internal slot (`@@native`, `@@bytes`, …) or a private class field
607 // is not a property; a SYMBOL key shares the `@@` prefix but is one, so
608 // the two cases cannot be told apart by the prefix alone.
609 !excl.contains(k)
610 && (host::is_symbol_key(k) || !(k.starts_with("@@") || k.starts_with('#')))
611 })
612 .collect();
613 // Each value is read through `[[Get]]`, OUTSIDE the host borrow: a getter is
614 // user code and re-entering the VM under the borrow aborts the process.
615 let mut pairs: Vec<(String, Value)> = Vec::with_capacity(keys.len());
616 for k in keys {
617 match get_property(&obj, &k) {
618 Ok(v) => pairs.push((k, v)),
619 Err(e) => return abort(vm, e),
620 }
621 }
622 with_host(|h| {
623 let props: IndexMap<String, Value> = pairs.into_iter().collect();
624 h.new_object(props)
625 })
626}
627
628// ── helpers ──────────────────────────────────────────────────────────────────
629
630fn pop_n(vm: &mut VM, n: usize) -> Vec<Value> {
631 let mut v = Vec::with_capacity(n);
632 for _ in 0..n {
633 v.push(vm.pop());
634 }
635 v.reverse();
636 v
637}
638
639/// Read a compiler-internal name string (native `Value::Str` or heap `str`).
640fn sval(v: &Value) -> String {
641 if let Value::Str(s) = v {
642 return (**s).clone();
643 }
644 with_host(|h| h.as_str(v)).unwrap_or_default()
645}
646
647/// The same string, without `sval`'s deep copy. Every identifier the compiler
648/// emits is a `Value::Str` constant, so a variable read or write that went
649/// through `sval` heap-allocated and memcpy'd the NAME once per access — on the
650/// hot path of every loop. `Value::Str` is an `Arc<String>`, so cloning the
651/// handle is a refcount bump instead.
652fn sname(v: &Value) -> std::sync::Arc<String> {
653 match v {
654 Value::Str(s) => s.clone(),
655 _ => std::sync::Arc::new(sval(v)),
656 }
657}
658
659fn abort(vm: &mut VM, e: String) -> Value {
660 with_host(|h| h.error = Some(e));
661 vm.ip = vm.chunk.ops.len();
662 Value::Undef
663}
664
665/// Halt the chunk if a call left an error or non-local signal pending.
666fn finish(vm: &mut VM, r: Result<Value, String>) -> Value {
667 match r {
668 Ok(v) => {
669 if with_host(|h| h.error.is_some() || h.signal.is_some()) {
670 vm.ip = vm.chunk.ops.len();
671 }
672 v
673 }
674 Err(e) => abort(vm, e),
675 }
676}
677
678// ── name handlers ─────────────────────────────────────────────────────────────
679
680/// The value a bare global identifier resolves to, or `None` if unbound.
681///
682/// Shared by `b_getlocal` (the `x` form) and the `globalThis.x` property read,
683/// which must agree: a name reachable one way and not the other is exactly the
684/// discrepancy that left `globalThis.process` undefined while `process` worked.
685pub(crate) fn global_binding(name: &str) -> Option<Value> {
686 global_binding_from(name, false)
687}
688
689/// [`global_binding`] restricted to what the GLOBAL OBJECT really holds.
690///
691/// A `globalThis.x` read falls back to the same lazy binding a bare `x` gets,
692/// which is what makes `globalThis.Math` and `globalThis.process` work — but
693/// the bare-identifier lookup walks the SCOPE CHAIN, so while any function was
694/// running its locals were readable off `globalThis`: `function f() { let zzq =
695/// 2; return typeof globalThis.zzq }` answered for a name the global object has
696/// never heard of. Only the globals map and the lazy builtins below may answer
697/// here.
698pub(crate) fn global_object_binding(name: &str) -> Option<Value> {
699 global_binding_from(name, true)
700}
701
702fn global_binding_from(name: &str, object_only: bool) -> Option<Value> {
703 let bound = with_host(|h| {
704 if object_only {
705 h.read_global(name)
706 } else {
707 h.read_name(name)
708 }
709 });
710 if let Some(v) = bound {
711 return Some(v);
712 }
713 // Globals bound lazily: numeric sentinels + builtin namespaces.
714 match name {
715 "undefined" => return Some(Value::Undef),
716 "NaN" => return Some(Value::Float(f64::NAN)),
717 "Infinity" => return Some(Value::Float(f64::INFINITY)),
718 // One object, not a fresh one per read: `globalThis === globalThis` is
719 // `true` in JS, and `globalThis.x = 1` is readable back as
720 // `globalThis.x`. Both were false while each read minted a new object.
721 // `global` is Node's alias for the same object.
722 "globalThis" | "global" => return Some(with_host(|h| h.global_object())),
723 // The WHATWG `crypto` global IS `require('crypto').webcrypto`, not the
724 // node-flavoured module: `globalThis.crypto.randomUUID` exists while
725 // `globalThis.crypto.createHash` does not.
726 "crypto" => return Some(with_host(|h| h.alloc(JsObj::Builtin("webcrypto".into())))),
727 _ => {}
728 }
729 if is_namespace(name) || is_known_builtin(name) {
730 return Some(with_host(|h| h.alloc(JsObj::Builtin(name.to_string()))));
731 }
732 None
733}
734
735fn b_getlocal(vm: &mut VM, _: u8) -> Value {
736 let name = sname(&vm.pop());
737 // A module-top-level dead zone is tracked by NAME rather than by a parked
738 // marker, so that the marker is never reachable as `globalThis.<name>`. It
739 // only applies when nothing on the scope chain SHADOWS the name — a class's
740 // own inner binding for its name does exactly that while its static
741 // initializers run.
742 if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
743 return abort(vm, host::tdz_error(&name));
744 }
745 match global_binding(&name) {
746 // The binding EXISTS but has not reached its declaration yet.
747 Some(v) if with_host(|h| h.is_tdz(&v)) => abort(vm, host::tdz_error(&name)),
748 Some(v) => v,
749 None => abort(vm, host::ref_error(&name)),
750 }
751}
752
753/// `HOIST_TDZ` — declare one `let`/`const`/`class` name as uninitialized at the
754/// top of the scope that declares it.
755fn b_hoist_tdz(vm: &mut VM, _: u8) -> Value {
756 let name = sname(&vm.pop());
757 with_host(|h| h.hoist_tdz(&name));
758 Value::Undef
759}
760
761/// The three global VALUE properties that are `{writable: false}` (19.1.1-19.1.3).
762/// Assigning to one is a silent no-op in sloppy code and a `TypeError` in strict
763/// code — and, either way, never rebinds the name.
764const READONLY_GLOBALS: [&str; 3] = ["undefined", "NaN", "Infinity"];
765
766fn readonly_global_error(name: &str) -> String {
767 host::type_error(&format!(
768 "Cannot assign to read only property '{name}' of object '#<Object>'"
769 ))
770}
771
772fn b_setlocal(vm: &mut VM, _: u8) -> Value {
773 let val = vm.pop();
774 let name = sname(&vm.pop());
775 // Sloppy assignment to a non-writable global is DISCARDED, not applied:
776 // `undefined = 1` used to rebind the name and make every later `undefined`
777 // read back as `1`.
778 if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
779 return val;
780 }
781 // Assigning to a binding still in its temporal dead zone throws too —
782 // `{ x = 1; let x }` is a ReferenceError, not an initialization.
783 if with_host(|h| match h.read_name(&name) {
784 Some(v) => h.is_tdz(&v),
785 None => h.is_tdz_global(&name),
786 }) {
787 return abort(vm, host::tdz_error(&name));
788 }
789 // An assignment to a `const` binding throws (8.5.2 SetMutableBinding on an
790 // immutable binding). This used to succeed silently.
791 if !with_host(|h| h.set_name(&name, val.clone())) {
792 return abort(vm, host::type_error("Assignment to constant variable."));
793 }
794 val
795}
796
797/// Strict-mode `x = v` (6.2.5.6 `PutValue` with an unresolvable reference):
798/// where sloppy code silently creates a global, strict code throws
799/// `ReferenceError: x is not defined`.
800///
801/// A separate opcode rather than a runtime flag: strictness is a static property
802/// of the code, so the compiler already knows which of the two an assignment is
803/// and sloppy code — everything in a CommonJS module without the directive —
804/// keeps the exact instruction it had.
805fn b_setlocal_strict(vm: &mut VM, _: u8) -> Value {
806 let val = vm.pop();
807 let name = sname(&vm.pop());
808 if !binding_exists(&name) {
809 return abort(vm, host::ref_error(&name));
810 }
811 if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
812 return abort(vm, readonly_global_error(&name));
813 }
814 if !with_host(|h| h.set_name(&name, val.clone())) {
815 return abort(vm, host::type_error("Assignment to constant variable."));
816 }
817 val
818}
819
820/// Whether `name` resolves to anything — a scope binding, a global, or a lazily
821/// materialised builtin namespace. `global_binding` answers the same question
822/// but ALLOCATES the namespace object to do it, which an assignment then throws
823/// away.
824fn binding_exists(name: &str) -> bool {
825 if with_host(|h| h.has_name(name)) {
826 return true;
827 }
828 matches!(
829 name,
830 "undefined" | "NaN" | "Infinity" | "globalThis" | "global"
831 ) || is_namespace(name)
832 || is_known_builtin(name)
833}
834
835fn b_declare(vm: &mut VM, _: u8) -> Value {
836 let val = vm.pop();
837 let name = sname(&vm.pop());
838 with_host(|h| h.declare_name(&name, val.clone()));
839 val
840}
841
842/// `const x = …`: like `DECLARE`, but the binding is immutable, so a later
843/// assignment to the name throws instead of overwriting it.
844fn b_declare_const(vm: &mut VM, _: u8) -> Value {
845 let val = vm.pop();
846 let name = sname(&vm.pop());
847 with_host(|h| h.declare_const_name(&name, val.clone()));
848 val
849}
850
851/// `var x = …` / a hoisted `function f(){}`: bind at function scope, skipping any
852/// open block scopes, so the name outlives the block it was written in.
853/// `var` hoisting: create the binding as `undefined` unless it already exists.
854fn b_hoist_var(vm: &mut VM, _: u8) -> Value {
855 let name = sname(&vm.pop());
856 with_host(|h| h.hoist_var_name(&name));
857 Value::Undef
858}
859
860fn b_declare_var(vm: &mut VM, _: u8) -> Value {
861 let val = vm.pop();
862 let name = sname(&vm.pop());
863 with_host(|h| h.declare_var_name(&name, val.clone()));
864 val
865}
866
867fn b_push_scope(_: &mut VM, _: u8) -> Value {
868 with_host(|h| h.push_scope());
869 Value::Undef
870}
871
872fn b_pop_scope(_: &mut VM, _: u8) -> Value {
873 with_host(|h| h.pop_scope());
874 Value::Undef
875}
876
877fn b_copy_scope(_: &mut VM, _: u8) -> Value {
878 with_host(|h| h.copy_scope());
879 Value::Undef
880}
881
882fn b_delname(vm: &mut VM, _: u8) -> Value {
883 let name = sval(&vm.pop());
884 with_host(|h| h.del_name(&name));
885 Value::Bool(true)
886}
887
888fn b_this(vm: &mut VM, _: u8) -> Value {
889 if with_host(|h| h.this_state()) == host::ThisState::Pending {
890 return abort(vm, host::this_before_super_error());
891 }
892 with_host(|h| h.current_this().unwrap_or(Value::Undef))
893}
894
895fn b_load_null(_vm: &mut VM, _: u8) -> Value {
896 with_host(|h| h.null())
897}
898
899// ── attribute / item handlers ─────────────────────────────────────────────────
900
901fn b_getattr(vm: &mut VM, _: u8) -> Value {
902 let name = sval(&vm.pop());
903 let recv = vm.pop();
904 match get_property(&recv, &name) {
905 Ok(v) => v,
906 Err(e) => abort(vm, e),
907 }
908}
909
910/// Read `recv.name` (also the computed-key path for string keys). Walks own
911/// properties, accessors, and the prototype chain (class methods / getters).
912/// Read one small piece out of `recv`'s heap cell under a short borrow.
913///
914/// The closure must not call back into the host (`with_host` is a `RefCell`
915/// borrow and re-entering panics) — which is exactly why it hands back only the
916/// value needed: the caller re-enters freely afterwards. This replaces the old
917/// `h.get(recv).cloned()` habit, which deep-copied a whole `Vec`/`IndexMap`/
918/// `String` just to look at it.
919fn peek<R>(recv: &Value, f: impl FnOnce(&JsObj) -> Option<R>) -> Option<R> {
920 with_host(|h| h.get(recv).and_then(f))
921}
922
923/// The nearest `[[Prototype]]` link of `recv` that is a Proxy, when the chain
924/// reaches it without a closer link already owning `name`.
925///
926/// A proxy prototype answers only from the position it occupies in the chain: a
927/// nearer prototype that owns the key (as a data property or an accessor) still
928/// wins, exactly as `OrdinaryGet` walks one link at a time.
929pub(crate) fn proxy_proto_link(recv: &Value, name: &str) -> Option<Value> {
930 with_host(|h| {
931 let mut cur = h.proto_of(recv);
932 for _ in 0..100 {
933 let p = cur?;
934 match h.get(&p) {
935 Some(JsObj::Proxy { .. }) => return Some(p),
936 Some(JsObj::Object(props)) if props.contains_key(name) => return None,
937 _ => {}
938 }
939 if h.own_accessor(&p, name).is_some() {
940 return None;
941 }
942 cur = h.proto_of(&p);
943 }
944 None
945 })
946}
947
948/// The CommonJS wrapper's parameters. They are function locals in Node, not
949/// global-object properties, so `globalThis.require` is `undefined` and
950/// `Object.getOwnPropertyDescriptor(globalThis, 'module')` reports no property —
951/// even though the bare `require` and `module` both work.
952const CJS_WRAPPER_LOCALS: &[&str] = &[
953 "require",
954 "module",
955 "exports",
956 "__filename",
957 "__dirname",
958 "__cjs_require",
959 "__cjs_resolve",
960];
961
962/// The globals node exposes as ENUMERABLE own properties of the global object —
963/// the timer family and the WHATWG additions, measured on v26.8.1. Everything
964/// else (`Math`, `parseInt`, the constructors) is non-enumerable.
965const ENUMERABLE_GLOBALS: &[&str] = &[
966 "global",
967 "clearImmediate",
968 "setImmediate",
969 "clearInterval",
970 "clearTimeout",
971 "setInterval",
972 "setTimeout",
973 "queueMicrotask",
974 "structuredClone",
975 "atob",
976 "btoa",
977 "performance",
978 "fetch",
979 "crypto",
980 "navigator",
981 "sessionStorage",
982];
983
984pub fn get_property(recv: &Value, name: &str) -> Result<Value, String> {
985 // A `#`-prefixed key is a PRIVATE name. `[[PrivateGet]]` (7.3.31) throws
986 // when the receiver carries no such private element — it does NOT read back
987 // as `undefined`, which is what `C.prototype.method.call({})` used to do.
988 if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
989 return Err(private_brand_message(name, false));
990 }
991 get_property_recv(recv, name, recv)
992}
993
994/// The `TypeError` a failed private brand check raises. Node words it two ways:
995/// a private METHOD or accessor names the class the receiver should have been an
996/// instance of, while a private FIELD names the member.
997pub fn private_brand_message(name: &str, writing: bool) -> String {
998 if with_host(|h| h.is_private_method(name)) {
999 if let Some(class) = with_host(|h| h.current_home_class_name()) {
1000 return host::type_error(&format!("Receiver must be an instance of class {class}"));
1001 }
1002 }
1003 let verb = if writing { "write" } else { "read" };
1004 let prep = if writing { "to" } else { "from" };
1005 host::type_error(&format!(
1006 "Cannot {verb} private member {name} {prep} an object whose class did not declare it"
1007 ))
1008}
1009
1010/// `[[Get]](name, receiver)` — 10.1.8. `receiver` is the object the read STARTED
1011/// from and is what a getter sees as `this`; it differs from `recv` only when the
1012/// read was forwarded down a prototype chain, which is why `Reflect.get(t, k, r)`
1013/// and a Proxy `get` trap's third argument both need it. Every ordinary read
1014/// passes `recv` itself.
1015/// Re-format an error's `.stack` header on its first read, the way V8 does.
1016///
1017/// The constructor could only stamp the name it was called with, so a subclass
1018/// that sets `this.name` after `super()` — or any `e.name = …` / `e.message = …`
1019/// before the first read — left a stale header. Node re-reads both properties at
1020/// format time, including one inherited from the prototype (`E.prototype.name`).
1021///
1022/// It is formatted ONCE: node caches the string, so renaming AFTER a read does
1023/// not change what later reads return. `@@stackRaw` is the not-yet-formatted
1024/// marker and is dropped here; an explicit `e.stack = …` drops it too, so an
1025/// assignment is never clobbered by a later read.
1026/// The key of node's DEFAULT `Error.prepareStackTrace`. Recognised by name so
1027/// the ordinary stack path can skip the hook round-trip when nothing custom is
1028/// installed.
1029pub const DEFAULT_PREPARE: &str = "ErrorPrepareStackTrace";
1030
1031pub fn materialize_stack(recv: &Value) {
1032 let Some(frames) = with_host(|h| match h.get(recv) {
1033 Some(JsObj::Object(p)) => p.get("@@stackRaw").cloned(),
1034 _ => None,
1035 }) else {
1036 return;
1037 };
1038 // A custom `Error.prepareStackTrace` replaces the string entirely (V8's
1039 // stack-introspection hook, which every source-map library installs). It was
1040 // honoured only by `Error.captureStackTrace`, so an ordinary `err.stack`
1041 // read bypassed it and handed back the default text.
1042 let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
1043 if let Some(f) = prep.filter(|f| {
1044 // The default hook produces exactly what the fast path below produces,
1045 // so it is skipped rather than called.
1046 !matches!(
1047 with_host(|h| h.get(f).cloned()),
1048 Some(JsObj::Builtin(ref n)) if n == DEFAULT_PREPARE
1049 ) && matches!(
1050 with_host(|h| h.get(f).cloned()),
1051 Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
1052 )
1053 }) {
1054 // Clear the raw marker FIRST: the hook may read `.stack` itself, and a
1055 // second materialization would re-enter this path forever.
1056 with_host(|h| {
1057 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1058 p.shift_remove("@@stackRaw");
1059 }
1060 });
1061 let limit = with_host(|h| h.stack_trace_limit());
1062 if let Ok(sites) = crate::module::callsite_stack(limit) {
1063 if let Ok(out) = host::invoke(&f, vec![recv.clone(), sites], None) {
1064 with_host(|h| {
1065 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1066 p.insert("stack".into(), out);
1067 }
1068 });
1069 return;
1070 }
1071 }
1072 }
1073 with_host(|h| {
1074 let frames = h.str_of(&frames);
1075 let name = host::lookup_chain(h, recv, "name")
1076 .map(|v| h.str_of(&v))
1077 .unwrap_or_else(|| "Error".to_string());
1078 let message = host::lookup_chain(h, recv, "message")
1079 .map(|v| h.str_of(&v))
1080 .unwrap_or_default();
1081 let header = if message.is_empty() {
1082 name
1083 } else {
1084 format!("{name}: {message}")
1085 };
1086 let sv = h.new_str(format!("{header}{frames}"));
1087 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1088 p.insert("stack".into(), sv);
1089 p.shift_remove("@@stackRaw");
1090 }
1091 });
1092}
1093
1094pub fn get_property_recv(recv: &Value, name: &str, receiver: &Value) -> Result<Value, String> {
1095 // `[[Get]]` on a Proxy: the handler's `get` trap, or a forward to the
1096 // target. Checked before anything else so no ordinary-object shortcut can
1097 // read past the handler.
1098 if let Some(v) = crate::proxy::get(recv, name, receiver)? {
1099 return Ok(v);
1100 }
1101 if with_host(|h| h.is_nullish(recv)) {
1102 return Err(host::type_error(&format!(
1103 "Cannot read properties of {} (reading '{name}')",
1104 with_host(|h| h.str_of(recv))
1105 )));
1106 }
1107 if name == "stack" {
1108 materialize_stack(recv);
1109 }
1110 // A `DOMException`'s `name`/`message`/`code` are prototype accessors over
1111 // internal slots, so they resolve here rather than out of a property map.
1112 if let Some(v) = dom_exception_slot(recv, name) {
1113 return Ok(v);
1114 }
1115 // A read off `globalThis` for a name the object does not own falls back to
1116 // the same lazy global binding the bare identifier gets. Without it the
1117 // global object was an empty bag: `globalThis.process`, `.console`, `.Math`
1118 // and `.JSON` were all `undefined`, so `process === globalThis.process` was
1119 // `false` and any `globalThis.X` feature probe reported the feature missing.
1120 if with_host(|h| h.is_global_object(recv)) {
1121 let own = with_host(|h| match h.get(recv) {
1122 Some(JsObj::Object(p)) => p.contains_key(name),
1123 _ => false,
1124 });
1125 // The CommonJS wrapper's parameters are function locals in Node, not
1126 // global-object properties: `typeof globalThis.require` is `undefined`
1127 // there even though the bare `require` works.
1128 if !own && !CJS_WRAPPER_LOCALS.contains(&name) {
1129 if let Some(v) = global_object_binding(name) {
1130 return Ok(v);
1131 }
1132 }
1133 }
1134 // Accessor (own or inherited getter) takes precedence over the chain walk.
1135 // The getter runs with the RECEIVER as `this`, not the object that owns it.
1136 if let Some((getter, _)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
1137 return match getter {
1138 Some(g) => host::invoke(&g, Vec::new(), Some(receiver.clone())),
1139 None => Ok(Value::Undef), // set-only property reads as undefined
1140 };
1141 }
1142 // `Symbol.toStringTag` read as an ordinary property. The builtins that carry
1143 // one expose it to a plain read, not just to `Object.prototype.toString` —
1144 // `new Uint8Array(1)[Symbol.toStringTag]` is `'Uint8Array'`, and a `Buffer`
1145 // inherits `'Uint8Array'` from the typed-array prototype it now really has.
1146 // Anything the receiver's own chain provides wins (a class may define its
1147 // own getter), so this is only the fallback.
1148 if name == "@@toStringTag" && with_host(|h| host::lookup_chain(h, recv, name)).is_none() {
1149 if let Some(tag) = with_host(|h| well_known_tag(h, recv)) {
1150 return Ok(with_host(|h| h.new_str(tag)));
1151 }
1152 }
1153 // `constructor`: a user class/function sets it on the prototype chain, and
1154 // that wins; otherwise every builtin instance reports its native
1155 // constructor (so `[].constructor`, `new Map().constructor`,
1156 // `Promise.resolve(1).constructor`, `(5).constructor` match Node).
1157 if name == "constructor" {
1158 if let Some(v) = with_host(|h| {
1159 match h.get(recv) {
1160 Some(JsObj::Object(p)) => p.get("constructor").cloned(),
1161 _ => None,
1162 }
1163 .or_else(|| host::lookup_chain(h, recv, "constructor"))
1164 }) {
1165 return Ok(v);
1166 }
1167 // An intrinsic prototype the receiver's CHAIN reaches owns a
1168 // `constructor` too, and it wins over the receiver's own kind:
1169 // `Object.create(Map.prototype).constructor` is `Map`, not `Object`.
1170 // Deciding from the kind alone also mis-named the receiver in every
1171 // message that renders one — the brand-check errors say `#<Map>`.
1172 if let Some(c) = chain_intrinsic_ctors(recv)
1173 .into_iter()
1174 .find(|c| is_builtin_ctor(c))
1175 {
1176 return Ok(with_host(|h| h.alloc(JsObj::Builtin(c.to_string()))));
1177 }
1178 if let Some(cn) = with_host(|h| default_ctor_name(h, recv)) {
1179 return Ok(with_host(|h| h.alloc(JsObj::Builtin(cn.to_string()))));
1180 }
1181 }
1182 // `__proto__` (Annex B B.2.2.1) is an accessor on `Object.prototype`, so it
1183 // answers for EVERY object that inherits from it, not only plain ones —
1184 // `[].__proto__` is `Array.prototype`. Only the plain-object arm handled it,
1185 // so an array, function or builtin instance read `undefined`. An object with
1186 // a null prototype inherits no such accessor and reads `undefined`, which is
1187 // why this is skipped there rather than answering `null`.
1188 if name == "__proto__"
1189 && !with_host(|h| h.has_null_proto(recv))
1190 && peek(recv, |o| match o {
1191 JsObj::Object(p) => Some(p.contains_key("__proto__")),
1192 _ => Some(false),
1193 }) != Some(true)
1194 {
1195 return Ok(prototype_of(recv));
1196 }
1197 // An ACCESSOR member read off the intrinsic prototype ITSELF is not a
1198 // method: it RUNS the getter with that prototype as `this`, and all but two
1199 // of `RegExp.prototype`'s then fail their brand check and throw. Every one
1200 // answered `undefined`, so both the value and the failure were invisible.
1201 // Both representations of a prototype reach here — the namespace handles
1202 // and the real objects (`Symbol.prototype`, `String.prototype`).
1203 if let Some(ctor) = intrinsic_proto_of(recv) {
1204 if is_proto_accessor(&ctor, name) {
1205 return proto_getter_call(&ctor, name, recv);
1206 }
1207 }
1208 let kind = with_host(|h| h.kind_of(recv));
1209 #[allow(unused_mut)]
1210 let mut out = match kind {
1211 Some(ObjKind::Object) => {
1212 let numeric = !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit());
1213 // A view over a DETACHED buffer reports zero extent. Its own
1214 // `length`/`byteLength`/`byteOffset` properties still hold the old
1215 // numbers — the buffer does not know its views, so it cannot rewrite
1216 // them — and reading them straight back made a detached view still
1217 // look eight bytes long.
1218 if matches!(name, "length" | "byteLength" | "byteOffset")
1219 && crate::stdlib::typedarray::view_detached(recv)
1220 {
1221 match crate::stdlib::native_tag(recv).as_deref() {
1222 Some("TypedArray") => return Ok(Value::Float(0.0)),
1223 // A DataView THROWS where a typed array answers zero — its
1224 // extent accessors are brand-checked and node reports the
1225 // getter by name.
1226 Some("DataView") => {
1227 return Err(crate::stdlib::typedarray::detached_error(
1228 "get DataView.prototype",
1229 name,
1230 false,
1231 ))
1232 }
1233 _ => {}
1234 }
1235 }
1236 // Typed-array element read (`ta[i]`): elements live in a hidden
1237 // `@@elems`, not as own numeric props, so intercept integer keys.
1238 if numeric && crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray") {
1239 if let Some(v) = crate::stdlib::typedarray::elem_get(recv, name) {
1240 return Ok(v);
1241 }
1242 }
1243 // `buf[i]`: a Buffer's bytes live in a hidden `@@bytes` array, not as
1244 // own numeric props, so integer keys read through to it.
1245 if numeric
1246 && peek(recv, |o| match o {
1247 JsObj::Object(p) => Some(p.contains_key("@@bytes")),
1248 _ => None,
1249 })
1250 .unwrap_or(false)
1251 {
1252 return Ok(crate::stdlib::buffer::byte_get(recv, name));
1253 }
1254 if let Some(v) = peek(recv, |o| match o {
1255 JsObj::Object(p) => p.get(name).cloned(),
1256 _ => None,
1257 }) {
1258 v
1259 } else if let Some(link) = proxy_proto_link(recv, name) {
1260 // A Proxy sitting in the prototype chain. `OrdinaryGet` (10.1.8.1
1261 // step 4) forwards to the parent's `[[Get]]` with the ORIGINAL
1262 // receiver, so the trap sees the child as `receiver` and `this`
1263 // inside a trap-served getter resolves to the child, not the
1264 // proxy. `lookup_chain` cannot do this: it reads property maps,
1265 // and a proxy has none.
1266 return Ok(crate::proxy::get(&link, name, recv)?.expect("link is a proxy"));
1267 } else if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1268 // A method / data property inherited from the prototype chain.
1269 v
1270 } else if crate::stdlib::native_tag(recv)
1271 .map(|tag| crate::stdlib::instance_has_method(&tag, name))
1272 .unwrap_or(false)
1273 {
1274 // A native instance method read as a property (`server.listen`) →
1275 // a bound method, dispatched via `instance_call` when invoked.
1276 bound_method(recv, name)
1277 } else if is_object_method(name) && !with_host(|h| h.has_null_proto(recv)) {
1278 // `Object.create(null)` inherits nothing, so `toString`/`valueOf`
1279 // read as `undefined` there — which is also what makes
1280 // `Object.create(null) + 1` the spec `TypeError` instead of a
1281 // silent `"[object Object]1"`.
1282 bound_method(recv, name)
1283 } else {
1284 Value::Undef
1285 }
1286 }
1287 Some(ObjKind::Class) | Some(ObjKind::Func) | Some(ObjKind::BoundFunc) => {
1288 function_property(recv, name)
1289 }
1290 // A method READ off an instance (`[].slice`, `new Map().get`) is a bound
1291 // thunk here. It is a function value, so it answers the function
1292 // properties: `[].slice.name` was `undefined` where node reports
1293 // `slice`, and `String([].slice)` fell through to
1294 // `Object.prototype.toString`.
1295 Some(ObjKind::BoundMethod) => bound_method_property(recv, name),
1296 Some(ObjKind::Symbol) => match name {
1297 "description" => {
1298 match peek(recv, |o| match o {
1299 JsObj::Symbol { desc, .. } => desc.clone(),
1300 _ => None,
1301 }) {
1302 Some(d) => with_host(|h| h.new_str(d)),
1303 None => Value::Undef,
1304 }
1305 }
1306 "toString" => bound_method(recv, name),
1307 // Anything else a symbol answers, it inherits from
1308 // `Symbol.prototype`. The arm used to stop at `undefined`, so
1309 // `Symbol('x')[Symbol.toPrimitive]` and `Symbol('x').valueOf` read
1310 // as absent even though the prototype defines both — a symbol is an
1311 // ordinary object for the purpose of a property LOOKUP, only its
1312 // methods are branded.
1313 _ => with_host(|h| {
1314 h.ensure_wrapper_protos();
1315 h.native_proto("Symbol")
1316 })
1317 .and_then(|p| with_host(|h| host::lookup_chain(h, &p, name)))
1318 .unwrap_or(Value::Undef),
1319 },
1320 Some(ObjKind::BigInt) => {
1321 if matches!(
1322 name,
1323 "toString" | "valueOf" | "toLocaleString" | "constructor"
1324 ) {
1325 bound_method(recv, name)
1326 } else {
1327 Value::Undef
1328 }
1329 }
1330 Some(ObjKind::RegExp) => {
1331 // A RegExp holds no collection, so cloning the compiled pattern here
1332 // does not scale with any input size; `regexp_property` re-enters the
1333 // host to allocate `source`/`flags`, so it cannot run under a borrow.
1334 let r = peek(recv, |o| match o {
1335 JsObj::RegExp(r) => Some(r.clone()),
1336 _ => None,
1337 });
1338 match r {
1339 Some(r) => crate::regexp::regexp_property(&r, name).unwrap_or_else(|| {
1340 // An OWN property beats the prototype method of the same
1341 // name, which is ordinary resolution order. It mattered once
1342 // the symbol-keyed methods existed: `re[Symbol.match] =
1343 // false` disowns the regexp label (7.2.8), and the method
1344 // was shadowing the assignment so the value never took.
1345 if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1346 return v;
1347 }
1348 if crate::regexp::is_regexp_method(name) {
1349 bound_method(recv, name)
1350 } else {
1351 Value::Undef
1352 }
1353 }),
1354 None => Value::Undef,
1355 }
1356 }
1357 // A WeakMap/WeakSet has NO `size` (its contents are not observable), so
1358 // the read must be `undefined` rather than a live count.
1359 Some(ObjKind::Map) => {
1360 let (len, weak) = peek(recv, |o| match o {
1361 JsObj::Map { entries, weak } => Some((entries.len(), *weak)),
1362 _ => None,
1363 })
1364 .unwrap_or((0, false));
1365 match name {
1366 "size" if !weak => Value::Float(len as f64),
1367 "@@iterator" => bound_method(recv, name),
1368 _ if is_map_method(name) => bound_method(recv, name),
1369 _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1370 }
1371 }
1372 Some(ObjKind::Set) => {
1373 let (len, weak) = peek(recv, |o| match o {
1374 JsObj::Set { entries, weak } => Some((entries.len(), *weak)),
1375 _ => None,
1376 })
1377 .unwrap_or((0, false));
1378 match name {
1379 "size" if !weak => Value::Float(len as f64),
1380 "@@iterator" => bound_method(recv, name),
1381 _ if is_set_method(name) => bound_method(recv, name),
1382 _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1383 }
1384 }
1385 Some(ObjKind::Generator) => {
1386 // A generator IS its own iterator, so it answers for the matching
1387 // symbol — `@@asyncIterator` for an async one, `@@iterator` for a
1388 // sync one. Neither was advertised, so `ag()[Symbol.asyncIterator]`
1389 // was `undefined` even though `for await` over it worked through a
1390 // different path.
1391 let want = if with_host(|h| h.is_async_gen_val(recv)) {
1392 "@@asyncIterator"
1393 } else {
1394 "@@iterator"
1395 };
1396 if name == want || is_generator_method(name) || crate::stdlib::iterator::is_helper(name)
1397 {
1398 bound_method(recv, name)
1399 } else {
1400 with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1401 }
1402 }
1403 Some(ObjKind::Promise) => {
1404 if matches!(name, "then" | "catch" | "finally") {
1405 bound_method(recv, name)
1406 } else {
1407 with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1408 }
1409 }
1410 Some(ObjKind::Iter) => {
1411 if matches!(name, "next" | "return" | "@@iterator")
1412 || crate::stdlib::iterator::is_helper(name)
1413 {
1414 bound_method(recv, name)
1415 } else {
1416 with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1417 }
1418 }
1419 Some(ObjKind::Array) => {
1420 if name == "length" {
1421 let n = peek(recv, |o| match o {
1422 JsObj::Array(items) => Some(items.len()),
1423 _ => None,
1424 })
1425 .unwrap_or(0);
1426 Value::Float(n as f64)
1427 } else if let Ok(i) = name.parse::<usize>() {
1428 peek(recv, |o| match o {
1429 JsObj::Array(items) => items.get(i).cloned(),
1430 _ => None,
1431 })
1432 // An index PAST an `arguments` object's length is an ordinary
1433 // own property in the side table, since adding one must not
1434 // move `length`. The array read alone could not see it, so the
1435 // write was invisible to every later read.
1436 .or_else(|| with_host(|h| h.fn_prop(recv, name)))
1437 .unwrap_or(Value::Undef)
1438 } else if name == "@@iterator"
1439 || is_object_method(name)
1440 // An `arguments` object is array-BACKED here but is not an
1441 // Array: node's exposes no `Array.prototype` method, which is
1442 // exactly why the idiom is `Array.prototype.slice.call(args)`.
1443 // Exposing them made `arguments.map` a function.
1444 || (is_array_method(name) && !is_arguments(recv))
1445 {
1446 bound_method(recv, name)
1447 } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1448 // Extra own props attached to an array (e.g. `RegExp.exec` result's
1449 // `.index`/`.input`/`.groups`).
1450 v
1451 } else {
1452 Value::Undef
1453 }
1454 }
1455 Some(ObjKind::Str) => {
1456 // `.length` and `s[i]` count UTF-16 code units, not code points.
1457 if name == "length" {
1458 let n = peek(recv, |o| match o {
1459 JsObj::Str(s) => Some(crate::utf16::len(s)),
1460 _ => None,
1461 })
1462 .unwrap_or(0);
1463 Value::Float(n as f64)
1464 } else if let Ok(i) = name.parse::<usize>() {
1465 match peek(recv, |o| match o {
1466 JsObj::Str(s) => crate::utf16::Units::of(s).unit_str(i),
1467 _ => None,
1468 }) {
1469 Some(c) => with_host(|h| h.new_str(c)),
1470 None => Value::Undef,
1471 }
1472 } else if name == "@@iterator" || is_string_method(name) {
1473 bound_method(recv, name)
1474 } else {
1475 Value::Undef
1476 }
1477 }
1478 Some(ObjKind::Builtin) => {
1479 let ns = peek(recv, |o| match o {
1480 JsObj::Builtin(ns) => Some(ns.clone()),
1481 _ => None,
1482 })
1483 .unwrap_or_default();
1484 let v = namespace_property(&ns, name);
1485 // `Function.prototype`'s methods READ off a builtin function. The
1486 // CALL forms (`Math.max.call(null, 1, 2)`) already dispatched, but
1487 // the read answered `undefined` — so `typeof Math.max.bind` was
1488 // `"undefined"`, and `String(Math.max)` found no `toString` to
1489 // invoke and fell back to `Object.prototype.toString`'s
1490 // `[object Function]` where node reports the native-code form.
1491 if matches!(v, Value::Undef)
1492 && is_function_method(name)
1493 && host::builtin_is_callable(&ns)
1494 {
1495 return Ok(bound_method(recv, name));
1496 }
1497 v
1498 }
1499 _ => {
1500 // Primitive numbers/booleans: method access -> bound method.
1501 if matches!(recv, Value::Float(_) | Value::Int(_)) && is_number_method(name) {
1502 bound_method(recv, name)
1503 } else {
1504 Value::Undef
1505 }
1506 }
1507 };
1508 // Every object INHERITS the `Object.prototype` methods, and each kind's
1509 // read arm above knows only its OWN. So `typeof new Map().toString`,
1510 // `typeof f.hasOwnProperty` and `typeof /a/.propertyIsEnumerable` all
1511 // answered `undefined` — for Map the CALL already worked, which is the
1512 // read and the dispatch disagreeing about the same method.
1513 //
1514 // Which prototype owns the name is decided by the same helper the `in`
1515 // operator uses, so the two cannot drift, and the result is the SHARED
1516 // intrinsic rather than a per-read thunk.
1517 // `arguments.callee` (and `.caller`) is a POISON PILL in strict code — the
1518 // accessor throws rather than answering, which is how a strict function
1519 // keeps its caller unreachable. It read back as `undefined` here, which a
1520 // feature probe reads as "not supported" rather than "forbidden".
1521 // Measured: on an ARGUMENTS object only `callee` is poisoned (`caller` is
1522 // simply absent and reads `undefined`); on a strict FUNCTION both `caller`
1523 // and `arguments` are.
1524 if name == "callee" && is_arguments(recv) && with_host(|h| h.current_strict()) {
1525 return Err(host::type_error(POISON_PILL));
1526 }
1527 if matches!(name, "caller" | "arguments")
1528 && matches!(
1529 with_host(|h| h.kind_of(recv)),
1530 Some(ObjKind::Func) | Some(ObjKind::Class)
1531 )
1532 {
1533 return poison_pill_read(recv);
1534 }
1535 // `arguments.callee` in SLOPPY code is the running function — the
1536 // pre-`class` self-reference idiom. It read back `undefined`.
1537 if name == "callee" && is_arguments(recv) {
1538 if let Some(f) = with_host(|h| h.fn_prop(recv, "@@callee")) {
1539 return Ok(f);
1540 }
1541 }
1542 // A method SYNTHESIZED from the receiver's kind is only reachable while the
1543 // receiver's intrinsic prototype is still on its chain. `Object
1544 // .setPrototypeOf(a, {})` must make `a.join` `undefined`; the kind arm
1545 // above answers from the kind alone and cannot know the link changed. Only
1546 // a synthesized value is dropped — the two shapes a method read produces —
1547 // and only when the receiver does not own the name itself.
1548 if matches!(
1549 with_host(|h| h.get(&out).cloned()),
1550 Some(JsObj::BoundMethod { .. })
1551 ) || matches!(
1552 with_host(|h| h.get(&out).cloned()),
1553 Some(JsObj::Builtin(ns)) if ns.starts_with("@proto:")
1554 ) {
1555 // The kind arms synthesize their OWN kind's methods, so that is the
1556 // prototype whose reachability decides. Clearing the value here lets
1557 // the `inherited_method_owner` fallback below re-supply the
1558 // `Object.prototype` form where one exists — which is why
1559 // `a.toString` stays a function after the link is replaced while
1560 // `a.join` does not.
1561 if !own_intrinsic_reachable(recv) && !has_own_for_shadow(recv, name) {
1562 out = Value::Undef;
1563 }
1564 }
1565 // A key the receiver does not OWN is looked up on its prototype chain. The
1566 // exotic arms above answer from their own storage and stop, so an array
1567 // given a prototype inherited nothing through a read: with
1568 // `Object.setPrototypeOf(a, {1: 'q'})`, `a[1]` was `undefined` at an elided
1569 // index and at one past the end, while `1 in a` already answered true —
1570 // the two views of the same question disagreeing. An accessor was found
1571 // (`lookup_accessor` walks), so only DATA properties went missing.
1572 //
1573 // A plain object's arm already consults the chain, and an array with no
1574 // explicit prototype has no links to walk, so this changes neither.
1575 if !name.starts_with('#') && !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
1576 if matches!(out, Value::Undef) {
1577 if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1578 return Ok(v);
1579 }
1580 }
1581 // Then a monkey-patched intrinsic prototype member, which shadows the
1582 // synthesized one: after `Array.prototype.join = f`, `[1, 2].join` must
1583 // BE `f`. An explicitly-set prototype above wins over it, as the chain
1584 // order requires.
1585 if let Some(v) = inherited_builtin_static(recv, name) {
1586 return Ok(v);
1587 }
1588 }
1589 if matches!(out, Value::Undef) && !name.starts_with('#') {
1590 if let Some(owner) = inherited_method_owner(recv, name) {
1591 // An INHERITED accessor runs, it does not hand back a thunk, and
1592 // its brand check is about the receiver's internal slot rather than
1593 // its chain — `Object.create(Map.prototype).size` throws in node
1594 // even though `Map.prototype` is right there above it. This
1595 // answered `undefined`, which is the value a real Map would never
1596 // give and a plain object should never reach.
1597 if is_proto_accessor(owner, name) && !getter_in_flight(owner, name) {
1598 return proto_getter_call(owner, name, recv);
1599 }
1600 let key = format!("@proto:{owner}:{name}");
1601 if builtin_meta(&key).is_some() {
1602 return Ok(with_host(|h| h.alloc(JsObj::Builtin(key))));
1603 }
1604 // A DATA member of the prototype — `Array.prototype[Symbol
1605 // .unscopables]` is an object, not a method, so it is in neither
1606 // function table. Read it off the prototype itself rather than
1607 // answering `undefined`: an instance inherits it.
1608 let v = namespace_property(&format!("{owner}.prototype"), name);
1609 if !matches!(v, Value::Undef) {
1610 return Ok(v);
1611 }
1612 }
1613 }
1614 Ok(out)
1615}
1616
1617/// The namespace name of the `require.cache` view. A `Builtin` rather than an
1618/// object literal because the module cache is the single source of truth: a
1619/// populated copy would answer reads correctly and silently ignore a `delete`,
1620/// which is the operation the property exists for.
1621pub const REQUIRE_CACHE: &str = "__cjs_cache";
1622
1623/// The builtin constructor name for a value with no own/inherited `constructor`
1624/// property, so `x.constructor` (and thus `x.constructor.name`) matches Node for
1625/// arrays, plain objects, Map/Set, promises, iterators, functions, and boxed
1626/// primitives. `None` ⇒ leave `.constructor` as `undefined` (e.g. generators,
1627/// whose `.constructor.name` is `""` in Node — not worth modelling).
1628fn default_ctor_name(h: &host::JsHost, recv: &Value) -> Option<&'static str> {
1629 match h.get(recv) {
1630 Some(JsObj::Array(_)) => Some("Array"),
1631 Some(JsObj::Object(props)) => {
1632 // A native instance reports its own constructor, not Object — e.g.
1633 // `qs` does `buf.constructor.isBuffer(buf)`, so a Buffer's
1634 // `.constructor` must be `Buffer` (which carries `isBuffer`). Read
1635 // the `@@native` tag off the already-borrowed host (calling
1636 // `native_tag`, which re-enters `with_host`, would double-borrow).
1637 match props.get("@@native").map(|t| h.str_of(t)).as_deref() {
1638 Some("Buffer") => Some("Buffer"),
1639 Some("URL") => Some("URL"),
1640 Some("Date") => Some("Date"),
1641 Some("WeakRef") => Some("WeakRef"),
1642 Some("FinalizationRegistry") => Some("FinalizationRegistry"),
1643 Some("TextEncoder") => Some("TextEncoder"),
1644 Some("TextDecoder") => Some("TextDecoder"),
1645 Some("EventEmitter") => Some("EventEmitter"),
1646 Some("Timeout") => Some("Timeout"),
1647 Some("Immediate") => Some("Immediate"),
1648 _ => Some("Object"),
1649 }
1650 }
1651 Some(JsObj::Map { weak, .. }) => Some(if *weak { "WeakMap" } else { "Map" }),
1652 Some(JsObj::Set { weak, .. }) => Some(if *weak { "WeakSet" } else { "Set" }),
1653 Some(JsObj::Promise { .. }) => Some("Promise"),
1654 Some(JsObj::Str(_)) => Some("String"),
1655 Some(JsObj::Symbol { .. }) => Some("Symbol"),
1656 Some(JsObj::BigInt(_)) => Some("BigInt"),
1657 Some(JsObj::RegExp(_)) => Some("RegExp"),
1658 Some(JsObj::Iter { .. }) => Some("Iterator"),
1659 Some(JsObj::Func(f)) => {
1660 // A generator or async function is NOT an ordinary function: its
1661 // `[[Prototype]]` is `GeneratorFunction.prototype` (or the async
1662 // variants'), and so is its `constructor`. All three reported plain
1663 // `Function`, so `g.constructor.name` was `Function` where node
1664 // says `GeneratorFunction`.
1665 Some(match h.funcs.get(f.def_id) {
1666 Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction",
1667 Some(d) if d.is_generator => "GeneratorFunction",
1668 Some(d) if d.is_async => "AsyncFunction",
1669 _ => "Function",
1670 })
1671 }
1672 Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => Some("Function"),
1673 _ => match recv {
1674 Value::Float(_) | Value::Int(_) => Some("Number"),
1675 Value::Bool(_) => Some("Boolean"),
1676 _ => None,
1677 },
1678 }
1679}
1680
1681/// The builtin constructor *functions*, so `Ctor.name` is the constructor name.
1682/// Excludes the non-callable namespaces (`Math`, `JSON`, `console`, `Reflect`,
1683/// `process`), whose `.name` is `undefined` in Node.
1684///
1685/// Most are also globals, but not all: `Timeout`/`Immediate` are unexposed in
1686/// Node (`typeof Timeout === 'undefined'`) yet still name themselves through a
1687/// handle's `.constructor.name`, so they belong here and not in `GLOBALS`.
1688/// The builtins that expose a `Symbol.species` accessor. Each returns `this`,
1689/// so a subclass is its own species unless it overrides the getter.
1690fn has_species(name: &str) -> bool {
1691 matches!(
1692 name,
1693 "Array" | "Map" | "Set" | "WeakMap" | "WeakSet" | "Promise" | "RegExp" | "ArrayBuffer"
1694 ) || crate::stdlib::typedarray::is_ctor(name)
1695}
1696
1697fn is_builtin_ctor(name: &str) -> bool {
1698 matches!(
1699 name,
1700 "Array"
1701 | "Object"
1702 | "Number"
1703 | "String"
1704 | "Boolean"
1705 | "Symbol"
1706 | "Function"
1707 | "Map"
1708 | "Set"
1709 | "WeakMap"
1710 | "WeakSet"
1711 | "Promise"
1712 | "BigInt"
1713 | "Iterator"
1714 | "RegExp"
1715 | "Date"
1716 | "ArrayBuffer"
1717 | "DataView"
1718 | "Uint8Array"
1719 | "Int8Array"
1720 | "Uint8ClampedArray"
1721 | "Int16Array"
1722 | "Uint16Array"
1723 | "Int32Array"
1724 | "Uint32Array"
1725 | "Float32Array"
1726 | "Float64Array"
1727 | "BigInt64Array"
1728 | "BigUint64Array"
1729 | "WeakRef"
1730 | "FinalizationRegistry"
1731 | "TextEncoder"
1732 | "TextDecoder"
1733 | "IncomingMessage"
1734 | "ServerResponse"
1735 | "EventEmitter"
1736 | "Buffer"
1737 | "URL"
1738 | "URLSearchParams"
1739 | "Timeout"
1740 | "Immediate"
1741 ) || host::ERROR_NAMES.contains(&name)
1742 // The stream base classes are constructors too, and `require('stream')`
1743 // IS `Stream`, so `require('stream').name` has to answer.
1744 || crate::stdlib::stream::is_class(name)
1745}
1746
1747/// The intrinsic key of the method `<instance>.<method>` resolves to, so a bound
1748/// thunk can look its `name`/`length` up in the same table a
1749/// `<Ctor>.prototype.<method>` thunk uses. `None` when the receiver has no
1750/// builtin constructor to name (a native stdlib instance, whose methods are
1751/// node's own JS and have no specified arity).
1752fn bound_method_key(recv: &Value, method: &str) -> Option<String> {
1753 let ctor = with_host(|h| default_ctor_name(h, recv))?;
1754 Some(format!("@proto:{ctor}:{method}"))
1755}
1756
1757/// `[[Get]]` on a bound method thunk. It is a function, so `name`, `length` and
1758/// the `Function.prototype` methods all answer; `length` only when the intrinsic
1759/// table knows the method, because inventing an arity is worse than the
1760/// `undefined` a caller can test for.
1761fn bound_method_property(recv: &Value, name: &str) -> Value {
1762 let method = peek(recv, |o| match o {
1763 JsObj::BoundMethod { name, .. } => Some(name.clone()),
1764 _ => None,
1765 })
1766 .unwrap_or_default();
1767 let key = peek(recv, |o| match o {
1768 JsObj::BoundMethod { recv, .. } => Some(recv.clone()),
1769 _ => None,
1770 })
1771 .and_then(|inner| bound_method_key(&inner, &method));
1772 let meta = key.as_deref().and_then(builtin_meta);
1773 match name {
1774 "name" => {
1775 let n = meta.map(|(n, _)| n.to_string()).unwrap_or(method);
1776 with_host(|h| h.new_str(n))
1777 }
1778 "length" => match meta {
1779 Some((_, len)) => Value::Float(len as f64),
1780 None => Value::Undef,
1781 },
1782 _ if is_function_method(name) => bound_method(recv, name),
1783 _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1784 }
1785}
1786
1787fn bound_method(recv: &Value, name: &str) -> Value {
1788 // An ECMAScript intrinsic is ONE function object shared by every instance:
1789 // `[1].push === Array.prototype.push` and `[1].push === [2].push` are both
1790 // true. Reading one off an instance used to mint a fresh thunk bound to that
1791 // instance, so every such comparison answered false — and a detached method
1792 // kept working on the receiver it was read off, where node throws because it
1793 // has no `this` at all.
1794 if let Some(key) = bound_method_key(recv, name) {
1795 if builtin_meta(&key).is_some() {
1796 return with_host(|h| h.alloc(JsObj::Builtin(key)));
1797 }
1798 }
1799 with_host(|h| {
1800 h.alloc(JsObj::BoundMethod {
1801 recv: recv.clone(),
1802 name: name.to_string(),
1803 })
1804 })
1805}
1806
1807/// `Object.prototype` methods reachable on any object.
1808fn is_object_method(name: &str) -> bool {
1809 matches!(
1810 name,
1811 "hasOwnProperty"
1812 | "isPrototypeOf"
1813 | "propertyIsEnumerable"
1814 | "toString"
1815 | "toLocaleString"
1816 | "valueOf"
1817 | "constructor"
1818 | "__defineGetter__"
1819 | "__defineSetter__"
1820 | "__lookupGetter__"
1821 | "__lookupSetter__"
1822 )
1823}
1824
1825/// The `Object.prototype` methods installed as thunks on the real
1826/// `Object.prototype` object, so `Object.prototype.toString.call(x)` and a class
1827/// prototype's inherited `hasOwnProperty` both resolve through the chain.
1828pub const OBJECT_PROTO_METHODS: &[&str] = &[
1829 "hasOwnProperty",
1830 "isPrototypeOf",
1831 "propertyIsEnumerable",
1832 "toString",
1833 "toLocaleString",
1834 "valueOf",
1835 "__defineGetter__",
1836 "__defineSetter__",
1837 "__lookupGetter__",
1838 "__lookupSetter__",
1839];
1840
1841/// A typed array with elements cannot be frozen or sealed: its indices are
1842/// non-configurable by construction, so making them non-writable would violate
1843/// the invariant, and node refuses outright rather than half-applying it. An
1844/// EMPTY view and a `DataView` are both fine.
1845/// `TestIntegrityLevel` (7.3.16) — `Object.isFrozen` / `Object.isSealed`.
1846///
1847/// Over a PROXY it is a sequence of traps (`isExtensible`, `ownKeys`, then a
1848/// `getOwnPropertyDescriptor` per key), not a question for the host: the proxy
1849/// OBJECT was being inspected, so a frozen proxy answered false and the handler
1850/// never saw the query.
1851fn integrity_level(v: &Value, freeze: bool) -> Result<Value, String> {
1852 if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
1853 return Ok(Value::Bool(with_host(|h| h.is_sealed(v, freeze))));
1854 }
1855 // An EXTENSIBLE object is neither sealed nor frozen, whatever its keys say.
1856 if crate::proxy::is_extensible(v)?.unwrap_or(true) {
1857 return Ok(Value::Bool(false));
1858 }
1859 for key in crate::proxy::own_keys(v)?.unwrap_or_default() {
1860 let Some(d) = crate::proxy::get_own_descriptor(v, &key)? else {
1861 continue;
1862 };
1863 let flag = |name: &str| {
1864 with_host(|h| match h.get(&d) {
1865 Some(JsObj::Object(p)) => p.get(name).map(|x| h.truthy(x)).unwrap_or(false),
1866 _ => false,
1867 })
1868 };
1869 let is_data = with_host(
1870 |h| matches!(h.get(&d), Some(JsObj::Object(p)) if !p.contains_key("get") && !p.contains_key("set")),
1871 );
1872 if flag("configurable") || (freeze && is_data && flag("writable")) {
1873 return Ok(Value::Bool(false));
1874 }
1875 }
1876 Ok(Value::Bool(true))
1877}
1878
1879/// `SetIntegrityLevel` (7.3.15) over a PROXY, which is a sequence of TRAPS —
1880/// `preventExtensions`, then `ownKeys`, then a `getOwnPropertyDescriptor` and a
1881/// `defineProperty` per key. It ran none of them: the host sealed the proxy
1882/// OBJECT, so the handler never saw the operation and the target was untouched.
1883///
1884/// Returns false for a non-proxy, which takes the ordinary path.
1885fn seal_proxy(v: &Value, freeze: bool) -> Result<bool, String> {
1886 if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
1887 return Ok(false);
1888 }
1889 if !crate::proxy::prevent_extensions(v)? {
1890 return Err(host::type_error("Object.freeze called on non-object"));
1891 }
1892 let keys = crate::proxy::own_keys(v)?.unwrap_or_default();
1893 for key in keys {
1894 // SEALING asks for no descriptor at all — it only strips
1895 // `configurable`, which is the same for a data property and an
1896 // accessor. FREEZING has to know which it is, because only a data
1897 // property has a `writable` to strip, and that is the one extra trap
1898 // call node makes.
1899 let accessor = if freeze {
1900 let Some(cur) = crate::proxy::get_own_descriptor(v, &key)? else {
1901 continue;
1902 };
1903 with_host(
1904 |h| matches!(h.get(&cur), Some(JsObj::Object(p)) if p.contains_key("get") || p.contains_key("set")),
1905 )
1906 } else {
1907 false
1908 };
1909 let desc = with_host(|h| {
1910 let mut m: IndexMap<String, Value> = IndexMap::new();
1911 m.insert("configurable".into(), Value::Bool(false));
1912 if freeze && !accessor {
1913 m.insert("writable".into(), Value::Bool(false));
1914 }
1915 h.new_object(m)
1916 });
1917 if !crate::proxy::define_property(v, &key, &desc)? {
1918 return Err(host::type_error(&format!(
1919 "'defineProperty' on proxy: trap returned falsish for property '{key}'"
1920 )));
1921 }
1922 }
1923 Ok(true)
1924}
1925
1926fn reject_sealing_a_view(v: &Value, verb: &str) -> Result<(), String> {
1927 let has_elements = matches!(
1928 crate::stdlib::native_tag(v).as_deref(),
1929 Some("TypedArray") | Some("Buffer")
1930 ) && !crate::stdlib::typedarray::elem_values(v).is_empty();
1931 if has_elements {
1932 return Err(host::type_error(&format!(
1933 "Cannot {verb} array buffer views with elements"
1934 )));
1935 }
1936 Ok(())
1937}
1938
1939pub fn is_object_builtin_method(name: &str) -> bool {
1940 matches!(
1941 name,
1942 "hasOwnProperty"
1943 | "isPrototypeOf"
1944 | "propertyIsEnumerable"
1945 | "toString"
1946 | "toLocaleString"
1947 | "valueOf"
1948 | "__defineGetter__"
1949 | "__defineSetter__"
1950 | "__lookupGetter__"
1951 | "__lookupSetter__"
1952 )
1953}
1954
1955/// The `Symbol.toStringTag` STRING on `recv`'s chain, if any — steps 16-17 of
1956/// 20.1.3.6, the hook by which a class names its own brand.
1957///
1958/// A Proxy has no chain to probe: the step is an unconditional
1959/// `Get(O, @@toStringTag)`, so its `get` trap decides. Probing first (as an
1960/// ordinary receiver does, to keep the read off objects that carry no tag)
1961/// would always miss and brand every tagged proxy `[object Object]`.
1962///
1963/// The read runs OUTSIDE the host borrow so a getter-valued tag can be invoked.
1964fn to_string_tag(recv: &Value) -> Result<Option<String>, String> {
1965 let tagged = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
1966 || with_host(|h| {
1967 host::lookup_chain(h, recv, "@@toStringTag").is_some()
1968 || host::lookup_accessor(h, recv, "@@toStringTag").is_some()
1969 });
1970 if !tagged {
1971 return Ok(None);
1972 }
1973 let t = get_property(recv, "@@toStringTag")?;
1974 Ok(with_host(|h| h.as_str(&t)))
1975}
1976
1977/// Dispatch an `Object.prototype` builtin method on an object/instance.
1978pub fn object_builtin_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
1979 match name {
1980 // Annex B B.2.2.2-B.2.2.5. Legacy, but still present in node and still
1981 // reached by pre-`defineProperty` libraries; all four were missing, so
1982 // `o.__defineGetter__` threw "is not a function".
1983 "__defineGetter__" | "__defineSetter__" => {
1984 let getter = name == "__defineGetter__";
1985 let f = args.get(1).cloned().unwrap_or(Value::Undef);
1986 if !with_host(|h| host::is_callable(h, &f)) {
1987 return Err(host::type_error(&format!(
1988 "Object.prototype.{name}: Expecting function"
1989 )));
1990 }
1991 let key = host::to_property_key(&arg0(&args))?;
1992 let desc = with_host(|h| {
1993 let mut m: IndexMap<String, Value> = IndexMap::new();
1994 m.insert(if getter { "get" } else { "set" }.into(), f);
1995 m.insert("enumerable".into(), Value::Bool(true));
1996 m.insert("configurable".into(), Value::Bool(true));
1997 h.new_object(m)
1998 });
1999 apply_descriptor(recv, &key, &desc)?;
2000 Ok(Value::Undef)
2001 }
2002 "__lookupGetter__" | "__lookupSetter__" => {
2003 let want_get = name == "__lookupGetter__";
2004 let key = host::to_property_key(&arg0(&args))?;
2005 // Walks the prototype chain, unlike `getOwnPropertyDescriptor`.
2006 let found = with_host(|h| host::lookup_accessor(h, recv, &key));
2007 Ok(match found {
2008 Some((g, st)) => {
2009 let side = if want_get { g } else { st };
2010 side.unwrap_or(Value::Undef)
2011 }
2012 None => Value::Undef,
2013 })
2014 }
2015 "hasOwnProperty" => {
2016 let k = host::to_property_key(&arg0(&args))?;
2017 // The global object OWNS its lazily-bound builtins and every global
2018 // a script created; neither lives in its property map.
2019 if with_host(|h| h.is_global_object(recv))
2020 && !CJS_WRAPPER_LOCALS.contains(&k.as_str())
2021 && global_object_binding(&k).is_some()
2022 {
2023 return Ok(Value::Bool(true));
2024 }
2025 // A builtin namespace/prototype receiver (`Map.prototype`) reports
2026 // ownership via `has_property` (its methods resolve as thunks).
2027 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin) {
2028 return Ok(Value::Bool(has_property(recv, &k)?));
2029 }
2030 // `HasOwnProperty` (7.3.12) is `[[GetOwnProperty]]`, so on a Proxy it
2031 // is the `getOwnPropertyDescriptor` trap — NOT the `has` trap and not
2032 // the target's property map.
2033 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
2034 let d = crate::proxy::get_own_descriptor(recv, &k)?.unwrap_or(Value::Undef);
2035 return Ok(Value::Bool(!matches!(d, Value::Undef)));
2036 }
2037 // A Buffer's / typed array's own keys are its element indices: the
2038 // `length`/`byteLength` slots are internal bookkeeping, and V8
2039 // reports `hasOwnProperty('length')` as false for a typed array.
2040 // Shared with the `in` operator so the two cannot drift apart.
2041 if let Some(hit) = crate::stdlib::typedarray::has_index(recv, &k) {
2042 return Ok(Value::Bool(hit));
2043 }
2044 // A function's `length`/`name`/`prototype` and a RegExp's
2045 // `lastIndex` are SYNTHESIZED own properties: they read back but
2046 // own no map entry, so this answered false where node says true.
2047 if synthesized_own_descriptor(recv, &k).is_some() {
2048 return Ok(Value::Bool(true));
2049 }
2050 if uses_side_table(recv) {
2051 return Ok(Value::Bool(with_host(|h| h.fn_prop(recv, &k).is_some())));
2052 }
2053 let has = with_host(|h| match h.get(recv) {
2054 Some(JsObj::Object(p)) => p.contains_key(&k) || h.own_accessor(recv, &k).is_some(),
2055 Some(JsObj::Array(items)) => {
2056 k == "length"
2057 || k.parse::<usize>()
2058 .map(|i| i < items.len() && !h.is_hole(recv, i))
2059 .unwrap_or(false)
2060 }
2061 _ => false,
2062 });
2063 Ok(Value::Bool(has))
2064 }
2065 "isPrototypeOf" => {
2066 let target = arg0(&args);
2067 // The ARGUMENT is what gets walked, so a proxy there needs its
2068 // `getPrototypeOf` trap for the FIRST hop: `proto_of` reads a link a
2069 // proxy does not hold, which reported `false` for every proxy. From
2070 // the second hop on the chain is ordinary objects again, walked by
2071 // the recorded link exactly as before.
2072 // Each further hop is `[[GetPrototypeOf]]` (`prototype_of`), the same
2073 // answer `Object.getPrototypeOf` gives: the recorded link alone
2074 // misses a class's parent constructor and every default prototype,
2075 // so `A.isPrototypeOf(B)` for `class B extends A` and
2076 // `Error.isPrototypeOf(RangeError)` read false.
2077 let non_null = |p: Value| Some(p).filter(|p| !with_host(|h| h.is_null(p)));
2078 let mut cur = match crate::proxy::get_prototype_of(&target)? {
2079 Some(p) => non_null(p),
2080 None if with_host(|h| crate::host::is_primitive(h, &target)) => None,
2081 None => non_null(prototype_of(&target)),
2082 };
2083 // Bounded: a chain longer than any real one is a cycle.
2084 for _ in 0..100_000 {
2085 let Some(p) = cur else { break };
2086 if with_host(|h| h.strict_eq(&p, recv)) {
2087 return Ok(Value::Bool(true));
2088 }
2089 cur = non_null(prototype_of(&p));
2090 }
2091 Ok(Value::Bool(false))
2092 }
2093 "propertyIsEnumerable" => {
2094 let k = with_host(|h| h.str_of(&arg0(&args)));
2095 // Own *and* enumerable — a non-enumerable own slot reads false. On a
2096 // Proxy that question is `[[GetOwnProperty]]`, i.e. the descriptor
2097 // trap, since there is no property map to enumerate.
2098 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
2099 let has = crate::proxy::own_enum_string_keys(recv)?.contains(&k);
2100 return Ok(Value::Bool(has));
2101 }
2102 let has = with_host(|h| h.own_enum_key_names(recv).contains(&k));
2103 Ok(Value::Bool(has))
2104 }
2105 "toString" => {
2106 // An instance with a custom `toString` up the chain is handled by
2107 // call_method before reaching here; this is the default — and the
2108 // default consults `Symbol.toStringTag` (20.1.3.6 steps 16-17).
2109 // Only the EXPLICIT `Object.prototype.toString.call(o)` did, so a
2110 // tagged object branded itself `[object T]` when asked one way and
2111 // `[object Object]` when converted the other (`String(o)`, `${o}`,
2112 // `o + ''`, `o.toString()`), which is the path ordinary code takes.
2113 if let Some(t) = to_string_tag(recv)? {
2114 return Ok(with_host(|h| h.new_str(format!("[object {t}]"))));
2115 }
2116 Ok(with_host(|h| {
2117 let s = h.str_of(recv);
2118 h.new_str(s)
2119 }))
2120 }
2121 // `Object.prototype.toLocaleString` (20.1.3.5) is defined as
2122 // `Invoke(this, "toString")` — no locale behavior of its own. It was
2123 // installed as a thunk on `Object.prototype` but had no dispatch arm, so
2124 // calling it threw `is not a function` on every plain object.
2125 "toLocaleString" => {
2126 let v = host::call_method(recv, "toString", Vec::new())?;
2127 Ok(v)
2128 }
2129 "valueOf" => Ok(recv.clone()),
2130 _ => Err(host::type_error(&format!("{name} is not a function"))),
2131 }
2132}
2133
2134/// `Function.prototype` methods (`call`/`apply`/`bind`) plus `Symbol.prototype`/
2135/// generator handling done elsewhere. Returns `Ok(None)` if `name` is not one of
2136/// these (so the caller can try statics).
2137pub fn function_builtin_method(
2138 recv: &Value,
2139 name: &str,
2140 args: &[Value],
2141) -> Result<Option<Value>, String> {
2142 match name {
2143 "call" => {
2144 let this = args.first().cloned();
2145 let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2146 Ok(Some(host::invoke(recv, rest, this)?))
2147 }
2148 "apply" => {
2149 let this = args.first().cloned();
2150 let arr = args.get(1).cloned().unwrap_or(Value::Undef);
2151 // `Function.prototype.apply` takes an ARRAY-LIKE, not an iterable
2152 // (10.2.4.3 → CreateListFromArrayLike): `f.apply(null, arguments)`
2153 // and `f.apply(null, {length: 2, 0: 'x', 1: 'y'})` are the shapes
2154 // this is written for, and both produced an empty list. A nullish
2155 // second argument means no arguments at all.
2156 let call_args = if matches!(arr, Value::Undef) || with_host(|h| h.is_null(&arr)) {
2157 Vec::new()
2158 } else {
2159 create_list_from_array_like(&arr)?
2160 };
2161 Ok(Some(host::invoke(recv, call_args, this)?))
2162 }
2163 "bind" => {
2164 let this = args.first().cloned().unwrap_or(Value::Undef);
2165 let pre = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2166 Ok(Some(with_host(|h| {
2167 h.alloc(JsObj::BoundFunc {
2168 target: recv.clone(),
2169 this,
2170 args: pre,
2171 })
2172 })))
2173 }
2174 "toString" => Ok(Some(with_host(|h| {
2175 let s = h.str_of(recv);
2176 h.new_str(s)
2177 }))),
2178 _ => Ok(None),
2179 }
2180}
2181
2182fn is_function_method(name: &str) -> bool {
2183 matches!(name, "call" | "apply" | "bind" | "toString")
2184}
2185fn is_map_method(name: &str) -> bool {
2186 matches!(
2187 name,
2188 "get" | "set" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
2189 )
2190}
2191fn is_set_method(name: &str) -> bool {
2192 matches!(
2193 name,
2194 "add"
2195 | "has"
2196 | "delete"
2197 | "clear"
2198 | "forEach"
2199 | "keys"
2200 | "values"
2201 | "entries"
2202 | "union"
2203 | "intersection"
2204 | "difference"
2205 | "symmetricDifference"
2206 | "isSubsetOf"
2207 | "isSupersetOf"
2208 | "isDisjointFrom"
2209 )
2210}
2211fn is_generator_method(name: &str) -> bool {
2212 matches!(name, "next" | "return" | "throw")
2213}
2214
2215/// A property read on a function/class value: own fn-props (statics, name,
2216/// prototype, length) plus inherited statics and `call`/`apply`/`bind`.
2217fn function_property(recv: &Value, name: &str) -> Value {
2218 // A class static, inherited down the constructor chain.
2219 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
2220 if let Some(v) = with_host(|h| h.class_static(recv, name)) {
2221 return v;
2222 }
2223 // A class's own `name` and `length` are its own, not the builtin
2224 // ancestor's: `class A extends Array {}` has `A.name === "A"` and
2225 // `A.length === 0`, but both were read off `Array`. Only a class that
2226 // WOULD fall through to an ancestor takes this path; a plain class keeps
2227 // the ordinary computation below.
2228 if matches!(name, "name" | "length")
2229 && with_host(|h| h.class_static(recv, name)).is_none()
2230 && with_host(|h| h.class_builtin_ancestor(recv))
2231 .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2232 {
2233 if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2234 return v;
2235 }
2236 if name == "name" {
2237 let n = with_host(|h| h.callable_name(recv));
2238 return with_host(|h| h.new_str(n));
2239 }
2240 // The class's own constructor decides its arity; with no explicit
2241 // one the implicit `constructor(...args)` has length 0.
2242 let ctor = with_host(|h| match h.get(recv) {
2243 Some(JsObj::Class(c)) => c.ctor.clone(),
2244 _ => None,
2245 });
2246 return match ctor {
2247 Some(c) => get_property(&c, "length").unwrap_or(Value::Float(0.0)),
2248 None => Value::Float(0.0),
2249 };
2250 }
2251 // `Symbol.species` is an accessor returning `this`, so a subclass that
2252 // does not override it IS its own species. Reading it off the builtin
2253 // ancestor below would answer with the ancestor — `A[Symbol.species]`
2254 // came back as `Array`, which sent every derived result to a plain
2255 // array.
2256 if name == "@@species"
2257 && with_host(|h| h.class_static(recv, "@@species")).is_none()
2258 && with_host(|h| h.class_builtin_ancestor(recv))
2259 .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2260 {
2261 return recv.clone();
2262 }
2263 // The chain may bottom out in a BUILTIN constructor (`class D extends
2264 // Array {}`), whose statics `class_static` cannot see — it only walks
2265 // `ClassVal.parent` links between user classes. Finish the lookup with an
2266 // ordinary read on that ancestor so `D.from` inherits `Array.from`.
2267 if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
2268 if let Ok(v) = get_property(&anc, name) {
2269 if !matches!(v, Value::Undef) {
2270 return v;
2271 }
2272 }
2273 }
2274 } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2275 return v;
2276 }
2277 // A method inherited via the function's [[Prototype]] chain (set with
2278 // `Object.setPrototypeOf(fn, proto)` — the `router` package makes each router
2279 // *function* inherit `route`/`use`/`get`/… from `Router.prototype` this way).
2280 if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
2281 return v;
2282 }
2283 match name {
2284 "name" => with_host(|h| {
2285 let n = h.callable_name(recv);
2286 h.new_str(n)
2287 }),
2288 "length" => Value::Float(with_host(|h| h.func_arity(recv)) as f64),
2289 "prototype" => ensure_fn_prototype(recv),
2290 _ if is_function_method(name) => bound_method(recv, name),
2291 _ => Value::Undef,
2292 }
2293}
2294
2295/// The `.prototype` of a function value, auto-created on first access (as Node
2296/// does for every non-arrow function) with `.constructor` linking back. Arrow
2297/// functions have no `prototype`.
2298fn ensure_fn_prototype(recv: &Value) -> Value {
2299 if let Some(p) = with_host(|h| h.fn_prop(recv, "prototype")) {
2300 return p;
2301 }
2302 // Only a constructor gets one: an arrow, a method definition and an async
2303 // function are not constructors, and a class sets its own (10.2.5).
2304 if with_host(|h| h.kind_of(recv)) != Some(ObjKind::Func) {
2305 return Value::Undef;
2306 }
2307 if !with_host(|h| h.owns_prototype(recv)) {
2308 return Value::Undef;
2309 }
2310 with_host(|h| {
2311 let proto = h.new_object(IndexMap::new());
2312 if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
2313 p.insert("constructor".to_string(), recv.clone());
2314 }
2315 h.hide_prop(&proto, "constructor");
2316 h.set_fn_prop(recv, "prototype", proto.clone());
2317 proto
2318 })
2319}
2320
2321/// The numeric constants a core namespace owns, in the order node reports them
2322/// under `getOwnPropertyNames`. ONE table rather than a value match plus a name
2323/// list: the enumeration and the read have to agree, and they did not — every
2324/// one of these read correctly while `Object.getOwnPropertyNames(Math)` omitted
2325/// all eight of Math's, so a member that plainly exists was invisible to any
2326/// reflective copy of the namespace.
2327///
2328/// Each is `{ writable: false, enumerable: false, configurable: false }`, which
2329/// is what separates them from the methods alongside them.
2330pub fn namespace_constants(ns: &str) -> &'static [(&'static str, f64)] {
2331 const MATH: &[(&str, f64)] = &[
2332 ("E", std::f64::consts::E),
2333 ("LN10", std::f64::consts::LN_10),
2334 ("LN2", std::f64::consts::LN_2),
2335 ("LOG10E", std::f64::consts::LOG10_E),
2336 ("LOG2E", std::f64::consts::LOG2_E),
2337 ("PI", std::f64::consts::PI),
2338 ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2),
2339 ("SQRT2", std::f64::consts::SQRT_2),
2340 ];
2341 const NUMBER: &[(&str, f64)] = &[
2342 ("MAX_VALUE", f64::MAX),
2343 // The smallest positive value a Number can hold, which is the
2344 // smallest SUBNORMAL double (`5e-324`), not Rust's
2345 // `f64::MIN_POSITIVE` — that is the smallest *normal* double,
2346 // `2.2250738585072014e-308`, ~256 binary orders of magnitude too
2347 // large.
2348 // The literal, not `f64::from_bits(1)`: that is only const-callable from
2349 // Rust 1.83 and this crate's MSRV is 1.80. It parses to the same
2350 // bit pattern — the smallest positive subnormal.
2351 ("MIN_VALUE", 5e-324),
2352 ("NaN", f64::NAN),
2353 ("NEGATIVE_INFINITY", f64::NEG_INFINITY),
2354 ("POSITIVE_INFINITY", f64::INFINITY),
2355 ("MAX_SAFE_INTEGER", 9007199254740991.0),
2356 ("MIN_SAFE_INTEGER", -9007199254740991.0),
2357 ("EPSILON", f64::EPSILON),
2358 ];
2359 match ns {
2360 "Math" => MATH,
2361 "Number" => NUMBER,
2362 _ => &[],
2363 }
2364}
2365
2366/// The descriptor of `<ns>.<key>`, whose attributes fall into four groups —
2367/// measured on node v26.8.1:
2368///
2369/// ```text
2370/// Math.PI, Number.MAX_SAFE_INTEGER, Number.prototype w=false e=false c=false
2371/// Math.max.name, Math.max.length w=false e=false c=true
2372/// Math.floor, Array.from, Array.prototype.slice w=true e=false c=true
2373/// require('path').join w=true e=true c=true
2374/// ```
2375///
2376/// So: a constant (and a constructor's `prototype`) is frozen, a function's own
2377/// `name`/`length` is read-only but configurable, and everything else is an
2378/// ordinary method — enumerable exactly when the namespace enumerates it, which
2379/// is what separates a core module's exports from an ECMAScript namespace's.
2380fn builtin_member_descriptor(ns: &str, key: &str, value: Value) -> Value {
2381 let frozen = namespace_constants(ns).iter().any(|(k, _)| *k == key)
2382 || key == "prototype"
2383 || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key));
2384 let own_fn_meta = matches!(key, "name" | "length") && host::builtin_is_callable(ns);
2385 // A key a SCRIPT assigned is an ordinary writable/enumerable/configurable
2386 // data property, whatever the namespace's built-in members look like — the
2387 // synthesized answer reported it non-enumerable, so a monkey-patched member
2388 // described itself as one of the intrinsics.
2389 let assigned = !intrinsic_proto_member(ns, key)
2390 && !crate::stdlib::namespace_keys(ns).iter().any(|k| k == key)
2391 && with_host(|h| h.builtin_static(ns, key).is_some());
2392 let enumerable = assigned
2393 || (!frozen && !own_fn_meta && crate::stdlib::namespace_keys(ns).iter().any(|k| k == key));
2394 with_host(|h| {
2395 let mut m: IndexMap<String, Value> = IndexMap::new();
2396 m.insert("value".into(), value);
2397 m.insert(
2398 "writable".into(),
2399 Value::Bool(assigned || (!frozen && !own_fn_meta)),
2400 );
2401 m.insert("enumerable".into(), Value::Bool(enumerable));
2402 m.insert("configurable".into(), Value::Bool(assigned || !frozen));
2403 h.new_object(m)
2404 })
2405}
2406
2407/// Whether `<ns>.<key>` may be deleted — the `configurable` half of
2408/// [`builtin_member_descriptor`], split out so `delete` can ask without
2409/// building a descriptor object.
2410/// Whether `key` is one of the members the intrinsic prototype namespace `ns`
2411/// really defines — as opposed to a name a script added. An assignment over one
2412/// of these is a `[[Set]]` and leaves its attributes alone.
2413fn intrinsic_proto_member(ns: &str, key: &str) -> bool {
2414 intrinsic_proto_members(ns).is_some_and(|members| {
2415 members
2416 .iter()
2417 .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
2418 })
2419}
2420
2421fn builtin_member_configurable(ns: &str, key: &str) -> bool {
2422 !(namespace_constants(ns).iter().any(|(k, _)| *k == key)
2423 || key == "prototype"
2424 || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key)))
2425}
2426
2427/// The value of `<ns>.<name>` when it is one of those constants.
2428fn namespace_constant(ns: &str, name: &str) -> Option<f64> {
2429 namespace_constants(ns)
2430 .iter()
2431 .find(|(k, _)| *k == name)
2432 .map(|(_, v)| *v)
2433}
2434
2435/// Whether `ctor` is a WebIDL interface, whose prototype members are plain
2436/// assigned — and so ENUMERABLE — rather than the non-enumerable ones an
2437/// ECMAScript builtin defines. The generated member table records the same
2438/// distinction with its `+` prefix.
2439fn is_webidl_proto(ctor: &str) -> bool {
2440 intrinsic_proto_members(&format!("{ctor}.prototype"))
2441 .is_some_and(|ms| ms.iter().any(|m| m.starts_with('+')))
2442}
2443
2444/// The intrinsic constructor a value's own kind implies — the prototype it
2445/// inherits with no explicit link.
2446pub(crate) fn own_ctor_name(h: &host::JsHost, v: &Value) -> Option<&'static str> {
2447 default_ctor_name(h, v)
2448}
2449
2450/// Whether `ctor.prototype` defines `key` as a NON-WRITABLE data property, so
2451/// an object inheriting it refuses an assignment to that name.
2452pub(crate) fn is_proto_readonly(ctor: &str, key: &str) -> bool {
2453 crate::arity::PROTO_READONLY
2454 .binary_search_by(|(k, _)| (*k).cmp(ctor))
2455 .ok()
2456 .is_some_and(|i| crate::arity::PROTO_READONLY[i].1.contains(&key))
2457}
2458
2459/// Whether `ctor.prototype` defines `key` as an ACCESSOR rather than a data
2460/// property or a method.
2461pub(crate) fn is_proto_accessor(ctor: &str, key: &str) -> bool {
2462 crate::arity::PROTO_ACCESSORS
2463 .binary_search_by(|(k, _)| (*k).cmp(ctor))
2464 .ok()
2465 .is_some_and(|i| crate::arity::PROTO_ACCESSORS[i].1.contains(&key))
2466}
2467
2468/// The constructor whose `.prototype` IS `recv`, whichever of the two
2469/// representations it uses — a `Builtin` namespace handle or a real object.
2470pub(crate) fn intrinsic_proto_of(recv: &Value) -> Option<String> {
2471 with_host(|h| match h.get(recv) {
2472 Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
2473 _ => h.intrinsic_proto_ctor(recv).map(str::to_string),
2474 })
2475}
2476
2477/// The getter function of an intrinsic prototype accessor, as a first-class
2478/// value — what `Object.getOwnPropertyDescriptor(Map.prototype, 'size').get`
2479/// hands back, and the form a library uses to borrow one.
2480fn proto_getter(ctor: &str, key: &str) -> Value {
2481 with_host(|h| h.alloc(JsObj::Builtin(format!("@protoget:{ctor}:{key}"))))
2482}
2483
2484/// Whether `recv` carries the internal slot `ctor`'s accessor demands. This is
2485/// a BRAND check, not a chain walk: `Object.create(Map.prototype).size` throws
2486/// in node even though `Map.prototype` is right there on the chain.
2487fn brand_matches(recv: &Value, ctor: &str) -> bool {
2488 if let Some(tag) = crate::stdlib::native_tag(recv) {
2489 if tag == ctor || (ctor == "TypedArray" && tag == "TypedArray") {
2490 return true;
2491 }
2492 }
2493 match ctor {
2494 "TypedArray" => crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray"),
2495 "ArrayBuffer" => with_host(
2496 |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@bytes")),
2497 ),
2498 _ => {
2499 let own = match wrapped_primitive(recv).as_ref().and_then(wrapper_ctor_of) {
2500 Some(c) => Some(c),
2501 None => with_host(|h| default_ctor_name(h, recv)),
2502 };
2503 own == Some(ctor)
2504 }
2505 }
2506}
2507
2508thread_local! {
2509 /// The `(ctor, key)` prototype accessors whose tail read is in flight.
2510 ///
2511 /// A getter's last step reads the value off the receiver, and when the
2512 /// receiver does not STORE it that read walks the chain, finds the same
2513 /// accessor and runs it again: `new TextDecoder().fatal` recursed until the
2514 /// stack overflowed and aborted the process. An accessor already in flight
2515 /// answers `undefined` for its own key rather than re-entering — the value
2516 /// a missing internal slot has, and what node reports for one.
2517 static GETTERS_IN_FLIGHT: std::cell::RefCell<Vec<(String, String)>> =
2518 const { std::cell::RefCell::new(Vec::new()) };
2519}
2520
2521/// Whether `ctor`'s `key` getter is already running further down the stack.
2522fn getter_in_flight(ctor: &str, key: &str) -> bool {
2523 GETTERS_IN_FLIGHT.with(|g| g.borrow().iter().any(|(c, k)| c == ctor && k == key))
2524}
2525
2526/// Invoke an intrinsic prototype's getter against `recv` — the body behind the
2527/// `@protoget:` thunks.
2528///
2529/// Reading one OFF THE PROTOTYPE (`Map.prototype.size`) is the case that was
2530/// wrong: it answered `undefined` where node runs the getter, fails the brand
2531/// check and throws. `RegExp.prototype` is the documented exception — 22.2.6.10
2532/// and .13 return `"(?:)"` and `""` for it specifically, so the one receiver
2533/// that would otherwise throw for every flag reads two of them back.
2534pub(crate) fn proto_getter_call(ctor: &str, key: &str, recv: &Value) -> Result<Value, String> {
2535 let is_the_prototype = with_host(
2536 |h| matches!(h.get(recv), Some(JsObj::Builtin(ns)) if *ns == format!("{ctor}.prototype")),
2537 );
2538 if is_the_prototype && ctor == "RegExp" {
2539 // 22.2.6.x each carry the same step: when `this` IS `%RegExp.prototype%`
2540 // the getter returns rather than throwing. `source` and `flags` have
2541 // their own values there; every flag getter answers `undefined`.
2542 return Ok(match key {
2543 "source" => with_host(|h| h.new_str("(?:)".to_string())),
2544 "flags" => with_host(|h| h.new_str(String::new())),
2545 _ => Value::Undef,
2546 });
2547 }
2548 // `RegExp.prototype.flags` (22.2.6.5) is the one that is GENERIC: it reads
2549 // the individual flag properties off whatever object it is handed and
2550 // concatenates their letters, so a plain object answers `""` rather than
2551 // throwing, and one carrying `global`/`ignoreCase` answers `"gi"`.
2552 if ctor == "RegExp" && key == "flags" && !brand_matches(recv, ctor) {
2553 if !with_host(|h| is_object_like(h, recv)) {
2554 return Err(regexp_brand_error(key, recv));
2555 }
2556 let mut out = String::new();
2557 for (prop, letter) in REGEXP_FLAG_LETTERS {
2558 let v = get_property(recv, prop)?;
2559 if with_host(|h| h.truthy(&v)) {
2560 out.push(*letter);
2561 }
2562 }
2563 return Ok(with_host(|h| h.new_str(out)));
2564 }
2565 // `Function.prototype.arguments`/`caller` are POISON PILLS (10.2.4.1): both
2566 // the getter and the setter throw for every receiver, which is how a strict
2567 // function keeps its caller unreachable. They are not brand checks and do
2568 // not name the receiver.
2569 if ctor == "Function" && matches!(key, "arguments" | "caller") {
2570 return poison_pill_read(recv);
2571 }
2572 if !brand_matches(recv, ctor) {
2573 return Err(match ctor {
2574 "RegExp" => regexp_brand_error(key, recv),
2575 "Symbol" => {
2576 host::type_error("Symbol.prototype.description requires that 'this' be a Symbol")
2577 }
2578 _ => host::type_error(&format!(
2579 "Method get {ctor}.prototype.{key} called on incompatible receiver {}",
2580 brand_receiver_string(recv)
2581 )),
2582 });
2583 }
2584 // A native instance keeps an accessor's value in the hidden `@@<key>` slot,
2585 // so that the public name can be a getter on the prototype rather than an
2586 // own enumerable property. Read it straight: the chain walk below would
2587 // find this same accessor and run it again.
2588 if let Some(v) = with_host(|h| match h.get(recv) {
2589 Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
2590 _ => None,
2591 }) {
2592 return Ok(v);
2593 }
2594 GETTERS_IN_FLIGHT.with(|g| g.borrow_mut().push((ctor.to_string(), key.to_string())));
2595 let out = get_property(recv, key);
2596 GETTERS_IN_FLIGHT.with(|g| {
2597 g.borrow_mut().pop();
2598 });
2599 out
2600}
2601
2602/// `Function.prototype.arguments`/`caller` read against `recv`.
2603///
2604/// The pill is conditional and the condition is the RECEIVER, not the reading
2605/// code: a sloppy non-arrow function answers `null` (node stopped populating
2606/// these long ago but kept them readable), and everything else — an arrow, a
2607/// strict function, a non-function — throws. Keying it on the READER's
2608/// strictness, which is what this did, made `strictFn.arguments` answer
2609/// `undefined` from sloppy code and a sloppy function throw from strict code:
2610/// wrong in both directions.
2611pub(crate) fn poison_pill_read(recv: &Value) -> Result<Value, String> {
2612 if with_host(|h| h.fn_is_sloppy(recv)) {
2613 return Ok(with_host(|h| h.null()));
2614 }
2615 Err(host::type_error(POISON_PILL))
2616}
2617
2618/// The message both halves of the `arguments`/`caller` poison pill throw.
2619pub(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";
2620
2621/// How a REJECTED receiver is rendered in a brand-check message.
2622///
2623/// `no_side_effects_string` answers for most of them, but two kinds differ:
2624/// an intrinsic PROTOTYPE renders `#<Map>` rather than `[object Map]`, and so
2625/// does an `ArrayBuffer`/`DataView` instance, which this host tags natively and
2626/// that function therefore brands. Node draws the line at whether the value is
2627/// one of the ES5-era classes (`Array`, `Date`, `RegExp` are `[object X]`); the
2628/// two cases here are the ones that fall on the other side of it.
2629fn brand_receiver_string(recv: &Value) -> String {
2630 if let Some(ctor) = intrinsic_proto_of(recv) {
2631 return format!("#<{ctor}>");
2632 }
2633 match crate::stdlib::native_tag(recv).as_deref() {
2634 Some(tag @ ("ArrayBuffer" | "DataView")) => format!("#<{tag}>"),
2635 _ => no_side_effects_string(recv),
2636 }
2637}
2638
2639/// The flag properties `RegExp.prototype.flags` reads, in the order 22.2.6.5
2640/// concatenates their letters.
2641const REGEXP_FLAG_LETTERS: &[(&str, char)] = &[
2642 ("hasIndices", 'd'),
2643 ("global", 'g'),
2644 ("ignoreCase", 'i'),
2645 ("multiline", 'm'),
2646 ("dotAll", 's'),
2647 ("unicode", 'u'),
2648 ("unicodeSets", 'v'),
2649 ("sticky", 'y'),
2650];
2651
2652/// `RegExp.prototype`'s flag getters word their brand failure their own way,
2653/// and `flags` distinguishes a non-object receiver from a non-RegExp one
2654/// because 22.2.6.5 reads the individual flags off any object it is given.
2655fn regexp_brand_error(key: &str, recv: &Value) -> String {
2656 if key == "flags" && !with_host(|h| matches!(recv, Value::Obj(_)) && !h.is_null(recv)) {
2657 return host::type_error(&format!(
2658 "RegExp.prototype.flags getter called on non-object {}",
2659 no_side_effects_string(recv)
2660 ));
2661 }
2662 host::type_error(&format!(
2663 "RegExp.prototype.{key} getter called on non-RegExp object"
2664 ))
2665}
2666
2667/// A property on a builtin namespace object (`Math.PI`, `Number.MAX_SAFE_INTEGER`,
2668/// `console.log`).
2669pub fn namespace_property(ns: &str, name: &str) -> Value {
2670 // `require.cache[id]` — a LIVE view of the module cache, not a copy, so a
2671 // read sees whatever is loaded now and `delete` (see `delete_property`)
2672 // actually invalidates.
2673 if ns == REQUIRE_CACHE {
2674 return crate::module::cache_get(name).unwrap_or(Value::Undef);
2675 }
2676 // A property a SCRIPT assigned onto this namespace wins over everything
2677 // synthesized below, including a member the namespace really has. That is
2678 // what monkey-patching an intrinsic is: `Array.prototype.join = f` must make
2679 // `[1, 2].join()` call `f`, and a polyfill's `Array.prototype.at = impl` has
2680 // to read back at all. Only the two `Error` hooks consulted this table, so
2681 // every other assignment onto a builtin — the whole polyfill idiom — was
2682 // stored by `set_property` and then never read: the write appeared to
2683 // succeed, `Object.isExtensible` said true, and the value came back
2684 // `undefined`.
2685 if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2686 return v;
2687 }
2688 // The ENTRY script's `require` is this builtin rather than the per-module
2689 // closure, so its `cache` has to be handed out here too.
2690 // `require.extensions` — the legacy loader map. Deprecated but still read
2691 // (and sometimes written) by tooling that hooks module loading, and it was
2692 // absent entirely. The three keys node ships are present; installing a
2693 // custom loader through them is NOT honoured by this runtime's loader, so
2694 // the map reports what it can serve rather than pretending otherwise.
2695 // `util.promisify.custom` — the registered symbol a module attaches to a
2696 // callback function to supply its own promisified form. It was `undefined`,
2697 // so the lookup that decides whether to use one always missed.
2698 if ns == "util.promisify" && name == "custom" {
2699 return with_host(|h| h.symbol_for("nodejs.util.promisify.custom"));
2700 }
2701 // `process.memoryUsage.rss()` — node's fast path for the one figure that
2702 // does not need the whole object built.
2703 if ns == "process.memoryUsage" && name == "rss" {
2704 return with_host(|h| h.alloc(JsObj::Builtin("process.memoryUsage.rss".to_string())));
2705 }
2706 if ns == "require" && name == "extensions" {
2707 return with_host(|h| {
2708 let mut m: IndexMap<String, Value> = IndexMap::new();
2709 for ext in [".js", ".json", ".node"] {
2710 let f = h.alloc(JsObj::Builtin(format!("@@extension:{ext}")));
2711 m.insert(ext.to_string(), f);
2712 }
2713 h.new_object(m)
2714 });
2715 }
2716 // `require.resolve.paths(spec)` — the directories a lookup would search:
2717 // `null` for a core module, the `node_modules` chain otherwise.
2718 if ns == "require.resolve" && name == "paths" {
2719 return with_host(|h| h.alloc(JsObj::Builtin("require.resolve.paths".to_string())));
2720 }
2721 if ns == "require" && name == "cache" {
2722 return with_host(|h| h.alloc(JsObj::Builtin(REQUIRE_CACHE.to_string())));
2723 }
2724 // The legacy numeric codes `DOMException` carries as statics
2725 // (`DOMException.ABORT_ERR` is 20), named by uppercasing the error name.
2726 if ns == "DOMException" {
2727 if let Some((_, code)) = DOM_EXCEPTION_CODES
2728 .iter()
2729 .find(|(n, _)| legacy_code_name(n) == name)
2730 {
2731 return Value::Float(*code);
2732 }
2733 }
2734 // Numeric constants.
2735 if let Some(k) = namespace_constant(ns, name) {
2736 return Value::Float(k);
2737 }
2738 // `Ctor.name` on a builtin constructor is the constructor name (`Array.name`
2739 // === "Array"); non-callable namespaces (`Math`/`JSON`) fall through to
2740 // `undefined`.
2741 // `GeneratorFunction.prototype` and the two async variants are REAL objects
2742 // in `native_protos`, not `Builtin("X.prototype")` namespace handles — they
2743 // sit on the prototype chain of every generator/async function, which a
2744 // handle cannot do. Without this the read fell through to `undefined`.
2745 if name == "prototype"
2746 && matches!(
2747 ns,
2748 "GeneratorFunction" | "AsyncFunction" | "AsyncGeneratorFunction"
2749 )
2750 {
2751 return with_host(|h| {
2752 h.ensure_native_protos();
2753 h.native_proto(ns).unwrap_or(Value::Undef)
2754 });
2755 }
2756 // `Error.prepareStackTrace` has a DEFAULT hook in node
2757 // (`ErrorPrepareStackTrace`), so a library probing `if
2758 // (Error.prepareStackTrace)` finds one. Reading `undefined` sent that probe
2759 // down the wrong branch. The default renders the header plus the frames,
2760 // which is what the fast path in `materialize_stack` already produces — it
2761 // recognises this exact builtin and skips the round trip.
2762 if ns == "Error" && name == "prepareStackTrace" {
2763 return with_host(|h| h.builtin_static("Error", "prepareStackTrace")).unwrap_or_else(
2764 || with_host(|h| h.alloc(JsObj::Builtin(DEFAULT_PREPARE.to_string()))),
2765 );
2766 }
2767 // `Error.stackTraceLimit` defaults to 10 and is settable; an assignment
2768 // lands in the builtin-static side table, which the read below consults
2769 // first. Without a default the READ was `undefined`, so a library doing
2770 // `const old = Error.stackTraceLimit` and restoring it later installed
2771 // `undefined` and disabled the limit permanently.
2772 if ns == "Error" && name == "stackTraceLimit" {
2773 return with_host(|h| h.builtin_static("Error", "stackTraceLimit"))
2774 .unwrap_or(Value::Float(10.0));
2775 }
2776 // `Ctor[Symbol.species]` is an accessor returning `this` on every builtin
2777 // that has one (23.1.2.5, 27.2.4.7, …). It was absent, so the species
2778 // protocol had nothing to read and every derived result came back a plain
2779 // builtin.
2780 if name == "@@species" && has_species(ns) {
2781 return with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())));
2782 }
2783 if name == "name" && is_builtin_ctor(ns) {
2784 return with_host(|h| h.new_str(ns.to_string()));
2785 }
2786 // A well-known symbol (`Symbol.iterator`, `Symbol.toPrimitive`, …) used as a
2787 // computed property/method key.
2788 if ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&name) {
2789 return with_host(|h| h.well_known_symbol(name));
2790 }
2791 // Non-function constants on a stdlib namespace (`path.sep`, `os.EOL`,
2792 // `buffer.Buffer`, `url.URL`).
2793 if let Some(v) = crate::stdlib::constant(ns, name) {
2794 return v;
2795 }
2796 // `Ctor.prototype` on a builtin constructor (`Object.prototype`,
2797 // `Array.prototype`, …): a prototype namespace whose methods are callable
2798 // thunks (`Object.prototype.toString.call(x)` is a load-time idiom in the
2799 // `get-intrinsic`/`function-bind` family).
2800 if name == "prototype" && is_builtin_ctor(ns) {
2801 // Same reasoning as the native prototypes below, for the error
2802 // hierarchy: `new Error(...)` links its `[[Prototype]]` to the REAL
2803 // `error_protos` object, so `Error.prototype` has to read back that same
2804 // object. It resolved to a fresh `Builtin("Error.prototype")` thunk
2805 // instead, which is a FUNCTION — so `Object.getPrototypeOf(new
2806 // Error("x")) === Error.prototype` was false, and `typeof
2807 // Error.prototype` was `"function"` where node says `"object"`.
2808 if host::ERROR_NAMES.contains(&ns) {
2809 if let Some(p) = with_host(|h| {
2810 h.ensure_error_protos();
2811 host::error_proto_of(h, ns)
2812 }) {
2813 return p;
2814 }
2815 }
2816 // `Buffer`/`Uint8Array` have real prototype *objects* — a Buffer's
2817 // `[[Prototype]]` points at one, so `Object.getPrototypeOf(buf) ===
2818 // Buffer.prototype` must compare equal, which a freshly-allocated
2819 // `Builtin` handle never can.
2820 if let Some(p) = with_host(|h| {
2821 h.ensure_native_protos();
2822 h.native_proto(ns)
2823 }) {
2824 return p;
2825 }
2826 let _ = ns;
2827 return with_host(|h| h.alloc(JsObj::Builtin(format!("{ns}.prototype"))));
2828 }
2829 // A NATIVE stdlib constructor's `.prototype` (`StringDecoder`, `Hash`,
2830 // `URLSearchParams`, …). These are absent from `is_builtin_ctor`, so the arm
2831 // above never fired and the read produced `undefined` — which broke the ES5
2832 // subclassing pattern libraries still ship. `iconv-lite`'s internal codec
2833 // reads `StringDecoder.prototype.end` at load, and threw
2834 // `Cannot read properties of undefined (reading 'end')`. Built from the same
2835 // instance-method table a method read consults, so the two cannot disagree.
2836 if name == "prototype" {
2837 if let Some(p) = with_host(|h| h.ensure_ctor_proto(ns)) {
2838 return p;
2839 }
2840 }
2841 // A method read off a builtin prototype namespace (`Array.prototype.slice`):
2842 // a `@proto:<Ctor>:<method>` thunk that, when invoked (typically via
2843 // `.call`/`.apply`), dispatches `method` against the invoke-time `this`.
2844 //
2845 // The thunk is minted only for a name the prototype REALLY carries. Minting
2846 // one unconditionally made every absent name answer with a function:
2847 // `Array.prototype.totallyBogus` was `[Function: totallyBogus]` where node
2848 // says `undefined`, and so was every well-known symbol a prototype does not
2849 // define — `Array.prototype[Symbol.toStringTag]` came back a function
2850 // instead of `undefined`, which is a value `Object.prototype.toString` and
2851 // every `typeof`/truthiness test downstream then read wrong.
2852 //
2853 // Existence is decided by the generated intrinsic table, which is read out
2854 // of the reference engine, so this cannot drift from what node defines.
2855 // A name the prototype does not define but `Object.prototype` does is
2856 // INHERITED, and node hands back Object.prototype's own function object
2857 // (`Map.prototype.toString === Object.prototype.toString` is `true`), so it
2858 // resolves to the `Object` thunk rather than a per-ctor one. That is also
2859 // what makes `String(Map.prototype)` print `[object Map]`: `Map.prototype`
2860 // has no own `toString`, and the inherited one is the generic tag reader,
2861 // not a Map method that rejects a non-Map `this`.
2862 if let Some(ctor) = ns.strip_suffix(".prototype") {
2863 // `Array.prototype[Symbol.unscopables]` (23.1.3.38) is a DATA property,
2864 // not an intrinsic function, so it is not in the arity table the lookup
2865 // above consults. It lists the methods a `with` block must NOT bring
2866 // into scope — the ones added after `with` existed, so old code using a
2867 // variable of the same name keeps working.
2868 if name == "@@unscopables" && ctor == "Array" {
2869 return with_host(|h| {
2870 let mut m: IndexMap<String, Value> = IndexMap::new();
2871 for k in [
2872 "at",
2873 "copyWithin",
2874 "entries",
2875 "fill",
2876 "find",
2877 "findIndex",
2878 "findLast",
2879 "findLastIndex",
2880 "flat",
2881 "flatMap",
2882 "includes",
2883 "keys",
2884 "toReversed",
2885 "toSorted",
2886 "toSpliced",
2887 "values",
2888 ] {
2889 m.insert(k.to_string(), Value::Bool(true));
2890 }
2891 let o = h.new_object(m);
2892 let null = h.null();
2893 h.set_proto(&o, null);
2894 o
2895 });
2896 }
2897 if builtin_meta(&format!("@proto:{ctor}:{name}")).is_some() {
2898 return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:{ctor}:{name}"))));
2899 }
2900 if ctor != "Object" && builtin_meta(&format!("@proto:Object:{name}")).is_some() {
2901 return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:Object:{name}"))));
2902 }
2903 // `constructor` is excluded from the table because it is not a method:
2904 // it is the constructor function itself, and node compares equal
2905 // (`Array.prototype.constructor === Array`). It used to resolve to a
2906 // `@proto:Array:constructor` thunk, which is a different object every
2907 // read and so never compared equal to anything.
2908 if name == "constructor" && is_builtin_ctor(ctor) {
2909 return with_host(|h| h.alloc(JsObj::Builtin(ctor.to_string())));
2910 }
2911 return Value::Undef;
2912 }
2913 let qualified = format!("{ns}.{name}");
2914 if is_known_builtin(&qualified) {
2915 return with_host(|h| h.alloc(JsObj::Builtin(qualified)));
2916 }
2917 // A property the user stuck on this builtin namespace (`Error.prepareStackTrace`).
2918 if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2919 return v;
2920 }
2921 // A builtin FUNCTION's own `name` and `length` (10.3.3-4: every one has
2922 // both). `Math.max.name` was `undefined` — as was every `.name` a library
2923 // reads to identify a callback it was handed. The non-callable namespaces
2924 // fall through: `Math.name` and `require('fs').length` really are undefined.
2925 if host::builtin_is_callable(ns) {
2926 match name {
2927 "name" => {
2928 if let Some(n) = proto_getter_name(ns) {
2929 return with_host(|h| h.new_str(n));
2930 }
2931 return with_host(|h| h.new_str(builtin_name(ns).to_string()));
2932 }
2933 // Only the intrinsics have a specified arity; a core-module
2934 // function's is a property of node's own JS source, so it stays
2935 // `undefined` rather than being invented here.
2936 "length" => {
2937 // A getter takes no argument (10.2.9 / the accessor grammar),
2938 // so its `length` is 0 — it is not in the intrinsic table,
2939 // which holds only named functions.
2940 if proto_getter_name(ns).is_some() {
2941 return Value::Float(0.0);
2942 }
2943 if let Some((_, len)) = builtin_meta(ns) {
2944 return Value::Float(len as f64);
2945 }
2946 }
2947 _ => {}
2948 }
2949 }
2950 Value::Undef
2951}
2952
2953/// Dispatch a `@proto:<Ctor>:<method>` thunk (a method read off a builtin
2954/// prototype, e.g. `Object.prototype.toString`) against `recv` (its invoke-time
2955/// `this`). `Object.prototype.toString` yields the `[object Tag]` brand string
2956/// libraries type-check on; every other method routes through normal method
2957/// dispatch on `recv`.
2958/// The TypeError a `<Ctor>.prototype.<method>` thunk throws when it is invoked
2959/// with NO receiver — `const f = [].push; f(1)`.
2960///
2961/// Reading a method off an instance used to mint a thunk bound to that
2962/// instance, so a detached method silently kept working on the object it came
2963/// from. Now that it is the shared intrinsic, a bare call has no `this` and has
2964/// to say so. Node words it four ways, and which one a method gets is not
2965/// something that can be derived — the split was measured across every method
2966/// of each prototype:
2967///
2968/// ```text
2969/// ToObject(this) "Cannot convert undefined or null to object"
2970/// RequireObjectCoercible "<Ctor>.prototype.<m> called on null or undefined"
2971/// brand check "<Ctor>.prototype.<m> requires that 'this' be a <X>"
2972/// everything else the generic incompatible-receiver message
2973/// ```
2974fn nullish_receiver_error(ctor: &str, method: &str, recv: &str) -> Option<String> {
2975 // `Array.prototype` splits: the CALLBACK-taking methods plus `concat` and
2976 // the two `indexOf` family members name themselves, the rest go through
2977 // `ToObject` and report its message.
2978 const ARRAY_NAMED: &[&str] = &[
2979 "concat",
2980 "every",
2981 "filter",
2982 "find",
2983 "findIndex",
2984 "findLast",
2985 "findLastIndex",
2986 "forEach",
2987 "indexOf",
2988 "map",
2989 "reduce",
2990 "reduceRight",
2991 "some",
2992 ];
2993 const TO_OBJECT: &str = "Cannot convert undefined or null to object";
2994 let named = |c: &str| format!("{c}.prototype.{method} called on null or undefined");
2995 let branded =
2996 |c: &str, want: &str| format!("{c}.prototype.{method} requires that 'this' be a {want}");
2997 // The generic form names the receiver, so a `null` one must not be reported
2998 // as `undefined`.
2999 let generic = |c: &str, m: &str| {
3000 format!("Method {c}.prototype.{m} called on incompatible receiver {recv}")
3001 };
3002 Some(match ctor {
3003 "Array" if ARRAY_NAMED.contains(&method) => named("Array"),
3004 "Array" => TO_OBJECT.to_string(),
3005 // `Object.prototype.toString` is the one method that ACCEPTS a nullish
3006 // receiver — it answers `[object Undefined]`.
3007 "Object" if method == "toString" => return None,
3008 "Object" if method == "toLocaleString" => named("Object"),
3009 "Object" => TO_OBJECT.to_string(),
3010 // Both aliases report the LEGACY name in the message, which is the one
3011 // place `name` and the message disagree.
3012 "String" if method == "trimStart" => named("String").replace("trimStart", "trimLeft"),
3013 "String" if method == "trimEnd" => named("String").replace("trimEnd", "trimRight"),
3014 "String" if matches!(method, "toString" | "valueOf") => branded("String", "String"),
3015 "String" => named("String"),
3016 "Number" => branded("Number", "Number"),
3017 "Boolean" => branded("Boolean", "Boolean"),
3018 "Symbol" => branded("Symbol", "Symbol"),
3019 "Function" if method == "bind" => "Bind must be called on a function".to_string(),
3020 "Function" if matches!(method, "call" | "apply") => format!(
3021 "Function.prototype.{method} was called on undefined, which is undefined and not a function"
3022 ),
3023 "Function" => branded("Function", "Function"),
3024 // `Promise.prototype.catch`/`finally` are written in terms of `then`, so
3025 // a nullish receiver fails inside them and reports that instead.
3026 "Promise" if method == "catch" => {
3027 "Cannot read properties of undefined (reading 'then')".to_string()
3028 }
3029 "Promise" if method == "finally" => {
3030 "Promise.prototype.finally called on non-object".to_string()
3031 }
3032 "Date" if method == "toJSON" => TO_OBJECT.to_string(),
3033 // The plain GETTERS and `valueOf` read `[[DateValue]]` directly and
3034 // report that slot check; every setter, every `to*String` and the two
3035 // legacy year methods go through the generic receiver check first.
3036 "Date"
3037 if method == "valueOf"
3038 || (method.starts_with("get") && method != "getYear") =>
3039 {
3040 "this is not a Date object.".to_string()
3041 }
3042 // An ALIAS reports the method it aliases: `toGMTString` IS `toUTCString`
3043 // and `Set.prototype.keys` IS `values`, one function object each.
3044 "Date" if method == "toGMTString" => generic("Date", "toUTCString"),
3045 "Set" if method == "keys" => generic("Set", "values"),
3046 // Everything else that is brand-checked names itself. Node reaches this
3047 // wording from a `[[GetOwnProperty]]`-style slot check; here the check
3048 // is the receiver's kind, and only the message has to agree.
3049 "ArrayBuffer" | "DataView" | "RegExp" | "WeakRef" | "Map" | "Set" | "WeakMap"
3050 | "WeakSet" | "Promise" | "Date" => generic(ctor, method),
3051 "URLSearchParams" => "Value of \"this\" must be of type URLSearchParams".to_string(),
3052 // Node's `URL` methods fail while reaching for their internal state, and
3053 // report the read that failed rather than the method.
3054 "URL" => "Cannot read properties of undefined (reading 'URL')".to_string(),
3055 _ => return None,
3056 })
3057}
3058
3059/// Whether `<ctor>.prototype.<method>` begins with a `this<Type>Value` brand
3060/// check (21.1.3, 20.3.3, 22.1.3.29/.35, 21.2.3). Every `Number.prototype`
3061/// method does; of `String.prototype` only `toString`/`valueOf` do — the rest
3062/// are generic and coerce their receiver with `ToString`.
3063fn is_brand_checked_primitive_method(ctor: &str, method: &str) -> bool {
3064 match ctor {
3065 "Number" => matches!(
3066 method,
3067 "toString" | "toLocaleString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision"
3068 ),
3069 "BigInt" => matches!(method, "toString" | "toLocaleString" | "valueOf"),
3070 "String" | "Boolean" => matches!(method, "toString" | "valueOf"),
3071 _ => false,
3072 }
3073}
3074
3075/// `this<Type>Value(recv)` for `ctor` ∈ Number/String/Boolean/BigInt: the
3076/// primitive itself, the primitive a wrapper boxes, or — for the three
3077/// prototypes that are themselves wrappers (21.1.3, 22.1.3, 20.3.3) — the
3078/// prototype's own `+0` / `""` / `false`. `None` is the TypeError case.
3079fn this_primitive_value(ctor: &str, recv: &Value) -> Option<Value> {
3080 let expected = match ctor {
3081 "Number" => "number",
3082 "String" => "string",
3083 "Boolean" => "boolean",
3084 "BigInt" => "bigint",
3085 _ => return None,
3086 };
3087 let is_expected = |v: &Value| with_host(|h| h.type_of(v)) == expected;
3088 if is_expected(recv) {
3089 return Some(recv.clone());
3090 }
3091 if let Some(prim) = wrapped_primitive(recv).filter(is_expected) {
3092 return Some(prim);
3093 }
3094 if with_host(|h| h.intrinsic_proto_ctor(recv) == Some(ctor)) {
3095 return match ctor {
3096 "Number" => Some(Value::Float(0.0)),
3097 "String" => Some(with_host(|h| h.new_str(""))),
3098 "Boolean" => Some(Value::Bool(false)),
3099 _ => None,
3100 };
3101 }
3102 None
3103}
3104
3105pub fn proto_method(recv: &Value, ctor_method: &str, args: Vec<Value>) -> Result<Value, String> {
3106 let (ctor, method) = ctor_method.split_once(':').unwrap_or(("", ctor_method));
3107 // A prototype ACCESSOR installed by `ensure_ctor_proto`: it reads or writes
3108 // the instance's hidden `@@<name>` slot, which is where the value lives now
3109 // that the public name is a getter rather than an own property.
3110 if let Some(key) = method.strip_prefix("@get@") {
3111 if let Some(v) = with_host(|h| match h.get(recv) {
3112 Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
3113 _ => None,
3114 }) {
3115 return Ok(v);
3116 }
3117 // No stored slot: the value is COMPUTED, so ask the class. `KeyObject`'s
3118 // `symmetricKeySize` is the secret's byte length, which nothing stores.
3119 let tag = crate::stdlib::native_tag(recv).unwrap_or_default();
3120 return crate::stdlib::instance_call(&tag, recv, method, args);
3121 }
3122 if let Some(key) = method.strip_prefix("@set@") {
3123 let v = args.first().cloned().unwrap_or(Value::Undef);
3124 with_host(|h| {
3125 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
3126 p.insert(format!("@@{key}"), v);
3127 }
3128 });
3129 crate::stdlib::instance_accessor_written(ctor, key, recv);
3130 return Ok(Value::Undef);
3131 }
3132 if with_host(|h| h.is_nullish(recv)) {
3133 let shown = if with_host(|h| h.is_null(recv)) {
3134 "null"
3135 } else {
3136 "undefined"
3137 };
3138 if let Some(msg) = nullish_receiver_error(ctor, method, shown) {
3139 return Err(format!("TypeError: {msg}"));
3140 }
3141 }
3142 // `Error.prototype.toString` (20.5.3.4): `name`, `message`, or `name:
3143 // message`, read off the chain so a subclass's `this.name = 'E'` is honored.
3144 if ctor == "Error" && method == "toString" {
3145 // A `DOMException` keeps its `name`/`message` in internal slots, so the
3146 // chain read below would find the class name on the prototype instead.
3147 if let Some(n) = dom_exception_slot(recv, "name") {
3148 let name = with_host(|h| h.str_of(&n));
3149 let msg = dom_exception_slot(recv, "message")
3150 .map(|m| with_host(|h| h.str_of(&m)))
3151 .unwrap_or_default();
3152 let s = if msg.is_empty() {
3153 name
3154 } else {
3155 format!("{name}: {msg}")
3156 };
3157 return Ok(with_host(|h| h.new_str(s)));
3158 }
3159 // `name` and `message` are read with `[[Get]]` (20.5.3.4 steps 3 and 5),
3160 // so a PROXY supplies them through its `get` trap. Reading the stored
3161 // ones first made `String(new Proxy(err, handler))` ignore the handler.
3162 let via_proxy = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy);
3163 let stored = (!via_proxy).then(|| with_host(|h| h.error_to_string(recv)));
3164 let s = match stored.flatten() {
3165 Some(s) => s,
3166 None => {
3167 let read = |k: &str| -> Result<Option<String>, String> {
3168 Ok(host::protocol_lookup(recv, k)?.map(|v| with_host(|h| h.str_of(&v))))
3169 };
3170 let name = read("name")?.unwrap_or_else(|| "Error".into());
3171 let msg = read("message")?.unwrap_or_default();
3172 if msg.is_empty() {
3173 name
3174 } else {
3175 format!("{name}: {msg}")
3176 }
3177 }
3178 };
3179 return Ok(with_host(|h| h.new_str(s)));
3180 }
3181 // The methods that read their receiver through `thisNumberValue` /
3182 // `thisBooleanValue` / `thisStringValue` / `thisBigIntValue` accept only the
3183 // primitive, its wrapper, or the prototype object (which carries the zero
3184 // value) — anything else is a TypeError naming the method. Unchecked,
3185 // `Number.prototype.valueOf.call({})` answered `{}`, `toFixed.call({})`
3186 // reported "toFixed is not a function", and `Number.prototype.valueOf()`
3187 // recursed through the generic conversion until the stack overflowed.
3188 if is_brand_checked_primitive_method(ctor, method) {
3189 let Some(prim) = this_primitive_value(ctor, recv) else {
3190 return Err(format!(
3191 "TypeError: {ctor}.prototype.{method} requires that 'this' be a {ctor}"
3192 ));
3193 };
3194 return host::call_method(&prim, method, args);
3195 }
3196 // A primitive wrapper's `toString`/`valueOf`/`toLocaleString`: unwrap and
3197 // answer as the boxed primitive does. `Number.prototype.toString.call(5)`
3198 // arrives with an already-primitive receiver and needs no unwrapping.
3199 if matches!(ctor, "String" | "Number" | "Boolean") {
3200 let prim = wrapped_primitive(recv).unwrap_or_else(|| recv.clone());
3201 return host::call_method(&prim, method, args);
3202 }
3203 // `thisSymbolValue`/`thisBigIntValue` (20.4.3, 21.2.3) accept a WRAPPER as
3204 // readily as the primitive, and neither was unwrapped here. A BigInt
3205 // wrapper's `valueOf` therefore re-entered the generic conversion, which
3206 // looked `valueOf` up again and called it again: `+Object(9n)` recursed
3207 // until the stack overflowed and ABORTED the process, which no try/catch can
3208 // see. A Symbol wrapper failed the brand check below instead and reported
3209 // that `this` was not a Symbol, when it is one. Only a real wrapper is
3210 // unwrapped — `Symbol.prototype` itself boxes nothing and still has to reach
3211 // the brand check.
3212 if matches!(ctor, "Symbol" | "BigInt") {
3213 if let Some(prim) = wrapped_primitive(recv) {
3214 return host::call_method(&prim, method, args);
3215 }
3216 }
3217 if ctor == "Object" && method == "toString" {
3218 // Steps 16-17 of 20.1.3.6: a `Symbol.toStringTag` STRING on the receiver
3219 // (own or inherited, data property or getter) replaces the builtin brand,
3220 // which is how a class advertises its own (`class C { get
3221 // [Symbol.toStringTag]() { return 'Cee' } }` → `[object Cee]`). The read
3222 // runs outside the host borrow so an accessor can be invoked.
3223 // A Proxy has no chain to probe: 20.1.3.6 step 15 is an unconditional
3224 // `Get(O, @@toStringTag)`, so the `get` trap decides. Probing first (as
3225 // the ordinary receiver does, to keep the read off objects that have no
3226 // tag) would always miss and brand every tagged proxy `[object Object]`.
3227 if let Some(s) = to_string_tag(recv)? {
3228 return Ok(with_host(|h| h.new_str(format!("[object {s}]"))));
3229 }
3230 return Ok(with_host(|h| h.new_str(object_tag(h, recv))));
3231 }
3232 // These thunks now live on the real `Object.prototype` object, i.e. on the
3233 // receiver's own chain — routing back through `call_method` would re-resolve
3234 // this very thunk and recurse.
3235 if ctor == "Object" && is_object_builtin_method(method) {
3236 return object_builtin_method(recv, method, args);
3237 }
3238 // `EventEmitter.prototype.<m>` mixed onto a receiver (express's `app`): run the
3239 // emitter method directly against `recv` (routing back through `call_method`
3240 // would re-resolve the mixed-in thunk and recurse).
3241 if ctor == "EventEmitter" {
3242 return crate::stdlib::events::instance_call(recv, method, args);
3243 }
3244 // Same recursion hazard for the exotics with a real prototype object: the
3245 // thunk now lives ON the receiver's prototype chain, so `call_method` would
3246 // re-resolve this very thunk. Dispatch straight to the native instance
3247 // implementation when the receiver is in fact an instance of `ctor`.
3248 if ctor == "Buffer" && crate::stdlib::native_tag(recv).as_deref() == Some("Buffer") {
3249 return crate::stdlib::buffer::instance_call(recv, method, &args);
3250 }
3251 // The shared typed-array methods now live on the `%TypedArray%.prototype`
3252 // intermediate, so their thunks are tagged `TypedArray`; `Uint8Array` still
3253 // appears for anything read directly off `Uint8Array.prototype`. Both
3254 // dispatch the same way, and both must bypass `call_method` or the thunk
3255 // would re-resolve itself off the receiver's chain and recurse.
3256 if ctor == "Uint8Array" || ctor == "TypedArray" {
3257 match crate::stdlib::native_tag(recv).as_deref() {
3258 Some("Buffer") => return crate::stdlib::buffer::instance_call(recv, method, &args),
3259 Some("TypedArray") => {
3260 return crate::stdlib::typedarray::instance_call(recv, method, &args)
3261 }
3262 _ => {}
3263 }
3264 }
3265 // `Array.prototype.<m>.call(arrayLike)` — every `Array.prototype` method is
3266 // GENERIC over `this` (23.1.3: each starts with `ToObject(this)` and
3267 // `LengthOfArrayLike`), which is what makes
3268 // `Array.prototype.slice.call(arguments)` the idiom it is. The receiver here
3269 // is not an Array, so `call_method` would report the method missing.
3270 if ctor == "Array" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Array) {
3271 return array_generic(recv, method, args);
3272 }
3273 // The general form of the two special cases above: a thunk taken off a native
3274 // constructor's real prototype, invoked with a receiver that IS an instance of
3275 // that constructor. Routing back through `call_method` would re-resolve this
3276 // very thunk off the receiver's own chain and recurse forever, which is why
3277 // each such prototype needed a hand-written bypass; now they all have one.
3278 // A SUBCLASS counts: `SecretKeyObject` reaches `KeyObject.prototype.equals`
3279 // through its chain, and requiring an exact tag match sent that call back
3280 // into `call_method`, which re-resolved this same thunk and recursed until
3281 // the stack overflowed.
3282 if let Some(tag) = crate::stdlib::native_tag(recv) {
3283 let mut c = Some(tag.as_str());
3284 while let Some(t) = c {
3285 if t == ctor {
3286 return crate::stdlib::instance_call(&tag, recv, method, args);
3287 }
3288 c = crate::stdlib::native_parent(t);
3289 }
3290 }
3291 // A BRANDED method reached with a receiver that has no such internal slot.
3292 // Every arm above dispatches a receiver that IS an instance, so arriving
3293 // here with one of these constructors means the brand check failed — the
3294 // spec's very first step for each of them (24.2.3.x reads `[[SetData]]`,
3295 // 24.1.3.x `[[MapData]]`, 27.2.5.4 `[[PromiseState]]`, 23.2.3.x
3296 // `ValidateTypedArray`). Falling through to ordinary dispatch reported
3297 // `union is not a function`, which says the method does not exist rather
3298 // than that the receiver is the wrong kind of object.
3299 // `Date.prototype`'s methods split in two: the ones that read the time value
3300 // (`ThisTimeValue`, 21.4.4.x) report `this is not a Date object.`, and the
3301 // rest take the ordinary branded form. Measured on node v26.8.1:
3302 // `Date.prototype.getTime.call({})` is the first, `.toISOString.call({})`
3303 // and `.setHours.call({})` the second.
3304 if ctor == "Date" && crate::stdlib::native_tag(recv).as_deref() != Some("Date") {
3305 const THIS_TIME_VALUE: &[&str] = &[
3306 "getTime",
3307 "valueOf",
3308 "getYear",
3309 "getFullYear",
3310 "getMonth",
3311 "getDate",
3312 "getDay",
3313 "getHours",
3314 "getMinutes",
3315 "getSeconds",
3316 "getMilliseconds",
3317 "getUTCFullYear",
3318 "getUTCMonth",
3319 "getUTCDate",
3320 "getUTCDay",
3321 "getUTCHours",
3322 "getUTCMinutes",
3323 "getUTCSeconds",
3324 "getUTCMilliseconds",
3325 "getTimezoneOffset",
3326 ];
3327 if THIS_TIME_VALUE.contains(&method) {
3328 return Err(host::type_error("this is not a Date object."));
3329 }
3330 // `toJSON` (21.4.4.37) is deliberately generic — it converts the
3331 // receiver and INVOKES `toISOString` on it, so it fails on the missing
3332 // method rather than on a brand.
3333 if method != "toJSON" {
3334 return Err(host::type_error(&format!(
3335 "Method Date.prototype.{method} called on incompatible receiver {}",
3336 no_side_effects_string(recv)
3337 )));
3338 }
3339 }
3340 // `%TypedArray%.prototype`'s methods split the same way: `ValidateTypedArray`
3341 // (23.2.4.4) reports `this is not a typed array.`, while the handful that
3342 // check the receiver at the call boundary take the branded form. Measured
3343 // over all 27 shared methods on node v26.8.1; `toString` is the one that is
3344 // genuinely generic (it is `Array.prototype.toString`) and never brands.
3345 if matches!(ctor, "TypedArray" | "Uint8Array")
3346 && !matches!(
3347 crate::stdlib::native_tag(recv).as_deref(),
3348 Some("TypedArray") | Some("Buffer")
3349 )
3350 {
3351 const BRANDED: &[&str] = &[
3352 "slice",
3353 "subarray",
3354 "join",
3355 "sort",
3356 "at",
3357 "toReversed",
3358 "toSorted",
3359 "toLocaleString",
3360 ];
3361 if BRANDED.contains(&method) {
3362 return Err(host::type_error(&format!(
3363 "Method %TypedArray%.prototype.{method} called on incompatible receiver {}",
3364 no_side_effects_string(recv)
3365 )));
3366 }
3367 // The four base64/hex methods brand themselves against `Uint8Array`
3368 // specifically — a WRONG view is as incompatible as a plain object, and
3369 // the generic guard here cannot tell those apart.
3370 if crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS.contains(&method) {
3371 return Err(host::type_error(&format!(
3372 "Method Uint8Array.prototype.{method} called on incompatible receiver {}",
3373 no_side_effects_string(recv)
3374 )));
3375 }
3376 if method != "toString" {
3377 return Err(host::type_error("this is not a typed array."));
3378 }
3379 }
3380 // `Function.prototype.call`/`apply`/`bind` with a callable PROXY as `this`
3381 // (`pf.call(null, 4, 5)`, reached through the target's chain). Handing
3382 // that back to `call_method` read `call` off the proxy again, which
3383 // resolved to this same thunk, and recursed until the stack overflowed and
3384 // aborted the process. The three are defined on the callee alone, so they
3385 // run here: the proxy's `apply` trap (or its target) gets the call.
3386 // `toString` recursed the same way.
3387 if ctor == "Function"
3388 && matches!(method, "call" | "apply" | "bind" | "toString")
3389 && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
3390 {
3391 let mut rest = args.into_iter();
3392 let this_arg = rest.next().unwrap_or(Value::Undef);
3393 match method {
3394 "call" => return host::invoke(recv, rest.collect(), Some(this_arg)),
3395 "apply" => {
3396 let list = match rest.next() {
3397 None | Some(Value::Undef) => Vec::new(),
3398 Some(v) if with_host(|h| h.is_null(&v)) => Vec::new(),
3399 Some(v) => create_list_from_array_like(&v)?,
3400 };
3401 return host::invoke(recv, list, Some(this_arg));
3402 }
3403 "bind" => {
3404 let target = recv.clone();
3405 let pre: Vec<Value> = rest.collect();
3406 return Ok(with_host(|h| {
3407 h.alloc(JsObj::BoundFunc {
3408 target,
3409 this: this_arg,
3410 args: pre,
3411 })
3412 }));
3413 }
3414 // A proxy has no source text; V8 prints the native form for it.
3415 _ => return Ok(with_host(|h| h.new_str("function () { [native code] }"))),
3416 }
3417 }
3418 // `Symbol.prototype`'s methods are branded, and the receiver that reaches
3419 // them is very often NOT a symbol: `Symbol.prototype` itself is an ordinary
3420 // object. Without this check `Symbol.prototype.toString()` re-entered the
3421 // generic string conversion, which looked `toString` up again and called it
3422 // again — an infinite recursion that overflowed the stack and ABORTED the
3423 // process, which no `try`/`catch` can see. Node throws a plain TypeError.
3424 // The wording is Symbol's own, not the "incompatible receiver" form the
3425 // collections use.
3426 if ctor == "Symbol" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Symbol) {
3427 // A symbol-KEYED method is named in brackets rather than after a dot:
3428 // node's wording is `Symbol.prototype [ @@toPrimitive ] requires …`.
3429 // That is the message `String(Symbol.prototype)` produces, since the
3430 // conversion reaches `@@toPrimitive` before it would reach `toString`.
3431 let named = match method.strip_prefix("@@") {
3432 Some(sym) => format!("Symbol.prototype [ @@{sym} ]"),
3433 None => format!("Symbol.prototype.{method}"),
3434 };
3435 return Err(host::type_error(&format!(
3436 "{named} requires that 'this' be a Symbol"
3437 )));
3438 }
3439 if let Some(label) = branded_method_label(ctor, recv) {
3440 return Err(host::type_error(&format!(
3441 "Method {label}.prototype.{method} called on incompatible receiver {}",
3442 no_side_effects_string(recv)
3443 )));
3444 }
3445 host::call_method(recv, method, args)
3446}
3447
3448/// The name a branded prototype method reports itself under when its receiver
3449/// fails the brand check, or `None` when `ctor`'s methods are generic over
3450/// `this` (every `Array.prototype` and `Object.prototype` method is) or the
3451/// receiver really is an instance.
3452///
3453fn branded_method_label(ctor: &str, recv: &Value) -> Option<&'static str> {
3454 let kind = with_host(|h| h.kind_of(recv));
3455 // `weak` is part of the brand: a `WeakSet` has `[[WeakSetData]]`, not
3456 // `[[SetData]]`, so `Set.prototype.has.call(new WeakSet())` is incompatible
3457 // even though both are `JsObj::Set` here.
3458 let weak = peek(recv, |o| match o {
3459 JsObj::Set { weak, .. } | JsObj::Map { weak, .. } => Some(*weak),
3460 _ => None,
3461 })
3462 .unwrap_or(false);
3463 let ok = match ctor {
3464 "Set" => kind == Some(ObjKind::Set) && !weak,
3465 "WeakSet" => kind == Some(ObjKind::Set) && weak,
3466 "Map" => kind == Some(ObjKind::Map) && !weak,
3467 "WeakMap" => kind == Some(ObjKind::Map) && weak,
3468 "Promise" => kind == Some(ObjKind::Promise),
3469 _ => return None,
3470 };
3471 if ok {
3472 return None;
3473 }
3474 Some(match ctor {
3475 "Set" => "Set",
3476 "WeakSet" => "WeakSet",
3477 "Map" => "Map",
3478 "WeakMap" => "WeakMap",
3479 _ => "Promise",
3480 })
3481}
3482
3483/// V8's `Object::NoSideEffectsToString`, the rendering an engine-thrown message
3484/// uses for a value it must not run user code on. Measured on node v26.8.1
3485/// through `Map.prototype.get.call(x)`:
3486///
3487/// ```text
3488/// 5 / 'str' / true / null / undefined / 9n the value's own ToString
3489/// Symbol('s') Symbol(s)
3490/// function f(){} its source text
3491/// new Error('e') Error: e
3492/// {} / new (class A {}) #<Object> / #<A>
3493/// new Map() / Promise.resolve() #<Map> / #<Promise>
3494/// [] / new Date() / /re/ / new Uint8Array() [object Array] / [object Date] / …
3495/// { toString() {} } / Object.create(null) [object Object]
3496/// ```
3497///
3498/// The split is one test: a receiver whose `toString` is still
3499/// `Object.prototype.toString` prints `#<Constructor>`, and any other receiver
3500/// prints what the BUILTIN brand would be — V8 never calls the user's method,
3501/// which is why an object with its own `toString` prints `[object Object]` and
3502/// not what that method returns.
3503fn no_side_effects_string(recv: &Value) -> String {
3504 if with_host(|h| host::is_primitive(h, recv)) || with_host(|h| host::is_callable(h, recv)) {
3505 return with_host(|h| h.str_of(recv));
3506 }
3507 if let Some(s) = with_host(|h| h.error_to_string(recv)) {
3508 return s;
3509 }
3510 // `native_tag` re-enters the host, so it is read BEFORE the borrow below
3511 // rather than inside it.
3512 let native = crate::stdlib::native_tag(recv).is_some();
3513 let brands_itself = with_host(|h| {
3514 // `Object.prototype.toString` reaches every object as a thunk on the
3515 // real prototype object, so its presence proves nothing; only a
3516 // toString the receiver's chain OVERRIDES it with counts.
3517 let overridden = host::lookup_chain(h, recv, "toString")
3518 .map(|f| !matches!(h.get(&f), Some(JsObj::Builtin(n)) if n == "@proto:Object:toString"))
3519 .unwrap_or(false);
3520 native
3521 || overridden
3522 || h.has_null_proto(recv)
3523 || !matches!(
3524 h.kind_of(recv),
3525 Some(ObjKind::Object)
3526 | Some(ObjKind::Map)
3527 | Some(ObjKind::Set)
3528 | Some(ObjKind::Promise)
3529 )
3530 });
3531 if brands_itself {
3532 return with_host(|h| object_tag(h, recv));
3533 }
3534 let ctor = get_property(recv, "constructor")
3535 .ok()
3536 .map(|c| with_host(|h| h.callable_name(&c)))
3537 .filter(|n| !n.is_empty())
3538 .unwrap_or_else(|| "Object".to_string());
3539 format!("#<{ctor}>")
3540}
3541
3542/// The value of `v[Symbol.toStringTag]` for a builtin that genuinely carries
3543/// one, or `None` when reading that symbol must yield `undefined`.
3544///
3545/// Every builtin brand is already computed in exactly one place (`object_tag`),
3546/// so this reuses it and subtracts the legacy builtins, which brand for
3547/// `Object.prototype.toString` but expose no `Symbol.toStringTag` property.
3548/// The subtracted list is measured against node v26.7.0, not assumed: `[]`,
3549/// `function(){}`, `{}`, `new Date()`, `/x/` and `new Error()` all read
3550/// `undefined`, while `Map`/`Set`/`Promise`/typed arrays/`ArrayBuffer`/
3551/// `DataView`/`WeakRef`/`FinalizationRegistry`/`BigInt`/`Symbol`/generators/
3552/// async+generator functions/`Math`/`JSON`/`Reflect`/`URL`/`URLSearchParams`/
3553/// `TextEncoder`/`TextDecoder` all read their brand.
3554pub(crate) fn well_known_tag(h: &host::JsHost, v: &Value) -> Option<String> {
3555 // A primitive never carries the symbol except a BigInt/Symbol wrapper, both
3556 // of which `object_tag` already brands.
3557 let tag = object_brand(h, v);
3558 const NO_TAG: &[&str] = &[
3559 "Undefined",
3560 "Null",
3561 "Boolean",
3562 "Number",
3563 "String",
3564 "Array",
3565 "Function",
3566 "Object",
3567 "Date",
3568 "RegExp",
3569 "Error",
3570 ];
3571 if NO_TAG.contains(&tag.as_str()) {
3572 return None;
3573 }
3574 Some(tag)
3575}
3576
3577/// The constructor name of the nearest intrinsic prototype on `v`'s chain that
3578/// carries an own `Symbol.toStringTag`, if any.
3579fn chain_tag_ctor(h: &host::JsHost, v: &Value) -> Option<String> {
3580 let mut cur = h.proto_of(v);
3581 for _ in 0..100 {
3582 let p = cur?;
3583 if h.is_null(&p) {
3584 return None;
3585 }
3586 let name = match h.get(&p) {
3587 Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
3588 _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
3589 }
3590 // A CLASS prototype is not linked to the builtin its class extends —
3591 // the relationship lives on the class value — so the walk crosses over
3592 // there, or `Object.create(D.prototype)` for `class D extends Map`
3593 // finds nothing.
3594 .or_else(|| {
3595 h.class_owning_proto(&p)
3596 .and_then(|c| h.class_builtin_ancestor(&c))
3597 .map(|b| h.callable_name(&b))
3598 .filter(|n| !n.is_empty())
3599 });
3600 if let Some(n) = name {
3601 if intrinsic_proto_members(&format!("{n}.prototype"))
3602 .is_some_and(|ms| ms.contains(&"@@toStringTag"))
3603 {
3604 return Some(n);
3605 }
3606 }
3607 cur = h.proto_of(&p);
3608 }
3609 None
3610}
3611
3612/// The `Object.prototype.toString` brand tag for `v` (`[object Array]` etc.).
3613/// Every builtin exotic object reports its own brand, which is how packages
3614/// type-test values they did not construct (`toString.call(x) ===
3615/// '[object Uint8Array]'`). A `Buffer` reports `Uint8Array` because in Node it
3616/// IS a `Uint8Array` subclass and inherits that `Symbol.toStringTag`.
3617pub(crate) fn object_tag(h: &host::JsHost, v: &Value) -> String {
3618 format!("[object {}]", object_brand(h, v))
3619}
3620
3621/// The bare brand name behind `Object.prototype.toString` (`Array`, `Uint8Array`
3622/// …), without the `[object …]` wrapper. Split out so the brand and the
3623/// `Symbol.toStringTag` property read cannot disagree about what a value is.
3624/// Whether `v` is a function's `arguments` object.
3625///
3626/// Backed by an Array so indices, `length`, spread and `for-of` all work, but
3627/// marked so it does not pass for one: node's is an exotic, and `isArray`, the
3628/// brand and `util.types.isArgumentsObject` all have to tell them apart.
3629pub fn is_arguments(v: &Value) -> bool {
3630 with_host(|h| is_arguments_h(h, v))
3631}
3632
3633/// `is_arguments` for a caller that already holds the host borrow — `object_brand`
3634/// runs under one, and re-entering through `with_host` aborts the process.
3635pub fn is_arguments_h(h: &host::JsHost, v: &Value) -> bool {
3636 h.fn_prop(v, "@@arguments").is_some()
3637}
3638
3639fn object_brand(h: &host::JsHost, v: &Value) -> String {
3640 // A `<C>.prototype` this host built as a real object is an ORDINARY object:
3641 // it holds no instance slot, so only the branded few report anything but
3642 // `[object Object]`. Checked before the match because those prototypes are
3643 // plain `JsObj::Object`s and would otherwise be branded by whatever their
3644 // own properties happen to look like — `TypeError.prototype` has `name` and
3645 // `message`, which read as an Error instance.
3646 if let Some(ctor) = h.intrinsic_proto_ctor(v) {
3647 return if BRANDED_PROTOS.contains(&ctor) {
3648 ctor.to_string()
3649 } else {
3650 "Object".to_string()
3651 };
3652 }
3653 let tag: String = match v {
3654 Value::Undef => "Undefined".into(),
3655 Value::Bool(_) => "Boolean".into(),
3656 Value::Int(_) | Value::Float(_) => "Number".into(),
3657 Value::Str(_) => "String".into(),
3658 Value::Obj(_) => match h.get(v) {
3659 Some(JsObj::Null) => "Null".into(),
3660 Some(JsObj::Str(_)) => "String".into(),
3661 Some(JsObj::Array(_)) if is_arguments_h(h, v) => "Arguments".into(),
3662 Some(JsObj::Array(_)) => "Array".into(),
3663 // An array iterator and a Map/Set iterator carry the tags of their
3664 // prototypes (23.1.5.2.2, 24.1.5.2.2, 24.2.6.2.2).
3665 Some(JsObj::Iter { array: Some(_), .. }) => "Array Iterator".into(),
3666 Some(JsObj::Object(_)) if collection_iterator_view(h, v).is_some() => {
3667 match collection_iterator_view(h, v).map(|(b, _)| b) {
3668 Some(b) if b.starts_with("Map") => "Map Iterator".into(),
3669 _ => "Set Iterator".into(),
3670 }
3671 }
3672 // A lazy iterator helper brands as node does.
3673 Some(JsObj::Object(p))
3674 if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("IteratorHelper") =>
3675 {
3676 "Iterator Helper".into()
3677 }
3678 // A `DOMException` brands by its class, not as a plain `Error`.
3679 Some(JsObj::Object(p)) if p.contains_key("@@domName") => "DOMException".into(),
3680 // 20.1.3.6 steps 5-8 brand a wrapper by its internal slot, so
3681 // `Object.prototype.toString.call(new Number(1))` is
3682 // `[object Number]` rather than `[object Object]`.
3683 Some(JsObj::Object(p)) if p.contains_key("@@primitive") => match p["@@primitive"] {
3684 Value::Bool(_) => "Boolean".into(),
3685 Value::Int(_) | Value::Float(_) => "Number".into(),
3686 _ => "String".into(),
3687 },
3688 // 20.1.3.6 step 3 brands by `IsArray`, which follows a Proxy to its
3689 // `[[ProxyTarget]]` — `Object.prototype.toString.call(new Proxy([],
3690 // {}))` is `'[object Array]'`. Everything else about a proxy brands
3691 // as a plain Object (a `Symbol.toStringTag` read through the `get`
3692 // trap is handled by the caller, before this).
3693 Some(JsObj::Proxy { target, .. }) => {
3694 let mut cur = target;
3695 for _ in 0..100 {
3696 match h.get(cur) {
3697 Some(JsObj::Proxy { target: t, .. }) => cur = t,
3698 _ => break,
3699 }
3700 }
3701 match h.get(cur) {
3702 Some(JsObj::Array(_)) => "Array".into(),
3703 _ => "Object".into(),
3704 }
3705 }
3706 // `function*` / `async function` / `async function*` carry their own
3707 // `Symbol.toStringTag` in V8 (27.3.3.2, 27.7.3.2, 27.4.3.2).
3708 Some(JsObj::Func(f)) => match h.funcs.get(f.def_id) {
3709 Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction".into(),
3710 Some(d) if d.is_generator => "GeneratorFunction".into(),
3711 Some(d) if d.is_async => "AsyncFunction".into(),
3712 _ => "Function".into(),
3713 },
3714 // `Math`/`JSON`/`Reflect` are namespace OBJECTS, not callables, and
3715 // brand by name (21.3.1.9, 25.5.3, 28.1.14).
3716 Some(JsObj::Builtin(n)) if matches!(n.as_str(), "Math" | "JSON" | "Reflect") => {
3717 n.clone()
3718 }
3719 // A `<Ctor>.prototype` object brands as the constructor it belongs
3720 // to — `Object.prototype.toString.call(Set.prototype)` is
3721 // `[object Set]` — and a `require()`d module namespace is a plain
3722 // object. Neither is a function, so neither brands as one.
3723 Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
3724 match n.strip_suffix(".prototype") {
3725 Some(ctor) if BRANDED_PROTOS.contains(&ctor) => ctor.to_string(),
3726 _ => "Object".into(),
3727 }
3728 }
3729 Some(JsObj::Class(_))
3730 | Some(JsObj::Builtin(_))
3731 | Some(JsObj::BoundFunc { .. })
3732 | Some(JsObj::BoundMethod { .. }) => "Function".into(),
3733 // A suspended generator object is `[object Generator]`; an async one
3734 // `[object AsyncGenerator]`.
3735 Some(JsObj::Generator { .. }) if h.is_async_gen_val(v) => "AsyncGenerator".into(),
3736 Some(JsObj::Generator { .. }) => "Generator".into(),
3737 Some(JsObj::RegExp(_)) => "RegExp".into(),
3738 Some(JsObj::Map { weak, .. }) => if *weak { "WeakMap" } else { "Map" }.into(),
3739 Some(JsObj::Set { weak, .. }) => if *weak { "WeakSet" } else { "Set" }.into(),
3740 Some(JsObj::Promise { .. }) => "Promise".into(),
3741 Some(JsObj::Symbol { .. }) => "Symbol".into(),
3742 Some(JsObj::BigInt(_)) => "BigInt".into(),
3743 // Native-tagged instances brand by their tag; a typed array brands by
3744 // its element kind (`@@kind`), and every Error subclass is `Error`.
3745 Some(JsObj::Object(p)) => match p.get("@@native").map(|t| h.str_of(t)).as_deref() {
3746 Some("TypedArray") => p
3747 .get("@@kind")
3748 .map(|k| h.str_of(k))
3749 .unwrap_or_else(|| "Uint8Array".into()),
3750 Some("Buffer") => "Uint8Array".into(),
3751 // Every native class that really carries a `Symbol.toStringTag`
3752 // in Node brands by its own name. Verified against node v26:
3753 // `Object.prototype.toString.call(new WeakRef({}))` is
3754 // `[object WeakRef]`. The rest of the `@@native` tags
3755 // (`EventEmitter`, `Server`, `Hash`, `Readable`, …) are plain
3756 // classes with NO tag, so they stay `[object Object]` — listing
3757 // them here would invent a brand Node does not have.
3758 Some(
3759 t @ ("ArrayBuffer"
3760 | "DataView"
3761 | "Date"
3762 | "WeakRef"
3763 | "FinalizationRegistry"
3764 | "TextEncoder"
3765 | "TextDecoder"
3766 | "URL"
3767 | "URLSearchParams"),
3768 ) => t.into(),
3769 _ if has_error_data(h, v) => "Error".into(),
3770 _ => "Object".into(),
3771 },
3772 _ => "Object".into(),
3773 },
3774 // node-js only produces the Value variants above; fusevm's shell-oriented
3775 // variants never arise here.
3776 _ => "Object".into(),
3777 };
3778 // Nothing about the value itself brands it. An ordinary object whose CHAIN
3779 // reaches an intrinsic prototype carrying an own `Symbol.toStringTag`
3780 // borrows that one: 20.1.3.6 step 15 is a `Get`, which walks.
3781 // `Object.prototype.toString.call(Object.create(Map.prototype))` is
3782 // `[object Map]` and was `[object Object]`.
3783 //
3784 // Only as a FALLBACK, and only for the prototypes that REALLY carry the
3785 // symbol. A typed array reaches `%TypedArray%.prototype`, whose tag is an
3786 // ACCESSOR returning the specific kind, so consulting the chain FIRST
3787 // branded every view `[object TypedArray]` instead of `[object Uint8Array]`
3788 // — three records caught it. `Error.prototype` carries no tag at all, so
3789 // inheriting from it borrows nothing.
3790 if tag == "Object" && !has_error_data(h, v) {
3791 if let Some(ctor) = chain_tag_ctor(h, v) {
3792 return ctor;
3793 }
3794 }
3795 tag
3796}
3797
3798fn b_setattr(vm: &mut VM, _: u8) -> Value {
3799 let val = vm.pop();
3800 let name = sval(&vm.pop());
3801 let recv = vm.pop();
3802 if let Err(e) = set_property(&recv, &name, val.clone()) {
3803 return abort(vm, e);
3804 }
3805 val
3806}
3807
3808/// `NAMED_EVAL` — SetFunctionName (10.2.9) for a function whose name is only
3809/// known at run time, i.e. one defined under a COMPUTED key: `{ [k]: () => {} }`,
3810/// `class C { static [k] = function(){} }`.
3811///
3812/// The compiler emits this ONLY where the grammar says NamedEvaluation applies
3813/// (`IsAnonymousFunctionDefinition` is a syntactic predicate, not a runtime one:
3814/// `{ m: someAlreadyAnonymousFn }` must NOT be renamed), so the name is set
3815/// unconditionally here.
3816///
3817/// A symbol key becomes `[description]` per step 2 of SetFunctionName; `kind`
3818/// contributes the accessor prefix, so `{ get [k](){} }` is `get <key>`.
3819fn b_named_eval(vm: &mut VM, _: u8) -> Value {
3820 let func = vm.pop();
3821 let kind = vm.pop().to_int();
3822 let key = vm.pop();
3823 let key = sval(&key);
3824 // `@@sym:<id>` / `@@iterator` — an internal symbol key. Step 2: an empty
3825 // description gives the empty name, not `[undefined]`.
3826 let base = match with_host(|h| h.symbol_of_key(&key)) {
3827 Some(sym) => match with_host(|h| h.get(&sym).cloned()) {
3828 Some(JsObj::Symbol {
3829 desc: Some(desc), ..
3830 }) => format!("[{desc}]"),
3831 _ => String::new(),
3832 },
3833 None => key,
3834 };
3835 let name = match kind {
3836 host::member::GET => format!("get {base}"),
3837 host::member::SET => format!("set {base}"),
3838 _ => base,
3839 };
3840 with_host(|h| {
3841 let s = h.new_str(name);
3842 h.set_fn_prop(&func, "name", s);
3843 });
3844 func
3845}
3846
3847/// `[[Set]]` reachable from `crate::proxy`'s no-trap forward, which has to land
3848/// on the same path a plain `o.k = v` takes.
3849pub fn set_property_pub(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3850 set_property(recv, name, val)
3851}
3852
3853/// An object's OWN property as `(value, writable, configurable, is_accessor)`,
3854/// or `None` when it has none. Reads through a Proxy's
3855/// `getOwnPropertyDescriptor` trap, so it answers for any object.
3856pub fn own_prop_facts(obj: &Value, key: &str) -> Option<(Value, bool, bool, bool)> {
3857 let k = with_host(|h| h.new_str(key.to_string()));
3858 let d = own_descriptor_pub(obj, k).ok()?;
3859 if matches!(d, Value::Undef) {
3860 return None;
3861 }
3862 let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
3863 // Each read is hoisted out of the `with_host` borrow: `get_property` takes
3864 // the host itself, so reading inside the closure double-borrows.
3865 let value = field("value");
3866 let writable = field("writable");
3867 let configurable = field("configurable");
3868 let truthy = |v: &Value| with_host(|h| h.truthy(v));
3869 let is_accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
3870 Some((value, truthy(&writable), truthy(&configurable), is_accessor))
3871}
3872
3873/// `OrdinarySetWithOwnDescriptor` (10.1.9.2) with a receiver distinct from the
3874/// object the lookup started on — what `Reflect.set(t, k, v, receiver)` and a
3875/// proxy `set` trap forwarding to it both need.
3876///
3877/// The distinction that matters: an accessor found on `target`'s chain RUNS,
3878/// with `receiver` as `this`; a data property does not write to `target` at all
3879/// but is CREATED on `receiver` through its `[[DefineOwnProperty]]`. Routing
3880/// that second case back through `[[Set]]` made a proxy receiver re-enter its
3881/// own `set` trap forever — the trap body `Reflect.set(t, k, v, recv)` is the
3882/// documented way to forward a write, so the recursion hit every faithful
3883/// handler.
3884pub fn set_with_receiver(
3885 target: &Value,
3886 key: &str,
3887 val: Value,
3888 receiver: &Value,
3889) -> Result<bool, String> {
3890 // A proxy target answers through its own trap, which re-enters here with
3891 // whatever receiver the handler passes on.
3892 if crate::proxy::parts(target).is_some() {
3893 return crate::proxy::set(target, key, &val, receiver);
3894 }
3895 // An accessor anywhere on the target's chain wins, and sees `receiver`.
3896 if let Some((_, setter)) = with_host(|h| host::lookup_accessor(h, target, key)) {
3897 return match setter {
3898 Some(s) => {
3899 host::invoke(&s, vec![val], Some(receiver.clone()))?;
3900 Ok(true)
3901 }
3902 // A getter with no setter refuses the write rather than shadowing it.
3903 None => Ok(false),
3904 };
3905 }
3906 if !with_host(|h| h.can_write_prop(target, key)) {
3907 return Ok(false);
3908 }
3909 // Steps 3.b-3.d: only an object can receive the property, and its OWN
3910 // property decides — an accessor or a read-only slot refuses, and every
3911 // other case defines a plain data property.
3912 //
3913 // `is_object_like`, not a shape test: a string, a symbol and a bigint are
3914 // PRIMITIVES that ride as `Value::Obj` handles here, so the shape check
3915 // passed them through to `defineProperty`, which then threw `called on
3916 // non-object` where 10.1.9.2 step 3.b simply reports `false`.
3917 if !with_host(|h| is_object_like(h, receiver)) {
3918 return Ok(false);
3919 }
3920 if let Some((_, writable, _, is_accessor)) = own_prop_facts(receiver, key) {
3921 if is_accessor || !writable {
3922 return Ok(false);
3923 }
3924 }
3925 // Steps 3.d.iii and 3.e both DEFINE, they do not assign: a setter inherited
3926 // by the receiver must not run, and a proxy receiver must reach its
3927 // `defineProperty` trap rather than its `set` trap.
3928 let desc = with_host(|h| {
3929 let mut m: IndexMap<String, Value> = IndexMap::new();
3930 m.insert("value".into(), val);
3931 m.insert("writable".into(), Value::Bool(true));
3932 m.insert("enumerable".into(), Value::Bool(true));
3933 m.insert("configurable".into(), Value::Bool(true));
3934 h.new_object(m)
3935 });
3936 if crate::proxy::parts(receiver).is_some() {
3937 return crate::proxy::define_property(receiver, key, &desc);
3938 }
3939 let k = with_host(|h| h.new_str(key.to_string()));
3940 define_property_pub(receiver, k, desc)?;
3941 Ok(true)
3942}
3943
3944/// Whether the first argument is a PRIMITIVE — including the three that ride as
3945/// heap handles, which a shape test misses.
3946fn is_primitive_arg(args: &[Value]) -> bool {
3947 let v = arg0(args);
3948 with_host(|h| host::is_primitive(h, &v))
3949}
3950
3951/// The `TypeError` a refused write raises in strict code, worded as V8 does.
3952///
3953/// Adding a key to a non-extensible object reports differently from assigning
3954/// to a read-only one, and the object is named by its brand — `#<Object>` for a
3955/// plain object, `[object Array]` for an array.
3956fn write_refused(recv: &Value, name: &str) -> String {
3957 let extensible = with_host(|h| h.is_extensible(recv));
3958 // Which of the two messages applies turns on whether the key already
3959 // EXISTS. Every shape that keeps its own properties in the fn-prop side
3960 // table answered a blanket `true` here, so adding a key to a frozen
3961 // function reported "read only" where node reports "not extensible".
3962 let has_own = with_host(|h| match h.get(recv) {
3963 Some(JsObj::Object(p)) => p.contains_key(name),
3964 Some(JsObj::Array(items)) => {
3965 name.parse::<usize>()
3966 .map(|i| i < items.len())
3967 .unwrap_or(false)
3968 || h.fn_prop(recv, name).is_some()
3969 }
3970 Some(JsObj::RegExp(_)) => name == "lastIndex" || h.fn_prop(recv, name).is_some(),
3971 _ => h.fn_prop(recv, name).is_some(),
3972 });
3973 if !extensible && !has_own {
3974 return host::type_error(&format!(
3975 "Cannot add property {name}, object is not extensible"
3976 ));
3977 }
3978 // The receiver renders the way every other brand-check message renders one
3979 // — `#<Object>`, `[object Array]`, `[object RegExp]`, `#<Map>`, `#<C>` for a
3980 // class instance, `Error: m` for an error. Only Array was special-cased, so
3981 // every other exotic reported `#<Object>`.
3982 host::type_error(&format!(
3983 "Cannot assign to read only property '{name}' of object '{}'",
3984 no_side_effects_string(recv)
3985 ))
3986}
3987
3988fn set_property(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3989 // 6.2.5.6 `PutValue` begins with `RequireObjectCoercible`: writing any
3990 // property of `undefined` or `null` throws, naming the key. Every such
3991 // write was silently discarded, so `u.x = 1` — the mirror of the single
3992 // most common runtime fault in JS, which the READ side already reports —
3993 // looked like it had succeeded.
3994 if with_host(|h| h.is_nullish(recv)) {
3995 return Err(host::type_error(&format!(
3996 "Cannot set properties of {} (setting '{name}')",
3997 with_host(|h| h.str_of(recv))
3998 )));
3999 }
4000 // A write to a PRIMITIVE receiver has no target — `ToObject` makes a
4001 // throwaway wrapper — so it is discarded in sloppy code and throws in
4002 // strict (10.1.9.2 / 6.2.5.6 again). The refusal was silent in both.
4003 // `is_primitive` rather than a shape test: a string, a symbol and a bigint
4004 // ride as `Value::Obj` handles in this host, so a check for a non-`Obj`
4005 // value caught only numbers and booleans.
4006 if with_host(|h| host::is_primitive(h, recv)) && with_host(|h| h.current_strict()) {
4007 return Err(host::type_error(&format!(
4008 "Cannot create property '{name}' on {} '{}'",
4009 with_host(|h| h.type_of(recv)),
4010 with_host(|h| h.str_of(recv))
4011 )));
4012 }
4013 // `[[PrivateSet]]` (7.3.32) refuses a receiver that carries no such private
4014 // element. The class's own field initializers install theirs directly
4015 // (`host::init_one_field`), so a declaration never reaches this check.
4016 if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
4017 return Err(private_brand_message(name, true));
4018 }
4019 // `[[Set]]` on a Proxy: the handler's `set` trap, or a forward to the target.
4020 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
4021 // A `set` trap that returns falsish REFUSED the write: silent in sloppy
4022 // code, a TypeError in strict, exactly as an ordinary refused write is.
4023 if crate::proxy::set(recv, name, &val, recv)? {
4024 return Ok(());
4025 }
4026 if with_host(|h| h.current_strict()) {
4027 return Err(host::type_error(&format!(
4028 "'set' on proxy: trap returned falsish for property '{name}'"
4029 )));
4030 }
4031 return Ok(());
4032 }
4033 // A Proxy on the PROTOTYPE chain: `OrdinarySet` (10.1.9.2 step 2) hands a
4034 // key the receiver does not own to `parent.[[Set]](P, V, Receiver)`, so the
4035 // proxy's `set` trap runs with the ORIGINAL object as its receiver. The
4036 // write used to land straight on the receiver, and a trap inherited through
4037 // `Object.create(proxy)` never fired. A nearer link owning the key (data or
4038 // accessor) still wins, as `proxy_proto_link` checks.
4039 if name != "__proto__"
4040 && peek(recv, |o| match o {
4041 JsObj::Object(p) => Some(!p.contains_key(name)),
4042 _ => None,
4043 })
4044 .unwrap_or(false)
4045 && !with_host(|h| h.own_accessor(recv, name).is_some())
4046 {
4047 if let Some(link) = proxy_proto_link(recv, name) {
4048 if crate::proxy::set(&link, name, &val, recv)? {
4049 return Ok(());
4050 }
4051 if with_host(|h| h.current_strict()) {
4052 return Err(host::type_error(&format!(
4053 "'set' on proxy: trap returned falsish for property '{name}'"
4054 )));
4055 }
4056 return Ok(());
4057 }
4058 }
4059 // `globalThis.x = 1` creates a real global binding, so the bare `x` reads it
4060 // back. Writing only the own property left the two views disagreeing:
4061 // `globalThis.zz` was 7 while `zz` was still a `ReferenceError`.
4062 if with_host(|h| h.is_global_object(recv)) && !name.starts_with("@@") {
4063 with_host(|h| h.set_name(name, val.clone()));
4064 }
4065 // `obj.__proto__ = p` re-links the prototype — but only for the two values
4066 // the Annex B setter accepts, an Object or `null`. Everything else is a
4067 // silent no-op in Node (`o.__proto__ = 5` leaves `Object.getPrototypeOf(o)`
4068 // untouched and creates no own key), and a null-prototype object inherits
4069 // no such setter at all, so there the assignment is an ORDINARY own
4070 // property write. Re-linking unconditionally made `o.__proto__ = 5` set the
4071 // prototype to the number 5.
4072 if name == "__proto__" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
4073 if with_host(|h| h.has_null_proto(recv)) {
4074 // falls through to the ordinary own-property write below
4075 } else {
4076 let assignable =
4077 with_host(|h| h.is_null(&val) || matches!(h.kind_of(&val), Some(ObjKind::Object)));
4078 if assignable {
4079 // The `__proto__` setter runs `[[SetPrototypeOf]]`, which a
4080 // NON-EXTENSIBLE object refuses — and unlike an ordinary
4081 // refused write, the setter throws in sloppy code too. It was
4082 // rewriting the link of a frozen object.
4083 if would_cycle(recv, &val) {
4084 return Err(host::type_error("Cyclic __proto__ value"));
4085 }
4086 if !with_host(|h| h.is_extensible(recv)) && !same_prototype(recv, &val) {
4087 return Err(host::type_error(&format!(
4088 "{} is not extensible",
4089 no_side_effects_string(recv)
4090 )));
4091 }
4092 with_host(|h| h.set_proto(recv, val));
4093 }
4094 return Ok(());
4095 }
4096 }
4097 // Every environment value is a STRING. `process.env.PORT = 8080` stores
4098 // "8080", so `process.env.PORT + 1` concatenates the way it does in a real
4099 // process; storing the number made it add instead.
4100 if !name.starts_with("@@")
4101 && with_host(
4102 |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4103 )
4104 {
4105 let text = with_host(|h| h.str_of(&val));
4106 // Write THROUGH to the real environment as well. `process.env` is not a
4107 // private map: node applies the change to the process, so a child
4108 // spawned afterwards inherits it. Keeping it only in the JS object meant
4109 // `process.env.NODE_ENV = 'production'` was invisible to every
4110 // `spawnSync`/`execSync` that followed.
4111 std::env::set_var(name, &text);
4112 let sv = with_host(|h| h.new_str(text));
4113 with_host(|h| {
4114 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4115 p.insert(name.to_string(), sv);
4116 }
4117 });
4118 return Ok(());
4119 }
4120 // Assigning `e.stack` wins permanently: drop the not-yet-formatted marker so
4121 // no later read re-derives a header over the top of the assigned value.
4122 if name == "stack" {
4123 with_host(|h| {
4124 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4125 p.shift_remove("@@stackRaw");
4126 }
4127 });
4128 }
4129 // An inherited/own setter accessor intercepts the write. This is checked
4130 // BEFORE the writable test because 10.1.9.2 branches on the descriptor
4131 // kind first: `writable` is a data-property attribute and means nothing on
4132 // an accessor, where the setter alone decides. Testing it first meant an
4133 // accessor defined through `Object.defineProperty` — which leaves
4134 // `writable` false, having no such field — silently swallowed every write
4135 // instead of calling its setter, so the standard clone idiom
4136 // `Object.create(proto, Object.getOwnPropertyDescriptors(src))` produced an
4137 // object whose setters did nothing. An accessor from an object literal
4138 // carries all-true attributes, which is why only the former broke.
4139 if let Some((getter, setter)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
4140 if let Some(setter) = setter {
4141 let _ = host::invoke(&setter, vec![val], Some(recv.clone()));
4142 return Ok(());
4143 }
4144 // Only a getter: the write is refused — silent in sloppy mode, a
4145 // TypeError in strict code. The `return` above matters, since a
4146 // successful setter call must not fall into this.
4147 let _ = getter;
4148 if with_host(|h| h.current_strict()) {
4149 return Err(host::type_error(&format!(
4150 "Cannot set property {name} of #<Object> which has only a getter"
4151 )));
4152 }
4153 return Ok(());
4154 }
4155 // A non-writable property, or a new key on a non-extensible object, refuses
4156 // the write. In SLOPPY mode that is silent; in strict code it is a
4157 // TypeError, and the ASSIGNMENT SITE decides which — not the object. Every
4158 // refusal used to be silent, so `'use strict'` did not catch a write to a
4159 // frozen object, which is most of the reason to freeze one.
4160 if !with_host(|h| h.can_write_prop(recv, name)) {
4161 if with_host(|h| h.current_strict()) {
4162 return Err(write_refused(recv, name));
4163 }
4164 return Ok(());
4165 }
4166 // Writing `name`/`prototype`/statics on a function value.
4167 if matches!(
4168 with_host(|h| h.kind_of(recv)),
4169 Some(ObjKind::Func) | Some(ObjKind::Class)
4170 ) {
4171 with_host(|h| h.set_fn_prop(recv, name, val));
4172 return Ok(());
4173 }
4174 // Writing a static onto a builtin namespace/ctor (`Error.prepareStackTrace`).
4175 // Each bare reference is a fresh `Builtin` handle, so route to the stable
4176 // per-namespace side table rather than the per-index `fn_props`.
4177 if let Some(ns) = peek(recv, |o| match o {
4178 JsObj::Builtin(ns) => Some(ns.clone()),
4179 _ => None,
4180 }) {
4181 // `process.exitCode` is an accessor in Node, not a data property: the
4182 // setter validates and stores the code the process will finally exit
4183 // with. Landing it in the generic static table made it a write-only
4184 // decoration — `process.exitCode = 3` read back as 3 and the process
4185 // still exited 0.
4186 if ns == "process" && name == "exitCode" {
4187 return crate::stdlib::process::set_exit_code(&val);
4188 }
4189 with_host(|h| h.set_builtin_static(&ns, name, val));
4190 return Ok(());
4191 }
4192 // A write onto a REAL intrinsic prototype object (`Object.prototype`,
4193 // `String.prototype`, `TypeError.prototype`) is mirrored into the
4194 // per-namespace side table as well as the object's own map. Instances are
4195 // not linked to these objects by `proto_of` — the chain walk never reaches
4196 // them — so the mirror is what makes `String.prototype.pad = f` visible as
4197 // `"x".pad`. The own-map write below still happens, so reading the
4198 // prototype itself and enumerating it keep working unchanged.
4199 if let Some(ns) = with_host(|h| {
4200 h.intrinsic_proto_ctor(recv)
4201 .map(str::to_string)
4202 .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4203 }) {
4204 with_host(|h| h.set_builtin_static(&format!("{ns}.prototype"), name, val.clone()));
4205 }
4206 // `re.lastIndex = n` on a RegExp advances/resets its match cursor. The
4207 // writability check above already refused it on a FROZEN regexp, which it
4208 // could only do once `integrity_keys` learned that `lastIndex` is an own
4209 // property.
4210 if name == "lastIndex" {
4211 if let Some(n) = with_host(|h| match h.get(recv) {
4212 Some(JsObj::RegExp(_)) => Some(h.to_number(&val)),
4213 _ => None,
4214 }) {
4215 with_host(|h| {
4216 if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
4217 r.last_index = if n.is_finite() && n >= 0.0 {
4218 crate::utf16::U16Index::new(n as usize)
4219 } else {
4220 crate::utf16::U16Index::ZERO
4221 };
4222 }
4223 });
4224 return Ok(());
4225 }
4226 }
4227 // An `arguments` object is an ORDINARY object with a `length` data property,
4228 // not an array: a write PAST the end adds an index and leaves `length`
4229 // alone. The array backing grew it instead, so `f(1)` followed by
4230 // `arguments[1] = 9` reported `arguments.length` as 2.
4231 if let Ok(i) = name.parse::<usize>() {
4232 if is_arguments(recv) && i >= array_len(recv) {
4233 with_host(|h| h.set_fn_prop(recv, name, val));
4234 return Ok(());
4235 }
4236 }
4237 // Typed-array element write (`ta[i] = v`): coerce + store into `@@elems`.
4238 if !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
4239 let is_ta = crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray");
4240 if is_ta && crate::stdlib::typedarray::elem_set(recv, name, &val)? {
4241 return Ok(());
4242 }
4243 // An index write to a view over a DETACHED buffer is DROPPED. Falling
4244 // through would store it as an ordinary own property, which then showed
4245 // up in `getOwnPropertyDescriptor` over a buffer with no bytes.
4246 if is_ta && crate::stdlib::typedarray::view_detached(recv) {
4247 return Ok(());
4248 }
4249 // `buf[i] = n` writes through to the Buffer's hidden byte array.
4250 if crate::stdlib::buffer::byte_set(recv, name, &val) {
4251 return Ok(());
4252 }
4253 }
4254 // Any own property on an exotic with no property map of its own. This sits
4255 // BELOW the exotic-specific writes above, so a RegExp's `lastIndex` still
4256 // moves its match cursor rather than being shadowed by a side-table entry.
4257 if uses_side_table(recv) {
4258 with_host(|h| h.set_fn_prop(recv, name, val));
4259 return Ok(());
4260 }
4261 // An arbitrary own prop on an array (e.g. exec-result `.index`/`.input`).
4262 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array)
4263 && name != "length"
4264 && name.parse::<usize>().is_err()
4265 {
4266 with_host(|h| h.set_fn_prop(recv, name, val));
4267 return Ok(());
4268 }
4269 // `arr.length = n` (10.4.2.4 `ArraySetLength`) validates BEFORE it resizes,
4270 // and does so outside the host borrow because `ToNumber` may run a user
4271 // `valueOf`. An invalid length throws instead of being silently coerced to 0.
4272 let new_len = if name == "length" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array) {
4273 let want = host::to_array_length(&val)?;
4274 // 10.4.2.4 steps 15-17: shrinking deletes from the END downwards and
4275 // STOPS at the first element that cannot be deleted, leaving the length
4276 // just past it. Truncating regardless discarded a non-configurable
4277 // element and reported a length node would not have accepted.
4278 let floor = with_host(|h| {
4279 let old = match h.get(recv) {
4280 Some(JsObj::Array(items)) => items.len(),
4281 _ => 0,
4282 };
4283 let mut stop = want;
4284 for i in (want..old).rev() {
4285 if !h.prop_attrs(recv, &i.to_string()).configurable {
4286 stop = i + 1;
4287 break;
4288 }
4289 }
4290 stop
4291 });
4292 Some(floor.max(want))
4293 } else {
4294 None
4295 };
4296 with_host(|h| match h.get_mut(recv) {
4297 Some(JsObj::Object(props)) => {
4298 // Adding a *new* array-index key must re-place it into ascending
4299 // integer-key order (updating an existing key keeps its position).
4300 let is_new = !props.contains_key(name);
4301 props.insert(name.to_string(), val);
4302 if is_new && host::array_index(name).is_some() {
4303 host::canonicalize_own_keys(props);
4304 }
4305 }
4306 Some(JsObj::Array(items)) => {
4307 if let Some(n) = new_len {
4308 // Growing `length` appends HOLES (`a=[1]; a.length=3` still has
4309 // just the one own key); shrinking drops any hole past the end.
4310 let old = items.len();
4311 items.resize(n, Value::Undef);
4312 if n > old {
4313 h.mark_hole_range(recv, old..n);
4314 } else {
4315 h.truncate_holes(recv, n);
4316 }
4317 } else if let Ok(i) = name.parse::<usize>() {
4318 // A write PAST the end leaves the skipped positions elided.
4319 let old = items.len();
4320 if i >= old {
4321 items.resize(i + 1, Value::Undef);
4322 }
4323 items[i] = val;
4324 if i > old {
4325 h.mark_hole_range(recv, old..i);
4326 }
4327 // …and the written index itself is no longer one. This is the
4328 // single site that keeps a hole record from outliving the
4329 // elision it describes: every array element write in the
4330 // language reaches it.
4331 h.clear_hole(recv, i);
4332 }
4333 }
4334 _ => {}
4335 });
4336 Ok(())
4337}
4338
4339fn b_getitem(vm: &mut VM, _: u8) -> Value {
4340 let idx = vm.pop();
4341 let recv = vm.pop();
4342 let key = match host::to_property_key(&idx) {
4343 Ok(k) => k,
4344 Err(e) => return abort(vm, e),
4345 };
4346 match get_property(&recv, &key) {
4347 Ok(v) => v,
4348 Err(e) => abort(vm, e),
4349 }
4350}
4351
4352fn b_setitem(vm: &mut VM, _: u8) -> Value {
4353 let val = vm.pop();
4354 let idx = vm.pop();
4355 let recv = vm.pop();
4356 let key = match host::to_property_key(&idx) {
4357 Ok(k) => k,
4358 Err(e) => return abort(vm, e),
4359 };
4360 if let Err(e) = set_property(&recv, &key, val.clone()) {
4361 return abort(vm, e);
4362 }
4363 val
4364}
4365
4366/// `[[Delete]]` (10.1.10) for an already-resolved property key: the one place
4367/// `delete o[k]`, `delete o.k` and `Reflect.deleteProperty` all go through, so
4368/// the three cannot drift. Reports `false` for a non-configurable property
4369/// (sloppy mode ignores the failure rather than throwing) and `true` otherwise,
4370/// which is also what deleting an absent key reports.
4371pub fn delete_property(recv: &Value, key: &str) -> Result<bool, String> {
4372 // 13.5.1.2 step 5 runs `ToObject` on the base, which a nullish one refuses.
4373 // `delete u.x` reported success instead.
4374 if with_host(|h| h.is_nullish(recv)) {
4375 return Err(host::type_error(
4376 "Cannot convert undefined or null to object",
4377 ));
4378 }
4379 // `[[Delete]]` on a Proxy runs the handler's `deleteProperty` trap, which may
4380 // throw — the reason this reports a `Result` rather than a bare `bool`.
4381 if let Some(b) = crate::proxy::delete(recv, key)? {
4382 return Ok(b);
4383 }
4384 // `delete globalThis.x` removes a global a script created. It lives in the
4385 // globals map, not the object's property map, so the ordinary path reported
4386 // success and removed nothing — the binding stayed readable afterwards.
4387 if with_host(|h| h.is_global_object(recv)) && with_host(|h| h.remove_global(key)) {
4388 return Ok(true);
4389 }
4390 // `delete require.cache[id]` drops the module so the next `require` of that
4391 // file runs it again — the whole point of exposing the cache.
4392 if peek(recv, |o| match o {
4393 JsObj::Builtin(ns) => Some(ns == REQUIRE_CACHE),
4394 _ => None,
4395 }) == Some(true)
4396 {
4397 return Ok(crate::module::cache_delete(key));
4398 }
4399 // `delete process.env.X` unsets the variable in the PROCESS, not just in the
4400 // JS view, so a child spawned afterwards no longer sees it.
4401 if !key.starts_with("@@")
4402 && with_host(
4403 |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4404 )
4405 {
4406 std::env::remove_var(key);
4407 }
4408 // A member of a builtin NAMESPACE (`Math.PI`, `Number.MAX_VALUE`,
4409 // `Object.prototype`) is non-configurable when it is a constant or a
4410 // constructor's `prototype`, and `delete` of one answers false without
4411 // removing anything. There is no property map behind a namespace, so the
4412 // ordinary attribute lookup below cannot tell — it reported success for
4413 // every one of them.
4414 // A REAL intrinsic prototype object carries the write in its own map AND in
4415 // the side table the instance read consults, so the delete has to clear
4416 // both. Clearing only the map left `Object.prototype.patch` deleted as far
4417 // as the prototype was concerned and still inherited by every object.
4418 if let Some(ns) = with_host(|h| {
4419 h.intrinsic_proto_ctor(recv)
4420 .map(str::to_string)
4421 .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4422 }) {
4423 with_host(|h| h.remove_builtin_static(&format!("{ns}.prototype"), key));
4424 }
4425 if let Some(ns) = peek(recv, |o| match o {
4426 JsObj::Builtin(ns) => Some(ns.clone()),
4427 _ => None,
4428 }) {
4429 // A script-assigned static is an ordinary configurable property and is
4430 // removed from the side table the assignment landed in. Falling through
4431 // to the attribute check below answered true and deleted nothing, so a
4432 // patch survived its own `delete`.
4433 if with_host(|h| h.remove_builtin_static(&ns, key)) {
4434 return Ok(true);
4435 }
4436 if ns != REQUIRE_CACHE && !builtin_member_configurable(&ns, key) {
4437 return Ok(false);
4438 }
4439 }
4440 if !with_host(|h| h.prop_attrs(recv, key).configurable) {
4441 return Ok(false);
4442 }
4443 // An accessor lives in its own table, not the property map, so removing it
4444 // has to be explicit — otherwise `delete` reported success while the getter
4445 // kept answering and `in` kept reporting the key.
4446 if with_host(|h| h.own_accessor(recv, key).is_some()) {
4447 with_host(|h| h.remove_accessor(recv, key));
4448 return Ok(true);
4449 }
4450 with_host(|h| {
4451 let index = key.parse::<usize>();
4452 match h.get_mut(recv) {
4453 Some(JsObj::Object(props)) => {
4454 props.shift_remove(key);
4455 return;
4456 }
4457 Some(JsObj::Array(items)) => {
4458 if let Ok(i) = index {
4459 if i < items.len() {
4460 // `delete a[i]` punches a HOLE: the length is unchanged
4461 // but the index stops being an own property.
4462 items[i] = Value::Undef;
4463 h.mark_hole(recv, i);
4464 }
4465 return;
4466 }
4467 }
4468 _ => {}
4469 }
4470 // A non-index key on an array (`arr.foo`, `arr[sym]`), or any own key on
4471 // a function/class, is an ordinary own property kept in the side table.
4472 h.remove_fn_prop(recv, key);
4473 });
4474 Ok(true)
4475}
4476
4477fn b_delitem(vm: &mut VM, _: u8) -> Value {
4478 let strict = vm.pop();
4479 let idx = vm.pop();
4480 let recv = vm.pop();
4481 // `delete o[k]` keys through ToPropertyKey (7.1.19), exactly as the read and
4482 // the write do: `String(k)` would turn a Symbol into its `Symbol(desc)`
4483 // description and delete a key nothing ever wrote.
4484 let key = match host::to_property_key(&idx) {
4485 Ok(k) => k,
4486 Err(e) => return abort(vm, e),
4487 };
4488 match delete_property(&recv, &key) {
4489 Ok(false) if with_host(|h| h.truthy(&strict)) => {
4490 abort(vm, refused_delete_error(&recv, &key))
4491 }
4492 Ok(b) => Value::Bool(b),
4493 Err(e) => abort(vm, e),
4494 }
4495}
4496
4497fn b_delprop_name(vm: &mut VM, _: u8) -> Value {
4498 let strict = vm.pop();
4499 let name = sval(&vm.pop());
4500 let recv = vm.pop();
4501 match delete_property(&recv, &name) {
4502 Ok(false) if with_host(|h| h.truthy(&strict)) => {
4503 abort(vm, refused_delete_error(&recv, &name))
4504 }
4505 Ok(b) => Value::Bool(b),
4506 Err(e) => abort(vm, e),
4507 }
4508}
4509
4510/// The TypeError a STRICT `delete` of a non-configurable property raises. The
4511/// receiver renders the way every other brand-check message renders one.
4512fn refused_delete_error(recv: &Value, key: &str) -> String {
4513 // A PROXY names the trap that refused. Only the `delete` OPERATOR reports
4514 // it; `Reflect.deleteProperty` answers `false`, which is why this lives
4515 // here rather than in the shared `[[Delete]]`.
4516 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
4517 return host::type_error(&format!(
4518 "'deleteProperty' on proxy: trap returned falsish for property '{key}'"
4519 ));
4520 }
4521 // A non-callable builtin NAMESPACE renders as a plain object here — node
4522 // reports `#<Object>` for `Math`, not its `[object Math]` brand.
4523 let shown = match peek(recv, |o| match o {
4524 JsObj::Builtin(ns) => Some(ns.clone()),
4525 _ => None,
4526 }) {
4527 Some(ns) if !host::builtin_is_callable(&ns) => "#<Object>".to_string(),
4528 _ => no_side_effects_string(recv),
4529 };
4530 host::type_error(&format!("Cannot delete property '{key}' of {shown}"))
4531}
4532
4533// ── constructors ──────────────────────────────────────────────────────────────
4534
4535fn b_mkstr(vm: &mut VM, argc: u8) -> Value {
4536 let parts = pop_n(vm, argc as usize);
4537 let s: String = with_host(|h| parts.iter().map(|p| h.str_of(p)).collect());
4538 with_host(|h| h.new_str(s))
4539}
4540
4541fn b_mkarr(vm: &mut VM, argc: u8) -> Value {
4542 let items = pop_n(vm, argc as usize);
4543 with_host(|h| h.new_array(items))
4544}
4545
4546/// `MARK_HOLE [arr, index]`: record `arr[index]` as an ELIDED element. Emitted
4547/// only for an array literal that actually contains an elision, so a dense
4548/// literal costs nothing. Returns `undefined`; the array stays on the stack
4549/// underneath (the compiler `Dup`s it).
4550fn b_mark_hole(vm: &mut VM, _: u8) -> Value {
4551 let idx = vm.pop();
4552 let arr = vm.pop();
4553 let i = match idx {
4554 Value::Int(i) if i >= 0 => i as usize,
4555 _ => return Value::Undef,
4556 };
4557 with_host(|h| h.mark_hole(&arr, i));
4558 Value::Undef
4559}
4560
4561fn b_mkobj(vm: &mut VM, argc: u8) -> Value {
4562 let flat = pop_n(vm, argc as usize);
4563 let mut props: IndexMap<String, Value> = IndexMap::new();
4564 // A literal `__proto__: x` key sets the object's prototype (not an own prop).
4565 let mut proto_override: Option<Value> = None;
4566 let mut method_keys: Vec<String> = Vec::new();
4567 let mut i = 0;
4568 while i + 2 < flat.len() || (i + 2 == flat.len() && flat.len() % 3 == 0 && i < flat.len()) {
4569 if i + 2 >= flat.len() {
4570 break;
4571 }
4572 // Tag 2: an ACCESSOR's position. An accessor lives in its own table, so
4573 // the literal reserves its slot here with the `@@ord:` marker key that
4574 // `own_enum_data_keys` resolves back — otherwise `{ get g(){}, d: 2 }`
4575 // enumerated `d, g`, because `DEF_ACCESSOR` runs after `MKOBJ` and its
4576 // marker landed at the end.
4577 if matches!(flat[i], Value::Int(2)) {
4578 let key = with_host(|h| h.str_of(&flat[i + 1]));
4579 props
4580 .entry(format!("{}{key}", host::ORD_MARKER))
4581 .or_insert(Value::Undef);
4582 i += 3;
4583 continue;
4584 }
4585 // Tag 3: a METHOD DEFINITION — an ordinary property whose key is also
4586 // recorded so the literal can become its `[[HomeObject]]` below.
4587 if matches!(flat[i], Value::Int(3)) {
4588 let key = with_host(|h| h.str_of(&flat[i + 1]));
4589 method_keys.push(key.clone());
4590 props.insert(key, flat[i + 2].clone());
4591 i += 3;
4592 continue;
4593 }
4594 let spread = matches!(flat[i], Value::Int(1));
4595 if spread {
4596 let src = flat[i + 1].clone();
4597 // A STRING source spreads its index properties (`{..."ab"}` is
4598 // `{0:'a',1:'b'}`): CopyDataProperties (7.3.25) calls ToObject, and a
4599 // String exotic object owns one enumerable property per UTF-16 code
4600 // UNIT (10.4.3). `own_enum_entries_deep` only walks heap objects, so
4601 // a string source contributed nothing and `{..."ab"}` was `{}`.
4602 // Every other primitive (number/boolean/symbol) boxes to an object
4603 // with no own enumerable properties, and null/undefined are ignored,
4604 // so those correctly stay no-ops on the path below.
4605 if let Some(s) = with_host(|h| h.as_str(&src)) {
4606 for idx in 0..crate::utf16::len(&s) {
4607 if let Ok(ch) = get_property(&src, &idx.to_string()) {
4608 props.insert(idx.to_string(), ch);
4609 }
4610 }
4611 i += 3;
4612 continue;
4613 }
4614 // Object spread copies own *enumerable* properties only — never the
4615 // hidden `@@…` slots (copying `@@native` used to turn `{...buf}`
4616 // into something that still claimed to be a Buffer) and never a
4617 // property a descriptor marked non-enumerable.
4618 // A getter that throws during spread propagates as a thrown value,
4619 // which in the VM means aborting the frame.
4620 let entries = match host::own_enum_entries_deep(&src) {
4621 Ok(e) => e,
4622 Err(e) => return abort(vm, e),
4623 };
4624 for (k, v) in entries {
4625 props.insert(k, v);
4626 }
4627 // `CopyDataProperties` (7.3.25) copies own enumerable SYMBOL keys
4628 // too — only `Object.keys`/`for-in`/`JSON.stringify` skip them.
4629 for (k, v) in with_host(|h| h.own_symbol_entries(&src)) {
4630 props.insert(k, v);
4631 }
4632 } else {
4633 let key = with_host(|h| h.str_of(&flat[i + 1]));
4634 if key == "__proto__" {
4635 proto_override = Some(flat[i + 2].clone());
4636 } else {
4637 props.insert(key, flat[i + 2].clone());
4638 }
4639 }
4640 i += 3;
4641 }
4642 with_host(|h| {
4643 let o = h.new_object(props);
4644 if let Some(p) = proto_override {
4645 if matches!(p, Value::Obj(_)) {
4646 h.set_proto(&o, p);
4647 }
4648 }
4649 // A method DEFINED here takes the literal as its `[[HomeObject]]`, which
4650 // is what `super` inside it resolves through. The home object is fixed
4651 // at definition, so a method that merely arrives as a value
4652 // (`{ m: other.m }`) keeps the one it was defined with — stamping every
4653 // method-valued property instead rebound the original and changed what
4654 // IT resolved.
4655 for key in &method_keys {
4656 let m = match h.get(&o) {
4657 Some(JsObj::Object(p)) => p.get(key).cloned(),
4658 _ => None,
4659 };
4660 if let Some(m) = m {
4661 if let Some(JsObj::Func(f)) = h.get_mut(&m) {
4662 f.home_object = Some(o.clone());
4663 }
4664 }
4665 }
4666 o
4667 })
4668}
4669
4670fn b_mkfunc(vm: &mut VM, _: u8) -> Value {
4671 let def_id = match vm.pop() {
4672 Value::Int(n) => n as usize,
4673 Value::Float(f) => f as usize,
4674 _ => return abort(vm, "internal: MKFUNC id".into()),
4675 };
4676 let (is_arrow, self_name) = with_host(|h| match h.funcs.get(def_id) {
4677 Some(d) => (
4678 d.is_arrow,
4679 (d.self_name && !d.name.is_empty()).then(|| d.name.clone()),
4680 ),
4681 None => (false, None),
4682 });
4683 with_host(|h| {
4684 let mut env = h.current_env_capture();
4685 let this = h.current_this();
4686 // An arrow has no `super` of its own: it uses the enclosing METHOD's,
4687 // exactly as it uses the enclosing `this`. Nothing was captured, so
4688 // `super.m()` inside an arrow reported the method missing — in a class
4689 // method as well as an object literal.
4690 let (home_class, home_static, home_object) = if is_arrow {
4691 h.current_home()
4692 } else {
4693 (None, false, None)
4694 };
4695 // A named function expression closes over an extra scope holding its own
4696 // name, so the body can recurse through it (`function f(){ … f() … }`)
4697 // independently of whatever the outer binding is later set to.
4698 if self_name.is_some() {
4699 env = host::child_env(env);
4700 }
4701 let f = h.alloc(JsObj::Func(FuncVal {
4702 def_id,
4703 env: Some(env.clone()),
4704 this,
4705 is_arrow,
4706 home_class,
4707 home_static,
4708 home_object,
4709 }));
4710 if let Some(n) = self_name {
4711 env.borrow_mut().vars.insert(n, f.clone());
4712 }
4713 f
4714 })
4715}
4716
4717// ── truthiness / coercion / equality ──────────────────────────────────────────
4718
4719fn b_truthy(vm: &mut VM, _: u8) -> Value {
4720 let v = vm.pop();
4721 Value::Bool(with_host(|h| h.truthy(&v)))
4722}
4723
4724fn b_nullish(vm: &mut VM, _: u8) -> Value {
4725 let v = vm.pop();
4726 Value::Bool(with_host(|h| h.is_nullish(&v)))
4727}
4728
4729fn b_tostr(vm: &mut VM, _: u8) -> Value {
4730 let v = vm.pop();
4731 // ToString with user-`toString`/`valueOf` dispatch (template interpolation,
4732 // `String(x)`, object keys).
4733 match host::to_string_value(&v) {
4734 Ok(s) => s,
4735 Err(e) => abort(vm, e),
4736 }
4737}
4738
4739fn b_typeof(vm: &mut VM, _: u8) -> Value {
4740 let v = vm.pop();
4741 with_host(|h| {
4742 let t = h.type_of(&v);
4743 h.new_str(t)
4744 })
4745}
4746
4747/// `typeof <bare ident>`: read the name like `b_getlocal` but return "undefined"
4748/// (never a ReferenceError) when the name is unbound — JS `typeof` semantics.
4749fn b_typeof_name(vm: &mut VM, _: u8) -> Value {
4750 let name = sval(&vm.pop());
4751 // `typeof` does NOT excuse the temporal dead zone: it answers "undefined"
4752 // for an UNBOUND name, but a `let` above its declaration is bound and
4753 // throws. Reading the marker's type answered "function".
4754 if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
4755 return abort(vm, host::tdz_error(&name));
4756 }
4757 if let Some(v) = with_host(|h| h.read_name(&name)) {
4758 if with_host(|h| h.is_tdz(&v)) {
4759 return abort(vm, host::tdz_error(&name));
4760 }
4761 }
4762 // Bound name (user variable) → typeof its value.
4763 if let Some(v) = with_host(|h| h.read_name(&name)) {
4764 return with_host(|h| {
4765 let t = h.type_of(&v);
4766 h.new_str(t)
4767 });
4768 }
4769 // Lazily-bound globals mirror `b_getlocal`: resolve to the same value it
4770 // would produce, then take its type (so object-namespaces like `console`/
4771 // `Math`/`JSON`/`process` report "object", constructors report "function").
4772 let t = match name.as_str() {
4773 "undefined" => "undefined".to_string(),
4774 "NaN" | "Infinity" => "number".to_string(),
4775 "globalThis" | "global" => "object".to_string(),
4776 n if is_namespace(n) || is_known_builtin(n) => {
4777 let v = with_host(|h| h.alloc(JsObj::Builtin(name.clone())));
4778 with_host(|h| h.type_of(&v)).to_string()
4779 }
4780 _ => "undefined".to_string(), // genuinely unbound → JS returns "undefined"
4781 };
4782 with_host(|h| h.new_str(t))
4783}
4784
4785fn b_strict_eq(vm: &mut VM, _: u8) -> Value {
4786 let b = vm.pop();
4787 let a = vm.pop();
4788 Value::Bool(with_host(|h| h.strict_eq(&a, &b)))
4789}
4790
4791fn b_loose_eq(vm: &mut VM, _: u8) -> Value {
4792 let b = vm.pop();
4793 let a = vm.pop();
4794 // Abstract Equality steps 10-11 (7.2.15): object ⇄ primitive converts the
4795 // object with `ToPrimitive` — a JS `valueOf`/`Symbol.toPrimitive` call, so it
4796 // runs before the host borrow. Object ⇄ object stays a reference check.
4797 let (a, b) = match with_host(|h| (host::is_primitive(h, &a), host::is_primitive(h, &b))) {
4798 (false, true) if coerces_against_object(&b) => match host::to_primitive(&a, "default") {
4799 Ok(p) => (p, b),
4800 Err(e) => return abort(vm, e),
4801 },
4802 (true, false) if coerces_against_object(&a) => match host::to_primitive(&b, "default") {
4803 Ok(p) => (a, p),
4804 Err(e) => return abort(vm, e),
4805 },
4806 _ => (a, b),
4807 };
4808 Value::Bool(with_host(|h| h.loose_eq(&a, &b)))
4809}
4810
4811fn b_instanceof(vm: &mut VM, _: u8) -> Value {
4812 let ctor = vm.pop();
4813 let obj = vm.pop();
4814 match host::instance_of(&obj, &ctor) {
4815 Ok(b) => Value::Bool(b),
4816 Err(e) => abort(vm, e),
4817 }
4818}
4819
4820// ── bitwise / unary ───────────────────────────────────────────────────────────
4821
4822fn b_binop(vm: &mut VM, _: u8) -> Value {
4823 let b = vm.pop();
4824 let a = vm.pop();
4825 let tag = match vm.pop() {
4826 Value::Int(n) => n,
4827 _ => 0,
4828 };
4829 // Both operands are ToPrimitive-d with the number hint before ToInt32
4830 // (ECMA-262 13.12.1), which has to happen outside the host borrow.
4831 let r = host::to_primitive(&a, "number")
4832 .and_then(|a| host::to_primitive(&b, "number").map(|b| (a, b)))
4833 .and_then(|(a, b)| with_host(|h| h.bitwise(tag, &a, &b)));
4834 finish(vm, r)
4835}
4836
4837fn b_unary(vm: &mut VM, _: u8) -> Value {
4838 let v = vm.pop();
4839 let tag = match vm.pop() {
4840 Value::Int(n) => n,
4841 _ => 0,
4842 };
4843 // Unary `+`/`~` on a BigInt: `+` is a hard TypeError in JS; `~x` is `-x - 1`
4844 // computed in arbitrary precision.
4845 if with_host(|h| h.is_bigint_val(&v)) {
4846 return match tag {
4847 host::unop::POS => abort(
4848 vm,
4849 host::type_error("Cannot convert a BigInt value to a number"),
4850 ),
4851 host::unop::BITNOT => {
4852 let b = with_host(|h| h.as_bigint(&v)).unwrap();
4853 let r = -(b + num_bigint::BigInt::from(1));
4854 with_host(|h| h.new_bigint(r))
4855 }
4856 _ => Value::Undef,
4857 };
4858 }
4859 // `ToNumber` outside the host borrow: an object operand's `valueOf` /
4860 // `Symbol.toPrimitive` is a JS call, so it cannot run under `with_host`.
4861 let n = match host::to_number_value(&v) {
4862 Ok(n) => n,
4863 Err(e) => return abort(vm, e),
4864 };
4865 match tag {
4866 host::unop::POS => Value::Float(n),
4867 host::unop::BITNOT => {
4868 let i = if n.is_finite() {
4869 n.trunc() as i64 as i32
4870 } else {
4871 0
4872 };
4873 Value::Float(!i as f64)
4874 }
4875 _ => Value::Undef,
4876 }
4877}
4878
4879// ── membership ────────────────────────────────────────────────────────────────
4880
4881fn b_contains(vm: &mut VM, _: u8) -> Value {
4882 let container = vm.pop();
4883 let key = vm.pop();
4884 // `x in y` requires y to be an object. V8 names both operands:
4885 // `Cannot use 'in' operator to search for 'a' in 5`.
4886 // A heap-backed PRIMITIVE — a string, a symbol, a bigint — is a
4887 // `Value::Obj` in this host but is not an object, so the shape test alone
4888 // let `'length' in 'ab'` and `'description' in Symbol('x')` answer `true`
4889 // where node throws. `is_primitive` is the same predicate `ToObject` and
4890 // `typeof` use, so the three cannot disagree about what an object is.
4891 if !matches!(container, Value::Obj(_)) || with_host(|h| host::is_primitive(h, &container)) {
4892 let (k, c) = with_host(|h| (h.property_key(&key), h.str_of(&container)));
4893 return abort(
4894 vm,
4895 host::type_error(&format!(
4896 "Cannot use 'in' operator to search for '{k}' in {c}"
4897 )),
4898 );
4899 }
4900 let k = match host::to_property_key(&key) {
4901 Ok(k) => k,
4902 Err(e) => return abort(vm, e),
4903 };
4904 match has_property(&container, &k) {
4905 Ok(b) => Value::Bool(b),
4906 Err(e) => abort(vm, e),
4907 }
4908}
4909
4910// ── control ───────────────────────────────────────────────────────────────────
4911
4912fn b_sig_return(vm: &mut VM, _: u8) -> Value {
4913 let v = vm.pop();
4914 with_host(|h| h.signal = Some(host::Signal::Return(v.clone())));
4915 vm.ip = vm.chunk.ops.len();
4916 v
4917}
4918
4919/// `break [label]` whose target loop lives in an enclosing chunk (the statement is
4920/// inside a `try` block, which the host runs as its own chunk). Raise the signal
4921/// and halt this chunk; `SIG_UNWIND` after the `TRY` op re-dispatches it.
4922fn b_sig_break(vm: &mut VM, _: u8) -> Value {
4923 let label = sval(&vm.pop());
4924 let label = (!label.is_empty()).then_some(label);
4925 with_host(|h| h.signal = Some(host::Signal::Break(label)));
4926 vm.ip = vm.chunk.ops.len();
4927 Value::Undef
4928}
4929
4930/// `continue [label]` out of a `try` block — see [`b_sig_break`].
4931fn b_sig_continue(vm: &mut VM, _: u8) -> Value {
4932 let label = sval(&vm.pop());
4933 let label = (!label.is_empty()).then_some(label);
4934 with_host(|h| h.signal = Some(host::Signal::Continue(label)));
4935 vm.ip = vm.chunk.ops.len();
4936 Value::Undef
4937}
4938
4939/// Dispatch a pending control signal at the instruction after a `TRY`. `tag`
4940/// describes what the `try` is nested in (see [`host::unwind`]):
4941///
4942/// * no signal → `NONE`, execution continues normally;
4943/// * `Return`, or no enclosing loop in this chunk → halt the chunk so the signal
4944/// keeps travelling outward;
4945/// * `break`/`continue` targeting the enclosing loop → consume it and report
4946/// `BREAK`/`CONTINUE` so the compiler-emitted jump lands on the loop's exit /
4947/// continue target;
4948/// * a LABELED `break`/`continue` for some outer loop → report `BREAK` but leave
4949/// the signal pending, so leaving this loop re-dispatches it one level out.
4950fn b_sig_unwind(vm: &mut VM, _: u8) -> Value {
4951 let cont_tag = sval(&vm.pop());
4952 let brk_tag = sval(&vm.pop());
4953 let sig = match with_host(|h| h.signal.clone()) {
4954 Some(s) => s,
4955 None => return Value::Int(host::unwind::NONE),
4956 };
4957 // Nothing in this chunk can catch a `break`: halt so the signal keeps going.
4958 let propagate = |vm: &mut VM| {
4959 vm.ip = vm.chunk.ops.len();
4960 Value::Int(host::unwind::NONE)
4961 };
4962 match &sig {
4963 host::Signal::Return(_) => propagate(vm),
4964 host::Signal::Break(label) => {
4965 if brk_tag == host::unwind::NO_LOOP {
4966 return propagate(vm);
4967 }
4968 let mine = match label {
4969 None => true, // unlabeled: always the innermost enclosing context
4970 Some(l) => brk_tag == *l,
4971 };
4972 if mine {
4973 with_host(|h| h.signal = None);
4974 }
4975 // Not ours: still leave this context by its break exit, keeping the
4976 // signal pending for the next dispatch point one level out.
4977 Value::Int(host::unwind::BREAK)
4978 }
4979 host::Signal::Continue(label) => {
4980 let mine = match label {
4981 // Unlabeled `continue` binds to the innermost continue-catching
4982 // loop — which a `switch` between here and it is NOT.
4983 None => cont_tag != host::unwind::NO_LOOP,
4984 Some(l) => cont_tag == *l,
4985 };
4986 if mine {
4987 with_host(|h| h.signal = None);
4988 return Value::Int(host::unwind::CONTINUE);
4989 }
4990 if brk_tag == host::unwind::NO_LOOP {
4991 return propagate(vm);
4992 }
4993 // The target loop is further out: exit the innermost context here and
4994 // re-dispatch there.
4995 Value::Int(host::unwind::BREAK)
4996 }
4997 }
4998}
4999
5000fn b_throw(vm: &mut VM, _: u8) -> Value {
5001 let v = vm.pop();
5002 let msg = with_host(|h| {
5003 h.exc = Some(v.clone());
5004 // Prefer an error object's message for the top-level report.
5005 error_display(h, &v)
5006 });
5007 abort(vm, msg)
5008}
5009
5010fn error_display(h: &host::JsHost, v: &Value) -> String {
5011 if let Some(JsObj::Object(props)) = h.get(v) {
5012 let name = props
5013 .get("name")
5014 .map(|x| h.str_of(x))
5015 .unwrap_or_else(|| "Error".into());
5016 if let Some(m) = props.get("message") {
5017 return format!("Uncaught {name}: {}", h.str_of(m));
5018 }
5019 }
5020 format!("Uncaught {}", h.str_of(v))
5021}
5022
5023fn b_try(vm: &mut VM, _: u8) -> Value {
5024 let id = match vm.pop() {
5025 Value::Int(n) => n as usize,
5026 _ => return abort(vm, "internal: TRY id".into()),
5027 };
5028 // Shape only. Running a `try` used to clone the whole `TryDef` — its block,
5029 // its handler and its finalizer bytecode — every time control entered it,
5030 // which for a `try` inside a loop is once per iteration.
5031 let (has_handler, catch_bind, has_finalizer) = match with_host(|h| h.try_shape(id)) {
5032 Some(t) => t,
5033 None => return abort(vm, "internal: unknown try id".into()),
5034 };
5035 let mut pending: Option<String> = None;
5036 // Each sub-block runs as its own chunk on THIS frame, so a throw part-way
5037 // through can leave block scopes open. Snapshot the scope and restore it
5038 // before the handler and after the whole statement.
5039 let scope = with_host(|h| h.scope_snapshot());
5040
5041 with_host(|h| h.push_scope()); // the try block is its own block scope
5042 let body_res = host::run_chunk_keyed(host::try_key(id, 0), || {
5043 with_host(|h| h.try_chunk(id, 0)).expect("try block exists")
5044 });
5045 with_host(|h| h.restore_scope(scope.clone()));
5046 let signal_after = with_host(|h| h.signal.is_some());
5047 if let Err(e) = body_res {
5048 if signal_after {
5049 pending = Some(e);
5050 } else if has_handler {
5051 // Bind the thrown value (or a synthesized error) to the catch param.
5052 let thrown =
5053 with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
5054 with_host(|h| {
5055 h.error = None;
5056 h.exc = None;
5057 });
5058 // The catch parameter is block-scoped to the handler.
5059 with_host(|h| h.push_scope());
5060 if let Some(name) = &catch_bind {
5061 with_host(|h| h.declare_name(name, thrown));
5062 }
5063 let hres = host::run_chunk_keyed(host::try_key(id, 1), || {
5064 with_host(|h| h.try_chunk(id, 1)).expect("handler exists")
5065 });
5066 with_host(|h| h.restore_scope(scope.clone()));
5067 if let Err(e2) = hres {
5068 pending = Some(e2);
5069 }
5070 } else {
5071 pending = Some(e);
5072 }
5073 }
5074
5075 // finally always runs; a finally error/signal supersedes.
5076 if has_finalizer {
5077 let sig_before = with_host(|h| h.signal.take());
5078 with_host(|h| h.push_scope()); // ditto for `finally`
5079 let fres = host::run_chunk_keyed(host::try_key(id, 2), || {
5080 with_host(|h| h.try_chunk(id, 2)).expect("finalizer exists")
5081 });
5082 with_host(|h| h.restore_scope(scope.clone()));
5083 match fres {
5084 Ok(_) => {
5085 if with_host(|h| h.signal.is_none()) {
5086 // The finalizer completed normally: the try/catch block's own
5087 // abrupt completion resumes.
5088 with_host(|h| h.signal = sig_before);
5089 } else {
5090 // ECMA-262 14.15.3 TryStatement evaluation: when the finalizer's
5091 // completion is abrupt (`return`/`break`/`continue` inside
5092 // `finally`), that completion REPLACES the try/catch block's —
5093 // including a pending throw, which is discarded, not rethrown.
5094 pending = None;
5095 with_host(|h| {
5096 h.error = None;
5097 h.exc = None;
5098 });
5099 }
5100 }
5101 Err(e) => pending = Some(e),
5102 }
5103 }
5104
5105 if let Some(e) = pending {
5106 return abort(vm, e);
5107 }
5108 Value::Undef
5109}
5110
5111/// Synthesize an `Error`-shaped object from an internal error string, linked to
5112/// the matching builtin error prototype so `instanceof`/`.constructor` work.
5113pub(crate) fn synth_error(h: &mut host::JsHost, e: &str) -> Value {
5114 h.ensure_error_protos();
5115 // A `DOMException` marker: the WHATWG error NAME rides in the string, since
5116 // it is not one of the ECMAScript error classes below.
5117 if let Some(rest) = e.strip_prefix(host::DOM_MARK) {
5118 if let Some((name, msg)) = rest.split_once('\u{1}') {
5119 return dom_exception_with(h, name, msg);
5120 }
5121 }
5122 // A `Name [ERR_CODE]: message` head carries a Node error `code` next to the
5123 // error class, exactly as Node's internal errors render it in `.stack`.
5124 let (head, rest) = match e.split_once(": ") {
5125 Some((n, m)) => (n, m.to_string()),
5126 None => ("", e.to_string()),
5127 };
5128 let (base, code) = match head.split_once(" [") {
5129 Some((n, c)) if c.ends_with(']') => (n, Some(c[..c.len() - 1].to_string())),
5130 _ => (head, None),
5131 };
5132 let (name, mut message) = if host::ERROR_NAMES.contains(&base) {
5133 (base.to_string(), rest)
5134 } else {
5135 ("Error".to_string(), e.to_string())
5136 };
5137 // A `host::plain_coded_error` marker: the code rides at the head of the
5138 // MESSAGE rather than in the class, because Node's native-layer errors set
5139 // `.code` while leaving `String(err)` unbracketed (`TypeError: Invalid URL`
5140 // with `code === 'ERR_INVALID_URL'`). Strip it back off here — the marker is
5141 // internal and must never reach a user-visible `.message`.
5142 let mut code = code;
5143 // Whether `String(err)`/`err.stack` show `Name [CODE]:` — true for the
5144 // bracketed head, false for the marker form.
5145 let mut bracketed = code.is_some();
5146 // Extra own properties (`input`, `base`) from `host::plain_coded_error_with`.
5147 let mut fields: Vec<(String, String)> = Vec::new();
5148 if let Some(rest) = message.strip_prefix(host::CODE_MARK) {
5149 if let Some((c, m)) = rest.split_once('\u{1}') {
5150 code = Some(c.to_string());
5151 bracketed = false;
5152 let (m, fs) = host::split_error_fields(m);
5153 fields = fs
5154 .into_iter()
5155 .map(|(k, v)| (k.to_string(), v.to_string()))
5156 .collect();
5157 message = m.to_string();
5158 }
5159 }
5160 let mut props: IndexMap<String, Value> = IndexMap::new();
5161 let mv = h.new_str(message.clone());
5162 props.insert("message".into(), mv);
5163 if let Some(c) = &code {
5164 let cv = h.new_str(c.clone());
5165 props.insert("code".into(), cv);
5166 for (k, v) in fields {
5167 let fv = h.new_str(v);
5168 props.insert(k, fv);
5169 }
5170 if bracketed {
5171 // Marks this as a Node JS-layer error, whose `toString` brackets the
5172 // code. A native-layer error has the same `.code` and does not.
5173 props.insert("@@nodeError".into(), Value::Bool(true));
5174 }
5175 }
5176 let label = match (&code, bracketed) {
5177 (Some(c), true) => format!("{name} [{c}]"),
5178 _ => name.clone(),
5179 };
5180 let frames = h.stack_frames();
5181 let stack = if message.is_empty() {
5182 format!("{label}{frames}")
5183 } else {
5184 format!("{label}: {message}{frames}")
5185 };
5186 let sv = h.new_str(stack);
5187 props.insert("stack".into(), sv);
5188 // A libuv system-error message is itself the canonical encoding of the
5189 // error's metadata — `ENOENT: no such file or directory, open '/x'` — so a
5190 // filesystem/network failure recovers the enumerable `code`/`errno`/
5191 // `syscall`/`path` own properties that `err.code === 'ENOENT'` checks (the
5192 // single most common error-handling idiom in Node packages) depend on.
5193 for (k, v) in syscall_error_fields(&message) {
5194 let sv = match v {
5195 SysField::Str(s) => h.new_str(s),
5196 SysField::Num(n) => Value::Float(n),
5197 };
5198 props.insert(k.into(), sv);
5199 }
5200 let obj = h.new_object(props);
5201 if let Some(p) = host::error_proto_of(h, &name) {
5202 h.set_proto(&obj, p);
5203 }
5204 // `message`/`stack` are non-enumerable; a Node `ERR_*` error's `code` is not
5205 // (`Object.keys(e)` on an `ERR_INVALID_ARG_TYPE` reads `["code"]`).
5206 h.hide_prop(&obj, "message");
5207 h.hide_prop(&obj, "stack");
5208 obj
5209}
5210
5211enum SysField {
5212 Str(String),
5213 Num(f64),
5214}
5215
5216/// Decompose a libuv-shaped message (`ECODE: reason, syscall 'path'`) into the
5217/// own properties Node hangs off a system error. Returns empty for any message
5218/// that is not in that shape.
5219fn syscall_error_fields(message: &str) -> Vec<(&'static str, SysField)> {
5220 let (code, rest) = match message.split_once(": ") {
5221 Some((c, r))
5222 if c.len() >= 2
5223 && c.starts_with('E')
5224 && c.bytes()
5225 .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()) =>
5226 {
5227 (c, r)
5228 }
5229 _ => return Vec::new(),
5230 };
5231 let mut out: Vec<(&'static str, SysField)> = vec![
5232 ("errno", SysField::Num(errno_for(code))),
5233 ("code", SysField::Str(code.to_string())),
5234 ];
5235 // `reason, syscall 'path'` — the path is optional (`EPIPE: …, write`).
5236 if let Some((_, tail)) = rest.split_once(", ") {
5237 let (syscall, path) = match tail.split_once(" '") {
5238 // A two-path message ends `'from' -> 'to'`; `err.path` is the FIRST
5239 // one, so the scan stops at its closing quote rather than at the
5240 // end of the line — which had been swallowing `' -> 'dest` into the
5241 // path for every `rename` and `copyFile` failure.
5242 Some((s, p)) => (s, p.split_once('\'').map(|(first, _)| first)),
5243 None => (tail, None),
5244 };
5245 out.push(("syscall", SysField::Str(syscall.to_string())));
5246 if let Some(p) = path {
5247 out.push(("path", SysField::Str(p.to_string())));
5248 }
5249 }
5250 out
5251}
5252
5253/// The negative `errno` Node reports for a libuv error code on this platform.
5254/// Only the codes `err_str` can produce are mapped; anything else reports the
5255/// generic `EIO` number rather than inventing a value.
5256fn errno_for(code: &str) -> f64 {
5257 let n: i32 = match code {
5258 "ENOENT" => 2,
5259 "EACCES" => 13,
5260 "EEXIST" => 17,
5261 "ENOTDIR" => 20,
5262 "EISDIR" => 21,
5263 "EINVAL" => 22,
5264 "EPIPE" => 32,
5265 "ENOTEMPTY" => 66,
5266 _ => 5, // EIO
5267 };
5268 -f64::from(n)
5269}
5270
5271// ── iteration ─────────────────────────────────────────────────────────────────
5272
5273fn b_getiter(vm: &mut VM, _: u8) -> Value {
5274 let v = vm.pop();
5275 // A generator is its own iterator (resumed lazily by FORITER).
5276 if with_host(|h| h.is_generator_val(&v)) {
5277 return v;
5278 }
5279 // A Proxy's iterator comes from its traps, materialized eagerly: the
5280 // `lookup_chain` probe below reads the property map a proxy does not have.
5281 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5282 return match crate::proxy::iterate(&v) {
5283 Ok(Some(items)) => with_host(|h| {
5284 h.alloc(JsObj::Iter {
5285 items,
5286 idx: 0,
5287 array: None,
5288 })
5289 }),
5290 Ok(None) => abort(vm, "internal: kind_of said Proxy".into()),
5291 Err(e) => abort(vm, e),
5292 };
5293 }
5294 // Arrays and strings take the direct path below: they have no iterator
5295 // state to preserve and are the hot case, so they must not pay a property
5296 // lookup and a call per loop.
5297 let direct = matches!(
5298 with_host(|h| h.kind_of(&v)),
5299 Some(ObjKind::Array) | Some(ObjKind::Str)
5300 );
5301 // …but only while their `Symbol.iterator` is still reachable. It comes from
5302 // the intrinsic prototype, so replacing the link takes it away: node reports
5303 // `a is not iterable` for an array whose prototype is a plain object, where
5304 // the fast path below iterated the backing vector regardless.
5305 if !own_intrinsic_reachable(&v)
5306 && !matches!(
5307 get_property(&v, "@@iterator"),
5308 Ok(ref f) if with_host(|h| host::is_callable(h, f))
5309 )
5310 {
5311 let shown = with_host(|h| h.inspect(&v));
5312 let msg = host::type_error(&format!("{shown} is not iterable"));
5313 return abort(vm, host::name_call_site(vm, &shown, msg));
5314 }
5315 // Anything else with a `Symbol.iterator`: call it for the iterator object.
5316 //
5317 // Resolved as a full property READ, not a stored-property lookup. A
5318 // NATIVE-tagged object (`URLSearchParams`, `Headers`, `Map`, `Set`)
5319 // dispatches its methods through the stdlib table rather than a property
5320 // map, so a `lookup_chain` probe found nothing and the loop fell through to
5321 // materializing the value — which threw for `URLSearchParams` and
5322 // snapshotted for `Map`. Spreading the same object already worked, because
5323 // that path had been fixed and this one had not.
5324 if !direct {
5325 if let Ok(iter_fn) = get_property(&v, "@@iterator") {
5326 if with_host(|h| host::is_callable(h, &iter_fn)) {
5327 return match host::invoke(&iter_fn, Vec::new(), Some(v.clone())) {
5328 Ok(it) => it,
5329 Err(e) => abort(vm, e),
5330 };
5331 }
5332 }
5333 }
5334 // An array is iterated live, as its `values()` iterator does: a snapshot
5335 // missed every push during the loop, so a worklist `for (const n of q)
5336 // q.push(…)` stopped after the first element.
5337 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Array) {
5338 return array_iterator(&v, host::ArrayIterKind::Values);
5339 }
5340 match with_host(|h| h.iter_vec(&v)) {
5341 Ok(items) => with_host(|h| {
5342 h.alloc(JsObj::Iter {
5343 items,
5344 idx: 0,
5345 array: None,
5346 })
5347 }),
5348 // V8 names the SOURCE EXPRESSION, not the value: `for (const x of a)`
5349 // reports `a is not iterable`. The text was recorded for this op.
5350 Err(e) => {
5351 let shown = with_host(|h| h.inspect(&v));
5352 let named = host::name_call_site(vm, &shown, e);
5353 abort(vm, named)
5354 }
5355 }
5356}
5357
5358fn b_forin_keys(vm: &mut VM, _: u8) -> Value {
5359 let v = vm.pop();
5360 // `for-in` over a Proxy is 14.7.5.9 `EnumerateObjectProperties`: the
5361 // `ownKeys` trap filtered by `[[GetOwnProperty]]`'s `enumerable`. Both traps
5362 // are user code, so this cannot run inside `enum_keys`'s `&mut` host borrow.
5363 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5364 // `ownKeys` ONLY. The `enumerable` filter is 14.7.5.10's per-key
5365 // `[[GetOwnProperty]]`, which `FORIN_ALIVE` runs at the moment each key
5366 // is visited — so the `getOwnPropertyDescriptor` traps interleave with
5367 // the body the way node's do, instead of all firing up front.
5368 return match crate::proxy::own_keys(&v) {
5369 Ok(keys) => with_host(|h| {
5370 let out: Vec<Value> = keys
5371 .unwrap_or_default()
5372 .into_iter()
5373 .filter(|k| !host::is_symbol_key(k))
5374 .map(|k| h.new_str(k))
5375 .collect();
5376 h.new_array(out)
5377 }),
5378 Err(e) => abort(vm, e),
5379 };
5380 }
5381 let mut keys = with_host(|h| h.enum_keys(&v));
5382 // A member patched onto the receiver's INTRINSIC prototype is enumerable
5383 // and inherited, so `for-in` visits it after the own keys — but the
5384 // intrinsic prototypes are not links `enum_keys` can walk, so its chain
5385 // pass never reaches them.
5386 if !with_host(|h| h.has_null_proto(&v)) {
5387 let seen: Vec<String> = keys.iter().map(|k| with_host(|h| h.str_of(k))).collect();
5388 for ns in intrinsic_proto_namespaces(&v) {
5389 for k in with_host(|h| h.builtin_static_keys(&ns)) {
5390 if !seen.contains(&k) && !intrinsic_proto_member(&ns, &k) {
5391 keys.push(with_host(|h| h.new_str(k)));
5392 }
5393 }
5394 }
5395 }
5396 with_host(|h| h.new_array(keys))
5397}
5398
5399/// The intrinsic prototype namespaces `v` inherits from, nearest first — its
5400/// own constructor's and then `Object`'s, the same two steps
5401/// `inherited_builtin_static` looks a value up in.
5402fn intrinsic_proto_namespaces(v: &Value) -> Vec<String> {
5403 let ctor = match wrapped_primitive(v).as_ref().and_then(wrapper_ctor_of) {
5404 Some(c) => Some(c),
5405 None if is_arguments(v) => Some("Object"),
5406 None => with_host(|h| default_ctor_name(h, v)),
5407 };
5408 let mut out: Vec<String> = ctor
5409 .filter(|c| *c != "Object")
5410 .map(|c| format!("{c}.prototype"))
5411 .into_iter()
5412 .collect();
5413 out.push("Object.prototype".to_string());
5414 out
5415}
5416
5417/// `FORIN_ALIVE` — is `key` STILL an enumerable property of `obj`?
5418///
5419/// `for-in` takes its key list once (14.7.5.10 builds it lazily, but a snapshot
5420/// of the enumerable keys is observationally the same for everything except
5421/// this), and the body can delete a key before the loop reaches it. Node does
5422/// not visit a key deleted that way; without this check `delete d.z` inside the
5423/// loop still produced `x,y,z`.
5424///
5425/// The check is `[[GetOwnProperty]]`-shaped rather than `in`: on a Proxy it runs
5426/// the `getOwnPropertyDescriptor` trap, which is what node runs, and NOT the
5427/// `has` trap, which node never fires for `for-in`. That also puts each trap
5428/// call immediately before its visit, matching node's interleaving — the trap
5429/// log used to show every `gopd` up front because the key list was filtered
5430/// eagerly.
5431fn b_forin_alive(vm: &mut VM, _: u8) -> Value {
5432 let key = vm.pop();
5433 let obj = vm.pop();
5434 let name = with_host(|h| h.str_of(&key));
5435 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
5436 return match crate::proxy::own_enumerable(&obj, &name) {
5437 Ok(b) => Value::Bool(b),
5438 Err(e) => abort(vm, e),
5439 };
5440 }
5441 // A STRING's keys are its character indices. `in` is not defined on a string
5442 // primitive at all, so the ordinary path below has no answer for one and
5443 // `for (const i in 'abc')` came back empty.
5444 if let Some(s) = with_host(|h| h.as_str(&obj)) {
5445 let len = crate::utf16::len(&s);
5446 return Value::Bool(name.parse::<usize>().is_ok_and(|i| i < len));
5447 }
5448 // Any other receiver: EXISTENCE only. Node re-checks that the key is still
5449 // there and does NOT re-check enumerability — making one non-enumerable
5450 // mid-loop still visits it, where re-filtering on `enumerable` dropped it.
5451 // (The Proxy branch above does re-check, because there the answer comes from
5452 // the trap node itself calls.)
5453 Value::Bool(has_property_ordinary(&obj, &name))
5454}
5455
5456fn b_foriter(vm: &mut VM, _: u8) -> Value {
5457 let it = match vm.stack.last() {
5458 Some(v) => v.clone(),
5459 None => return abort(vm, "internal: FORITER with empty stack".into()),
5460 };
5461 // A built-in iterator: a snapshot (strings, a Proxy's items) or live over
5462 // an array.
5463 if let Some(step) = iter_step(&it) {
5464 return match step {
5465 Some(v) => {
5466 vm.push(v);
5467 Value::Bool(true)
5468 }
5469 None => Value::Bool(false),
5470 };
5471 }
5472 // Generator: resume one step.
5473 if with_host(|h| h.is_generator_val(&it)) {
5474 return match host::gen_resume(&it, Value::Undef) {
5475 Ok(host::GenStep::Yield(v)) => {
5476 vm.push(v);
5477 Value::Bool(true)
5478 }
5479 Ok(host::GenStep::Done(_)) => Value::Bool(false),
5480 Err(e) => abort(vm, e),
5481 };
5482 }
5483 // A user iterator object with a `.next()` returning `{ value, done }`.
5484 match host::call_method(&it, "next", Vec::new()) {
5485 Ok(step) => {
5486 let done = get_property(&step, "done")
5487 .map(|d| with_host(|h| h.truthy(&d)))
5488 .unwrap_or(true);
5489 if done {
5490 Value::Bool(false)
5491 } else {
5492 match get_property(&step, "value") {
5493 Ok(v) => {
5494 vm.push(v);
5495 Value::Bool(true)
5496 }
5497 Err(e) => abort(vm, e),
5498 }
5499 }
5500 }
5501 Err(e) => abort(vm, e),
5502 }
5503}
5504
5505fn b_unpack(vm: &mut VM, _: u8) -> Value {
5506 let star = match vm.pop() {
5507 Value::Int(n) => n,
5508 _ => -1,
5509 };
5510 let count = match vm.pop() {
5511 Value::Int(n) => n as usize,
5512 _ => 0,
5513 };
5514 let iterable = vm.pop();
5515 // Without a `...rest` element the pattern needs exactly `count` values and
5516 // must then close the iterator; draining hung on an unbounded source.
5517 let items = match if star < 0 {
5518 host::iter_take(&iterable, count)
5519 } else {
5520 host::iter_all(&iterable)
5521 } {
5522 Ok(v) => v,
5523 // Destructuring a non-iterable names the SOURCE EXPRESSION, the way
5524 // `for-of` does: `const [x] = o` reports `o is not iterable`. The text
5525 // was recorded for this op at compile time.
5526 Err(e) => {
5527 // Node names the source only when the pattern's right-hand side is
5528 // a plain IDENTIFIER — `const [x] = o` is `o is not iterable`.
5529 // Anything else (a member, a call, a nested pattern, a parameter)
5530 // reports the TYPE instead, with the property note. Measured across
5531 // twelve shapes rather than guessed.
5532 let msg = match host::call_site_text(vm) {
5533 Some(text) => host::type_error(&format!("{text} is not iterable")),
5534 None if e.ends_with(" is not iterable") => {
5535 host::type_error(¬_iterable_typed(&iterable))
5536 }
5537 None => e,
5538 };
5539 return abort(vm, msg);
5540 }
5541 };
5542 let ordered: Vec<Value> = if star < 0 {
5543 (0..count)
5544 .map(|i| items.get(i).cloned().unwrap_or(Value::Undef))
5545 .collect()
5546 } else {
5547 let si = star as usize;
5548 let after = count.saturating_sub(si + 1);
5549 let rest_end = items.len().saturating_sub(after).max(si);
5550 let mut out: Vec<Value> = Vec::with_capacity(count);
5551 for i in 0..si {
5552 out.push(items.get(i).cloned().unwrap_or(Value::Undef));
5553 }
5554 let rest: Vec<Value> = items
5555 .get(si..rest_end)
5556 .map(|s| s.to_vec())
5557 .unwrap_or_default();
5558 out.push(with_host(|h| h.new_array(rest)));
5559 for j in 0..after {
5560 out.push(items.get(rest_end + j).cloned().unwrap_or(Value::Undef));
5561 }
5562 out
5563 };
5564 if ordered.is_empty() {
5565 return Value::Undef;
5566 }
5567 for it in ordered[1..].iter().rev().cloned() {
5568 vm.push(it);
5569 }
5570 ordered[0].clone()
5571}
5572
5573fn b_build_args(vm: &mut VM, argc: u8) -> Value {
5574 let flat = pop_n(vm, argc as usize);
5575 let mut out = Vec::new();
5576 // Elided positions of an array literal (tag 2), recorded as the run-time
5577 // index each lands on — which only this walk knows, because a preceding
5578 // spread contributes an unknown number of elements. Call-argument lists,
5579 // the other `BUILD_ARGS` caller, cannot contain an elision, so this stays
5580 // empty for them.
5581 let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
5582 let mut i = 0;
5583 while i + 1 < flat.len() {
5584 let val = flat[i + 1].clone();
5585 match flat[i] {
5586 // Tag 1 is an ARRAY-LITERAL spread, tag 3 a CALL-ARGUMENT one. They
5587 // report a non-iterable differently, which is the only reason the
5588 // two are told apart here.
5589 Value::Int(1) => match host::iter_all(&val).map_err(|e| {
5590 let shown = with_host(|h| h.inspect(&val));
5591 host::name_call_site(vm, &shown, e)
5592 }) {
5593 Ok(items) => out.extend(items),
5594 Err(e) => return abort(vm, e),
5595 },
5596 Value::Int(3) => match host::iter_all(&val) {
5597 Ok(items) => out.extend(items),
5598 Err(e) => {
5599 // A NULLISH spread names the value and what could not be
5600 // read off it; anything else names the missing protocol.
5601 let shown = with_host(|h| h.is_nullish(&val).then(|| h.str_of(&val)));
5602 return abort(
5603 vm,
5604 match shown {
5605 Some(s) => host::type_error(&format!(
5606 "{s} is not iterable (cannot read property {s})"
5607 )),
5608 None if e.ends_with(" is not iterable") => host::type_error(
5609 "Spread syntax requires ...iterable[Symbol.iterator] to be a function",
5610 ),
5611 None => e,
5612 },
5613 );
5614 }
5615 },
5616 Value::Int(2) => {
5617 holes.insert(out.len());
5618 out.push(Value::Undef);
5619 }
5620 _ => out.push(val),
5621 }
5622 i += 2;
5623 }
5624 with_host(|h| {
5625 let arr = h.new_array(out);
5626 h.install_holes(&arr, holes);
5627 arr
5628 })
5629}
5630
5631// ── calls ──────────────────────────────────────────────────────────────────────
5632
5633fn b_call(vm: &mut VM, argc: u8) -> Value {
5634 let mut args = pop_n(vm, argc as usize);
5635 let name = sval(&args.remove(0));
5636 let r = host::call_named(&name, args);
5637 // A bare name that resolved to a non-callable reports the VALUE
5638 // (`undefined is not a function`); node names the identifier. Resolving it
5639 // again to learn what the message said costs nothing off the error path.
5640 let r = r.map_err(|e| {
5641 let shown = global_binding(&name)
5642 .map(|v| with_host(|h| h.str_of(&v)))
5643 .unwrap_or_default();
5644 host::name_call_site(vm, &shown, e)
5645 });
5646 finish(vm, r)
5647}
5648
5649/// `recv[0](…)` — a computed call whose key is an ARRAY INDEX rather than a
5650/// method name. `call_method` resolves by name and bottoms out in
5651/// `call_type_method`, which knows `sort`/`slice` and not `"0"`, so an element
5652/// that happens to be a function reported "is not a function". Read the element
5653/// and invoke it with `recv` as `this`, which is the receiver 13.3.6 gives it.
5654/// A computed call's key is a property key, so it goes through ToPropertyKey:
5655/// `arr[0](…)` looks up `"0"`. `sval` only unwraps an existing `Value::Str` and
5656/// answers "" for a number, which turned `arr[0]()` into a call to the method
5657/// named "" — so the key is stringified here instead.
5658fn call_key_of(v: &Value) -> String {
5659 if let Value::Str(s) = v {
5660 return (**s).clone();
5661 }
5662 // `ToPropertyKey`, not `ToString`. A SYMBOL key has an internal `@@name`
5663 // spelling that `str_of` does not produce — it renders
5664 // `Symbol(Symbol.iterator)` — so `obj[Symbol.iterator]()` dispatched a
5665 // method by that display text and reported it was not a function, for every
5666 // object including a plain literal with a computed symbol method. Reading
5667 // the same property without calling it worked, which is what hid this.
5668 with_host(|h| h.property_key(v))
5669}
5670
5671fn index_element_call(recv: &Value, name: &str, args: &[Value]) -> Option<Result<Value, String>> {
5672 if name.is_empty() || !name.bytes().all(|b| b.is_ascii_digit()) {
5673 return None;
5674 }
5675 let f = get_property(recv, name).ok()?;
5676 with_host(|h| host::is_callable(h, &f))
5677 .then(|| host::invoke(&f, args.to_vec(), Some(recv.clone())))
5678}
5679
5680fn b_call_method(vm: &mut VM, argc: u8) -> Value {
5681 let mut args = pop_n(vm, argc as usize);
5682 let recv = args.remove(0);
5683 let name = call_key_of(&args.remove(0));
5684 if let Some(r) = index_element_call(&recv, &name, &args) {
5685 return finish(vm, r);
5686 }
5687 let r = host::call_method(&recv, &name, args);
5688 // `z.f()` on a missing method is `z.f is not a function` in node, not
5689 // `f is not a function`: V8 names the callee as the source wrote it. The
5690 // text was recorded for this op at compile time.
5691 let r = r.map_err(|e| host::name_call_site(vm, &name, e));
5692 finish(vm, r)
5693}
5694
5695fn b_call_value(vm: &mut VM, argc: u8) -> Value {
5696 let mut args = pop_n(vm, argc as usize);
5697 let callable = args.remove(0);
5698 let r = host::invoke(&callable, args, None);
5699 // The callee here is an expression, not a name, so the message it produced
5700 // describes the VALUE (`undefined is not a function`); node names the
5701 // expression. Same site table, keyed on that rendering.
5702 let r = r.map_err(|e| {
5703 let shown = with_host(|h| h.str_of(&callable));
5704 host::name_call_site(vm, &shown, e)
5705 });
5706 finish(vm, r)
5707}
5708
5709/// `NEW_SPREAD` — `new C(...xs)`, where the argument list is a run-time array
5710/// rather than a fixed count of stack slots.
5711///
5712/// `compile_new` used to compile each argument with `compile_expr`, and a
5713/// spread there evaluates to the SPREAD OBJECT itself — so `new C(...[1, 2])`
5714/// passed the array as one argument and `new Date(...[2020, 0, 1])` built an
5715/// Invalid Date.
5716fn b_new_spread(vm: &mut VM, _: u8) -> Value {
5717 let args_arr = vm.pop();
5718 let ctor = vm.pop();
5719 let args = host::iter_all(&args_arr).unwrap_or_default();
5720 let r = host::construct(&ctor, args).map_err(|e| {
5721 let shown = with_host(|h| h.str_of(&ctor));
5722 host::name_call_site(vm, &shown, e)
5723 });
5724 finish(vm, r)
5725}
5726
5727fn b_new(vm: &mut VM, argc: u8) -> Value {
5728 let mut args = pop_n(vm, argc as usize);
5729 let ctor = args.remove(0);
5730 let r = host::construct(&ctor, args);
5731 // `new (o.a.b.c)()` on a non-constructor names the expression, as a failed
5732 // call does.
5733 let r = r.map_err(|e| {
5734 let shown = with_host(|h| h.str_of(&ctor));
5735 host::name_call_site(vm, &shown, e)
5736 });
5737 finish(vm, r)
5738}
5739
5740fn b_apply(vm: &mut VM, _: u8) -> Value {
5741 let args_arr = vm.pop();
5742 let callable = vm.pop();
5743 let args = host::iter_all(&args_arr).unwrap_or_default();
5744 let r = host::invoke(&callable, args, None);
5745 finish(vm, r)
5746}
5747
5748fn b_apply_method(vm: &mut VM, _: u8) -> Value {
5749 let args_arr = vm.pop();
5750 let name = call_key_of(&vm.pop());
5751 let recv = vm.pop();
5752 let args = host::iter_all(&args_arr).unwrap_or_default();
5753 if let Some(r) = index_element_call(&recv, &name, &args) {
5754 return finish(vm, r);
5755 }
5756 let r = host::call_method(&recv, &name, args);
5757 finish(vm, r)
5758}
5759
5760// ── numeric hook ──────────────────────────────────────────────────────────────
5761
5762/// Host callback for arithmetic fusevm cannot complete natively (a non-`Int`/
5763/// non-`Float` operand). Supplies JavaScript `+` concatenation and coercion.
5764///
5765/// Every operand is run through `ToPrimitive` FIRST (ECMA-262 13.15.3 for `+`,
5766/// 13.6.3 for the other arithmetic ops, 13.10.1 for the relational ones), which
5767/// is what invokes a user `valueOf`/`Symbol.toPrimitive`. It has to happen here
5768/// rather than inside `JsHost::arith`, because calling back into JS re-enters
5769/// the VM and `arith` runs under the host's `RefCell` borrow.
5770pub fn numeric_hook(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5771 use NumOp::*;
5772 let (a, b) = match op {
5773 // `==`/`!=` only convert when the OTHER side is a primitive that can be
5774 // compared numerically or textually; `{} == {}` stays a reference check.
5775 Eq | Ne => {
5776 let (pa, pb) = with_host(|h| (host::is_primitive(h, a), host::is_primitive(h, b)));
5777 match (pa, pb) {
5778 (false, true) if coerces_against_object(b) => {
5779 (host::to_primitive(a, "default")?, b.clone())
5780 }
5781 (true, false) if coerces_against_object(a) => {
5782 (a.clone(), host::to_primitive(b, "default")?)
5783 }
5784 _ => (a.clone(), b.clone()),
5785 }
5786 }
5787 // `+` uses the default hint (`valueOf` first, but a string result still
5788 // selects concatenation); everything else uses the number hint.
5789 Add => (
5790 host::to_primitive(a, "default")?,
5791 host::to_primitive(b, "default")?,
5792 ),
5793 _ => (
5794 host::to_primitive(a, "number")?,
5795 host::to_primitive(b, "number")?,
5796 ),
5797 };
5798 reject_symbol_operand(op, &a, &b)?;
5799 with_host(|h| h.arith(op, &a, &b))
5800}
5801
5802/// A symbol has no `ToNumber` and no `ToString`, so every operator except the
5803/// equality family rejects it (7.1.4 step 2, 7.1.17 step 2). node-js instead
5804/// concatenated `Symbol(desc)` into the result.
5805///
5806/// Which of the two messages V8 uses is decided by whether the operation is
5807/// STRING concatenation — measured on node v26.7.0, `Symbol() + ''` is
5808/// `Cannot convert a Symbol value to a string` while `Symbol() + 1`,
5809/// `Symbol() + Symbol()` and `Symbol() * 1` are all
5810/// `Cannot convert a Symbol value to a number`. `==`/`===` never convert
5811/// (`Symbol() == 1` is `false`), so they are left alone.
5812fn reject_symbol_operand(op: NumOp, a: &Value, b: &Value) -> Result<(), String> {
5813 use NumOp::*;
5814 if matches!(op, Eq | Ne) {
5815 return Ok(());
5816 }
5817 let (sym, concat) = with_host(|h| {
5818 let is_sym = |v: &Value| matches!(h.get(v), Some(JsObj::Symbol { .. }));
5819 let is_str =
5820 |v: &Value| matches!(v, Value::Str(_)) || matches!(h.get(v), Some(JsObj::Str(_)));
5821 (is_sym(a) || is_sym(b), is_str(a) || is_str(b))
5822 });
5823 if !sym {
5824 return Ok(());
5825 }
5826 Err(host::type_error(if matches!(op, Add) && concat {
5827 "Cannot convert a Symbol value to a string"
5828 } else {
5829 "Cannot convert a Symbol value to a number"
5830 }))
5831}
5832
5833/// Whether a primitive `v` makes `==` against an object convert that object
5834/// (7.2.15 steps 10-11): numbers, strings, bigints and symbols do; `null`,
5835/// `undefined` and booleans are settled without a `ToPrimitive` call
5836/// (a boolean is coerced to a number first, and then it does).
5837fn coerces_against_object(v: &Value) -> bool {
5838 match v {
5839 Value::Undef => false,
5840 Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_) => true,
5841 _ => with_host(|h| !h.is_null(v)),
5842 }
5843}
5844
5845// ══ standard library ═══════════════════════════════════════════════════════════
5846
5847/// Namespaces reachable as bare globals.
5848fn is_namespace(name: &str) -> bool {
5849 matches!(
5850 name,
5851 "console"
5852 | "Math"
5853 | "JSON"
5854 | "Object"
5855 | "Array"
5856 | "Number"
5857 | "String"
5858 | "Boolean"
5859 | "Symbol"
5860 | "Reflect"
5861 | "Promise"
5862 | "process"
5863 | "Buffer"
5864 | "URL"
5865 | "URLSearchParams"
5866 )
5867}
5868
5869const GLOBAL_FUNCS: &[&str] = &[
5870 "parseInt",
5871 "parseFloat",
5872 "isNaN",
5873 "isFinite",
5874 "encodeURIComponent",
5875 "decodeURIComponent",
5876 "encodeURI",
5877 "decodeURI",
5878 // Annex B legacy encoders. Still globals on every engine, and still called
5879 // by pre-`encodeURIComponent` library code.
5880 "escape",
5881 "unescape",
5882 "eval",
5883 "String",
5884 "Number",
5885 "Boolean",
5886 "Array",
5887 "Object",
5888 "Function",
5889 "Symbol",
5890 "Map",
5891 "Set",
5892 "WeakMap",
5893 "WeakSet",
5894 "Promise",
5895 "Error",
5896 "TypeError",
5897 "RangeError",
5898 "SyntaxError",
5899 "ReferenceError",
5900 "EvalError",
5901 "URIError",
5902 "AggregateError",
5903 "DOMException",
5904 "Iterator",
5905 "BigInt",
5906 "RegExp",
5907 "Date",
5908 "ArrayBuffer",
5909 "DataView",
5910 "Uint8Array",
5911 "Int8Array",
5912 "Uint8ClampedArray",
5913 "Int16Array",
5914 "Uint16Array",
5915 "Int32Array",
5916 "Uint32Array",
5917 "Float32Array",
5918 "Float64Array",
5919 "BigInt64Array",
5920 "BigUint64Array",
5921 "WeakRef",
5922 "FinalizationRegistry",
5923 "TextEncoder",
5924 "TextDecoder",
5925 // WHATWG Fetch globals (see `stdlib::fetch`).
5926 "fetch",
5927 "Headers",
5928 "Request",
5929 "Response",
5930 "Blob",
5931 "File",
5932 "FormData",
5933 "AbortController",
5934 "AbortSignal",
5935 "queueMicrotask",
5936 "setTimeout",
5937 "setInterval",
5938 "setImmediate",
5939 "clearTimeout",
5940 "clearInterval",
5941 "clearImmediate",
5942 "structuredClone",
5943 // Base64 helpers. They existed only as `require('buffer').btoa`, but node
5944 // exposes both as globals, so `btoa('abc')` was a ReferenceError.
5945 "btoa",
5946 "atob",
5947 "Proxy",
5948 "require",
5949 // CommonJS loader dispatch targets referenced by per-module `require`
5950 // closures (see `module.rs`); never written by user code.
5951 "__cjs_require",
5952 "__cjs_resolve",
5953 "__cjs_cache",
5954];
5955
5956const NS_METHODS: &[&str] = &[
5957 "console.log",
5958 "console.error",
5959 "console.warn",
5960 "console.info",
5961 "console.debug",
5962 "Math.abs",
5963 "Math.acos",
5964 "Math.acosh",
5965 "Math.asin",
5966 "Math.asinh",
5967 "Math.atan",
5968 "Math.atanh",
5969 "Math.atan2",
5970 "Math.ceil",
5971 "Math.cbrt",
5972 "Math.expm1",
5973 "Math.clz32",
5974 "Math.cos",
5975 "Math.cosh",
5976 "Math.exp",
5977 "Math.floor",
5978 "Math.fround",
5979 "Math.hypot",
5980 "Math.imul",
5981 "Math.log",
5982 "Math.log1p",
5983 "Math.log2",
5984 "Math.log10",
5985 "Math.max",
5986 "Math.min",
5987 "Math.pow",
5988 "Math.random",
5989 "Math.round",
5990 "Math.sign",
5991 "Math.sin",
5992 "Math.sinh",
5993 "Math.sqrt",
5994 "Math.tan",
5995 "Math.tanh",
5996 "Math.trunc",
5997 "JSON.stringify",
5998 "JSON.parse",
5999 "JSON.rawJSON",
6000 "JSON.isRawJSON",
6001 "Object.keys",
6002 "Object.values",
6003 "Object.entries",
6004 "Object.assign",
6005 "Object.freeze",
6006 "Object.is",
6007 "Object.fromEntries",
6008 "Object.getPrototypeOf",
6009 "Object.setPrototypeOf",
6010 "Object.create",
6011 "Object.getOwnPropertyNames",
6012 "Object.getOwnPropertySymbols",
6013 "Object.defineProperty",
6014 "Object.getOwnPropertyDescriptor",
6015 "Object.getOwnPropertyDescriptors",
6016 "Object.defineProperties",
6017 "Object.isFrozen",
6018 "Object.isSealed",
6019 "Object.seal",
6020 "Object.preventExtensions",
6021 "Object.isExtensible",
6022 "Object.hasOwn",
6023 "Object.groupBy",
6024 "Array.isArray",
6025 "Array.from",
6026 "Array.fromAsync",
6027 "Array.of",
6028 "Number.isFinite",
6029 "Number.isInteger",
6030 "Number.isNaN",
6031 "Number.isSafeInteger",
6032 "Number.parseFloat",
6033 "Number.parseInt",
6034 "String.fromCharCode",
6035 "String.fromCodePoint",
6036 "String.raw",
6037 "Symbol.for",
6038 "Symbol.keyFor",
6039 "BigInt.asIntN",
6040 "BigInt.asUintN",
6041 "Proxy.revocable",
6042 "Reflect.defineProperty",
6043 "Reflect.deleteProperty",
6044 "Reflect.apply",
6045 "Reflect.construct",
6046 "Reflect.get",
6047 "Reflect.getOwnPropertyDescriptor",
6048 "Reflect.getPrototypeOf",
6049 "Reflect.has",
6050 "Reflect.isExtensible",
6051 "Reflect.ownKeys",
6052 "Reflect.preventExtensions",
6053 "Reflect.set",
6054 "Reflect.setPrototypeOf",
6055 "Promise.resolve",
6056 "Promise.reject",
6057 "Promise.all",
6058 "Promise.allSettled",
6059 "Promise.race",
6060 "Promise.any",
6061 "Promise.withResolvers",
6062 "Promise.try",
6063 "RegExp.escape",
6064 "Error.isError",
6065 "Map.groupBy",
6066 "Response.json",
6067 "Response.error",
6068 "Response.redirect",
6069 "AbortSignal.abort",
6070 "AbortSignal.timeout",
6071 "process.nextTick",
6072 "Error.captureStackTrace",
6073 "require.resolve",
6074 "require.resolve.paths",
6075 "process.memoryUsage.rss",
6076];
6077
6078/// The `name` and `length` a builtin function reports, from the generated
6079/// intrinsic table ([`crate::arity::BUILTIN_ARITY`]). `None` for a key the table
6080/// does not cover — every non-function namespace (`Math`, `require('fs')`),
6081/// and the core-module functions, whose arity is not specified anywhere.
6082pub fn builtin_meta(key: &str) -> Option<(&'static str, u32)> {
6083 crate::arity::BUILTIN_ARITY
6084 .binary_search_by(|(k, _, _)| (*k).cmp(key))
6085 .ok()
6086 .map(|i| {
6087 let (_, name, len) = crate::arity::BUILTIN_ARITY[i];
6088 (name, len)
6089 })
6090}
6091
6092/// The `name` a builtin function reports. The table answers for an intrinsic;
6093/// anything else falls back to the last segment of the key, which is what the
6094/// name is for every builtin this frontend synthesizes: `@proto:TypedArray:set`
6095/// is `set` and `fs.readFileSync` is `readFileSync`. Reporting the whole key was
6096/// how `[Function: @proto:TypedArray:set]` reached `console.log`.
6097pub fn builtin_name(key: &str) -> &str {
6098 if let Some((name, _)) = builtin_meta(key) {
6099 return name;
6100 }
6101 match key.strip_prefix("@proto:") {
6102 Some(rest) => rest.rsplit(':').next().unwrap_or(rest),
6103 // An accessor's getter is named `get <member>` (10.2.9 SetFunctionName
6104 // with a `get` prefix), which is what `util.inspect` prints for it and
6105 // what a library reads to identify one.
6106 None => key.rsplit('.').next().unwrap_or(key),
6107 }
6108}
6109
6110/// The `name` of an intrinsic accessor's getter thunk, or `None` for anything
6111/// else. Kept out of `builtin_name`'s `&str` return, which cannot own the
6112/// `"get size"` it has to build.
6113pub fn proto_getter_name(key: &str) -> Option<String> {
6114 let (verb, rest) = match key.strip_prefix("@protoget:") {
6115 Some(rest) => ("get", rest),
6116 None => ("set", key.strip_prefix("@protoset:")?),
6117 };
6118 let (_, member) = rest.split_once(':')?;
6119 Some(format!("{verb} {member}"))
6120}
6121
6122pub fn is_known_builtin(name: &str) -> bool {
6123 // Binary search over a sorted INDEX of the two tables rather than a scan of
6124 // both. This runs on every call whose callee is a builtin — `call_method`
6125 // asks it before dispatching `Math.max(…)` or `JSON.parse(…)` — and the
6126 // answer came only after a full scan of `GLOBAL_FUNCS` (77) plus a scan of
6127 // `NS_METHODS` up to the entry — 106 string comparisons for `Math.max`, 120
6128 // for `Object.keys` — because those tables are ordered for ENUMERATION (V8's
6129 // own order for `Math`/`Number`/`Reflect`), not for lookup. Eight probes
6130 // now. The index is built once per process and derived FROM those tables, so
6131 // it cannot drift from them.
6132 //
6133 // That is an operation count, not a measured time, and NO wall-clock win is
6134 // claimed. Re-measured in isolation (this hunk alone applied to the previous
6135 // commit, interleaved against it, minimums over ten rounds each): the A/B
6136 // ratio came out 0.753, 1.072, 0.994 and 0.744 across four repeats, while
6137 // the A/A control — the SAME binary under both labels — came out 1.084,
6138 // 1.072, 0.787 and 1.093. The A/B spread lies inside the A/A spread, so on
6139 // this machine the change is not distinguishable from noise. It is kept for
6140 // the comparison count and because it cannot drift from the tables it is
6141 // derived from, not because anything got faster.
6142 static SORTED: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
6143 let sorted = SORTED.get_or_init(|| {
6144 let mut v: Vec<&'static str> = GLOBAL_FUNCS
6145 .iter()
6146 .chain(NS_METHODS.iter())
6147 .copied()
6148 .collect();
6149 v.sort_unstable();
6150 v
6151 });
6152 sorted.binary_search(&name).is_ok() || is_namespace(name) || crate::stdlib::is_method(name)
6153}
6154
6155// ── dynamic functions (runtime source → callable) ────────────────────────────
6156
6157/// Build a callable from a complete function-expression source text — the ONE
6158/// dynamic-function generator on this frontend.
6159///
6160/// `src` is the exact source V8 synthesizes for the construct, WITHOUT the
6161/// wrapping parentheses needed to parse it as an expression: those are added
6162/// here, and `src` itself is retained so `Function.prototype.toString` reports
6163/// what V8 reports. The two callers synthesize different text and both shapes
6164/// are observable — see `stdlib::vm::compile_function` for the measured diff.
6165///
6166/// The body runs in the MODULE scope, never the constructing function's scope
6167/// (20.2.1.1.1 step 26 instantiates a dynamic function's body against the
6168/// *global* environment). That also makes a `var` inside the body a function
6169/// local: measured on node v26.7.0, `new Function('a','var zz = 5; return zz + a')`
6170/// returns 6 and leaves `globalThis.zz` `undefined`.
6171pub fn dynamic_function(src: &str) -> Result<Value, String> {
6172 let f = crate::eval_in_global_scope(&format!("({src})"))?;
6173 with_host(|h| {
6174 let s = h.new_str(src.to_string());
6175 h.set_fn_prop(&f, "@@source", s);
6176 });
6177 Ok(f)
6178}
6179
6180/// `new Function(p1, …, pN, body)` / `Function(p1, …, pN, body)`.
6181///
6182/// Argument convention (20.2.1.1.1): the LAST argument is the body and the rest
6183/// are parameter-list fragments joined with `,` — so a fragment may itself hold
6184/// several parameters (`new Function('a,b', 'c', …)` takes three). With no
6185/// arguments at all, both the parameter list and the body are empty.
6186///
6187/// Measured on node v26.7.0:
6188///
6189/// ```text
6190/// new Function('a','b','return a+b').toString() === 'function anonymous(a,b\n) {\nreturn a+b\n}'
6191/// new Function().toString() === 'function anonymous(\n) {\n\n}'
6192/// new Function('a,b','c','return [a,b,c]').length === 3
6193/// new Function('a','b','return a+b').name === 'anonymous'
6194/// ```
6195pub fn function_ctor(args: &[Value]) -> Result<Value, String> {
6196 let parts: Vec<String> = args.iter().map(|a| with_host(|h| h.str_of(a))).collect();
6197 let (params, body) = match parts.split_last() {
6198 Some((body, params)) => (params.join(","), body.clone()),
6199 None => (String::new(), String::new()),
6200 };
6201 dynamic_function(&format!("function anonymous({params}\n) {{\n{body}\n}}"))
6202}
6203
6204/// `eval(src)`. `direct` selects the scope the source runs in: a DIRECT eval —
6205/// the literal `eval(...)` call form — evaluates in the CALLER's scope, every
6206/// other route to the same function value is an INDIRECT eval and evaluates in
6207/// the global scope (ECMA-262 19.2.1.1 `PerformEval`). The two are told apart in
6208/// `host::call_named`, which `ops::CALL` reaches and `ops::CALL_VALUE`/`APPLY`
6209/// do not.
6210///
6211/// A non-string argument is returned unchanged (19.2.1.1 step 2).
6212pub fn eval_source(arg: Option<&Value>, direct: bool) -> Result<Value, String> {
6213 let v = arg.cloned().unwrap_or(Value::Undef);
6214 let is_string =
6215 matches!(v, Value::Str(_)) || with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_))));
6216 if !is_string {
6217 return Ok(v);
6218 }
6219 let src = with_host(|h| h.str_of(&v));
6220 // A DIRECT eval inherits the caller's strictness (19.2.1.1 step 10), which
6221 // decides both the early errors the COMPILE raises and the variable
6222 // environment below. An INDIRECT one is global-scope sloppy code.
6223 let caller_strict = direct && with_host(|h| h.current_strict());
6224 let chunk = crate::load_merged(crate::compile_completion_strict(&src, caller_strict)?);
6225 if !direct {
6226 return host::run_chunk_in_global_scope(chunk);
6227 }
6228 // A STRICT direct eval gets its OWN variable environment (19.2.1.1 step 12),
6229 // so its `var`s and function declarations die with it. Only a SLOPPY one
6230 // shares the caller's, which is the form that can inject a binding — and
6231 // sharing it unconditionally meant `eval('var x=1')` inside strict code
6232 // left `x` behind.
6233 let strict = caller_strict
6234 || src.trim_start().starts_with("'use strict'")
6235 || src.trim_start().starts_with("\"use strict\"");
6236 if !strict {
6237 // 19.2.1.1 steps 12-13: a SLOPPY direct eval shares the caller's
6238 // VARIABLE environment — which is what lets `eval('var x=1')` inject a
6239 // binding — but gets a fresh LEXICAL one of its own. A `let`, `const`
6240 // or `class` declared inside therefore dies with the eval; every one of
6241 // them was landing in the caller's scope, so `eval('let a=1')` left `a`
6242 // behind and `let a=1; eval('let a=2')` overwrote it.
6243 //
6244 // `push_scope` is exactly that split: `var` and a hoisted function
6245 // declaration bind to `base_env`, which this does not touch.
6246 with_host(|h| h.push_scope());
6247 let out = host::run_chunk_on(chunk);
6248 with_host(|h| h.pop_scope());
6249 return out;
6250 }
6251 let prev = with_host(|h| h.push_var_scope());
6252 let out = host::run_chunk_on(chunk);
6253 with_host(|h| h.pop_var_scope(prev));
6254 out
6255}
6256
6257/// Call a resolved builtin function (global or `namespace.method`).
6258pub fn call_builtin_function(name: &str, args: Vec<Value>) -> Result<Value, String> {
6259 // `require(spec)`: the ENTRY script's top-level require — core module first,
6260 // else the CommonJS loader resolving from the entry file's directory.
6261 if name == "require" {
6262 let spec = with_host(|h| h.str_of(&arg0(&args)));
6263 return crate::module::require(&spec, &crate::module::entry_dir());
6264 }
6265 // `__cjs_require(spec, fromDir)`: a per-module `require` closure's dispatch
6266 // into the loader, resolving `spec` against the module's own directory.
6267 if name == "__cjs_require" {
6268 let spec = with_host(|h| h.str_of(&arg0(&args)));
6269 let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6270 return crate::module::require(&spec, std::path::Path::new(&from));
6271 }
6272 if name == "process.memoryUsage.rss" {
6273 return Ok(crate::stdlib::process::memory_usage_rss());
6274 }
6275 if name == "require.resolve.paths" {
6276 let spec = with_host(|h| h.str_of(&arg0(&args)));
6277 // A core module is not looked up on disk at all.
6278 if crate::stdlib::is_core(&spec) {
6279 return Ok(with_host(|h| h.null()));
6280 }
6281 let dirs = crate::module::resolve_paths(&spec, &crate::module::entry_dir());
6282 return Ok(with_host(|h| {
6283 let items: Vec<Value> = dirs.into_iter().map(|d| h.new_str(d)).collect();
6284 h.new_array(items)
6285 }));
6286 }
6287 // A `require.extensions` entry. This runtime's loader does not dispatch
6288 // through the map, so calling one is the loader's own behaviour for that
6289 // extension rather than a hook point.
6290 if let Some(ext) = name.strip_prefix("@@extension:") {
6291 let _ = ext;
6292 return Ok(Value::Undef);
6293 }
6294 // `require.resolve(spec)` at the ENTRY level: resolve from the entry dir.
6295 if name == "require.resolve" {
6296 let spec = with_host(|h| h.str_of(&arg0(&args)));
6297 if crate::stdlib::is_core(&spec) {
6298 return Ok(with_host(|h| h.new_str(spec)));
6299 }
6300 return match crate::module::resolve(&spec, &crate::module::entry_dir()) {
6301 Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6302 None => Err(crate::host::plain_coded_error(
6303 "Error",
6304 "MODULE_NOT_FOUND",
6305 &format!("Cannot find module '{spec}'"),
6306 )),
6307 };
6308 }
6309 // `__cjs_resolve(spec, fromDir)`: `require.resolve` — the resolved absolute
6310 // path (core modules resolve to the bare specifier, as in Node).
6311 if name == "__cjs_resolve" {
6312 let spec = with_host(|h| h.str_of(&arg0(&args)));
6313 let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6314 if crate::stdlib::is_core(&spec) {
6315 return Ok(with_host(|h| h.new_str(spec)));
6316 }
6317 return match crate::module::resolve(&spec, std::path::Path::new(&from)) {
6318 Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6319 None => Err(crate::host::plain_coded_error(
6320 "Error",
6321 "MODULE_NOT_FOUND",
6322 &format!("Cannot find module '{spec}'"),
6323 )),
6324 };
6325 }
6326 // `Error.captureStackTrace(target[, ctor])`: V8's stack capture. Sets
6327 // `target.stack`; when a custom `Error.prepareStackTrace` is installed (the
6328 // stack-introspection pattern used by `depd`), it is called with a synthetic
6329 // CallSite array and its result becomes `.stack`, else `.stack` is a string.
6330 if name == "Error.captureStackTrace" {
6331 let target = arg0(&args);
6332 let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
6333 let stack = match prep {
6334 Some(f)
6335 if matches!(
6336 with_host(|h| h.get(&f).cloned()),
6337 Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
6338 ) =>
6339 {
6340 let sites = crate::module::callsite_stack(10)?;
6341 host::invoke(&f, vec![target.clone(), sites], None)?
6342 }
6343 _ => with_host(|h| h.new_str("")),
6344 };
6345 let _ = set_property(&target, "stack", stack);
6346 return Ok(Value::Undef);
6347 }
6348 // Native stdlib module methods (path/os/fs/util/assert/crypto/buffer/url).
6349 if let Some(r) = crate::stdlib::call(name, &args) {
6350 return r;
6351 }
6352 match name {
6353 // Node's DEFAULT `Error.prepareStackTrace`: the `Name: message` header
6354 // followed by one ` at <site>` line per call site. Reachable because
6355 // the read hands the hook out, and a library may call it directly to
6356 // render a stack it captured.
6357 DEFAULT_PREPARE => {
6358 let err = arg0(&args);
6359 let header = with_host(|h| {
6360 let name = host::lookup_chain(h, &err, "name")
6361 .map(|v| h.str_of(&v))
6362 .unwrap_or_else(|| "Error".to_string());
6363 let msg = host::lookup_chain(h, &err, "message")
6364 .map(|v| h.str_of(&v))
6365 .unwrap_or_default();
6366 if msg.is_empty() {
6367 name
6368 } else {
6369 format!("{name}: {msg}")
6370 }
6371 });
6372 let sites = args.get(1).cloned().unwrap_or(Value::Undef);
6373 let lines = with_host(|h| match h.get(&sites) {
6374 Some(JsObj::Array(items)) => items.clone(),
6375 _ => Vec::new(),
6376 });
6377 let mut out = header;
6378 for s in lines {
6379 let rendered = host::to_string_value(&s)
6380 .map(|v| with_host(|h| h.str_of(&v)))
6381 .unwrap_or_default();
6382 out.push_str("\n at ");
6383 out.push_str(&rendered);
6384 }
6385 Ok(with_host(|h| h.new_str(out)))
6386 }
6387 "console.log" | "console.info" | "console.debug" => {
6388 print_line(&args, false)?;
6389 Ok(Value::Undef)
6390 }
6391 "console.error" | "console.warn" => {
6392 print_line(&args, true)?;
6393 Ok(Value::Undef)
6394 }
6395 "parseInt" | "Number.parseInt" => Ok(Value::Float(parse_int(&args)?)),
6396 "parseFloat" | "Number.parseFloat" => Ok(Value::Float(parse_float(&args)?)),
6397 // `isNaN`/`isFinite` are `ToNumber(x)` too (19.2.3/4).
6398 "isNaN" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_nan())),
6399 "isFinite" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_finite())),
6400 "encodeURIComponent" => uri_encode(&arg_to_string(&args, 0)?, false),
6401 "encodeURI" => uri_encode(&arg_to_string(&args, 0)?, true),
6402 "decodeURIComponent" => uri_decode(&arg_to_string(&args, 0)?, false),
6403 "decodeURI" => uri_decode(&arg_to_string(&args, 0)?, true),
6404 "escape" => legacy_escape(&with_host(|h| h.str_of(&arg0(&args)))),
6405 "unescape" => legacy_unescape(&with_host(|h| h.str_of(&arg0(&args)))),
6406 // Reaching `eval` through this table means the eval FUNCTION VALUE was
6407 // called — `(0, eval)(src)`, `const e = eval; e(src)`, `[eval][0](src)`.
6408 // Those are INDIRECT evals and run in the global scope. A literal
6409 // `eval(src)` is intercepted earlier, in `host::call_named`.
6410 "eval" => eval_source(args.first(), false),
6411 // `new Function(...)` and `Function(...)` are the same operation
6412 // (20.2.1.1 `CreateDynamicFunction` is reached from both [[Call]] and
6413 // [[Construct]]), so both route to the one generator.
6414 "Function" => function_ctor(&args),
6415 // `Buffer(arg[, encodingOrOffset[, length]])` — the deprecated call form
6416 // (DEP0005). Node still supports it and still routes it to the same place
6417 // `new Buffer` goes, which is why `safe-buffer`'s legacy `SafeBuffer`
6418 // wrapper is just `return Buffer(arg, encodingOrOffset, length)`. Measured
6419 // on node v26.7.0: `Buffer('abc').toString() === 'abc'`,
6420 // `Buffer([1,2]).toString('hex') === '0102'`, `Buffer(3).length === 3`.
6421 // Node emits DEP0005 once, on stderr, through the same one-shot machinery
6422 // `url.parse`'s DEP0169 uses, so this does too rather than staying silent
6423 // where Node warns.
6424 "Buffer" => {
6425 crate::stdlib::process::emit_deprecation_warning(
6426 "DEP0005",
6427 "Buffer() is deprecated due to security and usability issues. \
6428 Please use the Buffer.alloc(), Buffer.allocUnsafe(), or \
6429 Buffer.from() methods instead.",
6430 );
6431 crate::stdlib::construct("Buffer", &args)
6432 .unwrap_or_else(|| Err(host::type_error("Buffer is not a function")))
6433 }
6434 "Number.isInteger" => Ok(Value::Bool(is_integer(arg0(&args)))),
6435 "Number.isSafeInteger" => Ok(Value::Bool(is_safe_integer(arg0(&args)))),
6436 "Number.isNaN" => Ok(Value::Bool(
6437 matches!(arg0(&args), Value::Float(f) if f.is_nan()),
6438 )),
6439 "Number.isFinite" => Ok(Value::Bool(
6440 matches!(arg0(&args), Value::Float(f) if f.is_finite())
6441 || matches!(arg0(&args), Value::Int(_)),
6442 )),
6443 "String" => {
6444 if args.is_empty() {
6445 Ok(with_host(|h| h.new_str("")))
6446 } else {
6447 // A symbol argument stringifies to `Symbol(desc)` (explicit String()
6448 // is allowed); everything else via ToString method dispatch.
6449 host::string_ctor_value(&args[0])
6450 }
6451 }
6452 // `Number(v)` is NOT plain ToNumber: 21.1.1.1 step 2 converts the object
6453 // first and then explicitly ACCEPTS a BigInt, returning its mathematical
6454 // value as a Number. Only `Number` does — `+v` and `Math.abs(v)` reject
6455 // one — which is why this cannot just call `to_number_value`.
6456 "Number" => Ok(Value::Float(if args.is_empty() {
6457 0.0
6458 } else {
6459 let prim = host::to_primitive(&args[0], "number")?;
6460 match with_host(|h| h.as_bigint(&prim)) {
6461 Some(b) => host::bigint_to_f64(&b),
6462 None => host::to_number_value(&prim)?,
6463 }
6464 })),
6465 "BigInt" => bigint_ctor(&arg0(&args)),
6466 "RegExp" => regexp_ctor(&args),
6467 "BigInt.asIntN" | "BigInt.asUintN" => bigint_as_n(name.ends_with("asUintN"), &args),
6468 "Boolean" => Ok(Value::Bool(with_host(|h| h.truthy(&arg0(&args))))),
6469 // Each argument is truncated to a uint16 and taken as one code UNIT, so
6470 // `String.fromCharCode(0x1D4B3)` is U+D4B3, NOT the astral U+1D4B3, and
6471 // a surrogate PAIR of arguments composes into one character.
6472 "String.fromCharCode" => Ok(with_host(|h| {
6473 let units: Vec<u16> = args
6474 .iter()
6475 .map(|a| crate::utf16::to_uint16(h.to_number(a)))
6476 .collect();
6477 let s = crate::utf16::to_string_lossy(&units);
6478 h.new_str(s)
6479 })),
6480 // `fromCodePoint` takes whole code POINTS and rejects anything that is
6481 // not one — including a lone surrogate, which `fromCharCode` accepts.
6482 "String.fromCodePoint" => {
6483 let mut s = String::new();
6484 for a in &args {
6485 let n = with_host(|h| h.to_number(a));
6486 let cp = if n.is_finite() && n.trunc() == n && (0.0..=0x10FFFF as f64).contains(&n)
6487 {
6488 char::from_u32(n as u32)
6489 } else {
6490 None
6491 };
6492 match cp {
6493 Some(c) => s.push(c),
6494 None => {
6495 return Err(format!(
6496 "RangeError: Invalid code point {}",
6497 with_host(|h| h.str_of(a))
6498 ))
6499 }
6500 }
6501 }
6502 Ok(new_s(s))
6503 }
6504 "String.raw" => string_raw(&args),
6505 // `Array(5)` === `new Array(5)` (length-5 empty), but `Array.of(5)` is `[5]`.
6506 "Array" => construct_builtin("Array", args),
6507 "Array.of" => construct_array_like(host::current_static_this(), args),
6508 // 23.1.2.2 `IsArray` follows a Proxy to its `[[ProxyTarget]]` rather than
6509 // consulting any trap, so `Array.isArray(new Proxy([], {}))` is `true`.
6510 "Array.isArray" => {
6511 let v = arg0(&args);
6512 let subject = crate::proxy::ultimate_target(&v).unwrap_or(v);
6513 Ok(Value::Bool(
6514 matches!(
6515 with_host(|h| h.get(&subject).cloned()),
6516 Some(JsObj::Array(_))
6517 ) && !is_arguments(&subject),
6518 ))
6519 }
6520 "Array.from" => array_from(args),
6521 "Array.fromAsync" => array_from_async(args),
6522 "Object" => Ok(object_call(args)),
6523 "Object.keys" => object_keys(args, 0),
6524 "Object.values" => object_keys(args, 1),
6525 "Object.entries" => object_keys(args, 2),
6526 "Object.assign" => object_assign(args),
6527 "Object.freeze" => {
6528 let v = arg0(&args);
6529 reject_sealing_a_view(&v, "freeze")?;
6530 if seal_proxy(&v, true)? {
6531 return Ok(v);
6532 }
6533 with_host(|h| h.seal_object(&v, true));
6534 Ok(v)
6535 }
6536 "Object.seal" => {
6537 let v = arg0(&args);
6538 reject_sealing_a_view(&v, "seal")?;
6539 if seal_proxy(&v, false)? {
6540 return Ok(v);
6541 }
6542 with_host(|h| h.seal_object(&v, false));
6543 Ok(v)
6544 }
6545 "Object.preventExtensions" => {
6546 let v = arg0(&args);
6547 if crate::proxy::prevent_extensions(&v)? {
6548 return Ok(v);
6549 }
6550 with_host(|h| h.prevent_extensions(&v));
6551 Ok(v)
6552 }
6553 // A PRIMITIVE has no integrity to speak of and 7.3.15/16 answer for it
6554 // without coercion: it is not extensible, and vacuously frozen and
6555 // sealed. Reporting it extensible and unfrozen was the opposite of
6556 // every one of the three.
6557 "Object.isFrozen" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6558 "Object.isSealed" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6559 "Object.isExtensible" if is_primitive_arg(&args) => Ok(Value::Bool(false)),
6560 "Object.isFrozen" => integrity_level(&arg0(&args), true),
6561 "Object.isSealed" => integrity_level(&arg0(&args), false),
6562 "Object.isExtensible" => {
6563 let v = arg0(&args);
6564 match crate::proxy::is_extensible(&v)? {
6565 Some(b) => Ok(Value::Bool(b)),
6566 None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6567 }
6568 }
6569 // Object.is — SameValue: like `===` but NaN is equal to NaN and +0 is
6570 // distinct from -0.
6571 "Object.is" => {
6572 let a = arg0(&args);
6573 let b = args.get(1).cloned().unwrap_or(Value::Undef);
6574 let num = |v: &Value| match v {
6575 Value::Int(n) => Some(*n as f64),
6576 Value::Float(f) => Some(*f),
6577 _ => None,
6578 };
6579 let r = match (num(&a), num(&b)) {
6580 (Some(x), Some(y)) => {
6581 if x.is_nan() && y.is_nan() {
6582 true
6583 } else if x == 0.0 && y == 0.0 {
6584 x.is_sign_negative() == y.is_sign_negative()
6585 } else {
6586 x == y
6587 }
6588 }
6589 _ => with_host(|h| h.strict_eq(&a, &b)),
6590 };
6591 Ok(Value::Bool(r))
6592 }
6593 "Object.fromEntries" => object_from_entries(args),
6594 // `[[GetPrototypeOf]]`: a Proxy answers from its trap (which may throw),
6595 // so the proxy form cannot share `prototype_of`'s infallible signature.
6596 // `Object.getPrototypeOf` coerces a primitive to its wrapper and
6597 // answers; `Reflect.getPrototypeOf` requires an object (28.1.8).
6598 "Object.getPrototypeOf" | "Reflect.getPrototypeOf" => {
6599 if name == "Reflect.getPrototypeOf" {
6600 reflect_require_object(&arg0(&args), "getPrototypeOf")?;
6601 }
6602 let v = arg0(&args);
6603 match crate::proxy::get_prototype_of(&v)? {
6604 Some(p) => Ok(p),
6605 None => Ok(prototype_of(&v)),
6606 }
6607 }
6608 "Object.setPrototypeOf" => {
6609 let obj = arg0(&args);
6610 let proto = args.get(1).cloned().unwrap_or(Value::Undef);
6611 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6612 reject_bad_prototype(&proto)?;
6613 crate::proxy::set_prototype_of(&obj, &proto)?;
6614 return Ok(obj);
6615 }
6616 // 20.1.2.23: `RequireObjectCoercible` on the target, then the
6617 // prototype type check, then — only for an actual object target —
6618 // the extensibility check. A PRIMITIVE target is returned untouched
6619 // (`Object.setPrototypeOf(1, {})` is `1`), which is why the
6620 // extensibility test cannot come first.
6621 if with_host(|h| matches!(obj, Value::Undef) || h.is_null(&obj)) {
6622 return Err(host::type_error(
6623 "Object.setPrototypeOf called on null or undefined",
6624 ));
6625 }
6626 reject_bad_prototype(&proto)?;
6627 if with_host(|h| is_object_like(h, &obj)) {
6628 // Setting the SAME prototype is a no-op and stays legal even on a
6629 // frozen object: node v26.7.0 accepts
6630 // `Object.setPrototypeOf(Object.freeze({}), Object.prototype)`.
6631 // `prototype_of`, not `proto_of`: an object with no EXPLICIT
6632 // link still has `Object.prototype`, and comparing against the
6633 // absent link would call that a change.
6634 if would_cycle(&obj, &proto) {
6635 return Err(host::type_error("Cyclic __proto__ value"));
6636 }
6637 if !same_prototype(&obj, &proto) && !with_host(|h| h.is_extensible(&obj)) {
6638 // The receiver is named by its brand, as every other
6639 // refusal names it — a NULL-PROTOTYPE object is
6640 // `[object Object]`, not `#<Object>`, because it has no
6641 // constructor to name.
6642 return Err(host::type_error(&format!(
6643 "{} is not extensible",
6644 no_side_effects_string(&obj)
6645 )));
6646 }
6647 with_host(|h| h.set_proto(&obj, proto));
6648 }
6649 Ok(obj)
6650 }
6651 "Object.create" => object_create(args),
6652 "Object.getOwnPropertyNames" => object_keys(args, 3),
6653 "Object.getOwnPropertySymbols" => {
6654 let v = arg0(&args);
6655 require_object_coercible(&v)?;
6656 let syms = proxy_or_own_symbol_keys(&v)?;
6657 Ok(with_host(|h| h.new_array(syms)))
6658 }
6659 // `Object.hasOwn(obj, key)` — the static form of `hasOwnProperty`.
6660 "Object.hasOwn" => {
6661 let obj = arg0(&args);
6662 let key = args.get(1).cloned().unwrap_or(Value::Undef);
6663 object_builtin_method(&obj, "hasOwnProperty", vec![key])
6664 }
6665 "Object.defineProperty" => object_define_property(args),
6666 "Object.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6667 "Object.getOwnPropertyDescriptors" => object_get_own_descriptors(args),
6668 "Object.defineProperties" => object_define_properties(args),
6669 // `Object.groupBy(items, cb)` (ES2024): group into a null-prototype object
6670 // keyed by `ToPropertyKey(cb(item, i))`, each value an array of members.
6671 "Object.groupBy" => object_group_by(args),
6672 "Symbol" => Ok(with_host(|h| {
6673 let desc = args
6674 .first()
6675 .filter(|a| !matches!(a, Value::Undef))
6676 .map(|a| h.str_of(a));
6677 h.new_symbol(desc)
6678 })),
6679 "Symbol.for" => Ok(with_host(|h| {
6680 let key = h.str_of(&arg0(&args));
6681 h.symbol_for(&key)
6682 })),
6683 // `Symbol.keyFor(sym)` (20.4.2.6) is a REGISTRY lookup, not a
6684 // description read: it answers only for symbols `Symbol.for` created.
6685 // Returning the description made every symbol look registered —
6686 // `Symbol.keyFor(Symbol("k"))` was `"k"` where node says `undefined`.
6687 "Symbol.keyFor" => Ok(with_host(|h| h.symbol_registry_key(&arg0(&args)))),
6688 "Map" | "WeakMap" | "Set" | "WeakSet" | "Promise" => construct_builtin(name, args),
6689 // `Proxy` has no `[[Call]]` slot: it is constructor-only (28.2.1).
6690 "Proxy" => Err(host::type_error("Constructor Proxy requires 'new'")),
6691 "Proxy.revocable" => crate::proxy::revocable(&args),
6692 // `Reflect.ownKeys` reports EVERY own key, non-enumerable included —
6693 // the same set as `getOwnPropertyNames` (node-js has no symbol-keyed
6694 // own properties, so there is no second half to append).
6695 // `Reflect.ownKeys` is `OwnPropertyKeys` (7.3.23): every own key,
6696 // non-enumerable included, strings first and then the SYMBOLS.
6697 "Reflect.ownKeys" => {
6698 let v = arg0(&args);
6699 reflect_require_object(&v, "ownKeys")?;
6700 let names = object_keys(args, 3)?;
6701 let syms = proxy_or_own_symbol_keys(&v)?;
6702 if syms.is_empty() {
6703 return Ok(names);
6704 }
6705 let mut all = with_host(|h| h.iter_vec(&names)).unwrap_or_default();
6706 all.extend(syms);
6707 Ok(with_host(|h| h.new_array(all)))
6708 }
6709 "Reflect.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6710 // `Reflect.defineProperty` REPORTS success as a boolean where
6711 // `Object.defineProperty` throws (28.1.3). It was propagating the
6712 // throw, so the whole point of the reflective form was lost.
6713 "Reflect.defineProperty" => {
6714 reflect_require_object(&arg0(&args), "defineProperty")?;
6715 Ok(Value::Bool(object_define_property(args).is_ok()))
6716 }
6717 "Reflect.deleteProperty" => {
6718 let obj = arg0(&args);
6719 reflect_require_object(&obj, "deleteProperty")?;
6720 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6721 Ok(Value::Bool(delete_property(&obj, &k)?))
6722 }
6723 "Reflect.setPrototypeOf" => {
6724 let obj = arg0(&args);
6725 let p = args.get(1).cloned().unwrap_or(Value::Undef);
6726 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6727 crate::proxy::set_prototype_of(&obj, &p)?;
6728 return Ok(Value::Bool(true));
6729 }
6730 // 10.1.2.1: a NON-EXTENSIBLE object refuses a prototype change —
6731 // unless the new one is what it already has, which is a no-op. It
6732 // reported success and rewrote the link.
6733 // `Reflect` reports a refusal rather than throwing, for a cycle as
6734 // for a non-extensible receiver.
6735 if would_cycle(&obj, &p) {
6736 return Ok(Value::Bool(false));
6737 }
6738 if !with_host(|h| h.is_extensible(&obj)) {
6739 return Ok(Value::Bool(same_prototype(&obj, &p)));
6740 }
6741 with_host(|h| h.set_proto(&obj, p));
6742 Ok(Value::Bool(true))
6743 }
6744 "Reflect.isExtensible" => {
6745 let v = arg0(&args);
6746 match crate::proxy::is_extensible(&v)? {
6747 Some(b) => Ok(Value::Bool(b)),
6748 None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6749 }
6750 }
6751 "Reflect.preventExtensions" => {
6752 let v = arg0(&args);
6753 if crate::proxy::prevent_extensions(&v)? {
6754 return Ok(Value::Bool(true));
6755 }
6756 with_host(|h| h.prevent_extensions(&v));
6757 Ok(Value::Bool(true))
6758 }
6759 // `Reflect.apply(target, thisArg, argsList)` / `Reflect.construct(t, a)`.
6760 "Reflect.apply" => {
6761 let f = arg0(&args);
6762 let this = args.get(1).cloned();
6763 let list = create_list_from_array_like(&args.get(2).cloned().unwrap_or(Value::Undef))?;
6764 host::invoke(&f, list, this.filter(|t| !with_host(|h| h.is_nullish(t))))
6765 }
6766 // `Reflect.construct(target, args, newTarget)` — the optional third
6767 // argument decides which constructor's `prototype` the instance gets
6768 // (28.1.2). It was ignored, so the result always inherited from
6769 // `target` and `instanceof newTarget` was false.
6770 "Reflect.construct" => {
6771 let f = arg0(&args);
6772 let list = create_list_from_array_like(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6773 let new_target = args.get(2).cloned().unwrap_or_else(|| f.clone());
6774 host::construct_nt(&f, list, new_target)
6775 }
6776 "Reflect.has" => {
6777 let obj = arg0(&args);
6778 reflect_require_object(&obj, "has")?;
6779 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6780 Ok(Value::Bool(has_property(&obj, &k)?))
6781 }
6782 // `Reflect.get(target, key, receiver)` — the optional third argument is
6783 // what a getter sees as `this` (28.1.6). Defaults to the target.
6784 "Reflect.get" => {
6785 let obj = arg0(&args);
6786 reflect_require_object(&obj, "get")?;
6787 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6788 let receiver = args.get(2).cloned().unwrap_or_else(|| obj.clone());
6789 get_property_recv(&obj, &k, &receiver)
6790 }
6791 // `Reflect.set(target, key, value, receiver)` — the optional fourth
6792 // argument is what a setter sees as `this`, and where a DATA property
6793 // lands (28.1.13). It was ignored: the setter ran against the target
6794 // and the property was written there.
6795 "Reflect.set" => {
6796 let obj = arg0(&args);
6797 reflect_require_object(&obj, "set")?;
6798 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6799 let v = args.get(2).cloned().unwrap_or(Value::Undef);
6800 let receiver = args.get(3).cloned().unwrap_or_else(|| obj.clone());
6801 Ok(Value::Bool(set_with_receiver(&obj, &k, v, &receiver)?))
6802 }
6803 "JSON.stringify" => json_stringify(args),
6804 "JSON.parse" => json_parse(args),
6805 "JSON.rawJSON" => json_raw(args),
6806 "JSON.isRawJSON" => json_is_raw(args),
6807 "structuredClone" => structured_clone(args),
6808 // The deferred drain a `Readable.from` schedules; the suffix is the
6809 // stream's heap index.
6810 _ if name.starts_with("@@transformCb:") => {
6811 let idx: u32 = name["@@transformCb:".len()..].parse().unwrap_or(0);
6812 crate::stdlib::stream::transform_callback(&Value::Obj(idx), &args)?;
6813 Ok(Value::Undef)
6814 }
6815 _ if name.starts_with("@@streamFlush:") => {
6816 let idx: u32 = name["@@streamFlush:".len()..].parse().unwrap_or(0);
6817 crate::stdlib::stream::flush_from(&Value::Obj(idx))?;
6818 Ok(Value::Undef)
6819 }
6820 // Same implementation the `buffer` module exposes; only the binding was
6821 // missing.
6822 "btoa" | "atob" => crate::stdlib::buffer::module_call(name, &args)
6823 .unwrap_or_else(|| Err(host::type_error(&format!("{name} is not a function")))),
6824 "fetch" => crate::stdlib::fetch::fetch(&args),
6825 // An `AbortSignal.timeout` deadline reached its macrotask: the thunk's
6826 // suffix is the signal's heap index.
6827 _ if name.starts_with("@@aborttimeout:") => {
6828 let idx: u32 = name["@@aborttimeout:".len()..].parse().unwrap_or(0);
6829 crate::stdlib::fetch::fire_timeout_abort(idx)
6830 }
6831 // The `callback` handed to a `new Writable({ write(chunk, enc, cb) })`
6832 // implementation. Nothing here waits on backpressure, so it only has to
6833 // BE callable — an implementation that ends with `cb()`, which the
6834 // stream contract requires, would otherwise throw.
6835 "@@streamWriteCallback" => Ok(Value::Undef),
6836 "queueMicrotask" | "process.nextTick" => {
6837 let cb = arg0(&args);
6838 require_callback(&cb)?;
6839 let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
6840 enqueue_microtask(name == "process.nextTick", cb, rest);
6841 Ok(Value::Undef)
6842 }
6843 "setTimeout" | "setInterval" | "setImmediate" => {
6844 require_callback(&arg0(&args))?;
6845 Ok(schedule_timer(name, args))
6846 }
6847 "clearTimeout" | "clearInterval" | "clearImmediate" => {
6848 clear_timer(&arg0(&args));
6849 Ok(Value::Undef)
6850 }
6851 "Promise.resolve" => promise_resolve(arg0(&args)),
6852 "Promise.reject" => promise_reject(arg0(&args)),
6853 "Promise.all" => promise_all(args, AllMode::All),
6854 "Promise.allSettled" => promise_all(args, AllMode::AllSettled),
6855 "Promise.race" => promise_race(args, false),
6856 "Promise.any" => promise_race(args, true),
6857 // `Promise.withResolvers()` (ES2024): a new pending promise plus its own
6858 // resolve/reject functions, returned as `{ promise, resolve, reject }`.
6859 "Promise.withResolvers" => promise_with_resolvers(),
6860 "Promise.try" => promise_try(args),
6861 "RegExp.escape" => regexp_escape(args),
6862 "Error.isError" => error_is_error(args),
6863 // `Map.groupBy(items, cb)` (ES2024): group into a `Map` keyed by the raw
6864 // `cb(item, i)` result (SameValueZero), each value an array of members.
6865 "Map.groupBy" => map_group_by(args),
6866 n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
6867 _ if name.starts_with("Math.") => math_fn(&name[5..], &args),
6868 // Internal continuations (Promise resolve/reject fns, `.finally` wrappers).
6869 // The executor a species-constructed promise is built with: it does
6870 // nothing, because the caller settles the result through its id.
6871 "@@pnoop" => Ok(Value::Undef),
6872 _ if name.starts_with("@@presolve:") => {
6873 let id: u32 = name[11..].parse().unwrap_or(0);
6874 host::resolve_promise_val(id, arg0(&args));
6875 Ok(Value::Undef)
6876 }
6877 _ if name.starts_with("@@preject:") => {
6878 let id: u32 = name[10..].parse().unwrap_or(0);
6879 host::reject_promise_val(id, arg0(&args));
6880 Ok(Value::Undef)
6881 }
6882 // The revoker `Proxy.revocable` hands back, keyed by the proxy's heap
6883 // index so calling it twice is the spec's no-op rather than a re-tear.
6884 _ if name.starts_with("@@prevoke:") => {
6885 let i: u32 = name[10..].parse().unwrap_or(0);
6886 Ok(crate::proxy::revoke(i))
6887 }
6888 _ if name.starts_with("@@finpass:") => {
6889 // finally(cb) on fulfill: run cb, await whatever it returned, then
6890 // pass the original value through.
6891 let i: u32 = name["@@finpass:".len()..].parse().unwrap_or(0);
6892 let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6893 Ok(finally_chain(result, arg0(&args), false))
6894 }
6895 _ if name.starts_with("@@finthrow:") => {
6896 // finally(cb) on reject: same, then re-throw the original reason.
6897 let i: u32 = name["@@finthrow:".len()..].parse().unwrap_or(0);
6898 let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6899 Ok(finally_chain(result, arg0(&args), true))
6900 }
6901 // The two thunks `finally_chain` hangs off that awaited promise. Each
6902 // carries the value it must reinstate in a one-slot cell, since a
6903 // builtin is identified only by its name and cannot close over one.
6904 _ if name.starts_with("@@finret:") => {
6905 let i: u32 = name["@@finret:".len()..].parse().unwrap_or(0);
6906 get_property(&Value::Obj(i), "0")
6907 }
6908 _ if name.starts_with("@@finrethrow:") => {
6909 let i: u32 = name["@@finrethrow:".len()..].parse().unwrap_or(0);
6910 let reason = get_property(&Value::Obj(i), "0")?;
6911 with_host(|h| h.exc = Some(reason.clone()));
6912 Err(with_host(|h| error_string(h, &reason)))
6913 }
6914 _ => Err(host::type_error(&format!("{name} is not a function"))),
6915 }
6916}
6917
6918/// `BigInt(x)`: convert a boolean/number/string/bigint to a BigInt. A
6919/// non-integer number is a `RangeError`; an unparseable string a `SyntaxError`
6920/// (matching Node's messages).
6921/// V8 names the offending value: `BigInt(undefined)` is `Cannot convert
6922/// undefined to a BigInt`, `BigInt({})` is `Cannot convert [object Object] to a
6923/// BigInt`. The old text said "value" literally, for every input.
6924fn bigint_convert_error(v: &Value) -> String {
6925 let shown = with_host(|h| h.str_of(v));
6926 host::type_error(&format!("Cannot convert {shown} to a BigInt"))
6927}
6928
6929/// `ToBigInt(v)` — 7.1.13. The conversion every BigInt-typed SINK performs: a
6930/// 64-bit typed array's element write, `DataView.prototype.setBigInt64`, and
6931/// BigInt arithmetic's operand check.
6932///
6933/// It is NOT `BigInt(v)`: a Number is a `TypeError` here (`BigInt(1)` is `1n`,
6934/// but `new BigInt64Array(1)[0] = 1` throws), which is the whole point of the
6935/// separate abstract op. Everything else follows `ToPrimitive(v, number)` then
6936/// the type table — booleans convert (`true` → `1n`), strings parse with a
6937/// `SyntaxError` on failure, and `undefined`/`null`/symbols throw.
6938///
6939/// Measured on node v26.8.1, receiver `new BigInt64Array(1)`:
6940///
6941/// ```text
6942/// a[0] = true → 1n
6943/// a[0] = '12' → 12n
6944/// a[0] = [] → 0n (ToPrimitive → "" → 0n)
6945/// a[0] = ['3'] → 3n
6946/// a[0] = 1 → TypeError: Cannot convert 1 to a BigInt
6947/// a[0] = new Number(3) → TypeError: Cannot convert 3 to a BigInt
6948/// a[0] = 'a' → SyntaxError: Cannot convert a to a BigInt
6949/// a[0] = {} → SyntaxError: Cannot convert [object Object] to a BigInt
6950/// ```
6951pub fn to_bigint(v: &Value) -> Result<num_bigint::BigInt, String> {
6952 let prim = host::to_primitive(v, "number")?;
6953 if let Some(b) = with_host(|h| match h.get(&prim) {
6954 Some(JsObj::BigInt(b)) => Some(b.clone()),
6955 _ => None,
6956 }) {
6957 return Ok(b);
6958 }
6959 match &prim {
6960 Value::Bool(b) => Ok(num_bigint::BigInt::from(*b as i64)),
6961 Value::Str(s) => host::parse_bigint_str(s)
6962 .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt")),
6963 _ if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) => {
6964 let s = with_host(|h| h.str_of(&prim));
6965 host::parse_bigint_str(&s)
6966 .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt"))
6967 }
6968 _ => Err(bigint_convert_error(&prim)),
6969 }
6970}
6971
6972fn bigint_ctor(v: &Value) -> Result<Value, String> {
6973 use num_bigint::BigInt;
6974 let big = match v {
6975 Value::Bool(b) => BigInt::from(*b as i64),
6976 Value::Int(n) => BigInt::from(*n),
6977 Value::Float(f) => {
6978 if !f.is_finite() || f.fract() != 0.0 {
6979 let disp = with_host(|h| h.str_of(v));
6980 return Err(format!(
6981 "RangeError: The number {disp} cannot be converted to a BigInt because it is not an integer"
6982 ));
6983 }
6984 // The decimal EXPANSION, not `fmt_number`: `Number.prototype
6985 // .toString` switches to exponential notation at 1e21, and
6986 // `BigInt::parse_bytes` cannot read `"1e+21"` — so `BigInt(1e21)`
6987 // threw `Cannot convert value to a BigInt` where node returns
6988 // `1000000000000000000000n`. `{:.0}` prints an integral f64's exact
6989 // value, which is also what node reports for a magnitude past the
6990 // exactly-representable range (`BigInt(1e30)` is
6991 // `1000000000000000019884624838656n` in both).
6992 match BigInt::parse_bytes(format!("{f:.0}").as_bytes(), 10) {
6993 Some(b) => b,
6994 None => return Err(bigint_convert_error(v)),
6995 }
6996 }
6997 Value::Str(s) => match host::parse_bigint_str(s) {
6998 Some(b) => b,
6999 None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
7000 },
7001 Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
7002 Some(JsObj::BigInt(b)) => b,
7003 Some(JsObj::Str(s)) => match host::parse_bigint_str(&s) {
7004 Some(b) => b,
7005 None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
7006 },
7007 _ => return Err(bigint_convert_error(v)),
7008 },
7009 _ => return Err(bigint_convert_error(v)),
7010 };
7011 Ok(with_host(|h| h.new_bigint(big)))
7012}
7013
7014/// `new RegExp(source[, flags])` / `RegExp(...)`. A first `RegExp` argument copies
7015/// its source (and flags, unless new ones are given).
7016fn regexp_ctor(args: &[Value]) -> Result<Value, String> {
7017 let (source, existing_flags) = match with_host(|h| h.get(&arg0(args)).cloned()) {
7018 Some(JsObj::RegExp(r)) => (r.source.clone(), Some(r.flags.clone())),
7019 _ => {
7020 let a0 = arg0(args);
7021 // 22.2.4.1 step 9 is `ToString(pattern)`, which a SYMBOL refuses —
7022 // `new RegExp(sym)` was compiling the text `Symbol(d)` into a
7023 // pattern instead of throwing.
7024 let src = if matches!(a0, Value::Undef) {
7025 String::new()
7026 } else {
7027 arg_to_string(args, 0)?
7028 };
7029 (src, None)
7030 }
7031 };
7032 let flags = match args.get(1) {
7033 Some(v) if !matches!(v, Value::Undef) => arg_to_string(args, 1)?,
7034 _ => existing_flags.unwrap_or_default(),
7035 };
7036 // An empty source compiles as the JS canonical `(?:)`.
7037 let src = if source.is_empty() {
7038 "(?:)".to_string()
7039 } else {
7040 source
7041 };
7042 crate::regexp::build_regexp(&src, &flags)
7043}
7044
7045/// `BigInt.asIntN(bits, x)` / `BigInt.asUintN(bits, x)`: wrap `x` to a `bits`-wide
7046/// two's-complement (signed) or unsigned integer.
7047fn bigint_as_n(unsigned: bool, args: &[Value]) -> Result<Value, String> {
7048 use num_bigint::BigInt;
7049 use num_traits::Signed;
7050 let bits = with_host(|h| h.to_number(&arg0(args))) as i64;
7051 if bits < 0 {
7052 return Err("RangeError: Invalid value: not (convertible to) a safe integer".into());
7053 }
7054 let x = match with_host(|h| h.as_bigint(&args.get(1).cloned().unwrap_or(Value::Undef))) {
7055 Some(b) => b,
7056 None => return Err(host::type_error("Cannot convert to a BigInt")),
7057 };
7058 let bits = bits as u32;
7059 if bits == 0 {
7060 return Ok(with_host(|h| h.new_bigint(BigInt::from(0))));
7061 }
7062 let modulus = BigInt::from(1) << bits; // 2^bits
7063 // Reduce into [0, 2^bits); for the signed form fold the top half negative.
7064 let mut r = &x % &modulus;
7065 if r.is_negative() {
7066 r += &modulus;
7067 }
7068 if !unsigned {
7069 let half = BigInt::from(1) << (bits - 1);
7070 if r >= half {
7071 r -= &modulus;
7072 }
7073 }
7074 Ok(with_host(|h| h.new_bigint(r)))
7075}
7076
7077/// `String.raw(callSite, ...subs)`: concatenate the raw quasis (`callSite.raw`)
7078/// interleaved with the substitutions.
7079fn string_raw(args: &[Value]) -> Result<Value, String> {
7080 let call_site = arg0(args);
7081 let raw = get_property(&call_site, "raw")?;
7082 let raws = with_host(|h| h.iter_vec(&raw)).unwrap_or_default();
7083 let mut out = String::new();
7084 for (i, r) in raws.iter().enumerate() {
7085 out.push_str(&with_host(|h| h.str_of(r)));
7086 if i + 1 < raws.len() {
7087 if let Some(sub) = args.get(i + 1) {
7088 out.push_str(&with_host(|h| h.str_of(sub)));
7089 }
7090 }
7091 }
7092 Ok(with_host(|h| h.new_str(out)))
7093}
7094
7095/// `Object(x)`: box/pass-through — for our model, non-object args just return a
7096/// fresh object; objects pass through.
7097/// Whether `v`'s own properties live in the fn-prop SIDE TABLE rather than in a
7098/// property map. A `Map`/`Set`/`Promise`/`RegExp`/generator/symbol/bigint is an
7099/// ordinary object that also has internal slots, so it can carry own properties
7100/// like anything else — but its heap variant holds only those slots, so a write
7101/// had nowhere to go and vanished: `m.x = 5` left `m.x` undefined.
7102pub fn uses_side_table(v: &Value) -> bool {
7103 matches!(
7104 with_host(|h| h.kind_of(v)),
7105 Some(
7106 ObjKind::Map
7107 | ObjKind::Set
7108 | ObjKind::Promise
7109 | ObjKind::RegExp
7110 | ObjKind::Generator
7111 | ObjKind::Symbol
7112 | ObjKind::BigInt
7113 | ObjKind::Iter
7114 )
7115 )
7116}
7117
7118fn object_call(args: Vec<Value>) -> Value {
7119 let a = arg0(&args);
7120 // `Object(v)` is `ToObject(v)` (20.1.1.1): a primitive comes back BOXED,
7121 // not replaced by an empty object. `Object(1).valueOf()` was `undefined`.
7122 if matches!(a, Value::Undef) || with_host(|h| h.is_null(&a)) {
7123 return with_host(|h| h.new_object(IndexMap::new()));
7124 }
7125 to_object(&a)
7126}
7127
7128/// The name of the wrapper a primitive boxes into, or `None` when the value is
7129/// already an object.
7130fn wrapper_ctor_of(v: &Value) -> Option<&'static str> {
7131 match v {
7132 Value::Int(_) | Value::Float(_) => Some("Number"),
7133 Value::Bool(_) => Some("Boolean"),
7134 Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
7135 Some(JsObj::Str(_)) => Some("String"),
7136 Some(JsObj::Symbol { .. }) => Some("Symbol"),
7137 Some(JsObj::BigInt(_)) => Some("BigInt"),
7138 _ => None,
7139 },
7140 _ => None,
7141 }
7142}
7143
7144/// The primitive a wrapper object boxes (`new String("a")` → `"a"`), or `None`
7145/// for every other value. The slot is a hidden `@@primitive` own property —
7146/// the same `@@` marker convention the engine already uses for internal state,
7147/// so it stays out of `Object.keys` and `JSON.stringify` on its own.
7148pub fn wrapped_primitive(v: &Value) -> Option<Value> {
7149 with_host(|h| match h.get(v) {
7150 Some(JsObj::Object(p)) => p.get("@@primitive").cloned(),
7151 _ => None,
7152 })
7153}
7154
7155/// `ToObject(v)` (7.1.18) for a primitive: the wrapper object with the matching
7156/// prototype and a `[[StringData]]`/`[[NumberData]]`/`[[BooleanData]]` slot.
7157///
7158/// A String wrapper also owns its index properties and `length`, which is what
7159/// makes `w[0]`, `w.length` and `Object.keys(w)` answer; all of them are
7160/// non-writable and non-configurable, as the exotic `String` object's are.
7161pub fn to_object(v: &Value) -> Value {
7162 let Some(ctor) = wrapper_ctor_of(v) else {
7163 return v.clone();
7164 };
7165 with_host(|h| h.ensure_wrapper_protos());
7166 let chars: Vec<String> = if ctor == "String" {
7167 with_host(|h| h.str_of(v))
7168 .chars()
7169 .map(|c| c.to_string())
7170 .collect()
7171 } else {
7172 Vec::new()
7173 };
7174 with_host(|h| {
7175 let mut m: IndexMap<String, Value> = IndexMap::new();
7176 for (i, c) in chars.iter().enumerate() {
7177 let s = h.new_str(c.clone());
7178 m.insert(i.to_string(), s);
7179 }
7180 let w = h.new_object(m);
7181 if ctor == "String" {
7182 for i in 0..chars.len() {
7183 h.set_prop_attrs(
7184 &w,
7185 &i.to_string(),
7186 host::PropAttrs {
7187 writable: false,
7188 enumerable: true,
7189 configurable: false,
7190 },
7191 );
7192 }
7193 let len = Value::Float(chars.len() as f64);
7194 if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7195 p.insert("length".into(), len);
7196 }
7197 h.set_prop_attrs(
7198 &w,
7199 "length",
7200 host::PropAttrs {
7201 writable: false,
7202 enumerable: false,
7203 configurable: false,
7204 },
7205 );
7206 }
7207 if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7208 p.insert("@@primitive".into(), v.clone());
7209 }
7210 if let Some(proto) = h.native_proto(ctor) {
7211 h.set_proto(&w, proto);
7212 }
7213 w
7214 })
7215}
7216
7217/// Construct via `new` for the builtin constructors.
7218pub fn construct_builtin(name: &str, args: Vec<Value>) -> Result<Value, String> {
7219 // Native stdlib constructors (`new URL(...)`, `new EventEmitter()`, `new Buffer(...)`).
7220 if let Some(r) = crate::stdlib::construct(name, &args) {
7221 return r;
7222 }
7223 match name {
7224 "Array" => {
7225 // `new Array(n)` -> length-n array; `new Array(a, b)` -> [a, b].
7226 // A single NUMBER argument is a length and is validated as one
7227 // (23.1.1.1 step 6), so `new Array(-1)` / `new Array(1.5)` /
7228 // `new Array(2**32)` are all `RangeError: Invalid array length` on
7229 // node v26.7.0; only a non-number single argument is an element.
7230 if args.len() == 1 {
7231 if let Value::Float(_) | Value::Int(_) = args[0] {
7232 let n = host::to_array_length(&args[0])?;
7233 // Every element of `new Array(n)` is a HOLE, not a stored
7234 // `undefined`: `Object.keys(Array(3))` is `[]`.
7235 return Ok(with_host(|h| {
7236 let a = h.new_array(vec![Value::Undef; n]);
7237 h.mark_hole_range(&a, 0..n);
7238 a
7239 }));
7240 }
7241 }
7242 Ok(with_host(|h| h.new_array(args)))
7243 }
7244 "Object" => Ok(object_call(args)),
7245 // `new String(v)` / `new Number(v)` / `new Boolean(v)` — the wrapper
7246 // form. These were not constructors at all, so every one threw.
7247 "String" => Ok(to_object(&host::to_string_value(
7248 &args
7249 .first()
7250 .cloned()
7251 .unwrap_or_else(|| with_host(|h| h.new_str(String::new()))),
7252 )?)),
7253 "Number" => Ok(to_object(&Value::Float(match args.first() {
7254 Some(a) => host::to_number_value(a)?,
7255 None => 0.0,
7256 }))),
7257 "Boolean" => Ok(to_object(&Value::Bool(with_host(|h| {
7258 h.truthy(&arg0(&args))
7259 })))),
7260 "Map" | "WeakMap" => {
7261 let weak = name == "WeakMap";
7262 let m = with_host(|h| {
7263 h.alloc(JsObj::Map {
7264 entries: indexmap::IndexMap::new(),
7265 weak,
7266 })
7267 });
7268 if let Some(init) = args
7269 .first()
7270 .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7271 {
7272 // Stepped, not drained: an entry that is not a pair has to
7273 // stop the construction at that element and CLOSE the iterator
7274 // (24.1.1.2 step 8). Materializing first meant a bad entry in an
7275 // infinite source was never reached and the constructor HUNG.
7276 host::iter_for_each(init, |p, _| {
7277 // 24.1.1.2 step 8.d: each entry must be an OBJECT. A string
7278 // is iterable, so without this check `new Map(["ab"])`
7279 // happily stored `'a' => 'b'` instead of throwing — and over
7280 // an infinite source it never stopped.
7281 if !with_host(|h| is_object_like(h, &p)) {
7282 let shown = with_host(|h| h.str_of(&p));
7283 return Err(host::type_error(&format!(
7284 "Iterator value {shown} is not an entry object"
7285 )));
7286 }
7287 // The entry is read by INDEX with `[[Get]]` (step 8.e), not
7288 // iterated: an object with a `Symbol.iterator` but no `0`/`1`
7289 // gives `undefined => undefined`, and an array-LIKE entry
7290 // works. Iterating it instead accepted a string as a pair
7291 // and rejected the array-like.
7292 let k = get_property(&p, "0")?;
7293 let v = get_property(&p, "1")?;
7294 map_method(&m, "set", vec![k, v])?;
7295 Ok(())
7296 })?;
7297 }
7298 Ok(m)
7299 }
7300 "Set" | "WeakSet" => {
7301 let weak = name == "WeakSet";
7302 let s = with_host(|h| {
7303 h.alloc(JsObj::Set {
7304 entries: indexmap::IndexMap::new(),
7305 weak,
7306 })
7307 });
7308 if let Some(init) = args
7309 .first()
7310 .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7311 {
7312 host::iter_for_each(init, |v, _| {
7313 set_method(&s, "add", vec![v])?;
7314 Ok(())
7315 })?;
7316 }
7317 Ok(s)
7318 }
7319 "Promise" => new_promise(arg0(&args)),
7320 "Proxy" => crate::proxy::create(&args),
7321 // `new Function(p…, body)` — the same `CreateDynamicFunction` the plain
7322 // call form runs (20.2.1.1). `depd`'s `wrapfunction` builds its
7323 // deprecation wrapper this way, so `require('body-parser')` — and with it
7324 // `require('express')` — dies at load without it.
7325 "Function" => function_ctor(&args),
7326 "RegExp" => regexp_ctor(&args),
7327 "BigInt" => Err(host::type_error("BigInt is not a constructor")),
7328 "Error" => make_error_checked(name, &args),
7329 // `new DOMException(message, name)` — the name is an ARGUMENT, and the
7330 // legacy numeric `code` follows from it.
7331 "DOMException" => Ok(dom_exception(&args)),
7332 n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
7333 _ => Err(host::type_error(&format!("{name} is not a constructor"))),
7334 }
7335}
7336
7337/// The legacy numeric `DOMException.code` a WHATWG error name maps to. A name
7338/// outside the table — including the default `"Error"` — reports 0.
7339pub const DOM_EXCEPTION_CODES: &[(&str, f64)] = &[
7340 ("IndexSizeError", 1.0),
7341 ("DOMStringSizeError", 2.0),
7342 ("HierarchyRequestError", 3.0),
7343 ("WrongDocumentError", 4.0),
7344 ("InvalidCharacterError", 5.0),
7345 ("NoDataAllowedError", 6.0),
7346 ("NoModificationAllowedError", 7.0),
7347 ("NotFoundError", 8.0),
7348 ("NotSupportedError", 9.0),
7349 ("InUseAttributeError", 10.0),
7350 ("InvalidStateError", 11.0),
7351 ("SyntaxError", 12.0),
7352 ("InvalidModificationError", 13.0),
7353 ("NamespaceError", 14.0),
7354 ("InvalidAccessError", 15.0),
7355 ("ValidationError", 16.0),
7356 ("TypeMismatchError", 17.0),
7357 ("SecurityError", 18.0),
7358 ("NetworkError", 19.0),
7359 ("AbortError", 20.0),
7360 ("URLMismatchError", 21.0),
7361 ("QuotaExceededError", 22.0),
7362 ("TimeoutError", 23.0),
7363 ("InvalidNodeTypeError", 24.0),
7364 ("DataCloneError", 25.0),
7365];
7366
7367/// The static name a `DOMException` code is exposed under: the error name minus
7368/// its `Error` suffix, upper-snake-cased, plus `_ERR` — `AbortError` becomes
7369/// `ABORT_ERR`, `IndexSizeError` becomes `INDEX_SIZE_ERR`.
7370fn legacy_code_name(error_name: &str) -> String {
7371 let stem = error_name.strip_suffix("Error").unwrap_or(error_name);
7372 let mut out = String::new();
7373 for (i, c) in stem.chars().enumerate() {
7374 if c.is_ascii_uppercase() && i > 0 {
7375 out.push('_');
7376 }
7377 out.push(c.to_ascii_uppercase());
7378 }
7379 out.push_str("_ERR");
7380 out
7381}
7382
7383/// `new DOMException(message, name)`.
7384///
7385/// The class node's `AbortSignal.reason` rejects with. Its `name` is the second
7386/// ARGUMENT (defaulting to `"Error"`), not the class name, and its `code` is the
7387/// legacy number that name maps to.
7388pub fn dom_exception(args: &[Value]) -> Value {
7389 let message = match args.first() {
7390 None | Some(Value::Undef) => String::new(),
7391 Some(v) => with_host(|h| h.str_of(v)),
7392 };
7393 let name = match args.get(1) {
7394 None | Some(Value::Undef) => "Error".to_string(),
7395 Some(v) => with_host(|h| h.str_of(v)),
7396 };
7397 with_host(|h| dom_exception_with(h, &name, &message))
7398}
7399
7400/// `dom_exception` for a caller that already holds the host borrow.
7401pub(crate) fn dom_exception_with(h: &mut host::JsHost, name: &str, message: &str) -> Value {
7402 let name = name.to_string();
7403 let message = message.to_string();
7404 let code = DOM_EXCEPTION_CODES
7405 .iter()
7406 .find(|(n, _)| *n == name)
7407 .map(|(_, c)| *c)
7408 .unwrap_or(0.0);
7409 let head = if message.is_empty() {
7410 name.clone()
7411 } else {
7412 format!("{name}: {message}")
7413 };
7414 let e = synth_error(h, &head);
7415 {
7416 let nv = h.new_str(name);
7417 let mv = h.new_str(message);
7418 let sv = h.new_str(head);
7419 if let Some(JsObj::Object(p)) = h.get_mut(&e) {
7420 // `name`, `message` and `code` are PROTOTYPE accessors over internal
7421 // slots in node, so `stack` is the instance's only own property.
7422 // Storing them as own keys would show up in
7423 // `Object.getOwnPropertyNames`, which reports just `['stack']`.
7424 p.shift_remove("message");
7425 p.insert("@@domName".into(), nv);
7426 p.insert("@@domMessage".into(), mv);
7427 p.insert("@@domCode".into(), Value::Float(code));
7428 p.insert("stack".into(), sv);
7429 }
7430 h.ensure_error_protos();
7431 if let Some(proto) = host::error_proto_of(h, "DOMException") {
7432 h.set_proto(&e, proto);
7433 }
7434 }
7435 e
7436}
7437
7438/// A `DOMException`'s `name`/`message`/`code`, which live in internal slots
7439/// rather than as own properties. `None` for anything else.
7440pub fn dom_exception_slot(recv: &Value, name: &str) -> Option<Value> {
7441 let slot = match name {
7442 "name" => "@@domName",
7443 "message" => "@@domMessage",
7444 "code" => "@@domCode",
7445 _ => return None,
7446 };
7447 with_host(|h| match h.get(recv) {
7448 Some(JsObj::Object(p)) if p.contains_key("@@domName") => p.get(slot).cloned(),
7449 _ => None,
7450 })
7451}
7452
7453/// Build an `Error` object carrying `msg`, for stdlib callers that need to
7454/// throw a value with extra own properties on it.
7455pub(crate) fn make_error_pub(name: &str, msg: &str) -> Value {
7456 let m = with_host(|h| h.new_str(msg.to_string()));
7457 make_error_inner(name, &[m])
7458}
7459
7460/// [`make_error`] with the message's `ToString` allowed to FAIL. A symbol
7461/// refuses it (20.5.1.1 step 3), so `new Error(sym)` is a TypeError where this
7462/// rendered `Symbol(desc)` into `.message`.
7463fn make_error_checked(name: &str, args: &[Value]) -> Result<Value, String> {
7464 if let Some(m) = args.first().filter(|m| !matches!(m, Value::Undef)) {
7465 // AggregateError's message is its SECOND argument.
7466 let idx = usize::from(name == "AggregateError");
7467 if idx == 0 {
7468 host::to_string_value(m)?;
7469 } else if let Some(m2) = args.get(idx).filter(|m| !matches!(m, Value::Undef)) {
7470 host::to_string_value(m2)?;
7471 }
7472 }
7473 Ok(make_error_inner(name, args))
7474}
7475
7476fn make_error_inner(name: &str, args: &[Value]) -> Value {
7477 // `new AggregateError(errors, message)` takes the causes FIRST; every other
7478 // error constructor takes the message first.
7479 let agg = name == "AggregateError";
7480 let (errors, args) = if agg {
7481 (
7482 Some(args.first().cloned().unwrap_or(Value::Undef)),
7483 args.get(1..).unwrap_or(&[]),
7484 )
7485 } else {
7486 (None, args)
7487 };
7488 with_host(|h| {
7489 h.ensure_error_protos();
7490 let mut props: IndexMap<String, Value> = IndexMap::new();
7491 let msg = args
7492 .first()
7493 .filter(|a| !matches!(a, Value::Undef))
7494 .map(|a| h.str_of(a));
7495 if let Some(m) = &msg {
7496 let mv = h.new_str(m.clone());
7497 props.insert("message".into(), mv);
7498 }
7499 // `.stack` is engine-specific; a simple `Name: message` header line
7500 // suffices for parity (the fuzzer never prints raw stacks).
7501 //
7502 // V8 formats that header LAZILY, on the first read, from whatever `name`
7503 // and `message` the error carries at that moment — which is why the
7504 // near-universal
7505 //
7506 // class MyErr extends Error { constructor(m) { super(m); this.name = 'MyErr'; } }
7507 //
7508 // reports `MyErr: boom` and not the `Error: boom` this built eagerly,
7509 // inside `super()`, before the subclass had renamed anything. `@@stackRaw`
7510 // carries the frames so the read can redo it; see `materialize_stack`.
7511 let frames = h.stack_frames();
7512 let stack = match &msg {
7513 Some(m) if !m.is_empty() => format!("{name}: {m}{frames}"),
7514 _ => format!("{name}{frames}"),
7515 };
7516 let sv = h.new_str(stack);
7517 props.insert("stack".into(), sv);
7518 let raw = h.new_str(frames);
7519 props.insert("@@stackRaw".into(), raw);
7520 if let Some(errs) = errors {
7521 // Materialize the iterable into the own `errors` array property.
7522 let items = h.iter_vec(&errs).unwrap_or_default();
7523 let arr = h.new_array(items);
7524 props.insert("errors".into(), arr);
7525 }
7526 // `new Error(msg, { cause })` (ES2022): installed only when the options
7527 // bag actually has a `cause` key, so `new Error(m, {})` leaves none.
7528 let opts = args.get(1);
7529 if let Some(cause) = opts.and_then(|o| match h.get(o) {
7530 Some(JsObj::Object(p)) => p.get("cause").cloned(),
7531 _ => None,
7532 }) {
7533 props.insert("cause".into(), cause);
7534 }
7535 let e = h.new_object(props);
7536 if let Some(p) = host::error_proto_of(h, name) {
7537 h.set_proto(&e, p);
7538 }
7539 // Every own slot an error constructor installs is non-enumerable in V8,
7540 // which is why `Object.keys(err)` is `[]` and `JSON.stringify(err)` is
7541 // `{}` — properties a *script* later assigns stay enumerable.
7542 for k in ["message", "stack", "errors", "cause", "@@stackRaw"] {
7543 h.hide_prop(&e, k);
7544 }
7545 e
7546 })
7547}
7548
7549fn print_line(args: &[Value], stderr: bool) -> Result<(), String> {
7550 // Node's console.log(...args) === util.format(...args): printf-style
7551 // substitution when the first arg is a format string, else inspect-and-join.
7552 // A directive can THROW (`console.log('%j', 1n)`), and node lets that reach
7553 // the caller instead of printing a line — so nothing is written on failure.
7554 let line: String = crate::stdlib::util::format(args)?;
7555 with_host(|h| h.write_out(&format!("{line}\n"), stderr));
7556 Ok(())
7557}
7558
7559fn arg0(args: &[Value]) -> Value {
7560 args.first().cloned().unwrap_or(Value::Undef)
7561}
7562/// `ToString(arg)` for a builtin's argument — fallible, because a SYMBOL
7563/// refuses the conversion (7.1.17). Every site that reached for `str_of`
7564/// instead rendered `Symbol(desc)` into its result and reported nothing.
7565fn arg_to_string(args: &[Value], i: usize) -> Result<String, String> {
7566 let v = args.get(i).cloned().unwrap_or(Value::Undef);
7567 let sv = host::to_string_value(&v)?;
7568 Ok(with_host(|h| h.str_of(&sv)))
7569}
7570
7571fn arg_num(args: &[Value], i: usize) -> f64 {
7572 with_host(|h| h.to_number(&args.get(i).cloned().unwrap_or(Value::Undef)))
7573}
7574
7575fn is_integer(v: Value) -> bool {
7576 match v {
7577 Value::Int(_) => true,
7578 Value::Float(f) => f.is_finite() && f.fract() == 0.0,
7579 _ => false,
7580 }
7581}
7582fn is_safe_integer(v: Value) -> bool {
7583 match v {
7584 Value::Float(f) => f.is_finite() && f.fract() == 0.0 && f.abs() <= 9007199254740991.0,
7585 Value::Int(_) => true,
7586 _ => false,
7587 }
7588}
7589
7590/// `encodeURI`/`encodeURIComponent`: percent-encode `s`'s UTF-8 bytes, leaving
7591/// the unreserved set unescaped. `encodeURI` additionally preserves the reserved
7592/// URI characters (`;,/?:@&=+$#`) that delimit a URI's structure.
7593fn uri_encode(s: &str, uri: bool) -> Result<Value, String> {
7594 // Always-unescaped (`encodeURIComponent`'s unreserved set), per the spec.
7595 const UNRESERVED: &[u8] =
7596 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
7597 // Reserved characters `encodeURI` leaves intact on top of the unreserved set.
7598 const RESERVED: &[u8] = b";,/?:@&=+$#";
7599 let mut out = String::with_capacity(s.len());
7600 for &b in s.as_bytes() {
7601 if UNRESERVED.contains(&b) || (uri && RESERVED.contains(&b)) {
7602 out.push(b as char);
7603 } else {
7604 out.push('%');
7605 out.push(
7606 char::from_digit((b >> 4) as u32, 16)
7607 .unwrap()
7608 .to_ascii_uppercase(),
7609 );
7610 out.push(
7611 char::from_digit((b & 0xf) as u32, 16)
7612 .unwrap()
7613 .to_ascii_uppercase(),
7614 );
7615 }
7616 }
7617 Ok(with_host(|h| h.new_str(out)))
7618}
7619
7620/// `decodeURI`/`decodeURIComponent`: reverse `%XX` escapes back to UTF-8 text.
7621/// For `decodeURI`, escapes of the reserved delimiters are left as-is (the spec's
7622/// asymmetry with `encodeURI`). Throws `URIError` on a malformed escape.
7623fn uri_decode(s: &str, uri: bool) -> Result<Value, String> {
7624 const RESERVED: &[u8] = b";,/?:@&=+$#";
7625 let bytes = s.as_bytes();
7626 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
7627 let mut i = 0;
7628 while i < bytes.len() {
7629 if bytes[i] == b'%' {
7630 if i + 2 >= bytes.len() {
7631 return Err("URIError: URI malformed".into());
7632 }
7633 let hi = (bytes[i + 1] as char).to_digit(16);
7634 let lo = (bytes[i + 2] as char).to_digit(16);
7635 match (hi, lo) {
7636 (Some(h), Some(l)) => {
7637 let byte = (h * 16 + l) as u8;
7638 // decodeURI keeps reserved-delimiter escapes literal.
7639 if uri && RESERVED.contains(&byte) {
7640 out.extend_from_slice(&bytes[i..i + 3]);
7641 } else {
7642 out.push(byte);
7643 }
7644 i += 3;
7645 }
7646 _ => return Err("URIError: URI malformed".into()),
7647 }
7648 } else {
7649 out.push(bytes[i]);
7650 i += 1;
7651 }
7652 }
7653 match String::from_utf8(out) {
7654 Ok(decoded) => Ok(with_host(|h| h.new_str(decoded))),
7655 Err(_) => Err("URIError: URI malformed".into()),
7656 }
7657}
7658
7659/// `escape` (Annex B.2.1.1) — the pre-`encodeURIComponent` legacy encoder, still
7660/// present in every engine and still reached by old libraries (jQuery's cookie
7661/// plugin, `querystring`-era code). It works on UTF-16 CODE UNITS, not UTF-8
7662/// bytes, which is what separates it from `encodeURIComponent`: a unit below
7663/// `0x100` becomes `%XX`, anything above becomes `%uXXXX`, so an astral
7664/// character yields the two escapes of its surrogate pair
7665/// (`escape("\u{1D4B3}")` is `"%uD835%uDCB3"` on node v26.7.0).
7666///
7667/// The unescaped set is frozen by the spec and is NOT the URI unreserved set —
7668/// it keeps `@*_+-./` and drops `!~'()`.
7669fn legacy_escape(s: &str) -> Result<Value, String> {
7670 const KEEP: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";
7671 let mut out = String::with_capacity(s.len());
7672 for u in s.encode_utf16() {
7673 if u < 0x100 {
7674 if KEEP.contains(&(u as u8)) {
7675 out.push(u as u8 as char);
7676 } else {
7677 out.push_str(&format!("%{u:02X}"));
7678 }
7679 } else {
7680 out.push_str(&format!("%u{u:04X}"));
7681 }
7682 }
7683 Ok(with_host(|h| h.new_str(out)))
7684}
7685
7686/// `unescape` (Annex B.2.1.2) — the inverse of [`legacy_escape`]. Unlike
7687/// `decodeURIComponent` it never throws: a `%` that does not begin a well-formed
7688/// `%XX` or `%uXXXX` escape is passed through literally
7689/// (`unescape("%u0041%42%zz%2")` is `"AB%zz%2"` on node v26.7.0).
7690///
7691/// Decoding is done in code-unit space and re-joined at the end so a
7692/// `%uD835%uDCB3` pair recomposes into the one astral character it came from.
7693fn legacy_unescape(s: &str) -> Result<Value, String> {
7694 let b = s.as_bytes();
7695 let hex = |i: usize, n: usize| -> Option<u16> {
7696 if i + n > b.len() {
7697 return None;
7698 }
7699 let mut v: u16 = 0;
7700 for &c in &b[i..i + n] {
7701 v = v.checked_mul(16)? + (c as char).to_digit(16)? as u16;
7702 }
7703 Some(v)
7704 };
7705 let units: Vec<u16> = s.encode_utf16().collect();
7706 let mut out: Vec<u16> = Vec::with_capacity(units.len());
7707 let mut i = 0;
7708 while i < b.len() {
7709 // Escapes are pure ASCII, so a byte index is a unit index up to here —
7710 // but the tail may not be, so non-`%` bytes are re-decoded as chars.
7711 if b[i] == b'%' {
7712 if let Some(u) = hex(i + 1, 2) {
7713 out.push(u);
7714 i += 3;
7715 continue;
7716 }
7717 if b.get(i + 1) == Some(&b'u') {
7718 if let Some(u) = hex(i + 2, 4) {
7719 out.push(u);
7720 i += 6;
7721 continue;
7722 }
7723 }
7724 }
7725 let c = s[i..].chars().next().unwrap_or('%');
7726 let mut buf = [0u16; 2];
7727 out.extend_from_slice(c.encode_utf16(&mut buf));
7728 i += c.len_utf8();
7729 }
7730 Ok(with_host(|h| {
7731 h.new_str(crate::utf16::to_string_lossy(&out))
7732 }))
7733}
7734
7735/// `parseInt` begins with `ToString(argument)` (19.2.5 step 1), and that step can
7736/// THROW — a Symbol has no string form, so `parseInt([Symbol()])` is a TypeError
7737/// rather than `NaN`. Reading the argument with `str_of` took the object's brand
7738/// instead of converting it, which both swallowed that throw and ignored any
7739/// `toString` the value defines.
7740fn parse_int(args: &[Value]) -> Result<f64, String> {
7741 // Converted BEFORE the host borrow: `to_string_value` can call back into JS.
7742 let sv = host::to_string_value(&arg0(args))?;
7743 // 19.2.5 step 2 is `ToInt32(radix)`, which runs a user `valueOf` — the
7744 // infallible read below does no `ToPrimitive`, so an object radix came out
7745 // as NaN and the parse silently fell back to auto-detection.
7746 let radix = match args.get(1) {
7747 Some(r) if !matches!(r, Value::Undef) => {
7748 vec![arg0(args), Value::Float(to_number_arg(args, 1)?)]
7749 }
7750 _ => args.to_vec(),
7751 };
7752 Ok(parse_int_str(&with_host(|h| h.str_of(&sv)), &radix))
7753}
7754
7755fn parse_int_str(s: &str, args: &[Value]) -> f64 {
7756 // 19.2.5 step 8: an EXPLICIT radix outside 2..=36 is `NaN`, it does not fall
7757 // back to auto-detection. The old `.filter()` silently discarded a bad radix,
7758 // so `parseInt("10", 37)` answered 10 where every engine says NaN.
7759 let radix_arg = args
7760 .get(1)
7761 .map(|r| with_host(|h| host::to_int32(h.to_number(r))));
7762 let radix = match radix_arg {
7763 Some(0) | None => None,
7764 Some(r) if (2..=36).contains(&r) => Some(r as u32),
7765 Some(_) => return f64::NAN,
7766 };
7767 let t = crate::utf16::js_trim_start(s);
7768 let (neg, digits) = match t.strip_prefix('-') {
7769 Some(rest) => (true, rest),
7770 None => (false, t.strip_prefix('+').unwrap_or(t)),
7771 };
7772 let (radix, digits) = match radix {
7773 Some(16) => (
7774 16u32,
7775 digits
7776 .strip_prefix("0x")
7777 .or_else(|| digits.strip_prefix("0X"))
7778 .unwrap_or(digits),
7779 ),
7780 Some(r) => (r, digits),
7781 None => {
7782 if let Some(hex) = digits
7783 .strip_prefix("0x")
7784 .or_else(|| digits.strip_prefix("0X"))
7785 {
7786 (16, hex)
7787 } else {
7788 (10, digits)
7789 }
7790 }
7791 };
7792 let valid: String = digits.chars().take_while(|c| c.is_digit(radix)).collect();
7793 if valid.is_empty() {
7794 return f64::NAN;
7795 }
7796 // Accumulate in `f64`, not `i64`. `i64::from_str_radix` OVERFLOWS past ~19
7797 // digits and the error was mapped to `NaN`, so
7798 // `parseInt("999999999999999999999999")` was NaN instead of 1e+24. The spec
7799 // asks for the mathematical value rounded to a Number, which is what
7800 // repeated multiply-accumulate in `f64` produces.
7801 let n = if radix == 10 {
7802 // Rust's decimal float parser is correctly rounded; digit-by-digit
7803 // multiply-accumulate is not, and drifted a ULP on long inputs
7804 // (`parseInt("999999999999999999999999")` came out
7805 // 1.0000000000000003e+24 rather than 1e+24).
7806 valid.parse::<f64>().unwrap_or(f64::NAN)
7807 } else {
7808 let mut n = 0.0f64;
7809 for c in valid.chars() {
7810 n = n * radix as f64 + c.to_digit(radix).unwrap_or(0) as f64;
7811 }
7812 n
7813 };
7814 if neg {
7815 -n
7816 } else {
7817 n
7818 }
7819}
7820
7821/// `parseFloat` likewise starts from `ToString(argument)`; see `parse_int`.
7822fn parse_float(args: &[Value]) -> Result<f64, String> {
7823 let sv = host::to_string_value(&arg0(args))?;
7824 Ok(parse_float_str(&with_host(|h| h.str_of(&sv))))
7825}
7826
7827fn parse_float_str(s: &str) -> f64 {
7828 let t = crate::utf16::js_trim_start(s);
7829 // `Infinity` / `+Infinity` / `-Infinity` are valid parseFloat prefixes.
7830 let inf_body = t
7831 .strip_prefix('+')
7832 .or_else(|| t.strip_prefix('-'))
7833 .unwrap_or(t);
7834 if inf_body.starts_with("Infinity") {
7835 return if t.starts_with('-') {
7836 f64::NEG_INFINITY
7837 } else {
7838 f64::INFINITY
7839 };
7840 }
7841 // The LONGEST prefix that is itself a complete `StrDecimalLiteral`, which is
7842 // not the same as the longest run of characters that could appear in one:
7843 // `"1e"` and `"1e+"` are `1` in every engine, because the exponent part is
7844 // only valid once a digit follows `e`. Tracking `end` at every character
7845 // accepted the dangling `e`, `parse::<f64>` then failed, and the whole call
7846 // came back NaN.
7847 let mut end = 0;
7848 let bytes = t.as_bytes();
7849 let mut seen_dot = false;
7850 let mut seen_e = false;
7851 let mut digits_before_dot = false;
7852 for (i, &c) in bytes.iter().enumerate() {
7853 match c {
7854 b'0'..=b'9' => {
7855 if !seen_dot && !seen_e {
7856 digits_before_dot = true;
7857 }
7858 end = i + 1;
7859 }
7860 // A sign is only meaningful leading, or straight after the exponent
7861 // marker; it never completes a literal on its own.
7862 b'+' | b'-' if i == 0 || bytes[i - 1] == b'e' || bytes[i - 1] == b'E' => {}
7863 // `1.` is a complete literal; a bare `.` is not.
7864 b'.' if !seen_dot && !seen_e => {
7865 seen_dot = true;
7866 if digits_before_dot {
7867 end = i + 1;
7868 }
7869 }
7870 b'e' | b'E' if !seen_e && end > 0 => seen_e = true,
7871 _ => break,
7872 }
7873 }
7874 if end == 0 {
7875 return f64::NAN;
7876 }
7877 t[..end].parse::<f64>().unwrap_or(f64::NAN)
7878}
7879
7880/// ECMA-262 `Number::exponentiate` (6.1.6.1.3), backing both `Math.pow` and the
7881/// `**` operator. Three clauses differ from IEEE-754 `pow`, which is what Rust's
7882/// `powf` implements: a NaN exponent is NaN even for base 1, a NaN base is NaN
7883/// for any non-zero exponent, and `|base| == 1` with an infinite exponent is NaN
7884/// rather than 1.
7885pub(crate) fn js_pow(base: f64, exp: f64) -> f64 {
7886 if exp == 0.0 {
7887 return 1.0;
7888 }
7889 if base.is_nan() || exp.is_nan() {
7890 return f64::NAN;
7891 }
7892 if base.abs() == 1.0 && exp.is_infinite() {
7893 return f64::NAN;
7894 }
7895 base.powf(exp)
7896}
7897
7898fn math_fn(fname: &str, args: &[Value]) -> Result<Value, String> {
7899 // Every `Math` function coerces its arguments with `ToNumber`, and `ToNumber`
7900 // of a BigInt is a TypeError (7.1.4 step 2) — the whole point of BigInt being
7901 // a separate numeric type. `arg_num` reads a BigInt's magnitude instead, so
7902 // `Math.max(1n)` quietly answered 1 where V8 throws. `Math.random` is the one
7903 // exception: it never reads an argument, so `Math.random(1n)` is fine.
7904 // A BigInt WRAPPER converts to a BigInt and is rejected just as the
7905 // primitive is: `Math.abs(Object(9n))` is a TypeError where it answered NaN.
7906 // The boxed value is read BEFORE the borrow — `wrapped_primitive` borrows
7907 // the host itself and cannot run inside another borrow.
7908 let is_bigint = |a: &Value| {
7909 if with_host(|h| matches!(h.get(a), Some(JsObj::BigInt(_)))) {
7910 return true;
7911 }
7912 match wrapped_primitive(a) {
7913 Some(p) => with_host(|h| matches!(h.get(&p), Some(JsObj::BigInt(_)))),
7914 None => false,
7915 }
7916 };
7917 if fname != "random" && args.iter().any(is_bigint) {
7918 return Err(host::type_error(
7919 "Cannot convert a BigInt value to a number",
7920 ));
7921 }
7922 // Every argument is `ToNumber`d (21.3.2.x), which runs a user `valueOf` and
7923 // can throw from it. `arg_num` does no `ToPrimitive` at all, so
7924 // `Math.max({valueOf: () => 1}, 0)` answered NaN.
7925 // EVERY argument, not a fixed prefix: `Math.max`/`min`/`hypot` are
7926 // variadic, and coercing only the first few silently DROPPED the rest —
7927 // `Math.max(...gen)` over five values answered for four of them.
7928 let mut coerced = Vec::with_capacity(args.len());
7929 for a in args {
7930 if matches!(a, Value::Undef) {
7931 coerced.push(a.clone());
7932 continue;
7933 }
7934 let p = host::to_primitive(a, "number")?;
7935 coerced.push(Value::Float(with_host(|h| h.to_number(&p))));
7936 }
7937 let args: &[Value] = &coerced;
7938 let x = arg_num(args, 0);
7939 let r = match fname {
7940 "floor" => x.floor(),
7941 "ceil" => x.ceil(),
7942 // ECMA-262 `Math.round` (21.3.2.28) transcribed clause by clause. The
7943 // obvious `(x + 0.5).floor()` is NOT this function: the addition rounds
7944 // before the floor sees it, so it answers 1 for the largest double below
7945 // 0.5 (`Math.round(0.49999999999999994)` is 0 in every engine) and it
7946 // perturbs integers above 2^52, where `x + 0.5` is no longer
7947 // representable (`Math.round(4503599627370497)` must be the input).
7948 // Splitting the zero-band cases out first also carries the signed zero
7949 // the spec asks for without a post-hoc patch.
7950 "round" => {
7951 if !x.is_finite() || x == 0.0 {
7952 x
7953 } else if x > 0.0 && x < 0.5 {
7954 0.0
7955 } else if (-0.5..0.0).contains(&x) {
7956 -0.0
7957 } else {
7958 // |x| >= 0.5, so `floor` and the subtraction are both exact
7959 // (every double >= 2^52 is already an integer and yields 0 here).
7960 let f = x.floor();
7961 if x - f >= 0.5 {
7962 f + 1.0
7963 } else {
7964 f
7965 }
7966 }
7967 }
7968 "trunc" => x.trunc(),
7969 "abs" => x.abs(),
7970 "sign" => {
7971 if x.is_nan() {
7972 f64::NAN
7973 } else if x > 0.0 {
7974 1.0
7975 } else if x < 0.0 {
7976 -1.0
7977 } else {
7978 x
7979 }
7980 }
7981 "sqrt" => x.sqrt(),
7982 "cbrt" => x.cbrt(),
7983 "exp" => x.exp(),
7984 "log" => x.ln(),
7985 "log2" => x.log2(),
7986 "log10" => x.log10(),
7987 "sin" => x.sin(),
7988 "cos" => x.cos(),
7989 "tan" => x.tan(),
7990 "asin" => x.asin(),
7991 "acos" => x.acos(),
7992 "atan" => x.atan(),
7993 "atan2" => x.atan2(arg_num(args, 1)),
7994 // Rust `powf` is IEEE-754 `pow`, which is NOT JS `**`/`Math.pow`: IEEE
7995 // makes `pow(x, ±0)` and `pow(±1, y)` return 1 unconditionally, so
7996 // `(-1) ** Infinity` and `1 ** NaN` come back 1 where the spec
7997 // (6.1.6.1.3 Number::exponentiate) says NaN. Only the exponent-is-zero
7998 // clause is shared.
7999 "pow" => js_pow(x, arg_num(args, 1)),
8000 // Hyperbolics and the two precision-preserving log/exp forms.
8001 "sinh" => x.sinh(),
8002 "cosh" => x.cosh(),
8003 "tanh" => x.tanh(),
8004 "asinh" => x.asinh(),
8005 "acosh" => x.acosh(),
8006 "atanh" => x.atanh(),
8007 "log1p" => x.ln_1p(),
8008 "expm1" => x.exp_m1(),
8009 // C-style 32-bit integer multiply: ToInt32 both operands, multiply with
8010 // wraparound, reinterpret as a signed 32-bit result.
8011 "imul" => (host::to_int32(x).wrapping_mul(host::to_int32(arg_num(args, 1)))) as f64,
8012 "hypot" => {
8013 // Scale by the largest magnitude before squaring — this avoids the
8014 // last-ULP error of the naive `sqrt(Σ xᵢ²)` and matches V8's result.
8015 let xs: Vec<f64> = args.iter().map(|a| with_host(|h| h.to_number(a))).collect();
8016 let mut max = 0.0f64;
8017 for x in &xs {
8018 if x.abs() > max {
8019 max = x.abs();
8020 }
8021 }
8022 if xs.iter().any(|x| x.is_infinite()) {
8023 f64::INFINITY
8024 } else if max == 0.0 || !max.is_finite() {
8025 max
8026 } else {
8027 let s: f64 = xs.iter().map(|x| (x / max) * (x / max)).sum();
8028 max * s.sqrt()
8029 }
8030 }
8031 "random" => pseudo_random(),
8032 "max" => {
8033 if args.is_empty() {
8034 f64::NEG_INFINITY
8035 } else {
8036 let mut m = f64::NEG_INFINITY;
8037 for a in args {
8038 let n = with_host(|h| h.to_number(a));
8039 if n.is_nan() {
8040 return Ok(Value::Float(f64::NAN));
8041 }
8042 // `>` cannot separate the zeroes (`0.0 > -0.0` is false), but
8043 // the spec ranks +0 above -0, so `Math.max(-0, 0)` is +0 and
8044 // must not keep the -0 the first iteration installed.
8045 if n > m || (n == m && n == 0.0 && n.is_sign_positive()) {
8046 m = n;
8047 }
8048 }
8049 m
8050 }
8051 }
8052 "min" => {
8053 if args.is_empty() {
8054 f64::INFINITY
8055 } else {
8056 let mut m = f64::INFINITY;
8057 for a in args {
8058 let n = with_host(|h| h.to_number(a));
8059 if n.is_nan() {
8060 return Ok(Value::Float(f64::NAN));
8061 }
8062 // Mirror of `max`: -0 ranks below +0 even though `<` says
8063 // they are equal, so `Math.min(0, -0)` is -0.
8064 if n < m || (n == m && n == 0.0 && n.is_sign_negative()) {
8065 m = n;
8066 }
8067 }
8068 m
8069 }
8070 }
8071 // Count leading zero bits of ToUint32(x) (Math.clz32(1) === 31).
8072 "clz32" => {
8073 let u = if x.is_finite() {
8074 x.trunc().rem_euclid(4294967296.0) as u32
8075 } else {
8076 0
8077 };
8078 u.leading_zeros() as f64
8079 }
8080 // Round to the nearest single-precision float.
8081 "fround" => (x as f32) as f64,
8082 _ => return Err(host::type_error(&format!("Math.{fname} is not a function"))),
8083 };
8084 Ok(Value::Float(r))
8085}
8086
8087/// A small deterministic PRNG for `Math.random` (output is non-reproducible vs
8088/// Node by nature; kept simple).
8089fn pseudo_random() -> f64 {
8090 use std::cell::Cell;
8091 thread_local!(static SEED: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
8092 SEED.with(|s| {
8093 let mut x = s.get();
8094 x ^= x << 13;
8095 x ^= x >> 7;
8096 x ^= x << 17;
8097 s.set(x);
8098 (x >> 11) as f64 / (1u64 << 53) as f64
8099 })
8100}
8101
8102// ── Object.* ──────────────────────────────────────────────────────────────────
8103
8104/// The characters of a string PRIMITIVE, as the `ToObject` wrapper's own index
8105/// properties (10.4.3 `StringExoticObject`).
8106///
8107/// `getOwnPropertyDescriptor` begins with `ToObject`, which boxes a string into
8108/// an exotic object whose own keys are its code-unit indices plus `length`;
8109/// this is the descriptor half of that. (The KEY half lives in
8110/// `JsHost::own_enum_data_keys`, the single source every enumeration path
8111/// reads.) Indices are UTF-16 code units, matching `.length` and `s[i]`.
8112///
8113/// A boxed `String` object is deliberately NOT routed here: it can carry
8114/// ordinary own properties too (`const s = new String('ab'); s.x = 1`), and its
8115/// existing path already reports them alongside the indices.
8116fn string_primitive_units(v: &Value) -> Option<Vec<String>> {
8117 // A JS string primitive rides as a `Value::Obj` handle to `JsObj::Str` (see
8118 // `host.rs`); a BOXED `new String(...)` is a different heap object, so this
8119 // never catches one.
8120 let s = match v {
8121 Value::Str(s) => (**s).clone(),
8122 _ => with_host(|h| match h.get(v) {
8123 Some(JsObj::Str(s)) => Some(s.clone()),
8124 _ => None,
8125 })?,
8126 };
8127 let units = crate::utf16::Units::of(&s);
8128 Some((0..units.len()).filter_map(|i| units.unit_str(i)).collect())
8129}
8130
8131fn object_keys(args: Vec<Value>, mode: u8) -> Result<Value, String> {
8132 let v = arg0(&args);
8133 require_object_coercible(&v)?;
8134 // A Proxy answers from its `ownKeys` trap. `getOwnPropertyNames` (mode 3)
8135 // reports every own STRING key the trap named; the enumerating modes
8136 // additionally filter by each key's `[[GetOwnProperty]]`, so both traps run.
8137 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8138 if mode == 3 {
8139 let keys = crate::proxy::own_keys(&v)?.unwrap_or_default();
8140 return Ok(with_host(|h| {
8141 let out: Vec<Value> = keys
8142 .into_iter()
8143 .filter(|k| !host::is_symbol_key(k))
8144 .map(|k| h.new_str(k))
8145 .collect();
8146 h.new_array(out)
8147 }));
8148 }
8149 // `Object.keys` (mode 0) must run `ownKeys` and `getOwnPropertyDescriptor`
8150 // and STOP — 7.3.23 never performs `[[Get]]` when only keys are wanted.
8151 // Going through `own_enum_entries` fired the `get` trap once per key, so
8152 // the observable trap sequence carried a trailing `get` node does not
8153 // emit, and a trap with side effects ran when it should not have.
8154 if mode == 0 {
8155 let keys = crate::proxy::own_enum_string_keys(&v)?;
8156 return Ok(with_host(|h| {
8157 let out: Vec<Value> = keys.into_iter().map(|k| h.new_str(k)).collect();
8158 h.new_array(out)
8159 }));
8160 }
8161 let entries = crate::proxy::own_enum_entries(&v)?;
8162 return Ok(with_host(|h| {
8163 let out: Vec<Value> = entries
8164 .into_iter()
8165 .map(|(k, val)| match mode {
8166 0 => h.new_str(k),
8167 1 => val,
8168 _ => {
8169 let ks = h.new_str(k);
8170 h.new_array(vec![ks, val])
8171 }
8172 })
8173 .collect();
8174 h.new_array(out)
8175 }));
8176 }
8177 // An intrinsic prototype this host built as a REAL OBJECT — `Symbol
8178 // .prototype`, `String.prototype`, the error hierarchy — answers from the
8179 // generated table too. It was answering from its own property map instead,
8180 // which carries neither the right names nor V8's order: `Symbol.prototype`
8181 // reported `toLocaleString` and omitted `description`, and
8182 // `String.prototype` omitted `length` and every Annex B HTML method.
8183 //
8184 // `ns` is the namespace SPELLING, so the arm below is shared verbatim —
8185 // the two representations of a prototype cannot answer differently.
8186 let real_proto_ns = with_host(|h| h.intrinsic_proto_ctor(&v).map(|c| format!("{c}.prototype")))
8187 .filter(|ns| intrinsic_proto_members(ns).is_some());
8188 // A builtin prototype namespace that exposes enumerable methods for copying
8189 // (`Object.getOwnPropertyNames(EventEmitter.prototype)` — express's mixin).
8190 if let Some(ns) = real_proto_ns.or_else(|| {
8191 with_host(|h| match h.get(&v) {
8192 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
8193 _ => None,
8194 })
8195 }) {
8196 // An INTRINSIC prototype (`Map.prototype`, `URL.prototype`). Members are
8197 // non-enumerable on an ECMAScript builtin and enumerable on a WebIDL
8198 // interface, which the table records per name.
8199 if let Some(members) = intrinsic_proto_members(&ns) {
8200 let ctor = ns.trim_end_matches(".prototype");
8201 let mut names: Vec<String> = members
8202 .iter()
8203 .filter(|m| mode == 3 || m.starts_with('+'))
8204 .map(|m| m.strip_prefix('+').unwrap_or(m).to_string())
8205 // `getOwnPropertyNames` reports STRING keys only; the table's
8206 // `@@` entries are symbol-keyed members and belong to
8207 // `getOwnPropertySymbols` instead.
8208 .filter(|m| !m.starts_with("@@"))
8209 .collect();
8210 // Plus whatever a script patched onto this prototype under a NEW
8211 // name — an ordinary enumerable own property, so it lists in every
8212 // mode. Without it `Object.keys(Array.prototype)` stayed `[]` after
8213 // an assignment that `Array.prototype.patch` read back happily.
8214 //
8215 // Assigning over an EXISTING member is a `[[Set]]`, which leaves
8216 // that member's attributes alone: restoring a saved `join` must not
8217 // turn it into an enumerable key.
8218 for k in with_host(|h| h.builtin_static_keys(&ns)) {
8219 if !intrinsic_proto_member(&ns, &k) && !names.contains(&k) {
8220 names.push(k);
8221 }
8222 }
8223 return Ok(with_host(|h| {
8224 let out: Vec<Value> = names
8225 .iter()
8226 .map(|name| {
8227 // An accessor member has no thunk — `Map.prototype.size`
8228 // is not a function — so a VALUE read of one answers
8229 // undefined rather than synthesizing a callable.
8230 let val = |h: &mut host::JsHost| {
8231 if let Some(v) = h.builtin_static(&ns, name) {
8232 return v;
8233 }
8234 let key = format!("@proto:{ctor}:{name}");
8235 if builtin_meta(&key).is_some() {
8236 h.alloc(JsObj::Builtin(key))
8237 } else {
8238 Value::Undef
8239 }
8240 };
8241 match mode {
8242 1 => val(h),
8243 2 => {
8244 let ks = h.new_str(name.clone());
8245 let v = val(h);
8246 h.new_array(vec![ks, v])
8247 }
8248 _ => h.new_str(name.clone()),
8249 }
8250 })
8251 .collect();
8252 h.new_array(out)
8253 }));
8254 }
8255 if let Some(names) = builtin_proto_method_names(&ns) {
8256 return Ok(with_host(|h| {
8257 let out: Vec<Value> = names
8258 .iter()
8259 .map(|name| match mode {
8260 1 => h.alloc(JsObj::Builtin(format!(
8261 "@proto:{}:{name}",
8262 ns.trim_end_matches(".prototype")
8263 ))),
8264 2 => {
8265 let ks = h.new_str(*name);
8266 let val = h.alloc(JsObj::Builtin(format!(
8267 "@proto:{}:{name}",
8268 ns.trim_end_matches(".prototype")
8269 )));
8270 h.new_array(vec![ks, val])
8271 }
8272 _ => h.new_str(*name),
8273 })
8274 .collect();
8275 h.new_array(out)
8276 }));
8277 }
8278 // A stdlib namespace (`Buffer`, `require('buffer')`): its own enumerable
8279 // keys are the members node-js implements, each resolved to the same
8280 // first-class value a property read would give.
8281 let mut names = crate::stdlib::namespace_keys(&ns);
8282 // A core namespace (`Reflect`, `Math`, `JSON`) has no stdlib key list —
8283 // its members live in the builtin dispatch table. They are
8284 // non-enumerable in V8, so they surface only under
8285 // `getOwnPropertyNames`/`Reflect.ownKeys` (mode 3), never `Object.keys`.
8286 if names.is_empty() && mode == 3 {
8287 let prefix = format!("{ns}.");
8288 // A builtin constructor's own `length`/`name`/`prototype` come
8289 // first, as they do in V8.
8290 if is_builtin_ctor(&ns) {
8291 names.extend(["length", "name", "prototype"].map(str::to_string));
8292 }
8293 names.extend(
8294 NS_METHODS
8295 .iter()
8296 .filter_map(|q| q.strip_prefix(&prefix))
8297 .map(|m| m.to_string()),
8298 );
8299 // The numeric constants are members too. Without them
8300 // `getOwnPropertyNames(Math)` reported 35 of the 43 names node-js
8301 // actually answers — the eight it dropped being `PI` and its
8302 // siblings, which read fine and now own a descriptor as well.
8303 names.extend(
8304 namespace_constants(&ns)
8305 .iter()
8306 .map(|(k, _)| (*k).to_string()),
8307 );
8308 }
8309 // A builtin FUNCTION owns exactly `length` and `name` (10.3.3-4), so
8310 // `Object.getOwnPropertyNames(Math.max)` is `[ 'length', 'name' ]` — it
8311 // was `[]`, which said the function had no properties at all while both
8312 // of them read back a value. `length` is listed only where the intrinsic
8313 // table has an arity, so the names never advertise a read that answers
8314 // `undefined`.
8315 if names.is_empty() && mode == 3 && host::builtin_is_callable(&ns) {
8316 if builtin_meta(&ns).is_some() {
8317 names.push("length".to_string());
8318 }
8319 names.push("name".to_string());
8320 }
8321 // Whatever a script assigned onto the namespace, in assignment order and
8322 // after the built-in members — an ordinary enumerable own property, so
8323 // it surfaces under `Object.keys` too and not only `ownKeys`. These were
8324 // missing from every listing, which made a patched prototype read as
8325 // unpatched to any code that enumerates rather than reads.
8326 for k in with_host(|h| h.builtin_static_keys(&ns)) {
8327 if !names.contains(&k) {
8328 names.push(k);
8329 }
8330 }
8331 if !names.is_empty() {
8332 let entries: Vec<(String, Value)> = names
8333 .into_iter()
8334 .map(|k| {
8335 let val = namespace_property(&ns, &k);
8336 (k, val)
8337 })
8338 .collect();
8339 return Ok(with_host(|h| {
8340 let out: Vec<Value> = entries
8341 .into_iter()
8342 .map(|(k, val)| match mode {
8343 1 => val,
8344 2 => {
8345 let ks = h.new_str(k);
8346 h.new_array(vec![ks, val])
8347 }
8348 _ => h.new_str(k),
8349 })
8350 .collect();
8351 h.new_array(out)
8352 }));
8353 }
8354 }
8355 // mode 3 (`getOwnPropertyNames`) reports every own string key including the
8356 // non-enumerable ones, plus the exotic `length` an array carries.
8357 let entries: Vec<(String, Value)> = with_host(|h| {
8358 if mode == 3 {
8359 // An array's exotic `length` is already placed (after the indices,
8360 // before the ordinary string keys) by `own_key_names`.
8361 return h
8362 .own_key_names(&v, false)
8363 .into_iter()
8364 .map(|k| (k, Value::Undef))
8365 .collect();
8366 }
8367 Vec::new()
8368 });
8369 // `Object.keys` (mode 0) wants NAMES. `own_enum_entries_deep` returns
8370 // key/value pairs, so asking it for them ran every enumerable getter —
8371 // 20.1.2.17 -> 7.3.23 EnumerableOwnProperties only needs `[[GetOwnProperty]]`
8372 // for the enumerable flag, never `[[Get]]`, and a getter can throw or have
8373 // side effects:
8374 //
8375 // let n = 0; const o = { get g() { n++; return 1 } };
8376 // Object.keys(o); n // was 1, node says 0
8377 //
8378 // `values`/`entries` (modes 1 and 2) do read, and still do.
8379 let entries = match mode {
8380 3 => entries,
8381 0 => with_host(|h| h.own_enum_key_names(&v))
8382 .into_iter()
8383 .map(|k| (k, Value::Undef))
8384 .collect(),
8385 _ => host::own_enum_entries_deep(&v)?,
8386 };
8387 Ok(with_host(|h| {
8388 let out: Vec<Value> = entries
8389 .into_iter()
8390 .map(|(k, val)| match mode {
8391 0 | 3 => h.new_str(k),
8392 1 => val,
8393 _ => {
8394 let ks = h.new_str(k);
8395 h.new_array(vec![ks, val])
8396 }
8397 })
8398 .collect();
8399 h.new_array(out)
8400 }))
8401}
8402
8403fn object_assign(args: Vec<Value>) -> Result<Value, String> {
8404 let target = arg0(&args);
8405 // 20.1.2.1 step 1 is `ToObject(target)`, so a nullish TARGET throws while a
8406 // nullish SOURCE is skipped (`Object.assign({}, null)` is `{}`).
8407 require_object_coercible(&target)?;
8408 for src in args.iter().skip(1) {
8409 // `Object.assign` copies own *enumerable* properties, running any getter
8410 // — symbol-keyed ones included (7.3.25).
8411 let entries = host::own_enum_entries_deep(src)?;
8412 let syms = with_host(|h| h.own_symbol_entries(src));
8413 // A plain object target is filled in place (one borrow, then a single
8414 // re-canonicalization of the integer-index keys).
8415 let filled = with_host(|h| {
8416 if let Some(JsObj::Object(p)) = h.get_mut(&target) {
8417 for (k, v) in entries.iter().cloned().chain(syms.iter().cloned()) {
8418 p.insert(k, v);
8419 }
8420 host::canonicalize_own_keys(p);
8421 return true;
8422 }
8423 false
8424 });
8425 // Any OTHER target — an array being the common one — goes through the
8426 // ordinary Set path. The in-place branch above matched `JsObj::Object`
8427 // only, so `Object.assign([1,2], {extra:9})` silently copied NOTHING and
8428 // returned the untouched array: no error, just a missing property. The
8429 // Set path is what an `arr.extra = 9` assignment already used, so index
8430 // and non-index keys land where they do for a direct write.
8431 if !filled {
8432 for (k, v) in entries.into_iter().chain(syms) {
8433 set_property(&target, &k, v)?;
8434 }
8435 }
8436 }
8437 Ok(target)
8438}
8439
8440fn object_from_entries(args: Vec<Value>) -> Result<Value, String> {
8441 let pairs = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
8442 let mut props: IndexMap<String, Value> = IndexMap::new();
8443 for p in pairs {
8444 let kv = with_host(|h| h.iter_vec(&p)).unwrap_or_default();
8445 let key = with_host(|h| h.str_of(&kv.first().cloned().unwrap_or(Value::Undef)));
8446 let val = kv.get(1).cloned().unwrap_or(Value::Undef);
8447 props.insert(key, val);
8448 }
8449 Ok(with_host(|h| h.new_object(props)))
8450}
8451
8452/// `Object.groupBy(items, cb)` — group the iterable `items` into a null-prototype
8453/// object. Keys are `ToPropertyKey(cb(item, index))`; values are arrays of the
8454/// members mapped to that key, in first-seen key order.
8455fn object_group_by(args: Vec<Value>) -> Result<Value, String> {
8456 group_by_check_iterable(&arg0(&args), "Object.groupBy")?;
8457 let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8458 let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
8459 // Stepped, not drained: the callback runs per element, so a throwing one
8460 // stops at the first. Draining first meant an infinite source never reached
8461 // the callback at all and the call HUNG.
8462 host::iter_for_each(&arg0(&args), |item, i| {
8463 let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8464 let key = with_host(|h| h.property_key(&key_v));
8465 groups.entry(key).or_default().push(item);
8466 Ok(())
8467 })?;
8468 let props: IndexMap<String, Value> = with_host(|h| {
8469 groups
8470 .into_iter()
8471 .map(|(k, v)| (k, h.new_array(v)))
8472 .collect()
8473 });
8474 let obj = with_host(|h| h.new_object(props));
8475 // A null-prototype object (as Node returns), so it has no inherited members.
8476 with_host(|h| {
8477 let nv = h.null();
8478 h.set_proto(&obj, nv);
8479 });
8480 Ok(obj)
8481}
8482
8483/// The `groupBy` family words a non-iterable argument its OWN way — a third
8484/// vocabulary, alongside the array-literal spread's and the call spread's:
8485///
8486/// ```text
8487/// null / undefined "<Name> called on null or undefined"
8488/// anything else "<typeof> [value ]is not iterable (cannot read property
8489/// Symbol(Symbol.iterator))"
8490/// ```
8491///
8492/// A plain object, a symbol and a bigint name only their TYPE; a number, a
8493/// string and a boolean name the value too.
8494fn group_by_check_iterable(v: &Value, name: &str) -> Result<(), String> {
8495 if with_host(|h| h.is_nullish(v)) {
8496 return Err(host::type_error(&format!(
8497 "{name} called on null or undefined"
8498 )));
8499 }
8500 // Asked WITHOUT consuming anything: `iter_all` would drain the iterator
8501 // here, so the stepping loop below then saw an exhausted one — the finite
8502 // case returned an empty group and the infinite case was back to hanging.
8503 let iter_fn = get_property(v, "@@iterator").unwrap_or(Value::Undef);
8504 if with_host(|h| host::is_callable(h, &iter_fn)) {
8505 return Ok(());
8506 }
8507 Err(host::type_error(¬_iterable_typed(v)))
8508}
8509
8510/// The `<type> <value> is not iterable (cannot read property
8511/// Symbol(Symbol.iterator))` wording, which node uses wherever the source has
8512/// no name to report: a plain object, a symbol and a bigint name only their
8513/// TYPE; a number, a string and a boolean name the value too.
8514pub(crate) fn not_iterable_typed(v: &Value) -> String {
8515 let shown = with_host(|h| {
8516 let kind = h.type_of(v);
8517 match kind {
8518 "object" | "symbol" | "bigint" => kind.to_string(),
8519 "string" => format!("string \"{}\"", h.str_of(v)),
8520 _ => format!("{kind} {}", h.str_of(v)),
8521 }
8522 });
8523 format!("{shown} is not iterable (cannot read property Symbol(Symbol.iterator))")
8524}
8525
8526/// `Map.groupBy(items, cb)` — like `Object.groupBy` but returns a `Map` keyed by
8527/// the raw `cb(item, index)` value under SameValueZero (so object/any keys work).
8528fn map_group_by(args: Vec<Value>) -> Result<Value, String> {
8529 group_by_check_iterable(&arg0(&args), "Map.groupBy")?;
8530 let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8531 let m = with_host(|h| {
8532 h.alloc(JsObj::Map {
8533 entries: IndexMap::new(),
8534 weak: false,
8535 })
8536 });
8537 // Stepped for the same reason `Object.groupBy` is.
8538 host::iter_for_each(&arg0(&args), |item, i| {
8539 let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8540 let existing = map_method(&m, "get", vec![key_v.clone()])?;
8541 if matches!(existing, Value::Undef) {
8542 let arr = with_host(|h| h.new_array(vec![item]));
8543 map_method(&m, "set", vec![key_v, arr])?;
8544 } else {
8545 with_host(|h| {
8546 if let Some(JsObj::Array(a)) = h.get_mut(&existing) {
8547 a.push(item);
8548 }
8549 });
8550 }
8551 Ok(())
8552 })?;
8553 Ok(m)
8554}
8555
8556/// `Array.fromAsync(items[, mapFn])` — a Promise for an array, awaiting each
8557/// element and each `mapFn` result.
8558///
8559/// Written in JavaScript and compiled once, because the operation IS an async
8560/// function: a Rust builtin runs outside any coroutine and has no way to await,
8561/// so draining a promise from there would mean running the microtask queue by
8562/// hand. Delegating to the engine's own `async`/`for await` keeps the
8563/// suspension semantics — and the ordering they imply — exactly the language's.
8564///
8565/// The source may be an async iterable, a sync iterable, a bare iterator, or an
8566/// array-like. Everything iterable goes through `for await`, which awaits a sync
8567/// source's elements individually — that is what makes
8568/// `Array.fromAsync([1, Promise.resolve(2)])` answer `[1, 2]`. A bare `.next` is
8569/// accepted because an async generator object does not expose
8570/// `Symbol.asyncIterator` on this frontend.
8571fn array_from_async(args: Vec<Value>) -> Result<Value, String> {
8572 thread_local! {
8573 static IMPL: std::cell::RefCell<Option<Value>> = const { std::cell::RefCell::new(None) };
8574 }
8575 const SRC: &str = "(async function (items, mapFn, thisArg) {\n\
8576 const out = []; let i = 0;\n\
8577 const step = async (v) => { const a = await v; out.push(mapFn ? await mapFn.call(thisArg, a, i) : a); i++; };\n\
8578 const iterable = items != null && (typeof items[Symbol.asyncIterator] === 'function'\n\
8579 || typeof items[Symbol.iterator] === 'function' || typeof items.next === 'function');\n\
8580 if (iterable) {\n\
8581 for await (const v of items) { out.push(mapFn ? await mapFn.call(thisArg, v, i) : v); i++; }\n\
8582 return out;\n\
8583 }\n\
8584 const len = items == null ? 0 : (Math.trunc(Number(items.length)) || 0);\n\
8585 while (i < len) { await step(items[i]); }\n\
8586 return out;\n\
8587 })";
8588 let f = IMPL.with(|c| c.borrow().clone());
8589 let f = match f {
8590 Some(f) => f,
8591 None => {
8592 let f = crate::eval_in_global_scope(SRC)?;
8593 IMPL.with(|c| *c.borrow_mut() = Some(f.clone()));
8594 f
8595 }
8596 };
8597 host::invoke(&f, args, None)
8598}
8599
8600fn array_from(args: Vec<Value>) -> Result<Value, String> {
8601 // `Array.from` accepts generators and user iterables, plus array-likes with a
8602 // numeric `.length`.
8603 let src = arg0(&args);
8604 if let Some(cb) = args.get(1).cloned() {
8605 // Stepped, not drained: the mapper runs per element as the iterator
8606 // yields it (23.1.2.1 step 6.e). Materializing the whole sequence first
8607 // meant `Array.from(infiniteIterator, fn)` never reached the mapper at
8608 // all and HUNG, and a throwing mapper could not close the iterator.
8609 let this = this_arg(&args, 2);
8610 let mut out = Vec::new();
8611 let mapped = host::iter_for_each(&src, |v, i| {
8612 out.push(host::invoke(
8613 &cb,
8614 vec![v, Value::Float(i as f64)],
8615 this.clone(),
8616 )?);
8617 Ok(())
8618 });
8619 match mapped {
8620 Ok(()) => {}
8621 // An array-LIKE has no iterator; fall back to its indexed items.
8622 Err(e) if host::user_iterator_fn(&src).is_none() && e.ends_with(" is not iterable") => {
8623 out.clear();
8624 for (i, it) in array_like_items(&src).into_iter().enumerate() {
8625 out.push(host::invoke(
8626 &cb,
8627 vec![it, Value::Float(i as f64)],
8628 this.clone(),
8629 )?);
8630 }
8631 }
8632 Err(e) => return Err(e),
8633 }
8634 return construct_array_like(host::current_static_this(), out);
8635 }
8636 let items = match host::iter_all(&src) {
8637 Ok(v) => v,
8638 Err(_) => array_like_items(&src),
8639 };
8640 // 23.1.2.1 step 5: `Array.from` builds through `this`, so on a subclass the
8641 // result is an instance of it. It always allocated a plain array, which is
8642 // also why `A.from([1]).map(f) instanceof A` was false — the species chain
8643 // never started.
8644 construct_array_like(host::current_static_this(), items)
8645}
8646
8647/// Items of an array-like `{ length, 0, 1, … }` object (for `Array.from`).
8648pub(crate) fn array_like_items(src: &Value) -> Vec<Value> {
8649 // `LengthOfArrayLike` is `ToLength(Get(O, "length"))`, and `ToNumber` runs a
8650 // user `valueOf` — `Array.from({length: {valueOf: () => 1}})` was empty
8651 // because the infallible read does no `ToPrimitive`. A throw from it is
8652 // swallowed here for the same reason the `length` read is: this helper has
8653 // no way to report one, and every caller treats an unreadable length as 0.
8654 let len = get_property(src, "length")
8655 .ok()
8656 .and_then(|l| host::to_primitive(&l, "number").ok())
8657 .map(|l| with_host(|h| h.to_number(&l)))
8658 .unwrap_or(0.0);
8659 if !len.is_finite() || len <= 0.0 {
8660 return Vec::new();
8661 }
8662 (0..len as usize)
8663 .map(|i| get_property(src, &i.to_string()).unwrap_or(Value::Undef))
8664 .collect()
8665}
8666
8667// ── JSON ──────────────────────────────────────────────────────────────────────
8668
8669fn json_stringify(args: Vec<Value>) -> Result<Value, String> {
8670 // A CALLABLE second argument is the replacer function, and it is checked
8671 // before the array form (`IsCallable` precedes `IsArray` in the spec), so a
8672 // callable never also reaches the key-filter path below.
8673 let replacer = args
8674 .get(1)
8675 .filter(|r| with_host(|h| host::is_callable(h, r)))
8676 .cloned();
8677 // `toJSON` and the replacer run BEFORE serialization and are user code, so
8678 // the tree is rewritten first — outside the host borrow `json_str` holds,
8679 // and before the BigInt walk, which has no cycle guard of its own.
8680 //
8681 // The top-level value is a property of a synthetic wrapper `{ "": value }`
8682 // under key `""`, which is exactly the holder the replacer receives as
8683 // `this` on its first call.
8684 let root = arg0(&args);
8685 let wrapper = with_host(|h| {
8686 let mut m: IndexMap<String, Value> = IndexMap::new();
8687 m.insert(String::new(), root.clone());
8688 h.new_object(m)
8689 });
8690 let v = apply_to_json(&wrapper, "", &root, &mut Vec::new(), replacer.as_ref())?;
8691 // A BigInt anywhere in a serializable position is a TypeError (JSON has no
8692 // bigint form), matching Node's exact message.
8693 if with_host(|h| json_has_bigint(h, &v)) {
8694 return Err(host::type_error("Do not know how to serialize a BigInt"));
8695 }
8696 let indent = match args.get(2) {
8697 Some(Value::Float(f)) => " ".repeat((*f as usize).min(10)),
8698 Some(other) => with_host(|h| h.as_str(other)).unwrap_or_default(),
8699 None => String::new(),
8700 };
8701 // A replacer array (args[1]) restricts which object keys are serialized.
8702 let keys: Option<Vec<String>> = args.get(1).and_then(|r| {
8703 with_host(|h| match h.get(r) {
8704 Some(JsObj::Array(items)) => {
8705 Some(items.iter().map(|k| h.str_of(k)).collect::<Vec<_>>())
8706 }
8707 _ => None,
8708 })
8709 });
8710 let s = with_host(|h| json_str(h, &v, &indent, 0, keys.as_deref()));
8711 match s {
8712 Some(s) => Ok(with_host(|h| h.new_str(s))),
8713 None => Ok(Value::Undef),
8714 }
8715}
8716
8717/// One `SerializeJSONProperty(key, holder)` step: rewrite `v` (the value read
8718/// from `holder[key]`) by calling its `toJSON(key)` and then the replacer
8719/// function as `replacer.call(holder, key, value)`, then recurse into whatever
8720/// object survives. Applies to user methods, class methods, and the native
8721/// `Date`/`Buffer`/`URL` accessors alike.
8722///
8723/// Returns a fresh tree; the input is never mutated. `path` carries the chain of
8724/// objects currently being walked so a cyclic structure is reported rather than
8725/// spinning forever.
8726///
8727/// `toJSON` is called on the value ONCE and is NOT re-applied to its own result
8728/// — `{toJSON(){ return {toJSON(){ return 1 }} }}` serializes as `{}` in Node,
8729/// because the inner method is a plain (unserializable) function property of the
8730/// returned object, not a second conversion hook.
8731fn apply_to_json(
8732 holder: &Value,
8733 key: &str,
8734 v: &Value,
8735 path: &mut JsonPath,
8736 rep: Option<&Value>,
8737) -> Result<Value, String> {
8738 let mut v = v.clone();
8739 if matches!(v, Value::Obj(_)) {
8740 let tag = crate::stdlib::native_tag(&v);
8741 // 25.5.2.1 step 2: `toJSON` is looked up with `[[Get]]`, so a PROXY
8742 // supplies one through its `get` trap. `lookup_chain` walks the
8743 // property map and never asks the handler, so a proxy carrying a
8744 // `toJSON` was serialized as a plain object instead of by its own
8745 // method — and node's trap log starts with that `get`.
8746 let to_json = if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8747 get_property(&v, "toJSON")?
8748 } else {
8749 with_host(|h| host::lookup_chain(h, &v, "toJSON")).unwrap_or(Value::Undef)
8750 };
8751 let has_to_json = with_host(|h| host::is_callable(h, &to_json))
8752 || tag
8753 .as_deref()
8754 .map(crate::stdlib::has_to_json)
8755 .unwrap_or(false);
8756 if has_to_json {
8757 let k = with_host(|h| h.new_str(key.to_string()));
8758 v = host::call_method(&v, "toJSON", vec![k])?;
8759 }
8760 }
8761 if let Some(rep) = rep {
8762 let k = with_host(|h| h.new_str(key.to_string()));
8763 v = host::invoke(rep, vec![k, v.clone()], Some(holder.clone()))?;
8764 }
8765 // How `v` was reached from its holder, as V8 names the step in a
8766 // circular-structure message: `index 1` under an array, else `property 'k'`.
8767 let via = if matches!(with_host(|h| h.get(holder).cloned()), Some(JsObj::Array(_))) {
8768 format!("index {key}")
8769 } else {
8770 format!("property '{key}'")
8771 };
8772 json_walk_children(&v, path, &via, rep)
8773}
8774
8775/// The objects `JSON.stringify` is inside of, outermost first, each with the
8776/// step that reached it from its holder (`property 'x'` / `index 1`).
8777type JsonPath = Vec<(String, Value)>;
8778
8779/// V8's `ConstructCircularStructureErrorMessage`: the cycle from the object it
8780/// starts at to the key that closes it. At most the first two and the last one
8781/// intermediate step are listed, with `| ...` standing for the rest.
8782fn circular_json_message(path: &JsonPath, start: usize, closing: &str) -> String {
8783 const PREFIX: usize = 2;
8784 const POSTFIX: usize = 1;
8785 let ctor = |v: &Value| -> String {
8786 with_host(|h| match h.get(v) {
8787 Some(JsObj::Array(_)) if h.proto_of(v).is_none() => "Array".to_string(),
8788 _ => match h.ctor_name(v) {
8789 n if n.is_empty() => "Object".to_string(),
8790 n => n,
8791 },
8792 })
8793 };
8794 let line = |i: usize| {
8795 format!(
8796 "\n | {} -> object with constructor '{}'",
8797 path[i].0,
8798 ctor(&path[i].1)
8799 )
8800 };
8801 let mut msg = format!(
8802 "Converting circular structure to JSON\n --> starting at object with constructor '{}'",
8803 ctor(&path[start].1)
8804 );
8805 let prefix_end = path.len().min(start + 1 + PREFIX);
8806 for i in start + 1..prefix_end {
8807 msg.push_str(&line(i));
8808 }
8809 if path.len() > prefix_end + POSTFIX {
8810 msg.push_str("\n | ...");
8811 }
8812 for i in prefix_end.max(path.len().saturating_sub(POSTFIX))..path.len() {
8813 msg.push_str(&line(i));
8814 }
8815 msg.push_str(&format!("\n --- {closing} closes the circle"));
8816 msg
8817}
8818
8819/// Whether a raw property key of a host object is one `json_str` serializes. The
8820/// internal slots (`@@`-prefixed symbol keys, `#`-prefixed private fields) are
8821/// invisible to JSON, so the replacer must not be invoked for them either.
8822fn json_visible_key(k: &str) -> bool {
8823 !k.starts_with("@@") && !k.starts_with('#')
8824}
8825
8826/// Recurse into the elements/properties of an already-converted value, running
8827/// `apply_to_json` for each with this value as the holder.
8828fn json_walk_children(
8829 v: &Value,
8830 path: &mut JsonPath,
8831 via: &str,
8832 rep: Option<&Value>,
8833) -> Result<Value, String> {
8834 if !matches!(v, Value::Obj(_)) {
8835 return Ok(v.clone());
8836 }
8837 // A value that contains itself has no JSON form.
8838 if let Some(start) = with_host(|h| path.iter().position(|(_, p)| h.strict_eq(p, v))) {
8839 return Err(host::type_error(&circular_json_message(path, start, via)));
8840 }
8841 // A Proxy owns no property map, so it is snapshotted through its traps into
8842 // the plain array/object `SerializeJSONArray`/`SerializeJSONObject` describe
8843 // — which read every member through `[[Get]]`, exactly as the snapshot does.
8844 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8845 let snap = crate::proxy::json_snapshot(v)?;
8846 path.push((via.to_string(), v.clone()));
8847 let out = json_walk_children(&snap, path, via, rep);
8848 path.pop();
8849 return out;
8850 }
8851 let obj = with_host(|h| h.get(v).cloned());
8852 path.push((via.to_string(), v.clone()));
8853 let out = (|| match obj {
8854 Some(JsObj::Array(items)) => {
8855 // Read the elements through the accessor-aware funnel: an index
8856 // with a getter must be SERIALIZED as what the getter returns, and
8857 // the backing vector still holds the stale slot.
8858 let mut resolved = items;
8859 // An index with a getter must be SERIALIZED as what the getter
8860 // returns, and it also forces a rebuild below: keeping the original
8861 // array would hand the serializer back the stale backing vector.
8862 let had_accessor = resolve_index_accessors(v, &mut resolved);
8863 let items = resolved;
8864 let mut out = Vec::with_capacity(items.len());
8865 let mut changed = had_accessor;
8866 for (i, it) in items.iter().enumerate() {
8867 let nv = apply_to_json(v, &i.to_string(), it, path, rep)?;
8868 changed |= !with_host(|h| h.strict_eq(&nv, it));
8869 out.push(nv);
8870 }
8871 // Keep identity when nothing changed, so an enclosing object is not
8872 // needlessly rebuilt (which would drop its property attributes).
8873 if changed {
8874 Ok(with_host(|h| h.new_array(out)))
8875 } else {
8876 Ok(v.clone())
8877 }
8878 }
8879 Some(JsObj::Object(props)) => {
8880 // An enumerable own accessor must have its getter RUN and the result
8881 // serialized. That cannot happen inside `json_str` (which holds the
8882 // host borrow), so materialize here — the same reason `toJSON` is
8883 // applied in this pass.
8884 let has_accessor = with_host(|h| {
8885 h.own_accessor_keys(v)
8886 .iter()
8887 .any(|k| h.prop_attrs(v, k).enumerable)
8888 });
8889 if has_accessor {
8890 let mut next: IndexMap<String, Value> = IndexMap::new();
8891 for (k, val) in host::own_enum_entries_deep(v)? {
8892 let nv = if json_visible_key(&k) {
8893 apply_to_json(v, &k, &val, path, rep)?
8894 } else {
8895 val
8896 };
8897 next.insert(k, nv);
8898 }
8899 return Ok(with_host(|h| h.new_object(next)));
8900 }
8901 // Only rebuild when a descendant actually changed, so plain data keeps
8902 // its identity (and its prototype / native tag).
8903 let mut next: IndexMap<String, Value> = IndexMap::new();
8904 let mut changed = false;
8905 for (k, val) in &props {
8906 let nv = if json_visible_key(k) {
8907 apply_to_json(v, k, val, path, rep)?
8908 } else {
8909 val.clone()
8910 };
8911 changed |= !with_host(|h| h.strict_eq(&nv, val));
8912 next.insert(k.clone(), nv);
8913 }
8914 if changed {
8915 Ok(with_host(|h| {
8916 let o = h.new_object(next);
8917 h.copy_prop_attrs(v, &o);
8918 o
8919 }))
8920 } else {
8921 Ok(v.clone())
8922 }
8923 }
8924 _ => Ok(v.clone()),
8925 })();
8926 path.pop();
8927 out
8928}
8929
8930/// Whether a value tree contains a `BigInt` in a position `JSON.stringify` would
8931/// try to serialize (a value in an array/object) — such a value throws.
8932fn json_has_bigint(h: &host::JsHost, v: &Value) -> bool {
8933 match h.get(v) {
8934 Some(JsObj::BigInt(_)) => true,
8935 Some(JsObj::Array(items)) => items.iter().any(|x| json_has_bigint(h, x)),
8936 Some(JsObj::Object(props)) => props
8937 .iter()
8938 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
8939 .any(|(_, val)| json_has_bigint(h, val)),
8940 _ => false,
8941 }
8942}
8943
8944fn json_str(
8945 h: &host::JsHost,
8946 v: &Value,
8947 indent: &str,
8948 depth: usize,
8949 keys: Option<&[String]>,
8950) -> Option<String> {
8951 let sep = if indent.is_empty() { ":" } else { ": " };
8952 match v {
8953 Value::Undef => None,
8954 Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
8955 Value::Int(n) => Some(n.to_string()),
8956 Value::Float(f) => Some(if f.is_finite() {
8957 host::fmt_number(*f)
8958 } else {
8959 "null".into()
8960 }),
8961 Value::Str(s) => Some(json_quote(s)),
8962 Value::Obj(_) => match h.get(v) {
8963 Some(JsObj::Str(s)) => Some(json_quote(s)),
8964 Some(JsObj::Null) => Some("null".into()),
8965 // A `JSON.rawJSON` marker contributes its text VERBATIM — that is the
8966 // whole point of it, and it is why a number wider than a `double`
8967 // can survive a round trip.
8968 _ if h.fn_prop(v, "@@rawJSON").is_some() => match h.get(v) {
8969 Some(JsObj::Object(p)) => p.get("rawJSON").map(|r| h.str_of(r)),
8970 _ => None,
8971 },
8972 // A Map/Set has no ENTRIES to serialize (they are internal slots),
8973 // but any own property a script attached is serialized like an
8974 // ordinary object's: `JSON.stringify(Object.assign(new Map(), {a:1}))`
8975 // is `{"a":1}`.
8976 Some(JsObj::Map { .. })
8977 | Some(JsObj::Set { .. })
8978 | Some(JsObj::RegExp(_))
8979 // A Promise and a generator are ORDINARY objects to the serializer:
8980 // their state is internal slots, so they contribute no entries and
8981 // render as `{}`. They were being omitted entirely instead, so a
8982 // promise in an array became `null` and one in an object vanished.
8983 | Some(JsObj::Promise { .. })
8984 | Some(JsObj::Generator { .. }) => {
8985 let parts: Vec<String> = h
8986 .own_enum_entries(v)
8987 .into_iter()
8988 .filter(|(k, _)| !k.starts_with("@@") && !host::is_symbol_key(k))
8989 .filter_map(|(k, val)| {
8990 json_str(h, &val, indent, depth + 1, keys)
8991 .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8992 })
8993 .collect();
8994 Some(wrap(&parts, "{", "}", indent, depth))
8995 }
8996 // A NON-callable builtin is a namespace object, not a function, so
8997 // it serializes as one: `JSON.stringify(Math)` is `{}` (its members
8998 // are all non-enumerable), where omitting it made the whole property
8999 // disappear from its holder.
9000 Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
9001 let parts: Vec<String> = crate::stdlib::namespace_keys(n)
9002 .into_iter()
9003 .filter_map(|k| {
9004 let val = h.builtin_static(n, &k)?;
9005 json_str(h, &val, indent, depth + 1, keys)
9006 .map(|s| format!("{}{sep}{s}", json_quote(&k)))
9007 })
9008 .collect();
9009 Some(wrap(&parts, "{", "}", indent, depth))
9010 }
9011 // Functions and symbols are omitted (undefined) as values.
9012 Some(JsObj::Func(_))
9013 | Some(JsObj::Builtin(_))
9014 | Some(JsObj::BoundMethod { .. })
9015 | Some(JsObj::BoundFunc { .. })
9016 | Some(JsObj::Class(_))
9017 | Some(JsObj::Symbol { .. }) => None,
9018 Some(JsObj::Array(items)) => {
9019 if items.is_empty() {
9020 return Some("[]".into());
9021 }
9022 let parts: Vec<String> = items
9023 .iter()
9024 .map(|x| {
9025 json_str(h, x, indent, depth + 1, keys).unwrap_or_else(|| "null".into())
9026 })
9027 .collect();
9028 Some(wrap(&parts, "[", "]", indent, depth))
9029 }
9030 Some(JsObj::Object(props)) if props.contains_key("@@primitive") => {
9031 // 25.5.2.2 step 4: a String/Number/Boolean wrapper serializes as
9032 // the primitive it boxes, not as the object holding it —
9033 // `JSON.stringify(new Number(1))` is `1`, not `{}`.
9034 json_str(h, &props["@@primitive"].clone(), indent, depth, keys)
9035 }
9036 Some(JsObj::Object(props)) => {
9037 // A replacer array restricts (and orders) which keys are emitted.
9038 let parts: Vec<String> = match keys {
9039 Some(allow) => allow
9040 .iter()
9041 .filter_map(|k| {
9042 props.get(k).and_then(|val| {
9043 json_str(h, val, indent, depth + 1, keys)
9044 .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
9045 })
9046 })
9047 .collect(),
9048 None => h
9049 .own_enum_entries(v)
9050 .iter()
9051 .filter_map(|(k, val)| {
9052 json_str(h, val, indent, depth + 1, keys)
9053 .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
9054 })
9055 .collect(),
9056 };
9057 if parts.is_empty() {
9058 return Some("{}".into());
9059 }
9060 Some(wrap(&parts, "{", "}", indent, depth))
9061 }
9062 _ => Some("null".into()),
9063 },
9064 _ => Some("null".into()),
9065 }
9066}
9067
9068fn wrap(parts: &[String], open: &str, close: &str, indent: &str, depth: usize) -> String {
9069 if indent.is_empty() {
9070 format!("{open}{}{close}", parts.join(","))
9071 } else {
9072 let pad = indent.repeat(depth + 1);
9073 let pad_close = indent.repeat(depth);
9074 format!(
9075 "{open}\n{pad}{}\n{pad_close}{close}",
9076 parts.join(&format!(",\n{pad}"))
9077 )
9078 }
9079}
9080
9081fn json_quote(s: &str) -> String {
9082 let mut out = String::from("\"");
9083 for c in s.chars() {
9084 match c {
9085 '"' => out.push_str("\\\""),
9086 '\\' => out.push_str("\\\\"),
9087 '\n' => out.push_str("\\n"),
9088 '\t' => out.push_str("\\t"),
9089 '\r' => out.push_str("\\r"),
9090 // QuoteJSONString (25.5.2.2) names SIX short escapes, not four.
9091 // Backspace and form feed were missing, so they fell through to the
9092 // `\uXXXX` arm below and `JSON.stringify("\b")` produced
9093 // `""` where node produces `"\b"`. Both parse back to the same
9094 // string, so the difference is invisible to a round trip and shows
9095 // up only as a byte mismatch against a fixture or a checksum.
9096 '\u{8}' => out.push_str("\\b"),
9097 '\u{c}' => out.push_str("\\f"),
9098 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
9099 _ => out.push(c),
9100 }
9101 }
9102 out.push('"');
9103 out
9104}
9105
9106fn json_parse(args: Vec<Value>) -> Result<Value, String> {
9107 let s = with_host(|h| h.str_of(&arg0(&args)));
9108 let mut p = JsonParser {
9109 chars: s.chars().collect(),
9110 pos: 0,
9111 prims: Vec::new(),
9112 record: args
9113 .get(1)
9114 .is_some_and(|r| with_host(|h| host::is_callable(h, r))),
9115 };
9116 p.skip_ws();
9117 if p.peek().is_none() {
9118 return Err("SyntaxError: Unexpected end of JSON input".into());
9119 }
9120 let v = p.parse_value()?;
9121 let value_end = p.pos;
9122 p.skip_ws();
9123 // Anything after the top-level value is an error — the parser used to accept
9124 // and silently discard it, so `JSON.parse('{"a":1}x')` succeeded.
9125 if let Some(c) = p.peek() {
9126 // V8 names the token kind only when it butts directly against the value
9127 // (`01` -> "Unexpected number at position 1"); with whitespace between
9128 // it is just a non-whitespace character (`1 2`).
9129 // Only a digit butted directly against a completed number literal —
9130 // V8's number scanner is still in number context there. `5"x"` and
9131 // `[0,1]0` exit the scanner cleanly and get the generic message.
9132 let after_number = value_end > 0
9133 && p.pos == value_end
9134 && p.chars[value_end - 1].is_ascii_digit()
9135 && c.is_ascii_digit();
9136 return Err(if after_number {
9137 p.err_at("Unexpected number", p.pos)
9138 } else {
9139 p.err_trailing(p.pos)
9140 });
9141 }
9142 // Optional reviver: walk bottom-up, transforming each (key, value).
9143 if let Some(reviver) = args
9144 .get(1)
9145 .filter(|r| with_host(|h| host::is_callable(h, r)))
9146 .cloned()
9147 {
9148 // The top-level holder is a fresh `{ "": value }` wrapper, as the spec
9149 // constructs before the walk.
9150 let root = with_host(|h| {
9151 let mut m: IndexMap<String, Value> = IndexMap::new();
9152 m.insert(String::new(), v.clone());
9153 h.new_object(m)
9154 });
9155 return json_revive("", v, &reviver, &root, &p.prims, &mut 0);
9156 }
9157 Ok(v)
9158}
9159
9160/// `JSON.parse` reviver walk: recurse into children first, then call
9161/// `reviver(key, value)`; a returned `undefined` drops the property.
9162///
9163/// The reviver runs with the HOLDER as `this` (25.5.1.1
9164/// InternalizeJSONProperty) — the object or array the key lives in, and at the
9165/// top level a wrapper `{ "": value }`. It was being called with no receiver,
9166/// so `this` was undefined and a reviver could not reach its siblings.
9167/// `JSON.rawJSON(text)` — a marker object whose text `JSON.stringify` emits
9168/// VERBATIM, so a number too large for a `double` survives a round trip
9169/// (`JSON.stringify({n: JSON.rawJSON("12345678901234567890")})`).
9170///
9171/// The validation is not "does `JSON.parse` accept it": node's rule, measured
9172/// across the whole matrix, is
9173///
9174/// ```text
9175/// "" -> SyntaxError: Invalid value for JSON.rawJSON
9176/// leading whitespace -> the parse error for that first character
9177/// a complete literal -> ok
9178/// anything left over -> SyntaxError: Invalid value for JSON.rawJSON
9179/// a broken literal -> the parse error the scanner raised
9180/// ```
9181///
9182/// so `" 1"` reports an unexpected token while `"1 "` and `"1,2"` report the
9183/// invalid-value message even though `JSON.parse` accepts the former and gives
9184/// a token error for the latter.
9185fn json_raw(args: Vec<Value>) -> Result<Value, String> {
9186 const INVALID: &str = "SyntaxError: Invalid value for JSON.rawJSON";
9187 let s = with_host(|h| h.str_of(&arg0(&args)));
9188 if s.is_empty() {
9189 return Err(INVALID.into());
9190 }
9191 let mut p = JsonParser {
9192 chars: s.chars().collect(),
9193 pos: 0,
9194 prims: Vec::new(),
9195 record: false,
9196 };
9197 // An object or an array is rejected where it starts, as leading whitespace
9198 // is — both are "not a primitive", but node reports the token.
9199 if matches!(p.peek(), Some('{') | Some('[')) || p.peek().is_some_and(|c| c.is_whitespace()) {
9200 return Err(p.err_token(0));
9201 }
9202 p.parse_value()?;
9203 if p.pos != p.chars.len() {
9204 // A digit butted against a completed number is still in the number
9205 // scanner, so `"01"` reports the scanner's error rather than leftover
9206 // input — the same distinction `json_parse` draws for trailing text.
9207 if p.chars[p.pos - 1].is_ascii_digit() && p.chars[p.pos].is_ascii_digit() {
9208 return Err(p.err_at("Unexpected number", p.pos));
9209 }
9210 return Err(INVALID.into());
9211 }
9212 // A null prototype and one own `rawJSON` property, frozen — the brand is a
9213 // hidden slot so `Object.keys` stays `["rawJSON"]`.
9214 Ok(with_host(|h| {
9215 let mut m: IndexMap<String, Value> = IndexMap::new();
9216 let text = h.new_str(s);
9217 m.insert("rawJSON".into(), text);
9218 let o = h.new_object(m);
9219 let null = h.null();
9220 h.set_proto(&o, null);
9221 h.set_fn_prop(&o, "@@rawJSON", Value::Bool(true));
9222 h.seal_object(&o, true);
9223 o
9224 }))
9225}
9226
9227/// `JSON.isRawJSON(v)` — the brand check. A hand-built `{ rawJSON: "1" }` is
9228/// NOT one, which is why the marker is a hidden slot rather than the property.
9229fn json_is_raw(args: Vec<Value>) -> Result<Value, String> {
9230 Ok(Value::Bool(is_raw_json(&arg0(&args))))
9231}
9232
9233fn is_raw_json(v: &Value) -> bool {
9234 with_host(|h| h.fn_prop(v, "@@rawJSON")).is_some()
9235}
9236
9237fn json_revive(
9238 key: &str,
9239 val: Value,
9240 reviver: &Value,
9241 holder: &Value,
9242 prims: &[String],
9243 next: &mut usize,
9244) -> Result<Value, String> {
9245 // A PRIMITIVE claims the next recorded source slice before its children
9246 // would — it has none — and a container claims nothing. The walk descends in
9247 // the same order the parse produced them, so one cursor lines the two up.
9248 let is_container =
9249 with_host(|h| matches!(h.get(&val), Some(JsObj::Array(_)) | Some(JsObj::Object(_))));
9250 let source = if !is_container {
9251 let s = prims.get(*next).cloned();
9252 if s.is_some() {
9253 *next += 1;
9254 }
9255 s
9256 } else {
9257 None
9258 };
9259 match with_host(|h| h.get(&val).cloned()) {
9260 Some(JsObj::Array(items)) => {
9261 for i in 0..items.len() {
9262 let elem = with_host(|h| match h.get(&val) {
9263 Some(JsObj::Array(it)) => it[i].clone(),
9264 _ => Value::Undef,
9265 });
9266 let nv = json_revive(&i.to_string(), elem, reviver, &val, prims, next)?;
9267 with_host(|h| {
9268 if let Some(JsObj::Array(it)) = h.get_mut(&val) {
9269 it[i] = nv;
9270 }
9271 });
9272 }
9273 }
9274 Some(JsObj::Object(props)) => {
9275 let keys: Vec<String> = props
9276 .keys()
9277 .filter(|k| !k.starts_with("@@"))
9278 .cloned()
9279 .collect();
9280 for k in keys {
9281 let elem = with_host(|h| match h.get(&val) {
9282 Some(JsObj::Object(p)) => p.get(&k).cloned().unwrap_or(Value::Undef),
9283 _ => Value::Undef,
9284 });
9285 let nv = json_revive(&k, elem, reviver, &val, prims, next)?;
9286 with_host(|h| {
9287 if let Some(JsObj::Object(p)) = h.get_mut(&val) {
9288 if matches!(nv, Value::Undef) {
9289 p.shift_remove(&k);
9290 } else {
9291 p.insert(k.clone(), nv);
9292 }
9293 }
9294 });
9295 }
9296 }
9297 _ => {}
9298 }
9299 let kv = with_host(|h| h.new_str(key.to_string()));
9300 // 25.5.1.1 step 2.b: the reviver's THIRD argument. `{ source }` for a
9301 // primitive, an empty object for an array or an object — node passes it
9302 // either way, and code reading `ctx.source` used to die on `undefined`
9303 // because only two arguments were passed.
9304 let ctx = with_host(|h| {
9305 let mut m: IndexMap<String, Value> = IndexMap::new();
9306 if let Some(s) = source {
9307 let sv = h.new_str(s);
9308 m.insert("source".into(), sv);
9309 }
9310 h.new_object(m)
9311 });
9312 host::invoke(reviver, vec![kv, val, ctx], Some(holder.clone()))
9313}
9314
9315struct JsonParser {
9316 chars: Vec<char>,
9317 pos: usize,
9318 /// Source text of each PRIMITIVE value, in parse order — what the reviver's
9319 /// third argument reports as `context.source` (25.5.1.1). Only collected
9320 /// when a reviver was supplied.
9321 ///
9322 /// A flat list rather than a parallel tree because the reviver walk visits
9323 /// primitives in the same depth-first order the parse produced them, so an
9324 /// index into this is enough to line them up.
9325 prims: Vec<String>,
9326 record: bool,
9327}
9328impl JsonParser {
9329 fn peek(&self) -> Option<char> {
9330 self.chars.get(self.pos).copied()
9331 }
9332
9333 /// `at position N (line L column C)` — the location suffix V8 appends to the
9334 /// positional JSON parse errors. Positions are in UTF-16-ish code units;
9335 /// node-js counts `char`s, which agree for the BMP.
9336 fn at(&self, pos: usize) -> String {
9337 let mut line = 1usize;
9338 let mut col = 1usize;
9339 for c in &self.chars[..pos.min(self.chars.len())] {
9340 if *c == '\n' {
9341 line += 1;
9342 col = 1;
9343 } else {
9344 col += 1;
9345 }
9346 }
9347 format!(" at position {pos} (line {line} column {col})")
9348 }
9349
9350 /// A positional error (`Expected ':' after property name in JSON at …`).
9351 fn err_at(&self, what: &str, pos: usize) -> String {
9352 format!("SyntaxError: {what} in JSON{}", self.at(pos))
9353 }
9354
9355 /// The one positional message V8 does NOT suffix with `in JSON`.
9356 fn err_trailing(&self, pos: usize) -> String {
9357 format!(
9358 "SyntaxError: Unexpected non-whitespace character after JSON{}",
9359 self.at(pos)
9360 )
9361 }
9362
9363 /// V8's default parse error: the offending character plus a window of the
9364 /// source. The whole input is quoted when it is short (<= 20 chars);
9365 /// otherwise a 10-character context window either side of `pos` is shown,
9366 /// elided with `...` on whichever side was cut.
9367 fn err_token(&self, pos: usize) -> String {
9368 const MAX_WHOLE: usize = 20;
9369 const CONTEXT: usize = 10;
9370 let len = self.chars.len();
9371 let Some(c) = self.chars.get(pos) else {
9372 return "SyntaxError: Unexpected end of JSON input".into();
9373 };
9374 // V8 reports the whole input for the JS literals that are famously not
9375 // JSON, without naming an offending character.
9376 let whole: String = self.chars.iter().collect();
9377 if matches!(
9378 whole.as_str(),
9379 "undefined" | "NaN" | "Infinity" | "-Infinity"
9380 ) {
9381 return format!("SyntaxError: \"{whole}\" is not valid JSON");
9382 }
9383 let snippet = if len <= MAX_WHOLE {
9384 format!("\"{whole}\"")
9385 } else {
9386 let start = pos.saturating_sub(CONTEXT);
9387 let end = (pos + CONTEXT).min(len);
9388 let body: String = self.chars[start..end].iter().collect();
9389 let head = if start > 0 { "..." } else { "" };
9390 let tail = if end < len { "..." } else { "" };
9391 format!("{head}\"{body}\"{tail}")
9392 };
9393 format!("SyntaxError: Unexpected token '{c}', {snippet} is not valid JSON")
9394 }
9395
9396 fn skip_ws(&mut self) {
9397 while matches!(
9398 self.peek(),
9399 Some(' ') | Some('\n') | Some('\t') | Some('\r')
9400 ) {
9401 self.pos += 1;
9402 }
9403 }
9404 fn parse_value(&mut self) -> Result<Value, String> {
9405 self.skip_ws();
9406 let start = self.pos;
9407 let prim = matches!(self.peek(), Some(c) if c != '{' && c != '[');
9408 let v = match self.peek() {
9409 Some('{') => self.parse_object(),
9410 Some('[') => self.parse_array(),
9411 Some('"') => {
9412 let s = self.parse_string()?;
9413 Ok(with_host(|h| h.new_str(s)))
9414 }
9415 Some('t') | Some('f') => self.parse_bool(),
9416 Some('n') => {
9417 self.expect_lit("null")?;
9418 Ok(with_host(|h| h.null()))
9419 }
9420 Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
9421 None => Err("SyntaxError: Unexpected end of JSON input".into()),
9422 _ => Err(self.err_token(self.pos)),
9423 }?;
9424 if prim && self.record {
9425 self.prims
9426 .push(self.chars[start..self.pos].iter().collect());
9427 }
9428 Ok(v)
9429 }
9430 fn expect_lit(&mut self, lit: &str) -> Result<(), String> {
9431 for ch in lit.chars() {
9432 match self.peek() {
9433 Some(c) if c == ch => self.pos += 1,
9434 // V8 reports the first character that broke the literal, which is
9435 // why `foo` complains about `'o'` (index 2) and not `'f'`.
9436 None => return Err("SyntaxError: Unexpected end of JSON input".into()),
9437 _ => return Err(self.err_token(self.pos)),
9438 }
9439 }
9440 Ok(())
9441 }
9442 fn parse_bool(&mut self) -> Result<Value, String> {
9443 if self.peek() == Some('t') {
9444 self.expect_lit("true")?;
9445 Ok(Value::Bool(true))
9446 } else {
9447 self.expect_lit("false")?;
9448 Ok(Value::Bool(false))
9449 }
9450 }
9451 /// JSON's number grammar: `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?`.
9452 /// A leading zero does NOT swallow the following digits — `01` parses as `0`
9453 /// and the stray `1` becomes a trailing-token error, which is how V8 reports
9454 /// it. Each way the grammar can run out has its own message.
9455 fn parse_number(&mut self) -> Result<Value, String> {
9456 let start = self.pos;
9457 if self.peek() == Some('-') {
9458 self.pos += 1;
9459 if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9460 return Err(self.err_at("No number after minus sign", self.pos));
9461 }
9462 }
9463 if self.peek() == Some('0') {
9464 self.pos += 1;
9465 } else {
9466 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9467 self.pos += 1;
9468 }
9469 }
9470 if self.peek() == Some('.') {
9471 self.pos += 1;
9472 if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9473 return Err(self.err_at("Unterminated fractional number", self.pos));
9474 }
9475 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9476 self.pos += 1;
9477 }
9478 }
9479 if matches!(self.peek(), Some('e') | Some('E')) {
9480 self.pos += 1;
9481 if matches!(self.peek(), Some('+') | Some('-')) {
9482 self.pos += 1;
9483 }
9484 if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9485 return Err(self.err_at("Exponent part is missing a number", self.pos));
9486 }
9487 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9488 self.pos += 1;
9489 }
9490 }
9491 let s: String = self.chars[start..self.pos].iter().collect();
9492 s.parse::<f64>()
9493 .map(Value::Float)
9494 .map_err(|_| self.err_at("Unexpected number", start))
9495 }
9496 fn parse_string(&mut self) -> Result<String, String> {
9497 self.pos += 1; // opening quote
9498 let mut out = String::new();
9499 loop {
9500 match self.peek() {
9501 None => return Err(self.err_at("Unterminated string", self.pos)),
9502 Some('"') => {
9503 self.pos += 1;
9504 break;
9505 }
9506 Some('\\') => {
9507 self.pos += 1;
9508 match self.peek() {
9509 Some('n') => out.push('\n'),
9510 Some('t') => out.push('\t'),
9511 Some('r') => out.push('\r'),
9512 Some('"') => out.push('"'),
9513 Some('\\') => out.push('\\'),
9514 Some('/') => out.push('/'),
9515 Some('b') => out.push('\u{08}'),
9516 Some('f') => out.push('\u{0C}'),
9517 Some('u') => {
9518 let h: String = self.chars
9519 [self.pos + 1..(self.pos + 5).min(self.chars.len())]
9520 .iter()
9521 .collect();
9522 if let Ok(n) = u32::from_str_radix(&h, 16) {
9523 if let Some(ch) = char::from_u32(n) {
9524 out.push(ch);
9525 }
9526 }
9527 self.pos += 4;
9528 }
9529 _ => {}
9530 }
9531 self.pos += 1;
9532 }
9533 // A raw control character is not legal inside a JSON string; it
9534 // has to be escaped. V8 rejects it rather than passing it through.
9535 Some(c) if (c as u32) < 0x20 => {
9536 return Err(self.err_at("Bad control character in string literal", self.pos))
9537 }
9538 Some(c) => {
9539 out.push(c);
9540 self.pos += 1;
9541 }
9542 }
9543 }
9544 Ok(out)
9545 }
9546 fn parse_array(&mut self) -> Result<Value, String> {
9547 self.pos += 1; // [
9548 let mut items = Vec::new();
9549 self.skip_ws();
9550 if self.peek() == Some(']') {
9551 self.pos += 1;
9552 return Ok(with_host(|h| h.new_array(items)));
9553 }
9554 loop {
9555 items.push(self.parse_value()?);
9556 self.skip_ws();
9557 match self.peek() {
9558 Some(',') => {
9559 self.pos += 1;
9560 }
9561 Some(']') => {
9562 self.pos += 1;
9563 break;
9564 }
9565 _ => return Err(self.err_at("Expected ',' or ']' after array element", self.pos)),
9566 }
9567 }
9568 Ok(with_host(|h| h.new_array(items)))
9569 }
9570 fn parse_object(&mut self) -> Result<Value, String> {
9571 self.pos += 1; // {
9572 let mut props: IndexMap<String, Value> = IndexMap::new();
9573 self.skip_ws();
9574 if self.peek() == Some('}') {
9575 self.pos += 1;
9576 return Ok(with_host(|h| h.new_object(props)));
9577 }
9578 loop {
9579 self.skip_ws();
9580 if self.peek() != Some('"') {
9581 // The first key uses the "or '}'" wording (an empty object is
9582 // still legal there); a key after a comma does not. End of input
9583 // reports the same expectation, at the end position.
9584 return Err(if props.is_empty() {
9585 self.err_at("Expected property name or '}'", self.pos)
9586 } else {
9587 self.err_at("Expected double-quoted property name", self.pos)
9588 });
9589 }
9590 let key = self.parse_string()?;
9591 self.skip_ws();
9592 if self.peek() != Some(':') {
9593 return Err(match self.peek() {
9594 None => "SyntaxError: Unexpected end of JSON input".into(),
9595 _ => self.err_at("Expected ':' after property name", self.pos),
9596 });
9597 }
9598 self.pos += 1;
9599 let val = self.parse_value()?;
9600 props.insert(key, val);
9601 self.skip_ws();
9602 match self.peek() {
9603 Some(',') => {
9604 self.pos += 1;
9605 }
9606 Some('}') => {
9607 self.pos += 1;
9608 break;
9609 }
9610 _ => return Err(self.err_at("Expected ',' or '}' after property value", self.pos)),
9611 }
9612 }
9613 Ok(with_host(|h| h.new_object(props)))
9614 }
9615}
9616
9617// ══ type methods (array / string / number) ═══════════════════════════════════
9618
9619fn is_array_method(name: &str) -> bool {
9620 matches!(
9621 name,
9622 "push"
9623 | "pop"
9624 | "shift"
9625 | "unshift"
9626 | "map"
9627 | "filter"
9628 | "forEach"
9629 | "join"
9630 | "slice"
9631 | "indexOf"
9632 | "lastIndexOf"
9633 | "includes"
9634 | "reduce"
9635 | "concat"
9636 | "reverse"
9637 | "sort"
9638 | "find"
9639 | "findIndex"
9640 | "some"
9641 | "every"
9642 | "flat"
9643 | "fill"
9644 | "splice"
9645 | "keys"
9646 | "values"
9647 | "entries"
9648 | "flatMap"
9649 | "at"
9650 | "toString"
9651 | "reduceRight"
9652 | "findLast"
9653 | "findLastIndex"
9654 | "copyWithin"
9655 )
9656}
9657/// Every `String.prototype` method node-js implements.
9658///
9659/// A LIST rather than a `matches!` arm because the same set has to be installed
9660/// on the real `String.prototype` object: a method read off the prototype
9661/// (`String.prototype.trim.call(s)`, the generic-borrowing idiom libraries use)
9662/// found nothing there, so the two views of "which methods exist" would drift
9663/// if they were written twice.
9664pub(crate) const STRING_PROTO_METHODS: &[&str] = &[
9665 "toUpperCase",
9666 "toLowerCase",
9667 "charAt",
9668 "charCodeAt",
9669 "codePointAt",
9670 "indexOf",
9671 "lastIndexOf",
9672 "includes",
9673 "slice",
9674 "substring",
9675 "substr",
9676 "split",
9677 "trim",
9678 "trimStart",
9679 "trimEnd",
9680 "replace",
9681 "replaceAll",
9682 "repeat",
9683 "startsWith",
9684 "endsWith",
9685 "padStart",
9686 "padEnd",
9687 "concat",
9688 "at",
9689 "toString",
9690 "toLocaleString",
9691 "valueOf",
9692 "match",
9693 "matchAll",
9694 "search",
9695 "normalize",
9696 "localeCompare",
9697 "toLocaleUpperCase",
9698 "toLocaleLowerCase",
9699 "isWellFormed",
9700 "toWellFormed",
9701];
9702
9703fn is_string_method(name: &str) -> bool {
9704 STRING_PROTO_METHODS.contains(&name)
9705}
9706
9707/// Every SYMBOL-keyed intrinsic method the generated table lists for `ctor`,
9708/// spelled the way this frontend spells the key (`@@iterator`).
9709///
9710/// A prototype built as a REAL object (`String.prototype`, `URLSearchParams
9711/// .prototype`) installs its methods from a list, and only the string-keyed
9712/// list was walked — so `String.prototype[Symbol.iterator]` read `undefined`
9713/// while `Array.prototype[Symbol.iterator]`, which resolves through the
9714/// `Builtin` namespace and its table gate, answered a function. Derived from
9715/// the table rather than written out, so it cannot name a method node does not
9716/// define nor miss one it does.
9717pub(crate) fn proto_symbol_methods(ctor: &str) -> Vec<&'static str> {
9718 let prefix = format!("@proto:{ctor}:");
9719 crate::arity::BUILTIN_ARITY
9720 .iter()
9721 .filter_map(|(k, _, _)| k.strip_prefix(prefix.as_str()))
9722 .filter(|m| m.starts_with("@@"))
9723 .collect()
9724}
9725
9726/// The builtin constructors whose `.prototype` object is BRANDED — every other
9727/// `<C>.prototype` is an ordinary object and reports `[object Object]`.
9728///
9729/// Measured on node v26.8.1 over every constructor this frontend knows:
9730///
9731/// ```text
9732/// Array/Object/Number/String/Boolean/Function the ES5 legacy slot prototypes
9733/// Symbol/BigInt/Map/Set/WeakMap/WeakSet carry an own @@toStringTag
9734/// Promise/Iterator/ArrayBuffer/DataView "
9735/// WeakRef/FinalizationRegistry/URL "
9736/// URLSearchParams/TextEncoder/TextDecoder "
9737/// Date/RegExp/Error/TypeError/Uint8Array/… [object Object]
9738/// ```
9739///
9740/// The rule this replaces branded EVERY `<C>.prototype` as `C`, so
9741/// `Object.prototype.toString.call(Date.prototype)` read `[object Date]` — and
9742/// a `Date.prototype.toString` call on a plain object named `[object Date]` in
9743/// its own failure message where node names `[object Object]`.
9744pub(crate) const BRANDED_PROTOS: &[&str] = &[
9745 "Array",
9746 "ArrayBuffer",
9747 "BigInt",
9748 "Boolean",
9749 "DataView",
9750 "FinalizationRegistry",
9751 "Function",
9752 "Iterator",
9753 "Map",
9754 "Number",
9755 "Object",
9756 "Promise",
9757 "Set",
9758 "SharedArrayBuffer",
9759 "String",
9760 "Symbol",
9761 "TextDecoder",
9762 "TextEncoder",
9763 "URL",
9764 "URLSearchParams",
9765 "WeakMap",
9766 "WeakRef",
9767 "WeakSet",
9768];
9769
9770/// Whether `v` is a `RegExp` value (drives the regex path of `match`/`replace`/…).
9771/// A user `Symbol.match`/`replace`/`search`/`split`/`matchAll` method on the
9772/// ARGUMENT, which the string method must delegate to (22.1.3.x step 2).
9773///
9774/// `"abc".match(o)` where `o` defines `Symbol.match` calls that method rather
9775/// than coercing `o` to a pattern — the protocol every regexp-like library
9776/// implements. None of the five were consulted, so a custom matcher was
9777/// silently stringified instead.
9778fn symbol_protocol(arg: &Value, sym: &str) -> Option<Value> {
9779 if matches!(arg, Value::Undef) || with_host(|h| h.is_null(arg)) {
9780 return None;
9781 }
9782 let f = get_property(arg, sym).ok()?;
9783 with_host(|h| host::is_callable(h, &f)).then_some(f)
9784}
9785
9786fn is_regexp_arg(v: &Value) -> bool {
9787 // 7.2.8 `IsRegExp` asks `Symbol.match` FIRST, so an object can declare
9788 // itself a regexp — or a real one can disown the label. Only the heap kind
9789 // was checked, so `"a".startsWith({[Symbol.match]: true})` did not throw
9790 // the TypeError the spec requires.
9791 if let Ok(m) = get_property(v, "@@match") {
9792 if !matches!(m, Value::Undef) {
9793 return with_host(|h| h.truthy(&m));
9794 }
9795 }
9796 with_host(|h| h.kind_of(v)) == Some(ObjKind::RegExp)
9797}
9798
9799/// `str.replace(strPattern, fn)` — a function replacer against a literal (string)
9800/// pattern: replace the first (or all) occurrence, calling `fn(match, offset, s)`.
9801fn replace_str_fn(s: &str, pat: &str, repl: &Value, all: bool) -> Result<String, String> {
9802 if pat.is_empty() {
9803 return Ok(s.to_string());
9804 }
9805 let mut out = String::new();
9806 let mut rest = s;
9807 let mut base = 0usize;
9808 while let Some(pos) = rest.find(pat) {
9809 out.push_str(&rest[..pos]);
9810 let offset = base + pos;
9811 let m = with_host(|h| h.new_str(pat.to_string()));
9812 let str_arg = with_host(|h| h.new_str(s.to_string()));
9813 let r = host::invoke(repl, vec![m, Value::Float(offset as f64), str_arg], None)?;
9814 out.push_str(&with_host(|h| h.str_of(&r)));
9815 let consumed = pos + pat.len();
9816 base += consumed;
9817 rest = &rest[consumed..];
9818 if !all {
9819 break;
9820 }
9821 }
9822 out.push_str(rest);
9823 Ok(out)
9824}
9825/// Every `Number.prototype` method node-js implements — a list for the same
9826/// reason [`STRING_PROTO_METHODS`] is one.
9827pub(crate) const NUMBER_PROTO_METHODS: &[&str] = &[
9828 "toFixed",
9829 "toExponential",
9830 "toString",
9831 "toPrecision",
9832 "toLocaleString",
9833 "valueOf",
9834];
9835
9836fn is_number_method(name: &str) -> bool {
9837 NUMBER_PROTO_METHODS.contains(&name)
9838}
9839
9840/// The exotic kinds whose own dispatch table does NOT already reach the
9841/// `Object.prototype` methods, so the inherited ones have to be routed to.
9842///
9843/// An allowlist rather than a catch-all: a primitive receiver also reaches this
9844/// function, and a Number's `toString` is `Number.prototype.toString` — routing
9845/// it to the object form made `(255).toString(16)` report `[object Number]`.
9846fn inherits_object_methods(recv: &Value) -> bool {
9847 matches!(
9848 with_host(|h| h.kind_of(recv)),
9849 Some(
9850 ObjKind::Map
9851 | ObjKind::Set
9852 | ObjKind::Promise
9853 | ObjKind::RegExp
9854 | ObjKind::Generator
9855 | ObjKind::Symbol
9856 | ObjKind::BigInt
9857 | ObjKind::Iter
9858 )
9859 )
9860}
9861
9862/// Whether `recv`'s own prototype defines `name`, shadowing the
9863/// `Object.prototype` method of that name — `RegExp.prototype.toString` does,
9864/// `Map.prototype` does not.
9865fn overrides_object_method(recv: &Value, name: &str) -> bool {
9866 match with_host(|h| h.kind_of(recv)) {
9867 Some(ObjKind::Map) => is_map_method(name),
9868 Some(ObjKind::Set) => is_set_method(name),
9869 Some(ObjKind::RegExp) => crate::regexp::is_regexp_method(name),
9870 // A Symbol has its own `toString`; `valueOf` is the inherited one,
9871 // which returns the receiver — exactly what a symbol needs.
9872 Some(ObjKind::Symbol) => matches!(name, "toString" | "valueOf" | "@@toPrimitive"),
9873 Some(ObjKind::BigInt) => matches!(name, "toString" | "valueOf" | "toLocaleString"),
9874 _ => false,
9875 }
9876}
9877
9878/// Dispatch `recv.name(args)` for the built-in prototype methods.
9879pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
9880 // A USER method on the receiver's prototype chain wins over the builtin of
9881 // the same name — that is how a `class X extends Array` method is reached,
9882 // since the dispatch below goes straight to the builtin table and has no
9883 // entry for it.
9884 //
9885 // Deliberately restricted to a user function: the shared `Object.prototype`
9886 // carries real `@proto:Object:*` thunks, so accepting any callable made a
9887 // bare `map.toString()` resolve to the object form instead of the builtin
9888 // one the exotic is supposed to use.
9889 if let Some(f) = with_host(|h| host::lookup_chain(h, recv, name)) {
9890 if matches!(
9891 with_host(|h| h.kind_of(&f)),
9892 Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc)
9893 ) {
9894 return host::invoke(&f, args, Some(recv.clone()));
9895 }
9896 }
9897 // A method synthesized from the receiver's KIND is unreachable once its
9898 // intrinsic prototype is off the chain. The read already answers
9899 // `undefined` for one; dispatch has its own table and would still have
9900 // called it, so `Object.setPrototypeOf(a, {}); a.join()` returned "1,2"
9901 // while `a.join` was `undefined` — the read and the call disagreeing again,
9902 // in the opposite direction from the monkey-patch case below.
9903 if !own_intrinsic_reachable(recv)
9904 && inherited_method_owner(recv, name).is_none()
9905 && !has_own_for_shadow(recv, name)
9906 && inherited_builtin_static(recv, name).is_none()
9907 && with_host(|h| host::lookup_chain(h, recv, name)).is_none()
9908 {
9909 return Err(host::type_error(&format!("{name} is not a function")));
9910 }
9911 // A method monkey-patched onto the receiver's intrinsic prototype. The READ
9912 // path resolves these, but dispatch goes straight to the builtin table and
9913 // never consults it, so `Array.prototype.last = f; [1].last()` threw "is not
9914 // a function" while `[1].last` WAS `f` — the read and the call disagreeing
9915 // about the same name, on the one path a polyfill actually uses.
9916 if !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
9917 if let Some(f) = inherited_builtin_static(recv, name) {
9918 if with_host(|h| host::is_callable(h, &f)) {
9919 return host::invoke(&f, args, Some(recv.clone()));
9920 }
9921 }
9922 }
9923 // Every object INHERITS the `Object.prototype` methods, and an exotic that
9924 // does not define its own reaches them the same way. Each kind's dispatch
9925 // table below only knows its own methods, so `new Map().toString()`,
9926 // `promise.hasOwnProperty(k)` and `sym.toLocaleString()` all reported "is
9927 // not a function" — `Object.prototype.toString.call(m)` worked while
9928 // `m.toString()` did not.
9929 // The allowlist is the kinds whose own dispatch table below would otherwise
9930 // claim the name. Every OTHER receiver reaches an `Object.prototype` method
9931 // the same way — a function, a class and a bound function included, where
9932 // `f.hasOwnProperty(k)` reported "is not a function" even though the READ
9933 // resolved it. `inherited_method_owner` decides which prototype owns the
9934 // name, so a kind that defines its own still gets its own.
9935 if is_object_builtin_method(name)
9936 && (inherited_method_owner(recv, name) == Some("Object")
9937 || (inherits_object_methods(recv) && !overrides_object_method(recv, name)))
9938 {
9939 // `toString` goes through the branded form (20.1.3.6), which reads
9940 // `Symbol.toStringTag` and falls back to the receiver's own brand —
9941 // `[object Map]`, not the generic stringification.
9942 if name == "toString" {
9943 return proto_method(recv, "Object:toString", args);
9944 }
9945 return object_builtin_method(recv, name, args);
9946 }
9947 // `Object.prototype.valueOf` is inherited by every exotic that does not
9948 // override it (an Array does not), and returns the receiver. Without this
9949 // the `ToPrimitive` probe on `[o] + ''` reached `array_method("valueOf")`
9950 // and threw `valueOf is not a function`.
9951 if name == "valueOf"
9952 && matches!(
9953 with_host(|h| h.kind_of(recv)),
9954 Some(
9955 ObjKind::Array
9956 | ObjKind::Map
9957 | ObjKind::Set
9958 | ObjKind::Generator
9959 | ObjKind::Promise
9960 | ObjKind::Iter
9961 | ObjKind::RegExp
9962 )
9963 )
9964 {
9965 return Ok(recv.clone());
9966 }
9967 // Only the tag is needed to pick the branch — cloning the receiver here made
9968 // every `arr.push(x)` copy the whole array, so a fill loop was O(n^2).
9969 match with_host(|h| h.kind_of(recv)) {
9970 Some(ObjKind::Array) => array_method(recv, name, args),
9971 Some(ObjKind::Str) => {
9972 // `string_method` consumes the text itself, so this clone is the
9973 // payload, not a tag probe.
9974 let s = peek(recv, |o| match o {
9975 JsObj::Str(s) => Some(s.clone()),
9976 _ => None,
9977 })
9978 .unwrap_or_default();
9979 string_method(&s, name, args)
9980 }
9981 Some(ObjKind::Map) => map_method(recv, name, args),
9982 Some(ObjKind::Set) => set_method(recv, name, args),
9983 Some(ObjKind::Generator) if crate::stdlib::iterator::is_helper(name) => {
9984 crate::stdlib::iterator::call(recv, name, &args)
9985 }
9986 Some(ObjKind::Generator) => generator_method(recv, name, args),
9987 Some(ObjKind::Promise) => promise_method(recv, name, args),
9988 Some(ObjKind::Iter) if crate::stdlib::iterator::is_helper(name) => {
9989 crate::stdlib::iterator::call(recv, name, &args)
9990 }
9991 Some(ObjKind::Iter) => iter_method(recv, name, args),
9992 Some(ObjKind::Symbol) => symbol_method(recv, name, args),
9993 Some(ObjKind::BigInt) => {
9994 let b = peek(recv, |o| match o {
9995 JsObj::BigInt(b) => Some(b.clone()),
9996 _ => None,
9997 })
9998 .unwrap_or_default();
9999 bigint_method(&b, name, args)
10000 }
10001 Some(ObjKind::RegExp) => crate::regexp::regexp_method(recv, name, args),
10002 Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc) => {
10003 match function_builtin_method(recv, name, &args)? {
10004 Some(v) => Ok(v),
10005 None => Err(host::type_error(&format!("{name} is not a function"))),
10006 }
10007 }
10008 Some(ObjKind::Object) => {
10009 if let Some(f) = peek(recv, |o| match o {
10010 JsObj::Object(p) => p.get(name).cloned(),
10011 _ => None,
10012 }) {
10013 host::invoke(&f, args, Some(recv.clone()))
10014 } else if name == "hasOwnProperty" {
10015 let k = with_host(|h| h.str_of(&arg0(&args)));
10016 let has = peek(recv, |o| match o {
10017 JsObj::Object(p) => Some(p.contains_key(&k)),
10018 _ => None,
10019 })
10020 .unwrap_or(false);
10021 Ok(Value::Bool(has))
10022 } else if name == "toString" {
10023 Ok(with_host(|h| h.new_str("[object Object]")))
10024 } else {
10025 Err(host::type_error(&format!("{} is not a function", name)))
10026 }
10027 }
10028 _ => {
10029 // Primitive number/bool/string coercions.
10030 if let Value::Float(_) | Value::Int(_) = recv {
10031 return number_method(with_host(|h| h.to_number(recv)), name, args);
10032 }
10033 if let Some(s) = with_host(|h| h.as_str(recv)) {
10034 return string_method(&s, name, args);
10035 }
10036 // `Boolean.prototype` (20.3.3): a boolean is not a heap object here,
10037 // so it reached no branch at all and `true.toString()` threw `is not
10038 // a function`. Its three methods are `toString`, `valueOf`, and the
10039 // inherited `Object.prototype.toLocaleString` — which
10040 // `[1,'a',true].toLocaleString()` invokes per element, so the hole
10041 // was reachable from the array form too.
10042 if let Value::Bool(b) = recv {
10043 return match name {
10044 "toString" | "toLocaleString" => {
10045 Ok(new_s(if *b { "true" } else { "false" }.to_string()))
10046 }
10047 "valueOf" => Ok(Value::Bool(*b)),
10048 _ => Err(host::type_error(&format!("{name} is not a function"))),
10049 };
10050 }
10051 Err(host::type_error(&format!("{} is not a function", name)))
10052 }
10053 }
10054}
10055
10056/// A copy of the whole backing store, for the methods that genuinely consume
10057/// every element (`map`, `filter`, `join`, …). Never call it just to read
10058/// `.len()` — use [`array_len`], or `push`/`unshift` become O(n) per call.
10059/// A LIVE iterator over a `Map` or `Set`.
10060///
10061/// Node's collection iterators see the collection as it is at each step: an
10062/// entry added during iteration IS visited, and one deleted before it is
10063/// reached is NOT. Ours materialized every entry up front, so both were wrong —
10064/// a loop that deletes as it goes still processed the entries it had removed.
10065///
10066/// The cursor is the last key yielded plus the index it was at. On each step
10067/// the key is located again in the CURRENT order: if it is still there the next
10068/// entry follows it, and if it was itself deleted the stored index now names
10069/// the entry that shifted into its place. That reproduces node for the cases
10070/// its own tests turn on — add-during, delete-ahead, delete-self,
10071/// delete-behind, delete-the-rest and clear — without giving `Map` the
10072/// tombstoned entry list node uses internally.
10073fn collection_iterator(coll: &Value, kind: &str) -> Value {
10074 with_host(|h| {
10075 let mut m = IndexMap::new();
10076 m.insert(
10077 "@@native".into(),
10078 h.new_str("CollectionIterator".to_string()),
10079 );
10080 m.insert("@@coll".into(), coll.clone());
10081 m.insert("@@kind".into(), h.new_str(kind.to_string()));
10082 m.insert("@@started".into(), Value::Bool(false));
10083 m.insert("@@lastIdx".into(), Value::Float(0.0));
10084 h.new_object(m)
10085 })
10086}
10087
10088/// One step of a live collection iterator.
10089pub(crate) fn collection_iterator_next(recv: &Value) -> Result<Value, String> {
10090 let slot = |k: &str| {
10091 with_host(|h| match h.get(recv) {
10092 Some(JsObj::Object(p)) => p.get(k).cloned(),
10093 _ => None,
10094 })
10095 };
10096 let coll = slot("@@coll").unwrap_or(Value::Undef);
10097 let kind = slot("@@kind")
10098 .map(|v| with_host(|h| h.str_of(&v)))
10099 .unwrap_or_default();
10100 let started = slot("@@started").is_some_and(|v| with_host(|h| h.truthy(&v)));
10101 let last_idx = slot("@@lastIdx")
10102 .map(|v| with_host(|h| h.to_number(&v)) as usize)
10103 .unwrap_or(0);
10104 let last_key = slot("@@lastKey");
10105
10106 let next_idx = if !started {
10107 0
10108 } else {
10109 match last_key
10110 .as_ref()
10111 .and_then(|k| with_host(|h| collection_index_of(h, &coll, k)))
10112 {
10113 // Still present: continue after it.
10114 Some(i) => i + 1,
10115 // Deleted since: whatever shifted into its slot is next.
10116 None => last_idx,
10117 }
10118 };
10119 let entry = with_host(|h| collection_entry_at(h, &coll, next_idx));
10120 let Some((k, v)) = entry else {
10121 return Ok(iter_result(Value::Undef, true));
10122 };
10123 with_host(|h| {
10124 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
10125 p.insert("@@started".into(), Value::Bool(true));
10126 p.insert("@@lastIdx".into(), Value::Float(next_idx as f64));
10127 p.insert("@@lastKey".into(), k.clone());
10128 }
10129 });
10130 let out = match kind.as_str() {
10131 "keys" => k,
10132 "values" => v,
10133 _ => with_host(|h| h.new_array(vec![k, v])),
10134 };
10135 Ok(iter_result(out, false))
10136}
10137
10138/// What `util.inspect` shows for a live `Map`/`Set` iterator: its brand
10139/// (`Map Iterator`, `Map Entries`, `Set Iterator`, `Set Entries`) and the
10140/// entries it has still to yield, as `(key, value)` pairs — without advancing
10141/// it. `None` when `v` is not one.
10142pub(crate) fn collection_iterator_view(
10143 h: &host::JsHost,
10144 v: &Value,
10145) -> Option<(&'static str, Vec<(Value, Value)>)> {
10146 let Some(JsObj::Object(p)) = h.get(v) else {
10147 return None;
10148 };
10149 if p.get("@@native").map(|t| h.str_of(t)).as_deref() != Some("CollectionIterator") {
10150 return None;
10151 }
10152 let coll = p.get("@@coll").cloned().unwrap_or(Value::Undef);
10153 let kind = p.get("@@kind").map(|k| h.str_of(k)).unwrap_or_default();
10154 let started = p.get("@@started").is_some_and(|s| h.truthy(s));
10155 let last_idx = p
10156 .get("@@lastIdx")
10157 .map(|n| h.to_number(n) as usize)
10158 .unwrap_or(0);
10159 let is_map = matches!(h.get(&coll), Some(JsObj::Map { .. }));
10160 let brand = match (is_map, kind == "entries") {
10161 (true, true) => "Map Entries",
10162 (true, false) => "Map Iterator",
10163 (false, true) => "Set Entries",
10164 (false, false) => "Set Iterator",
10165 };
10166 // The same cursor rule `collection_iterator_next` steps by.
10167 let mut idx = if !started {
10168 0
10169 } else {
10170 match p
10171 .get("@@lastKey")
10172 .and_then(|k| collection_index_of(h, &coll, k))
10173 {
10174 Some(i) => i + 1,
10175 None => last_idx,
10176 }
10177 };
10178 let mut rest = Vec::new();
10179 while let Some((k, val)) = collection_entry_at(h, &coll, idx) {
10180 rest.push(match kind.as_str() {
10181 "keys" => (k.clone(), k),
10182 "values" => (val.clone(), val),
10183 _ => (k, val),
10184 });
10185 idx += 1;
10186 }
10187 Some((brand, rest))
10188}
10189
10190/// The (key, value) at `idx` in a Map, or (value, value) in a Set.
10191fn collection_entry_at(h: &host::JsHost, coll: &Value, idx: usize) -> Option<(Value, Value)> {
10192 match h.get(coll) {
10193 Some(JsObj::Map { entries, .. }) => entries.get_index(idx).map(|(_, kv)| kv.clone()),
10194 Some(JsObj::Set { entries, .. }) => {
10195 entries.get_index(idx).map(|(_, v)| (v.clone(), v.clone()))
10196 }
10197 _ => None,
10198 }
10199}
10200
10201/// Where `key` currently sits in the collection's order.
10202fn collection_index_of(h: &host::JsHost, coll: &Value, key: &Value) -> Option<usize> {
10203 let mk = host::map_key(h, key);
10204 match h.get(coll) {
10205 Some(JsObj::Map { entries, .. }) => entries.get_index_of(&mk),
10206 Some(JsObj::Set { entries, .. }) => entries.get_index_of(&mk),
10207 _ => None,
10208 }
10209}
10210
10211/// The `thisArg` an iteration method was given, if any.
10212///
10213/// `[1].forEach(fn, thisArg)` binds `thisArg` as the callback's `this`, and so
10214/// do `map`/`filter`/`some`/`every`/`find`/`findIndex`/`findLast`/
10215/// `findLastIndex`/`flatMap`, `Map`/`Set`/TypedArray `forEach`, and
10216/// `Array.from`'s map function. Every one of them was invoking the callback
10217/// with no receiver, so `this` inside it was undefined and the argument did
10218/// nothing.
10219fn this_arg(args: &[Value], idx: usize) -> Option<Value> {
10220 args.get(idx)
10221 .filter(|v| !matches!(v, Value::Undef))
10222 .cloned()
10223}
10224
10225/// The elements of an array, with any INDEX ACCESSOR resolved.
10226///
10227/// `Object.defineProperty(arr, 1, { get })` stores the getter in the accessor
10228/// table, and an array's elements live in a backing vector — so every method
10229/// reading that vector directly (`join`, `map`, `indexOf`, …) saw the stale
10230/// slot and never called the getter, while a plain `arr[1]` read did.
10231///
10232/// An array with no accessors pays one lookup returning an empty list, so the
10233/// ordinary case is unchanged. The getters are invoked OUTSIDE the host borrow,
10234/// since calling one re-enters.
10235/// Walk `recv` the way an `Array.prototype` iteration method does: the LENGTH
10236/// is captured once at entry (LengthOfArrayLike, step 3), but each element is
10237/// read LIVE at its index, and an index that no longer exists is skipped.
10238///
10239/// Snapshotting the whole array instead meant a callback that mutated it was
10240/// not observed: `[1,2,3].forEach(v => a.shift())` visited 1, 2, 3 where node
10241/// visits 1 and 3, and `filter` kept elements the callback had already removed.
10242///
10243/// `f` returns `Some(x)` to stop early with `x`.
10244fn array_walk<T>(
10245 recv: &Value,
10246 mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10247) -> Result<Option<T>, String> {
10248 let len = array_len(recv);
10249 for i in 0..len {
10250 // A HOLE — and an index a shrinking mutation has dropped — is skipped
10251 // without calling the callback.
10252 if index_absent(recv, i) || i >= array_len(recv) {
10253 continue;
10254 }
10255 let v = get_property(recv, &i.to_string())?;
10256 if let Some(out) = f(i, v)? {
10257 return Ok(Some(out));
10258 }
10259 }
10260 Ok(None)
10261}
10262
10263/// `array_walk`'s descending twin, for `reduceRight`/`findLast*`: the same
10264/// capture-length-once, read-each-element-live rule walked from the end. A
10265/// callback that SHRINKS the array is observed by every later step, so the
10266/// indices it drops are skipped rather than served from a stale copy.
10267fn array_walk_rev<T>(
10268 recv: &Value,
10269 from: usize,
10270 mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10271) -> Result<Option<T>, String> {
10272 for i in (0..from).rev() {
10273 if index_absent(recv, i) || i >= array_len(recv) {
10274 continue;
10275 }
10276 let v = get_property(recv, &i.to_string())?;
10277 if let Some(out) = f(i, v)? {
10278 return Ok(Some(out));
10279 }
10280 }
10281 Ok(None)
10282}
10283
10284/// The live read behind `indexOf`/`includes`/`join`: the element at `i`, or
10285/// `undefined` once a mutation has shrunk the array past it.
10286fn array_elem_live(recv: &Value, i: usize) -> Result<Value, String> {
10287 if i >= array_len(recv) {
10288 return Ok(Value::Undef);
10289 }
10290 get_property(recv, &i.to_string())
10291}
10292
10293fn array_items(recv: &Value) -> Vec<Value> {
10294 let mut items = with_host(|h| match h.get(recv) {
10295 Some(JsObj::Array(items)) => items.clone(),
10296 _ => Vec::new(),
10297 });
10298 resolve_index_accessors(recv, &mut items);
10299 items
10300}
10301
10302/// Replace each slot that has an own accessor with what its getter returns.
10303pub(crate) fn resolve_index_accessors_pub(recv: &Value, items: &mut [Value]) {
10304 resolve_index_accessors(recv, items);
10305}
10306
10307/// Returns whether any slot was replaced, which the JSON walk needs: it keeps
10308/// the ORIGINAL array when nothing changed, and the original still holds the
10309/// stale slots.
10310fn resolve_index_accessors(recv: &Value, items: &mut [Value]) -> bool {
10311 let mut indices: Vec<usize> = with_host(|h| h.own_accessor_keys(recv))
10312 .into_iter()
10313 .filter_map(|k| k.parse::<usize>().ok())
10314 .filter(|i| *i < items.len())
10315 .collect();
10316 // An ELIDED index the prototype chain supplies is stale in the backing
10317 // vector too — it holds `undefined` where `[[Get]]` answers the inherited
10318 // value. Spread and `JSON.stringify` both read through here, and both
10319 // rendered the hole rather than what `a[i]` reads.
10320 let inherited: Vec<usize> = with_host(|h| h.hole_indices(recv))
10321 .into_iter()
10322 .filter(|i| *i < items.len() && !indices.contains(i))
10323 .filter(|i| has_property(recv, &i.to_string()).unwrap_or(false))
10324 .collect();
10325 indices.extend(inherited);
10326 let mut replaced = false;
10327 for i in indices {
10328 if let Ok(v) = get_property(recv, &i.to_string()) {
10329 items[i] = v;
10330 replaced = true;
10331 }
10332 }
10333 replaced
10334}
10335
10336/// The ELIDED positions of array `recv` as a membership set. A dense array —
10337/// which is nearly every array — answers with an empty set after a single
10338/// negative hash probe and allocates nothing.
10339///
10340/// The iteration methods split into two groups, and the split is not a matter of
10341/// taste: the ones spec'd through `HasProperty` (`forEach`, `map`, `filter`,
10342/// `some`, `every`, `reduce`, `indexOf`, `flat`, `sort`) SKIP a hole, while the
10343/// ones spec'd through a bare `Get` (`for…of`, spread, `find`, `includes`,
10344/// `join`, `entries`, `Array.from`) see the `undefined` a hole reads back as.
10345fn hole_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10346 with_host(|h| h.hole_indices(recv)).into_iter().collect()
10347}
10348
10349/// The indices `recv` genuinely has NO property at — the elided ones the
10350/// prototype chain does not supply either.
10351///
10352/// Every array method tests `HasProperty` before deciding to skip a position
10353/// (23.1.3.x, uniformly), and `HasProperty` walks the chain. Testing elision
10354/// alone made an inherited element invisible to all of them: with
10355/// `Array.prototype[1] = 'p'`, `[1,,3].map(v => v)` produced a hole where node
10356/// produces `'p'`, and `flat`/`concat`/`slice`/`sort`/`indexOf` each dropped
10357/// the same position.
10358///
10359/// `hole_set` remains the elision record itself, which is what `splice` moves
10360/// around — that bookkeeping is about the array's OWN storage and must not
10361/// consult the chain.
10362fn absent_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10363 hole_set(recv)
10364 .into_iter()
10365 .filter(|i| !has_property(recv, &i.to_string()).unwrap_or(false))
10366 .collect()
10367}
10368
10369/// The single-index form of [`absent_set`], for the walkers that test one
10370/// position at a time.
10371fn index_absent(recv: &Value, i: usize) -> bool {
10372 with_host(|h| h.is_hole(recv, i)) && !has_property(recv, &i.to_string()).unwrap_or(false)
10373}
10374
10375/// The element count, without copying the elements.
10376fn array_len(recv: &Value) -> usize {
10377 peek(recv, |o| match o {
10378 JsObj::Array(items) => Some(items.len()),
10379 _ => None,
10380 })
10381 .unwrap_or(0)
10382}
10383
10384/// `ArraySpeciesCreate(originalArray, length)` (23.1.3.4) — the constructor an
10385/// array method builds its RESULT with.
10386///
10387/// `map`, `filter`, `slice`, `concat`, `splice`, `flat` and `flatMap` all
10388/// produce an array of the receiver's own species, so on a `class A extends
10389/// Array` the result is an `A`. Every one of them allocated a plain array
10390/// instead, so `A.from([1]).map(x => x) instanceof A` was false.
10391///
10392/// The default `get [Symbol.species]() { return this }` is what makes the
10393/// subclass the species; a class overriding it with `Array` gets a plain array
10394/// back, which is the documented way to opt out.
10395/// Build an array-shaped result through `ctor`, or a plain array when there is
10396/// none to build through.
10397///
10398/// The constructor is called with the LENGTH and the elements written after, as
10399/// 23.1.2.1 and 23.1.3.4 both specify — which is what lets a subclass
10400/// constructor observe the allocation.
10401fn construct_array_like(ctor: Option<Value>, items: Vec<Value>) -> Result<Value, String> {
10402 let Some(ctor) = ctor.filter(|c| {
10403 matches!(
10404 with_host(|h| h.kind_of(c)),
10405 Some(ObjKind::Class) | Some(ObjKind::Func)
10406 )
10407 }) else {
10408 return Ok(with_host(|h| h.new_array(items)));
10409 };
10410 let out = host::construct(&ctor, vec![Value::Float(items.len() as f64)])?;
10411 write_elements(&out, items);
10412 Ok(out)
10413}
10414
10415/// Write `items` into a freshly constructed array-shaped `out`, clearing the
10416/// hole marks the length-only construction left behind.
10417///
10418/// `new A(3)` on `class A extends Array` really does produce three HOLES, and
10419/// the elements written over them stayed marked — so every subclass result of
10420/// `map`/`filter`/`flat` read back as holes: `A.from([1,2,3]).map(x => x * 2)`
10421/// had length 3 and printed `[null,null,null]`, and `0 in` it was false.
10422fn write_elements(out: &Value, items: Vec<Value>) {
10423 with_host(|h| {
10424 h.clear_holes(out);
10425 if let Some(JsObj::Array(dst)) = h.get_mut(out) {
10426 *dst = items;
10427 }
10428 });
10429}
10430
10431fn array_species_create(recv: &Value, items: Vec<Value>) -> Result<Value, String> {
10432 let plain = || with_host(|h| h.new_array(items.clone()));
10433 // Only a subclass instance can have a species of its own: a plain array's
10434 // `constructor` is the `Array` builtin, whose species is `Array`.
10435 // A chain lookup, not `get_property`: an Array receiver resolves its
10436 // properties through the stdlib funnel, which has no `constructor` entry,
10437 // so the read alone reports `undefined` for every subclass instance. A
10438 // Proxy is the exception — it has no property map to walk, and its
10439 // `constructor` comes from the `get` trap, so a proxied subclass array
10440 // produced plain arrays.
10441 let ctor = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
10442 get_property(recv, "constructor").unwrap_or(Value::Undef)
10443 } else {
10444 with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef)
10445 };
10446 if !matches!(
10447 with_host(|h| h.kind_of(&ctor)),
10448 Some(ObjKind::Class) | Some(ObjKind::Func)
10449 ) {
10450 return Ok(plain());
10451 }
10452 // `C[@@species]` (23.1.3.4 step 5): a subclass that does not override the
10453 // accessor reads back ITSELF (the `@@species` arm of the class static
10454 // lookup), so an `undefined` or `null` here was written by user code — a
10455 // `static get [Symbol.species]() { return undefined }` — and both mean
10456 // "make a plain Array". A getter that throws propagates.
10457 let species = match get_property(&ctor, "@@species")? {
10458 Value::Undef => return Ok(plain()),
10459 s if with_host(|h| h.is_null(&s)) => return Ok(plain()),
10460 s => s,
10461 };
10462 if !matches!(
10463 with_host(|h| h.kind_of(&species)),
10464 Some(ObjKind::Class) | Some(ObjKind::Func)
10465 ) {
10466 return Ok(plain());
10467 }
10468 let out = host::construct(&species, vec![Value::Float(items.len() as f64)])?;
10469 // The constructor is called with the LENGTH, so the elements are written
10470 // afterwards — which is also what lets a subclass constructor observe the
10471 // allocation, as node's does.
10472 write_elements(&out, items);
10473 Ok(out)
10474}
10475
10476fn array_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
10477 array_method_on(recv, recv, name, args)
10478}
10479
10480/// The `Array.prototype` methods that WRITE to their receiver, and so need the
10481/// generic path to copy the result back onto the array-like.
10482const ARRAY_MUTATORS: &[&str] = &[
10483 "push",
10484 "pop",
10485 "shift",
10486 "unshift",
10487 "splice",
10488 "sort",
10489 "reverse",
10490 "fill",
10491 "copyWithin",
10492];
10493
10494/// Run `Array.prototype.<method>` against an array-LIKE (`{0: 'a', length: 1}`,
10495/// a DOM-ish collection, `arguments`).
10496///
10497/// 23.1.3 defines every one of these over `LengthOfArrayLike(O)` and `Get(O, k)`
10498/// rather than over an Array's element vector, so the receiver only has to have
10499/// a `length`. The elements are read out into a temporary Array, the ordinary
10500/// implementation runs on that, and a MUTATING method writes the result back —
10501/// which keeps one implementation of each method rather than a second, generic
10502/// one that could drift from it.
10503///
10504/// An index the receiver does not own is a HOLE in the temporary, so the
10505/// methods that skip holes skip it here too, exactly as `HasProperty` makes them.
10506fn array_generic(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
10507 let len = match get_property(recv, "length") {
10508 Ok(v) => host::to_array_length(&v).unwrap_or(0),
10509 Err(_) => 0,
10510 };
10511 // A STRING receiver owns every index of its length; `has_property` answers
10512 // for objects and reports none of them, which made `[].map.call('abc', f)`
10513 // an array of three holes.
10514 let dense = with_host(|h| h.as_str(recv)).is_some();
10515 let mut items = Vec::with_capacity(len);
10516 let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
10517 for i in 0..len {
10518 let k = i.to_string();
10519 if dense || has_property(recv, &k)? {
10520 items.push(get_property(recv, &k)?);
10521 } else {
10522 holes.insert(i);
10523 items.push(Value::Undef);
10524 }
10525 }
10526 let tmp = with_host(|h| {
10527 let a = h.new_array(items);
10528 h.install_holes(&a, holes);
10529 a
10530 });
10531 let out = array_method_on(&tmp, recv, method, args)?;
10532 if ARRAY_MUTATORS.contains(&method) {
10533 let result = with_host(|h| match h.get(&tmp) {
10534 Some(JsObj::Array(items)) => items.clone(),
10535 _ => Vec::new(),
10536 });
10537 for (i, v) in result.iter().enumerate() {
10538 set_property(recv, &i.to_string(), v.clone())?;
10539 }
10540 set_property(recv, "length", Value::Float(result.len() as f64))?;
10541 }
10542 Ok(out)
10543}
10544
10545/// `Array.prototype.<name>` on `recv`.
10546///
10547/// `this_value` is what a callback receives as its third argument and what a
10548/// mutating method returns — the same object as `recv` for an ordinary array
10549/// call, but the ORIGINAL array-like when `array_generic` runs a method against
10550/// a temporary copy (`Array.prototype.slice.call(arguments)`).
10551fn array_method_on(
10552 recv: &Value,
10553 this_value: &Value,
10554 name: &str,
10555 args: Vec<Value>,
10556) -> Result<Value, String> {
10557 let args = coerce_numeric_args(ARRAY_METHOD_NUMERIC_ARGS, name, args)?;
10558 match name {
10559 "push" => {
10560 // 23.1.3.23 step 4 defines each new element through
10561 // `CreateDataPropertyOrThrow`, so a NON-EXTENSIBLE array refuses it:
10562 // `Object.seal(a)` / `preventExtensions(a)` then `a.push(x)` is a
10563 // TypeError. The elements were appended to the backing vector
10564 // regardless, so sealing an array did not seal it.
10565 if !args.is_empty() && !with_host(|h| h.is_extensible(recv)) {
10566 let at = array_len(recv);
10567 return Err(host::type_error(&format!(
10568 "Cannot add property {at}, object is not extensible"
10569 )));
10570 }
10571 // Step 5 then SETS `length`, so a non-writable one refuses the push
10572 // too — `defineProperty(a, 'length', {writable: false})` makes an
10573 // array append-proof without sealing it.
10574 if !args.is_empty() && !with_host(|h| h.prop_attrs(recv, "length").writable) {
10575 return Err(host::type_error(
10576 "Cannot assign to read only property 'length' of object '[object Array]'",
10577 ));
10578 }
10579 // `push` returns the new length; take it from the same mutable
10580 // borrow rather than copying the array back out to count it.
10581 let len = with_host(|h| {
10582 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10583 items.extend(args.iter().cloned());
10584 items.len()
10585 } else {
10586 0
10587 }
10588 });
10589 Ok(Value::Float(len as f64))
10590 }
10591 "pop" => Ok(with_host(|h| {
10592 let popped = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10593 items.pop().unwrap_or(Value::Undef)
10594 } else {
10595 Value::Undef
10596 };
10597 let len = match h.get(recv) {
10598 Some(JsObj::Array(items)) => items.len(),
10599 _ => 0,
10600 };
10601 h.truncate_holes(recv, len);
10602 popped
10603 })),
10604 "shift" => Ok(with_host(|h| {
10605 let shifted = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10606 if items.is_empty() {
10607 Value::Undef
10608 } else {
10609 items.remove(0)
10610 }
10611 } else {
10612 Value::Undef
10613 };
10614 h.remap_holes(recv, |i| i.checked_sub(1));
10615 shifted
10616 })),
10617 "unshift" => {
10618 with_host(|h| {
10619 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10620 for (i, a) in args.iter().enumerate() {
10621 items.insert(i, a.clone());
10622 }
10623 }
10624 let n = args.len();
10625 h.remap_holes(recv, |i| Some(i + n));
10626 });
10627 Ok(Value::Float(array_len(recv) as f64))
10628 }
10629 "join" => {
10630 let sep = if args.is_empty() || matches!(args[0], Value::Undef) {
10631 ",".to_string()
10632 } else {
10633 arg_to_string(&args, 0)?
10634 };
10635 join_array(recv, &sep)
10636 }
10637 // `Array.prototype.toLocaleString` (23.1.3.32): comma-join the elements'
10638 // OWN `toLocaleString` results, with `null`/`undefined` contributing the
10639 // empty string. It threw `is not a function` — the whole method was
10640 // missing — so `[1234.5, 'x'].toLocaleString()` was unreachable.
10641 "toLocaleString" => {
10642 // Shares `join`'s JoinStack: measured on node v26.7.0, `h=[1]`
10643 // `h.push(h)` makes `h.toLocaleString()` `"1,"`, not a stack overflow.
10644 if !host::join_stack_push(recv) {
10645 return Ok(with_host(|h| h.new_str(String::new())));
10646 }
10647 let items = array_items(recv);
10648 let mut parts: Vec<String> = Vec::with_capacity(items.len());
10649 for it in &items {
10650 if with_host(|h| h.is_nullish(it)) {
10651 parts.push(String::new());
10652 continue;
10653 }
10654 let v = match host::call_method(it, "toLocaleString", Vec::new()) {
10655 Ok(v) => v,
10656 Err(e) => {
10657 host::join_stack_pop();
10658 return Err(e);
10659 }
10660 };
10661 parts.push(with_host(|h| h.str_of(&v)));
10662 }
10663 host::join_stack_pop();
10664 Ok(with_host(|h| h.new_str(parts.join(","))))
10665 }
10666 // `indexOf`/`lastIndexOf` are spec'd through `HasProperty`, so a hole is
10667 // never a match: `[1,,3].indexOf(undefined)` is `-1`, while the
10668 // `Get`-based `includes` reports `true` for the same array.
10669 "indexOf" => {
10670 let target = arg0(&args);
10671 let len = array_len(recv);
10672 let start = search_start(arg_num(&args, 1), len);
10673 let mut idx = None;
10674 for i in start..len {
10675 // 23.1.3.17 steps 8a-8b: HasProperty first, so a hole — and an
10676 // index a mutation has since dropped — is skipped, not compared.
10677 if index_absent(recv, i) || i >= array_len(recv) {
10678 continue;
10679 }
10680 let x = get_property(recv, &i.to_string())?;
10681 if with_host(|h| h.strict_eq(&x, &target)) {
10682 idx = Some(i);
10683 break;
10684 }
10685 }
10686 Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10687 }
10688 "lastIndexOf" => {
10689 let items = array_items(recv);
10690 let holes = absent_set(recv);
10691 let target = arg0(&args);
10692 let from = (args.len() > 1).then(|| arg_num(&args, 1));
10693 let idx = match search_start_last(from, items.len()) {
10694 None => None,
10695 Some(start) => with_host(|h| {
10696 items[..=start]
10697 .iter()
10698 .enumerate()
10699 .rev()
10700 .find(|(i, x)| !holes.contains(i) && h.strict_eq(x, &target))
10701 .map(|(i, _)| i)
10702 }),
10703 };
10704 Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10705 }
10706 "includes" => {
10707 // Array.includes uses SameValueZero: unlike `===`, NaN matches NaN.
10708 // Unlike `indexOf` it has no HasProperty step (23.1.3.16 step 5b), so
10709 // a hole reads as `undefined` and `[,].includes(undefined)` is true.
10710 let target = arg0(&args);
10711 let tnan = matches!(target, Value::Float(f) if f.is_nan());
10712 let len = array_len(recv);
10713 let start = search_start(arg_num(&args, 1), len);
10714 let mut found = false;
10715 for i in start..len {
10716 let x = array_elem_live(recv, i)?;
10717 if (tnan && matches!(x, Value::Float(f) if f.is_nan()))
10718 || with_host(|h| h.strict_eq(&x, &target))
10719 {
10720 found = true;
10721 break;
10722 }
10723 }
10724 Ok(Value::Bool(found))
10725 }
10726 "slice" => {
10727 let items = array_items(recv);
10728 let (lo, hi) = slice_bounds(&args, items.len());
10729 let out = array_species_create(this_value, items[lo..hi].to_vec())?;
10730 with_host(|h| h.copy_holes(recv, &out, |i| (i >= lo && i < hi).then(|| i - lo)));
10731 Ok(out)
10732 }
10733 "concat" => {
10734 // `Symbol.isConcatSpreadable` (23.1.3.1) decides whether a value
10735 // is spread, overriding `IsArray` in BOTH directions: a plain
10736 // array-like opts IN, and an array opts OUT.
10737 let spreadable = |a: &Value| -> bool {
10738 let flag = get_property(a, "@@isConcatSpreadable").unwrap_or(Value::Undef);
10739 if matches!(flag, Value::Undef) {
10740 matches!(with_host(|h| h.get(a).cloned()), Some(JsObj::Array(_)))
10741 && !is_arguments(a)
10742 } else {
10743 with_host(|h| h.truthy(&flag))
10744 }
10745 };
10746 // Step 5 iterates `« O » ++ items`, so the receiver takes the same
10747 // test: a non-spreadable `this` (`concat.call("ab", 1)`) is ONE
10748 // element, its `ToObject` box, not the characters `array_generic`
10749 // read out of it. A hole in a spread receiver or argument stays a
10750 // hole in the result, at its shifted position.
10751 let (mut out, mut holes) = if spreadable(this_value) {
10752 (array_items(recv), absent_set(recv))
10753 } else {
10754 (vec![to_object(this_value)], Default::default())
10755 };
10756 let mut sources: Vec<(Value, usize)> = Vec::new();
10757 for a in &args {
10758 if !spreadable(a) {
10759 out.push(a.clone());
10760 continue;
10761 }
10762 match with_host(|h| h.get(a).cloned()) {
10763 // Read off the backing vector rather than through
10764 // `array_items`, so the resolve that does for the receiver
10765 // has to be done here too: 23.1.3.1 step 5.c.iv is a
10766 // `[[Get]]`, and an index with a getter — or an elided one
10767 // the chain supplies — is stale in that vector.
10768 Some(JsObj::Array(mut items)) => {
10769 resolve_index_accessors(a, &mut items);
10770 sources.push((a.clone(), out.len()));
10771 out.extend(items);
10772 }
10773 // An opted-in array-LIKE spreads by its `length` and index
10774 // properties rather than by a backing vector it has none of.
10775 _ => {
10776 let len = get_property(a, "length").unwrap_or(Value::Undef);
10777 let n = with_host(|h| h.to_number(&len));
10778 let n = if n.is_finite() {
10779 n.max(0.0) as usize
10780 } else {
10781 0
10782 };
10783 for i in 0..n {
10784 out.push(get_property(a, &i.to_string()).unwrap_or(Value::Undef));
10785 }
10786 }
10787 }
10788 }
10789
10790 for (src, base) in sources {
10791 holes.extend(
10792 with_host(|h| h.hole_indices(&src))
10793 .into_iter()
10794 .map(|i| i + base),
10795 );
10796 }
10797 let arr = array_species_create(this_value, out)?;
10798 with_host(|h| h.install_holes(&arr, holes));
10799 Ok(arr)
10800 }
10801 "reverse" => {
10802 let len = array_len(recv);
10803 with_host(|h| {
10804 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10805 items.reverse();
10806 }
10807 h.remap_holes(recv, |i| Some(len - 1 - i));
10808 });
10809 Ok(this_value.clone())
10810 }
10811 "fill" => {
10812 // fill(value[, start[, end]]) — negative indices count from the end.
10813 let val = arg0(&args);
10814 let len = array_len(recv) as i64;
10815 let norm =
10816 |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10817 let start = if args.len() >= 2 {
10818 norm(arg_num(&args, 1) as i64)
10819 } else {
10820 0
10821 };
10822 let end = if args.len() >= 3 {
10823 norm(arg_num(&args, 2) as i64)
10824 } else {
10825 len as usize
10826 };
10827 with_host(|h| {
10828 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10829 for it in items.iter_mut().take(end).skip(start) {
10830 *it = val.clone();
10831 }
10832 }
10833 // Every filled position now holds a real value.
10834 h.remap_holes(recv, |i| (i < start || i >= end).then_some(i));
10835 });
10836 Ok(this_value.clone())
10837 }
10838 "copyWithin" => {
10839 // copyWithin(target, start[, end]) — copy a slice within the array.
10840 let items = array_items(recv);
10841 let len = items.len() as i64;
10842 let norm =
10843 |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10844 let target = norm(arg_num(&args, 0) as i64);
10845 let start = if args.len() >= 2 {
10846 norm(arg_num(&args, 1) as i64)
10847 } else {
10848 0
10849 };
10850 let end = if args.len() >= 3 {
10851 norm(arg_num(&args, 2) as i64)
10852 } else {
10853 len as usize
10854 };
10855 let slice: Vec<Value> = items[start..end.max(start)].to_vec();
10856 let copied = slice.len();
10857 // A copied position takes its SOURCE's hole-ness (10.4.2 copyWithin
10858 // deletes the target when the source has no such property);
10859 // everything outside the written range keeps its own.
10860 let src_holes = absent_set(recv);
10861 with_host(|h| {
10862 if let Some(JsObj::Array(a)) = h.get_mut(recv) {
10863 for (k, v) in slice.into_iter().enumerate() {
10864 if target + k < a.len() {
10865 a[target + k] = v;
10866 }
10867 }
10868 }
10869 let len = len as usize;
10870 let mut holes: rustc_hash::FxHashSet<usize> = src_holes
10871 .iter()
10872 .copied()
10873 .filter(|i| *i < target || *i >= (target + copied).min(len))
10874 .collect();
10875 for k in 0..copied {
10876 if target + k < len && src_holes.contains(&(start + k)) {
10877 holes.insert(target + k);
10878 }
10879 }
10880 h.install_holes(recv, holes);
10881 });
10882 Ok(this_value.clone())
10883 }
10884 "at" => {
10885 let items = array_items(recv);
10886 let mut i = arg_num(&args, 0) as i64;
10887 if i < 0 {
10888 i += items.len() as i64;
10889 }
10890 Ok(if i >= 0 && (i as usize) < items.len() {
10891 items[i as usize].clone()
10892 } else {
10893 Value::Undef
10894 })
10895 }
10896 // 23.1.3.21: the callback runs only where `HasProperty` holds, and the
10897 // result array is created with the SAME holes — `[1,,3].map(f)` calls `f`
10898 // twice and yields `[2, <1 empty item>, 6]`.
10899 "map" => {
10900 let holes = absent_set(recv);
10901 let cb = arg0(&args);
10902 // The result keeps the source's LENGTH, so a skipped index still
10903 // occupies a slot; `array_walk` only tells us which ones ran.
10904 let mut out = vec![Value::Undef; array_len(recv)];
10905 array_walk(recv, |i, it| {
10906 let v = host::invoke(
10907 &cb,
10908 vec![it, Value::Float(i as f64), this_value.clone()],
10909 this_arg(&args, 1),
10910 )?;
10911 if i < out.len() {
10912 out[i] = v;
10913 }
10914 Ok(None::<()>)
10915 })?;
10916 let arr = array_species_create(this_value, out)?;
10917 with_host(|h| h.install_holes(&arr, holes));
10918 Ok(arr)
10919 }
10920 "flatMap" => {
10921 let cb = arg0(&args);
10922 let thisarg = this_arg(&args, 1);
10923 let mut out = Vec::new();
10924 array_walk(recv, |i, v| {
10925 let r = host::invoke(
10926 &cb,
10927 vec![v, Value::Float(i as f64), this_value.clone()],
10928 thisarg.clone(),
10929 )?;
10930 match with_host(|h| h.get(&r).cloned()) {
10931 Some(JsObj::Array(inner)) => out.extend(inner),
10932 _ => out.push(r),
10933 }
10934 Ok(None::<()>)
10935 })?;
10936 array_species_create(this_value, out)
10937 }
10938 "filter" => {
10939 let cb = arg0(&args);
10940 let mut out = Vec::new();
10941 array_walk(recv, |i, it| {
10942 let keep = host::invoke(
10943 &cb,
10944 vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10945 this_arg(&args, 1),
10946 )?;
10947 if with_host(|h| h.truthy(&keep)) {
10948 out.push(it);
10949 }
10950 Ok(None::<()>)
10951 })?;
10952 array_species_create(this_value, out)
10953 }
10954 "forEach" => {
10955 let cb = arg0(&args);
10956 array_walk(recv, |i, it| {
10957 host::invoke(
10958 &cb,
10959 vec![it, Value::Float(i as f64), this_value.clone()],
10960 this_arg(&args, 1),
10961 )?;
10962 Ok(None::<()>)
10963 })?;
10964 Ok(Value::Undef)
10965 }
10966 "find" => {
10967 let items = array_items(recv);
10968 let cb = arg0(&args);
10969 for (i, it) in items.iter().enumerate() {
10970 let m = host::invoke(
10971 &cb,
10972 vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10973 this_arg(&args, 1),
10974 )?;
10975 if with_host(|h| h.truthy(&m)) {
10976 return Ok(it.clone());
10977 }
10978 }
10979 Ok(Value::Undef)
10980 }
10981 "findIndex" => {
10982 let items = array_items(recv);
10983 let cb = arg0(&args);
10984 for (i, it) in items.iter().enumerate() {
10985 let m = host::invoke(
10986 &cb,
10987 vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10988 this_arg(&args, 1),
10989 )?;
10990 if with_host(|h| h.truthy(&m)) {
10991 return Ok(Value::Float(i as f64));
10992 }
10993 }
10994 Ok(Value::Float(-1.0))
10995 }
10996 "some" => {
10997 let cb = arg0(&args);
10998 let thisarg = this_arg(&args, 1);
10999 let hit = array_walk(recv, |i, v| {
11000 let m = host::invoke(
11001 &cb,
11002 vec![v, Value::Float(i as f64), this_value.clone()],
11003 thisarg.clone(),
11004 )?;
11005 Ok(with_host(|h| h.truthy(&m)).then_some(()))
11006 })?;
11007 Ok(Value::Bool(hit.is_some()))
11008 }
11009 "every" => {
11010 let cb = arg0(&args);
11011 let failed = array_walk(recv, |i, it| {
11012 let m = host::invoke(
11013 &cb,
11014 vec![it, Value::Float(i as f64), this_value.clone()],
11015 this_arg(&args, 1),
11016 )?;
11017 Ok((!with_host(|h| h.truthy(&m))).then_some(()))
11018 })?;
11019 Ok(Value::Bool(failed.is_none()))
11020 }
11021 "reduce" => {
11022 let items = array_items(recv);
11023 let holes = absent_set(recv);
11024 let cb = arg0(&args);
11025 let acc;
11026 let mut start = 0;
11027 if args.len() >= 2 {
11028 acc = args[1].clone();
11029 } else {
11030 // With no seed the accumulator is the first PRESENT element, so a
11031 // leading run of holes is skipped rather than seeding `undefined`.
11032 match (0..items.len()).find(|i| !holes.contains(i)) {
11033 Some(i) => {
11034 acc = items[i].clone();
11035 start = i + 1;
11036 }
11037 None => {
11038 return Err(host::type_error(
11039 "Reduce of empty array with no initial value",
11040 ))
11041 }
11042 }
11043 }
11044 // Each element is read LIVE at its index, so a callback that
11045 // shrinks the array is observed — the tail is skipped rather than
11046 // folded from a stale snapshot.
11047 let mut cur = acc;
11048 array_walk(recv, |i, it| {
11049 if i < start {
11050 return Ok(None::<()>);
11051 }
11052 cur = host::invoke(
11053 &cb,
11054 vec![
11055 std::mem::replace(&mut cur, Value::Undef),
11056 it,
11057 Value::Float(i as f64),
11058 this_value.clone(),
11059 ],
11060 this_arg(&args, 1),
11061 )?;
11062 Ok(None::<()>)
11063 })?;
11064 Ok(cur)
11065 }
11066 "reduceRight" => {
11067 let cb = arg0(&args);
11068 let n = array_len(recv);
11069 let mut acc;
11070 let mut from = n; // one past the next index to process (walking down)
11071 if args.len() >= 2 {
11072 acc = args[1].clone();
11073 } else {
11074 let holes = absent_set(recv);
11075 match (0..n).rev().find(|i| !holes.contains(i)) {
11076 Some(k) => {
11077 acc = get_property(recv, &k.to_string())?;
11078 from = k;
11079 }
11080 None => {
11081 return Err(host::type_error(
11082 "Reduce of empty array with no initial value",
11083 ))
11084 }
11085 }
11086 }
11087 // `acc` moves into the closure and back out on every step, so it
11088 // lives in an Option the closure can take from and refill.
11089 let mut slot = Some(acc);
11090 array_walk_rev(recv, from, |i, v| {
11091 let prev = slot.take().expect("accumulator is refilled each step");
11092 slot = Some(host::invoke(
11093 &cb,
11094 vec![prev, v, Value::Float(i as f64), this_value.clone()],
11095 None,
11096 )?);
11097 Ok(None::<()>)
11098 })?;
11099 acc = slot.expect("accumulator is refilled each step");
11100 Ok(acc)
11101 }
11102 "findLast" => {
11103 let items = array_items(recv);
11104 let cb = arg0(&args);
11105 for i in (0..items.len()).rev() {
11106 let m = host::invoke(
11107 &cb,
11108 vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
11109 this_arg(&args, 1),
11110 )?;
11111 if with_host(|h| h.truthy(&m)) {
11112 return Ok(items[i].clone());
11113 }
11114 }
11115 Ok(Value::Undef)
11116 }
11117 "findLastIndex" => {
11118 let items = array_items(recv);
11119 let cb = arg0(&args);
11120 for i in (0..items.len()).rev() {
11121 let m = host::invoke(
11122 &cb,
11123 vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
11124 this_arg(&args, 1),
11125 )?;
11126 if with_host(|h| h.truthy(&m)) {
11127 return Ok(Value::Float(i as f64));
11128 }
11129 }
11130 Ok(Value::Float(-1.0))
11131 }
11132 // 23.1.3.30: `SortIndexedProperties` collects only the PRESENT elements,
11133 // and the holes are re-created at the tail — `[3,,1].sort()` is
11134 // `[1, 3, <1 empty item>]` with own keys `['0','1']`.
11135 "sort" => {
11136 let all = array_items(recv);
11137 let holes = absent_set(recv);
11138 let mut items: Vec<Value> = all
11139 .iter()
11140 .enumerate()
11141 .filter(|(i, _)| !holes.contains(i))
11142 .map(|(_, v)| v.clone())
11143 .collect();
11144 sort_values(&mut items, args.first())?;
11145 let present = items.len();
11146 // 23.1.3.30 steps 4-5 write back only the indices BELOW the length
11147 // captured at step 1: `Set` for each sorted element, then `Delete`
11148 // for the holes that followed them. Replacing the whole backing
11149 // vector instead discarded anything the COMPARATOR appended —
11150 // `a.sort((x, y) => { a.push(0); return x - y })` came back at its
11151 // original length with every pushed element gone.
11152 with_host(|h| {
11153 let len = all.len();
11154 if let Some(JsObj::Array(a)) = h.get_mut(recv) {
11155 if a.len() < len {
11156 a.resize(len, Value::Undef);
11157 }
11158 for (i, v) in items.into_iter().enumerate() {
11159 a[i] = v;
11160 }
11161 for slot in a[present..len].iter_mut() {
11162 *slot = Value::Undef;
11163 }
11164 }
11165 h.install_holes(recv, (present..len).collect());
11166 });
11167 Ok(this_value.clone())
11168 }
11169 // ES2023 change-by-copy: sort a fresh copy, leaving the receiver untouched.
11170 "toSorted" => {
11171 let mut items = array_items(recv);
11172 sort_values(&mut items, args.first())?;
11173 Ok(with_host(|h| h.new_array(items)))
11174 }
11175 "toReversed" => {
11176 let mut items = array_items(recv);
11177 items.reverse();
11178 Ok(with_host(|h| h.new_array(items)))
11179 }
11180 "toSpliced" => {
11181 let mut items = array_items(recv);
11182 let len = items.len();
11183 let start = {
11184 let s = arg_num(&args, 0);
11185 if s < 0.0 {
11186 ((len as f64 + s).max(0.0)) as usize
11187 } else {
11188 (s as usize).min(len)
11189 }
11190 };
11191 let delete = if args.len() >= 2 {
11192 (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11193 } else if args.is_empty() {
11194 0
11195 } else {
11196 len - start
11197 };
11198 let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11199 items.splice(start..start + delete, inserts);
11200 Ok(with_host(|h| h.new_array(items)))
11201 }
11202 "with" => {
11203 let mut items = array_items(recv);
11204 let len = items.len() as i64;
11205 let rel = arg_num(&args, 0) as i64;
11206 let idx = if rel < 0 { len + rel } else { rel };
11207 if idx < 0 || idx >= len {
11208 return Err(host::range_error(&format!("Invalid index : {rel}")));
11209 }
11210 items[idx as usize] = args.get(1).cloned().unwrap_or(Value::Undef);
11211 Ok(with_host(|h| h.new_array(items)))
11212 }
11213 "flat" => {
11214 // depth defaults to 1; `Infinity` flattens fully. ToIntegerOrInfinity:
11215 // NaN → 0, otherwise truncate toward zero (negatives act as 0).
11216 let raw = if args.is_empty() {
11217 1.0
11218 } else {
11219 arg_num(&args, 0)
11220 };
11221 let depth = if raw.is_nan() {
11222 0.0
11223 } else if raw.is_infinite() {
11224 raw
11225 } else {
11226 raw.trunc()
11227 };
11228 let mut out = Vec::new();
11229 flatten_into(recv, depth, &mut out)?;
11230 array_species_create(this_value, out)
11231 }
11232 // Live over the array (23.1.5.1): each step reads it as it is then.
11233 "keys" => Ok(array_iterator(recv, host::ArrayIterKind::Keys)),
11234 "values" | "@@iterator" => Ok(array_iterator(recv, host::ArrayIterKind::Values)),
11235 "entries" => Ok(array_iterator(recv, host::ArrayIterKind::Entries)),
11236 "splice" => array_splice(recv, args),
11237 // `Array.prototype.toString` IS `join()` with the default separator
11238 // (23.1.3.36), so it converts each element with `ToString` too — and
11239 // shares its cycle cut, which is the whole reason it must not call
11240 // `join_parts` directly: `ToString` of a nested array lands back here.
11241 "toString" => join_array(recv, ","),
11242 // An Array inherits from `Object.prototype` too, so the methods it does
11243 // not override resolve there. `[].hasOwnProperty` already read back as a
11244 // function through the property path, but CALLING it landed here and
11245 // threw `is not a function`.
11246 _ if is_object_builtin_method(name) => object_builtin_method(recv, name, args),
11247 _ => Err(host::type_error(&format!("{name} is not a function"))),
11248 }
11249}
11250
11251/// `Array.prototype.join` (23.1.3.18) and, with the default separator,
11252/// `Array.prototype.toString` (23.1.3.36) — one body so both share the cycle
11253/// cut, which is not optional here: `ToString` of an element that is itself an
11254/// array re-enters through `toString`, so guarding only `join` left
11255/// `a=[]; a.push(a); a.join('-')` recursing until the native stack aborted the
11256/// process. On node v26.7.0 that expression is `""`.
11257fn join_array(recv: &Value, sep: &str) -> Result<Value, String> {
11258 if !host::join_stack_push(recv) {
11259 return Ok(with_host(|h| h.new_str(String::new())));
11260 }
11261 // 23.1.3.18 step 6: the length is captured once, then each element is read
11262 // and STRINGIFIED before the next is read. Both halves are observable —
11263 // a getter or a `toString` that shrinks the array is seen by every later
11264 // element, which a read-all-then-convert pass misses.
11265 let parts = (|| -> Result<Vec<String>, String> {
11266 let len = array_len(recv);
11267 let mut out = Vec::with_capacity(len);
11268 for i in 0..len {
11269 let v = array_elem_live(recv, i)?;
11270 out.push(join_parts(std::slice::from_ref(&v))?.remove(0));
11271 }
11272 Ok(out)
11273 })();
11274 host::join_stack_pop();
11275 let s = parts?.join(sep);
11276 Ok(with_host(|h| h.new_str(s)))
11277}
11278
11279/// `Array.prototype.join`'s per-element conversion (23.1.3.18 step 4): a
11280/// `null`/`undefined` element contributes the empty string, every other element
11281/// is `ToString(element)` — which for an object means invoking its `toString`,
11282/// so `[{ toString() { return 'x' } }].join()` is `"x"` and not
11283/// `"[object Object]"`.
11284///
11285/// The all-primitive array — the overwhelmingly common one — is rendered under
11286/// a single host borrow; only an array actually holding an object pays for the
11287/// re-entrant per-element conversion.
11288fn join_parts(items: &[Value]) -> Result<Vec<String>, String> {
11289 let fast = with_host(|h| {
11290 items
11291 .iter()
11292 .map(|x| match x {
11293 Value::Undef => Some(String::new()),
11294 _ if h.is_null(x) => Some(String::new()),
11295 // A SYMBOL element is primitive but has no `ToString`, so it must
11296 // fall through to the fallible path and throw there:
11297 // `[Symbol()].join()` is a TypeError on node v26.7.0.
11298 _ if matches!(h.get(x), Some(JsObj::Symbol { .. })) => None,
11299 _ if host::is_primitive(h, x) => Some(h.str_of(x)),
11300 _ => None,
11301 })
11302 .collect::<Vec<_>>()
11303 });
11304 if fast.iter().all(Option::is_some) {
11305 return Ok(fast.into_iter().flatten().collect());
11306 }
11307 let mut out = Vec::with_capacity(items.len());
11308 for (x, p) in items.iter().zip(fast) {
11309 match p {
11310 Some(s) => out.push(s),
11311 None => {
11312 let s = host::to_string_value(x)?;
11313 out.push(with_host(|h| h.str_of(&s)));
11314 }
11315 }
11316 }
11317 Ok(out)
11318}
11319
11320/// In-place sort of `items` (shared by `sort` and `toSorted`). Stable merge
11321/// sort — O(n log n) comparisons — with the fallible JS comparator called from
11322/// the merge step; default order is by the string form of each element.
11323/// Propagates a comparator error.
11324///
11325/// This was an insertion sort, which is O(n²): sorting 200k numbers with a
11326/// comparator did not finish inside 120s (node v26.7.0: 70ms), and each
11327/// doubling of the input quadrupled the time — 1k/2k/4k/8k/16k measured at
11328/// 0.21/0.81/3.39/12.94/51.36s. The comparator contract is unchanged; only the
11329/// number of times it is called is.
11330pub(crate) fn sort_values(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11331 // 23.1.3.30 step 1: a comparator that is neither `undefined` nor callable is
11332 // rejected BEFORE any comparison runs. `[2,1].sort(null)` was reaching the
11333 // invoke path and reporting the generic `null is not a function`.
11334 let cmp = match cmp {
11335 Some(Value::Undef) => None,
11336 Some(v) if !with_host(|h| host::is_callable(h, v)) => {
11337 // V8 renders the offending value with `NoSideEffectsToString`, not
11338 // with `util.inspect`: a string appears bare (`: x`) rather than
11339 // quoted, and an array is `[object Array]` rather than `[ 1, 2 ]`.
11340 let shown = no_side_effects_string(v);
11341 return Err(host::type_error(&format!(
11342 "The comparison function must be either a function or undefined: {shown}"
11343 )));
11344 }
11345 other => other,
11346 };
11347 // 23.1.3.30.1 SortIndexedProperties: `undefined` is never handed to the
11348 // comparator — it sorts to the end after the defined values are ordered.
11349 // `[3,undefined,1].sort((x,y)=>x-y)` is `[1,3,undefined]` with ONE call on
11350 // node v26.7.0; the insertion sort called the comparator twice, on
11351 // `undefined`, and left `[3,undefined,1]`. Every element passed over here
11352 // is `undefined`, so swapping keeps the defined values in input order.
11353 let mut defined = 0;
11354 for i in 0..items.len() {
11355 if !matches!(items[i], Value::Undef) {
11356 items.swap(defined, i);
11357 defined += 1;
11358 }
11359 }
11360 merge_sort(&mut items[..defined], cmp)
11361}
11362
11363/// One SortCompare: `> 0` means `b` sorts before `a`. A comparator result runs
11364/// through ToNumber, so a NaN (or a comparator returning `undefined`) is not
11365/// `> 0` and the pair keeps its input order.
11366fn sort_compare(a: &Value, b: &Value, cmp: Option<&Value>) -> Result<f64, String> {
11367 match cmp {
11368 Some(cb) => {
11369 let v = host::invoke(cb, vec![a.clone(), b.clone()], None)?;
11370 Ok(with_host(|h| h.to_number(&v)))
11371 }
11372 None => {
11373 // 23.1.3.30.2 SortCompare with no comparator: compare the ToString
11374 // of each element by CODE UNIT (`utf16::cmp_units`), which differs
11375 // from Rust's `String` order off the BMP.
11376 let x = with_host(|h| h.str_of(a));
11377 let y = with_host(|h| h.str_of(b));
11378 if crate::utf16::cmp_units(&x, &y) == std::cmp::Ordering::Greater {
11379 Ok(1.0)
11380 } else {
11381 Ok(-1.0)
11382 }
11383 }
11384 }
11385}
11386
11387/// Bottom-up stable merge sort. Bottom-up rather than recursive so a large
11388/// array cannot walk the native stack the JS comparator also runs on, and the
11389/// two buffers are swapped each pass instead of copied back.
11390fn merge_sort(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11391 let n = items.len();
11392 if n < 2 {
11393 return Ok(());
11394 }
11395 let mut src = items.to_vec();
11396 let mut dst = src.clone();
11397 let mut width = 1;
11398 while width < n {
11399 let mut lo = 0;
11400 while lo < n {
11401 let mid = (lo + width).min(n);
11402 let hi = (lo + 2 * width).min(n);
11403 merge(&src[lo..mid], &src[mid..hi], &mut dst[lo..hi], cmp)?;
11404 lo = hi;
11405 }
11406 std::mem::swap(&mut src, &mut dst);
11407 width *= 2;
11408 }
11409 items.clone_from_slice(&src);
11410 Ok(())
11411}
11412
11413/// Merge two sorted runs into `out`. Ties take from `left` first, which is what
11414/// makes the sort stable — `[{k:1},{k:0},{k:1},{k:0}].sort((x,y)=>x.k-y.k)`
11415/// keeps the two `k:0` entries in input order, as node does.
11416fn merge(
11417 left: &[Value],
11418 right: &[Value],
11419 out: &mut [Value],
11420 cmp: Option<&Value>,
11421) -> Result<(), String> {
11422 let (mut i, mut j, mut k) = (0, 0, 0);
11423 while i < left.len() && j < right.len() {
11424 if sort_compare(&left[i], &right[j], cmp)? > 0.0 {
11425 out[k] = right[j].clone();
11426 j += 1;
11427 } else {
11428 out[k] = left[i].clone();
11429 i += 1;
11430 }
11431 k += 1;
11432 }
11433 for v in left[i..].iter().chain(&right[j..]) {
11434 out[k] = v.clone();
11435 k += 1;
11436 }
11437 Ok(())
11438}
11439
11440/// Recursively flatten `items` up to `depth` levels into `out`. `depth` is an
11441/// f64 so `Infinity` (full flatten) and finite counts share one path.
11442///
11443/// `flat` has NO cycle cut — unlike `join`, V8 lets it run out of stack, and
11444/// `a=[1]; a.push(a); a.flat(Infinity)` is `RangeError: Maximum call stack size
11445/// exceeded` on node v26.7.0. That is reproduced by checking the same native
11446/// stack floor the VM does, so the answer is a catchable error rather than the
11447/// `fatal runtime error: stack overflow` abort this used to produce.
11448/// `FlattenIntoArray` (23.1.3.13.1). Takes the source ARRAY rather than its
11449/// elements because each level tests `HasProperty` before recursing, so a hole
11450/// contributes nothing at any depth: `[1,,3].flat()` is the dense `[1, 3]`.
11451fn flatten_into(src: &Value, depth: f64, out: &mut Vec<Value>) -> Result<(), String> {
11452 if host::stack_exhausted() {
11453 return Err(host::stack_overflow_error());
11454 }
11455 let items = array_items(src);
11456 let holes = absent_set(src);
11457 for (i, it) in items.into_iter().enumerate() {
11458 if holes.contains(&i) {
11459 continue;
11460 }
11461 let nested = depth > 0.0 && with_host(|h| h.kind_of(&it)) == Some(ObjKind::Array);
11462 if nested {
11463 flatten_into(&it, depth - 1.0, out)?;
11464 } else {
11465 out.push(it);
11466 }
11467 }
11468 Ok(())
11469}
11470
11471fn array_splice(recv: &Value, args: Vec<Value>) -> Result<Value, String> {
11472 let len = array_len(recv);
11473 let start = {
11474 let s = arg_num(&args, 0);
11475 if s < 0.0 {
11476 ((len as f64 + s).max(0.0)) as usize
11477 } else {
11478 (s as usize).min(len)
11479 }
11480 };
11481 let delete = if args.len() >= 2 {
11482 (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11483 } else {
11484 len - start
11485 };
11486 let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11487 let inserted = inserts.len();
11488 // The receiver's holes shift by (inserted - deleted) past the cut, and the
11489 // ones inside the cut move into the RETURNED array at their offset there.
11490 let holes = hole_set(recv);
11491 let removed = with_host(|h| {
11492 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
11493 let removed: Vec<Value> = items.splice(start..start + delete, inserts).collect();
11494 removed
11495 } else {
11496 Vec::new()
11497 }
11498 });
11499 let spliced = with_host(|h| {
11500 h.install_holes(
11501 recv,
11502 holes
11503 .iter()
11504 .filter_map(|&i| {
11505 if i < start {
11506 Some(i)
11507 } else if i < start + delete {
11508 None
11509 } else {
11510 Some(i - delete + inserted)
11511 }
11512 })
11513 .collect(),
11514 );
11515 (removed, holes.clone())
11516 });
11517 // The REMOVED elements come back as an array of the receiver's species
11518 // (23.1.3.31 step 8), so a subclass gets one of its own kind.
11519 let (removed, holes) = spliced;
11520 let out = array_species_create(recv, removed)?;
11521 with_host(|h| {
11522 h.install_holes(
11523 &out,
11524 holes
11525 .iter()
11526 .filter(|&&i| i >= start && i < start + delete)
11527 .map(|&i| i - start)
11528 .collect(),
11529 );
11530 });
11531 Ok(out)
11532}
11533
11534fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
11535 let norm = |v: f64| -> usize {
11536 if v < 0.0 {
11537 ((len as f64 + v).max(0.0)) as usize
11538 } else {
11539 (v as usize).min(len)
11540 }
11541 };
11542 let lo = if args.is_empty() || matches!(args[0], Value::Undef) {
11543 0
11544 } else {
11545 norm(arg_num(args, 0))
11546 };
11547 let hi = if args.len() < 2 || matches!(args[1], Value::Undef) {
11548 len
11549 } else {
11550 norm(arg_num(args, 1))
11551 };
11552 // A start at or past the end (`'World'.slice(2, 1)`) yields the empty range,
11553 // never a reversed one: JS `slice` clamps `end` up to `start`.
11554 (lo, hi.max(lo))
11555}
11556
11557/// The argument positions each `String.prototype` method coerces with
11558/// `ToNumber` rather than `ToString`. Everything not listed is a string
11559/// position — which matters only for a SYMBOL argument, the one value both
11560/// conversions refuse, and refuse with different wording.
11561///
11562/// Measured per method and per position: `'x'.indexOf(sym)` reports the STRING
11563/// message and `'x'.indexOf('a', sym)` the NUMBER one, and `padStart` is the
11564/// pair the other way round (a length then a pad string).
11565const STRING_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11566 ("at", &[0]),
11567 ("charAt", &[0]),
11568 ("charCodeAt", &[0]),
11569 ("codePointAt", &[0]),
11570 ("endsWith", &[1]),
11571 ("includes", &[1]),
11572 ("indexOf", &[1]),
11573 ("lastIndexOf", &[1]),
11574 ("padEnd", &[0]),
11575 ("padStart", &[0]),
11576 ("repeat", &[0]),
11577 ("slice", &[0, 1]),
11578 ("split", &[1]),
11579 ("startsWith", &[1]),
11580 ("substr", &[0, 1]),
11581 ("substring", &[0, 1]),
11582];
11583
11584/// Reject a SYMBOL argument before any string method coerces it. 7.1.17 and
11585/// 7.1.4 both refuse one, so `'x'.padStart(3, sym)` is a TypeError where this
11586/// rendered `Symbol(d)` into the result — silently, which is the shape of
11587/// mistake that makes a symbol key leak into text.
11588fn reject_symbol_args(name: &str, args: &[Value]) -> Result<(), String> {
11589 let numeric = STRING_METHOD_NUMERIC_ARGS
11590 .iter()
11591 .find(|(m, _)| *m == name)
11592 .map(|(_, ps)| *ps)
11593 .unwrap_or(&[]);
11594 for (i, a) in args.iter().enumerate() {
11595 if with_host(|h| matches!(h.get(a), Some(JsObj::Symbol { .. }))) {
11596 let kind = if numeric.contains(&i) {
11597 "number"
11598 } else {
11599 "string"
11600 };
11601 return Err(host::type_error(&format!(
11602 "Cannot convert a Symbol value to a {kind}"
11603 )));
11604 }
11605 }
11606 Ok(())
11607}
11608
11609/// Coerce a string method's arguments the way 22.1.3.x does, BEFORE any arm
11610/// reads them: a numeric position through `ToNumber`, every other through
11611/// `ToString`. Both run a user `valueOf`/`toString`, and none of them ran —
11612/// `'x'.padStart({valueOf: () => 3})` produced `"x"` and
11613/// `'x'.concat({toString: () => 'y'})` produced `"x[object Object]"`.
11614///
11615/// The positions that must NOT be coerced are the ones with their own protocol:
11616/// a RegExp or a `Symbol.replace`/`split`/`match`/`search` carrier at position
11617/// 0 of the method that honours it, and a callable REPLACEMENT at position 1 of
11618/// `replace`/`replaceAll`. Each of those already has a path that handles the
11619/// value as an object, and stringifying it first would take that path away.
11620/// The argument positions each `Array.prototype` method coerces with
11621/// `ToNumber` (23.1.3.x). Everything not listed is a VALUE position and must be
11622/// left alone: `fill`'s first argument, `with`'s second and `splice`'s items
11623/// are stored as given, and `indexOf`/`includes` compare their first argument
11624/// without converting it.
11625const ARRAY_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11626 ("at", &[0]),
11627 ("copyWithin", &[0, 1, 2]),
11628 ("fill", &[1, 2]),
11629 ("flat", &[0]),
11630 ("includes", &[1]),
11631 ("indexOf", &[1]),
11632 ("lastIndexOf", &[1]),
11633 ("slice", &[0, 1]),
11634 ("splice", &[0, 1]),
11635 ("toSpliced", &[0, 1]),
11636 ("with", &[0]),
11637];
11638
11639/// The same for `Number.prototype`. `toLocaleString` takes a LOCALE, not a
11640/// number, and is deliberately absent.
11641const NUMBER_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11642 ("toExponential", &[0]),
11643 ("toFixed", &[0]),
11644 ("toPrecision", &[0]),
11645 ("toString", &[0]),
11646];
11647
11648/// Replace the listed argument positions with their `ToNumber` value, running a
11649/// user `valueOf` and propagating a throw from it. Every one of these read the
11650/// argument with an INFALLIBLE conversion that does no `ToPrimitive` at all, so
11651/// `[1,2,3].slice({valueOf: () => 1})` sliced from 0 and `(1.234).toFixed(obj)`
11652/// was a RangeError.
11653/// `ToNumber(args[i])`, running a user `valueOf` and propagating its throw.
11654fn to_number_arg(args: &[Value], i: usize) -> Result<f64, String> {
11655 let v = args.get(i).cloned().unwrap_or(Value::Undef);
11656 let p = host::to_primitive(&v, "number")?;
11657 Ok(with_host(|h| h.to_number(&p)))
11658}
11659
11660fn coerce_numeric_args(
11661 table: &[(&str, &[usize])],
11662 name: &str,
11663 mut args: Vec<Value>,
11664) -> Result<Vec<Value>, String> {
11665 let Some((_, positions)) = table.iter().find(|(m, _)| *m == name) else {
11666 return Ok(args);
11667 };
11668 for &i in *positions {
11669 let Some(a) = args.get(i) else { continue };
11670 if matches!(a, Value::Undef) {
11671 continue;
11672 }
11673 let p = host::to_primitive(a, "number")?;
11674 args[i] = Value::Float(with_host(|h| h.to_number(&p)));
11675 }
11676 Ok(args)
11677}
11678
11679/// `RegExpCreate(v, flags)` — the regexp a string method builds from a
11680/// non-RegExp argument. An empty/absent argument makes the empty pattern, which
11681/// matches at position 0.
11682fn regexp_from_arg(v: &Value, flags: &str) -> Result<Value, String> {
11683 let src = if matches!(v, Value::Undef) {
11684 String::new()
11685 } else {
11686 with_host(|h| h.str_of(v))
11687 };
11688 let fv = with_host(|h| h.new_str(flags.to_string()));
11689 let sv = with_host(|h| h.new_str(src));
11690 regexp_ctor(&[sv, fv])
11691}
11692
11693fn coerce_string_args(name: &str, args: Vec<Value>) -> Result<Vec<Value>, String> {
11694 let numeric = STRING_METHOD_NUMERIC_ARGS
11695 .iter()
11696 .find(|(m, _)| *m == name)
11697 .map(|(_, ps)| *ps)
11698 .unwrap_or(&[]);
11699 let protocol = match name {
11700 "replace" | "replaceAll" => Some("@@replace"),
11701 "split" => Some("@@split"),
11702 "match" => Some("@@match"),
11703 "matchAll" => Some("@@matchAll"),
11704 "search" => Some("@@search"),
11705 // These three do not CONSUME `Symbol.match`, they reject a value that
11706 // carries it (22.1.3.7/23/24 step 3 — `IsRegExp`). Exempting it keeps
11707 // the object intact so that check still sees one; stringifying first
11708 // turned the TypeError into an ordinary search.
11709 "startsWith" | "endsWith" | "includes" => Some("@@match"),
11710 _ => None,
11711 };
11712 let mut out = Vec::with_capacity(args.len());
11713 for (i, a) in args.into_iter().enumerate() {
11714 if matches!(a, Value::Undef) {
11715 out.push(a);
11716 continue;
11717 }
11718 if numeric.contains(&i) {
11719 let p = host::to_primitive(&a, "number")?;
11720 out.push(Value::Float(with_host(|h| h.to_number(&p))));
11721 continue;
11722 }
11723 // The IsRegExp trio tests `Symbol.match` for TRUTHINESS (7.2.8 step 2),
11724 // not for presence: an object carrying `[Symbol.match]: false` is NOT a
11725 // regexp and coerces like anything else. The consuming protocols use
11726 // `GetMethod`, which additionally requires a callable.
11727 let is_regexp_like = matches!(name, "startsWith" | "endsWith" | "includes");
11728 let carries = |p: &str| match host::protocol_lookup(&a, p) {
11729 Ok(Some(m)) => {
11730 if is_regexp_like {
11731 with_host(|h| h.truthy(&m))
11732 } else {
11733 with_host(|h| host::is_callable(h, &m))
11734 }
11735 }
11736 _ => false,
11737 };
11738 let exempt = with_host(|h| matches!(h.get(&a), Some(JsObj::RegExp(_))))
11739 || (i == 0 && protocol.is_some_and(carries))
11740 || (i == 1
11741 && matches!(name, "replace" | "replaceAll")
11742 && with_host(|h| host::is_callable(h, &a)));
11743 if exempt {
11744 out.push(a);
11745 continue;
11746 }
11747 out.push(host::to_string_value(&a)?);
11748 }
11749 Ok(out)
11750}
11751
11752fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
11753 reject_symbol_args(name, &args)?;
11754 let args = coerce_string_args(name, args)?;
11755 // Every index-bearing method below counts UTF-16 code units, so they all
11756 // work off this one decoding rather than off `s.chars()` (code points),
11757 // which agrees only on the BMP. `@@iterator` is the deliberate exception.
11758 let u = crate::utf16::Units::of(s);
11759 match name {
11760 // `for…of` / spread over a string iterates CODE POINTS, not code units:
11761 // `[..."𝒳"]` is one element in node even though `"𝒳".length` is 2. This
11762 // is the one string operation that is specified in chars, so it stays
11763 // on `s.chars()` on purpose — do not "fix" it to match the others.
11764 "@@iterator" => {
11765 let items: Vec<Value> = s.chars().map(|c| new_s(c.to_string())).collect();
11766 Ok(with_host(|h| {
11767 h.alloc(JsObj::Iter {
11768 items,
11769 idx: 0,
11770 array: None,
11771 })
11772 }))
11773 }
11774 "toUpperCase" => Ok(new_s(s.to_uppercase())),
11775 "toLowerCase" => Ok(new_s(s.to_lowercase())),
11776 // `toLocaleUpperCase`/`toLocaleLowerCase` (22.1.3.26/22.1.3.24) differ
11777 // from the plain forms only for the locale-specific mappings (Turkish
11778 // dotless i, Lithuanian accents); with no locale argument they are the
11779 // Unicode Default Case Conversion, which is exactly `to_uppercase`/
11780 // `to_lowercase`. They threw `is not a function` before, so the common
11781 // no-argument call — the only form this runtime can answer, since it
11782 // carries no ICU — failed outright rather than agreeing with node.
11783 // A locale ARGUMENT is accepted and ignored; `'I'.toLocaleLowerCase('tr')`
11784 // is `'i'` here and `'ı'` in node.
11785 // `String.prototype.toLocaleString` (22.1.3.27) is `toString` — a string
11786 // has no locale rendering. Missing it made an ARRAY of strings fail too,
11787 // since `Array.prototype.toLocaleString` invokes it per element.
11788 "toLocaleString" => Ok(new_s(s.to_string())),
11789 "toLocaleUpperCase" => Ok(new_s(s.to_uppercase())),
11790 "toLocaleLowerCase" => Ok(new_s(s.to_lowercase())),
11791 // Locale comparison (ASCII approximation of ICU collation): primary by
11792 // case-folded order, then lowercase sorts before uppercase at a tie.
11793 "localeCompare" => {
11794 let other = with_host(|h| h.str_of(&arg0(&args)));
11795 let (la, lb) = (s.to_lowercase(), other.to_lowercase());
11796 let r = match la.cmp(&lb) {
11797 std::cmp::Ordering::Less => -1.0,
11798 std::cmp::Ordering::Greater => 1.0,
11799 std::cmp::Ordering::Equal => {
11800 let mut t = 0.0;
11801 for (ca, cb) in s.chars().zip(other.chars()) {
11802 if ca != cb {
11803 t = if ca.is_lowercase() { -1.0 } else { 1.0 };
11804 break;
11805 }
11806 }
11807 t
11808 }
11809 };
11810 Ok(Value::Float(r))
11811 }
11812 // `String.prototype.normalize` (22.1.3.15) — real UAX-15 normalization.
11813 //
11814 // This used to return the receiver unchanged and only validate the FORM
11815 // argument, which made every one of the four forms a no-op: `"Å"` (NFC,
11816 // one code point) and `"Å"` (NFD, two) stayed distinct under
11817 // `.normalize()`, so the standard way to compare Unicode text for
11818 // canonical equivalence silently answered `false`, and `NFKC` never
11819 // folded a compatibility character (`"fi"` stayed one code point instead
11820 // of becoming `"fi"`). The tables come from `unicode-normalization`.
11821 "normalize" => {
11822 use unicode_normalization::UnicodeNormalization;
11823 let form = match args.first() {
11824 Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
11825 _ => "NFC".to_string(),
11826 };
11827 let out = match form.as_str() {
11828 "NFC" => s.nfc().collect::<String>(),
11829 "NFD" => s.nfd().collect::<String>(),
11830 "NFKC" => s.nfkc().collect::<String>(),
11831 "NFKD" => s.nfkd().collect::<String>(),
11832 _ => {
11833 return Err(host::range_error(
11834 "The normalization form should be one of NFC, NFD, NFKC, NFKD.",
11835 ))
11836 }
11837 };
11838 Ok(new_s(out))
11839 }
11840 // ES2024 well-formedness (22.1.3.9 / 22.1.3.29). A `String` here is a
11841 // Rust `String`, whose `char` type EXCLUDES `U+D800..=U+DFFF`, so every
11842 // value this runtime can hold is well-formed by construction and
11843 // `toWellFormed` has nothing to replace. Both answers are therefore
11844 // exact for every string that survives storage; the one case node
11845 // answers differently is a surrogate half extracted by `charAt`/`slice`,
11846 // which is already `U+FFFD` here — the documented lone-surrogate
11847 // boundary in `utf16`, not a separate gap.
11848 "isWellFormed" => Ok(Value::Bool(true)),
11849 "toWellFormed" => Ok(new_s(s.to_string())),
11850 // The JS `WhiteSpace` set, not Rust's — they differ on `U+FEFF`.
11851 "trim" => Ok(new_s(crate::utf16::js_trim(s).to_string())),
11852 "trimStart" => Ok(new_s(crate::utf16::js_trim_start(s).to_string())),
11853 "trimEnd" => Ok(new_s(crate::utf16::js_trim_end(s).to_string())),
11854 "toString" | "valueOf" => Ok(new_s(s.to_string())),
11855 "charAt" => {
11856 let at = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit_str(i));
11857 Ok(new_s(at.unwrap_or_default()))
11858 }
11859 "at" => {
11860 let n = arg_num(&args, 0);
11861 // A negative position counts back from the end; `NaN` is 0. An
11862 // infinite position is out of range in either direction.
11863 let i = if n.is_nan() {
11864 Some(0i64)
11865 } else if n.is_finite() {
11866 let i = n.trunc() as i64;
11867 Some(if i < 0 { i + u.len() as i64 } else { i })
11868 } else {
11869 None
11870 };
11871 match i
11872 .and_then(|i| usize::try_from(i).ok())
11873 .and_then(|i| u.unit_str(i))
11874 {
11875 Some(c) => Ok(new_s(c)),
11876 None => Ok(Value::Undef),
11877 }
11878 }
11879 // `charCodeAt` reports the bare code UNIT — the high surrogate of an
11880 // astral character, not the character. `codePointAt` looks ahead one
11881 // unit and reports the whole scalar when the pair is well formed. They
11882 // agree everywhere on the BMP, which is why they used to share an arm.
11883 // They also disagree OUT of range: `charCodeAt` yields `NaN` while
11884 // `codePointAt` yields `undefined` (measured on node v26.7.0).
11885 "charCodeAt" => {
11886 let unit = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit(i));
11887 Ok(Value::Float(unit.map(f64::from).unwrap_or(f64::NAN)))
11888 }
11889 "codePointAt" => match unit_pos(arg_num(&args, 0)).and_then(|i| u.code_point(i)) {
11890 Some(cp) => Ok(Value::Float(f64::from(cp))),
11891 None => Ok(Value::Undef),
11892 },
11893 // The search quartet all honor their optional position argument.
11894 // `"a&b&c".indexOf("&", 2)` must be 3, not 1 — body-parser's
11895 // parameterCount walks a query string with exactly that call.
11896 "indexOf" => {
11897 let needle = needle_units(&args);
11898 let from = clamp_pos(arg_num(&args, 1), u.len());
11899 Ok(Value::Float(
11900 search_from(u.as_slice(), needle.as_slice(), from)
11901 .map(|i| i as f64)
11902 .unwrap_or(-1.0),
11903 ))
11904 }
11905 "lastIndexOf" => {
11906 let needle = needle_units(&args);
11907 // An absent or NaN position means "search the whole string".
11908 let n = arg_num(&args, 1);
11909 let upto = if n.is_nan() {
11910 u.len()
11911 } else {
11912 clamp_pos(n, u.len())
11913 };
11914 Ok(Value::Float(
11915 search_last(u.as_slice(), needle.as_slice(), upto)
11916 .map(|i| i as f64)
11917 .unwrap_or(-1.0),
11918 ))
11919 }
11920 // 22.1.3.7/22.1.3.23/22.1.3.14 step 2: these three reject a REGEXP
11921 // argument outright, and `IsRegExp` is what decides — so an object
11922 // advertising `Symbol.match` is rejected too. None of them checked.
11923 "startsWith" | "endsWith" | "includes" if is_regexp_arg(&arg0(&args)) => {
11924 Err(host::type_error(&format!(
11925 "First argument to String.prototype.{name} must not be a regular expression"
11926 )))
11927 }
11928 "includes" => {
11929 let needle = needle_units(&args);
11930 let from = clamp_pos(arg_num(&args, 1), u.len());
11931 Ok(Value::Bool(
11932 search_from(u.as_slice(), needle.as_slice(), from).is_some(),
11933 ))
11934 }
11935 "startsWith" => {
11936 let needle = needle_units(&args);
11937 let from = clamp_pos(arg_num(&args, 1), u.len());
11938 Ok(Value::Bool(
11939 u.as_slice()[from..].starts_with(needle.as_slice()),
11940 ))
11941 }
11942 "endsWith" => {
11943 let needle = needle_units(&args);
11944 // The 2nd argument is where the string is treated as ENDING.
11945 let end = if args.len() < 2 || matches!(args[1], Value::Undef) {
11946 u.len()
11947 } else {
11948 clamp_pos(arg_num(&args, 1), u.len())
11949 };
11950 Ok(Value::Bool(
11951 u.as_slice()[..end].ends_with(needle.as_slice()),
11952 ))
11953 }
11954 "slice" => {
11955 let (lo, hi) = slice_bounds(&args, u.len());
11956 Ok(new_s(u.slice(lo, hi)))
11957 }
11958 "substring" => {
11959 let mut a = arg_num(&args, 0).max(0.0) as usize;
11960 let mut b = if args.len() < 2 || matches!(args[1], Value::Undef) {
11961 u.len()
11962 } else {
11963 (arg_num(&args, 1).max(0.0) as usize).min(u.len())
11964 };
11965 a = a.min(u.len());
11966 if a > b {
11967 std::mem::swap(&mut a, &mut b);
11968 }
11969 Ok(new_s(u.slice(a, b)))
11970 }
11971 "substr" => {
11972 // A negative start counts from the end: max(len + start, 0).
11973 let len = u.len() as i64;
11974 let mut start = arg_num(&args, 0) as i64;
11975 if start < 0 {
11976 start = (len + start).max(0);
11977 }
11978 let start = (start as usize).min(u.len());
11979 let count = if args.len() >= 2 {
11980 arg_num(&args, 1).max(0.0) as usize
11981 } else {
11982 u.len()
11983 };
11984 let end = start.saturating_add(count).min(u.len());
11985 Ok(new_s(u.slice(start, end)))
11986 }
11987 "repeat" => {
11988 let n = arg_num(&args, 0);
11989 // `RangeError`, not `TypeError`, and the count is named:
11990 // `"x".repeat(-1)` is `RangeError: Invalid count value: -1`.
11991 if n < 0.0 || !n.is_finite() {
11992 return Err(host::range_error(&format!(
11993 "Invalid count value: {}",
11994 host::fmt_number(n)
11995 )));
11996 }
11997 // The PRODUCT is what V8 bounds, so `''.repeat(2**53)` is legal (and
11998 // `''`) while `'ab'.repeat(268435445)` is not: measured on node
11999 // v26.7.0, `'ab'.repeat(268435444).length` is 536870888 and one more
12000 // is `RangeError: Invalid string length`.
12001 if n * crate::utf16::len(s) as f64 > host::MAX_STRING_LENGTH as f64 {
12002 return Err(host::invalid_string_length());
12003 }
12004 Ok(new_s(s.repeat(n as usize)))
12005 }
12006 "concat" => {
12007 let mut out = s.to_string();
12008 for a in &args {
12009 out.push_str(&with_host(|h| h.str_of(a)));
12010 }
12011 Ok(new_s(out))
12012 }
12013 "padStart" => Ok(new_s(pad(s, &args, true)?)),
12014 "padEnd" => Ok(new_s(pad(s, &args, false)?)),
12015 // Regex-taking string methods: dispatch to the regexp module when the
12016 // argument is a RegExp; otherwise keep the plain-string behavior.
12017 // 22.1.3.20 step 2.a: `replaceAll` validates the `g` flag BEFORE it
12018 // consults `Symbol.replace`, so a non-global regexp is a TypeError even
12019 // though a RegExp does define that method. Delegating first skipped the
12020 // check and silently did a single replacement.
12021 "replaceAll"
12022 if is_regexp_arg(&arg0(&args))
12023 && !with_host(
12024 |h| matches!(h.get(&arg0(&args)), Some(JsObj::RegExp(r)) if r.global),
12025 ) =>
12026 {
12027 Err(host::type_error(
12028 "String.prototype.replaceAll called with a non-global RegExp argument",
12029 ))
12030 }
12031 "match" | "matchAll" | "search" | "split" | "replace" | "replaceAll"
12032 if symbol_protocol(
12033 &arg0(&args),
12034 match name {
12035 "match" => "@@match",
12036 "matchAll" => "@@matchAll",
12037 "search" => "@@search",
12038 "split" => "@@split",
12039 _ => "@@replace",
12040 },
12041 )
12042 .is_some() =>
12043 {
12044 let sym = match name {
12045 "match" => "@@match",
12046 "matchAll" => "@@matchAll",
12047 "search" => "@@search",
12048 "split" => "@@split",
12049 _ => "@@replace",
12050 };
12051 let f = symbol_protocol(&arg0(&args), sym).expect("guard checked");
12052 let sv = with_host(|h| h.new_str(s.to_string()));
12053 let mut rest = vec![sv];
12054 rest.extend(args.iter().skip(1).cloned());
12055 host::invoke(&f, rest, Some(arg0(&args)))
12056 }
12057 // 22.1.3.13/14: a non-RegExp argument is turned INTO one
12058 // (`RegExpCreate(regexp, …)`), so `'abc'.match('b')` matches. It
12059 // answered `null` for every string argument, which reads as "no match"
12060 // — the one answer a caller cannot tell from a real failure.
12061 // `matchAll` builds its with `g`, which 22.1.3.14 requires.
12062 "match" => {
12063 let a = arg0(&args);
12064 let re = if is_regexp_arg(&a) {
12065 a
12066 } else {
12067 regexp_from_arg(&a, "")?
12068 };
12069 crate::regexp::str_match(s, &re)
12070 }
12071 "matchAll" => {
12072 let a = arg0(&args);
12073 let re = if is_regexp_arg(&a) {
12074 a
12075 } else {
12076 regexp_from_arg(&a, "g")?
12077 };
12078 crate::regexp::str_match_all(s, &re)
12079 }
12080 "search" => {
12081 if is_regexp_arg(&arg0(&args)) {
12082 crate::regexp::str_search(s, &arg0(&args))
12083 } else {
12084 // 22.1.3.17 builds a RegExp from the argument, so a
12085 // METACHARACTER matches as one: `'a.c'.search('.')` is 0, not
12086 // 1. The substring approximation this replaces agreed only for
12087 // a literal needle, and answered -1 for an absent argument
12088 // where the empty pattern matches at 0.
12089 let re = regexp_from_arg(&arg0(&args), "")?;
12090 crate::regexp::str_search(s, &re)
12091 }
12092 }
12093 "replace" => {
12094 let pat = arg0(&args);
12095 let repl = args.get(1).cloned().unwrap_or(Value::Undef);
12096 if is_regexp_arg(&pat) {
12097 crate::regexp::str_replace_regex(s, &pat, &repl, false)
12098 } else if with_host(|h| host::is_callable(h, &repl)) {
12099 Ok(new_s(replace_str_fn(
12100 s,
12101 &with_host(|h| h.str_of(&pat)),
12102 &repl,
12103 false,
12104 )?))
12105 } else {
12106 let from = with_host(|h| h.str_of(&pat));
12107 let to = with_host(|h| h.str_of(&repl));
12108 Ok(new_s(replace_str_plain(s, &from, &to, false)))
12109 }
12110 }
12111 "replaceAll" => {
12112 let pat = arg0(&args);
12113 let repl = args.get(1).cloned().unwrap_or(Value::Undef);
12114 if is_regexp_arg(&pat) {
12115 // 22.1.3.20 step 2: a non-global regexp is a TypeError here,
12116 // because `replaceAll` cannot honour "all" without `g`. This
12117 // used to replace only the first match and say nothing.
12118 let global = with_host(|h| match h.get(&pat) {
12119 Some(JsObj::RegExp(r)) => r.global,
12120 _ => true,
12121 });
12122 if !global {
12123 return Err(host::type_error(
12124 "String.prototype.replaceAll called with a non-global RegExp argument",
12125 ));
12126 }
12127 crate::regexp::str_replace_regex(s, &pat, &repl, true)
12128 } else if with_host(|h| host::is_callable(h, &repl)) {
12129 Ok(new_s(replace_str_fn(
12130 s,
12131 &with_host(|h| h.str_of(&pat)),
12132 &repl,
12133 true,
12134 )?))
12135 } else {
12136 let from = with_host(|h| h.str_of(&pat));
12137 let to = with_host(|h| h.str_of(&repl));
12138 Ok(new_s(replace_str_plain(s, &from, &to, true)))
12139 }
12140 }
12141 "split" => {
12142 if is_regexp_arg(&arg0(&args)) {
12143 let limit = args
12144 .get(1)
12145 .filter(|v| !matches!(v, Value::Undef))
12146 .map(|v| with_host(|h| h.to_number(v)) as usize);
12147 return crate::regexp::str_split_regex(s, &arg0(&args), limit);
12148 }
12149 let mut parts: Vec<Value> = if args.is_empty() || matches!(args[0], Value::Undef) {
12150 vec![new_s(s.to_string())]
12151 } else {
12152 let sep = with_host(|h| h.str_of(&args[0]));
12153 if sep.is_empty() {
12154 // `split('')` yields one element per code UNIT, so an astral
12155 // character becomes its two surrogate halves.
12156 (0..u.len())
12157 .filter_map(|i| u.unit_str(i))
12158 .map(new_s)
12159 .collect()
12160 } else {
12161 s.split(&sep as &str)
12162 .map(|p| new_s(p.to_string()))
12163 .collect()
12164 }
12165 };
12166 // Optional limit: keep at most `limit` substrings.
12167 if let Some(lim) = args.get(1).filter(|v| !matches!(v, Value::Undef)) {
12168 let n = with_host(|h| h.to_number(lim));
12169 if n.is_finite() && n >= 0.0 {
12170 parts.truncate(n as usize);
12171 }
12172 }
12173 Ok(with_host(|h| h.new_array(parts)))
12174 }
12175 _ => Err(host::type_error(&format!("{name} is not a function"))),
12176 }
12177}
12178
12179/// GetSubstitution (22.1.3.19) for a STRING search value.
12180///
12181/// `String.prototype.replace`/`replaceAll` expand the same `$` patterns whether
12182/// the pattern is a regexp or a plain string, but the string path here did a
12183/// raw `str::replace` and passed the template through verbatim — so
12184/// `'abc'.replace('b', '[$&]')` produced `a[$&]c` instead of `a[b]c`. The
12185/// regexp path has always expanded them.
12186///
12187/// A string search captures nothing, so only `$$`, `$&`, `` $` `` and `$'`
12188/// apply; `$1` and `$<name>` have no referent and stay literal, which is also
12189/// what node does.
12190fn substitute_plain(templ: &str, matched: &str, position: usize, subject: &str) -> String {
12191 let chars: Vec<char> = templ.chars().collect();
12192 let mut out = String::new();
12193 let mut i = 0;
12194 while i < chars.len() {
12195 if chars[i] == '$' && i + 1 < chars.len() {
12196 match chars[i + 1] {
12197 '$' => {
12198 out.push('$');
12199 i += 2;
12200 continue;
12201 }
12202 '&' => {
12203 out.push_str(matched);
12204 i += 2;
12205 continue;
12206 }
12207 '`' => {
12208 out.push_str(&subject[..position]);
12209 i += 2;
12210 continue;
12211 }
12212 '\'' => {
12213 out.push_str(&subject[position + matched.len()..]);
12214 i += 2;
12215 continue;
12216 }
12217 _ => {}
12218 }
12219 }
12220 out.push(chars[i]);
12221 i += 1;
12222 }
12223 out
12224}
12225
12226/// `replace`/`replaceAll` with a string pattern and a string replacement,
12227/// expanding each match's `$` patterns against its own position.
12228fn replace_str_plain(s: &str, from: &str, to: &str, all: bool) -> String {
12229 if from.is_empty() && !all {
12230 return format!("{}{s}", substitute_plain(to, "", 0, s));
12231 }
12232 let mut out = String::new();
12233 let mut rest = 0usize;
12234 while let Some(rel) = s[rest..].find(from) {
12235 let at = rest + rel;
12236 out.push_str(&s[rest..at]);
12237 out.push_str(&substitute_plain(to, from, at, s));
12238 rest = at + from.len();
12239 if !all {
12240 break;
12241 }
12242 // An empty pattern matches between every character; step one along so
12243 // the scan terminates.
12244 if from.is_empty() {
12245 if rest >= s.len() {
12246 break;
12247 }
12248 let step = s[rest..].chars().next().map(|c| c.len_utf8()).unwrap_or(1);
12249 out.push_str(&s[rest..rest + step]);
12250 rest += step;
12251 }
12252 }
12253 out.push_str(&s[rest..]);
12254 out
12255}
12256
12257fn new_s(s: String) -> Value {
12258 with_host(|h| h.new_str(s))
12259}
12260
12261/// Where a forward `indexOf`/`includes` search starts, given the optional
12262/// `fromIndex` (23.1.3.17 steps 4-6, 23.1.3.16 steps 5-7). A negative value
12263/// counts back from the end and clamps at 0; absent or `NaN` is 0. A start at
12264/// or past the end finds nothing, which callers report as `-1` / `false`.
12265pub(crate) fn search_start(n: f64, len: usize) -> usize {
12266 if n.is_nan() {
12267 return 0;
12268 }
12269 let n = n.trunc();
12270 if n >= 0.0 {
12271 if n >= len as f64 {
12272 len
12273 } else {
12274 n as usize
12275 }
12276 } else {
12277 let from_end = len as f64 + n;
12278 if from_end <= 0.0 {
12279 0
12280 } else {
12281 from_end as usize
12282 }
12283 }
12284}
12285
12286/// The INCLUSIVE index a backward `lastIndexOf` starts at (23.1.3.20 steps
12287/// 4-6), or `None` when `fromIndex` places it before the array. Absent means
12288/// the last element — which is why this takes an `Option` rather than reading
12289/// `NaN` as "absent" the way the forward form can: an explicit `NaN` is
12290/// `ToIntegerOrInfinity`'d to 0 and searches only index 0.
12291pub(crate) fn search_start_last(from: Option<f64>, len: usize) -> Option<usize> {
12292 if len == 0 {
12293 return None;
12294 }
12295 let n = match from {
12296 None => return Some(len - 1),
12297 Some(v) if v.is_nan() => 0.0,
12298 Some(v) => v.trunc(),
12299 };
12300 if n >= 0.0 {
12301 Some(if n >= len as f64 { len - 1 } else { n as usize })
12302 } else {
12303 let k = len as f64 + n;
12304 if k < 0.0 {
12305 None
12306 } else {
12307 Some(k as usize)
12308 }
12309 }
12310}
12311
12312/// `ToIntegerOrInfinity(n)` clamped into `0..=len` — the position argument of
12313/// the `String.prototype` search methods. `NaN` (an absent argument) is `0`.
12314fn clamp_pos(n: f64, len: usize) -> usize {
12315 if n.is_nan() || n <= 0.0 {
12316 0
12317 } else if n >= len as f64 {
12318 len
12319 } else {
12320 n.trunc() as usize
12321 }
12322}
12323
12324/// `ToIntegerOrInfinity(n)` as a code-unit position, or `None` when there can be
12325/// no such unit. `NaN` (an absent argument) is 0; a negative or infinite
12326/// position is out of range — `"abc".charCodeAt(-1)` is `NaN`, not `'a'`.
12327fn unit_pos(n: f64) -> Option<usize> {
12328 if n.is_nan() {
12329 Some(0)
12330 } else if n < 0.0 || !n.is_finite() {
12331 None
12332 } else {
12333 Some(n.trunc() as usize)
12334 }
12335}
12336
12337/// The search argument of `indexOf`/`includes`/`startsWith`/… as code units, so
12338/// the needle is compared in the same alphabet the haystack is indexed by.
12339fn needle_units(args: &[Value]) -> crate::utf16::Units {
12340 crate::utf16::Units::of(&with_host(|h| h.str_of(&arg0(args))))
12341}
12342
12343/// The lowest index `>= from` at which `needle` occurs in `hay`. An empty
12344/// needle matches at `from` itself, as JS specifies.
12345fn search_from(hay: &[u16], needle: &[u16], from: usize) -> Option<usize> {
12346 if needle.is_empty() {
12347 return Some(from.min(hay.len()));
12348 }
12349 if needle.len() > hay.len() {
12350 return None;
12351 }
12352 (from..=hay.len().saturating_sub(needle.len())).find(|&i| &hay[i..i + needle.len()] == needle)
12353}
12354
12355/// The highest index `<= upto` at which `needle` occurs in `hay`.
12356fn search_last(hay: &[u16], needle: &[u16], upto: usize) -> Option<usize> {
12357 if needle.is_empty() {
12358 return Some(upto.min(hay.len()));
12359 }
12360 if needle.len() > hay.len() {
12361 return None;
12362 }
12363 let last = hay.len() - needle.len();
12364 (0..=upto.min(last))
12365 .rev()
12366 .find(|&i| &hay[i..i + needle.len()] == needle)
12367}
12368
12369fn pad(s: &str, args: &[Value], start: bool) -> Result<String, String> {
12370 let target_f = arg_num(args, 0);
12371 let target = if target_f.is_finite() && target_f > 0.0 {
12372 target_f as usize
12373 } else {
12374 0
12375 };
12376 // `targetLength` and the padding both count code units: `'𝒳'.padStart(3,'-')`
12377 // is `'-𝒳'` in node, not `'--𝒳'`.
12378 let cur = crate::utf16::len(s);
12379 if cur >= target {
12380 return Ok(s.to_string());
12381 }
12382 let filler = if args.len() >= 2 {
12383 with_host(|h| h.str_of(&args[1]))
12384 } else {
12385 " ".to_string()
12386 };
12387 if filler.is_empty() {
12388 return Ok(s.to_string());
12389 }
12390 // Checked only AFTER the two short-circuits, which is the order V8 uses:
12391 // measured on node v26.7.0, `'ab'.padStart(2**40, '')` is `'ab'` while
12392 // `'ab'.padStart(536870889, 'x')` is `RangeError: Invalid string length`.
12393 if target_f > host::MAX_STRING_LENGTH as f64 {
12394 return Err(host::invalid_string_length());
12395 }
12396 let need = target - cur;
12397 let fill = crate::utf16::Units::of(&filler);
12398 // The filler repeats and is TRUNCATED to the exact unit count, which can cut
12399 // a surrogate pair — node yields a lone surrogate there, we yield U+FFFD
12400 // (see src/utf16.rs).
12401 let units: Vec<u16> = (0..need)
12402 .filter_map(|i| fill.unit(i % fill.len()))
12403 .collect();
12404 let padding = crate::utf16::to_string_lossy(&units);
12405 Ok(if start {
12406 format!("{padding}{s}")
12407 } else {
12408 format!("{s}{padding}")
12409 })
12410}
12411
12412/// V8's radix rejection, shared by `Number.prototype.toString` and
12413/// `BigInt.prototype.toString` — one string, because they are one message and
12414/// the two sites had drifted apart ("radix must be" vs V8's "radix argument
12415/// must be").
12416const RADIX_RANGE: &str = "toString() radix argument must be between 2 and 36";
12417
12418/// `BigInt.prototype` methods: `toString([radix])`, `valueOf`, `toLocaleString`.
12419fn bigint_method(b: &num_bigint::BigInt, name: &str, args: Vec<Value>) -> Result<Value, String> {
12420 match name {
12421 "toString" => {
12422 let radix = match args.first() {
12423 None | Some(Value::Undef) => 10,
12424 Some(_) => {
12425 let t = arg_num(&args, 0).trunc();
12426 if !(2.0..=36.0).contains(&t) {
12427 return Err(host::range_error(RADIX_RANGE));
12428 }
12429 t as u32
12430 }
12431 };
12432 Ok(new_s(b.to_str_radix(radix)))
12433 }
12434 // `BigInt.prototype.toLocaleString` groups thousands like the Number
12435 // one does — `(1234567n).toLocaleString()` is `1,234,567` in node, and
12436 // returning the bare digits made it the only numeric type that skipped
12437 // grouping. Same en-US-shaped output as `Number.prototype`, formatted
12438 // from the EXACT digits; `options` is honored, `locales` ignored (no
12439 // ICU here).
12440 "toLocaleString" => {
12441 let digits = b.magnitude().to_string();
12442 let neg = b.sign() == num_bigint::Sign::Minus;
12443 let opts = args.get(1).cloned().unwrap_or(Value::Undef);
12444 Ok(new_s(crate::numfmt::to_locale_string(
12445 crate::numfmt::Num::BigInt(&digits, neg),
12446 &opts,
12447 )?))
12448 }
12449 "valueOf" => Ok(with_host(|h| h.new_bigint(b.clone()))),
12450 _ => Err(host::type_error(&format!("{name} is not a function"))),
12451 }
12452}
12453
12454fn number_method(n: f64, name: &str, args: Vec<Value>) -> Result<Value, String> {
12455 let args = coerce_numeric_args(NUMBER_METHOD_NUMERIC_ARGS, name, args)?;
12456 match name {
12457 "toFixed" => {
12458 let digits = arg_num(&args, 0);
12459 if !(0.0..=100.0).contains(&digits.trunc()) {
12460 return Err(host::range_error(
12461 "toFixed() digits argument must be between 0 and 100",
12462 ));
12463 }
12464 Ok(new_s(to_fixed(n, digits as usize)))
12465 }
12466 "toExponential" => {
12467 // `undefined` (or a missing argument) selects the shortest form.
12468 let f = match args.first() {
12469 None | Some(Value::Undef) => None,
12470 Some(_) => {
12471 let d = arg_num(&args, 0).trunc();
12472 if !(0.0..=100.0).contains(&d) {
12473 return Err(host::range_error(
12474 "toExponential() argument must be between 0 and 100",
12475 ));
12476 }
12477 Some(d as usize)
12478 }
12479 };
12480 Ok(new_s(to_exponential(n, f)))
12481 }
12482 "toString" => {
12483 // An out-of-range radix THROWS; it does not silently fall back to
12484 // base 10. `(1).toString(37)` returned "1" here, so a support probe
12485 // was told every radix worked.
12486 let radix = match args.first() {
12487 None | Some(Value::Undef) => 10,
12488 Some(_) => {
12489 let r = arg_num(&args, 0);
12490 let t = r.trunc();
12491 if !(2.0..=36.0).contains(&t) {
12492 return Err(host::range_error(RADIX_RANGE));
12493 }
12494 t as u32
12495 }
12496 };
12497 if radix == 10 {
12498 Ok(new_s(host::fmt_number(n)))
12499 } else {
12500 Ok(new_s(to_radix(n, radix)))
12501 }
12502 }
12503 "toPrecision" => {
12504 // `undefined` (or a missing argument) behaves like `toString()`.
12505 match args.first() {
12506 None | Some(Value::Undef) => Ok(new_s(host::fmt_number(n))),
12507 Some(_) => {
12508 let p = arg_num(&args, 0).trunc();
12509 if !(1.0..=100.0).contains(&p) {
12510 return Err(host::range_error(
12511 "toPrecision() argument must be between 1 and 100",
12512 ));
12513 }
12514 Ok(new_s(to_precision(n, p as usize)))
12515 }
12516 }
12517 }
12518 // The `options` argument (digit options, grouping, percent/currency
12519 // style) is honored in the en-US shape; see `crate::numfmt`.
12520 "toLocaleString" => {
12521 let opts = args.get(1).cloned().unwrap_or(Value::Undef);
12522 Ok(new_s(crate::numfmt::to_locale_string(
12523 crate::numfmt::Num::Float(n),
12524 &opts,
12525 )?))
12526 }
12527 "valueOf" => Ok(Value::Float(n)),
12528 _ => Err(host::type_error(&format!("{name} is not a function"))),
12529 }
12530}
12531
12532/// `Number.prototype.toFixed(f)`: fixed-point with `f` fractional digits, rounding
12533/// half away from zero on the actual IEEE-754 value (so `(1.005).toFixed(2)` is
12534/// `"1.00"` because 1.005 is really 1.00499…). The sign of a negative input is
12535/// preserved even when the rounded magnitude is zero: `(-0.4).toFixed(0) === "-0"`.
12536///
12537/// The rounding is done on the value's EXACT decimal expansion (Rust's fixed
12538/// formatting is exact), not on `x * 10^f` — the latter loses precision for large
12539/// magnitudes (`(9.999999e20).toFixed(4)` must keep every integer digit).
12540fn to_fixed(n: f64, f: usize) -> String {
12541 if !n.is_finite() {
12542 return host::fmt_number(n);
12543 }
12544 // Spec: for |x| ≥ 10^21, toFixed falls back to ToString(x).
12545 if n.abs() >= 1e21 {
12546 return host::fmt_number(n);
12547 }
12548 let neg = n < 0.0;
12549 // Exact decimal with guard digits past the rounding position; then round the
12550 // digit string half-away-from-zero (nonneg operand ⇒ round-half-up).
12551 let full = format!("{:.*}", f + 25, n.abs());
12552 let mut body = round_decimal_string(&full, f);
12553 if neg {
12554 body.insert(0, '-'); // JS keeps the sign even for "-0" / "-0.00".
12555 }
12556 body
12557}
12558
12559/// Round the exact decimal string `s` (`"int.frac"`, nonnegative) to `f`
12560/// fractional digits, half away from zero, propagating carry across the point.
12561fn round_decimal_string(s: &str, f: usize) -> String {
12562 let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
12563 let mut digits: Vec<u8> = int_part
12564 .bytes()
12565 .chain(frac_part.bytes())
12566 .map(|b| b - b'0')
12567 .collect();
12568 let point = int_part.len(); // digits before the decimal point
12569 let keep = point + f; // number of leading digits to keep
12570
12571 // Round up if the first dropped digit is ≥ 5 (exact-half ⇒ up).
12572 if digits.get(keep).map(|&d| d >= 5).unwrap_or(false) {
12573 let mut i = keep;
12574 loop {
12575 if i == 0 {
12576 digits.insert(0, 1);
12577 // A new leading digit shifts the decimal point right by one.
12578 return assemble_decimal(&digits, point + 1, f);
12579 }
12580 i -= 1;
12581 if digits[i] == 9 {
12582 digits[i] = 0;
12583 } else {
12584 digits[i] += 1;
12585 break;
12586 }
12587 }
12588 }
12589 assemble_decimal(&digits, point, f)
12590}
12591
12592/// Reassemble `digits` into `"int.frac"` keeping `f` fractional digits, given that
12593/// `point` digits precede the decimal point.
12594fn assemble_decimal(digits: &[u8], point: usize, f: usize) -> String {
12595 let int_str: String = digits[..point].iter().map(|d| (d + b'0') as char).collect();
12596 let int_str = int_str.trim_start_matches('0');
12597 let int_str = if int_str.is_empty() { "0" } else { int_str };
12598 if f == 0 {
12599 return int_str.to_string();
12600 }
12601 let frac: String = digits[point..point + f]
12602 .iter()
12603 .map(|d| (d + b'0') as char)
12604 .collect();
12605 format!("{int_str}.{frac}")
12606}
12607
12608/// Round the nonnegative finite `a` to `p` significant decimal digits, half away
12609/// from zero, returning the `p` digits and the decimal exponent `e` such that the
12610/// value is `0.d…d × 10^(e+1)` (i.e. `d.d…d e±e`). Rust's `{:.*e}` rounds half to
12611/// EVEN (`(2.5)` at 1 digit would give "2"), but JS rounds half up ("3"), so the
12612/// exact digits are taken with guard positions and rounded here.
12613fn round_significant(a: f64, p: usize) -> (String, i32) {
12614 let sci = format!("{a:.*e}", p - 1 + 25);
12615 let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12616 let mut e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
12617 let all: Vec<u8> = mant
12618 .chars()
12619 .filter(|c| c.is_ascii_digit())
12620 .map(|c| c as u8 - b'0')
12621 .collect();
12622 let mut s: String = all[..p].iter().map(|d| (d + b'0') as char).collect();
12623 if all.get(p).map(|&d| d >= 5).unwrap_or(false) {
12624 // Round the p-digit mantissa up, propagating carry; a carry out of the
12625 // leading digit (`9.99 → 10`) bumps the decimal exponent by one.
12626 let mut d: Vec<u8> = all[..p].to_vec();
12627 let mut i = p;
12628 loop {
12629 if i == 0 {
12630 d.insert(0, 1);
12631 d.truncate(p);
12632 e += 1;
12633 break;
12634 }
12635 i -= 1;
12636 if d[i] == 9 {
12637 d[i] = 0;
12638 } else {
12639 d[i] += 1;
12640 break;
12641 }
12642 }
12643 s = d.iter().map(|x| (x + b'0') as char).collect();
12644 }
12645 (s, e)
12646}
12647
12648/// `Number.prototype.toExponential(f)`: one digit before the point and `f` after,
12649/// with a signed decimal exponent (`(100).toExponential(2) === "1.00e+2"`). With
12650/// `f` omitted, as many digits as uniquely identify the value are used
12651/// (`(123456).toExponential() === "1.23456e+5"`). Rounding is half away from zero
12652/// on the exact value, matching `toPrecision`.
12653fn to_exponential(n: f64, f: Option<usize>) -> String {
12654 if !n.is_finite() {
12655 return host::fmt_number(n);
12656 }
12657 let neg = n < 0.0;
12658 let a = n.abs();
12659 let (s, e) = if a == 0.0 {
12660 // Zero has no significant digits: emit "0" padded to the requested width.
12661 ("0".repeat(f.unwrap_or(0) + 1), 0)
12662 } else {
12663 match f {
12664 Some(f) => round_significant(a, f + 1),
12665 None => {
12666 // Shortest round-tripping digits (Rust's `{:e}` is shortest).
12667 let sci = format!("{a:e}");
12668 let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12669 let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
12670 let trimmed = digits.trim_end_matches('0');
12671 let digits = if trimmed.is_empty() { "0" } else { trimmed };
12672 (digits.to_string(), exp_str.parse().unwrap_or(0))
12673 }
12674 }
12675 };
12676 let sign = if e >= 0 { '+' } else { '-' };
12677 let mag = e.abs();
12678 let body = if s.len() == 1 {
12679 format!("{s}e{sign}{mag}")
12680 } else {
12681 format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12682 };
12683 if neg {
12684 format!("-{body}")
12685 } else {
12686 body
12687 }
12688}
12689
12690/// `Number.prototype.toPrecision(p)`: `p` significant digits, switching to
12691/// exponential form when the decimal exponent `e` satisfies `e < -6` or `e ≥ p`
12692/// (ECMAScript Number.prototype.toPrecision). Trailing zeros are significant and
12693/// retained (`(100).toPrecision(5) === "100.00"`).
12694fn to_precision(n: f64, p: usize) -> String {
12695 if !n.is_finite() {
12696 return host::fmt_number(n);
12697 }
12698 if n == 0.0 {
12699 return if p == 1 {
12700 "0".into()
12701 } else {
12702 format!("0.{}", "0".repeat(p - 1))
12703 };
12704 }
12705 let neg = n < 0.0;
12706 let (s, e) = round_significant(n.abs(), p);
12707 let pp = p as i32;
12708
12709 let body = if e < -6 || e >= pp {
12710 // Exponential: first digit, optional '.rest', signed exponent.
12711 let sign = if e >= 0 { '+' } else { '-' };
12712 let mag = e.abs();
12713 if p == 1 {
12714 format!("{s}e{sign}{mag}")
12715 } else {
12716 format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12717 }
12718 } else if e >= 0 {
12719 // e in 0..p-1: (e+1) integer digits, then any remaining as fraction.
12720 let ip = (e + 1) as usize;
12721 if ip == p {
12722 s
12723 } else {
12724 format!("{}.{}", &s[..ip], &s[ip..])
12725 }
12726 } else {
12727 // -6 ≤ e < 0: "0." then (−e−1) zeros then all p digits.
12728 format!("0.{}{}", "0".repeat((-e - 1) as usize), s)
12729 };
12730 if neg {
12731 format!("-{body}")
12732 } else {
12733 body
12734 }
12735}
12736
12737/// `Number.prototype.toString(radix)` for radix 2..=36 (radix 10 goes through
12738/// `fmt_number`). Faithful port of V8's `DoubleToRadixCString`: the integer part
12739/// is emitted exact, and fractional digits are produced up to the input double's
12740/// precision (terminating via a ULP-sized `delta`), with round-half-to-even and
12741/// carry-over back into already-written digits (and into the integer part).
12742fn to_radix(n: f64, radix: u32) -> String {
12743 if !n.is_finite() {
12744 return host::fmt_number(n);
12745 }
12746 let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
12747 let rf = radix as f64;
12748 let neg = n < 0.0;
12749 let value = n.abs();
12750
12751 let mut integer = value.floor();
12752 let mut fraction = value - integer;
12753
12754 // Fraction digits, most-significant first.
12755 let mut frac: Vec<u8> = Vec::new();
12756 // Only compute fractional digits down to the input double's precision.
12757 let mut delta = 0.5 * (next_up(value) - value);
12758 delta = delta.max(next_up(0.0));
12759 if fraction >= delta {
12760 loop {
12761 // Shift up by one digit.
12762 fraction *= rf;
12763 delta *= rf;
12764 let digit = fraction as usize;
12765 frac.push(digits[digit]);
12766 fraction -= digit as f64;
12767 // Round to even.
12768 if (fraction > 0.5 || (fraction == 0.5 && (digit & 1) == 1)) && fraction + delta > 1.0 {
12769 // Carry-over: back-trace already-written fraction digits.
12770 loop {
12771 match frac.pop() {
12772 None => {
12773 // Carried past the point into the integer part.
12774 integer += 1.0;
12775 break;
12776 }
12777 Some(c) => {
12778 let d = if c > b'9' {
12779 (c - b'a' + 10) as u32
12780 } else {
12781 (c - b'0') as u32
12782 };
12783 if d + 1 < radix {
12784 frac.push(digits[(d + 1) as usize]);
12785 break;
12786 }
12787 // digit was radix-1: drop it and keep carrying.
12788 }
12789 }
12790 }
12791 break;
12792 }
12793 if fraction < delta {
12794 break;
12795 }
12796 }
12797 }
12798
12799 // Integer digits, least-significant first (reversed at the end).
12800 let mut int_out: Vec<u8> = Vec::new();
12801 // For magnitudes ≥ 2^53, `fmod` loses low bits: pre-fill trailing zeros.
12802 while v8_exponent(integer / rf) > 0 {
12803 integer /= rf;
12804 int_out.push(b'0');
12805 }
12806 loop {
12807 let remainder = integer % rf;
12808 int_out.push(digits[remainder as usize]);
12809 integer = (integer - remainder) / rf;
12810 if integer <= 0.0 {
12811 break;
12812 }
12813 }
12814 int_out.reverse();
12815
12816 let mut out: Vec<u8> = Vec::new();
12817 if neg {
12818 out.push(b'-');
12819 }
12820 out.extend_from_slice(&int_out);
12821 if !frac.is_empty() {
12822 out.push(b'.');
12823 out.extend_from_slice(&frac);
12824 }
12825 String::from_utf8(out).unwrap()
12826}
12827
12828/// Next representable f64 above `x` (`x` finite, `x ≥ 0`) — V8's `NextDouble`.
12829fn next_up(x: f64) -> f64 {
12830 f64::from_bits(x.to_bits() + 1)
12831}
12832
12833/// V8's `Double::Exponent`: the binary exponent of the significand-scaled value
12834/// (`> 0` iff |x| ≥ 2^53). Used to detect integers past `fmod`'s exact range.
12835fn v8_exponent(x: f64) -> i32 {
12836 let biased = ((x.to_bits() >> 52) & 0x7ff) as i32;
12837 if biased == 0 {
12838 -1074 // denormal
12839 } else {
12840 biased - 1075
12841 }
12842}
12843
12844// ══ Map / Set / Symbol / generator methods ═══════════════════════════════════
12845
12846/// `Map.prototype.set` step 6 and `Set.prototype.add` step 4: a key of `-0` is
12847/// STORED as `+0`. `map_key` already treats the two as one key (SameValueZero),
12848/// but the value kept alongside it is what iteration and `console.log` report,
12849/// and node shows `0` there — `new Map().set(-0, 1)` renders `Map(1) { 0 => 1 }`.
12850fn normalize_zero_key(v: Value) -> Value {
12851 match v {
12852 Value::Float(f) if f == 0.0 && f.is_sign_negative() => Value::Float(0.0),
12853 other => other,
12854 }
12855}
12856
12857fn map_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
12858 match name {
12859 "get" => {
12860 let key = with_host(|h| host::map_key(h, &arg0(&args)));
12861 Ok(with_host(|h| match h.get(recv) {
12862 Some(JsObj::Map { entries, .. }) => entries
12863 .get(&key)
12864 .map(|(_, v)| v.clone())
12865 .unwrap_or(Value::Undef),
12866 _ => Value::Undef,
12867 }))
12868 }
12869 "set" => {
12870 let kv = normalize_zero_key(arg0(&args));
12871 let vv = args.get(1).cloned().unwrap_or(Value::Undef);
12872 reject_non_object_weak_key(recv, &kv, "WeakMap")?;
12873 let key = with_host(|h| host::map_key(h, &kv));
12874 with_host(|h| {
12875 if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12876 entries.insert(key, (kv, vv));
12877 }
12878 });
12879 Ok(recv.clone())
12880 }
12881 "has" => {
12882 let key = with_host(|h| host::map_key(h, &arg0(&args)));
12883 Ok(Value::Bool(with_host(
12884 |h| matches!(h.get(recv), Some(JsObj::Map { entries, .. }) if entries.contains_key(&key)),
12885 )))
12886 }
12887 "delete" => {
12888 let key = with_host(|h| host::map_key(h, &arg0(&args)));
12889 Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
12890 Some(JsObj::Map { entries, .. }) => entries.shift_remove(&key).is_some(),
12891 _ => false,
12892 })))
12893 }
12894 "clear" => {
12895 with_host(|h| {
12896 if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12897 entries.clear();
12898 }
12899 });
12900 Ok(Value::Undef)
12901 }
12902 "forEach" => {
12903 let cb = arg0(&args);
12904 let pairs: Vec<(Value, Value)> = with_host(|h| match h.get(recv) {
12905 Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
12906 _ => Vec::new(),
12907 });
12908 for (k, v) in pairs {
12909 host::invoke(&cb, vec![v, k, recv.clone()], this_arg(&args, 1))?;
12910 }
12911 Ok(Value::Undef)
12912 }
12913 // LIVE, not a snapshot: an entry added during iteration is visited and
12914 // one deleted before it is reached is not.
12915 "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
12916 recv,
12917 if name == "@@iterator" {
12918 "entries"
12919 } else {
12920 name
12921 },
12922 )),
12923 _ => Err(host::type_error(&format!("map.{name} is not a function"))),
12924 }
12925}
12926
12927/// A weak collection can only hold objects (and unregistered symbols) — a
12928/// primitive key is a `TypeError`, which is how packages probe for weak support.
12929fn reject_non_object_weak_key(recv: &Value, key: &Value, kind: &str) -> Result<(), String> {
12930 let weak = with_host(|h| {
12931 matches!(
12932 h.get(recv),
12933 Some(JsObj::Map { weak: true, .. }) | Some(JsObj::Set { weak: true, .. })
12934 )
12935 });
12936 if !weak {
12937 return Ok(());
12938 }
12939 let is_object = with_host(|h| match key {
12940 Value::Obj(_) => !h.is_null(key) && h.as_str(key).is_none() && h.as_bigint(key).is_none(),
12941 _ => false,
12942 });
12943 if is_object {
12944 return Ok(());
12945 }
12946 Err(host::type_error(if kind == "WeakMap" {
12947 "Invalid value used as weak map key"
12948 } else {
12949 "Invalid value used in weak set"
12950 }))
12951}
12952
12953/// A `Set`-like operand of the ES2025 set methods — 24.2.1.2 `GetSetRecord`.
12954///
12955/// The seven set operations do NOT require a real `Set` on the right-hand side:
12956/// anything with a numeric `size` and callable `has`/`keys` participates, which
12957/// is what lets a `Map`'s key view or a user-written set stand in. The reads
12958/// happen in this order (`size`, `has`, `keys`) and each failure has its own
12959/// diagnostic, so a bad operand reports which field was wrong rather than
12960/// failing later inside the iteration.
12961struct SetRecord {
12962 obj: Value,
12963 /// `size` truncated toward zero, as the spec's `intSize` is; the fractional
12964 /// part is dropped BEFORE the negative check, so `size: -0.5` truncates to
12965 /// `-0` and is accepted while `-1.5` reports `'-1' is an invalid size`.
12966 size: f64,
12967 has: Value,
12968 keys: Value,
12969}
12970
12971fn get_set_record(other: &Value, method: &str) -> Result<SetRecord, String> {
12972 if !with_host(|h| is_object_like(h, other)) {
12973 return Err(host::type_error(&format!(
12974 "Set.prototype.{method} argument must be an object"
12975 )));
12976 }
12977 let raw = get_property(other, "size")?;
12978 let num = host::to_number_value(&raw)?;
12979 if num.is_nan() {
12980 return Err(host::type_error("The .size property is NaN"));
12981 }
12982 let size = num.trunc();
12983 if size < 0.0 {
12984 return Err(host::range_error(&format!("'{size}' is an invalid size")));
12985 }
12986 let has = get_property(other, "has")?;
12987 if !with_host(|h| host::is_callable(h, &has)) {
12988 return Err(host::type_error("string \"has\" is not a function"));
12989 }
12990 let keys = get_property(other, "keys")?;
12991 if !with_host(|h| host::is_callable(h, &keys)) {
12992 return Err(host::type_error("string \"keys\" is not a function"));
12993 }
12994 Ok(SetRecord {
12995 obj: other.clone(),
12996 size,
12997 has,
12998 keys,
12999 })
13000}
13001
13002impl SetRecord {
13003 /// `Call(has, obj, [v])`, coerced to a boolean the way the spec's
13004 /// `ToBoolean(Call(...))` is — a set-like may answer with anything truthy.
13005 fn has(&self, v: &Value) -> Result<bool, String> {
13006 let r = host::invoke(&self.has, vec![v.clone()], Some(self.obj.clone()))?;
13007 Ok(with_host(|h| h.truthy(&r)))
13008 }
13009
13010 /// The operand's elements, drained from the iterator its `keys` method
13011 /// returns. A non-object result is the spec's `Result of the keys method is
13012 /// not an object`, reported before anything is iterated.
13013 fn keys(&self) -> Result<Vec<Value>, String> {
13014 let it = host::invoke(&self.keys, Vec::new(), Some(self.obj.clone()))?;
13015 if !with_host(|h| is_object_like(h, &it)) {
13016 return Err(host::type_error(
13017 "Result of the keys method is not an object",
13018 ));
13019 }
13020 host::drain_iterator(&it)
13021 }
13022}
13023
13024/// The receiver of a set operation must be a real (non-weak) `Set`: these seven
13025/// methods read `[[SetData]]` directly, so a look-alike cannot stand in on the
13026/// LEFT even though it can on the right.
13027fn require_set_receiver(recv: &Value, method: &str) -> Result<(), String> {
13028 if with_host(|h| matches!(h.get(recv), Some(JsObj::Set { weak: false, .. }))) {
13029 return Ok(());
13030 }
13031 Err(host::type_error(&format!(
13032 "Method Set.prototype.{method} called on incompatible receiver {}",
13033 with_host(|h| object_tag(h, recv))
13034 )))
13035}
13036
13037/// The receiver's elements, READ AT THE POINT THE SPEC READS THEM.
13038///
13039/// Every one of these operations copies `[[SetData]]` *after* it has touched
13040/// the operand — `union` and `symmetricDifference` call the operand's `keys`
13041/// first — so a `keys` (or a `has`) that mutates the receiver is visible in the
13042/// result. Snapshotting the receiver up front instead dropped such an element:
13043/// node's `s.union({ keys(){ s.add(99); … } })` contains `99`.
13044fn set_values(recv: &Value) -> Vec<Value> {
13045 with_host(|h| match h.get(recv) {
13046 Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13047 _ => Vec::new(),
13048 })
13049}
13050
13051fn set_size(recv: &Value) -> f64 {
13052 with_host(|h| match h.get(recv) {
13053 Some(JsObj::Set { entries, .. }) => entries.len() as f64,
13054 _ => 0.0,
13055 })
13056}
13057
13058/// A fresh, ordinary `Set`. The set operations are NOT species-aware: on node
13059/// `class S extends Set {}`, `new S([1]).union(other).constructor` is `Set`.
13060fn new_set(items: Vec<Value>) -> Result<Value, String> {
13061 let s = with_host(|h| {
13062 h.alloc(JsObj::Set {
13063 entries: IndexMap::new(),
13064 weak: false,
13065 })
13066 });
13067 for v in items {
13068 set_method(&s, "add", vec![v])?;
13069 }
13070 Ok(s)
13071}
13072
13073fn set_contains(s: &Value, v: &Value) -> bool {
13074 let key = with_host(|h| host::map_key(h, v));
13075 with_host(
13076 |h| matches!(h.get(s), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13077 )
13078}
13079
13080/// The seven ES2025 set operations (24.2.4.3, .8, .5, .16, .10, .12, .7).
13081///
13082/// Each one branches on the two sizes and iterates the SMALLER side — not an
13083/// optimization but observable behaviour: which side is walked decides the
13084/// result's order and whether the operand's `has` or its `keys` is the method
13085/// that runs. `intersection` of a 3-element receiver with a 2-element operand
13086/// yields the operand's order, and its `keys` (never its `has`) is called.
13087fn set_operation(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13088 require_set_receiver(recv, name)?;
13089 let other = get_set_record(&arg0(&args), name)?;
13090 let my_size = set_size(recv);
13091 match name {
13092 "union" => {
13093 let keys = other.keys()?;
13094 let mut out = set_values(recv);
13095 out.extend(keys);
13096 new_set(out)
13097 }
13098 "intersection" => {
13099 let mut out = Vec::new();
13100 if my_size <= other.size {
13101 for v in set_values(recv) {
13102 if other.has(&v)? {
13103 out.push(v);
13104 }
13105 }
13106 } else {
13107 for k in other.keys()? {
13108 if set_contains(recv, &k) {
13109 out.push(k);
13110 }
13111 }
13112 }
13113 new_set(out)
13114 }
13115 "difference" => {
13116 if my_size <= other.size {
13117 let mut out = Vec::new();
13118 for v in set_values(recv) {
13119 if !other.has(&v)? {
13120 out.push(v);
13121 }
13122 }
13123 return new_set(out);
13124 }
13125 let out = new_set(set_values(recv))?;
13126 for k in other.keys()? {
13127 set_method(&out, "delete", vec![k])?;
13128 }
13129 Ok(out)
13130 }
13131 "symmetricDifference" => {
13132 // The operand is drained FIRST — the spec takes the iterator before
13133 // it copies `[[SetData]]`, so a `keys` that mutates the receiver is
13134 // reflected in the result.
13135 let keys = other.keys()?;
13136 let out = new_set(set_values(recv))?;
13137 for k in keys {
13138 if set_contains(recv, &k) {
13139 set_method(&out, "delete", vec![k])?;
13140 } else {
13141 set_method(&out, "add", vec![k])?;
13142 }
13143 }
13144 Ok(out)
13145 }
13146 "isSubsetOf" => {
13147 if my_size > other.size {
13148 return Ok(Value::Bool(false));
13149 }
13150 for v in set_values(recv) {
13151 if !other.has(&v)? {
13152 return Ok(Value::Bool(false));
13153 }
13154 }
13155 Ok(Value::Bool(true))
13156 }
13157 "isSupersetOf" => {
13158 if my_size < other.size {
13159 return Ok(Value::Bool(false));
13160 }
13161 for k in other.keys()? {
13162 if !set_contains(recv, &k) {
13163 return Ok(Value::Bool(false));
13164 }
13165 }
13166 Ok(Value::Bool(true))
13167 }
13168 "isDisjointFrom" => {
13169 if my_size <= other.size {
13170 for v in set_values(recv) {
13171 if other.has(&v)? {
13172 return Ok(Value::Bool(false));
13173 }
13174 }
13175 } else {
13176 for k in other.keys()? {
13177 if set_contains(recv, &k) {
13178 return Ok(Value::Bool(false));
13179 }
13180 }
13181 }
13182 Ok(Value::Bool(true))
13183 }
13184 _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13185 }
13186}
13187
13188fn set_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13189 match name {
13190 "add" => {
13191 let vv = normalize_zero_key(arg0(&args));
13192 reject_non_object_weak_key(recv, &vv, "WeakSet")?;
13193 let key = with_host(|h| host::map_key(h, &vv));
13194 with_host(|h| {
13195 if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13196 entries.insert(key, vv);
13197 }
13198 });
13199 Ok(recv.clone())
13200 }
13201 "has" => {
13202 let key = with_host(|h| host::map_key(h, &arg0(&args)));
13203 Ok(Value::Bool(with_host(
13204 |h| matches!(h.get(recv), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13205 )))
13206 }
13207 "delete" => {
13208 let key = with_host(|h| host::map_key(h, &arg0(&args)));
13209 Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
13210 Some(JsObj::Set { entries, .. }) => entries.shift_remove(&key).is_some(),
13211 _ => false,
13212 })))
13213 }
13214 "clear" => {
13215 with_host(|h| {
13216 if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13217 entries.clear();
13218 }
13219 });
13220 Ok(Value::Undef)
13221 }
13222 "forEach" => {
13223 let cb = arg0(&args);
13224 let vals: Vec<Value> = with_host(|h| match h.get(recv) {
13225 Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13226 _ => Vec::new(),
13227 });
13228 for v in vals {
13229 host::invoke(&cb, vec![v.clone(), v, recv.clone()], this_arg(&args, 1))?;
13230 }
13231 Ok(Value::Undef)
13232 }
13233 "union"
13234 | "intersection"
13235 | "difference"
13236 | "symmetricDifference"
13237 | "isSubsetOf"
13238 | "isSupersetOf"
13239 | "isDisjointFrom" => set_operation(recv, name, args),
13240 // LIVE, as for `Map`. A Set's `keys` and `values` are the same thing.
13241 "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
13242 recv,
13243 if name == "entries" {
13244 "entries"
13245 } else {
13246 "values"
13247 },
13248 )),
13249 _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13250 }
13251}
13252
13253fn generator_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13254 // A generator IS its own iterator: both symbol forms return the receiver.
13255 if matches!(name, "@@iterator" | "@@asyncIterator") {
13256 return Ok(recv.clone());
13257 }
13258 // An `async function*` object's methods return PROMISES of the record, and
13259 // its body has to be driven through the await-aware stepper (a plain
13260 // `gen_resume` would surface an internal `await` suspension as a bogus yield).
13261 if host::is_async_generator(recv) {
13262 // All three go through `[[AsyncGeneratorQueue]]` (ECMA-262 27.6.3.6):
13263 // `.return`/`.throw` must wait behind a `.next()` that is still
13264 // suspended on an internal `await`, or that `.next()` would report
13265 // `{done: true}` for a value the body had not yet reached. An uncaught
13266 // `.throw(e)` rejects the returned promise; it does not throw here.
13267 return match name {
13268 "next" => Ok(host::async_gen_enqueue(
13269 recv,
13270 host::GenReq::Next(arg0(&args)),
13271 )),
13272 "return" => Ok(host::async_gen_enqueue(
13273 recv,
13274 host::GenReq::Return(arg0(&args)),
13275 )),
13276 "throw" => Ok(host::async_gen_enqueue(
13277 recv,
13278 host::GenReq::Throw(arg0(&args)),
13279 )),
13280 "@@asyncIterator" => Ok(recv.clone()),
13281 _ => Err(host::type_error(&format!(
13282 "asyncGenerator.{name} is not a function"
13283 ))),
13284 };
13285 }
13286 match name {
13287 "next" => {
13288 let send = arg0(&args);
13289 match host::gen_resume(recv, send)? {
13290 host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13291 host::GenStep::Done(v) => Ok(iter_result(v, true)),
13292 }
13293 }
13294 "return" => {
13295 // Resume with an injected return so any pending `finally` runs; the
13296 // completion may itself be a `finally` yield (not-done) or the value.
13297 match host::gen_return(recv, arg0(&args))? {
13298 host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13299 host::GenStep::Done(v) => Ok(iter_result(v, true)),
13300 }
13301 }
13302 "throw" => {
13303 // Inject a throw at the suspension point: an enclosing `try/catch` in
13304 // the body can handle it (and any `finally` runs); otherwise it
13305 // propagates to the caller.
13306 match host::gen_throw(recv, arg0(&args))? {
13307 host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13308 host::GenStep::Done(v) => Ok(iter_result(v, true)),
13309 }
13310 }
13311 _ => Err(host::type_error(&format!(
13312 "generator.{name} is not a function"
13313 ))),
13314 }
13315}
13316
13317/// A `{ value, done }` iterator-result object.
13318fn iter_result(value: Value, done: bool) -> Value {
13319 with_host(|h| {
13320 let mut m: IndexMap<String, Value> = IndexMap::new();
13321 m.insert("value".into(), value);
13322 m.insert("done".into(), Value::Bool(done));
13323 h.new_object(m)
13324 })
13325}
13326
13327/// A live iterator over array `arr` — what `keys()`, `values()`, `entries()`
13328/// and `Symbol.iterator` return, and what a `for-of` over an array steps.
13329pub(crate) fn array_iterator(arr: &Value, kind: host::ArrayIterKind) -> Value {
13330 with_host(|h| {
13331 h.alloc(JsObj::Iter {
13332 items: Vec::new(),
13333 idx: 0,
13334 array: Some((arr.clone(), kind)),
13335 })
13336 })
13337}
13338
13339/// One step of a `JsObj::Iter`: `None` when `it` is not one, `Some(None)` once
13340/// it is exhausted, otherwise the next value.
13341///
13342/// An array iterator reads the array at every step (23.1.5.1
13343/// `%ArrayIteratorPrototype%.next`): the length is re-read, so an element
13344/// pushed during a `for-of` is visited and one popped is not, and the element
13345/// is read as `a[i]` reads it — an accessor runs, a hole reads through the
13346/// prototype. Once it reports done it stays done, even if the array grows.
13347pub(crate) fn iter_step(it: &Value) -> Option<Option<Value>> {
13348 use host::ArrayIterKind;
13349 // One host borrow for the common case — a snapshot, or an array slot that
13350 // is neither a hole nor an accessor. `Err` carries what only `[[Get]]` can
13351 // read: the array, the kind and the index.
13352 let step = with_host(|h| {
13353 let (arr, kind, i) = match h.get_mut(it) {
13354 Some(JsObj::Iter {
13355 items,
13356 idx,
13357 array: None,
13358 }) => {
13359 let v = items.get(*idx).cloned();
13360 if v.is_some() {
13361 *idx += 1;
13362 }
13363 return Some(Ok(v));
13364 }
13365 Some(JsObj::Iter {
13366 idx,
13367 array: Some((arr, kind)),
13368 ..
13369 }) => (arr.clone(), *kind, *idx),
13370 _ => return None,
13371 };
13372 // `usize::MAX` marks an iterator that has already reported done, and
13373 // it stays done even if the array grows.
13374 let len = match h.get(&arr) {
13375 Some(JsObj::Array(items)) => items.len(),
13376 _ => 0,
13377 };
13378 let done = i == usize::MAX || i >= len;
13379 if let Some(JsObj::Iter { idx, .. }) = h.get_mut(it) {
13380 *idx = if done { usize::MAX } else { i + 1 };
13381 }
13382 if done {
13383 return Some(Ok(None));
13384 }
13385 let key = Value::Float(i as f64);
13386 let slot = match (kind, h.get(&arr)) {
13387 (ArrayIterKind::Keys, _) => return Some(Ok(Some(key))),
13388 (_, Some(JsObj::Array(items)))
13389 if !h.is_hole(&arr, i) && h.own_accessor_keys(&arr).is_empty() =>
13390 {
13391 items[i].clone()
13392 }
13393 _ => return Some(Err((arr, kind, i))),
13394 };
13395 Some(Ok(Some(match kind {
13396 ArrayIterKind::Entries => h.new_array(vec![key, slot]),
13397 _ => slot,
13398 })))
13399 })?;
13400 let (arr, kind, i) = match step {
13401 Ok(step) => return Some(step),
13402 Err(slow) => slow,
13403 };
13404 let value = get_property(&arr, &i.to_string()).unwrap_or(Value::Undef);
13405 Some(Some(match kind {
13406 ArrayIterKind::Entries => with_host(|h| h.new_array(vec![Value::Float(i as f64), value])),
13407 _ => value,
13408 }))
13409}
13410
13411/// Built-in iterator object (`arr.values()`, `arr[Symbol.iterator]()`): a
13412/// cursor over a snapshot, or live over an array ([`iter_step`]).
13413fn iter_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13414 match name {
13415 "next" => Ok(match iter_step(recv).flatten() {
13416 Some(v) => iter_result(v, false),
13417 None => iter_result(Value::Undef, true),
13418 }),
13419 "return" => {
13420 // Exhaust the cursor and report done.
13421 with_host(|h| {
13422 if let Some(JsObj::Iter { items, idx, array }) = h.get_mut(recv) {
13423 *idx = if array.is_some() {
13424 usize::MAX
13425 } else {
13426 items.len()
13427 };
13428 }
13429 });
13430 Ok(iter_result(arg0(&args), true))
13431 }
13432 // An iterator is its own iterable.
13433 "@@iterator" => Ok(recv.clone()),
13434 _ => Err(host::type_error(&format!(
13435 "iterator.{name} is not a function"
13436 ))),
13437 }
13438}
13439
13440fn symbol_method(recv: &Value, name: &str, _args: Vec<Value>) -> Result<Value, String> {
13441 match name {
13442 "toString" => Ok(with_host(|h| {
13443 let s = h.str_of(recv);
13444 h.new_str(s)
13445 })),
13446 // 20.4.3.5: `Symbol.prototype[@@toPrimitive]` returns the symbol
13447 // itself for EVERY hint — it ignores its argument. That is what makes
13448 // `sym + ''` a TypeError rather than a concatenation: the conversion
13449 // succeeds and hands back a symbol, and it is `+` that then rejects it.
13450 "@@toPrimitive" | "valueOf" => Ok(recv.clone()),
13451 _ => Err(host::type_error(&format!(
13452 "symbol.{name} is not a function"
13453 ))),
13454 }
13455}
13456
13457// ══ Object.* prototype helpers, `in`, deep clone ═════════════════════════════
13458
13459fn object_create(args: Vec<Value>) -> Result<Value, String> {
13460 let proto = arg0(&args);
13461 // 20.1.2.2 step 1: the prototype must be an Object or exactly `null`.
13462 // `undefined` is NOT accepted — measured on node v26.7.0,
13463 // `Object.create(undefined)` is
13464 // `TypeError: Object prototype may only be an Object or null: undefined`,
13465 // where node-js quietly built a normal object.
13466 reject_bad_prototype(&proto)?;
13467 let obj = with_host(|h| h.new_object(IndexMap::new()));
13468 // `set_proto` records a null proto as an explicit null-prototype object.
13469 with_host(|h| h.set_proto(&obj, proto));
13470 // Optional second arg: a property-descriptor map.
13471 if let Some(descs) = args.get(1).filter(|d| !matches!(d, Value::Undef)) {
13472 let entries: Vec<(String, Value)> = with_host(|h| match h.get(descs) {
13473 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
13474 _ => Vec::new(),
13475 });
13476 for (k, d) in entries {
13477 apply_descriptor(&obj, &k, &d)?;
13478 }
13479 }
13480 Ok(obj)
13481}
13482
13483/// The enumerable method names of a builtin `<Ctor>.prototype` namespace that
13484/// supports being copied via `mixin`/`getOwnPropertyNames`. Currently only
13485/// `EventEmitter.prototype` (the one express mixes onto its app function).
13486/// The own property names of `<Ctor>.prototype`, and whether each is
13487/// enumerable, from the generated [`crate::arity::PROTO_MEMBERS`] table.
13488///
13489/// `Object.getOwnPropertyNames(Map.prototype)` answered `[]` for every
13490/// intrinsic — the members are reachable by NAME through the `@proto:` thunks
13491/// but were not enumerable, so feature detection that walks a prototype found
13492/// nothing there. The table is read from the reference engine rather than
13493/// derived from the arity table because the arity table holds functions only:
13494/// `Map.prototype.size`, `RegExp.prototype.source` and the twelve
13495/// `URL.prototype` components are accessors.
13496fn intrinsic_proto_members(ns: &str) -> Option<&'static [&'static str]> {
13497 let ctor = ns.strip_suffix(".prototype")?;
13498 crate::arity::PROTO_MEMBERS
13499 .binary_search_by(|(k, _)| (*k).cmp(ctor))
13500 .ok()
13501 .map(|i| crate::arity::PROTO_MEMBERS[i].1)
13502}
13503
13504fn builtin_proto_method_names(ns: &str) -> Option<&'static [&'static str]> {
13505 match ns {
13506 "EventEmitter.prototype" => Some(crate::stdlib::events::METHODS),
13507 _ => None,
13508 }
13509}
13510
13511/// The own SYMBOL-keyed property keys of `v` as symbol values. A Proxy's come
13512/// from its `ownKeys` trap (the symbol half of the same list the string keys are
13513/// filtered out of); every other receiver answers from its property map.
13514fn proxy_or_own_symbol_keys(v: &Value) -> Result<Vec<Value>, String> {
13515 if let Some(keys) = crate::proxy::own_keys(v)? {
13516 return Ok(keys
13517 .iter()
13518 .filter(|k| host::is_symbol_key(k))
13519 .map(|k| crate::proxy::key_value(k))
13520 .collect());
13521 }
13522 // An intrinsic prototype's symbol-keyed members come from the generated
13523 // table, which is the only record of them: they own no map entry, so
13524 // `Object.getOwnPropertySymbols(Array.prototype)` was `[]` where node
13525 // reports `Symbol.iterator` and `Symbol.unscopables`.
13526 if let Some(ns) = intrinsic_proto_of(v).map(|c| format!("{c}.prototype")) {
13527 if let Some(members) = intrinsic_proto_members(&ns) {
13528 return Ok(with_host(|h| {
13529 members
13530 .iter()
13531 .filter_map(|m| m.strip_prefix('+').unwrap_or(m).strip_prefix("@@"))
13532 .map(|name| h.well_known_symbol(name))
13533 .collect()
13534 }));
13535 }
13536 }
13537 Ok(with_host(|h| h.own_symbol_keys(v)))
13538}
13539
13540/// `[[DefineOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13541pub fn define_property_pub(obj: &Value, key: Value, desc: Value) -> Result<Value, String> {
13542 object_define_property(vec![obj.clone(), key, desc])
13543}
13544
13545/// `[[GetOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13546pub fn own_descriptor_pub(obj: &Value, key: Value) -> Result<Value, String> {
13547 object_get_own_descriptor(vec![obj.clone(), key])
13548}
13549
13550fn object_define_property(args: Vec<Value>) -> Result<Value, String> {
13551 let obj = arg0(&args);
13552 // A Proxy defines through its `defineProperty` trap; the target it forwards
13553 // to is where the ordinary path below finally runs.
13554 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
13555 let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13556 let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13557 if !with_host(|h| is_object_like(h, &desc)) {
13558 return Err(host::type_error(&format!(
13559 "Property description must be an object: {}",
13560 with_host(|h| h.str_of(&desc))
13561 )));
13562 }
13563 // `Object.defineProperty` THROWS on a refusing trap — in sloppy code
13564 // too. `Reflect.defineProperty` is the form that reports `false`.
13565 if !crate::proxy::define_property(&obj, &key, &desc)? {
13566 return Err(host::type_error(&format!(
13567 "'defineProperty' on proxy: trap returned falsish for property '{key}'"
13568 )));
13569 }
13570 return Ok(obj);
13571 }
13572 // 20.1.2.4 steps 1-3, both of which node-js skipped entirely: a non-object
13573 // target and a non-object descriptor each throw before anything is written.
13574 if !with_host(|h| is_object_like(h, &obj)) {
13575 return Err(host::type_error(
13576 "Object.defineProperty called on non-object",
13577 ));
13578 }
13579 let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13580 if !with_host(|h| is_object_like(h, &desc)) {
13581 return Err(host::type_error(&format!(
13582 "Property description must be an object: {}",
13583 with_host(|h| h.str_of(&desc))
13584 )));
13585 }
13586 let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13587 apply_descriptor(&obj, &key, &desc)?;
13588 Ok(obj)
13589}
13590
13591/// Every `Reflect` method requires an OBJECT target and reports a `TypeError`
13592/// for anything else (28.1). A primitive was being accepted and silently
13593/// producing nothing.
13594/// `CreateListFromArrayLike` (7.3.18) — the argument list `Reflect.apply` and
13595/// `Reflect.construct` take.
13596///
13597/// An ARRAY-LIKE counts: `{length: 2, 0: 1, 1: 5}` is a two-element list. The
13598/// iterator was being used instead, so an array-like produced nothing and a
13599/// primitive produced nothing rather than the TypeError node raises.
13600/// Whether `p` is ALREADY `obj`'s prototype — the one case a non-extensible
13601/// object still accepts, because it changes nothing.
13602///
13603/// The observable prototype, not the stored link: an ordinary object has no
13604/// explicit link and inherits `Object.prototype`, so comparing the raw slot
13605/// reported "different" for `setPrototypeOf(frozen, Object.prototype)`.
13606/// Whether making `p` the prototype of `obj` would create a CYCLE — 10.1.2.1
13607/// step 8 walks up from `p` looking for `obj`.
13608///
13609/// Without the check `Object.setPrototypeOf(a, b)` followed by the reverse
13610/// built a ring. Nothing hung, because every chain walk in this host carries a
13611/// hop limit, but a lookup then silently gave up instead of finding a property
13612/// that really was there.
13613fn would_cycle(obj: &Value, p: &Value) -> bool {
13614 let mut cur = Some(p.clone());
13615 for _ in 0..1000 {
13616 let Some(c) = cur else { return false };
13617 if with_host(|h| h.strict_eq(&c, obj)) {
13618 return true;
13619 }
13620 // A PROXY's prototype is its handler's business; the spec skips the
13621 // walk entirely when one is in the chain.
13622 if with_host(|h| h.kind_of(&c)) == Some(ObjKind::Proxy) {
13623 return false;
13624 }
13625 cur = with_host(|h| h.proto_of(&c));
13626 }
13627 false
13628}
13629
13630fn same_prototype(obj: &Value, p: &Value) -> bool {
13631 let cur = prototype_of(obj);
13632 with_host(|h| h.strict_eq(&cur, p) || (h.is_null(&cur) && h.is_null(p)))
13633}
13634
13635fn create_list_from_array_like(v: &Value) -> Result<Vec<Value>, String> {
13636 if !with_host(|h| is_object_like(h, v)) {
13637 return Err(host::type_error(
13638 "CreateListFromArrayLike called on non-object",
13639 ));
13640 }
13641 let len = get_property(v, "length")?;
13642 let n = with_host(|h| h.to_number(&len));
13643 let n = if n.is_finite() && n > 0.0 {
13644 n as usize
13645 } else {
13646 0
13647 };
13648 (0..n).map(|i| get_property(v, &i.to_string())).collect()
13649}
13650
13651fn reflect_require_object(v: &Value, method: &str) -> Result<(), String> {
13652 if with_host(|h| is_object_like(h, v)) {
13653 return Ok(());
13654 }
13655 Err(host::type_error(&format!(
13656 "Reflect.{method} called on non-object"
13657 )))
13658}
13659
13660/// Whether `v` is an Object in the language sense — anything `typeof` calls
13661/// `"object"` (bar `null`) or `"function"`. Used by the argument checks that
13662/// distinguish "an object" from a primitive.
13663fn is_object_like(h: &host::JsHost, v: &Value) -> bool {
13664 matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v)
13665}
13666
13667/// `RequireObjectCoercible(v)` — 7.2.1. The check in front of every `ToObject`,
13668/// which node-js was missing on the whole `Object.keys`/`values`/`entries`/
13669/// `getOwnPropertyNames`/`getOwnPropertySymbols`/`getOwnPropertyDescriptor`/
13670/// `assign` family: each returned an empty result for `null` where node v26.7.0
13671/// throws `TypeError: Cannot convert undefined or null to object`. A PRIMITIVE
13672/// is coercible and keeps working (`Object.keys(1)` is `[]`).
13673fn require_object_coercible(v: &Value) -> Result<(), String> {
13674 if with_host(|h| matches!(v, Value::Undef) || h.is_null(v)) {
13675 return Err(host::type_error(
13676 "Cannot convert undefined or null to object",
13677 ));
13678 }
13679 Ok(())
13680}
13681
13682/// 10.1.2 / 20.1.2.2 step 1: reject a `[[Prototype]]` that is neither an Object
13683/// nor `null`, with V8's wording. Measured on node v26.7.0:
13684/// `Object.create("s")` is
13685/// `TypeError: Object prototype may only be an Object or null: s`.
13686fn reject_bad_prototype(proto: &Value) -> Result<(), String> {
13687 if with_host(|h| h.is_null(proto) || is_object_like(h, proto)) {
13688 return Ok(());
13689 }
13690 Err(host::type_error(&format!(
13691 "Object prototype may only be an Object or null: {}",
13692 with_host(|h| h.str_of(proto))
13693 )))
13694}
13695
13696/// Apply a `{ value | get | set }` descriptor object to `obj[key]`.
13697///
13698/// Per ECMAScript `ToPropertyDescriptor`, an omitted `writable`/`enumerable`/
13699/// `configurable` field defaults to **false** — which is why a `defineProperty`
13700/// data property is invisible to `Object.keys` unless the caller opts in. That
13701/// asymmetry against plain assignment is the whole reason the attribute table
13702/// exists.
13703/// The requested fields of a property descriptor — 10.1.6.2
13704/// `ToPropertyDescriptor`. Each is `None` when the descriptor omits it, which
13705/// is the distinction the merge below turns on: an omitted field LEAVES an
13706/// existing attribute alone rather than resetting it.
13707struct Requested {
13708 value: Option<Value>,
13709 get: Option<Option<Value>>,
13710 set: Option<Option<Value>>,
13711 writable: Option<bool>,
13712 enumerable: Option<bool>,
13713 configurable: Option<bool>,
13714}
13715
13716impl Requested {
13717 /// Reads through the prototype chain, as `ToPropertyDescriptor`'s
13718 /// `HasProperty`/`Get` pairs do — a descriptor built with
13719 /// `Object.create({ value: 1 })` is legal.
13720 fn read(desc: &Value) -> Self {
13721 let has = |k: &str| {
13722 with_host(|h| {
13723 host::lookup_chain(h, desc, k).is_some()
13724 || host::lookup_accessor(h, desc, k).is_some()
13725 })
13726 };
13727 let val = |k: &str| get_property(desc, k).unwrap_or(Value::Undef);
13728 // Resolve the value BEFORE the borrow: `val` re-enters the host, and
13729 // doing it inside the `with_host` closure aborts on the double borrow.
13730 let flag = |k: &str| {
13731 has(k).then(|| {
13732 let v = val(k);
13733 with_host(|h| h.truthy(&v))
13734 })
13735 };
13736 Requested {
13737 value: has("value").then(|| val("value")),
13738 get: has("get").then(|| match val("get") {
13739 Value::Undef => None,
13740 g => Some(g),
13741 }),
13742 set: has("set").then(|| match val("set") {
13743 Value::Undef => None,
13744 st => Some(st),
13745 }),
13746 writable: flag("writable"),
13747 enumerable: flag("enumerable"),
13748 configurable: flag("configurable"),
13749 }
13750 }
13751
13752 fn is_accessor(&self) -> bool {
13753 self.get.is_some() || self.set.is_some()
13754 }
13755
13756 fn is_data(&self) -> bool {
13757 self.value.is_some() || self.writable.is_some()
13758 }
13759}
13760
13761/// The own property already at `key`, if any, read back through
13762/// `Object.getOwnPropertyDescriptor` so every object kind (array indices, the
13763/// fn-prop side table, Buffer bytes) is covered by one code path.
13764struct Existing {
13765 accessor: bool,
13766 value: Value,
13767 get: Option<Value>,
13768 set: Option<Value>,
13769 writable: bool,
13770 enumerable: bool,
13771 configurable: bool,
13772}
13773
13774fn existing_property(obj: &Value, key: &str) -> Option<Existing> {
13775 let k = with_host(|h| h.new_str(key.to_string()));
13776 let d = own_descriptor_pub(obj, k).ok()?;
13777 if matches!(d, Value::Undef) {
13778 return None;
13779 }
13780 let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
13781 let truthy = |n: &str| {
13782 let v = field(n);
13783 with_host(|h| h.truthy(&v))
13784 };
13785 let accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
13786 Some(Existing {
13787 accessor,
13788 value: field("value"),
13789 get: match field("get") {
13790 Value::Undef => None,
13791 g => Some(g),
13792 },
13793 set: match field("set") {
13794 Value::Undef => None,
13795 st => Some(st),
13796 },
13797 writable: truthy("writable"),
13798 enumerable: truthy("enumerable"),
13799 configurable: truthy("configurable"),
13800 })
13801}
13802
13803/// SameValue (7.2.11) — `===` except that `NaN` equals itself and `+0` and
13804/// `-0` are distinct. 10.1.6.3 compares a redefined value against the current
13805/// one with this, not with strict equality.
13806pub(crate) fn same_value(a: &Value, b: &Value) -> bool {
13807 let num = |v: &Value| match v {
13808 Value::Int(n) => Some(*n as f64),
13809 Value::Float(f) => Some(*f),
13810 _ => None,
13811 };
13812 match (num(a), num(b)) {
13813 (Some(x), Some(y)) => {
13814 if x.is_nan() && y.is_nan() {
13815 true
13816 } else if x == 0.0 && y == 0.0 {
13817 x.is_sign_negative() == y.is_sign_negative()
13818 } else {
13819 x == y
13820 }
13821 }
13822 _ => with_host(|h| h.strict_eq(a, b)),
13823 }
13824}
13825
13826/// 10.1.6.3 `ValidateAndApplyPropertyDescriptor`.
13827///
13828/// None of the validation existed: every `Object.defineProperty` was applied
13829/// unconditionally, so redefining a non-configurable property silently
13830/// succeeded where node throws. Worse in practice, an OMITTED field was read as
13831/// `false` rather than "leave alone", so the ordinary
13832/// `Object.defineProperty(o, 'k', { enumerable: false })` also stripped
13833/// `writable` and `configurable` from a property that had both.
13834///
13835/// Converting an accessor to a data property did not take effect at all: the
13836/// value was written but the accessor stayed in its side table, and accessors
13837/// win on read, so the getter kept answering.
13838fn apply_descriptor(obj: &Value, key: &str, desc: &Value) -> Result<(), String> {
13839 let req = Requested::read(desc);
13840 let cur = existing_property(obj, key);
13841
13842 // An array's `length` is the exotic own property whose write resizes the
13843 // array (10.4.2.1); routing it through the ordinary path stored a shadowing
13844 // key and left the elements untouched.
13845 if key == "length" && with_host(|h| h.kind_of(obj)) == Some(ObjKind::Array) {
13846 if let Some(v) = req.value.clone() {
13847 return set_property_pub(obj, "length", v);
13848 }
13849 }
13850
13851 // The other exotics whose own properties are SYNTHESIZED rather than stored
13852 // in a property map: a typed array's elements and a RegExp's `lastIndex`.
13853 // The ordinary path below writes a shadowing map entry the read never
13854 // consults, so `Object.defineProperty(u8, '0', {value: 9})` left `u8[0]`
13855 // unchanged.
13856 // A builtin namespace/prototype has no property map either, so a data
13857 // descriptor has to reach the same side table an assignment does.
13858 // `Object.defineProperty(Array.prototype, 'at', {value: impl})` — how a
13859 // careful polyfill installs itself, precisely to avoid the enumerable
13860 // property a bare assignment creates — wrote a map entry nothing read.
13861 if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Builtin) {
13862 if let Some(v) = req.value.clone() {
13863 return set_property_pub(obj, key, v);
13864 }
13865 }
13866 let exotic_own = (crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray")
13867 && key.parse::<usize>().is_ok())
13868 || (key == "lastIndex" && with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))));
13869 if exotic_own {
13870 if let Some(v) = req.value.clone() {
13871 return set_property_pub(obj, key, v);
13872 }
13873 }
13874
13875 // 10.1.6.3 step 2: a NEW property cannot be added to a non-extensible
13876 // object. Only an existing property's attributes were being validated, so
13877 // `defineProperty(Object.freeze({}), 'z', …)` silently added one.
13878 if cur.is_none() && !with_host(|h| h.is_extensible(obj)) {
13879 return Err(host::type_error(&format!(
13880 "Cannot define property {key}, object is not extensible"
13881 )));
13882 }
13883 if let Some(c) = &cur {
13884 if !c.configurable {
13885 let rejected = req.configurable == Some(true)
13886 || req.enumerable.is_some_and(|e| e != c.enumerable)
13887 || (req.is_accessor() && !c.accessor)
13888 || (req.is_data() && c.accessor)
13889 || (c.accessor
13890 && ((req.get.is_some() && req.get.clone().flatten() != c.get)
13891 || (req.set.is_some() && req.set.clone().flatten() != c.set)))
13892 || (!c.accessor
13893 && !c.writable
13894 && (req.writable == Some(true)
13895 || req.value.as_ref().is_some_and(|v| !same_value(v, &c.value))));
13896 if rejected {
13897 return Err(host::type_error(&format!(
13898 "Cannot redefine property: {key}"
13899 )));
13900 }
13901 }
13902 }
13903
13904 // An omitted field keeps what the property already had; a brand-new
13905 // property defaults every one of them to false.
13906 let attrs = host::PropAttrs {
13907 writable: req
13908 .writable
13909 .unwrap_or(cur.as_ref().is_some_and(|c| c.writable)),
13910 enumerable: req
13911 .enumerable
13912 .unwrap_or(cur.as_ref().is_some_and(|c| c.enumerable)),
13913 configurable: req
13914 .configurable
13915 .unwrap_or(cur.as_ref().is_some_and(|c| c.configurable)),
13916 };
13917 with_host(|h| h.set_prop_attrs(obj, key, attrs));
13918
13919 if req.is_accessor() {
13920 let get = req
13921 .get
13922 .clone()
13923 .unwrap_or_else(|| cur.as_ref().and_then(|c| c.get.clone()));
13924 let set = req
13925 .set
13926 .clone()
13927 .unwrap_or_else(|| cur.as_ref().and_then(|c| c.set.clone()));
13928 // An ACCESSOR at an index past the end still extends the array
13929 // (10.4.2.1): `Object.defineProperty([1], '4', {get})` gives
13930 // `length === 5` with holes between. Only the DATA path grew it, so
13931 // the accessor landed in the side table while `length` stayed put —
13932 // and with it out of range, `Object.keys` and `JSON.stringify` never
13933 // saw the index at all.
13934 if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>())
13935 {
13936 with_host(|h| {
13937 let old_len = match h.get(obj) {
13938 Some(JsObj::Array(items)) => items.len(),
13939 _ => 0,
13940 };
13941 if i >= old_len {
13942 if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13943 items.resize(i + 1, Value::Undef);
13944 }
13945 h.mark_hole_range(obj, old_len..i + 1);
13946 }
13947 });
13948 }
13949 with_host(|h| h.set_accessor(obj, key, get, set));
13950 return Ok(());
13951 }
13952
13953 if let Some(c) = &cur {
13954 if c.accessor {
13955 if !req.is_data() {
13956 // A generic descriptor — flags only — leaves an accessor an
13957 // accessor. They were already applied above.
13958 return Ok(());
13959 }
13960 let v = req.value.clone().unwrap_or(Value::Undef);
13961 with_host(|h| h.accessor_to_data(obj, key, v));
13962 return Ok(());
13963 }
13964 }
13965
13966 let Some(v) = req.value else {
13967 // Nothing to write: a flags-only redefinition of a data property.
13968 return Ok(());
13969 };
13970 write_data_slot(obj, key, v);
13971 Ok(())
13972}
13973
13974/// Store `v` as an own data property, in whichever slot the object kind keeps
13975/// its own properties.
13976fn write_data_slot(obj: &Value, key: &str, v: Value) {
13977 // A function/class receiver stores its own props in the fn-prop side table
13978 // (express `mixin(app, proto)` defines methods onto the `app` *function*).
13979 if matches!(
13980 with_host(|h| h.get(obj).cloned()),
13981 Some(JsObj::Func(_)) | Some(JsObj::Class(_))
13982 ) || uses_side_table(obj)
13983 {
13984 with_host(|h| h.set_fn_prop(obj, key, v));
13985 return;
13986 }
13987 if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>()) {
13988 // An array's index keys ARE its elements, and defining one past the end
13989 // grows the array with holes in between (10.4.2.1). This whole branch
13990 // used to be missing: `Object.defineProperty(arr, 1, {value})` wrote
13991 // into the ordinary property map an array does not have, so it was a
13992 // silent no-op.
13993 with_host(|h| {
13994 let old = match h.get(obj) {
13995 Some(JsObj::Array(items)) => items.len(),
13996 _ => 0,
13997 };
13998 if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13999 if i >= old {
14000 items.resize(i + 1, Value::Undef);
14001 }
14002 items[i] = v;
14003 }
14004 if i > old {
14005 h.mark_hole_range(obj, old..i);
14006 }
14007 h.clear_hole(obj, i);
14008 });
14009 return;
14010 }
14011 with_host(|h| {
14012 if let Some(JsObj::Object(p)) = h.get_mut(obj) {
14013 p.insert(key.to_string(), v);
14014 host::canonicalize_own_keys(p);
14015 }
14016 });
14017}
14018
14019/// `Object.defineProperties(obj, descriptorMap)`.
14020fn object_define_properties(args: Vec<Value>) -> Result<Value, String> {
14021 let obj = arg0(&args);
14022 let descs = args.get(1).cloned().unwrap_or(Value::Undef);
14023 let entries: Vec<(String, Value)> = with_host(|h| match h.get(&descs) {
14024 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
14025 _ => Vec::new(),
14026 });
14027 for (k, d) in entries {
14028 apply_descriptor(&obj, &k, &d)?;
14029 }
14030 Ok(obj)
14031}
14032
14033/// The descriptor of an own property a function, a typed array or a RegExp
14034/// SYNTHESIZES rather than keeping in a property map.
14035///
14036/// These read back through the ordinary path but owned no descriptor and did
14037/// not appear under `hasOwnProperty` or `getOwnPropertyNames`, so the five
14038/// views of "does this property exist" disagreed — a read said yes while
14039/// `Object.getOwnPropertyDescriptor(f, 'name')` said no such property, which is
14040/// what a shim checks before patching.
14041fn synthesized_own_descriptor(obj: &Value, key: &str) -> Option<(Value, host::PropAttrs)> {
14042 let ro_configurable = host::PropAttrs {
14043 writable: false,
14044 enumerable: false,
14045 configurable: true,
14046 };
14047 // A callable's `length`/`name` are read-only but configurable; its
14048 // `prototype` is writable and NOT configurable, and a class's is neither.
14049 // An arrow, a method and a bound function own no `prototype` at all.
14050 if with_host(|h| host::is_callable(h, obj)) && !matches!(key, "length" | "name" | "prototype") {
14051 return None;
14052 }
14053 if with_host(|h| host::is_callable(h, obj)) {
14054 if key == "prototype" {
14055 let p = get_property(obj, "prototype").ok()?;
14056 if matches!(p, Value::Undef) {
14057 return None;
14058 }
14059 return Some((
14060 p,
14061 host::PropAttrs {
14062 writable: with_host(|h| h.kind_of(obj)) != Some(ObjKind::Class),
14063 enumerable: false,
14064 configurable: false,
14065 },
14066 ));
14067 }
14068 return Some((get_property(obj, key).ok()?, ro_configurable));
14069 }
14070 // A typed array's elements are own, enumerable, writable, configurable
14071 // properties; an index past the end owns nothing.
14072 if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") {
14073 let v = crate::stdlib::typedarray::elem_get(obj, key)?;
14074 return Some((
14075 v,
14076 host::PropAttrs {
14077 writable: true,
14078 enumerable: true,
14079 configurable: true,
14080 },
14081 ));
14082 }
14083 // A RegExp's `lastIndex` is its own, writable, non-configurable cursor.
14084 if with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))) && key == "lastIndex" {
14085 return Some((
14086 get_property(obj, "lastIndex").ok()?,
14087 host::PropAttrs {
14088 writable: true,
14089 enumerable: false,
14090 configurable: false,
14091 },
14092 ));
14093 }
14094 None
14095}
14096
14097fn object_get_own_descriptor(args: Vec<Value>) -> Result<Value, String> {
14098 let obj = arg0(&args);
14099 require_object_coercible(&obj)?;
14100 let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
14101 // A string primitive's boxed own properties: each code-unit index is an
14102 // enumerable, non-writable, non-configurable data property, and `length` is
14103 // the same minus enumerable.
14104 if let Some(units) = string_primitive_units(&obj) {
14105 let entry = match key.parse::<usize>() {
14106 Ok(i) => units
14107 .get(i)
14108 .map(|c| (with_host(|h| h.new_str(c.clone())), true)),
14109 Err(_) if key == "length" => Some((Value::Float(units.len() as f64), false)),
14110 Err(_) => None,
14111 };
14112 return Ok(match entry {
14113 Some((value, enumerable)) => with_host(|h| {
14114 let mut m: IndexMap<String, Value> = IndexMap::new();
14115 m.insert("value".into(), value);
14116 m.insert("writable".into(), Value::Bool(false));
14117 m.insert("enumerable".into(), Value::Bool(enumerable));
14118 m.insert("configurable".into(), Value::Bool(false));
14119 h.new_object(m)
14120 }),
14121 None => Value::Undef,
14122 });
14123 }
14124 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
14125 return Ok(crate::proxy::get_own_descriptor(&obj, &key)?.unwrap_or(Value::Undef));
14126 }
14127 // A method read off an enumerable builtin prototype (`EventEmitter.prototype`)
14128 // yields a `{ value: <method thunk> }` data descriptor so `mixin` can copy it.
14129 if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&obj).cloned()) {
14130 if let Some(names) = builtin_proto_method_names(&ns) {
14131 if names.contains(&key.as_str()) {
14132 return Ok(with_host(|h| {
14133 let thunk = h.alloc(JsObj::Builtin(format!(
14134 "@proto:{}:{key}",
14135 ns.trim_end_matches(".prototype")
14136 )));
14137 let mut m: IndexMap<String, Value> = IndexMap::new();
14138 m.insert("value".into(), thunk);
14139 m.insert("writable".into(), Value::Bool(true));
14140 m.insert("enumerable".into(), Value::Bool(true));
14141 m.insert("configurable".into(), Value::Bool(true));
14142 h.new_object(m)
14143 }));
14144 }
14145 }
14146 }
14147 // A global the object does not own outright is still an own property of the
14148 // global object — the same lazy binding the bare identifier resolves to.
14149 // Every one of them reported `undefined`, so a feature probe written as
14150 // `getOwnPropertyDescriptor(globalThis, 'structuredClone')` concluded the
14151 // global was absent. The immutable trio (11.1.1 / 19.1.1-3) is frozen; the
14152 // rest are ordinary writable, non-enumerable, configurable bindings.
14153 if with_host(|h| h.is_global_object(&obj)) {
14154 let owned = with_host(|h| match h.get(&obj) {
14155 Some(JsObj::Object(p)) => p.contains_key(&key),
14156 _ => false,
14157 });
14158 if !owned && !CJS_WRAPPER_LOCALS.contains(&key.as_str()) {
14159 // A global a SCRIPT created — `x = 1` with no declaration — is an
14160 // ordinary enumerable property, unlike the builtins.
14161 let script_made = with_host(|h| h.read_global(&key).is_some());
14162 if let Some(v) = global_object_binding(&key) {
14163 let frozen = matches!(key.as_str(), "undefined" | "NaN" | "Infinity");
14164 return Ok(with_host(|h| {
14165 let mut m: IndexMap<String, Value> = IndexMap::new();
14166 m.insert("value".into(), v);
14167 m.insert("writable".into(), Value::Bool(!frozen));
14168 m.insert(
14169 "enumerable".into(),
14170 Value::Bool(script_made || ENUMERABLE_GLOBALS.contains(&key.as_str())),
14171 );
14172 m.insert("configurable".into(), Value::Bool(!frozen));
14173 h.new_object(m)
14174 }));
14175 }
14176 }
14177 }
14178 // Any other member of a builtin namespace (`Math.PI`, `Math.floor`,
14179 // `Array.prototype.slice`, a builtin function's own `name`/`length`). Every
14180 // one of these reads back a value, but none owned a DESCRIPTOR:
14181 // `Object.getOwnPropertyDescriptor(Math, 'PI')` was `undefined`, which reads
14182 // as "no such property" to the shim/polyfill family that probes a namespace
14183 // before patching it.
14184 // An ACCESSOR member describes itself with a `get`, never a `value` — and
14185 // it must do so without READING the property, since running the getter
14186 // against the prototype is exactly what throws. Both prototype
14187 // representations are covered, so `Symbol.prototype.description` and
14188 // `Map.prototype.size` answer alike; both were `undefined`, which reads as
14189 // "no such property" to anything that probes before patching.
14190 if let Some(ctor) = intrinsic_proto_of(&obj) {
14191 if is_proto_accessor(&ctor, &key) {
14192 let getter = proto_getter(&ctor, &key);
14193 // The poison pair is the only ECMAScript accessor here with a
14194 // SETTER, but a WebIDL class has plenty: `URL.prototype.href`,
14195 // `hostname` and the rest are all writable, and reporting them as
14196 // read-only made `Object.getOwnPropertyDescriptor(URL.prototype,
14197 // 'href').set` read `undefined` for a setter that runs.
14198 let writable = (ctor == "Function" && matches!(key.as_str(), "arguments" | "caller"))
14199 || crate::stdlib::instance_accessors(&ctor)
14200 .0
14201 .iter()
14202 .any(|(k, settable)| *k == key && *settable);
14203 let setter = writable
14204 .then(|| with_host(|h| h.alloc(JsObj::Builtin(format!("@protoset:{ctor}:{key}")))));
14205 return Ok(with_host(|h| {
14206 let mut m: IndexMap<String, Value> = IndexMap::new();
14207 m.insert("get".into(), getter);
14208 // `undefined`, not null: a read-only accessor has no setter at
14209 // all, and `JSON.stringify` of the descriptor must drop the key
14210 // rather than report `"set": null`.
14211 m.insert("set".into(), setter.unwrap_or(Value::Undef));
14212 m.insert("enumerable".into(), Value::Bool(is_webidl_proto(&ctor)));
14213 m.insert("configurable".into(), Value::Bool(true));
14214 h.new_object(m)
14215 }));
14216 }
14217 }
14218 if let Some(ns) = with_host(|h| match h.get(&obj) {
14219 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
14220 _ => None,
14221 }) {
14222 let value = namespace_property(&ns, &key);
14223 if !matches!(value, Value::Undef) {
14224 return Ok(builtin_member_descriptor(&ns, &key, value));
14225 }
14226 }
14227 if let Some((value, attrs)) = synthesized_own_descriptor(&obj, &key) {
14228 return Ok(with_host(|h| {
14229 let mut m: IndexMap<String, Value> = IndexMap::new();
14230 m.insert("value".into(), value);
14231 m.insert("writable".into(), Value::Bool(attrs.writable));
14232 m.insert("enumerable".into(), Value::Bool(attrs.enumerable));
14233 m.insert("configurable".into(), Value::Bool(attrs.configurable));
14234 h.new_object(m)
14235 }));
14236 }
14237 // Accessor descriptor?
14238 if let Some((get, set)) = with_host(|h| h.own_accessor(&obj, &key)) {
14239 return Ok(with_host(|h| {
14240 let a = h.prop_attrs(&obj, &key);
14241 let mut m: IndexMap<String, Value> = IndexMap::new();
14242 m.insert("get".into(), get.unwrap_or(Value::Undef));
14243 m.insert("set".into(), set.unwrap_or(Value::Undef));
14244 m.insert("enumerable".into(), Value::Bool(a.enumerable));
14245 m.insert("configurable".into(), Value::Bool(a.configurable));
14246 h.new_object(m)
14247 }));
14248 }
14249 let val = with_host(|h| match h.get(&obj) {
14250 // A Buffer's own properties are exactly its byte indices, read out of the
14251 // hidden `@@bytes` slot; `length`/`byteLength` are internal bookkeeping
14252 // that V8 keeps on the prototype, so they own no descriptor.
14253 Some(JsObj::Object(p))
14254 if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("Buffer") =>
14255 {
14256 match (
14257 p.get("@@bytes").and_then(|b| h.get(b)),
14258 key.parse::<usize>(),
14259 ) {
14260 (Some(JsObj::Array(items)), Ok(i)) => items.get(i).cloned(),
14261 _ => None,
14262 }
14263 }
14264 Some(JsObj::Object(p)) => p.get(&key).cloned(),
14265 // An array's index keys read the elements; `length` is the exotic own
14266 // property; anything else is an ordinary own key in the side table.
14267 Some(JsObj::Array(items)) => match key.parse::<usize>() {
14268 // An ELIDED index owns no property at all, so it has no descriptor.
14269 Ok(i) if h.is_hole(&obj, i) => None,
14270 Ok(i) => items.get(i).cloned(),
14271 Err(_) if key == "length" => Some(Value::Float(items.len() as f64)),
14272 Err(_) => h.fn_prop(&obj, &key),
14273 },
14274 // A function/class own prop lives in the fn-prop side table.
14275 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(&obj, &key),
14276 _ => None,
14277 });
14278 match val {
14279 Some(v) => Ok(with_host(|h| {
14280 let a = h.prop_attrs(&obj, &key);
14281 let mut m: IndexMap<String, Value> = IndexMap::new();
14282 m.insert("value".into(), v);
14283 m.insert("writable".into(), Value::Bool(a.writable));
14284 m.insert("enumerable".into(), Value::Bool(a.enumerable));
14285 m.insert("configurable".into(), Value::Bool(a.configurable));
14286 h.new_object(m)
14287 })),
14288 None => Ok(Value::Undef),
14289 }
14290}
14291
14292/// `Object.getOwnPropertyDescriptors(obj)` — the descriptor of every own string
14293/// key, keyed by name. `Object.create(proto, getOwnPropertyDescriptors(src))` is
14294/// the standard "clone with accessors intact" idiom, so this must agree
14295/// key-for-key with `getOwnPropertyNames`.
14296fn object_get_own_descriptors(args: Vec<Value>) -> Result<Value, String> {
14297 let obj = arg0(&args);
14298 let names = object_keys(vec![obj.clone()], 3)?;
14299 let keys: Vec<String> = with_host(|h| match h.get(&names) {
14300 Some(JsObj::Array(items)) => items.iter().map(|k| h.str_of(k)).collect(),
14301 _ => Vec::new(),
14302 });
14303 let mut out: IndexMap<String, Value> = IndexMap::new();
14304 for k in keys {
14305 let ks = with_host(|h| h.new_str(k.clone()));
14306 let d = object_get_own_descriptor(vec![obj.clone(), ks])?;
14307 if !matches!(d, Value::Undef) {
14308 out.insert(k, d);
14309 }
14310 }
14311 Ok(with_host(|h| h.new_object(out)))
14312}
14313
14314/// `key in obj` respecting the prototype chain. Reports a `Result` because a
14315/// Proxy's `has` trap is user code and may throw.
14316pub fn has_property(obj: &Value, key: &str) -> Result<bool, String> {
14317 if let Some(b) = crate::proxy::has(obj, key)? {
14318 return Ok(b);
14319 }
14320 Ok(has_property_ordinary(obj, key))
14321}
14322
14323/// `[[HasProperty]]` for every non-Proxy receiver.
14324fn has_property_ordinary(obj: &Value, key: &str) -> bool {
14325 // `key in globalThis`: membership matches what the READ answers, which for
14326 // the global object includes every lazily-bound builtin and every global a
14327 // script created. `'Math' in globalThis` and `'x' in globalThis` after
14328 // `x = 1` both answered FALSE while `globalThis.Math` and `globalThis.x`
14329 // read back fine.
14330 if with_host(|h| h.is_global_object(obj))
14331 && !CJS_WRAPPER_LOCALS.contains(&key)
14332 && global_object_binding(key).is_some()
14333 {
14334 return true;
14335 }
14336 // `key in <builtin namespace/prototype>`: membership matches what a property
14337 // read would yield. `String.prototype.indexOf` (and the rest of the builtin
14338 // prototype methods) resolve as callable thunks via `namespace_property`, so
14339 // `'indexOf' in String.prototype` must report true (get-intrinsic probes this
14340 // with the `in` operator before reading the intrinsic).
14341 if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(obj).cloned()) {
14342 return !matches!(namespace_property(&ns, key), Value::Undef);
14343 }
14344 // An integer index of a typed array / Buffer is an own property, and lives
14345 // in the hidden element array rather than the property map — the same
14346 // question `hasOwnProperty` answers, through the same helper. Only a hit
14347 // short-circuits: a non-index key like `'length'` must still fall through
14348 // to the ordinary chain lookup below.
14349 if crate::stdlib::typedarray::has_index(obj, key) == Some(true) {
14350 return true;
14351 }
14352 if with_host(|h| host::lookup_chain(h, obj, key)).is_some() {
14353 return true;
14354 }
14355 if with_host(|h| host::lookup_accessor(h, obj, key)).is_some() {
14356 return true;
14357 }
14358 // A member patched onto the receiver's intrinsic prototype. The READ
14359 // resolves it, so without this `Array.prototype.at = f` made `[].at` a
14360 // function while `'at' in []` stayed false.
14361 if !key.starts_with('#') && inherited_builtin_static(obj, key).is_some() {
14362 return true;
14363 }
14364 if with_host(|h| match h.get(obj) {
14365 Some(JsObj::Object(p)) => p.contains_key(key),
14366 Some(JsObj::Array(items)) => {
14367 key == "length"
14368 || key
14369 .parse::<usize>()
14370 .map(|i| i < items.len() && !h.is_hole(obj, i))
14371 .unwrap_or(false)
14372 // A non-index own property (`arr.foo`, `arr[sym]`) lives in the
14373 // side table, and `in` must see it.
14374 || h.fn_prop(obj, key).is_some()
14375 }
14376 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(obj, key).is_some(),
14377 // A RegExp's `lastIndex` is an OWN property in node. Here it lives in
14378 // the `RegExpObj` struct rather than a property map, so nothing above
14379 // can see it.
14380 Some(JsObj::RegExp(_)) => key == "lastIndex" || h.fn_prop(obj, key).is_some(),
14381 _ => false,
14382 }) {
14383 return true;
14384 }
14385 // An INHERITED builtin prototype method. These are not objects on the
14386 // prototype chain — they are synthesized by the read path from the
14387 // intrinsic table — so neither `lookup_chain` nor the property map above
14388 // can see them, and `'toString' in {}`, `'push' in []` and `'then' in
14389 // Promise.resolve()` all answered false. That last one is the standard
14390 // thenable test, so the `in` operator disagreed with what a read gives for
14391 // every builtin method of every builtin kind.
14392 inherited_builtin_method(obj, key)
14393}
14394
14395/// Whether a READ of `key` on `obj` would resolve to an inherited builtin
14396/// prototype method. Asked by `in` and `hasOwnProperty`'s negative case; it
14397/// performs no read, so a getter cannot fire.
14398/// A property a script MONKEY-PATCHED onto the intrinsic prototype `obj`
14399/// inherits from (`Array.prototype.at = impl`, `Object.prototype.foo = 1`), or
14400/// `None`.
14401///
14402/// The intrinsic prototypes are namespace handles rather than real objects on
14403/// the chain, so an assignment onto one lands in `builtin_statics` and no
14404/// ordinary chain walk can see it. This is the read side: the receiver's own
14405/// constructor's prototype first, then `Object.prototype`, mirroring
14406/// `inherited_method_owner`'s two-step.
14407pub(crate) fn inherited_builtin_static(obj: &Value, key: &str) -> Option<Value> {
14408 if with_host(|h| h.has_null_proto(obj)) {
14409 return None;
14410 }
14411 let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14412 Some(c) => Some(c),
14413 None if is_arguments(obj) => Some("Object"),
14414 None => with_host(|h| default_ctor_name(h, obj)),
14415 };
14416 // Only the side table is consulted, never the real prototype OBJECT's map:
14417 // `String.prototype` and friends are materialized with their intrinsic
14418 // members present, so reading their maps here would re-route every ordinary
14419 // `"a".toString()` through this path — which recursed until the stack blew.
14420 // `set_property` mirrors a write onto a real intrinsic prototype INTO this
14421 // table precisely so the read side can stay this narrow.
14422 let on = |c: &str| with_host(|h| h.builtin_static(&format!("{c}.prototype"), key));
14423 let found = ctor.and_then(on).or_else(|| on("Object"))?;
14424 // Restoring a saved intrinsic (`const orig = Array.prototype.join; …;
14425 // Array.prototype.join = orig`) stores the SYNTHESIZED thunk for this very
14426 // name back into the table. Dispatching to it would re-enter this lookup
14427 // and recurse until the stack blew, so a thunk that is already this key's
14428 // own intrinsic reports nothing and the ordinary builtin path answers.
14429 let self_thunk = with_host(
14430 |h| matches!(h.get(&found), Some(JsObj::Builtin(s)) if s.starts_with("@proto:") && s.ends_with(&format!(":{key}"))),
14431 );
14432 (!self_thunk).then_some(found)
14433}
14434
14435/// Whether `recv` carries `key` as an OWN property — the guard on
14436/// [`inherited_builtin_static`], since an own property shadows anything
14437/// patched onto a prototype.
14438fn has_own_for_shadow(recv: &Value, key: &str) -> bool {
14439 with_host(|h| {
14440 if h.fn_prop(recv, key).is_some() || h.own_accessor(recv, key).is_some() {
14441 return true;
14442 }
14443 match h.get(recv) {
14444 Some(JsObj::Object(p)) => p.contains_key(key),
14445 // An ELIDED index owns nothing — the whole point of a hole is that
14446 // the lookup continues up the chain — so it must not count as a
14447 // shadow here or an inherited value at that index stays invisible.
14448 Some(JsObj::Array(items)) => {
14449 key == "length" || {
14450 key.parse::<usize>()
14451 .is_ok_and(|i| i < items.len() && !h.is_hole(recv, i))
14452 }
14453 }
14454 _ => false,
14455 }
14456 })
14457}
14458
14459fn inherited_builtin_method(obj: &Value, key: &str) -> bool {
14460 if with_host(|h| h.has_null_proto(obj)) {
14461 return false;
14462 }
14463 if let Some(tag) = crate::stdlib::native_tag(obj) {
14464 if crate::stdlib::instance_has_method(&tag, key) {
14465 return true;
14466 }
14467 }
14468 inherited_method_owner(obj, key).is_some()
14469}
14470
14471/// Whether `recv`'s intrinsic prototype is still on its chain — that is,
14472/// whether `Array.prototype`'s methods are still reachable from an array.
14473///
14474/// A builtin's methods are synthesized from the receiver's KIND rather than
14475/// found on a chain, so replacing the prototype could not take them away:
14476/// `Object.setPrototypeOf(a, {})` left `a.join` a function where node reports
14477/// `undefined`, and `Object.setPrototypeOf(a, null)` did too. The exotic
14478/// storage is unaffected either way — `Array.isArray`, `a.length` and `a[0]`
14479/// all still answer, as they do in node.
14480///
14481/// The overwhelmingly common case is the DEFAULT link, which is recorded as no
14482/// link at all, so this answers true after one map probe and allocates nothing.
14483pub(crate) fn own_intrinsic_reachable_pub(recv: &Value) -> bool {
14484 own_intrinsic_reachable(recv)
14485}
14486
14487fn own_intrinsic_reachable(recv: &Value) -> bool {
14488 // A BOXED primitive needs no special case here: its methods resolve through
14489 // `inherited_method_owner`, which applies the wrapper rule itself.
14490 with_host(|h| default_ctor_name(h, recv)).map_or(true, |c| intrinsic_reachable(recv, c))
14491}
14492
14493/// Whether the intrinsic prototype for `ctor` is still on `recv`'s chain.
14494fn intrinsic_reachable(recv: &Value, ctor: &str) -> bool {
14495 let own = Some(ctor);
14496 let mut cur = recv.clone();
14497 for _ in 0..100 {
14498 let explicit = with_host(|h| h.proto_of(&cur));
14499 let Some(p) = explicit else {
14500 // No explicit link: the implicit prototype is this object's own
14501 // kind's, which is what `recv` is asking about only while `cur` is
14502 // still `recv` itself.
14503 if with_host(|h| h.has_null_proto(&cur)) {
14504 return false;
14505 }
14506 let implicit = with_host(|h| default_ctor_name(h, &cur));
14507 // Every implicit prototype chain ends at `Object.prototype`, so a
14508 // question about `Object` is answered yes by any of them.
14509 return implicit == own || ctor == "Object";
14510 };
14511 if with_host(|h| h.is_null(&p)) {
14512 return false;
14513 }
14514 let hit = with_host(|h| {
14515 own.is_some_and(|c| {
14516 matches!(h.get(&p), Some(JsObj::Builtin(ns)) if *ns == format!("{c}.prototype"))
14517 || h.intrinsic_proto_ctor(&p) == Some(c)
14518 || (c == "Object" && h.object_proto() == p)
14519 })
14520 });
14521 if hit {
14522 return true;
14523 }
14524 // A CLASS prototype object is not linked to the builtin its class
14525 // extends — the `extends` relationship is recorded on the class value —
14526 // so the walk has to cross over there or `class D extends Array {}` ends
14527 // it, and every inherited method of every subclass instance vanishes.
14528 if let Some(builtin) = with_host(|h| {
14529 h.class_owning_proto(&p)
14530 .and_then(|c| h.class_builtin_ancestor(&c))
14531 .map(|b| h.callable_name(&b))
14532 }) {
14533 if own == Some(builtin.as_str()) || ctor == "Object" {
14534 return true;
14535 }
14536 }
14537 cur = p;
14538 }
14539 false
14540}
14541
14542/// The intrinsic prototypes actually ON `recv`'s explicit chain, nearest first
14543/// — the complement of [`intrinsic_reachable`], which asks about one known
14544/// constructor.
14545///
14546/// `Object.create(Array.prototype)` is an ordinary object whose chain reaches
14547/// `Array.prototype`, and node resolves the whole of `Array.prototype` through
14548/// it: `o.push(1)` works, because those methods are generic over their receiver
14549/// (which is also why `Array.prototype.push.call({length: 0}, 1)` already
14550/// worked here). Deciding the owner from the receiver's KIND alone made every
14551/// one of them `undefined` — the same "methods come from the kind, not the
14552/// chain" mistake as the detachment case, in the opposite direction.
14553pub(crate) fn chain_intrinsic_ctors_pub(recv: &Value) -> Vec<&'static str> {
14554 chain_intrinsic_ctors(recv)
14555}
14556
14557fn chain_intrinsic_ctors(recv: &Value) -> Vec<&'static str> {
14558 with_host(|h| chain_intrinsic_ctors_h(h, recv))
14559}
14560
14561/// [`chain_intrinsic_ctors`] against an already-held host borrow, for the
14562/// callers that are inside one — `can_write_prop` takes `&JsHost`, so going
14563/// back through `with_host` there aborts the process on a double borrow.
14564pub(crate) fn chain_intrinsic_ctors_h(h: &host::JsHost, recv: &Value) -> Vec<&'static str> {
14565 let mut out: Vec<&'static str> = Vec::new();
14566 let mut cur = recv.clone();
14567 for _ in 0..100 {
14568 let Some(p) = h.proto_of(&cur) else {
14569 break;
14570 };
14571 if h.is_null(&p) {
14572 break;
14573 }
14574 let name = match h.get(&p) {
14575 Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
14576 _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
14577 };
14578 if let Some(n) = name {
14579 if let Some(c) = crate::arity::PROTO_MEMBERS
14580 .iter()
14581 .map(|(k, _)| *k)
14582 .find(|k| *k == n)
14583 {
14584 if !out.contains(&c) {
14585 out.push(c);
14586 }
14587 }
14588 }
14589 cur = p;
14590 }
14591 out
14592}
14593
14594/// The constructor whose prototype defines `key` for `obj` — its own if that
14595/// prototype has it, otherwise `Object` — or `None` when neither does.
14596///
14597/// Used both by `in` and by the READ, so the two cannot disagree about which
14598/// prototype a name comes from. `new Map().toString` is `Map.prototype`'s and
14599/// `new Map().hasOwnProperty` is `Object.prototype`'s.
14600pub(crate) fn inherited_method_owner_pub(obj: &Value, key: &str) -> Option<&'static str> {
14601 inherited_method_owner(obj, key)
14602}
14603
14604fn inherited_method_owner(obj: &Value, key: &str) -> Option<&'static str> {
14605 if with_host(|h| h.has_null_proto(obj)) {
14606 return None;
14607 }
14608 // The generated prototype-member table, which unlike the arity table knows
14609 // about the ACCESSORS — `size` on a Map, `source` on a RegExp, `description`
14610 // on a Symbol are members but not functions — and about `constructor`.
14611 // A BOXED primitive reports its wrapper's constructor, not `Object` —
14612 // `'description' in Object(Symbol())` is true. The box is an ordinary
14613 // object carrying the primitive in a slot, so the ctor comes from what it
14614 // holds rather than from the box itself.
14615 let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14616 Some(c) => Some(c),
14617 // An `arguments` object is ARRAY-BACKED here so that indices, `length`,
14618 // spread and `for-of` work, but node's is an exotic that inherits from
14619 // `Object.prototype` — `typeof arguments.map` is `undefined`. Reporting
14620 // its backing kind would hand it the whole `Array.prototype`.
14621 None if is_arguments(obj) => Some("Object"),
14622 None => with_host(|h| default_ctor_name(h, obj)),
14623 };
14624 let on_proto = |c: &str| {
14625 crate::arity::PROTO_MEMBERS
14626 .binary_search_by(|(k, _)| (*k).cmp(c))
14627 .ok()
14628 .is_some_and(|i| {
14629 crate::arity::PROTO_MEMBERS[i]
14630 .1
14631 .iter()
14632 .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
14633 })
14634 };
14635 // `PROTO_MEMBERS` is generated from the prototypes' STRING keys, so a
14636 // well-known symbol member is absent from it. For an object whose CHAIN
14637 // reaches an intrinsic prototype the intrinsic table has to be consulted as
14638 // well, or `[...Object.create(Array.prototype)]` finds no `Symbol.iterator`
14639 // at all. It is deliberately NOT consulted for the receiver's own kind:
14640 // there a thunk would be minted for every `@@` member the table names,
14641 // including ones whose dispatch has no implementation for that receiver,
14642 // and `[...buffer]` then failed with `@@iterator is not a function`.
14643 //
14644 // It is narrowed further to an ORDINARY object: a natively-tagged receiver
14645 // (a typed array, a Buffer) is linked to a real intrinsic prototype too,
14646 // and minting a thunk there produced `@@iterator is not a function` for
14647 // `[...new Uint8Array(ab)]` — those kinds reach their iterator by their own
14648 // fast path, which the table entry would shadow.
14649 let plain = with_host(|h| h.kind_of(obj)) == Some(ObjKind::Object)
14650 && crate::stdlib::native_tag(obj).is_none();
14651 let on_proto_or_symbol =
14652 |c: &str| on_proto(c) || (plain && builtin_meta(&format!("@proto:{c}:{key}")).is_some());
14653 // Each candidate is only an answer while ITS prototype is still on the
14654 // receiver's chain. The two are asked separately: replacing an array's
14655 // prototype with a plain object takes `Array.prototype`'s methods away and
14656 // leaves `Object.prototype`'s, since the replacement inherits from it.
14657 if let Some(c) = ctor.filter(|c| on_proto(c) && intrinsic_reachable(obj, c)) {
14658 return Some(c);
14659 }
14660 // An intrinsic prototype the receiver's chain passes THROUGH, which its own
14661 // kind does not account for.
14662 if let Some(c) = chain_intrinsic_ctors(obj)
14663 .into_iter()
14664 .find(|c| on_proto_or_symbol(c))
14665 {
14666 return Some(c);
14667 }
14668 // Everything else inherits `Object.prototype`'s.
14669 if on_proto("Object") && intrinsic_reachable(obj, "Object") {
14670 return Some("Object");
14671 }
14672 None
14673}
14674
14675/// `structuredClone` — a deep copy of plain data (objects/arrays/primitives).
14676/// `structuredClone` — the HTML structured-clone algorithm's shape: a deep copy
14677/// that preserves the *reference graph*. Two properties pointing at the same
14678/// object clone to two properties pointing at the same clone, and a cycle clones
14679/// to a cycle instead of recursing forever. `seen` maps each source heap index
14680/// to its clone, which is what buys both.
14681/// The rendering node puts in a `DataCloneError` for a value the structured
14682/// clone algorithm refuses, or `None` when the value IS cloneable.
14683///
14684/// Refusing at all is the point: these used to be copied through by reference,
14685/// so `structuredClone({f: () => 1})` handed back an object sharing the
14686/// original's function and `structuredClone(new WeakMap())` returned the very
14687/// same WeakMap. Node throws on every one of them.
14688fn clone_refusal(v: &Value) -> Option<String> {
14689 let kind = with_host(|h| h.kind_of(v))?;
14690 let render = |ctor: &str| Some(format!("#<{ctor}>"));
14691 match kind {
14692 // A function renders as its SOURCE TEXT here, which each FuncDef
14693 // keeps as a span into its script (`JsHost::func_source`).
14694 ObjKind::Func | ObjKind::Class | ObjKind::BoundFunc | ObjKind::BoundMethod => {
14695 Some(with_host(|h| h.str_of(v)))
14696 }
14697 ObjKind::Builtin if with_host(|h| host::is_callable(h, v)) => {
14698 Some(with_host(|h| h.str_of(v)))
14699 }
14700 ObjKind::Symbol => Some(with_host(|h| h.str_of(v))),
14701 ObjKind::Promise => render("Promise"),
14702 ObjKind::Generator => Some("[object Generator]".to_string()),
14703 // A proxy is refused by its TARGET's shape: a callable one renders like
14704 // the function it wraps, everything else as a plain object.
14705 ObjKind::Proxy => Some(if with_host(|h| host::is_callable(h, v)) {
14706 with_host(|h| h.str_of(v))
14707 } else {
14708 "#<Object>".to_string()
14709 }),
14710 ObjKind::Map if with_host(|h| matches!(h.get(v), Some(JsObj::Map { weak: true, .. }))) => {
14711 render("WeakMap")
14712 }
14713 ObjKind::Set if with_host(|h| matches!(h.get(v), Some(JsObj::Set { weak: true, .. }))) => {
14714 render("WeakSet")
14715 }
14716 _ => match crate::stdlib::native_tag(v).as_deref() {
14717 Some(t @ ("WeakRef" | "FinalizationRegistry")) => render(t),
14718 _ => None,
14719 },
14720 }
14721}
14722
14723/// `structuredClone(value[, { transfer }])`.
14724///
14725/// Everything in `transfer` must be an `ArrayBuffer`, and each one is DETACHED
14726/// after the clone — its bytes belong to the copy. The option used to be
14727/// ignored entirely, so the source buffer stayed usable where node leaves it
14728/// with zero length.
14729fn structured_clone(args: Vec<Value>) -> Result<Value, String> {
14730 let list: Vec<Value> = match args.get(1).filter(|v| !matches!(v, Value::Undef)) {
14731 Some(opts) => {
14732 let t = get_property(opts, "transfer")?;
14733 if matches!(t, Value::Undef) {
14734 Vec::new()
14735 } else {
14736 host::iter_all(&t)?
14737 }
14738 }
14739 None => Vec::new(),
14740 };
14741 for item in &list {
14742 if crate::stdlib::native_tag(item).as_deref() != Some("ArrayBuffer") {
14743 return Err(host::dom_error(
14744 "DataCloneError",
14745 "Found invalid value in transferList.",
14746 ));
14747 }
14748 }
14749 let out = deep_clone(&arg0(&args))?;
14750 for item in &list {
14751 crate::stdlib::typedarray::detach_buffer(item);
14752 }
14753 Ok(out)
14754}
14755
14756pub(crate) fn deep_clone(v: &Value) -> Result<Value, String> {
14757 deep_clone_seen(v, &mut std::collections::HashMap::new())
14758}
14759
14760fn deep_clone_seen(
14761 v: &Value,
14762 seen: &mut std::collections::HashMap<u32, Value>,
14763) -> Result<Value, String> {
14764 let idx = match v {
14765 Value::Obj(i) => *i,
14766 _ => return Ok(v.clone()),
14767 };
14768 if let Some(done) = seen.get(&idx) {
14769 return Ok(done.clone());
14770 }
14771 if crate::stdlib::typedarray::is_detached(v) {
14772 return Err(host::dom_error(
14773 "DataCloneError",
14774 "An ArrayBuffer is detached and could not be cloned.",
14775 ));
14776 }
14777 if let Some(render) = clone_refusal(v) {
14778 return Err(host::dom_error(
14779 "DataCloneError",
14780 &format!("{render} could not be cloned."),
14781 ));
14782 }
14783 // A REGEXP is cloned, not shared: it carries a mutable `lastIndex`, so
14784 // handing back the same object let a write through the clone move the
14785 // original's match cursor.
14786 if let Some((src, flags)) = with_host(|h| match h.get(v) {
14787 Some(JsObj::RegExp(r)) => Some((r.source.clone(), r.flags.clone())),
14788 _ => None,
14789 }) {
14790 let args = with_host(|h| vec![h.new_str(src), h.new_str(flags)]);
14791 let out = regexp_ctor(&args)?;
14792 seen.insert(idx, out.clone());
14793 return Ok(out);
14794 }
14795 Ok(match with_host(|h| h.get(v).cloned()) {
14796 Some(JsObj::Array(items)) => {
14797 // Register the (empty) clone BEFORE recursing so a self-reference
14798 // resolves to it.
14799 let out = with_host(|h| h.new_array(Vec::new()));
14800 seen.insert(idx, out.clone());
14801 let mut cloned: Vec<Value> = Vec::with_capacity(items.len());
14802 for x in &items {
14803 cloned.push(deep_clone_seen(x, seen)?);
14804 }
14805 with_host(|h| {
14806 if let Some(JsObj::Array(a)) = h.get_mut(&out) {
14807 *a = cloned;
14808 }
14809 // A sparse source clones to an equally sparse array: the clone
14810 // walks own properties, so a hole is nothing to copy.
14811 h.copy_holes(v, &out, Some);
14812 });
14813 out
14814 }
14815 Some(JsObj::Object(_)) => {
14816 let out = with_host(|h| h.new_object(IndexMap::new()));
14817 seen.insert(idx, out.clone());
14818 // Own ENUMERABLE string keys, read THROUGH any accessor: the clone
14819 // walked the property map, where an accessor stores nothing, so
14820 // `structuredClone({get p(){return 1}})` silently lost `p`. A symbol
14821 // key and a non-enumerable one are dropped, as node drops them.
14822 let is_error = with_host(|h| h.error_to_string(v)).is_some();
14823 let proto = clone_proto(v);
14824 let keeps_proto = !matches!(proto, CloneProto::Plain);
14825 // An ERROR clones its name, message and stack and NOTHING else —
14826 // node drops any other own property, even an enumerable one.
14827 let keys: Vec<String> = if is_error {
14828 // An ERROR clones its name, message and stack and NOTHING else —
14829 // node drops any other own property, even an enumerable one.
14830 ["name", "message", "stack"]
14831 .iter()
14832 .filter(|k| has_property(v, k).unwrap_or(false))
14833 .map(|k| (*k).to_string())
14834 .collect()
14835 } else if keeps_proto {
14836 // A preserved exotic keeps EVERY own property, including the
14837 // non-enumerable ones and the internal slots — a Date's time
14838 // value, an ArrayBuffer's `byteLength` and byte store, a typed
14839 // array's view. The enumerable-only walk dropped all of those,
14840 // so a cloned Date read `Invalid Date` and a cloned
14841 // ArrayBuffer had no `byteLength`.
14842 with_host(|h| match h.get(v) {
14843 Some(JsObj::Object(p)) => p.keys().cloned().collect(),
14844 _ => Vec::new(),
14845 })
14846 } else {
14847 with_host(|h| h.own_enum_key_names(v))
14848 };
14849 let mut cloned: IndexMap<String, Value> = IndexMap::new();
14850 for k in keys {
14851 // An internal slot is read straight out of the map: it is not a
14852 // property, so a `[[Get]]` would not find it.
14853 let val = if k.starts_with("@@") {
14854 match with_host(|h| match h.get(v) {
14855 Some(JsObj::Object(p)) => p.get(&k).cloned(),
14856 _ => None,
14857 }) {
14858 Some(val) => val,
14859 None => continue,
14860 }
14861 } else {
14862 get_property(v, &k)?
14863 };
14864 cloned.insert(k, deep_clone_seen(&val, seen)?);
14865 }
14866 // The prototype survives only for the exotics the algorithm knows —
14867 // a Date, an Error, a typed array, a boxed primitive. A USER class
14868 // instance becomes a plain object, which is what node produces;
14869 // keeping every prototype made `structuredClone(new K())
14870 // instanceof K` true.
14871 with_host(|h| {
14872 if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14873 *p = cloned;
14874 }
14875 match &proto {
14876 CloneProto::Same => {
14877 if let Some(p) = h.proto_of(v) {
14878 h.set_proto(&out, p);
14879 }
14880 }
14881 CloneProto::Ctor(c) => {
14882 h.ensure_error_protos();
14883 let p = h.error_proto(c).or_else(|| h.ensure_ctor_proto(c));
14884 if let Some(p) = p {
14885 h.set_proto(&out, p);
14886 }
14887 // A Buffer clones to a plain `Uint8Array`, so the native
14888 // tag has to change with the prototype — left alone,
14889 // `Buffer.isBuffer` still answered true for the clone.
14890 // A Buffer clones to a plain `Uint8Array`, so the native
14891 // tag has to change with the prototype — left alone,
14892 // `Buffer.isBuffer` answered true for the clone and the
14893 // brand stayed `[object Object]`. A typed array is
14894 // tagged `TypedArray` and names its element type in
14895 // `@@kind`; `@@native = "Uint8Array"` matches no arm.
14896 if c == "Uint8Array" {
14897 let tag = h.new_str("TypedArray");
14898 let kind = h.new_str("Uint8Array");
14899 if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14900 p.insert("@@native".into(), tag);
14901 p.insert("@@kind".into(), kind);
14902 }
14903 }
14904 }
14905 CloneProto::Plain => {}
14906 }
14907 h.copy_prop_attrs(v, &out);
14908 });
14909 out
14910 }
14911 // Map/Set are structured types: clone the entries, keep the kind.
14912 Some(JsObj::Map { entries, weak }) => {
14913 let out = with_host(|h| {
14914 h.alloc(JsObj::Map {
14915 entries: IndexMap::new(),
14916 weak,
14917 })
14918 });
14919 seen.insert(idx, out.clone());
14920 let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
14921 for (k, val) in pairs {
14922 let ck = deep_clone_seen(&k, seen)?;
14923 let cv = deep_clone_seen(&val, seen)?;
14924 let _ = map_method(&out, "set", vec![ck, cv]);
14925 }
14926 out
14927 }
14928 Some(JsObj::Set { entries, weak }) => {
14929 let out = with_host(|h| {
14930 h.alloc(JsObj::Set {
14931 entries: IndexMap::new(),
14932 weak,
14933 })
14934 });
14935 seen.insert(idx, out.clone());
14936 let vals: Vec<Value> = entries.values().cloned().collect();
14937 for x in vals {
14938 let cx = deep_clone_seen(&x, seen)?;
14939 let _ = set_method(&out, "add", vec![cx]);
14940 }
14941 out
14942 }
14943 // A string, a BigInt and a boxed primitive are immutable enough to
14944 // share; anything left is a value type.
14945 _ => v.clone(),
14946 })
14947}
14948
14949/// Whether a cloned object keeps the source's prototype.
14950///
14951/// The structured clone algorithm reproduces the exotics it knows and turns
14952/// everything else into a plain object — so a `Date` clones to a `Date` and a
14953/// user class instance clones to an `Object`.
14954fn clone_proto(v: &Value) -> CloneProto {
14955 // An ERROR clones to the BUILT-IN class its `name` selects, so a subclass
14956 // flattens: `structuredClone(new (class E extends Error{})('m'))` reports
14957 // `Error`, not `E`.
14958 if with_host(|h| h.error_to_string(v)).is_some() {
14959 let name = get_property(v, "name")
14960 .map(|n| with_host(|h| h.str_of(&n)))
14961 .unwrap_or_else(|_| "Error".into());
14962 let class = if host::ERROR_NAMES.contains(&name.as_str()) {
14963 name
14964 } else {
14965 "Error".to_string()
14966 };
14967 return CloneProto::Ctor(class);
14968 }
14969 match crate::stdlib::native_tag(v).as_deref() {
14970 // A Buffer is not reproduced as a Buffer: node hands back a plain
14971 // `Uint8Array` over the same bytes.
14972 Some("Buffer") => CloneProto::Ctor("Uint8Array".into()),
14973 Some(_) => CloneProto::Same,
14974 // A boxed primitive keeps its wrapper; anything else — a user class
14975 // instance included — becomes a plain object.
14976 None if wrapped_primitive(v).is_some() => CloneProto::Same,
14977 None => CloneProto::Plain,
14978 }
14979}
14980
14981/// Which prototype a clone gets: the source's, a named builtin's, or none.
14982enum CloneProto {
14983 Same,
14984 Ctor(String),
14985 Plain,
14986}
14987
14988// ══ Promises, timers, microtasks (event-loop-driven) ═════════════════════════
14989
14990/// A short `Name: message` string for an error value (used when an await
14991/// rejection unwinds as a thrown error).
14992pub fn error_string(h: &host::JsHost, v: &Value) -> String {
14993 if let Some(JsObj::Object(props)) = h.get(v) {
14994 let name = props
14995 .get("name")
14996 .map(|x| h.str_of(x))
14997 .or_else(|| host::lookup_chain(h, v, "name").map(|x| h.str_of(&x)))
14998 .unwrap_or_else(|| "Error".into());
14999 if let Some(m) = props.get("message") {
15000 return format!("{name}: {}", h.str_of(m));
15001 }
15002 return name;
15003 }
15004 h.str_of(v)
15005}
15006
15007/// 27.2.5.3 `thenFinally`/`catchFinally`: `PromiseResolve(result).then(() =>
15008/// value)`, or `() => { throw reason }` on the reject path.
15009///
15010/// Returning the carried value directly — what this used to do — skipped both
15011/// halves. A promise returned by the callback was never awaited, so the
15012/// ordinary async-cleanup shape
15013///
15014/// ```text
15015/// work().finally(() => closeConnection()).then(next)
15016/// ```
15017///
15018/// ran `next` before the connection had closed. And the chain settled three
15019/// microtask ticks early, which is observable in ordering against any other
15020/// chain, not just against a timer.
15021///
15022/// A rejection from the callback's own promise wins over the carried value, so
15023/// no reject handler is attached here: it propagates on its own.
15024fn finally_chain(result: Value, carried: Value, rethrow: bool) -> Value {
15025 // PromiseResolve (27.2.4.7) returns an argument that is already a promise
15026 // UNCHANGED. Wrapping it anyway costs the extra tick that resolving with a
15027 // thenable takes to adopt it, which showed up as a callback returning a
15028 // rejected promise settling one tick late against every other chain.
15029 let p = match with_host(|h| h.promise_id(&result)) {
15030 Some(_) => result,
15031 None => {
15032 let fresh = with_host(|h| h.new_promise());
15033 if let Some(pid) = with_host(|h| h.promise_id(&fresh)) {
15034 host::resolve_promise_val(pid, result);
15035 }
15036 fresh
15037 }
15038 };
15039 let cell = with_host(|h| h.new_array(vec![carried]));
15040 let idx = match cell {
15041 Value::Obj(i) => i,
15042 _ => 0,
15043 };
15044 let tag = if rethrow { "finrethrow" } else { "finret" };
15045 let thunk = make_builtin(format!("@@{tag}:{idx}"));
15046 host::promise_then(&p, thunk, Value::Undef)
15047}
15048
15049fn make_builtin(name: String) -> Value {
15050 with_host(|h| h.alloc(JsObj::Builtin(name)))
15051}
15052
15053/// `[[GetPrototypeOf]]` (10.1.1) — the answer `Object.getPrototypeOf`,
15054/// `Reflect.getPrototypeOf` and a `__proto__` READ all have to agree on.
15055///
15056/// `__proto__` used to answer from `JsHost::proto_of` alone, which records only
15057/// an EXPLICIT link, so an object on the default prototype reported `null`:
15058/// `({}).__proto__ === Object.prototype` was false while
15059/// `Object.getPrototypeOf({}) === Object.prototype` was true. One function, so
15060/// the three cannot drift apart again.
15061pub fn prototype_of(v: &Value) -> Value {
15062 // Constructor-side inheritance: `Buffer extends Uint8Array`, so
15063 // `Object.getPrototypeOf(Buffer)` is the `Uint8Array` constructor itself,
15064 // not `Function.prototype`. This is the class-side half of the subclass
15065 // link — the instance-side half is `Buffer.prototype`'s `[[Prototype]]`.
15066 if matches!(with_host(|h| h.get(v).cloned()), Some(JsObj::Builtin(ref n)) if n == "Buffer") {
15067 return with_host(|h| h.alloc(JsObj::Builtin("Uint8Array".into())));
15068 }
15069 // The NativeError constructors inherit from `Error` (20.5.6.2: their
15070 // `[[Prototype]]` is %Error%), so `Object.getPrototypeOf(RangeError) ===
15071 // Error`. They reported `null`.
15072 if matches!(
15073 with_host(|h| h.get(v).cloned()),
15074 Some(JsObj::Builtin(ref n)) if matches!(
15075 n.as_str(),
15076 "EvalError" | "RangeError" | "ReferenceError" | "SyntaxError" | "TypeError"
15077 | "URIError" | "AggregateError"
15078 )
15079 ) {
15080 return with_host(|h| h.alloc(JsObj::Builtin("Error".into())));
15081 }
15082 // Constructor-side inheritance for a `class B extends A` (ClassDefinition
15083 // 15.7.14 step 6.d: the constructor's `[[Prototype]]` is the parent
15084 // CONSTRUCTOR, not `Function.prototype`). Statics already resolved through
15085 // `ClassVal.parent`, but the link itself was invisible, so
15086 // `Object.getPrototypeOf(B) === A` read false and any library walking the
15087 // constructor chain — rather than calling a static — saw a base class.
15088 // A base class keeps the default answer below (`Function.prototype`).
15089 if let Some(JsObj::Class(c)) = with_host(|h| h.get(v).cloned()) {
15090 if let Some(parent) = c.parent {
15091 return parent;
15092 }
15093 }
15094 // `Object.create(null)` and friends really do have a null prototype.
15095 if with_host(|h| h.has_null_proto(v)) {
15096 return with_host(|h| h.null());
15097 }
15098 // `Object.prototype` is the CHAIN ROOT, so its own prototype is `null`. It
15099 // reported itself, because the fallback below answers by constructor name
15100 // and a plain object's is `Object` — an infinite chain to anything walking
15101 // it.
15102 if with_host(|h| h.strict_eq(v, &h.object_proto())) {
15103 return with_host(|h| h.null());
15104 }
15105 // Every OTHER builtin prototype namespace (`Array.prototype`,
15106 // `Function.prototype`, …) inherits from `Object.prototype`; the fallback
15107 // would send it back to a namespace handle for its own constructor.
15108 if matches!(
15109 with_host(|h| h.get(v).cloned()),
15110 Some(JsObj::Builtin(ref n)) if n.ends_with(".prototype")
15111 ) {
15112 return with_host(|h| h.object_proto());
15113 }
15114 if let Some(p) = with_host(|h| h.proto_of(v)) {
15115 return p;
15116 }
15117 // A builtin exotic with no explicit `[[Prototype]]` link reports its
15118 // constructor's prototype namespace (`Object.getPrototypeOf([]) ===
15119 // Array.prototype`), which `strict_eq` compares by name. A plain object
15120 // reports the one real `Object.prototype` object.
15121 with_host(|h| {
15122 h.ensure_native_protos();
15123 match default_ctor_name(h, v) {
15124 Some("Object") => h.object_proto(),
15125 // `String`/`Number`/`Boolean` own REAL prototype objects, so a
15126 // primitive must report that object and not a fresh namespace
15127 // thunk — otherwise `Object.getPrototypeOf(1) === Number.prototype`
15128 // compares a thunk against the real object and reads false.
15129 Some(c) => h
15130 .native_proto(c)
15131 .unwrap_or_else(|| h.alloc(JsObj::Builtin(format!("{c}.prototype")))),
15132 // A builtin FUNCTION (`Error`, `Math.max`, `parseInt`) is an ordinary
15133 // function object whose `[[Prototype]]` is `Function.prototype`.
15134 None if h.type_of(v) == "function" => h
15135 .native_proto("Function")
15136 .unwrap_or_else(|| h.alloc(JsObj::Builtin("Function.prototype".into()))),
15137 None => h.null(),
15138 }
15139 })
15140}
15141
15142/// `new Promise((resolve, reject) => …)` — run the executor synchronously with
15143/// internal resolve/reject functions.
15144/// A fresh promise built through the SPECIES constructor, when a `Promise`
15145/// static was reached through a subclass.
15146///
15147/// `class P extends Promise {}` makes `P.resolve(1)` a `P`, because every
15148/// combinator builds its result with `this` (27.2.4.x). They all allocated a
15149/// plain promise, so nothing a subclass produced was an instance of it. The
15150/// executor is a no-op: the result is settled through its promise id, which is
15151/// what the ordinary path does too.
15152fn promise_species_create() -> Result<Option<Value>, String> {
15153 let Some(ctor) = host::current_static_this() else {
15154 return Ok(None);
15155 };
15156 if !matches!(
15157 with_host(|h| h.kind_of(&ctor)),
15158 Some(ObjKind::Class) | Some(ObjKind::Func)
15159 ) {
15160 return Ok(None);
15161 }
15162 let species = match get_property(&ctor, "@@species") {
15163 Ok(Value::Undef) => ctor,
15164 Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15165 Ok(s) => s,
15166 Err(_) => ctor,
15167 };
15168 if !matches!(
15169 with_host(|h| h.kind_of(&species)),
15170 Some(ObjKind::Class) | Some(ObjKind::Func)
15171 ) {
15172 return Ok(None);
15173 }
15174 let noop = make_builtin("@@pnoop".to_string());
15175 let p = host::construct(&species, vec![noop])?;
15176 // Only usable if the subclass really produced a promise; a constructor that
15177 // returned something else has no id to settle.
15178 Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15179}
15180
15181/// The species constructor of a promise RECEIVER — what `then`/`catch`/`finally`
15182/// build their result with (`SpeciesConstructor(p, %Promise%)`, 27.2.5.4 step 3).
15183///
15184/// Distinct from `promise_species_create`, which answers for a STATIC reached
15185/// through a subclass. Here the subclass comes from the receiver itself, so
15186/// `P.resolve(1).then(f)` is also a `P`.
15187pub fn promise_species_from(recv: &Value) -> Result<Option<Value>, String> {
15188 // A chain lookup: a Promise receiver resolves through the stdlib funnel,
15189 // which has no `constructor` entry of its own.
15190 let ctor = with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef);
15191 if !matches!(
15192 with_host(|h| h.kind_of(&ctor)),
15193 Some(ObjKind::Class) | Some(ObjKind::Func)
15194 ) {
15195 return Ok(None);
15196 }
15197 let species = match get_property(&ctor, "@@species") {
15198 Ok(Value::Undef) => ctor,
15199 Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15200 Ok(s) => s,
15201 Err(_) => ctor,
15202 };
15203 if !matches!(
15204 with_host(|h| h.kind_of(&species)),
15205 Some(ObjKind::Class) | Some(ObjKind::Func)
15206 ) {
15207 return Ok(None);
15208 }
15209 let noop = make_builtin("@@pnoop".to_string());
15210 let p = host::construct(&species, vec![noop])?;
15211 Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15212}
15213
15214fn new_promise(executor: Value) -> Result<Value, String> {
15215 let p = with_host(|h| h.new_promise());
15216 let id = with_host(|h| h.promise_id(&p).unwrap());
15217 let res = make_builtin(format!("@@presolve:{id}"));
15218 let rej = make_builtin(format!("@@preject:{id}"));
15219 if let Err(e) = host::invoke(&executor, vec![res, rej], None) {
15220 // A throw in the executor rejects the promise.
15221 let ev = host::take_exc_or_error(&e);
15222 host::reject_promise_val(id, ev);
15223 }
15224 Ok(p)
15225}
15226
15227/// `Promise.resolve(v)` for stdlib callers that need to hand back an
15228/// already-settled promise.
15229pub fn promise_resolve_pub(v: Value) -> Result<Value, String> {
15230 promise_resolve(v)
15231}
15232
15233fn promise_resolve(v: Value) -> Result<Value, String> {
15234 if let Some(p) = promise_species_create()? {
15235 let id = with_host(|h| h.promise_id(&p).unwrap());
15236 host::resolve_promise_val(id, v);
15237 return Ok(p);
15238 }
15239 Ok(host::promise_of(&v))
15240}
15241fn promise_reject(v: Value) -> Result<Value, String> {
15242 let p = match promise_species_create()? {
15243 Some(p) => p,
15244 None => with_host(|h| h.new_promise()),
15245 };
15246 let id = with_host(|h| h.promise_id(&p).unwrap());
15247 host::reject_promise_val(id, v);
15248 Ok(p)
15249}
15250
15251/// `Promise.withResolvers()` — a fresh pending promise paired with its own
15252/// resolve/reject continuations (the same `@@presolve`/`@@preject` thunks the
15253/// executor receives), returned as a plain `{ promise, resolve, reject }` object.
15254/// A fresh pending promise paired with the thunk that resolves it, for stdlib
15255/// callers that hand the resolver to an event listener.
15256pub fn pending_promise_with_resolver() -> (Value, Value) {
15257 let p = with_host(|h| h.new_promise());
15258 let id = with_host(|h| h.promise_id(&p).unwrap());
15259 let resolve = make_builtin(format!("@@presolve:{id}"));
15260 (p, resolve)
15261}
15262
15263/// `RegExp.escape(s)` (22.2.4.2) — a string that matches `s` literally.
15264///
15265/// The rule is not "backslash the syntax characters": it also escapes a LEADING
15266/// ASCII alphanumeric, so the result can be concatenated after a `\` or a `{`
15267/// without the two running together, and it escapes the punctuation that is
15268/// meaningful inside a character class or a group name.
15269fn regexp_escape(args: Vec<Value>) -> Result<Value, String> {
15270 let v = arg0(&args);
15271 if !matches!(v, Value::Str(_)) && !with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_)))) {
15272 return Err(host::type_error("input argument must be a string"));
15273 }
15274 let s = with_host(|h| h.str_of(&v));
15275 // Punctuation that is escaped by CODE POINT rather than with a backslash.
15276 // Measured against node over the whole ASCII range, not taken from a list:
15277 // `-` and `=` are here, `$` and `*` are syntax characters and are not.
15278 const OTHER_PUNCTUATORS: &str = " !\"#%&',-:;<=>@`~";
15279 const SYNTAX: &str = "^$\\.*+?()[]{}|/";
15280 let mut out = String::with_capacity(s.len());
15281 for (i, c) in s.chars().enumerate() {
15282 // A leading ASCII alphanumeric, and only a leading one.
15283 if i == 0 && c.is_ascii_alphanumeric() {
15284 out.push_str(&format!("\\x{:02x}", c as u32));
15285 continue;
15286 }
15287 if SYNTAX.contains(c) {
15288 out.push('\\');
15289 out.push(c);
15290 continue;
15291 }
15292 match c {
15293 '\t' => out.push_str("\\t"),
15294 '\n' => out.push_str("\\n"),
15295 '\u{b}' => out.push_str("\\v"),
15296 '\u{c}' => out.push_str("\\f"),
15297 '\r' => out.push_str("\\r"),
15298 _ if OTHER_PUNCTUATORS.contains(c) || is_regex_escape_space(c) => {
15299 let n = c as u32;
15300 if n <= 0xff {
15301 out.push_str(&format!("\\x{n:02x}"));
15302 } else {
15303 out.push_str(&format!("\\u{n:04x}"));
15304 }
15305 }
15306 _ => out.push(c),
15307 }
15308 }
15309 Ok(with_host(|h| h.new_str(out)))
15310}
15311
15312/// The WhiteSpace and LineTerminator code points `RegExp.escape` spells out.
15313/// Deliberately NOT `char::is_whitespace`: U+180E and U+200B are whitespace to
15314/// Unicode but not to ECMAScript, and node leaves both alone.
15315fn is_regex_escape_space(c: char) -> bool {
15316 matches!(
15317 c,
15318 '\u{a0}' | '\u{1680}' | '\u{2000}'
15319 ..='\u{200a}'
15320 | '\u{2028}'
15321 | '\u{2029}'
15322 | '\u{202f}'
15323 | '\u{205f}'
15324 | '\u{3000}'
15325 | '\u{feff}'
15326 )
15327}
15328
15329/// `Error.isError(v)` (20.5.2.1) — a brand check for `[[ErrorData]]`, so an
15330/// object that merely INHERITS from `Error.prototype` is not one.
15331fn error_is_error(args: Vec<Value>) -> Result<Value, String> {
15332 let v = arg0(&args);
15333 Ok(Value::Bool(with_host(|h| has_error_data(h, &v))))
15334}
15335
15336/// Whether `v` carries `[[ErrorData]]` — the slot `Error.isError` (20.5.2.1)
15337/// and `Object.prototype.toString`'s step 9 both test.
15338///
15339/// The brand is the OWN `stack` an error is built with (a `DOMException`
15340/// carries `@@domName` instead); a plain `Object.create(Error.prototype)` has
15341/// neither, which is why inheriting from an error prototype does not make a
15342/// value an error. Shared so the two cannot disagree — branding by a chain
15343/// lookup for `name`/`message` made `Object.create(Error.prototype)` report
15344/// `[object Error]` where node says `[object Object]`, while `Error.isError`
15345/// on the same value already said false.
15346pub(crate) fn has_error_data(h: &host::JsHost, v: &Value) -> bool {
15347 match h.get(v) {
15348 Some(JsObj::Object(p)) => {
15349 p.contains_key("stack") || p.contains_key("@@stackRaw") || p.contains_key("@@domName")
15350 }
15351 _ => false,
15352 }
15353}
15354
15355/// `Promise.try(fn, ...args)` (27.2.4.6) — call `fn` and settle the promise with
15356/// what it does, so a SYNCHRONOUS throw becomes a rejection instead of
15357/// propagating. `Promise.resolve().then(fn)` is the shape it replaces, and it
15358/// costs a tick that this does not.
15359fn promise_try(args: Vec<Value>) -> Result<Value, String> {
15360 let f = arg0(&args);
15361 // A non-callable argument REJECTS, it does not throw: `Promise.try(5)`
15362 // returns a rejected promise, so the surrounding `try` never sees it.
15363 if !with_host(|h| host::is_callable(h, &f)) {
15364 // Node names the TYPE alongside the value — `number 5 is not a
15365 // function` — which the ordinary call-site message does not. A plain
15366 // object and a symbol name only the type; `null` names both.
15367 let shown = with_host(|h| {
15368 let kind = h.type_of(&f);
15369 match kind {
15370 "undefined" => "undefined".to_string(),
15371 "symbol" | "bigint" => kind.to_string(),
15372 "object" if h.is_null(&f) => "object null".to_string(),
15373 "object" => "object".to_string(),
15374 "string" => format!("string \"{}\"", h.str_of(&f)),
15375 _ => format!("{kind} {}", h.str_of(&f)),
15376 }
15377 });
15378 let p = with_host(|h| h.new_promise());
15379 let id = with_host(|h| h.promise_id(&p).unwrap());
15380 let reject = make_builtin(format!("@@preject:{id}"));
15381 let err =
15382 with_host(|h| synth_error(h, &host::type_error(&format!("{shown} is not a function"))));
15383 host::invoke(&reject, vec![err], None)?;
15384 return Ok(p);
15385 }
15386 let rest: Vec<Value> = args.iter().skip(1).cloned().collect();
15387 let p = with_host(|h| h.new_promise());
15388 let id = with_host(|h| h.promise_id(&p).unwrap());
15389 let resolve = make_builtin(format!("@@presolve:{id}"));
15390 let reject = make_builtin(format!("@@preject:{id}"));
15391 let promise = p;
15392 match host::invoke(&f, rest, None) {
15393 Ok(v) => {
15394 host::invoke(&resolve, vec![v], None)?;
15395 }
15396 Err(e) => {
15397 // The thrown VALUE, not a re-synthesis of its rendering: a callback
15398 // that throws a `TypeError` must reject with that object, and
15399 // rebuilding it from the message string flattened it to a plain
15400 // `Error` whose message was the rendered `Uncaught TypeError: t`.
15401 let err =
15402 with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15403 with_host(|h| {
15404 h.error = None;
15405 h.exc = None;
15406 });
15407 host::invoke(&reject, vec![err], None)?;
15408 }
15409 }
15410 Ok(promise)
15411}
15412
15413fn promise_with_resolvers() -> Result<Value, String> {
15414 let p = with_host(|h| h.new_promise());
15415 let id = with_host(|h| h.promise_id(&p).unwrap());
15416 let resolve = make_builtin(format!("@@presolve:{id}"));
15417 let reject = make_builtin(format!("@@preject:{id}"));
15418 let mut props: IndexMap<String, Value> = IndexMap::new();
15419 props.insert("promise".into(), p);
15420 props.insert("resolve".into(), resolve);
15421 props.insert("reject".into(), reject);
15422 Ok(with_host(|h| h.new_object(props)))
15423}
15424
15425/// A promise already rejected with `e` — what every combinator hands back when
15426/// the ITERABLE misbehaves.
15427///
15428/// 27.2.4.1 step 4 catches an abrupt completion from the iteration and rejects
15429/// rather than letting it propagate, so `Promise.all(badIterable)` returns a
15430/// rejected promise. Throwing synchronously meant a `.catch()` never attached
15431/// and the caller saw the error at the call site instead.
15432fn rejected_promise(e: String) -> Value {
15433 let p = with_host(|h| h.new_promise());
15434 let id = with_host(|h| h.promise_id(&p).unwrap());
15435 let reject = make_builtin(format!("@@preject:{id}"));
15436 let err = with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15437 with_host(|h| {
15438 h.error = None;
15439 h.exc = None;
15440 });
15441 let _ = host::invoke(&reject, vec![err], None);
15442 p
15443}
15444
15445#[derive(Clone, Copy)]
15446enum AllMode {
15447 All,
15448 AllSettled,
15449}
15450
15451/// `Promise.all` / `Promise.allSettled`.
15452fn promise_all(args: Vec<Value>, mode: AllMode) -> Result<Value, String> {
15453 let items = match host::iter_all(&arg0(&args)) {
15454 Ok(v) => v,
15455 Err(e) => return Ok(rejected_promise(e)),
15456 };
15457 // 27.2.4.1 step 3: the combinator builds its result with `this`, so on a
15458 // subclass the promise it hands back is an instance of that subclass.
15459 let result = match promise_species_create()? {
15460 Some(p) => p,
15461 None => with_host(|h| h.new_promise()),
15462 };
15463 let rid = with_host(|h| h.promise_id(&result).unwrap());
15464 let n = items.len();
15465 if n == 0 {
15466 let empty = with_host(|h| h.new_array(Vec::new()));
15467 host::resolve_promise_val(rid, empty);
15468 return Ok(result);
15469 }
15470 // Shared mutable accumulator via Rc<RefCell<…>>.
15471 let slots = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15472 let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15473 for (i, it) in items.into_iter().enumerate() {
15474 let ap = host::promise_of(&it);
15475 let aid = with_host(|h| h.promise_id(&ap).unwrap());
15476 let slots = slots.clone();
15477 let remaining = remaining.clone();
15478 host::subscribe_native(
15479 aid,
15480 Box::new(move |state, val| {
15481 let settled = match mode {
15482 AllMode::All => {
15483 if state == host::PromiseState::Rejected {
15484 host::reject_promise_val(rid, val);
15485 return Ok(());
15486 }
15487 val
15488 }
15489 AllMode::AllSettled => with_host(|h| {
15490 let mut m: IndexMap<String, Value> = IndexMap::new();
15491 if state == host::PromiseState::Rejected {
15492 m.insert("status".into(), h.new_str("rejected"));
15493 m.insert("reason".into(), val);
15494 } else {
15495 m.insert("status".into(), h.new_str("fulfilled"));
15496 m.insert("value".into(), val);
15497 }
15498 h.new_object(m)
15499 }),
15500 };
15501 slots.borrow_mut()[i] = settled;
15502 let mut r = remaining.borrow_mut();
15503 *r -= 1;
15504 if *r == 0 {
15505 let arr = with_host(|h| h.new_array(slots.borrow().clone()));
15506 host::resolve_promise_val(rid, arr);
15507 }
15508 Ok(())
15509 }),
15510 );
15511 }
15512 Ok(result)
15513}
15514
15515/// `Promise.race` (first to settle wins) / `Promise.any` (first to fulfill wins).
15516fn promise_race(args: Vec<Value>, any: bool) -> Result<Value, String> {
15517 let items = match host::iter_all(&arg0(&args)) {
15518 Ok(v) => v,
15519 Err(e) => return Ok(rejected_promise(e)),
15520 };
15521 // Built with `this`, as every combinator is (27.2.4.5 / 27.2.4.3).
15522 let result = match promise_species_create()? {
15523 Some(p) => p,
15524 None => with_host(|h| h.new_promise()),
15525 };
15526 let rid = with_host(|h| h.promise_id(&result).unwrap());
15527 let n = items.len();
15528 let errors = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15529 let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15530 for (i, it) in items.into_iter().enumerate() {
15531 let ap = host::promise_of(&it);
15532 let aid = with_host(|h| h.promise_id(&ap).unwrap());
15533 let errors = errors.clone();
15534 let remaining = remaining.clone();
15535 host::subscribe_native(
15536 aid,
15537 Box::new(move |state, val| {
15538 if any {
15539 if state == host::PromiseState::Fulfilled {
15540 host::resolve_promise_val(rid, val);
15541 } else {
15542 errors.borrow_mut()[i] = val;
15543 let mut r = remaining.borrow_mut();
15544 *r -= 1;
15545 if *r == 0 {
15546 // All rejected → AggregateError carrying every reason.
15547 let reasons = with_host(|h| h.new_array(errors.borrow().clone()));
15548 let msg = with_host(|h| h.new_str("All promises were rejected"));
15549 let agg = make_error_inner("AggregateError", &[reasons, msg]);
15550 host::reject_promise_val(rid, agg);
15551 }
15552 }
15553 } else if state == host::PromiseState::Rejected {
15554 host::reject_promise_val(rid, val);
15555 } else {
15556 host::resolve_promise_val(rid, val);
15557 }
15558 Ok(())
15559 }),
15560 );
15561 }
15562 Ok(result)
15563}
15564
15565/// `.then` / `.catch` / `.finally` on a promise.
15566fn promise_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
15567 match name {
15568 "then" => Ok(host::promise_then(
15569 recv,
15570 args.first().cloned().unwrap_or(Value::Undef),
15571 args.get(1).cloned().unwrap_or(Value::Undef),
15572 )),
15573 "catch" => Ok(host::promise_then(
15574 recv,
15575 Value::Undef,
15576 args.first().cloned().unwrap_or(Value::Undef),
15577 )),
15578 "finally" => {
15579 let cb = arg0(&args);
15580 // 27.2.5.3 step 3: a non-callable `onFinally` is handed to `then`
15581 // as BOTH handlers, and `then` ignores a non-callable one — so the
15582 // value or reason simply passes through. Building the thunks
15583 // regardless meant `p.finally(null)` tried to call `null`.
15584 if !with_host(|h| host::is_callable(h, &cb)) {
15585 return Ok(host::promise_then(recv, cb.clone(), cb));
15586 }
15587 let i = match cb {
15588 Value::Obj(i) => i,
15589 _ => 0,
15590 };
15591 let pass = make_builtin(format!("@@finpass:{i}"));
15592 let throw = make_builtin(format!("@@finthrow:{i}"));
15593 Ok(host::promise_then(recv, pass, throw))
15594 }
15595 _ => Err(host::type_error(&format!(
15596 "promise.{name} is not a function"
15597 ))),
15598 }
15599}
15600
15601fn enqueue_microtask(next_tick: bool, cb: Value, args: Vec<Value>) {
15602 with_host(|h| {
15603 if next_tick {
15604 h.queue_nexttick(cb, args);
15605 } else {
15606 h.queue_micro(cb, args);
15607 }
15608 });
15609}
15610
15611/// `setTimeout`/`setInterval`/`setImmediate` — register a macrotask and return
15612/// the handle object Node returns (`Timeout` for the first two, `Immediate` for
15613/// the third), carrying `ref`/`unref`/`hasRef`/`refresh`.
15614///
15615/// `setInterval` schedules a *repeating* timer: the loop re-arms it each time it
15616/// fires, so it runs until cleared and — being referenced — holds the process
15617/// open exactly as in Node.
15618fn schedule_timer(name: &str, args: Vec<Value>) -> Value {
15619 let cb = arg0(&args);
15620 let delay = if name == "setImmediate" {
15621 -1.0 // before any 0ms timeout
15622 } else {
15623 args.get(1)
15624 .map(|d| with_host(|h| h.to_number(d)))
15625 .unwrap_or(0.0)
15626 .max(0.0)
15627 };
15628 let extra = if name == "setImmediate" {
15629 args.get(1..).map(|s| s.to_vec()).unwrap_or_default()
15630 } else {
15631 args.get(2..).map(|s| s.to_vec()).unwrap_or_default()
15632 };
15633 // Node clamps a sub-1ms interval to 1ms, so `setInterval(fn, 0)` yields a
15634 // ~1000Hz timer rather than a busy loop that starves the rest of the queue.
15635 let interval = (name == "setInterval").then(|| delay.max(1.0));
15636 let id = with_host(|h| h.add_timer(delay, cb, extra, interval));
15637 let tag = if name == "setImmediate" {
15638 "Immediate"
15639 } else {
15640 "Timeout"
15641 };
15642 crate::stdlib::timers::new_handle(id, tag)
15643}
15644
15645/// `clearTimeout`/`clearInterval`/`clearImmediate` — cancel by handle object or
15646/// by the bare id it coerces to (code that stored `+timer` still works).
15647fn clear_timer(v: &Value) {
15648 let id =
15649 crate::stdlib::timers::handle_id(v).unwrap_or_else(|| with_host(|h| h.to_number(v)) as u64);
15650 with_host(|h| h.cancel_timer(id));
15651}