nodejs/builtins.rs
1//! Builtin op handlers (compiler-emitted `CallBuiltin` ids) plus the JS standard
2//! library (`console`, `Math`, `JSON`, `Object`, array/string methods) reachable
3//! from the host. Handlers pop their arguments off the VM operand stack and
4//! return the result value, which the VM pushes back.
5
6use crate::host::{self, ops, with_host, FuncVal, JsObj, ObjKind};
7use fusevm::{NumOp, Value, VM};
8use indexmap::IndexMap;
9
10/// Register every node-js builtin id on a VM.
11pub fn install(vm: &mut VM) {
12 vm.register_builtin(ops::GETLOCAL, b_getlocal);
13 vm.register_builtin(ops::SETLOCAL, b_setlocal);
14 vm.register_builtin(ops::SETLOCAL_STRICT, b_setlocal_strict);
15 vm.register_builtin(ops::DECLARE, b_declare);
16 vm.register_builtin(ops::DECLARE_CONST, b_declare_const);
17 vm.register_builtin(ops::MARK_HOLE, b_mark_hole);
18 vm.register_builtin(ops::DELNAME, b_delname);
19 vm.register_builtin(ops::GETATTR, b_getattr);
20 vm.register_builtin(ops::SETATTR, b_setattr);
21 vm.register_builtin(ops::GETITEM, b_getitem);
22 vm.register_builtin(ops::SETITEM, b_setitem);
23 vm.register_builtin(ops::DELITEM, b_delitem);
24 vm.register_builtin(ops::MKSTR, b_mkstr);
25 vm.register_builtin(ops::MKARR, b_mkarr);
26 vm.register_builtin(ops::MKOBJ, b_mkobj);
27 vm.register_builtin(ops::CALL, b_call);
28 vm.register_builtin(ops::CALL_METHOD, b_call_method);
29 vm.register_builtin(ops::CALL_VALUE, b_call_value);
30 vm.register_builtin(ops::NEW, b_new);
31 vm.register_builtin(ops::TRUTHY, b_truthy);
32 vm.register_builtin(ops::TOSTR, b_tostr);
33 vm.register_builtin(ops::MKFUNC, b_mkfunc);
34 vm.register_builtin(ops::GETITER, b_getiter);
35 vm.register_builtin(ops::FORITER, b_foriter);
36 vm.register_builtin(ops::FORIN_KEYS, b_forin_keys);
37 vm.register_builtin(ops::FORIN_ALIVE, b_forin_alive);
38 vm.register_builtin(ops::HOIST_TDZ, b_hoist_tdz);
39 vm.register_builtin(ops::NEW_SPREAD, b_new_spread);
40 vm.register_builtin(ops::SUPER_CALL_SPREAD, b_super_call_spread);
41 vm.register_builtin(ops::CONTAINS, b_contains);
42 vm.register_builtin(ops::SIG_RETURN, b_sig_return);
43 vm.register_builtin(ops::BINOP, b_binop);
44 vm.register_builtin(ops::UNARY, b_unary);
45 vm.register_builtin(ops::STRICT_EQ, b_strict_eq);
46 vm.register_builtin(ops::LOOSE_EQ, b_loose_eq);
47 vm.register_builtin(ops::TYPEOF, b_typeof);
48 vm.register_builtin(ops::LOAD_NULL, b_load_null);
49 vm.register_builtin(ops::THROW, b_throw);
50 vm.register_builtin(ops::TRY, b_try);
51 vm.register_builtin(ops::NULLISH, b_nullish);
52 vm.register_builtin(ops::UNPACK, b_unpack);
53 vm.register_builtin(ops::BUILD_ARGS, b_build_args);
54 vm.register_builtin(ops::THIS, b_this);
55 vm.register_builtin(ops::INSTANCEOF, b_instanceof);
56 vm.register_builtin(ops::DELPROP_NAME, b_delprop_name);
57 vm.register_builtin(ops::APPLY, b_apply);
58 vm.register_builtin(ops::APPLY_METHOD, b_apply_method);
59 vm.register_builtin(ops::OBJ_REST, b_obj_rest);
60 vm.register_builtin(ops::DIV, b_div);
61 vm.register_builtin(ops::POW, b_pow);
62 vm.register_builtin(ops::MKCLASS, b_mkclass);
63 vm.register_builtin(ops::DEF_MEMBER, b_def_member);
64 vm.register_builtin(ops::DEF_FIELD, b_def_field);
65 vm.register_builtin(ops::SUPER_CALL, b_super_call);
66 vm.register_builtin(ops::SUPER_GET, b_super_get);
67 vm.register_builtin(ops::YIELD, b_yield);
68 vm.register_builtin(ops::PROPKEY, b_propkey);
69 vm.register_builtin(ops::NEW_TARGET, b_new_target);
70 vm.register_builtin(ops::AWAIT, b_await);
71 vm.register_builtin(ops::DEF_ACCESSOR, b_def_accessor);
72 vm.register_builtin(ops::DBG_LINE, b_dbg_line);
73 vm.register_builtin(ops::MKBIGINT, b_mkbigint);
74 vm.register_builtin(ops::MKREGEX, b_mkregex);
75 vm.register_builtin(ops::TAG_TMPL, b_tag_tmpl);
76 vm.register_builtin(ops::GET_ASYNC_ITER, b_get_async_iter);
77 vm.register_builtin(ops::ASYNC_STEP, b_async_step);
78 vm.register_builtin(ops::NUM_STEP, b_num_step);
79 vm.register_builtin(ops::ITER_CLOSE, b_iter_close);
80 vm.register_builtin(ops::TYPEOF_NAME, b_typeof_name);
81 vm.register_builtin(ops::SIG_BREAK, b_sig_break);
82 vm.register_builtin(ops::SIG_CONTINUE, b_sig_continue);
83 vm.register_builtin(ops::SIG_UNWIND, b_sig_unwind);
84 vm.register_builtin(ops::PUSH_SCOPE, b_push_scope);
85 vm.register_builtin(ops::POP_SCOPE, b_pop_scope);
86 vm.register_builtin(ops::COPY_SCOPE, b_copy_scope);
87 vm.register_builtin(ops::DECLARE_VAR, b_declare_var);
88 vm.register_builtin(ops::HOIST_VAR, b_hoist_var);
89 vm.register_builtin(ops::NAMED_EVAL, b_named_eval);
90}
91
92/// `ITER_CLOSE`: close the iterator on the stack (a for-of `break`). A generator
93/// runs its pending `finally`; a user iterator object gets its `.return()` called
94/// if present; a plain materialized iterator just drops. Returns `undefined`.
95/// `IteratorClose` (7.4.9): resume a generator with a forced return so its
96/// pending `finally` runs, or invoke a user iterator's `.return()`. A value that
97/// is neither is left alone.
98pub(crate) fn close_iterator(it: &Value) -> Result<(), String> {
99 if with_host(|h| h.is_generator_val(it)) {
100 host::gen_return(it, Value::Undef)?;
101 return Ok(());
102 }
103 if matches!(with_host(|h| h.get(it).cloned()), Some(JsObj::Object(_))) {
104 if let Some(f) = with_host(|h| host::lookup_chain(h, it, "return")) {
105 if with_host(|h| host::is_callable(h, &f)) {
106 host::invoke(&f, Vec::new(), Some(it.clone()))?;
107 }
108 }
109 }
110 Ok(())
111}
112
113fn b_iter_close(vm: &mut VM, _: u8) -> Value {
114 let it = vm.pop();
115 // A `finally` may print or yield, but the loop is done either way; an error
116 // it raises still propagates.
117 match close_iterator(&it) {
118 Ok(()) => Value::Undef,
119 Err(e) => abort(vm, e),
120 }
121}
122
123/// `NUM_STEP`: the `++`/`--` core. Pops `old` and the step `tag` (`+1`/`-1`),
124/// pushes `ToNumeric(old)` (a BigInt stays a BigInt, else a Number), and returns
125/// `old ± 1` in the SAME numeric type — so `x++` on a BigInt neither coerces to
126/// Number nor throws the mix error.
127fn b_num_step(vm: &mut VM, _: u8) -> Value {
128 let old = vm.pop();
129 let tag = match vm.pop() {
130 Value::Int(n) => n,
131 Value::Float(f) => f as i64,
132 _ => 1,
133 };
134 if with_host(|h| h.is_bigint_val(&old)) {
135 let b = with_host(|h| h.as_bigint(&old)).unwrap();
136 let old_n = with_host(|h| h.new_bigint(b.clone()));
137 let new = with_host(|h| h.new_bigint(b + num_bigint::BigInt::from(tag)));
138 vm.push(old_n);
139 new
140 } else {
141 let n = with_host(|h| h.to_number(&old));
142 vm.push(Value::Float(n));
143 Value::Float(n + tag as f64)
144 }
145}
146
147/// `ASYNC_STEP`: one step of a `for await` loop — returns a Promise of the
148/// `{value, done}` record (see `host::async_step`).
149fn b_async_step(vm: &mut VM, _: u8) -> Value {
150 let iter = vm.pop();
151 let r = host::async_step(&iter);
152 finish(vm, r)
153}
154
155/// `MKBIGINT`: pop the canonical decimal digit string constant, allocate the heap
156/// BigInt. The lexer already validated the digits, so parsing cannot fail here.
157fn b_mkbigint(vm: &mut VM, _: u8) -> Value {
158 let digits = sval(&vm.pop());
159 match digits.parse::<num_bigint::BigInt>() {
160 Ok(b) => with_host(|h| h.new_bigint(b)),
161 Err(_) => abort(vm, host::type_error("invalid BigInt literal")),
162 }
163}
164
165/// `TAG_TMPL`: invoke a tagged template. The compiler emits the operands as
166/// `[tag, n, m, cooked×n, raw×n, values×m]` (see `compile_tagged_template`).
167/// Builds the `strings` array (carrying its `.raw` array) and calls
168/// `tag(strings, ...values)`.
169/// Reject a non-callable where node's scheduling entry points demand one.
170///
171/// Every one of them validates SYNCHRONOUSLY — `try { queueMicrotask(1) }
172/// catch` catches an `ERR_INVALID_ARG_TYPE` in node. Here the value was queued
173/// unchecked and the failure surfaced from the event loop instead, as an
174/// uncaught `1 is not a function` that killed the process past any `try` around
175/// the call.
176fn require_callback(cb: &Value) -> Result<(), String> {
177 if with_host(|h| host::is_callable(h, cb)) {
178 return Ok(());
179 }
180 Err(host::invalid_arg_type(
181 "callback", "argument", "function", cb,
182 ))
183}
184
185fn b_tag_tmpl(vm: &mut VM, argc: u8) -> Value {
186 // The chunk holding this site, read before the operands are popped and
187 // before any host borrow: together with the compiler's per-site ordinal it
188 // names the Parse Node whose template object 13.2.8.4 caches.
189 let chunk = vm.chunk.op_hash;
190 let mut all = pop_n(vm, argc as usize);
191 let int_of = |v: &Value| match v {
192 Value::Int(n) => *n as usize,
193 Value::Float(f) => *f as usize,
194 _ => 0,
195 };
196 let this = all.remove(0);
197 let tag = all.remove(0);
198 let n = int_of(&all.remove(0));
199 let mcount = int_of(&all.remove(0));
200 let site = int_of(&all.remove(0)) as u64;
201 let cooked: Vec<Value> = all.drain(0..n.min(all.len())).collect();
202 let raw: Vec<Value> = all.drain(0..n.min(all.len())).collect();
203 let values: Vec<Value> = all.drain(0..mcount.min(all.len())).collect();
204 // GetTemplateObject caches by Parse Node, so a site evaluated twice hands
205 // back the SAME object — the whole point of the caching, since a tag that
206 // memoizes on the strings array (lit-html, graphql-tag) re-parses its
207 // template on every call without it.
208 let key = (chunk, site);
209 let strings = match with_host(|h| h.template_object(key)) {
210 Some(cached) => cached,
211 None => {
212 // strings = cooked array; strings.raw = raw array.
213 let strings = with_host(|h| h.new_array(cooked));
214 let raw_arr = with_host(|h| h.new_array(raw));
215 // `raw` is an own property that is neither writable, enumerable, nor
216 // configurable, so it stays out of `Object.keys(strings)` while
217 // `getOwnPropertyNames` still reports it.
218 with_host(|h| {
219 h.set_fn_prop(&strings, "raw", raw_arr.clone());
220 h.set_prop_attrs(
221 &strings,
222 "raw",
223 host::PropAttrs {
224 writable: false,
225 enumerable: false,
226 configurable: false,
227 },
228 );
229 // Steps 12-13 run SetIntegrityLevel(frozen) on the raw array and
230 // then on the template object itself. Without them a tag could
231 // write through its own strings array and corrupt every later
232 // evaluation of the site — which is exactly what caching makes
233 // reachable, so the freeze and the cache belong together.
234 h.seal_object(&raw_arr, true);
235 h.seal_object(&strings, true);
236 h.set_template_object(key, strings.clone());
237 });
238 strings
239 }
240 };
241 let mut call_args = vec![strings];
242 call_args.extend(values);
243 let this = match this {
244 Value::Undef => None,
245 v => Some(v),
246 };
247 let r = host::invoke(&tag, call_args, this);
248 finish(vm, r)
249}
250
251/// `GET_ASYNC_ITER`: obtain an async iterator for `for await (… of …)`. If the
252/// value has a `Symbol.asyncIterator`, use it; otherwise fall back to its sync
253/// iterator (each yielded value is awaited). Returns the iterator object/handle.
254fn b_get_async_iter(vm: &mut VM, _: u8) -> Value {
255 let src = vm.pop();
256 let r = host::get_async_iterator(&src).map_err(|e| {
257 // `for await` names the source AND says ASYNC: `for await (const x of
258 // o)` is `o is not async iterable`. Built here rather than through
259 // `name_call_site`, whose suffix table has no entry that composes.
260 match host::call_site_text(vm) {
261 Some(t) if e.ends_with(" is not iterable") => {
262 host::type_error(&format!("{t} is not async iterable"))
263 }
264 _ => e,
265 }
266 });
267 finish(vm, r)
268}
269
270/// `MKREGEX`: pop `(pattern, flags)`, translate the JS pattern to a Rust `regex`,
271/// and allocate a `RegExp`. A pattern using a JS feature Rust `regex` cannot
272/// express (backreference/lookaround) throws a `SyntaxError` here.
273fn b_mkregex(vm: &mut VM, _: u8) -> Value {
274 let flags = sval(&vm.pop());
275 let pattern = sval(&vm.pop());
276 match crate::regexp::build_regexp(&pattern, &flags) {
277 Ok(v) => v,
278 Err(e) => abort(vm, e),
279 }
280}
281
282/// DAP per-statement marker (`node --dap` only; the compiler emits this before
283/// each statement under `debug`). Pops the source line pushed by the preceding
284/// `LoadInt` and fires the debugger line hook, which pauses at breakpoints/step
285/// targets. Returns `undefined` (the compiler pops it). A no-op unless a debug
286/// session is active.
287fn b_dbg_line(vm: &mut VM, _: u8) -> Value {
288 let line = match vm.pop() {
289 Value::Int(n) => n as u32,
290 _ => 0,
291 };
292 crate::dap::on_debug_line(line);
293 Value::Undef
294}
295
296/// Install an object-literal getter/setter on an object (`kind` is `member::GET`
297/// or `member::SET`). Keeps the object on the stack.
298fn b_def_accessor(vm: &mut VM, _: u8) -> Value {
299 let func = vm.pop();
300 let kind = match vm.pop() {
301 Value::Int(n) => n,
302 _ => 0,
303 };
304 let name = sval(&vm.pop());
305 let obj = vm.pop();
306 with_host(|h| {
307 if kind == host::member::SET {
308 h.set_accessor(&obj, &name, None, Some(func));
309 } else {
310 h.set_accessor(&obj, &name, Some(func), None);
311 }
312 });
313 obj
314}
315
316fn b_await(vm: &mut VM, _: u8) -> Value {
317 let v = vm.pop();
318 match host::await_value(v) {
319 Ok(r) => r,
320 Err(e) => abort(vm, e),
321 }
322}
323
324// ── classes / super / generators / property keys (compiler-emitted ops) ──────
325
326fn b_mkclass(vm: &mut VM, argc: u8) -> Value {
327 // The fourth argument, when present, is the FuncDef carrying the class's
328 // source span.
329 let source_def = match argc {
330 4 => match vm.pop() {
331 Value::Int(n) => Some(n as usize),
332 _ => None,
333 },
334 _ => None,
335 };
336 let ctor = vm.pop();
337 let parent = vm.pop();
338 let name = sval(&vm.pop());
339 host::build_class(&name, parent, ctor, source_def)
340}
341
342fn b_def_member(vm: &mut VM, _: u8) -> Value {
343 let func = vm.pop();
344 let is_static = matches!(vm.pop(), Value::Bool(true));
345 let kind = match vm.pop() {
346 Value::Int(n) => n,
347 _ => 0,
348 };
349 let name = sval(&vm.pop());
350 let class_val = vm.pop();
351 host::define_member(&class_val, &name, kind, is_static, func);
352 class_val
353}
354
355fn b_def_field(vm: &mut VM, _: u8) -> Value {
356 // `name_anon`: the initializer was an anonymous function definition, so
357 // 15.7.10 NamedEvaluation names its result after the field. Syntactic —
358 // decided by the compiler, not re-derived from the produced value.
359 let name_anon = matches!(vm.pop(), Value::Bool(true));
360 let thunk = vm.pop();
361 let name = sval(&vm.pop());
362 let class_val = vm.pop();
363 host::define_field(&class_val, &name, thunk, name_anon);
364 class_val
365}
366
367/// `super(...args)` in a derived constructor: run the parent constructor on the
368/// current `this`, then this class's field initializers.
369/// `SUPER_CALL_SPREAD` — `super(...xs)`, where the argument list is built at
370/// run time. Shares everything below with the fixed-arity form; only where the
371/// arguments come from differs.
372fn b_super_call_spread(vm: &mut VM, _: u8) -> Value {
373 let arr = vm.pop();
374 let args = host::iter_all(&arr).unwrap_or_default();
375 super_call_with(vm, args)
376}
377
378fn b_super_call(vm: &mut VM, argc: u8) -> Value {
379 let args = pop_n(vm, argc as usize);
380 super_call_with(vm, args)
381}
382
383fn super_call_with(vm: &mut VM, args: Vec<Value>) -> Value {
384 let this = with_host(|h| h.current_this());
385 let this = match this {
386 Some(t) => t,
387 None => return abort(vm, host::type_error("'super' keyword unexpected here")),
388 };
389 // The class whose constructor is running = the running method's home class.
390 let (parent, fields) = with_host(|h| h.super_context());
391 let (parent, fields) = match parent {
392 Some(p) => (p, fields),
393 None => return abort(vm, host::type_error("'super' keyword unexpected here")),
394 };
395 let nt = with_host(|h| h.current_new_target()).unwrap_or_else(|| this.clone());
396 let this = match host::super_construct(&parent, args, &this, &nt) {
397 Err(e) => return abort(vm, e),
398 // The parent returned an object of its own: 15.7.15 makes THAT the
399 // instance, so `this` is rebound to it for the rest of the constructor
400 // and it is what `new` hands back.
401 Ok(Some(replacement)) => {
402 with_host(|h| h.set_current_this(replacement.clone()));
403 replacement
404 }
405 Ok(None) => this,
406 };
407 if !with_host(|h| h.bind_super_this()) {
408 return abort(
409 vm,
410 "ReferenceError: Super constructor may only be called once".to_string(),
411 );
412 }
413 // Run this (derived) class's own instance-field initializers after super.
414 for (name, thunk, name_anon) in fields {
415 if let Err(e) = host::init_one_field(&this, &name, &thunk, name_anon) {
416 return abort(vm, e);
417 }
418 }
419 Value::Undef
420}
421
422/// `super.name` — a method from the parent's prototype, or a getter's result.
423fn b_super_get(vm: &mut VM, _: u8) -> Value {
424 let name = sval(&vm.pop());
425 match with_host(|h| h.super_resolve(&name)) {
426 host::SuperRef::Data(v) => v,
427 host::SuperRef::Getter(getter) => {
428 let this = with_host(|h| h.current_this());
429 match host::invoke(&getter, Vec::new(), this) {
430 Ok(v) => v,
431 Err(e) => abort(vm, e),
432 }
433 }
434 }
435}
436
437/// Close every loop iterator parked on `vm`'s stack at the op now executing,
438/// innermost first. Called where a chunk is about to be halted abruptly, since
439/// the code that would ordinarily close them is being jumped over.
440///
441/// A close runs user code (a generator's `finally`), which can itself throw; the
442/// error is deliberately dropped, because it must not replace the completion
443/// that caused the unwind.
444fn close_parked_iters(vm: &mut VM) {
445 let n = host::parked_iters(vm);
446 if n == 0 {
447 return;
448 }
449 // The completion that caused the unwind is already pending on the host.
450 // Closing an iterator resumes ANOTHER generator, which settles its own
451 // signal/error state, so the pending one is saved across the close and put
452 // back — otherwise the outer `.return()` would be lost.
453 let saved = with_host(|h| (h.signal.take(), h.error.take()));
454 for _ in 0..n {
455 let it = vm.pop();
456 let _ = close_iterator(&it);
457 }
458 with_host(|h| {
459 h.signal = saved.0;
460 h.error = saved.1;
461 });
462}
463
464fn b_yield(vm: &mut VM, _: u8) -> Value {
465 let v = vm.pop();
466 match host::gen_yield(v) {
467 Ok(sent) => {
468 // A `.return()`/`.throw()` injected on resume sets a pending Return
469 // signal (or error); halt the chunk so the body unwinds through any
470 // enclosing `try/finally`, exactly like a source `return`/`throw`.
471 if with_host(|h| h.error.is_some() || h.signal.is_some()) {
472 // Halting jumps past the loop exits, so the `for…of` / `yield*`
473 // iterators parked on this chunk's stack would be abandoned
474 // still-suspended. They sit directly beneath the yielded value
475 // (innermost last), and the compiler recorded how many are
476 // there for this exact op.
477 close_parked_iters(vm);
478 vm.ip = vm.chunk.ops.len();
479 }
480 sent
481 }
482 // An injected `.throw()` comes back as an error rather than a signal,
483 // and abandons the parked iterators the same way. The thrown value is
484 // already on the host as `exc`; `close_parked_iters` puts back whatever
485 // it saves, so the close cannot swallow it.
486 Err(e) => {
487 close_parked_iters(vm);
488 abort(vm, e)
489 }
490 }
491}
492
493/// `PROPKEY` — ToPropertyKey (7.1.19) for an object literal's COMPUTED key.
494///
495/// It called `JsHost::property_key` directly, which is the primitive-only half
496/// of the conversion, so an object key never ran `ToPrimitive`:
497/// `{ [{toString(){return "TS"}}]: 1 }` keyed on `"[object Object]"` while the
498/// member form `a[o] = 1` — which does go through `host::to_property_key` —
499/// keyed on `"TS"`. The two forms are the same abstract operation and now share
500/// the same implementation.
501fn b_propkey(vm: &mut VM, _: u8) -> Value {
502 let v = vm.pop();
503 match host::to_property_key(&v) {
504 Ok(k) => with_host(|h| h.new_str(k)),
505 Err(e) => abort(vm, e),
506 }
507}
508
509fn b_new_target(_vm: &mut VM, _: u8) -> Value {
510 with_host(|h| h.current_new_target().unwrap_or(Value::Undef))
511}
512
513/// `a / b` with JS/IEEE-754 semantics. fusevm's native `Op::Div` returns `Undef`
514/// for a zero divisor (so a frontend whose `/` differs must lower to a builtin —
515/// its own documented guidance), but JavaScript requires `x/0 === ±Infinity` and
516/// `0/0 === NaN`, so `/` is lowered here instead.
517///
518/// Being a builtin rather than a native op means it does NOT reach the numeric
519/// hook, so `/` was the one arithmetic operator that never ran `ToPrimitive`:
520/// `({valueOf(){return 7}}) / 2` was `NaN` where every other operator gave
521/// `3.5`, and `new Date(2) / 1` was `NaN` instead of `2`. It goes through the
522/// hook now, so `/` coerces exactly as `*` and `-` do.
523fn b_div(vm: &mut VM, _: u8) -> Value {
524 let b = vm.pop();
525 let a = vm.pop();
526 let r = numeric_hook(NumOp::Div, &a, &b);
527 finish(vm, r)
528}
529
530/// `a ** b`. Same reason `/` is a builtin: fusevm's native `Op::Pow` is IEEE-754
531/// `pow`, which returns 1 for `(-1) ** Infinity` and for `1 ** NaN` where the
532/// spec says NaN. Routing through the numeric hook also keeps BigInt `**` on the
533/// one code path that already handles it.
534fn b_pow(vm: &mut VM, _: u8) -> Value {
535 let b = vm.pop();
536 let a = vm.pop();
537 let r = numeric_hook(NumOp::Pow, &a, &b);
538 finish(vm, r)
539}
540
541/// `{ ...rest } = obj`: a new object of `obj`'s own keys minus the excluded set.
542fn b_obj_rest(vm: &mut VM, _: u8) -> Value {
543 let excluded = vm.pop();
544 let obj = vm.pop();
545 // The excluded keys are normalized exactly as a property READ normalizes
546 // them, not merely stringified: a symbol key lives on the object under its
547 // internal `@@sym:<id>` spelling, and `str_of` renders it `Symbol(k)`, which
548 // matches no key at all — so `const { [sym]: v, ...rest } = o` left the
549 // symbol-keyed property in `rest`.
550 let excl: Vec<String> = with_host(|h| h.iter_vec(&excluded))
551 .unwrap_or_default()
552 .iter()
553 .filter_map(|v| host::to_property_key(v).ok())
554 .collect();
555 // CopyDataProperties (ECMA-262 7.3.25) copies the own ENUMERABLE keys,
556 // symbol-keyed ones included. `own_enum_key_names` is what `Object.keys`
557 // uses, so an ACCESSOR is in the list — reading the property map directly
558 // missed one entirely, and `const { ...r } = { get g() {…} }` produced an
559 // object with no `g` and never ran the getter.
560 // A PROXY answers from its traps — `ownKeys`, then a
561 // `getOwnPropertyDescriptor` per key to test enumerability — which
562 // `own_enum_key_names` cannot see. Rest over one produced an empty object
563 // and ran no traps at all.
564 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
565 let keys = match crate::proxy::own_keys(&obj) {
566 Ok(k) => k.unwrap_or_default(),
567 Err(e) => return abort(vm, e),
568 };
569 let mut pairs: Vec<(String, Value)> = Vec::new();
570 for k in keys {
571 if excl.contains(&k) {
572 continue;
573 }
574 // The enumerability test and the READ interleave per key, as node's
575 // trap log shows — testing every key first and then reading them
576 // all produced the right object through the wrong trap sequence.
577 match crate::proxy::own_enumerable(&obj, &k) {
578 Ok(false) => continue,
579 Ok(true) => {}
580 Err(e) => return abort(vm, e),
581 }
582 match get_property(&obj, &k) {
583 Ok(v) => pairs.push((k, v)),
584 Err(e) => return abort(vm, e),
585 }
586 }
587 return with_host(|h| h.new_object(pairs.into_iter().collect()));
588 }
589 let keys: Vec<String> = with_host(|h| {
590 // `own_enum_key_names` is the STRING half — the same list `Object.keys`
591 // gives, so an accessor is in it. The symbol-keyed half lives in the
592 // property map under the internal `@@sym:` spelling and has to be
593 // collected separately, since `Object.keys` deliberately omits it.
594 let mut ks = h.own_enum_key_names(&obj);
595 if let Some(JsObj::Object(m)) = h.get(&obj) {
596 for k in m.keys() {
597 if host::is_symbol_key(k) && h.prop_attrs(&obj, k).enumerable {
598 ks.push(k.clone());
599 }
600 }
601 }
602 ks
603 })
604 .into_iter()
605 .filter(|k| {
606 // An internal slot (`@@native`, `@@bytes`, …) or a private class field
607 // is not a property; a SYMBOL key shares the `@@` prefix but is one, so
608 // the two cases cannot be told apart by the prefix alone.
609 !excl.contains(k)
610 && (host::is_symbol_key(k) || !(k.starts_with("@@") || k.starts_with('#')))
611 })
612 .collect();
613 // Each value is read through `[[Get]]`, OUTSIDE the host borrow: a getter is
614 // user code and re-entering the VM under the borrow aborts the process.
615 let mut pairs: Vec<(String, Value)> = Vec::with_capacity(keys.len());
616 for k in keys {
617 match get_property(&obj, &k) {
618 Ok(v) => pairs.push((k, v)),
619 Err(e) => return abort(vm, e),
620 }
621 }
622 with_host(|h| {
623 let props: IndexMap<String, Value> = pairs.into_iter().collect();
624 h.new_object(props)
625 })
626}
627
628// ── helpers ──────────────────────────────────────────────────────────────────
629
630fn pop_n(vm: &mut VM, n: usize) -> Vec<Value> {
631 let mut v = Vec::with_capacity(n);
632 for _ in 0..n {
633 v.push(vm.pop());
634 }
635 v.reverse();
636 v
637}
638
639/// Read a compiler-internal name string (native `Value::Str` or heap `str`).
640fn sval(v: &Value) -> String {
641 if let Value::Str(s) = v {
642 return (**s).clone();
643 }
644 with_host(|h| h.as_str(v)).unwrap_or_default()
645}
646
647/// The same string, without `sval`'s deep copy. Every identifier the compiler
648/// emits is a `Value::Str` constant, so a variable read or write that went
649/// through `sval` heap-allocated and memcpy'd the NAME once per access — on the
650/// hot path of every loop. `Value::Str` is an `Arc<String>`, so cloning the
651/// handle is a refcount bump instead.
652fn sname(v: &Value) -> std::sync::Arc<String> {
653 match v {
654 Value::Str(s) => s.clone(),
655 _ => std::sync::Arc::new(sval(v)),
656 }
657}
658
659fn abort(vm: &mut VM, e: String) -> Value {
660 with_host(|h| h.error = Some(e));
661 vm.ip = vm.chunk.ops.len();
662 Value::Undef
663}
664
665/// Halt the chunk if a call left an error or non-local signal pending.
666fn finish(vm: &mut VM, r: Result<Value, String>) -> Value {
667 match r {
668 Ok(v) => {
669 if with_host(|h| h.error.is_some() || h.signal.is_some()) {
670 vm.ip = vm.chunk.ops.len();
671 }
672 v
673 }
674 Err(e) => abort(vm, e),
675 }
676}
677
678// ── name handlers ─────────────────────────────────────────────────────────────
679
680/// The value a bare global identifier resolves to, or `None` if unbound.
681///
682/// Shared by `b_getlocal` (the `x` form) and the `globalThis.x` property read,
683/// which must agree: a name reachable one way and not the other is exactly the
684/// discrepancy that left `globalThis.process` undefined while `process` worked.
685pub(crate) fn global_binding(name: &str) -> Option<Value> {
686 global_binding_from(name, false)
687}
688
689/// [`global_binding`] restricted to what the GLOBAL OBJECT really holds.
690///
691/// A `globalThis.x` read falls back to the same lazy binding a bare `x` gets,
692/// which is what makes `globalThis.Math` and `globalThis.process` work — but
693/// the bare-identifier lookup walks the SCOPE CHAIN, so while any function was
694/// running its locals were readable off `globalThis`: `function f() { let zzq =
695/// 2; return typeof globalThis.zzq }` answered for a name the global object has
696/// never heard of. Only the globals map and the lazy builtins below may answer
697/// here.
698pub(crate) fn global_object_binding(name: &str) -> Option<Value> {
699 global_binding_from(name, true)
700}
701
702fn global_binding_from(name: &str, object_only: bool) -> Option<Value> {
703 let bound = with_host(|h| {
704 if object_only {
705 h.read_global(name)
706 } else {
707 h.read_name(name)
708 }
709 });
710 if let Some(v) = bound {
711 return Some(v);
712 }
713 // Globals bound lazily: numeric sentinels + builtin namespaces.
714 match name {
715 "undefined" => return Some(Value::Undef),
716 "NaN" => return Some(Value::Float(f64::NAN)),
717 "Infinity" => return Some(Value::Float(f64::INFINITY)),
718 // One object, not a fresh one per read: `globalThis === globalThis` is
719 // `true` in JS, and `globalThis.x = 1` is readable back as
720 // `globalThis.x`. Both were false while each read minted a new object.
721 // `global` is Node's alias for the same object.
722 "globalThis" | "global" => return Some(with_host(|h| h.global_object())),
723 // The WHATWG `crypto` global IS `require('crypto').webcrypto`, not the
724 // node-flavoured module: `globalThis.crypto.randomUUID` exists while
725 // `globalThis.crypto.createHash` does not.
726 "crypto" => return Some(with_host(|h| h.alloc(JsObj::Builtin("webcrypto".into())))),
727 _ => {}
728 }
729 if is_namespace(name) || is_known_builtin(name) {
730 return Some(with_host(|h| h.alloc(JsObj::Builtin(name.to_string()))));
731 }
732 None
733}
734
735fn b_getlocal(vm: &mut VM, _: u8) -> Value {
736 let name = sname(&vm.pop());
737 // A module-top-level dead zone is tracked by NAME rather than by a parked
738 // marker, so that the marker is never reachable as `globalThis.<name>`. It
739 // only applies when nothing on the scope chain SHADOWS the name — a class's
740 // own inner binding for its name does exactly that while its static
741 // initializers run.
742 if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
743 return abort(vm, host::tdz_error(&name));
744 }
745 match global_binding(&name) {
746 // The binding EXISTS but has not reached its declaration yet.
747 Some(v) if with_host(|h| h.is_tdz(&v)) => abort(vm, host::tdz_error(&name)),
748 Some(v) => v,
749 None => abort(vm, host::ref_error(&name)),
750 }
751}
752
753/// `HOIST_TDZ` — declare one `let`/`const`/`class` name as uninitialized at the
754/// top of the scope that declares it.
755fn b_hoist_tdz(vm: &mut VM, _: u8) -> Value {
756 let name = sname(&vm.pop());
757 with_host(|h| h.hoist_tdz(&name));
758 Value::Undef
759}
760
761/// The three global VALUE properties that are `{writable: false}` (19.1.1-19.1.3).
762/// Assigning to one is a silent no-op in sloppy code and a `TypeError` in strict
763/// code — and, either way, never rebinds the name.
764const READONLY_GLOBALS: [&str; 3] = ["undefined", "NaN", "Infinity"];
765
766fn readonly_global_error(name: &str) -> String {
767 host::type_error(&format!(
768 "Cannot assign to read only property '{name}' of object '#<Object>'"
769 ))
770}
771
772fn b_setlocal(vm: &mut VM, _: u8) -> Value {
773 let val = vm.pop();
774 let name = sname(&vm.pop());
775 // Sloppy assignment to a non-writable global is DISCARDED, not applied:
776 // `undefined = 1` used to rebind the name and make every later `undefined`
777 // read back as `1`.
778 if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
779 return val;
780 }
781 // Assigning to a binding still in its temporal dead zone throws too —
782 // `{ x = 1; let x }` is a ReferenceError, not an initialization.
783 if with_host(|h| match h.read_name(&name) {
784 Some(v) => h.is_tdz(&v),
785 None => h.is_tdz_global(&name),
786 }) {
787 return abort(vm, host::tdz_error(&name));
788 }
789 // An assignment to a `const` binding throws (8.5.2 SetMutableBinding on an
790 // immutable binding). This used to succeed silently.
791 if !with_host(|h| h.set_name(&name, val.clone())) {
792 return abort(vm, host::type_error("Assignment to constant variable."));
793 }
794 val
795}
796
797/// Strict-mode `x = v` (6.2.5.6 `PutValue` with an unresolvable reference):
798/// where sloppy code silently creates a global, strict code throws
799/// `ReferenceError: x is not defined`.
800///
801/// A separate opcode rather than a runtime flag: strictness is a static property
802/// of the code, so the compiler already knows which of the two an assignment is
803/// and sloppy code — everything in a CommonJS module without the directive —
804/// keeps the exact instruction it had.
805fn b_setlocal_strict(vm: &mut VM, _: u8) -> Value {
806 let val = vm.pop();
807 let name = sname(&vm.pop());
808 if !binding_exists(&name) {
809 return abort(vm, host::ref_error(&name));
810 }
811 if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
812 return abort(vm, readonly_global_error(&name));
813 }
814 if !with_host(|h| h.set_name(&name, val.clone())) {
815 return abort(vm, host::type_error("Assignment to constant variable."));
816 }
817 val
818}
819
820/// Whether `name` resolves to anything — a scope binding, a global, or a lazily
821/// materialised builtin namespace. `global_binding` answers the same question
822/// but ALLOCATES the namespace object to do it, which an assignment then throws
823/// away.
824fn binding_exists(name: &str) -> bool {
825 if with_host(|h| h.has_name(name)) {
826 return true;
827 }
828 matches!(
829 name,
830 "undefined" | "NaN" | "Infinity" | "globalThis" | "global"
831 ) || is_namespace(name)
832 || is_known_builtin(name)
833}
834
835fn b_declare(vm: &mut VM, _: u8) -> Value {
836 let val = vm.pop();
837 let name = sname(&vm.pop());
838 with_host(|h| h.declare_name(&name, val.clone()));
839 val
840}
841
842/// `const x = …`: like `DECLARE`, but the binding is immutable, so a later
843/// assignment to the name throws instead of overwriting it.
844fn b_declare_const(vm: &mut VM, _: u8) -> Value {
845 let val = vm.pop();
846 let name = sname(&vm.pop());
847 with_host(|h| h.declare_const_name(&name, val.clone()));
848 val
849}
850
851/// `var x = …` / a hoisted `function f(){}`: bind at function scope, skipping any
852/// open block scopes, so the name outlives the block it was written in.
853/// `var` hoisting: create the binding as `undefined` unless it already exists.
854fn b_hoist_var(vm: &mut VM, _: u8) -> Value {
855 let name = sname(&vm.pop());
856 with_host(|h| h.hoist_var_name(&name));
857 Value::Undef
858}
859
860fn b_declare_var(vm: &mut VM, _: u8) -> Value {
861 let val = vm.pop();
862 let name = sname(&vm.pop());
863 with_host(|h| h.declare_var_name(&name, val.clone()));
864 val
865}
866
867fn b_push_scope(_: &mut VM, _: u8) -> Value {
868 with_host(|h| h.push_scope());
869 Value::Undef
870}
871
872fn b_pop_scope(_: &mut VM, _: u8) -> Value {
873 with_host(|h| h.pop_scope());
874 Value::Undef
875}
876
877fn b_copy_scope(_: &mut VM, _: u8) -> Value {
878 with_host(|h| h.copy_scope());
879 Value::Undef
880}
881
882fn b_delname(vm: &mut VM, _: u8) -> Value {
883 let name = sval(&vm.pop());
884 with_host(|h| h.del_name(&name));
885 Value::Bool(true)
886}
887
888fn b_this(vm: &mut VM, _: u8) -> Value {
889 if with_host(|h| h.this_state()) == host::ThisState::Pending {
890 return abort(vm, host::this_before_super_error());
891 }
892 with_host(|h| h.current_this().unwrap_or(Value::Undef))
893}
894
895fn b_load_null(_vm: &mut VM, _: u8) -> Value {
896 with_host(|h| h.null())
897}
898
899// ── attribute / item handlers ─────────────────────────────────────────────────
900
901fn b_getattr(vm: &mut VM, _: u8) -> Value {
902 let name = sval(&vm.pop());
903 let recv = vm.pop();
904 match get_property(&recv, &name) {
905 Ok(v) => v,
906 Err(e) => abort(vm, e),
907 }
908}
909
910/// Read `recv.name` (also the computed-key path for string keys). Walks own
911/// properties, accessors, and the prototype chain (class methods / getters).
912/// Read one small piece out of `recv`'s heap cell under a short borrow.
913///
914/// The closure must not call back into the host (`with_host` is a `RefCell`
915/// borrow and re-entering panics) — which is exactly why it hands back only the
916/// value needed: the caller re-enters freely afterwards. This replaces the old
917/// `h.get(recv).cloned()` habit, which deep-copied a whole `Vec`/`IndexMap`/
918/// `String` just to look at it.
919fn peek<R>(recv: &Value, f: impl FnOnce(&JsObj) -> Option<R>) -> Option<R> {
920 with_host(|h| h.get(recv).and_then(f))
921}
922
923/// The nearest `[[Prototype]]` link of `recv` that is a Proxy, when the chain
924/// reaches it without a closer link already owning `name`.
925///
926/// A proxy prototype answers only from the position it occupies in the chain: a
927/// nearer prototype that owns the key (as a data property or an accessor) still
928/// wins, exactly as `OrdinaryGet` walks one link at a time.
929pub(crate) fn proxy_proto_link(recv: &Value, name: &str) -> Option<Value> {
930 with_host(|h| {
931 let mut cur = h.proto_of(recv);
932 for _ in 0..100 {
933 let p = cur?;
934 match h.get(&p) {
935 Some(JsObj::Proxy { .. }) => return Some(p),
936 Some(JsObj::Object(props)) if props.contains_key(name) => return None,
937 _ => {}
938 }
939 if h.own_accessor(&p, name).is_some() {
940 return None;
941 }
942 cur = h.proto_of(&p);
943 }
944 None
945 })
946}
947
948/// The CommonJS wrapper's parameters. They are function locals in Node, not
949/// global-object properties, so `globalThis.require` is `undefined` and
950/// `Object.getOwnPropertyDescriptor(globalThis, 'module')` reports no property —
951/// even though the bare `require` and `module` both work.
952const CJS_WRAPPER_LOCALS: &[&str] = &[
953 "require",
954 "module",
955 "exports",
956 "__filename",
957 "__dirname",
958 "__cjs_require",
959 "__cjs_resolve",
960];
961
962/// The globals node exposes as ENUMERABLE own properties of the global object —
963/// the timer family and the WHATWG additions, measured on v26.8.1. Everything
964/// else (`Math`, `parseInt`, the constructors) is non-enumerable.
965const ENUMERABLE_GLOBALS: &[&str] = &[
966 "global",
967 "clearImmediate",
968 "setImmediate",
969 "clearInterval",
970 "clearTimeout",
971 "setInterval",
972 "setTimeout",
973 "queueMicrotask",
974 "structuredClone",
975 "atob",
976 "btoa",
977 "performance",
978 "fetch",
979 "crypto",
980 "navigator",
981 "sessionStorage",
982];
983
984pub fn get_property(recv: &Value, name: &str) -> Result<Value, String> {
985 // A `#`-prefixed key is a PRIVATE name. `[[PrivateGet]]` (7.3.31) throws
986 // when the receiver carries no such private element — it does NOT read back
987 // as `undefined`, which is what `C.prototype.method.call({})` used to do.
988 if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
989 return Err(private_brand_message(name, false));
990 }
991 get_property_recv(recv, name, recv)
992}
993
994/// The `TypeError` a failed private brand check raises. Node words it two ways:
995/// a private METHOD or accessor names the class the receiver should have been an
996/// instance of, while a private FIELD names the member.
997pub fn private_brand_message(name: &str, writing: bool) -> String {
998 if with_host(|h| h.is_private_method(name)) {
999 if let Some(class) = with_host(|h| h.current_home_class_name()) {
1000 return host::type_error(&format!("Receiver must be an instance of class {class}"));
1001 }
1002 }
1003 let verb = if writing { "write" } else { "read" };
1004 let prep = if writing { "to" } else { "from" };
1005 host::type_error(&format!(
1006 "Cannot {verb} private member {name} {prep} an object whose class did not declare it"
1007 ))
1008}
1009
1010/// `[[Get]](name, receiver)` — 10.1.8. `receiver` is the object the read STARTED
1011/// from and is what a getter sees as `this`; it differs from `recv` only when the
1012/// read was forwarded down a prototype chain, which is why `Reflect.get(t, k, r)`
1013/// and a Proxy `get` trap's third argument both need it. Every ordinary read
1014/// passes `recv` itself.
1015/// Re-format an error's `.stack` header on its first read, the way V8 does.
1016///
1017/// The constructor could only stamp the name it was called with, so a subclass
1018/// that sets `this.name` after `super()` — or any `e.name = …` / `e.message = …`
1019/// before the first read — left a stale header. Node re-reads both properties at
1020/// format time, including one inherited from the prototype (`E.prototype.name`).
1021///
1022/// It is formatted ONCE: node caches the string, so renaming AFTER a read does
1023/// not change what later reads return. `@@stackRaw` is the not-yet-formatted
1024/// marker and is dropped here; an explicit `e.stack = …` drops it too, so an
1025/// assignment is never clobbered by a later read.
1026/// The key of node's DEFAULT `Error.prepareStackTrace`. Recognised by name so
1027/// the ordinary stack path can skip the hook round-trip when nothing custom is
1028/// installed.
1029pub const DEFAULT_PREPARE: &str = "ErrorPrepareStackTrace";
1030
1031pub fn materialize_stack(recv: &Value) {
1032 let Some(frames) = with_host(|h| match h.get(recv) {
1033 Some(JsObj::Object(p)) => p.get("@@stackRaw").cloned(),
1034 _ => None,
1035 }) else {
1036 return;
1037 };
1038 // A custom `Error.prepareStackTrace` replaces the string entirely (V8's
1039 // stack-introspection hook, which every source-map library installs). It was
1040 // honoured only by `Error.captureStackTrace`, so an ordinary `err.stack`
1041 // read bypassed it and handed back the default text.
1042 let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
1043 if let Some(f) = prep.filter(|f| {
1044 // The default hook produces exactly what the fast path below produces,
1045 // so it is skipped rather than called.
1046 !matches!(
1047 with_host(|h| h.get(f).cloned()),
1048 Some(JsObj::Builtin(ref n)) if n == DEFAULT_PREPARE
1049 ) && matches!(
1050 with_host(|h| h.get(f).cloned()),
1051 Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
1052 )
1053 }) {
1054 // Clear the raw marker FIRST: the hook may read `.stack` itself, and a
1055 // second materialization would re-enter this path forever.
1056 with_host(|h| {
1057 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1058 p.shift_remove("@@stackRaw");
1059 }
1060 });
1061 let limit = with_host(|h| h.stack_trace_limit());
1062 if let Ok(sites) = crate::module::callsite_stack(limit) {
1063 if let Ok(out) = host::invoke(&f, vec![recv.clone(), sites], None) {
1064 with_host(|h| {
1065 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1066 p.insert("stack".into(), out);
1067 }
1068 });
1069 return;
1070 }
1071 }
1072 }
1073 with_host(|h| {
1074 let frames = h.str_of(&frames);
1075 let name = host::lookup_chain(h, recv, "name")
1076 .map(|v| h.str_of(&v))
1077 .unwrap_or_else(|| "Error".to_string());
1078 let message = host::lookup_chain(h, recv, "message")
1079 .map(|v| h.str_of(&v))
1080 .unwrap_or_default();
1081 let header = if message.is_empty() {
1082 name
1083 } else {
1084 format!("{name}: {message}")
1085 };
1086 let sv = h.new_str(format!("{header}{frames}"));
1087 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1088 p.insert("stack".into(), sv);
1089 p.shift_remove("@@stackRaw");
1090 }
1091 });
1092}
1093
1094pub fn get_property_recv(recv: &Value, name: &str, receiver: &Value) -> Result<Value, String> {
1095 // `[[Get]]` on a Proxy: the handler's `get` trap, or a forward to the
1096 // target. Checked before anything else so no ordinary-object shortcut can
1097 // read past the handler.
1098 if let Some(v) = crate::proxy::get(recv, name, receiver)? {
1099 return Ok(v);
1100 }
1101 if with_host(|h| h.is_nullish(recv)) {
1102 return Err(host::type_error(&format!(
1103 "Cannot read properties of {} (reading '{name}')",
1104 with_host(|h| h.str_of(recv))
1105 )));
1106 }
1107 if name == "stack" {
1108 materialize_stack(recv);
1109 }
1110 // A `DOMException`'s `name`/`message`/`code` are prototype accessors over
1111 // internal slots, so they resolve here rather than out of a property map.
1112 if let Some(v) = dom_exception_slot(recv, name) {
1113 return Ok(v);
1114 }
1115 // A read off `globalThis` for a name the object does not own falls back to
1116 // the same lazy global binding the bare identifier gets. Without it the
1117 // global object was an empty bag: `globalThis.process`, `.console`, `.Math`
1118 // and `.JSON` were all `undefined`, so `process === globalThis.process` was
1119 // `false` and any `globalThis.X` feature probe reported the feature missing.
1120 if with_host(|h| h.is_global_object(recv)) {
1121 let own = with_host(|h| match h.get(recv) {
1122 Some(JsObj::Object(p)) => p.contains_key(name),
1123 _ => false,
1124 });
1125 // The CommonJS wrapper's parameters are function locals in Node, not
1126 // global-object properties: `typeof globalThis.require` is `undefined`
1127 // there even though the bare `require` works.
1128 if !own && !CJS_WRAPPER_LOCALS.contains(&name) {
1129 if let Some(v) = global_object_binding(name) {
1130 return Ok(v);
1131 }
1132 }
1133 }
1134 // Accessor (own or inherited getter) takes precedence over the chain walk.
1135 // The getter runs with the RECEIVER as `this`, not the object that owns it.
1136 if let Some((getter, _)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
1137 return match getter {
1138 Some(g) => host::invoke(&g, Vec::new(), Some(receiver.clone())),
1139 None => Ok(Value::Undef), // set-only property reads as undefined
1140 };
1141 }
1142 // `Symbol.toStringTag` read as an ordinary property. The builtins that carry
1143 // one expose it to a plain read, not just to `Object.prototype.toString` —
1144 // `new Uint8Array(1)[Symbol.toStringTag]` is `'Uint8Array'`, and a `Buffer`
1145 // inherits `'Uint8Array'` from the typed-array prototype it now really has.
1146 // Anything the receiver's own chain provides wins (a class may define its
1147 // own getter), so this is only the fallback.
1148 if name == "@@toStringTag" && with_host(|h| host::lookup_chain(h, recv, name)).is_none() {
1149 if let Some(tag) = with_host(|h| well_known_tag(h, recv)) {
1150 return Ok(with_host(|h| h.new_str(tag)));
1151 }
1152 }
1153 // `constructor`: a user class/function sets it on the prototype chain, and
1154 // that wins; otherwise every builtin instance reports its native
1155 // constructor (so `[].constructor`, `new Map().constructor`,
1156 // `Promise.resolve(1).constructor`, `(5).constructor` match Node).
1157 if name == "constructor" {
1158 if let Some(v) = with_host(|h| {
1159 match h.get(recv) {
1160 Some(JsObj::Object(p)) => p.get("constructor").cloned(),
1161 _ => None,
1162 }
1163 .or_else(|| host::lookup_chain(h, recv, "constructor"))
1164 }) {
1165 return Ok(v);
1166 }
1167 // An intrinsic prototype the receiver's CHAIN reaches owns a
1168 // `constructor` too, and it wins over the receiver's own kind:
1169 // `Object.create(Map.prototype).constructor` is `Map`, not `Object`.
1170 // Deciding from the kind alone also mis-named the receiver in every
1171 // message that renders one — the brand-check errors say `#<Map>`.
1172 if let Some(c) = chain_intrinsic_ctors(recv)
1173 .into_iter()
1174 .find(|c| is_builtin_ctor(c))
1175 {
1176 return Ok(with_host(|h| h.alloc(JsObj::Builtin(c.to_string()))));
1177 }
1178 if let Some(cn) = with_host(|h| default_ctor_name(h, recv)) {
1179 return Ok(with_host(|h| h.alloc(JsObj::Builtin(cn.to_string()))));
1180 }
1181 }
1182 // `__proto__` (Annex B B.2.2.1) is an accessor on `Object.prototype`, so it
1183 // answers for EVERY object that inherits from it, not only plain ones —
1184 // `[].__proto__` is `Array.prototype`. Only the plain-object arm handled it,
1185 // so an array, function or builtin instance read `undefined`. An object with
1186 // a null prototype inherits no such accessor and reads `undefined`, which is
1187 // why this is skipped there rather than answering `null`.
1188 if name == "__proto__"
1189 && !with_host(|h| h.has_null_proto(recv))
1190 && peek(recv, |o| match o {
1191 JsObj::Object(p) => Some(p.contains_key("__proto__")),
1192 _ => Some(false),
1193 }) != Some(true)
1194 {
1195 return Ok(prototype_of(recv));
1196 }
1197 // An ACCESSOR member read off the intrinsic prototype ITSELF is not a
1198 // method: it RUNS the getter with that prototype as `this`, and all but two
1199 // of `RegExp.prototype`'s then fail their brand check and throw. Every one
1200 // answered `undefined`, so both the value and the failure were invisible.
1201 // Both representations of a prototype reach here — the namespace handles
1202 // and the real objects (`Symbol.prototype`, `String.prototype`).
1203 if let Some(ctor) = intrinsic_proto_of(recv) {
1204 if is_proto_accessor(&ctor, name) {
1205 return proto_getter_call(&ctor, name, recv);
1206 }
1207 }
1208 let kind = with_host(|h| h.kind_of(recv));
1209 #[allow(unused_mut)]
1210 let mut out = match kind {
1211 Some(ObjKind::Object) => {
1212 let numeric = !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit());
1213 // A view over a DETACHED buffer reports zero extent. Its own
1214 // `length`/`byteLength`/`byteOffset` properties still hold the old
1215 // numbers — the buffer does not know its views, so it cannot rewrite
1216 // them — and reading them straight back made a detached view still
1217 // look eight bytes long.
1218 if matches!(name, "length" | "byteLength" | "byteOffset")
1219 && crate::stdlib::typedarray::view_detached(recv)
1220 {
1221 match crate::stdlib::native_tag(recv).as_deref() {
1222 Some("TypedArray") => return Ok(Value::Float(0.0)),
1223 // A DataView THROWS where a typed array answers zero — its
1224 // extent accessors are brand-checked and node reports the
1225 // getter by name.
1226 Some("DataView") => {
1227 return Err(crate::stdlib::typedarray::detached_error(
1228 "get DataView.prototype",
1229 name,
1230 false,
1231 ))
1232 }
1233 _ => {}
1234 }
1235 }
1236 // Typed-array element read (`ta[i]`): elements live in a hidden
1237 // `@@elems`, not as own numeric props, so intercept integer keys.
1238 if numeric && crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray") {
1239 if let Some(v) = crate::stdlib::typedarray::elem_get(recv, name) {
1240 return Ok(v);
1241 }
1242 }
1243 // `buf[i]`: a Buffer's bytes live in a hidden `@@bytes` array, not as
1244 // own numeric props, so integer keys read through to it.
1245 if numeric
1246 && peek(recv, |o| match o {
1247 JsObj::Object(p) => Some(p.contains_key("@@bytes")),
1248 _ => None,
1249 })
1250 .unwrap_or(false)
1251 {
1252 return Ok(crate::stdlib::buffer::byte_get(recv, name));
1253 }
1254 if let Some(v) = peek(recv, |o| match o {
1255 JsObj::Object(p) => p.get(name).cloned(),
1256 _ => None,
1257 }) {
1258 v
1259 } else if let Some(link) = proxy_proto_link(recv, name) {
1260 // A Proxy sitting in the prototype chain. `OrdinaryGet` (10.1.8.1
1261 // step 4) forwards to the parent's `[[Get]]` with the ORIGINAL
1262 // receiver, so the trap sees the child as `receiver` and `this`
1263 // inside a trap-served getter resolves to the child, not the
1264 // proxy. `lookup_chain` cannot do this: it reads property maps,
1265 // and a proxy has none.
1266 return Ok(crate::proxy::get(&link, name, recv)?.expect("link is a proxy"));
1267 } else if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1268 // A method / data property inherited from the prototype chain.
1269 v
1270 } else if crate::stdlib::native_tag(recv)
1271 .map(|tag| crate::stdlib::instance_has_method(&tag, name))
1272 .unwrap_or(false)
1273 {
1274 // A native instance method read as a property (`server.listen`) →
1275 // a bound method, dispatched via `instance_call` when invoked.
1276 bound_method(recv, name)
1277 } else if is_object_method(name) && !with_host(|h| h.has_null_proto(recv)) {
1278 // `Object.create(null)` inherits nothing, so `toString`/`valueOf`
1279 // read as `undefined` there — which is also what makes
1280 // `Object.create(null) + 1` the spec `TypeError` instead of a
1281 // silent `"[object Object]1"`.
1282 bound_method(recv, name)
1283 } else {
1284 Value::Undef
1285 }
1286 }
1287 Some(ObjKind::Class) | Some(ObjKind::Func) | Some(ObjKind::BoundFunc) => {
1288 function_property(recv, name)
1289 }
1290 // A method READ off an instance (`[].slice`, `new Map().get`) is a bound
1291 // thunk here. It is a function value, so it answers the function
1292 // properties: `[].slice.name` was `undefined` where node reports
1293 // `slice`, and `String([].slice)` fell through to
1294 // `Object.prototype.toString`.
1295 Some(ObjKind::BoundMethod) => bound_method_property(recv, name),
1296 Some(ObjKind::Symbol) => match name {
1297 "description" => {
1298 match peek(recv, |o| match o {
1299 JsObj::Symbol { desc, .. } => desc.clone(),
1300 _ => None,
1301 }) {
1302 Some(d) => with_host(|h| h.new_str(d)),
1303 None => Value::Undef,
1304 }
1305 }
1306 "toString" => bound_method(recv, name),
1307 // Anything else a symbol answers, it inherits from
1308 // `Symbol.prototype`. The arm used to stop at `undefined`, so
1309 // `Symbol('x')[Symbol.toPrimitive]` and `Symbol('x').valueOf` read
1310 // as absent even though the prototype defines both — a symbol is an
1311 // ordinary object for the purpose of a property LOOKUP, only its
1312 // methods are branded.
1313 _ => with_host(|h| {
1314 h.ensure_wrapper_protos();
1315 h.native_proto("Symbol")
1316 })
1317 .and_then(|p| with_host(|h| host::lookup_chain(h, &p, name)))
1318 .unwrap_or(Value::Undef),
1319 },
1320 Some(ObjKind::BigInt) => {
1321 if matches!(
1322 name,
1323 "toString" | "valueOf" | "toLocaleString" | "constructor"
1324 ) {
1325 bound_method(recv, name)
1326 } else {
1327 Value::Undef
1328 }
1329 }
1330 Some(ObjKind::RegExp) => {
1331 // A RegExp holds no collection, so cloning the compiled pattern here
1332 // does not scale with any input size; `regexp_property` re-enters the
1333 // host to allocate `source`/`flags`, so it cannot run under a borrow.
1334 let r = peek(recv, |o| match o {
1335 JsObj::RegExp(r) => Some(r.clone()),
1336 _ => None,
1337 });
1338 match r {
1339 Some(r) => crate::regexp::regexp_property(&r, name).unwrap_or_else(|| {
1340 // An OWN property beats the prototype method of the same
1341 // name, which is ordinary resolution order. It mattered once
1342 // the symbol-keyed methods existed: `re[Symbol.match] =
1343 // false` disowns the regexp label (7.2.8), and the method
1344 // was shadowing the assignment so the value never took.
1345 if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1346 return v;
1347 }
1348 if crate::regexp::is_regexp_method(name) {
1349 bound_method(recv, name)
1350 } else {
1351 Value::Undef
1352 }
1353 }),
1354 None => Value::Undef,
1355 }
1356 }
1357 // A WeakMap/WeakSet has NO `size` (its contents are not observable), so
1358 // the read must be `undefined` rather than a live count.
1359 Some(ObjKind::Map) => {
1360 let (len, weak) = peek(recv, |o| match o {
1361 JsObj::Map { entries, weak } => Some((entries.len(), *weak)),
1362 _ => None,
1363 })
1364 .unwrap_or((0, false));
1365 match name {
1366 "size" if !weak => Value::Float(len as f64),
1367 "@@iterator" => bound_method(recv, name),
1368 _ if is_map_method(name) => bound_method(recv, name),
1369 _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1370 }
1371 }
1372 Some(ObjKind::Set) => {
1373 let (len, weak) = peek(recv, |o| match o {
1374 JsObj::Set { entries, weak } => Some((entries.len(), *weak)),
1375 _ => None,
1376 })
1377 .unwrap_or((0, false));
1378 match name {
1379 "size" if !weak => Value::Float(len as f64),
1380 "@@iterator" => bound_method(recv, name),
1381 _ if is_set_method(name) => bound_method(recv, name),
1382 _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1383 }
1384 }
1385 Some(ObjKind::Generator) => {
1386 // A generator IS its own iterator, so it answers for the matching
1387 // symbol — `@@asyncIterator` for an async one, `@@iterator` for a
1388 // sync one. Neither was advertised, so `ag()[Symbol.asyncIterator]`
1389 // was `undefined` even though `for await` over it worked through a
1390 // different path.
1391 let want = if with_host(|h| h.is_async_gen_val(recv)) {
1392 "@@asyncIterator"
1393 } else {
1394 "@@iterator"
1395 };
1396 if name == want || is_generator_method(name) || crate::stdlib::iterator::is_helper(name)
1397 {
1398 bound_method(recv, name)
1399 } else {
1400 with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1401 }
1402 }
1403 Some(ObjKind::Promise) => {
1404 if matches!(name, "then" | "catch" | "finally") {
1405 bound_method(recv, name)
1406 } else {
1407 with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1408 }
1409 }
1410 Some(ObjKind::Iter) => {
1411 if matches!(name, "next" | "return" | "@@iterator")
1412 || crate::stdlib::iterator::is_helper(name)
1413 {
1414 bound_method(recv, name)
1415 } else {
1416 with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
1417 }
1418 }
1419 Some(ObjKind::Array) => {
1420 if name == "length" {
1421 let n = peek(recv, |o| match o {
1422 JsObj::Array(items) => Some(items.len()),
1423 _ => None,
1424 })
1425 .unwrap_or(0);
1426 Value::Float(n as f64)
1427 } else if let Ok(i) = name.parse::<usize>() {
1428 peek(recv, |o| match o {
1429 JsObj::Array(items) => items.get(i).cloned(),
1430 _ => None,
1431 })
1432 // An index PAST an `arguments` object's length is an ordinary
1433 // own property in the side table, since adding one must not
1434 // move `length`. The array read alone could not see it, so the
1435 // write was invisible to every later read.
1436 .or_else(|| with_host(|h| h.fn_prop(recv, name)))
1437 .unwrap_or(Value::Undef)
1438 } else if name == "@@iterator"
1439 || is_object_method(name)
1440 // An `arguments` object is array-BACKED here but is not an
1441 // Array: node's exposes no `Array.prototype` method, which is
1442 // exactly why the idiom is `Array.prototype.slice.call(args)`.
1443 // Exposing them made `arguments.map` a function.
1444 || (is_array_method(name) && !is_arguments(recv))
1445 {
1446 bound_method(recv, name)
1447 } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
1448 // Extra own props attached to an array (e.g. `RegExp.exec` result's
1449 // `.index`/`.input`/`.groups`).
1450 v
1451 } else {
1452 Value::Undef
1453 }
1454 }
1455 Some(ObjKind::Str) => {
1456 // `.length` and `s[i]` count UTF-16 code units, not code points.
1457 if name == "length" {
1458 let n = peek(recv, |o| match o {
1459 JsObj::Str(s) => Some(crate::utf16::len(s)),
1460 _ => None,
1461 })
1462 .unwrap_or(0);
1463 Value::Float(n as f64)
1464 } else if let Ok(i) = name.parse::<usize>() {
1465 match peek(recv, |o| match o {
1466 JsObj::Str(s) => crate::utf16::Units::of(s).unit_str(i),
1467 _ => None,
1468 }) {
1469 Some(c) => with_host(|h| h.new_str(c)),
1470 None => Value::Undef,
1471 }
1472 } else if name == "@@iterator" || is_string_method(name) {
1473 bound_method(recv, name)
1474 } else {
1475 Value::Undef
1476 }
1477 }
1478 Some(ObjKind::Builtin) => {
1479 let ns = peek(recv, |o| match o {
1480 JsObj::Builtin(ns) => Some(ns.clone()),
1481 _ => None,
1482 })
1483 .unwrap_or_default();
1484 let v = namespace_property(&ns, name);
1485 // `Function.prototype`'s methods READ off a builtin function. The
1486 // CALL forms (`Math.max.call(null, 1, 2)`) already dispatched, but
1487 // the read answered `undefined` — so `typeof Math.max.bind` was
1488 // `"undefined"`, and `String(Math.max)` found no `toString` to
1489 // invoke and fell back to `Object.prototype.toString`'s
1490 // `[object Function]` where node reports the native-code form.
1491 if matches!(v, Value::Undef)
1492 && is_function_method(name)
1493 && host::builtin_is_callable(&ns)
1494 {
1495 return Ok(bound_method(recv, name));
1496 }
1497 v
1498 }
1499 _ => {
1500 // Primitive numbers/booleans: method access -> bound method.
1501 if matches!(recv, Value::Float(_) | Value::Int(_)) && is_number_method(name) {
1502 bound_method(recv, name)
1503 } else {
1504 Value::Undef
1505 }
1506 }
1507 };
1508 // Every object INHERITS the `Object.prototype` methods, and each kind's
1509 // read arm above knows only its OWN. So `typeof new Map().toString`,
1510 // `typeof f.hasOwnProperty` and `typeof /a/.propertyIsEnumerable` all
1511 // answered `undefined` — for Map the CALL already worked, which is the
1512 // read and the dispatch disagreeing about the same method.
1513 //
1514 // Which prototype owns the name is decided by the same helper the `in`
1515 // operator uses, so the two cannot drift, and the result is the SHARED
1516 // intrinsic rather than a per-read thunk.
1517 // `arguments.callee` (and `.caller`) is a POISON PILL in strict code — the
1518 // accessor throws rather than answering, which is how a strict function
1519 // keeps its caller unreachable. It read back as `undefined` here, which a
1520 // feature probe reads as "not supported" rather than "forbidden".
1521 // Measured: on an ARGUMENTS object only `callee` is poisoned (`caller` is
1522 // simply absent and reads `undefined`); on a strict FUNCTION both `caller`
1523 // and `arguments` are.
1524 if name == "callee" && is_arguments(recv) && with_host(|h| h.current_strict()) {
1525 return Err(host::type_error(POISON_PILL));
1526 }
1527 if matches!(name, "caller" | "arguments")
1528 && matches!(
1529 with_host(|h| h.kind_of(recv)),
1530 Some(ObjKind::Func) | Some(ObjKind::Class)
1531 )
1532 {
1533 return poison_pill_read(recv);
1534 }
1535 // `arguments.callee` in SLOPPY code is the running function — the
1536 // pre-`class` self-reference idiom. It read back `undefined`.
1537 if name == "callee" && is_arguments(recv) {
1538 if let Some(f) = with_host(|h| h.fn_prop(recv, "@@callee")) {
1539 return Ok(f);
1540 }
1541 }
1542 // A method SYNTHESIZED from the receiver's kind is only reachable while the
1543 // receiver's intrinsic prototype is still on its chain. `Object
1544 // .setPrototypeOf(a, {})` must make `a.join` `undefined`; the kind arm
1545 // above answers from the kind alone and cannot know the link changed. Only
1546 // a synthesized value is dropped — the two shapes a method read produces —
1547 // and only when the receiver does not own the name itself.
1548 if matches!(
1549 with_host(|h| h.get(&out).cloned()),
1550 Some(JsObj::BoundMethod { .. })
1551 ) || matches!(
1552 with_host(|h| h.get(&out).cloned()),
1553 Some(JsObj::Builtin(ns)) if ns.starts_with("@proto:")
1554 ) {
1555 // The kind arms synthesize their OWN kind's methods, so that is the
1556 // prototype whose reachability decides. Clearing the value here lets
1557 // the `inherited_method_owner` fallback below re-supply the
1558 // `Object.prototype` form where one exists — which is why
1559 // `a.toString` stays a function after the link is replaced while
1560 // `a.join` does not.
1561 if !own_intrinsic_reachable(recv) && !has_own_for_shadow(recv, name) {
1562 out = Value::Undef;
1563 }
1564 }
1565 // A key the receiver does not OWN is looked up on its prototype chain. The
1566 // exotic arms above answer from their own storage and stop, so an array
1567 // given a prototype inherited nothing through a read: with
1568 // `Object.setPrototypeOf(a, {1: 'q'})`, `a[1]` was `undefined` at an elided
1569 // index and at one past the end, while `1 in a` already answered true —
1570 // the two views of the same question disagreeing. An accessor was found
1571 // (`lookup_accessor` walks), so only DATA properties went missing.
1572 //
1573 // A plain object's arm already consults the chain, and an array with no
1574 // explicit prototype has no links to walk, so this changes neither.
1575 if !name.starts_with('#') && !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
1576 if matches!(out, Value::Undef) {
1577 if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
1578 return Ok(v);
1579 }
1580 }
1581 // Then a monkey-patched intrinsic prototype member, which shadows the
1582 // synthesized one: after `Array.prototype.join = f`, `[1, 2].join` must
1583 // BE `f`. An explicitly-set prototype above wins over it, as the chain
1584 // order requires.
1585 if let Some(v) = inherited_builtin_static(recv, name) {
1586 return Ok(v);
1587 }
1588 }
1589 if matches!(out, Value::Undef) && !name.starts_with('#') {
1590 if let Some(owner) = inherited_method_owner(recv, name) {
1591 // An INHERITED accessor runs, it does not hand back a thunk, and
1592 // its brand check is about the receiver's internal slot rather than
1593 // its chain — `Object.create(Map.prototype).size` throws in node
1594 // even though `Map.prototype` is right there above it. This
1595 // answered `undefined`, which is the value a real Map would never
1596 // give and a plain object should never reach.
1597 if is_proto_accessor(owner, name) && !getter_in_flight(owner, name) {
1598 return proto_getter_call(owner, name, recv);
1599 }
1600 let key = format!("@proto:{owner}:{name}");
1601 if builtin_meta(&key).is_some() {
1602 return Ok(with_host(|h| h.alloc(JsObj::Builtin(key))));
1603 }
1604 // A DATA member of the prototype — `Array.prototype[Symbol
1605 // .unscopables]` is an object, not a method, so it is in neither
1606 // function table. Read it off the prototype itself rather than
1607 // answering `undefined`: an instance inherits it.
1608 let v = namespace_property(&format!("{owner}.prototype"), name);
1609 if !matches!(v, Value::Undef) {
1610 return Ok(v);
1611 }
1612 }
1613 }
1614 Ok(out)
1615}
1616
1617/// The namespace name of the `require.cache` view. A `Builtin` rather than an
1618/// object literal because the module cache is the single source of truth: a
1619/// populated copy would answer reads correctly and silently ignore a `delete`,
1620/// which is the operation the property exists for.
1621pub const REQUIRE_CACHE: &str = "__cjs_cache";
1622
1623/// The builtin constructor name for a value with no own/inherited `constructor`
1624/// property, so `x.constructor` (and thus `x.constructor.name`) matches Node for
1625/// arrays, plain objects, Map/Set, promises, iterators, functions, and boxed
1626/// primitives. `None` ⇒ leave `.constructor` as `undefined` (e.g. generators,
1627/// whose `.constructor.name` is `""` in Node — not worth modelling).
1628fn default_ctor_name(h: &host::JsHost, recv: &Value) -> Option<&'static str> {
1629 match h.get(recv) {
1630 Some(JsObj::Array(_)) => Some("Array"),
1631 Some(JsObj::Object(props)) => {
1632 // A native instance reports its own constructor, not Object — e.g.
1633 // `qs` does `buf.constructor.isBuffer(buf)`, so a Buffer's
1634 // `.constructor` must be `Buffer` (which carries `isBuffer`). Read
1635 // the `@@native` tag off the already-borrowed host (calling
1636 // `native_tag`, which re-enters `with_host`, would double-borrow).
1637 match props.get("@@native").map(|t| h.str_of(t)).as_deref() {
1638 Some("Buffer") => Some("Buffer"),
1639 Some("URL") => Some("URL"),
1640 Some("Date") => Some("Date"),
1641 Some("WeakRef") => Some("WeakRef"),
1642 Some("FinalizationRegistry") => Some("FinalizationRegistry"),
1643 Some("TextEncoder") => Some("TextEncoder"),
1644 Some("TextDecoder") => Some("TextDecoder"),
1645 Some("EventEmitter") => Some("EventEmitter"),
1646 Some("Timeout") => Some("Timeout"),
1647 Some("Immediate") => Some("Immediate"),
1648 _ => Some("Object"),
1649 }
1650 }
1651 Some(JsObj::Map { weak, .. }) => Some(if *weak { "WeakMap" } else { "Map" }),
1652 Some(JsObj::Set { weak, .. }) => Some(if *weak { "WeakSet" } else { "Set" }),
1653 Some(JsObj::Promise { .. }) => Some("Promise"),
1654 Some(JsObj::Str(_)) => Some("String"),
1655 Some(JsObj::Symbol { .. }) => Some("Symbol"),
1656 Some(JsObj::BigInt(_)) => Some("BigInt"),
1657 Some(JsObj::RegExp(_)) => Some("RegExp"),
1658 Some(JsObj::Iter { .. }) => Some("Iterator"),
1659 Some(JsObj::Func(f)) => {
1660 // A generator or async function is NOT an ordinary function: its
1661 // `[[Prototype]]` is `GeneratorFunction.prototype` (or the async
1662 // variants'), and so is its `constructor`. All three reported plain
1663 // `Function`, so `g.constructor.name` was `Function` where node
1664 // says `GeneratorFunction`.
1665 Some(match h.funcs.get(f.def_id) {
1666 Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction",
1667 Some(d) if d.is_generator => "GeneratorFunction",
1668 Some(d) if d.is_async => "AsyncFunction",
1669 _ => "Function",
1670 })
1671 }
1672 Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => Some("Function"),
1673 _ => match recv {
1674 Value::Float(_) | Value::Int(_) => Some("Number"),
1675 Value::Bool(_) => Some("Boolean"),
1676 _ => None,
1677 },
1678 }
1679}
1680
1681/// The builtin constructor *functions*, so `Ctor.name` is the constructor name.
1682/// Excludes the non-callable namespaces (`Math`, `JSON`, `console`, `Reflect`,
1683/// `process`), whose `.name` is `undefined` in Node.
1684///
1685/// Most are also globals, but not all: `Timeout`/`Immediate` are unexposed in
1686/// Node (`typeof Timeout === 'undefined'`) yet still name themselves through a
1687/// handle's `.constructor.name`, so they belong here and not in `GLOBALS`.
1688/// The builtins that expose a `Symbol.species` accessor. Each returns `this`,
1689/// so a subclass is its own species unless it overrides the getter.
1690fn has_species(name: &str) -> bool {
1691 matches!(
1692 name,
1693 "Array" | "Map" | "Set" | "WeakMap" | "WeakSet" | "Promise" | "RegExp" | "ArrayBuffer"
1694 ) || crate::stdlib::typedarray::is_ctor(name)
1695}
1696
1697fn is_builtin_ctor(name: &str) -> bool {
1698 matches!(
1699 name,
1700 "Array"
1701 | "Object"
1702 | "Number"
1703 | "String"
1704 | "Boolean"
1705 | "Symbol"
1706 | "Function"
1707 | "Map"
1708 | "Set"
1709 | "WeakMap"
1710 | "WeakSet"
1711 | "Promise"
1712 | "BigInt"
1713 | "Iterator"
1714 | "RegExp"
1715 | "Date"
1716 | "ArrayBuffer"
1717 | "DataView"
1718 | "Uint8Array"
1719 | "Int8Array"
1720 | "Uint8ClampedArray"
1721 | "Int16Array"
1722 | "Uint16Array"
1723 | "Int32Array"
1724 | "Uint32Array"
1725 | "Float32Array"
1726 | "Float64Array"
1727 | "BigInt64Array"
1728 | "BigUint64Array"
1729 | "WeakRef"
1730 | "FinalizationRegistry"
1731 | "TextEncoder"
1732 | "TextDecoder"
1733 | "IncomingMessage"
1734 | "ServerResponse"
1735 | "EventEmitter"
1736 | "Buffer"
1737 | "URL"
1738 | "URLSearchParams"
1739 | "Timeout"
1740 | "Immediate"
1741 ) || host::ERROR_NAMES.contains(&name)
1742 // The stream base classes are constructors too, and `require('stream')`
1743 // IS `Stream`, so `require('stream').name` has to answer.
1744 || crate::stdlib::stream::is_class(name)
1745}
1746
1747/// The intrinsic key of the method `<instance>.<method>` resolves to, so a bound
1748/// thunk can look its `name`/`length` up in the same table a
1749/// `<Ctor>.prototype.<method>` thunk uses. `None` when the receiver has no
1750/// builtin constructor to name (a native stdlib instance, whose methods are
1751/// node's own JS and have no specified arity).
1752fn bound_method_key(recv: &Value, method: &str) -> Option<String> {
1753 let ctor = with_host(|h| default_ctor_name(h, recv))?;
1754 Some(format!("@proto:{ctor}:{method}"))
1755}
1756
1757/// `[[Get]]` on a bound method thunk. It is a function, so `name`, `length` and
1758/// the `Function.prototype` methods all answer; `length` only when the intrinsic
1759/// table knows the method, because inventing an arity is worse than the
1760/// `undefined` a caller can test for.
1761fn bound_method_property(recv: &Value, name: &str) -> Value {
1762 let method = peek(recv, |o| match o {
1763 JsObj::BoundMethod { name, .. } => Some(name.clone()),
1764 _ => None,
1765 })
1766 .unwrap_or_default();
1767 let key = peek(recv, |o| match o {
1768 JsObj::BoundMethod { recv, .. } => Some(recv.clone()),
1769 _ => None,
1770 })
1771 .and_then(|inner| bound_method_key(&inner, &method));
1772 let meta = key.as_deref().and_then(builtin_meta);
1773 match name {
1774 "name" => {
1775 let n = meta.map(|(n, _)| n.to_string()).unwrap_or(method);
1776 with_host(|h| h.new_str(n))
1777 }
1778 "length" => match meta {
1779 Some((_, len)) => Value::Float(len as f64),
1780 None => Value::Undef,
1781 },
1782 _ if is_function_method(name) => bound_method(recv, name),
1783 _ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
1784 }
1785}
1786
1787fn bound_method(recv: &Value, name: &str) -> Value {
1788 // An ECMAScript intrinsic is ONE function object shared by every instance:
1789 // `[1].push === Array.prototype.push` and `[1].push === [2].push` are both
1790 // true. Reading one off an instance used to mint a fresh thunk bound to that
1791 // instance, so every such comparison answered false — and a detached method
1792 // kept working on the receiver it was read off, where node throws because it
1793 // has no `this` at all.
1794 if let Some(key) = bound_method_key(recv, name) {
1795 if builtin_meta(&key).is_some() {
1796 return with_host(|h| h.alloc(JsObj::Builtin(key)));
1797 }
1798 }
1799 with_host(|h| {
1800 h.alloc(JsObj::BoundMethod {
1801 recv: recv.clone(),
1802 name: name.to_string(),
1803 })
1804 })
1805}
1806
1807/// `Object.prototype` methods reachable on any object.
1808fn is_object_method(name: &str) -> bool {
1809 matches!(
1810 name,
1811 "hasOwnProperty"
1812 | "isPrototypeOf"
1813 | "propertyIsEnumerable"
1814 | "toString"
1815 | "toLocaleString"
1816 | "valueOf"
1817 | "constructor"
1818 | "__defineGetter__"
1819 | "__defineSetter__"
1820 | "__lookupGetter__"
1821 | "__lookupSetter__"
1822 )
1823}
1824
1825/// The `Object.prototype` methods installed as thunks on the real
1826/// `Object.prototype` object, so `Object.prototype.toString.call(x)` and a class
1827/// prototype's inherited `hasOwnProperty` both resolve through the chain.
1828pub const OBJECT_PROTO_METHODS: &[&str] = &[
1829 "hasOwnProperty",
1830 "isPrototypeOf",
1831 "propertyIsEnumerable",
1832 "toString",
1833 "toLocaleString",
1834 "valueOf",
1835 "__defineGetter__",
1836 "__defineSetter__",
1837 "__lookupGetter__",
1838 "__lookupSetter__",
1839];
1840
1841/// A typed array with elements cannot be frozen or sealed: its indices are
1842/// non-configurable by construction, so making them non-writable would violate
1843/// the invariant, and node refuses outright rather than half-applying it. An
1844/// EMPTY view and a `DataView` are both fine.
1845/// `TestIntegrityLevel` (7.3.16) — `Object.isFrozen` / `Object.isSealed`.
1846///
1847/// Over a PROXY it is a sequence of traps (`isExtensible`, `ownKeys`, then a
1848/// `getOwnPropertyDescriptor` per key), not a question for the host: the proxy
1849/// OBJECT was being inspected, so a frozen proxy answered false and the handler
1850/// never saw the query.
1851fn integrity_level(v: &Value, freeze: bool) -> Result<Value, String> {
1852 if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
1853 return Ok(Value::Bool(with_host(|h| h.is_sealed(v, freeze))));
1854 }
1855 // An EXTENSIBLE object is neither sealed nor frozen, whatever its keys say.
1856 if crate::proxy::is_extensible(v)?.unwrap_or(true) {
1857 return Ok(Value::Bool(false));
1858 }
1859 for key in crate::proxy::own_keys(v)?.unwrap_or_default() {
1860 let Some(d) = crate::proxy::get_own_descriptor(v, &key)? else {
1861 continue;
1862 };
1863 let flag = |name: &str| {
1864 with_host(|h| match h.get(&d) {
1865 Some(JsObj::Object(p)) => p.get(name).map(|x| h.truthy(x)).unwrap_or(false),
1866 _ => false,
1867 })
1868 };
1869 let is_data = with_host(
1870 |h| matches!(h.get(&d), Some(JsObj::Object(p)) if !p.contains_key("get") && !p.contains_key("set")),
1871 );
1872 if flag("configurable") || (freeze && is_data && flag("writable")) {
1873 return Ok(Value::Bool(false));
1874 }
1875 }
1876 Ok(Value::Bool(true))
1877}
1878
1879/// `SetIntegrityLevel` (7.3.15) over a PROXY, which is a sequence of TRAPS —
1880/// `preventExtensions`, then `ownKeys`, then a `getOwnPropertyDescriptor` and a
1881/// `defineProperty` per key. It ran none of them: the host sealed the proxy
1882/// OBJECT, so the handler never saw the operation and the target was untouched.
1883///
1884/// Returns false for a non-proxy, which takes the ordinary path.
1885fn seal_proxy(v: &Value, freeze: bool) -> Result<bool, String> {
1886 if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
1887 return Ok(false);
1888 }
1889 if !crate::proxy::prevent_extensions(v)? {
1890 return Err(host::type_error("Object.freeze called on non-object"));
1891 }
1892 let keys = crate::proxy::own_keys(v)?.unwrap_or_default();
1893 for key in keys {
1894 // SEALING asks for no descriptor at all — it only strips
1895 // `configurable`, which is the same for a data property and an
1896 // accessor. FREEZING has to know which it is, because only a data
1897 // property has a `writable` to strip, and that is the one extra trap
1898 // call node makes.
1899 let accessor = if freeze {
1900 let Some(cur) = crate::proxy::get_own_descriptor(v, &key)? else {
1901 continue;
1902 };
1903 with_host(
1904 |h| matches!(h.get(&cur), Some(JsObj::Object(p)) if p.contains_key("get") || p.contains_key("set")),
1905 )
1906 } else {
1907 false
1908 };
1909 let desc = with_host(|h| {
1910 let mut m: IndexMap<String, Value> = IndexMap::new();
1911 m.insert("configurable".into(), Value::Bool(false));
1912 if freeze && !accessor {
1913 m.insert("writable".into(), Value::Bool(false));
1914 }
1915 h.new_object(m)
1916 });
1917 if !crate::proxy::define_property(v, &key, &desc)? {
1918 return Err(host::type_error(&format!(
1919 "'defineProperty' on proxy: trap returned falsish for property '{key}'"
1920 )));
1921 }
1922 }
1923 Ok(true)
1924}
1925
1926fn reject_sealing_a_view(v: &Value, verb: &str) -> Result<(), String> {
1927 let has_elements = matches!(
1928 crate::stdlib::native_tag(v).as_deref(),
1929 Some("TypedArray") | Some("Buffer")
1930 ) && !crate::stdlib::typedarray::elem_values(v).is_empty();
1931 if has_elements {
1932 return Err(host::type_error(&format!(
1933 "Cannot {verb} array buffer views with elements"
1934 )));
1935 }
1936 Ok(())
1937}
1938
1939pub fn is_object_builtin_method(name: &str) -> bool {
1940 matches!(
1941 name,
1942 "hasOwnProperty"
1943 | "isPrototypeOf"
1944 | "propertyIsEnumerable"
1945 | "toString"
1946 | "toLocaleString"
1947 | "valueOf"
1948 | "__defineGetter__"
1949 | "__defineSetter__"
1950 | "__lookupGetter__"
1951 | "__lookupSetter__"
1952 )
1953}
1954
1955/// The `Symbol.toStringTag` STRING on `recv`'s chain, if any — steps 16-17 of
1956/// 20.1.3.6, the hook by which a class names its own brand.
1957///
1958/// A Proxy has no chain to probe: the step is an unconditional
1959/// `Get(O, @@toStringTag)`, so its `get` trap decides. Probing first (as an
1960/// ordinary receiver does, to keep the read off objects that carry no tag)
1961/// would always miss and brand every tagged proxy `[object Object]`.
1962///
1963/// The read runs OUTSIDE the host borrow so a getter-valued tag can be invoked.
1964fn to_string_tag(recv: &Value) -> Result<Option<String>, String> {
1965 let tagged = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
1966 || with_host(|h| {
1967 host::lookup_chain(h, recv, "@@toStringTag").is_some()
1968 || host::lookup_accessor(h, recv, "@@toStringTag").is_some()
1969 });
1970 if !tagged {
1971 return Ok(None);
1972 }
1973 let t = get_property(recv, "@@toStringTag")?;
1974 Ok(with_host(|h| h.as_str(&t)))
1975}
1976
1977/// Dispatch an `Object.prototype` builtin method on an object/instance.
1978pub fn object_builtin_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
1979 match name {
1980 // Annex B B.2.2.2-B.2.2.5. Legacy, but still present in node and still
1981 // reached by pre-`defineProperty` libraries; all four were missing, so
1982 // `o.__defineGetter__` threw "is not a function".
1983 "__defineGetter__" | "__defineSetter__" => {
1984 let getter = name == "__defineGetter__";
1985 let f = args.get(1).cloned().unwrap_or(Value::Undef);
1986 if !with_host(|h| host::is_callable(h, &f)) {
1987 return Err(host::type_error(&format!(
1988 "Object.prototype.{name}: Expecting function"
1989 )));
1990 }
1991 let key = host::to_property_key(&arg0(&args))?;
1992 let desc = with_host(|h| {
1993 let mut m: IndexMap<String, Value> = IndexMap::new();
1994 m.insert(if getter { "get" } else { "set" }.into(), f);
1995 m.insert("enumerable".into(), Value::Bool(true));
1996 m.insert("configurable".into(), Value::Bool(true));
1997 h.new_object(m)
1998 });
1999 apply_descriptor(recv, &key, &desc)?;
2000 Ok(Value::Undef)
2001 }
2002 "__lookupGetter__" | "__lookupSetter__" => {
2003 let want_get = name == "__lookupGetter__";
2004 let key = host::to_property_key(&arg0(&args))?;
2005 // Walks the prototype chain, unlike `getOwnPropertyDescriptor`.
2006 let found = with_host(|h| host::lookup_accessor(h, recv, &key));
2007 Ok(match found {
2008 Some((g, st)) => {
2009 let side = if want_get { g } else { st };
2010 side.unwrap_or(Value::Undef)
2011 }
2012 None => Value::Undef,
2013 })
2014 }
2015 "hasOwnProperty" => {
2016 let k = host::to_property_key(&arg0(&args))?;
2017 // The global object OWNS its lazily-bound builtins and every global
2018 // a script created; neither lives in its property map.
2019 if with_host(|h| h.is_global_object(recv))
2020 && !CJS_WRAPPER_LOCALS.contains(&k.as_str())
2021 && global_object_binding(&k).is_some()
2022 {
2023 return Ok(Value::Bool(true));
2024 }
2025 // A builtin namespace/prototype receiver (`Map.prototype`) reports
2026 // ownership via `has_property` (its methods resolve as thunks).
2027 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin) {
2028 return Ok(Value::Bool(has_property(recv, &k)?));
2029 }
2030 // `HasOwnProperty` (7.3.12) is `[[GetOwnProperty]]`, so on a Proxy it
2031 // is the `getOwnPropertyDescriptor` trap — NOT the `has` trap and not
2032 // the target's property map.
2033 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
2034 let d = crate::proxy::get_own_descriptor(recv, &k)?.unwrap_or(Value::Undef);
2035 return Ok(Value::Bool(!matches!(d, Value::Undef)));
2036 }
2037 // A Buffer's / typed array's own keys are its element indices: the
2038 // `length`/`byteLength` slots are internal bookkeeping, and V8
2039 // reports `hasOwnProperty('length')` as false for a typed array.
2040 // Shared with the `in` operator so the two cannot drift apart.
2041 if let Some(hit) = crate::stdlib::typedarray::has_index(recv, &k) {
2042 return Ok(Value::Bool(hit));
2043 }
2044 // A function's `length`/`name`/`prototype` and a RegExp's
2045 // `lastIndex` are SYNTHESIZED own properties: they read back but
2046 // own no map entry, so this answered false where node says true.
2047 if synthesized_own_descriptor(recv, &k).is_some() {
2048 return Ok(Value::Bool(true));
2049 }
2050 if uses_side_table(recv) {
2051 return Ok(Value::Bool(with_host(|h| h.fn_prop(recv, &k).is_some())));
2052 }
2053 let has = with_host(|h| match h.get(recv) {
2054 Some(JsObj::Object(p)) => p.contains_key(&k) || h.own_accessor(recv, &k).is_some(),
2055 Some(JsObj::Array(items)) => {
2056 k == "length"
2057 || k.parse::<usize>()
2058 .map(|i| i < items.len() && !h.is_hole(recv, i))
2059 .unwrap_or(false)
2060 }
2061 _ => false,
2062 });
2063 Ok(Value::Bool(has))
2064 }
2065 "isPrototypeOf" => {
2066 let target = arg0(&args);
2067 // The ARGUMENT is what gets walked, so a proxy there needs its
2068 // `getPrototypeOf` trap for the FIRST hop: `proto_of` reads a link a
2069 // proxy does not hold, which reported `false` for every proxy. From
2070 // the second hop on the chain is ordinary objects again, walked by
2071 // the recorded link exactly as before.
2072 let mut cur = match crate::proxy::get_prototype_of(&target)? {
2073 Some(p) => Some(p).filter(|p| !with_host(|h| h.is_null(p))),
2074 None => with_host(|h| h.proto_of(&target)),
2075 };
2076 while let Some(p) = cur {
2077 if with_host(|h| h.strict_eq(&p, recv)) {
2078 return Ok(Value::Bool(true));
2079 }
2080 cur = with_host(|h| h.proto_of(&p));
2081 }
2082 Ok(Value::Bool(false))
2083 }
2084 "propertyIsEnumerable" => {
2085 let k = with_host(|h| h.str_of(&arg0(&args)));
2086 // Own *and* enumerable — a non-enumerable own slot reads false. On a
2087 // Proxy that question is `[[GetOwnProperty]]`, i.e. the descriptor
2088 // trap, since there is no property map to enumerate.
2089 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
2090 let has = crate::proxy::own_enum_string_keys(recv)?.contains(&k);
2091 return Ok(Value::Bool(has));
2092 }
2093 let has = with_host(|h| h.own_enum_key_names(recv).contains(&k));
2094 Ok(Value::Bool(has))
2095 }
2096 "toString" => {
2097 // An instance with a custom `toString` up the chain is handled by
2098 // call_method before reaching here; this is the default — and the
2099 // default consults `Symbol.toStringTag` (20.1.3.6 steps 16-17).
2100 // Only the EXPLICIT `Object.prototype.toString.call(o)` did, so a
2101 // tagged object branded itself `[object T]` when asked one way and
2102 // `[object Object]` when converted the other (`String(o)`, `${o}`,
2103 // `o + ''`, `o.toString()`), which is the path ordinary code takes.
2104 if let Some(t) = to_string_tag(recv)? {
2105 return Ok(with_host(|h| h.new_str(format!("[object {t}]"))));
2106 }
2107 Ok(with_host(|h| {
2108 let s = h.str_of(recv);
2109 h.new_str(s)
2110 }))
2111 }
2112 // `Object.prototype.toLocaleString` (20.1.3.5) is defined as
2113 // `Invoke(this, "toString")` — no locale behavior of its own. It was
2114 // installed as a thunk on `Object.prototype` but had no dispatch arm, so
2115 // calling it threw `is not a function` on every plain object.
2116 "toLocaleString" => {
2117 let v = host::call_method(recv, "toString", Vec::new())?;
2118 Ok(v)
2119 }
2120 "valueOf" => Ok(recv.clone()),
2121 _ => Err(host::type_error(&format!("{name} is not a function"))),
2122 }
2123}
2124
2125/// `Function.prototype` methods (`call`/`apply`/`bind`) plus `Symbol.prototype`/
2126/// generator handling done elsewhere. Returns `Ok(None)` if `name` is not one of
2127/// these (so the caller can try statics).
2128pub fn function_builtin_method(
2129 recv: &Value,
2130 name: &str,
2131 args: &[Value],
2132) -> Result<Option<Value>, String> {
2133 match name {
2134 "call" => {
2135 let this = args.first().cloned();
2136 let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2137 Ok(Some(host::invoke(recv, rest, this)?))
2138 }
2139 "apply" => {
2140 let this = args.first().cloned();
2141 let arr = args.get(1).cloned().unwrap_or(Value::Undef);
2142 // `Function.prototype.apply` takes an ARRAY-LIKE, not an iterable
2143 // (10.2.4.3 → CreateListFromArrayLike): `f.apply(null, arguments)`
2144 // and `f.apply(null, {length: 2, 0: 'x', 1: 'y'})` are the shapes
2145 // this is written for, and both produced an empty list. A nullish
2146 // second argument means no arguments at all.
2147 let call_args = if matches!(arr, Value::Undef) || with_host(|h| h.is_null(&arr)) {
2148 Vec::new()
2149 } else {
2150 create_list_from_array_like(&arr)?
2151 };
2152 Ok(Some(host::invoke(recv, call_args, this)?))
2153 }
2154 "bind" => {
2155 let this = args.first().cloned().unwrap_or(Value::Undef);
2156 let pre = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
2157 Ok(Some(with_host(|h| {
2158 h.alloc(JsObj::BoundFunc {
2159 target: recv.clone(),
2160 this,
2161 args: pre,
2162 })
2163 })))
2164 }
2165 "toString" => Ok(Some(with_host(|h| {
2166 let s = h.str_of(recv);
2167 h.new_str(s)
2168 }))),
2169 _ => Ok(None),
2170 }
2171}
2172
2173fn is_function_method(name: &str) -> bool {
2174 matches!(name, "call" | "apply" | "bind" | "toString")
2175}
2176fn is_map_method(name: &str) -> bool {
2177 matches!(
2178 name,
2179 "get" | "set" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
2180 )
2181}
2182fn is_set_method(name: &str) -> bool {
2183 matches!(
2184 name,
2185 "add"
2186 | "has"
2187 | "delete"
2188 | "clear"
2189 | "forEach"
2190 | "keys"
2191 | "values"
2192 | "entries"
2193 | "union"
2194 | "intersection"
2195 | "difference"
2196 | "symmetricDifference"
2197 | "isSubsetOf"
2198 | "isSupersetOf"
2199 | "isDisjointFrom"
2200 )
2201}
2202fn is_generator_method(name: &str) -> bool {
2203 matches!(name, "next" | "return" | "throw")
2204}
2205
2206/// A property read on a function/class value: own fn-props (statics, name,
2207/// prototype, length) plus inherited statics and `call`/`apply`/`bind`.
2208fn function_property(recv: &Value, name: &str) -> Value {
2209 // A class static, inherited down the constructor chain.
2210 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
2211 if let Some(v) = with_host(|h| h.class_static(recv, name)) {
2212 return v;
2213 }
2214 // A class's own `name` and `length` are its own, not the builtin
2215 // ancestor's: `class A extends Array {}` has `A.name === "A"` and
2216 // `A.length === 0`, but both were read off `Array`. Only a class that
2217 // WOULD fall through to an ancestor takes this path; a plain class keeps
2218 // the ordinary computation below.
2219 if matches!(name, "name" | "length")
2220 && with_host(|h| h.class_static(recv, name)).is_none()
2221 && with_host(|h| h.class_builtin_ancestor(recv))
2222 .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2223 {
2224 if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2225 return v;
2226 }
2227 if name == "name" {
2228 let n = with_host(|h| h.callable_name(recv));
2229 return with_host(|h| h.new_str(n));
2230 }
2231 // The class's own constructor decides its arity; with no explicit
2232 // one the implicit `constructor(...args)` has length 0.
2233 let ctor = with_host(|h| match h.get(recv) {
2234 Some(JsObj::Class(c)) => c.ctor.clone(),
2235 _ => None,
2236 });
2237 return match ctor {
2238 Some(c) => get_property(&c, "length").unwrap_or(Value::Float(0.0)),
2239 None => Value::Float(0.0),
2240 };
2241 }
2242 // `Symbol.species` is an accessor returning `this`, so a subclass that
2243 // does not override it IS its own species. Reading it off the builtin
2244 // ancestor below would answer with the ancestor — `A[Symbol.species]`
2245 // came back as `Array`, which sent every derived result to a plain
2246 // array.
2247 if name == "@@species"
2248 && with_host(|h| h.class_static(recv, "@@species")).is_none()
2249 && with_host(|h| h.class_builtin_ancestor(recv))
2250 .is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
2251 {
2252 return recv.clone();
2253 }
2254 // The chain may bottom out in a BUILTIN constructor (`class D extends
2255 // Array {}`), whose statics `class_static` cannot see — it only walks
2256 // `ClassVal.parent` links between user classes. Finish the lookup with an
2257 // ordinary read on that ancestor so `D.from` inherits `Array.from`.
2258 if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
2259 if let Ok(v) = get_property(&anc, name) {
2260 if !matches!(v, Value::Undef) {
2261 return v;
2262 }
2263 }
2264 }
2265 } else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
2266 return v;
2267 }
2268 // A method inherited via the function's [[Prototype]] chain (set with
2269 // `Object.setPrototypeOf(fn, proto)` — the `router` package makes each router
2270 // *function* inherit `route`/`use`/`get`/… from `Router.prototype` this way).
2271 if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
2272 return v;
2273 }
2274 match name {
2275 "name" => with_host(|h| {
2276 let n = h.callable_name(recv);
2277 h.new_str(n)
2278 }),
2279 "length" => Value::Float(with_host(|h| h.func_arity(recv)) as f64),
2280 "prototype" => ensure_fn_prototype(recv),
2281 _ if is_function_method(name) => bound_method(recv, name),
2282 _ => Value::Undef,
2283 }
2284}
2285
2286/// The `.prototype` of a function value, auto-created on first access (as Node
2287/// does for every non-arrow function) with `.constructor` linking back. Arrow
2288/// functions have no `prototype`.
2289fn ensure_fn_prototype(recv: &Value) -> Value {
2290 if let Some(p) = with_host(|h| h.fn_prop(recv, "prototype")) {
2291 return p;
2292 }
2293 // Only a constructor gets one: an arrow, a method definition and an async
2294 // function are not constructors, and a class sets its own (10.2.5).
2295 if with_host(|h| h.kind_of(recv)) != Some(ObjKind::Func) {
2296 return Value::Undef;
2297 }
2298 if !with_host(|h| h.owns_prototype(recv)) {
2299 return Value::Undef;
2300 }
2301 with_host(|h| {
2302 let proto = h.new_object(IndexMap::new());
2303 if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
2304 p.insert("constructor".to_string(), recv.clone());
2305 }
2306 h.hide_prop(&proto, "constructor");
2307 h.set_fn_prop(recv, "prototype", proto.clone());
2308 proto
2309 })
2310}
2311
2312/// The numeric constants a core namespace owns, in the order node reports them
2313/// under `getOwnPropertyNames`. ONE table rather than a value match plus a name
2314/// list: the enumeration and the read have to agree, and they did not — every
2315/// one of these read correctly while `Object.getOwnPropertyNames(Math)` omitted
2316/// all eight of Math's, so a member that plainly exists was invisible to any
2317/// reflective copy of the namespace.
2318///
2319/// Each is `{ writable: false, enumerable: false, configurable: false }`, which
2320/// is what separates them from the methods alongside them.
2321pub fn namespace_constants(ns: &str) -> &'static [(&'static str, f64)] {
2322 const MATH: &[(&str, f64)] = &[
2323 ("E", std::f64::consts::E),
2324 ("LN10", std::f64::consts::LN_10),
2325 ("LN2", std::f64::consts::LN_2),
2326 ("LOG10E", std::f64::consts::LOG10_E),
2327 ("LOG2E", std::f64::consts::LOG2_E),
2328 ("PI", std::f64::consts::PI),
2329 ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2),
2330 ("SQRT2", std::f64::consts::SQRT_2),
2331 ];
2332 const NUMBER: &[(&str, f64)] = &[
2333 ("MAX_VALUE", f64::MAX),
2334 // The smallest positive value a Number can hold, which is the
2335 // smallest SUBNORMAL double (`5e-324`), not Rust's
2336 // `f64::MIN_POSITIVE` — that is the smallest *normal* double,
2337 // `2.2250738585072014e-308`, ~256 binary orders of magnitude too
2338 // large.
2339 // The literal, not `f64::from_bits(1)`: that is only const-callable from
2340 // Rust 1.83 and this crate's MSRV is 1.80. It parses to the same
2341 // bit pattern — the smallest positive subnormal.
2342 ("MIN_VALUE", 5e-324),
2343 ("NaN", f64::NAN),
2344 ("NEGATIVE_INFINITY", f64::NEG_INFINITY),
2345 ("POSITIVE_INFINITY", f64::INFINITY),
2346 ("MAX_SAFE_INTEGER", 9007199254740991.0),
2347 ("MIN_SAFE_INTEGER", -9007199254740991.0),
2348 ("EPSILON", f64::EPSILON),
2349 ];
2350 match ns {
2351 "Math" => MATH,
2352 "Number" => NUMBER,
2353 _ => &[],
2354 }
2355}
2356
2357/// The descriptor of `<ns>.<key>`, whose attributes fall into four groups —
2358/// measured on node v26.8.1:
2359///
2360/// ```text
2361/// Math.PI, Number.MAX_SAFE_INTEGER, Number.prototype w=false e=false c=false
2362/// Math.max.name, Math.max.length w=false e=false c=true
2363/// Math.floor, Array.from, Array.prototype.slice w=true e=false c=true
2364/// require('path').join w=true e=true c=true
2365/// ```
2366///
2367/// So: a constant (and a constructor's `prototype`) is frozen, a function's own
2368/// `name`/`length` is read-only but configurable, and everything else is an
2369/// ordinary method — enumerable exactly when the namespace enumerates it, which
2370/// is what separates a core module's exports from an ECMAScript namespace's.
2371fn builtin_member_descriptor(ns: &str, key: &str, value: Value) -> Value {
2372 let frozen = namespace_constants(ns).iter().any(|(k, _)| *k == key)
2373 || key == "prototype"
2374 || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key));
2375 let own_fn_meta = matches!(key, "name" | "length") && host::builtin_is_callable(ns);
2376 // A key a SCRIPT assigned is an ordinary writable/enumerable/configurable
2377 // data property, whatever the namespace's built-in members look like — the
2378 // synthesized answer reported it non-enumerable, so a monkey-patched member
2379 // described itself as one of the intrinsics.
2380 let assigned = !intrinsic_proto_member(ns, key)
2381 && !crate::stdlib::namespace_keys(ns).iter().any(|k| k == key)
2382 && with_host(|h| h.builtin_static(ns, key).is_some());
2383 let enumerable = assigned
2384 || (!frozen && !own_fn_meta && crate::stdlib::namespace_keys(ns).iter().any(|k| k == key));
2385 with_host(|h| {
2386 let mut m: IndexMap<String, Value> = IndexMap::new();
2387 m.insert("value".into(), value);
2388 m.insert(
2389 "writable".into(),
2390 Value::Bool(assigned || (!frozen && !own_fn_meta)),
2391 );
2392 m.insert("enumerable".into(), Value::Bool(enumerable));
2393 m.insert("configurable".into(), Value::Bool(assigned || !frozen));
2394 h.new_object(m)
2395 })
2396}
2397
2398/// Whether `<ns>.<key>` may be deleted — the `configurable` half of
2399/// [`builtin_member_descriptor`], split out so `delete` can ask without
2400/// building a descriptor object.
2401/// Whether `key` is one of the members the intrinsic prototype namespace `ns`
2402/// really defines — as opposed to a name a script added. An assignment over one
2403/// of these is a `[[Set]]` and leaves its attributes alone.
2404fn intrinsic_proto_member(ns: &str, key: &str) -> bool {
2405 intrinsic_proto_members(ns).is_some_and(|members| {
2406 members
2407 .iter()
2408 .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
2409 })
2410}
2411
2412fn builtin_member_configurable(ns: &str, key: &str) -> bool {
2413 !(namespace_constants(ns).iter().any(|(k, _)| *k == key)
2414 || key == "prototype"
2415 || (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key)))
2416}
2417
2418/// The value of `<ns>.<name>` when it is one of those constants.
2419fn namespace_constant(ns: &str, name: &str) -> Option<f64> {
2420 namespace_constants(ns)
2421 .iter()
2422 .find(|(k, _)| *k == name)
2423 .map(|(_, v)| *v)
2424}
2425
2426/// Whether `ctor` is a WebIDL interface, whose prototype members are plain
2427/// assigned — and so ENUMERABLE — rather than the non-enumerable ones an
2428/// ECMAScript builtin defines. The generated member table records the same
2429/// distinction with its `+` prefix.
2430fn is_webidl_proto(ctor: &str) -> bool {
2431 intrinsic_proto_members(&format!("{ctor}.prototype"))
2432 .is_some_and(|ms| ms.iter().any(|m| m.starts_with('+')))
2433}
2434
2435/// The intrinsic constructor a value's own kind implies — the prototype it
2436/// inherits with no explicit link.
2437pub(crate) fn own_ctor_name(h: &host::JsHost, v: &Value) -> Option<&'static str> {
2438 default_ctor_name(h, v)
2439}
2440
2441/// Whether `ctor.prototype` defines `key` as a NON-WRITABLE data property, so
2442/// an object inheriting it refuses an assignment to that name.
2443pub(crate) fn is_proto_readonly(ctor: &str, key: &str) -> bool {
2444 crate::arity::PROTO_READONLY
2445 .binary_search_by(|(k, _)| (*k).cmp(ctor))
2446 .ok()
2447 .is_some_and(|i| crate::arity::PROTO_READONLY[i].1.contains(&key))
2448}
2449
2450/// Whether `ctor.prototype` defines `key` as an ACCESSOR rather than a data
2451/// property or a method.
2452pub(crate) fn is_proto_accessor(ctor: &str, key: &str) -> bool {
2453 crate::arity::PROTO_ACCESSORS
2454 .binary_search_by(|(k, _)| (*k).cmp(ctor))
2455 .ok()
2456 .is_some_and(|i| crate::arity::PROTO_ACCESSORS[i].1.contains(&key))
2457}
2458
2459/// The constructor whose `.prototype` IS `recv`, whichever of the two
2460/// representations it uses — a `Builtin` namespace handle or a real object.
2461pub(crate) fn intrinsic_proto_of(recv: &Value) -> Option<String> {
2462 with_host(|h| match h.get(recv) {
2463 Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
2464 _ => h.intrinsic_proto_ctor(recv).map(str::to_string),
2465 })
2466}
2467
2468/// The getter function of an intrinsic prototype accessor, as a first-class
2469/// value — what `Object.getOwnPropertyDescriptor(Map.prototype, 'size').get`
2470/// hands back, and the form a library uses to borrow one.
2471fn proto_getter(ctor: &str, key: &str) -> Value {
2472 with_host(|h| h.alloc(JsObj::Builtin(format!("@protoget:{ctor}:{key}"))))
2473}
2474
2475/// Whether `recv` carries the internal slot `ctor`'s accessor demands. This is
2476/// a BRAND check, not a chain walk: `Object.create(Map.prototype).size` throws
2477/// in node even though `Map.prototype` is right there on the chain.
2478fn brand_matches(recv: &Value, ctor: &str) -> bool {
2479 if let Some(tag) = crate::stdlib::native_tag(recv) {
2480 if tag == ctor || (ctor == "TypedArray" && tag == "TypedArray") {
2481 return true;
2482 }
2483 }
2484 match ctor {
2485 "TypedArray" => crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray"),
2486 "ArrayBuffer" => with_host(
2487 |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@bytes")),
2488 ),
2489 _ => {
2490 let own = match wrapped_primitive(recv).as_ref().and_then(wrapper_ctor_of) {
2491 Some(c) => Some(c),
2492 None => with_host(|h| default_ctor_name(h, recv)),
2493 };
2494 own == Some(ctor)
2495 }
2496 }
2497}
2498
2499thread_local! {
2500 /// The `(ctor, key)` prototype accessors whose tail read is in flight.
2501 ///
2502 /// A getter's last step reads the value off the receiver, and when the
2503 /// receiver does not STORE it that read walks the chain, finds the same
2504 /// accessor and runs it again: `new TextDecoder().fatal` recursed until the
2505 /// stack overflowed and aborted the process. An accessor already in flight
2506 /// answers `undefined` for its own key rather than re-entering — the value
2507 /// a missing internal slot has, and what node reports for one.
2508 static GETTERS_IN_FLIGHT: std::cell::RefCell<Vec<(String, String)>> =
2509 const { std::cell::RefCell::new(Vec::new()) };
2510}
2511
2512/// Whether `ctor`'s `key` getter is already running further down the stack.
2513fn getter_in_flight(ctor: &str, key: &str) -> bool {
2514 GETTERS_IN_FLIGHT.with(|g| g.borrow().iter().any(|(c, k)| c == ctor && k == key))
2515}
2516
2517/// Invoke an intrinsic prototype's getter against `recv` — the body behind the
2518/// `@protoget:` thunks.
2519///
2520/// Reading one OFF THE PROTOTYPE (`Map.prototype.size`) is the case that was
2521/// wrong: it answered `undefined` where node runs the getter, fails the brand
2522/// check and throws. `RegExp.prototype` is the documented exception — 22.2.6.10
2523/// and .13 return `"(?:)"` and `""` for it specifically, so the one receiver
2524/// that would otherwise throw for every flag reads two of them back.
2525pub(crate) fn proto_getter_call(ctor: &str, key: &str, recv: &Value) -> Result<Value, String> {
2526 let is_the_prototype = with_host(
2527 |h| matches!(h.get(recv), Some(JsObj::Builtin(ns)) if *ns == format!("{ctor}.prototype")),
2528 );
2529 if is_the_prototype && ctor == "RegExp" {
2530 // 22.2.6.x each carry the same step: when `this` IS `%RegExp.prototype%`
2531 // the getter returns rather than throwing. `source` and `flags` have
2532 // their own values there; every flag getter answers `undefined`.
2533 return Ok(match key {
2534 "source" => with_host(|h| h.new_str("(?:)".to_string())),
2535 "flags" => with_host(|h| h.new_str(String::new())),
2536 _ => Value::Undef,
2537 });
2538 }
2539 // `RegExp.prototype.flags` (22.2.6.5) is the one that is GENERIC: it reads
2540 // the individual flag properties off whatever object it is handed and
2541 // concatenates their letters, so a plain object answers `""` rather than
2542 // throwing, and one carrying `global`/`ignoreCase` answers `"gi"`.
2543 if ctor == "RegExp" && key == "flags" && !brand_matches(recv, ctor) {
2544 if !with_host(|h| is_object_like(h, recv)) {
2545 return Err(regexp_brand_error(key, recv));
2546 }
2547 let mut out = String::new();
2548 for (prop, letter) in REGEXP_FLAG_LETTERS {
2549 let v = get_property(recv, prop)?;
2550 if with_host(|h| h.truthy(&v)) {
2551 out.push(*letter);
2552 }
2553 }
2554 return Ok(with_host(|h| h.new_str(out)));
2555 }
2556 // `Function.prototype.arguments`/`caller` are POISON PILLS (10.2.4.1): both
2557 // the getter and the setter throw for every receiver, which is how a strict
2558 // function keeps its caller unreachable. They are not brand checks and do
2559 // not name the receiver.
2560 if ctor == "Function" && matches!(key, "arguments" | "caller") {
2561 return poison_pill_read(recv);
2562 }
2563 if !brand_matches(recv, ctor) {
2564 return Err(match ctor {
2565 "RegExp" => regexp_brand_error(key, recv),
2566 "Symbol" => {
2567 host::type_error("Symbol.prototype.description requires that 'this' be a Symbol")
2568 }
2569 _ => host::type_error(&format!(
2570 "Method get {ctor}.prototype.{key} called on incompatible receiver {}",
2571 brand_receiver_string(recv)
2572 )),
2573 });
2574 }
2575 // A native instance keeps an accessor's value in the hidden `@@<key>` slot,
2576 // so that the public name can be a getter on the prototype rather than an
2577 // own enumerable property. Read it straight: the chain walk below would
2578 // find this same accessor and run it again.
2579 if let Some(v) = with_host(|h| match h.get(recv) {
2580 Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
2581 _ => None,
2582 }) {
2583 return Ok(v);
2584 }
2585 GETTERS_IN_FLIGHT.with(|g| g.borrow_mut().push((ctor.to_string(), key.to_string())));
2586 let out = get_property(recv, key);
2587 GETTERS_IN_FLIGHT.with(|g| {
2588 g.borrow_mut().pop();
2589 });
2590 out
2591}
2592
2593/// `Function.prototype.arguments`/`caller` read against `recv`.
2594///
2595/// The pill is conditional and the condition is the RECEIVER, not the reading
2596/// code: a sloppy non-arrow function answers `null` (node stopped populating
2597/// these long ago but kept them readable), and everything else — an arrow, a
2598/// strict function, a non-function — throws. Keying it on the READER's
2599/// strictness, which is what this did, made `strictFn.arguments` answer
2600/// `undefined` from sloppy code and a sloppy function throw from strict code:
2601/// wrong in both directions.
2602pub(crate) fn poison_pill_read(recv: &Value) -> Result<Value, String> {
2603 if with_host(|h| h.fn_is_sloppy(recv)) {
2604 return Ok(with_host(|h| h.null()));
2605 }
2606 Err(host::type_error(POISON_PILL))
2607}
2608
2609/// The message both halves of the `arguments`/`caller` poison pill throw.
2610pub(crate) const POISON_PILL: &str = "'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them";
2611
2612/// How a REJECTED receiver is rendered in a brand-check message.
2613///
2614/// `no_side_effects_string` answers for most of them, but two kinds differ:
2615/// an intrinsic PROTOTYPE renders `#<Map>` rather than `[object Map]`, and so
2616/// does an `ArrayBuffer`/`DataView` instance, which this host tags natively and
2617/// that function therefore brands. Node draws the line at whether the value is
2618/// one of the ES5-era classes (`Array`, `Date`, `RegExp` are `[object X]`); the
2619/// two cases here are the ones that fall on the other side of it.
2620fn brand_receiver_string(recv: &Value) -> String {
2621 if let Some(ctor) = intrinsic_proto_of(recv) {
2622 return format!("#<{ctor}>");
2623 }
2624 match crate::stdlib::native_tag(recv).as_deref() {
2625 Some(tag @ ("ArrayBuffer" | "DataView")) => format!("#<{tag}>"),
2626 _ => no_side_effects_string(recv),
2627 }
2628}
2629
2630/// The flag properties `RegExp.prototype.flags` reads, in the order 22.2.6.5
2631/// concatenates their letters.
2632const REGEXP_FLAG_LETTERS: &[(&str, char)] = &[
2633 ("hasIndices", 'd'),
2634 ("global", 'g'),
2635 ("ignoreCase", 'i'),
2636 ("multiline", 'm'),
2637 ("dotAll", 's'),
2638 ("unicode", 'u'),
2639 ("unicodeSets", 'v'),
2640 ("sticky", 'y'),
2641];
2642
2643/// `RegExp.prototype`'s flag getters word their brand failure their own way,
2644/// and `flags` distinguishes a non-object receiver from a non-RegExp one
2645/// because 22.2.6.5 reads the individual flags off any object it is given.
2646fn regexp_brand_error(key: &str, recv: &Value) -> String {
2647 if key == "flags" && !with_host(|h| matches!(recv, Value::Obj(_)) && !h.is_null(recv)) {
2648 return host::type_error(&format!(
2649 "RegExp.prototype.flags getter called on non-object {}",
2650 no_side_effects_string(recv)
2651 ));
2652 }
2653 host::type_error(&format!(
2654 "RegExp.prototype.{key} getter called on non-RegExp object"
2655 ))
2656}
2657
2658/// A property on a builtin namespace object (`Math.PI`, `Number.MAX_SAFE_INTEGER`,
2659/// `console.log`).
2660pub fn namespace_property(ns: &str, name: &str) -> Value {
2661 // `require.cache[id]` — a LIVE view of the module cache, not a copy, so a
2662 // read sees whatever is loaded now and `delete` (see `delete_property`)
2663 // actually invalidates.
2664 if ns == REQUIRE_CACHE {
2665 return crate::module::cache_get(name).unwrap_or(Value::Undef);
2666 }
2667 // A property a SCRIPT assigned onto this namespace wins over everything
2668 // synthesized below, including a member the namespace really has. That is
2669 // what monkey-patching an intrinsic is: `Array.prototype.join = f` must make
2670 // `[1, 2].join()` call `f`, and a polyfill's `Array.prototype.at = impl` has
2671 // to read back at all. Only the two `Error` hooks consulted this table, so
2672 // every other assignment onto a builtin — the whole polyfill idiom — was
2673 // stored by `set_property` and then never read: the write appeared to
2674 // succeed, `Object.isExtensible` said true, and the value came back
2675 // `undefined`.
2676 if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2677 return v;
2678 }
2679 // The ENTRY script's `require` is this builtin rather than the per-module
2680 // closure, so its `cache` has to be handed out here too.
2681 // `require.extensions` — the legacy loader map. Deprecated but still read
2682 // (and sometimes written) by tooling that hooks module loading, and it was
2683 // absent entirely. The three keys node ships are present; installing a
2684 // custom loader through them is NOT honoured by this runtime's loader, so
2685 // the map reports what it can serve rather than pretending otherwise.
2686 // `util.promisify.custom` — the registered symbol a module attaches to a
2687 // callback function to supply its own promisified form. It was `undefined`,
2688 // so the lookup that decides whether to use one always missed.
2689 if ns == "util.promisify" && name == "custom" {
2690 return with_host(|h| h.symbol_for("nodejs.util.promisify.custom"));
2691 }
2692 // `process.memoryUsage.rss()` — node's fast path for the one figure that
2693 // does not need the whole object built.
2694 if ns == "process.memoryUsage" && name == "rss" {
2695 return with_host(|h| h.alloc(JsObj::Builtin("process.memoryUsage.rss".to_string())));
2696 }
2697 if ns == "require" && name == "extensions" {
2698 return with_host(|h| {
2699 let mut m: IndexMap<String, Value> = IndexMap::new();
2700 for ext in [".js", ".json", ".node"] {
2701 let f = h.alloc(JsObj::Builtin(format!("@@extension:{ext}")));
2702 m.insert(ext.to_string(), f);
2703 }
2704 h.new_object(m)
2705 });
2706 }
2707 // `require.resolve.paths(spec)` — the directories a lookup would search:
2708 // `null` for a core module, the `node_modules` chain otherwise.
2709 if ns == "require.resolve" && name == "paths" {
2710 return with_host(|h| h.alloc(JsObj::Builtin("require.resolve.paths".to_string())));
2711 }
2712 if ns == "require" && name == "cache" {
2713 return with_host(|h| h.alloc(JsObj::Builtin(REQUIRE_CACHE.to_string())));
2714 }
2715 // The legacy numeric codes `DOMException` carries as statics
2716 // (`DOMException.ABORT_ERR` is 20), named by uppercasing the error name.
2717 if ns == "DOMException" {
2718 if let Some((_, code)) = DOM_EXCEPTION_CODES
2719 .iter()
2720 .find(|(n, _)| legacy_code_name(n) == name)
2721 {
2722 return Value::Float(*code);
2723 }
2724 }
2725 // Numeric constants.
2726 if let Some(k) = namespace_constant(ns, name) {
2727 return Value::Float(k);
2728 }
2729 // `Ctor.name` on a builtin constructor is the constructor name (`Array.name`
2730 // === "Array"); non-callable namespaces (`Math`/`JSON`) fall through to
2731 // `undefined`.
2732 // `GeneratorFunction.prototype` and the two async variants are REAL objects
2733 // in `native_protos`, not `Builtin("X.prototype")` namespace handles — they
2734 // sit on the prototype chain of every generator/async function, which a
2735 // handle cannot do. Without this the read fell through to `undefined`.
2736 if name == "prototype"
2737 && matches!(
2738 ns,
2739 "GeneratorFunction" | "AsyncFunction" | "AsyncGeneratorFunction"
2740 )
2741 {
2742 return with_host(|h| {
2743 h.ensure_native_protos();
2744 h.native_proto(ns).unwrap_or(Value::Undef)
2745 });
2746 }
2747 // `Error.prepareStackTrace` has a DEFAULT hook in node
2748 // (`ErrorPrepareStackTrace`), so a library probing `if
2749 // (Error.prepareStackTrace)` finds one. Reading `undefined` sent that probe
2750 // down the wrong branch. The default renders the header plus the frames,
2751 // which is what the fast path in `materialize_stack` already produces — it
2752 // recognises this exact builtin and skips the round trip.
2753 if ns == "Error" && name == "prepareStackTrace" {
2754 return with_host(|h| h.builtin_static("Error", "prepareStackTrace")).unwrap_or_else(
2755 || with_host(|h| h.alloc(JsObj::Builtin(DEFAULT_PREPARE.to_string()))),
2756 );
2757 }
2758 // `Error.stackTraceLimit` defaults to 10 and is settable; an assignment
2759 // lands in the builtin-static side table, which the read below consults
2760 // first. Without a default the READ was `undefined`, so a library doing
2761 // `const old = Error.stackTraceLimit` and restoring it later installed
2762 // `undefined` and disabled the limit permanently.
2763 if ns == "Error" && name == "stackTraceLimit" {
2764 return with_host(|h| h.builtin_static("Error", "stackTraceLimit"))
2765 .unwrap_or(Value::Float(10.0));
2766 }
2767 // `Ctor[Symbol.species]` is an accessor returning `this` on every builtin
2768 // that has one (23.1.2.5, 27.2.4.7, …). It was absent, so the species
2769 // protocol had nothing to read and every derived result came back a plain
2770 // builtin.
2771 if name == "@@species" && has_species(ns) {
2772 return with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())));
2773 }
2774 if name == "name" && is_builtin_ctor(ns) {
2775 return with_host(|h| h.new_str(ns.to_string()));
2776 }
2777 // A well-known symbol (`Symbol.iterator`, `Symbol.toPrimitive`, …) used as a
2778 // computed property/method key.
2779 if ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&name) {
2780 return with_host(|h| h.well_known_symbol(name));
2781 }
2782 // Non-function constants on a stdlib namespace (`path.sep`, `os.EOL`,
2783 // `buffer.Buffer`, `url.URL`).
2784 if let Some(v) = crate::stdlib::constant(ns, name) {
2785 return v;
2786 }
2787 // `Ctor.prototype` on a builtin constructor (`Object.prototype`,
2788 // `Array.prototype`, …): a prototype namespace whose methods are callable
2789 // thunks (`Object.prototype.toString.call(x)` is a load-time idiom in the
2790 // `get-intrinsic`/`function-bind` family).
2791 if name == "prototype" && is_builtin_ctor(ns) {
2792 // Same reasoning as the native prototypes below, for the error
2793 // hierarchy: `new Error(...)` links its `[[Prototype]]` to the REAL
2794 // `error_protos` object, so `Error.prototype` has to read back that same
2795 // object. It resolved to a fresh `Builtin("Error.prototype")` thunk
2796 // instead, which is a FUNCTION — so `Object.getPrototypeOf(new
2797 // Error("x")) === Error.prototype` was false, and `typeof
2798 // Error.prototype` was `"function"` where node says `"object"`.
2799 if host::ERROR_NAMES.contains(&ns) {
2800 if let Some(p) = with_host(|h| {
2801 h.ensure_error_protos();
2802 host::error_proto_of(h, ns)
2803 }) {
2804 return p;
2805 }
2806 }
2807 // `Buffer`/`Uint8Array` have real prototype *objects* — a Buffer's
2808 // `[[Prototype]]` points at one, so `Object.getPrototypeOf(buf) ===
2809 // Buffer.prototype` must compare equal, which a freshly-allocated
2810 // `Builtin` handle never can.
2811 if let Some(p) = with_host(|h| {
2812 h.ensure_native_protos();
2813 h.native_proto(ns)
2814 }) {
2815 return p;
2816 }
2817 let _ = ns;
2818 return with_host(|h| h.alloc(JsObj::Builtin(format!("{ns}.prototype"))));
2819 }
2820 // A NATIVE stdlib constructor's `.prototype` (`StringDecoder`, `Hash`,
2821 // `URLSearchParams`, …). These are absent from `is_builtin_ctor`, so the arm
2822 // above never fired and the read produced `undefined` — which broke the ES5
2823 // subclassing pattern libraries still ship. `iconv-lite`'s internal codec
2824 // reads `StringDecoder.prototype.end` at load, and threw
2825 // `Cannot read properties of undefined (reading 'end')`. Built from the same
2826 // instance-method table a method read consults, so the two cannot disagree.
2827 if name == "prototype" {
2828 if let Some(p) = with_host(|h| h.ensure_ctor_proto(ns)) {
2829 return p;
2830 }
2831 }
2832 // A method read off a builtin prototype namespace (`Array.prototype.slice`):
2833 // a `@proto:<Ctor>:<method>` thunk that, when invoked (typically via
2834 // `.call`/`.apply`), dispatches `method` against the invoke-time `this`.
2835 //
2836 // The thunk is minted only for a name the prototype REALLY carries. Minting
2837 // one unconditionally made every absent name answer with a function:
2838 // `Array.prototype.totallyBogus` was `[Function: totallyBogus]` where node
2839 // says `undefined`, and so was every well-known symbol a prototype does not
2840 // define — `Array.prototype[Symbol.toStringTag]` came back a function
2841 // instead of `undefined`, which is a value `Object.prototype.toString` and
2842 // every `typeof`/truthiness test downstream then read wrong.
2843 //
2844 // Existence is decided by the generated intrinsic table, which is read out
2845 // of the reference engine, so this cannot drift from what node defines.
2846 // A name the prototype does not define but `Object.prototype` does is
2847 // INHERITED, and node hands back Object.prototype's own function object
2848 // (`Map.prototype.toString === Object.prototype.toString` is `true`), so it
2849 // resolves to the `Object` thunk rather than a per-ctor one. That is also
2850 // what makes `String(Map.prototype)` print `[object Map]`: `Map.prototype`
2851 // has no own `toString`, and the inherited one is the generic tag reader,
2852 // not a Map method that rejects a non-Map `this`.
2853 if let Some(ctor) = ns.strip_suffix(".prototype") {
2854 // `Array.prototype[Symbol.unscopables]` (23.1.3.38) is a DATA property,
2855 // not an intrinsic function, so it is not in the arity table the lookup
2856 // above consults. It lists the methods a `with` block must NOT bring
2857 // into scope — the ones added after `with` existed, so old code using a
2858 // variable of the same name keeps working.
2859 if name == "@@unscopables" && ctor == "Array" {
2860 return with_host(|h| {
2861 let mut m: IndexMap<String, Value> = IndexMap::new();
2862 for k in [
2863 "at",
2864 "copyWithin",
2865 "entries",
2866 "fill",
2867 "find",
2868 "findIndex",
2869 "findLast",
2870 "findLastIndex",
2871 "flat",
2872 "flatMap",
2873 "includes",
2874 "keys",
2875 "toReversed",
2876 "toSorted",
2877 "toSpliced",
2878 "values",
2879 ] {
2880 m.insert(k.to_string(), Value::Bool(true));
2881 }
2882 let o = h.new_object(m);
2883 let null = h.null();
2884 h.set_proto(&o, null);
2885 o
2886 });
2887 }
2888 if builtin_meta(&format!("@proto:{ctor}:{name}")).is_some() {
2889 return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:{ctor}:{name}"))));
2890 }
2891 if ctor != "Object" && builtin_meta(&format!("@proto:Object:{name}")).is_some() {
2892 return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:Object:{name}"))));
2893 }
2894 // `constructor` is excluded from the table because it is not a method:
2895 // it is the constructor function itself, and node compares equal
2896 // (`Array.prototype.constructor === Array`). It used to resolve to a
2897 // `@proto:Array:constructor` thunk, which is a different object every
2898 // read and so never compared equal to anything.
2899 if name == "constructor" && is_builtin_ctor(ctor) {
2900 return with_host(|h| h.alloc(JsObj::Builtin(ctor.to_string())));
2901 }
2902 return Value::Undef;
2903 }
2904 let qualified = format!("{ns}.{name}");
2905 if is_known_builtin(&qualified) {
2906 return with_host(|h| h.alloc(JsObj::Builtin(qualified)));
2907 }
2908 // A property the user stuck on this builtin namespace (`Error.prepareStackTrace`).
2909 if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
2910 return v;
2911 }
2912 // A builtin FUNCTION's own `name` and `length` (10.3.3-4: every one has
2913 // both). `Math.max.name` was `undefined` — as was every `.name` a library
2914 // reads to identify a callback it was handed. The non-callable namespaces
2915 // fall through: `Math.name` and `require('fs').length` really are undefined.
2916 if host::builtin_is_callable(ns) {
2917 match name {
2918 "name" => {
2919 if let Some(n) = proto_getter_name(ns) {
2920 return with_host(|h| h.new_str(n));
2921 }
2922 return with_host(|h| h.new_str(builtin_name(ns).to_string()));
2923 }
2924 // Only the intrinsics have a specified arity; a core-module
2925 // function's is a property of node's own JS source, so it stays
2926 // `undefined` rather than being invented here.
2927 "length" => {
2928 // A getter takes no argument (10.2.9 / the accessor grammar),
2929 // so its `length` is 0 — it is not in the intrinsic table,
2930 // which holds only named functions.
2931 if proto_getter_name(ns).is_some() {
2932 return Value::Float(0.0);
2933 }
2934 if let Some((_, len)) = builtin_meta(ns) {
2935 return Value::Float(len as f64);
2936 }
2937 }
2938 _ => {}
2939 }
2940 }
2941 Value::Undef
2942}
2943
2944/// Dispatch a `@proto:<Ctor>:<method>` thunk (a method read off a builtin
2945/// prototype, e.g. `Object.prototype.toString`) against `recv` (its invoke-time
2946/// `this`). `Object.prototype.toString` yields the `[object Tag]` brand string
2947/// libraries type-check on; every other method routes through normal method
2948/// dispatch on `recv`.
2949/// The TypeError a `<Ctor>.prototype.<method>` thunk throws when it is invoked
2950/// with NO receiver — `const f = [].push; f(1)`.
2951///
2952/// Reading a method off an instance used to mint a thunk bound to that
2953/// instance, so a detached method silently kept working on the object it came
2954/// from. Now that it is the shared intrinsic, a bare call has no `this` and has
2955/// to say so. Node words it four ways, and which one a method gets is not
2956/// something that can be derived — the split was measured across every method
2957/// of each prototype:
2958///
2959/// ```text
2960/// ToObject(this) "Cannot convert undefined or null to object"
2961/// RequireObjectCoercible "<Ctor>.prototype.<m> called on null or undefined"
2962/// brand check "<Ctor>.prototype.<m> requires that 'this' be a <X>"
2963/// everything else the generic incompatible-receiver message
2964/// ```
2965fn nullish_receiver_error(ctor: &str, method: &str, recv: &str) -> Option<String> {
2966 // `Array.prototype` splits: the CALLBACK-taking methods plus `concat` and
2967 // the two `indexOf` family members name themselves, the rest go through
2968 // `ToObject` and report its message.
2969 const ARRAY_NAMED: &[&str] = &[
2970 "concat",
2971 "every",
2972 "filter",
2973 "find",
2974 "findIndex",
2975 "findLast",
2976 "findLastIndex",
2977 "forEach",
2978 "indexOf",
2979 "map",
2980 "reduce",
2981 "reduceRight",
2982 "some",
2983 ];
2984 const TO_OBJECT: &str = "Cannot convert undefined or null to object";
2985 let named = |c: &str| format!("{c}.prototype.{method} called on null or undefined");
2986 let branded =
2987 |c: &str, want: &str| format!("{c}.prototype.{method} requires that 'this' be a {want}");
2988 // The generic form names the receiver, so a `null` one must not be reported
2989 // as `undefined`.
2990 let generic = |c: &str, m: &str| {
2991 format!("Method {c}.prototype.{m} called on incompatible receiver {recv}")
2992 };
2993 Some(match ctor {
2994 "Array" if ARRAY_NAMED.contains(&method) => named("Array"),
2995 "Array" => TO_OBJECT.to_string(),
2996 // `Object.prototype.toString` is the one method that ACCEPTS a nullish
2997 // receiver — it answers `[object Undefined]`.
2998 "Object" if method == "toString" => return None,
2999 "Object" if method == "toLocaleString" => named("Object"),
3000 "Object" => TO_OBJECT.to_string(),
3001 // Both aliases report the LEGACY name in the message, which is the one
3002 // place `name` and the message disagree.
3003 "String" if method == "trimStart" => named("String").replace("trimStart", "trimLeft"),
3004 "String" if method == "trimEnd" => named("String").replace("trimEnd", "trimRight"),
3005 "String" if matches!(method, "toString" | "valueOf") => branded("String", "String"),
3006 "String" => named("String"),
3007 "Number" => branded("Number", "Number"),
3008 "Boolean" => branded("Boolean", "Boolean"),
3009 "Symbol" => branded("Symbol", "Symbol"),
3010 "Function" if method == "bind" => "Bind must be called on a function".to_string(),
3011 "Function" if matches!(method, "call" | "apply") => format!(
3012 "Function.prototype.{method} was called on undefined, which is undefined and not a function"
3013 ),
3014 "Function" => branded("Function", "Function"),
3015 // `Promise.prototype.catch`/`finally` are written in terms of `then`, so
3016 // a nullish receiver fails inside them and reports that instead.
3017 "Promise" if method == "catch" => {
3018 "Cannot read properties of undefined (reading 'then')".to_string()
3019 }
3020 "Promise" if method == "finally" => {
3021 "Promise.prototype.finally called on non-object".to_string()
3022 }
3023 "Date" if method == "toJSON" => TO_OBJECT.to_string(),
3024 // The plain GETTERS and `valueOf` read `[[DateValue]]` directly and
3025 // report that slot check; every setter, every `to*String` and the two
3026 // legacy year methods go through the generic receiver check first.
3027 "Date"
3028 if method == "valueOf"
3029 || (method.starts_with("get") && method != "getYear") =>
3030 {
3031 "this is not a Date object.".to_string()
3032 }
3033 // An ALIAS reports the method it aliases: `toGMTString` IS `toUTCString`
3034 // and `Set.prototype.keys` IS `values`, one function object each.
3035 "Date" if method == "toGMTString" => generic("Date", "toUTCString"),
3036 "Set" if method == "keys" => generic("Set", "values"),
3037 // Everything else that is brand-checked names itself. Node reaches this
3038 // wording from a `[[GetOwnProperty]]`-style slot check; here the check
3039 // is the receiver's kind, and only the message has to agree.
3040 "ArrayBuffer" | "DataView" | "RegExp" | "WeakRef" | "Map" | "Set" | "WeakMap"
3041 | "WeakSet" | "Promise" | "Date" => generic(ctor, method),
3042 "URLSearchParams" => "Value of \"this\" must be of type URLSearchParams".to_string(),
3043 // Node's `URL` methods fail while reaching for their internal state, and
3044 // report the read that failed rather than the method.
3045 "URL" => "Cannot read properties of undefined (reading 'URL')".to_string(),
3046 _ => return None,
3047 })
3048}
3049
3050/// Whether `<ctor>.prototype.<method>` begins with a `this<Type>Value` brand
3051/// check (21.1.3, 20.3.3, 22.1.3.29/.35, 21.2.3). Every `Number.prototype`
3052/// method does; of `String.prototype` only `toString`/`valueOf` do — the rest
3053/// are generic and coerce their receiver with `ToString`.
3054fn is_brand_checked_primitive_method(ctor: &str, method: &str) -> bool {
3055 match ctor {
3056 "Number" => matches!(
3057 method,
3058 "toString" | "toLocaleString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision"
3059 ),
3060 "BigInt" => matches!(method, "toString" | "toLocaleString" | "valueOf"),
3061 "String" | "Boolean" => matches!(method, "toString" | "valueOf"),
3062 _ => false,
3063 }
3064}
3065
3066/// `this<Type>Value(recv)` for `ctor` ∈ Number/String/Boolean/BigInt: the
3067/// primitive itself, the primitive a wrapper boxes, or — for the three
3068/// prototypes that are themselves wrappers (21.1.3, 22.1.3, 20.3.3) — the
3069/// prototype's own `+0` / `""` / `false`. `None` is the TypeError case.
3070fn this_primitive_value(ctor: &str, recv: &Value) -> Option<Value> {
3071 let expected = match ctor {
3072 "Number" => "number",
3073 "String" => "string",
3074 "Boolean" => "boolean",
3075 "BigInt" => "bigint",
3076 _ => return None,
3077 };
3078 let is_expected = |v: &Value| with_host(|h| h.type_of(v)) == expected;
3079 if is_expected(recv) {
3080 return Some(recv.clone());
3081 }
3082 if let Some(prim) = wrapped_primitive(recv).filter(is_expected) {
3083 return Some(prim);
3084 }
3085 if with_host(|h| h.intrinsic_proto_ctor(recv) == Some(ctor)) {
3086 return match ctor {
3087 "Number" => Some(Value::Float(0.0)),
3088 "String" => Some(with_host(|h| h.new_str(""))),
3089 "Boolean" => Some(Value::Bool(false)),
3090 _ => None,
3091 };
3092 }
3093 None
3094}
3095
3096pub fn proto_method(recv: &Value, ctor_method: &str, args: Vec<Value>) -> Result<Value, String> {
3097 let (ctor, method) = ctor_method.split_once(':').unwrap_or(("", ctor_method));
3098 // A prototype ACCESSOR installed by `ensure_ctor_proto`: it reads or writes
3099 // the instance's hidden `@@<name>` slot, which is where the value lives now
3100 // that the public name is a getter rather than an own property.
3101 if let Some(key) = method.strip_prefix("@get@") {
3102 if let Some(v) = with_host(|h| match h.get(recv) {
3103 Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
3104 _ => None,
3105 }) {
3106 return Ok(v);
3107 }
3108 // No stored slot: the value is COMPUTED, so ask the class. `KeyObject`'s
3109 // `symmetricKeySize` is the secret's byte length, which nothing stores.
3110 let tag = crate::stdlib::native_tag(recv).unwrap_or_default();
3111 return crate::stdlib::instance_call(&tag, recv, method, args);
3112 }
3113 if let Some(key) = method.strip_prefix("@set@") {
3114 let v = args.first().cloned().unwrap_or(Value::Undef);
3115 with_host(|h| {
3116 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
3117 p.insert(format!("@@{key}"), v);
3118 }
3119 });
3120 crate::stdlib::instance_accessor_written(ctor, key, recv);
3121 return Ok(Value::Undef);
3122 }
3123 if with_host(|h| h.is_nullish(recv)) {
3124 let shown = if with_host(|h| h.is_null(recv)) {
3125 "null"
3126 } else {
3127 "undefined"
3128 };
3129 if let Some(msg) = nullish_receiver_error(ctor, method, shown) {
3130 return Err(format!("TypeError: {msg}"));
3131 }
3132 }
3133 // `Error.prototype.toString` (20.5.3.4): `name`, `message`, or `name:
3134 // message`, read off the chain so a subclass's `this.name = 'E'` is honored.
3135 if ctor == "Error" && method == "toString" {
3136 // A `DOMException` keeps its `name`/`message` in internal slots, so the
3137 // chain read below would find the class name on the prototype instead.
3138 if let Some(n) = dom_exception_slot(recv, "name") {
3139 let name = with_host(|h| h.str_of(&n));
3140 let msg = dom_exception_slot(recv, "message")
3141 .map(|m| with_host(|h| h.str_of(&m)))
3142 .unwrap_or_default();
3143 let s = if msg.is_empty() {
3144 name
3145 } else {
3146 format!("{name}: {msg}")
3147 };
3148 return Ok(with_host(|h| h.new_str(s)));
3149 }
3150 // `name` and `message` are read with `[[Get]]` (20.5.3.4 steps 3 and 5),
3151 // so a PROXY supplies them through its `get` trap. Reading the stored
3152 // ones first made `String(new Proxy(err, handler))` ignore the handler.
3153 let via_proxy = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy);
3154 let stored = (!via_proxy).then(|| with_host(|h| h.error_to_string(recv)));
3155 let s = match stored.flatten() {
3156 Some(s) => s,
3157 None => {
3158 let read = |k: &str| -> Result<Option<String>, String> {
3159 Ok(host::protocol_lookup(recv, k)?.map(|v| with_host(|h| h.str_of(&v))))
3160 };
3161 let name = read("name")?.unwrap_or_else(|| "Error".into());
3162 let msg = read("message")?.unwrap_or_default();
3163 if msg.is_empty() {
3164 name
3165 } else {
3166 format!("{name}: {msg}")
3167 }
3168 }
3169 };
3170 return Ok(with_host(|h| h.new_str(s)));
3171 }
3172 // The methods that read their receiver through `thisNumberValue` /
3173 // `thisBooleanValue` / `thisStringValue` / `thisBigIntValue` accept only the
3174 // primitive, its wrapper, or the prototype object (which carries the zero
3175 // value) — anything else is a TypeError naming the method. Unchecked,
3176 // `Number.prototype.valueOf.call({})` answered `{}`, `toFixed.call({})`
3177 // reported "toFixed is not a function", and `Number.prototype.valueOf()`
3178 // recursed through the generic conversion until the stack overflowed.
3179 if is_brand_checked_primitive_method(ctor, method) {
3180 let Some(prim) = this_primitive_value(ctor, recv) else {
3181 return Err(format!(
3182 "TypeError: {ctor}.prototype.{method} requires that 'this' be a {ctor}"
3183 ));
3184 };
3185 return host::call_method(&prim, method, args);
3186 }
3187 // A primitive wrapper's `toString`/`valueOf`/`toLocaleString`: unwrap and
3188 // answer as the boxed primitive does. `Number.prototype.toString.call(5)`
3189 // arrives with an already-primitive receiver and needs no unwrapping.
3190 if matches!(ctor, "String" | "Number" | "Boolean") {
3191 let prim = wrapped_primitive(recv).unwrap_or_else(|| recv.clone());
3192 return host::call_method(&prim, method, args);
3193 }
3194 // `thisSymbolValue`/`thisBigIntValue` (20.4.3, 21.2.3) accept a WRAPPER as
3195 // readily as the primitive, and neither was unwrapped here. A BigInt
3196 // wrapper's `valueOf` therefore re-entered the generic conversion, which
3197 // looked `valueOf` up again and called it again: `+Object(9n)` recursed
3198 // until the stack overflowed and ABORTED the process, which no try/catch can
3199 // see. A Symbol wrapper failed the brand check below instead and reported
3200 // that `this` was not a Symbol, when it is one. Only a real wrapper is
3201 // unwrapped — `Symbol.prototype` itself boxes nothing and still has to reach
3202 // the brand check.
3203 if matches!(ctor, "Symbol" | "BigInt") {
3204 if let Some(prim) = wrapped_primitive(recv) {
3205 return host::call_method(&prim, method, args);
3206 }
3207 }
3208 if ctor == "Object" && method == "toString" {
3209 // Steps 16-17 of 20.1.3.6: a `Symbol.toStringTag` STRING on the receiver
3210 // (own or inherited, data property or getter) replaces the builtin brand,
3211 // which is how a class advertises its own (`class C { get
3212 // [Symbol.toStringTag]() { return 'Cee' } }` → `[object Cee]`). The read
3213 // runs outside the host borrow so an accessor can be invoked.
3214 // A Proxy has no chain to probe: 20.1.3.6 step 15 is an unconditional
3215 // `Get(O, @@toStringTag)`, so the `get` trap decides. Probing first (as
3216 // the ordinary receiver does, to keep the read off objects that have no
3217 // tag) would always miss and brand every tagged proxy `[object Object]`.
3218 if let Some(s) = to_string_tag(recv)? {
3219 return Ok(with_host(|h| h.new_str(format!("[object {s}]"))));
3220 }
3221 return Ok(with_host(|h| h.new_str(object_tag(h, recv))));
3222 }
3223 // These thunks now live on the real `Object.prototype` object, i.e. on the
3224 // receiver's own chain — routing back through `call_method` would re-resolve
3225 // this very thunk and recurse.
3226 if ctor == "Object" && is_object_builtin_method(method) {
3227 return object_builtin_method(recv, method, args);
3228 }
3229 // `EventEmitter.prototype.<m>` mixed onto a receiver (express's `app`): run the
3230 // emitter method directly against `recv` (routing back through `call_method`
3231 // would re-resolve the mixed-in thunk and recurse).
3232 if ctor == "EventEmitter" {
3233 return crate::stdlib::events::instance_call(recv, method, args);
3234 }
3235 // Same recursion hazard for the exotics with a real prototype object: the
3236 // thunk now lives ON the receiver's prototype chain, so `call_method` would
3237 // re-resolve this very thunk. Dispatch straight to the native instance
3238 // implementation when the receiver is in fact an instance of `ctor`.
3239 if ctor == "Buffer" && crate::stdlib::native_tag(recv).as_deref() == Some("Buffer") {
3240 return crate::stdlib::buffer::instance_call(recv, method, &args);
3241 }
3242 // The shared typed-array methods now live on the `%TypedArray%.prototype`
3243 // intermediate, so their thunks are tagged `TypedArray`; `Uint8Array` still
3244 // appears for anything read directly off `Uint8Array.prototype`. Both
3245 // dispatch the same way, and both must bypass `call_method` or the thunk
3246 // would re-resolve itself off the receiver's chain and recurse.
3247 if ctor == "Uint8Array" || ctor == "TypedArray" {
3248 match crate::stdlib::native_tag(recv).as_deref() {
3249 Some("Buffer") => return crate::stdlib::buffer::instance_call(recv, method, &args),
3250 Some("TypedArray") => {
3251 return crate::stdlib::typedarray::instance_call(recv, method, &args)
3252 }
3253 _ => {}
3254 }
3255 }
3256 // `Array.prototype.<m>.call(arrayLike)` — every `Array.prototype` method is
3257 // GENERIC over `this` (23.1.3: each starts with `ToObject(this)` and
3258 // `LengthOfArrayLike`), which is what makes
3259 // `Array.prototype.slice.call(arguments)` the idiom it is. The receiver here
3260 // is not an Array, so `call_method` would report the method missing.
3261 if ctor == "Array" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Array) {
3262 return array_generic(recv, method, args);
3263 }
3264 // The general form of the two special cases above: a thunk taken off a native
3265 // constructor's real prototype, invoked with a receiver that IS an instance of
3266 // that constructor. Routing back through `call_method` would re-resolve this
3267 // very thunk off the receiver's own chain and recurse forever, which is why
3268 // each such prototype needed a hand-written bypass; now they all have one.
3269 // A SUBCLASS counts: `SecretKeyObject` reaches `KeyObject.prototype.equals`
3270 // through its chain, and requiring an exact tag match sent that call back
3271 // into `call_method`, which re-resolved this same thunk and recursed until
3272 // the stack overflowed.
3273 if let Some(tag) = crate::stdlib::native_tag(recv) {
3274 let mut c = Some(tag.as_str());
3275 while let Some(t) = c {
3276 if t == ctor {
3277 return crate::stdlib::instance_call(&tag, recv, method, args);
3278 }
3279 c = crate::stdlib::native_parent(t);
3280 }
3281 }
3282 // A BRANDED method reached with a receiver that has no such internal slot.
3283 // Every arm above dispatches a receiver that IS an instance, so arriving
3284 // here with one of these constructors means the brand check failed — the
3285 // spec's very first step for each of them (24.2.3.x reads `[[SetData]]`,
3286 // 24.1.3.x `[[MapData]]`, 27.2.5.4 `[[PromiseState]]`, 23.2.3.x
3287 // `ValidateTypedArray`). Falling through to ordinary dispatch reported
3288 // `union is not a function`, which says the method does not exist rather
3289 // than that the receiver is the wrong kind of object.
3290 // `Date.prototype`'s methods split in two: the ones that read the time value
3291 // (`ThisTimeValue`, 21.4.4.x) report `this is not a Date object.`, and the
3292 // rest take the ordinary branded form. Measured on node v26.8.1:
3293 // `Date.prototype.getTime.call({})` is the first, `.toISOString.call({})`
3294 // and `.setHours.call({})` the second.
3295 if ctor == "Date" && crate::stdlib::native_tag(recv).as_deref() != Some("Date") {
3296 const THIS_TIME_VALUE: &[&str] = &[
3297 "getTime",
3298 "valueOf",
3299 "getYear",
3300 "getFullYear",
3301 "getMonth",
3302 "getDate",
3303 "getDay",
3304 "getHours",
3305 "getMinutes",
3306 "getSeconds",
3307 "getMilliseconds",
3308 "getUTCFullYear",
3309 "getUTCMonth",
3310 "getUTCDate",
3311 "getUTCDay",
3312 "getUTCHours",
3313 "getUTCMinutes",
3314 "getUTCSeconds",
3315 "getUTCMilliseconds",
3316 "getTimezoneOffset",
3317 ];
3318 if THIS_TIME_VALUE.contains(&method) {
3319 return Err(host::type_error("this is not a Date object."));
3320 }
3321 // `toJSON` (21.4.4.37) is deliberately generic — it converts the
3322 // receiver and INVOKES `toISOString` on it, so it fails on the missing
3323 // method rather than on a brand.
3324 if method != "toJSON" {
3325 return Err(host::type_error(&format!(
3326 "Method Date.prototype.{method} called on incompatible receiver {}",
3327 no_side_effects_string(recv)
3328 )));
3329 }
3330 }
3331 // `%TypedArray%.prototype`'s methods split the same way: `ValidateTypedArray`
3332 // (23.2.4.4) reports `this is not a typed array.`, while the handful that
3333 // check the receiver at the call boundary take the branded form. Measured
3334 // over all 27 shared methods on node v26.8.1; `toString` is the one that is
3335 // genuinely generic (it is `Array.prototype.toString`) and never brands.
3336 if matches!(ctor, "TypedArray" | "Uint8Array")
3337 && !matches!(
3338 crate::stdlib::native_tag(recv).as_deref(),
3339 Some("TypedArray") | Some("Buffer")
3340 )
3341 {
3342 const BRANDED: &[&str] = &[
3343 "slice",
3344 "subarray",
3345 "join",
3346 "sort",
3347 "at",
3348 "toReversed",
3349 "toSorted",
3350 "toLocaleString",
3351 ];
3352 if BRANDED.contains(&method) {
3353 return Err(host::type_error(&format!(
3354 "Method %TypedArray%.prototype.{method} called on incompatible receiver {}",
3355 no_side_effects_string(recv)
3356 )));
3357 }
3358 // The four base64/hex methods brand themselves against `Uint8Array`
3359 // specifically — a WRONG view is as incompatible as a plain object, and
3360 // the generic guard here cannot tell those apart.
3361 if crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS.contains(&method) {
3362 return Err(host::type_error(&format!(
3363 "Method Uint8Array.prototype.{method} called on incompatible receiver {}",
3364 no_side_effects_string(recv)
3365 )));
3366 }
3367 if method != "toString" {
3368 return Err(host::type_error("this is not a typed array."));
3369 }
3370 }
3371 // `Function.prototype.call`/`apply`/`bind` with a callable PROXY as `this`
3372 // (`pf.call(null, 4, 5)`, reached through the target's chain). Handing
3373 // that back to `call_method` read `call` off the proxy again, which
3374 // resolved to this same thunk, and recursed until the stack overflowed and
3375 // aborted the process. The three are defined on the callee alone, so they
3376 // run here: the proxy's `apply` trap (or its target) gets the call.
3377 // `toString` recursed the same way.
3378 if ctor == "Function"
3379 && matches!(method, "call" | "apply" | "bind" | "toString")
3380 && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
3381 {
3382 let mut rest = args.into_iter();
3383 let this_arg = rest.next().unwrap_or(Value::Undef);
3384 match method {
3385 "call" => return host::invoke(recv, rest.collect(), Some(this_arg)),
3386 "apply" => {
3387 let list = match rest.next() {
3388 None | Some(Value::Undef) => Vec::new(),
3389 Some(v) if with_host(|h| h.is_null(&v)) => Vec::new(),
3390 Some(v) => create_list_from_array_like(&v)?,
3391 };
3392 return host::invoke(recv, list, Some(this_arg));
3393 }
3394 "bind" => {
3395 let target = recv.clone();
3396 let pre: Vec<Value> = rest.collect();
3397 return Ok(with_host(|h| {
3398 h.alloc(JsObj::BoundFunc {
3399 target,
3400 this: this_arg,
3401 args: pre,
3402 })
3403 }));
3404 }
3405 // A proxy has no source text; V8 prints the native form for it.
3406 _ => return Ok(with_host(|h| h.new_str("function () { [native code] }"))),
3407 }
3408 }
3409 // `Symbol.prototype`'s methods are branded, and the receiver that reaches
3410 // them is very often NOT a symbol: `Symbol.prototype` itself is an ordinary
3411 // object. Without this check `Symbol.prototype.toString()` re-entered the
3412 // generic string conversion, which looked `toString` up again and called it
3413 // again — an infinite recursion that overflowed the stack and ABORTED the
3414 // process, which no `try`/`catch` can see. Node throws a plain TypeError.
3415 // The wording is Symbol's own, not the "incompatible receiver" form the
3416 // collections use.
3417 if ctor == "Symbol" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Symbol) {
3418 // A symbol-KEYED method is named in brackets rather than after a dot:
3419 // node's wording is `Symbol.prototype [ @@toPrimitive ] requires …`.
3420 // That is the message `String(Symbol.prototype)` produces, since the
3421 // conversion reaches `@@toPrimitive` before it would reach `toString`.
3422 let named = match method.strip_prefix("@@") {
3423 Some(sym) => format!("Symbol.prototype [ @@{sym} ]"),
3424 None => format!("Symbol.prototype.{method}"),
3425 };
3426 return Err(host::type_error(&format!(
3427 "{named} requires that 'this' be a Symbol"
3428 )));
3429 }
3430 if let Some(label) = branded_method_label(ctor, recv) {
3431 return Err(host::type_error(&format!(
3432 "Method {label}.prototype.{method} called on incompatible receiver {}",
3433 no_side_effects_string(recv)
3434 )));
3435 }
3436 host::call_method(recv, method, args)
3437}
3438
3439/// The name a branded prototype method reports itself under when its receiver
3440/// fails the brand check, or `None` when `ctor`'s methods are generic over
3441/// `this` (every `Array.prototype` and `Object.prototype` method is) or the
3442/// receiver really is an instance.
3443///
3444fn branded_method_label(ctor: &str, recv: &Value) -> Option<&'static str> {
3445 let kind = with_host(|h| h.kind_of(recv));
3446 // `weak` is part of the brand: a `WeakSet` has `[[WeakSetData]]`, not
3447 // `[[SetData]]`, so `Set.prototype.has.call(new WeakSet())` is incompatible
3448 // even though both are `JsObj::Set` here.
3449 let weak = peek(recv, |o| match o {
3450 JsObj::Set { weak, .. } | JsObj::Map { weak, .. } => Some(*weak),
3451 _ => None,
3452 })
3453 .unwrap_or(false);
3454 let ok = match ctor {
3455 "Set" => kind == Some(ObjKind::Set) && !weak,
3456 "WeakSet" => kind == Some(ObjKind::Set) && weak,
3457 "Map" => kind == Some(ObjKind::Map) && !weak,
3458 "WeakMap" => kind == Some(ObjKind::Map) && weak,
3459 "Promise" => kind == Some(ObjKind::Promise),
3460 _ => return None,
3461 };
3462 if ok {
3463 return None;
3464 }
3465 Some(match ctor {
3466 "Set" => "Set",
3467 "WeakSet" => "WeakSet",
3468 "Map" => "Map",
3469 "WeakMap" => "WeakMap",
3470 _ => "Promise",
3471 })
3472}
3473
3474/// V8's `Object::NoSideEffectsToString`, the rendering an engine-thrown message
3475/// uses for a value it must not run user code on. Measured on node v26.8.1
3476/// through `Map.prototype.get.call(x)`:
3477///
3478/// ```text
3479/// 5 / 'str' / true / null / undefined / 9n the value's own ToString
3480/// Symbol('s') Symbol(s)
3481/// function f(){} its source text
3482/// new Error('e') Error: e
3483/// {} / new (class A {}) #<Object> / #<A>
3484/// new Map() / Promise.resolve() #<Map> / #<Promise>
3485/// [] / new Date() / /re/ / new Uint8Array() [object Array] / [object Date] / …
3486/// { toString() {} } / Object.create(null) [object Object]
3487/// ```
3488///
3489/// The split is one test: a receiver whose `toString` is still
3490/// `Object.prototype.toString` prints `#<Constructor>`, and any other receiver
3491/// prints what the BUILTIN brand would be — V8 never calls the user's method,
3492/// which is why an object with its own `toString` prints `[object Object]` and
3493/// not what that method returns.
3494fn no_side_effects_string(recv: &Value) -> String {
3495 if with_host(|h| host::is_primitive(h, recv)) || with_host(|h| host::is_callable(h, recv)) {
3496 return with_host(|h| h.str_of(recv));
3497 }
3498 if let Some(s) = with_host(|h| h.error_to_string(recv)) {
3499 return s;
3500 }
3501 // `native_tag` re-enters the host, so it is read BEFORE the borrow below
3502 // rather than inside it.
3503 let native = crate::stdlib::native_tag(recv).is_some();
3504 let brands_itself = with_host(|h| {
3505 // `Object.prototype.toString` reaches every object as a thunk on the
3506 // real prototype object, so its presence proves nothing; only a
3507 // toString the receiver's chain OVERRIDES it with counts.
3508 let overridden = host::lookup_chain(h, recv, "toString")
3509 .map(|f| !matches!(h.get(&f), Some(JsObj::Builtin(n)) if n == "@proto:Object:toString"))
3510 .unwrap_or(false);
3511 native
3512 || overridden
3513 || h.has_null_proto(recv)
3514 || !matches!(
3515 h.kind_of(recv),
3516 Some(ObjKind::Object)
3517 | Some(ObjKind::Map)
3518 | Some(ObjKind::Set)
3519 | Some(ObjKind::Promise)
3520 )
3521 });
3522 if brands_itself {
3523 return with_host(|h| object_tag(h, recv));
3524 }
3525 let ctor = get_property(recv, "constructor")
3526 .ok()
3527 .map(|c| with_host(|h| h.callable_name(&c)))
3528 .filter(|n| !n.is_empty())
3529 .unwrap_or_else(|| "Object".to_string());
3530 format!("#<{ctor}>")
3531}
3532
3533/// The value of `v[Symbol.toStringTag]` for a builtin that genuinely carries
3534/// one, or `None` when reading that symbol must yield `undefined`.
3535///
3536/// Every builtin brand is already computed in exactly one place (`object_tag`),
3537/// so this reuses it and subtracts the legacy builtins, which brand for
3538/// `Object.prototype.toString` but expose no `Symbol.toStringTag` property.
3539/// The subtracted list is measured against node v26.7.0, not assumed: `[]`,
3540/// `function(){}`, `{}`, `new Date()`, `/x/` and `new Error()` all read
3541/// `undefined`, while `Map`/`Set`/`Promise`/typed arrays/`ArrayBuffer`/
3542/// `DataView`/`WeakRef`/`FinalizationRegistry`/`BigInt`/`Symbol`/generators/
3543/// async+generator functions/`Math`/`JSON`/`Reflect`/`URL`/`URLSearchParams`/
3544/// `TextEncoder`/`TextDecoder` all read their brand.
3545pub(crate) fn well_known_tag(h: &host::JsHost, v: &Value) -> Option<String> {
3546 // A primitive never carries the symbol except a BigInt/Symbol wrapper, both
3547 // of which `object_tag` already brands.
3548 let tag = object_brand(h, v);
3549 const NO_TAG: &[&str] = &[
3550 "Undefined",
3551 "Null",
3552 "Boolean",
3553 "Number",
3554 "String",
3555 "Array",
3556 "Function",
3557 "Object",
3558 "Date",
3559 "RegExp",
3560 "Error",
3561 ];
3562 if NO_TAG.contains(&tag.as_str()) {
3563 return None;
3564 }
3565 Some(tag)
3566}
3567
3568/// The constructor name of the nearest intrinsic prototype on `v`'s chain that
3569/// carries an own `Symbol.toStringTag`, if any.
3570fn chain_tag_ctor(h: &host::JsHost, v: &Value) -> Option<String> {
3571 let mut cur = h.proto_of(v);
3572 for _ in 0..100 {
3573 let p = cur?;
3574 if h.is_null(&p) {
3575 return None;
3576 }
3577 let name = match h.get(&p) {
3578 Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
3579 _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
3580 }
3581 // A CLASS prototype is not linked to the builtin its class extends —
3582 // the relationship lives on the class value — so the walk crosses over
3583 // there, or `Object.create(D.prototype)` for `class D extends Map`
3584 // finds nothing.
3585 .or_else(|| {
3586 h.class_owning_proto(&p)
3587 .and_then(|c| h.class_builtin_ancestor(&c))
3588 .map(|b| h.callable_name(&b))
3589 .filter(|n| !n.is_empty())
3590 });
3591 if let Some(n) = name {
3592 if intrinsic_proto_members(&format!("{n}.prototype"))
3593 .is_some_and(|ms| ms.contains(&"@@toStringTag"))
3594 {
3595 return Some(n);
3596 }
3597 }
3598 cur = h.proto_of(&p);
3599 }
3600 None
3601}
3602
3603/// The `Object.prototype.toString` brand tag for `v` (`[object Array]` etc.).
3604/// Every builtin exotic object reports its own brand, which is how packages
3605/// type-test values they did not construct (`toString.call(x) ===
3606/// '[object Uint8Array]'`). A `Buffer` reports `Uint8Array` because in Node it
3607/// IS a `Uint8Array` subclass and inherits that `Symbol.toStringTag`.
3608pub(crate) fn object_tag(h: &host::JsHost, v: &Value) -> String {
3609 format!("[object {}]", object_brand(h, v))
3610}
3611
3612/// The bare brand name behind `Object.prototype.toString` (`Array`, `Uint8Array`
3613/// …), without the `[object …]` wrapper. Split out so the brand and the
3614/// `Symbol.toStringTag` property read cannot disagree about what a value is.
3615/// Whether `v` is a function's `arguments` object.
3616///
3617/// Backed by an Array so indices, `length`, spread and `for-of` all work, but
3618/// marked so it does not pass for one: node's is an exotic, and `isArray`, the
3619/// brand and `util.types.isArgumentsObject` all have to tell them apart.
3620pub fn is_arguments(v: &Value) -> bool {
3621 with_host(|h| is_arguments_h(h, v))
3622}
3623
3624/// `is_arguments` for a caller that already holds the host borrow — `object_brand`
3625/// runs under one, and re-entering through `with_host` aborts the process.
3626pub fn is_arguments_h(h: &host::JsHost, v: &Value) -> bool {
3627 h.fn_prop(v, "@@arguments").is_some()
3628}
3629
3630fn object_brand(h: &host::JsHost, v: &Value) -> String {
3631 // A `<C>.prototype` this host built as a real object is an ORDINARY object:
3632 // it holds no instance slot, so only the branded few report anything but
3633 // `[object Object]`. Checked before the match because those prototypes are
3634 // plain `JsObj::Object`s and would otherwise be branded by whatever their
3635 // own properties happen to look like — `TypeError.prototype` has `name` and
3636 // `message`, which read as an Error instance.
3637 if let Some(ctor) = h.intrinsic_proto_ctor(v) {
3638 return if BRANDED_PROTOS.contains(&ctor) {
3639 ctor.to_string()
3640 } else {
3641 "Object".to_string()
3642 };
3643 }
3644 let tag: String = match v {
3645 Value::Undef => "Undefined".into(),
3646 Value::Bool(_) => "Boolean".into(),
3647 Value::Int(_) | Value::Float(_) => "Number".into(),
3648 Value::Str(_) => "String".into(),
3649 Value::Obj(_) => match h.get(v) {
3650 Some(JsObj::Null) => "Null".into(),
3651 Some(JsObj::Str(_)) => "String".into(),
3652 Some(JsObj::Array(_)) if is_arguments_h(h, v) => "Arguments".into(),
3653 Some(JsObj::Array(_)) => "Array".into(),
3654 // A lazy iterator helper brands as node does.
3655 Some(JsObj::Object(p))
3656 if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("IteratorHelper") =>
3657 {
3658 "Iterator Helper".into()
3659 }
3660 // A `DOMException` brands by its class, not as a plain `Error`.
3661 Some(JsObj::Object(p)) if p.contains_key("@@domName") => "DOMException".into(),
3662 // 20.1.3.6 steps 5-8 brand a wrapper by its internal slot, so
3663 // `Object.prototype.toString.call(new Number(1))` is
3664 // `[object Number]` rather than `[object Object]`.
3665 Some(JsObj::Object(p)) if p.contains_key("@@primitive") => match p["@@primitive"] {
3666 Value::Bool(_) => "Boolean".into(),
3667 Value::Int(_) | Value::Float(_) => "Number".into(),
3668 _ => "String".into(),
3669 },
3670 // 20.1.3.6 step 3 brands by `IsArray`, which follows a Proxy to its
3671 // `[[ProxyTarget]]` — `Object.prototype.toString.call(new Proxy([],
3672 // {}))` is `'[object Array]'`. Everything else about a proxy brands
3673 // as a plain Object (a `Symbol.toStringTag` read through the `get`
3674 // trap is handled by the caller, before this).
3675 Some(JsObj::Proxy { target, .. }) => {
3676 let mut cur = target;
3677 for _ in 0..100 {
3678 match h.get(cur) {
3679 Some(JsObj::Proxy { target: t, .. }) => cur = t,
3680 _ => break,
3681 }
3682 }
3683 match h.get(cur) {
3684 Some(JsObj::Array(_)) => "Array".into(),
3685 _ => "Object".into(),
3686 }
3687 }
3688 // `function*` / `async function` / `async function*` carry their own
3689 // `Symbol.toStringTag` in V8 (27.3.3.2, 27.7.3.2, 27.4.3.2).
3690 Some(JsObj::Func(f)) => match h.funcs.get(f.def_id) {
3691 Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction".into(),
3692 Some(d) if d.is_generator => "GeneratorFunction".into(),
3693 Some(d) if d.is_async => "AsyncFunction".into(),
3694 _ => "Function".into(),
3695 },
3696 // `Math`/`JSON`/`Reflect` are namespace OBJECTS, not callables, and
3697 // brand by name (21.3.1.9, 25.5.3, 28.1.14).
3698 Some(JsObj::Builtin(n)) if matches!(n.as_str(), "Math" | "JSON" | "Reflect") => {
3699 n.clone()
3700 }
3701 // A `<Ctor>.prototype` object brands as the constructor it belongs
3702 // to — `Object.prototype.toString.call(Set.prototype)` is
3703 // `[object Set]` — and a `require()`d module namespace is a plain
3704 // object. Neither is a function, so neither brands as one.
3705 Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
3706 match n.strip_suffix(".prototype") {
3707 Some(ctor) if BRANDED_PROTOS.contains(&ctor) => ctor.to_string(),
3708 _ => "Object".into(),
3709 }
3710 }
3711 Some(JsObj::Class(_))
3712 | Some(JsObj::Builtin(_))
3713 | Some(JsObj::BoundFunc { .. })
3714 | Some(JsObj::BoundMethod { .. }) => "Function".into(),
3715 // A suspended generator object is `[object Generator]`; an async one
3716 // `[object AsyncGenerator]`.
3717 Some(JsObj::Generator { .. }) if h.is_async_gen_val(v) => "AsyncGenerator".into(),
3718 Some(JsObj::Generator { .. }) => "Generator".into(),
3719 Some(JsObj::RegExp(_)) => "RegExp".into(),
3720 Some(JsObj::Map { weak, .. }) => if *weak { "WeakMap" } else { "Map" }.into(),
3721 Some(JsObj::Set { weak, .. }) => if *weak { "WeakSet" } else { "Set" }.into(),
3722 Some(JsObj::Promise { .. }) => "Promise".into(),
3723 Some(JsObj::Symbol { .. }) => "Symbol".into(),
3724 Some(JsObj::BigInt(_)) => "BigInt".into(),
3725 // Native-tagged instances brand by their tag; a typed array brands by
3726 // its element kind (`@@kind`), and every Error subclass is `Error`.
3727 Some(JsObj::Object(p)) => match p.get("@@native").map(|t| h.str_of(t)).as_deref() {
3728 Some("TypedArray") => p
3729 .get("@@kind")
3730 .map(|k| h.str_of(k))
3731 .unwrap_or_else(|| "Uint8Array".into()),
3732 Some("Buffer") => "Uint8Array".into(),
3733 // Every native class that really carries a `Symbol.toStringTag`
3734 // in Node brands by its own name. Verified against node v26:
3735 // `Object.prototype.toString.call(new WeakRef({}))` is
3736 // `[object WeakRef]`. The rest of the `@@native` tags
3737 // (`EventEmitter`, `Server`, `Hash`, `Readable`, …) are plain
3738 // classes with NO tag, so they stay `[object Object]` — listing
3739 // them here would invent a brand Node does not have.
3740 Some(
3741 t @ ("ArrayBuffer"
3742 | "DataView"
3743 | "Date"
3744 | "WeakRef"
3745 | "FinalizationRegistry"
3746 | "TextEncoder"
3747 | "TextDecoder"
3748 | "URL"
3749 | "URLSearchParams"),
3750 ) => t.into(),
3751 _ if has_error_data(h, v) => "Error".into(),
3752 _ => "Object".into(),
3753 },
3754 _ => "Object".into(),
3755 },
3756 // node-js only produces the Value variants above; fusevm's shell-oriented
3757 // variants never arise here.
3758 _ => "Object".into(),
3759 };
3760 // Nothing about the value itself brands it. An ordinary object whose CHAIN
3761 // reaches an intrinsic prototype carrying an own `Symbol.toStringTag`
3762 // borrows that one: 20.1.3.6 step 15 is a `Get`, which walks.
3763 // `Object.prototype.toString.call(Object.create(Map.prototype))` is
3764 // `[object Map]` and was `[object Object]`.
3765 //
3766 // Only as a FALLBACK, and only for the prototypes that REALLY carry the
3767 // symbol. A typed array reaches `%TypedArray%.prototype`, whose tag is an
3768 // ACCESSOR returning the specific kind, so consulting the chain FIRST
3769 // branded every view `[object TypedArray]` instead of `[object Uint8Array]`
3770 // — three records caught it. `Error.prototype` carries no tag at all, so
3771 // inheriting from it borrows nothing.
3772 if tag == "Object" && !has_error_data(h, v) {
3773 if let Some(ctor) = chain_tag_ctor(h, v) {
3774 return ctor;
3775 }
3776 }
3777 tag
3778}
3779
3780fn b_setattr(vm: &mut VM, _: u8) -> Value {
3781 let val = vm.pop();
3782 let name = sval(&vm.pop());
3783 let recv = vm.pop();
3784 if let Err(e) = set_property(&recv, &name, val.clone()) {
3785 return abort(vm, e);
3786 }
3787 val
3788}
3789
3790/// `NAMED_EVAL` — SetFunctionName (10.2.9) for a function whose name is only
3791/// known at run time, i.e. one defined under a COMPUTED key: `{ [k]: () => {} }`,
3792/// `class C { static [k] = function(){} }`.
3793///
3794/// The compiler emits this ONLY where the grammar says NamedEvaluation applies
3795/// (`IsAnonymousFunctionDefinition` is a syntactic predicate, not a runtime one:
3796/// `{ m: someAlreadyAnonymousFn }` must NOT be renamed), so the name is set
3797/// unconditionally here.
3798///
3799/// A symbol key becomes `[description]` per step 2 of SetFunctionName; `kind`
3800/// contributes the accessor prefix, so `{ get [k](){} }` is `get <key>`.
3801fn b_named_eval(vm: &mut VM, _: u8) -> Value {
3802 let func = vm.pop();
3803 let kind = vm.pop().to_int();
3804 let key = vm.pop();
3805 let key = sval(&key);
3806 // `@@sym:<id>` / `@@iterator` — an internal symbol key. Step 2: an empty
3807 // description gives the empty name, not `[undefined]`.
3808 let base = match with_host(|h| h.symbol_of_key(&key)) {
3809 Some(sym) => match with_host(|h| h.get(&sym).cloned()) {
3810 Some(JsObj::Symbol {
3811 desc: Some(desc), ..
3812 }) => format!("[{desc}]"),
3813 _ => String::new(),
3814 },
3815 None => key,
3816 };
3817 let name = match kind {
3818 host::member::GET => format!("get {base}"),
3819 host::member::SET => format!("set {base}"),
3820 _ => base,
3821 };
3822 with_host(|h| {
3823 let s = h.new_str(name);
3824 h.set_fn_prop(&func, "name", s);
3825 });
3826 func
3827}
3828
3829/// `[[Set]]` reachable from `crate::proxy`'s no-trap forward, which has to land
3830/// on the same path a plain `o.k = v` takes.
3831pub fn set_property_pub(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3832 set_property(recv, name, val)
3833}
3834
3835/// An object's OWN property as `(value, writable, configurable, is_accessor)`,
3836/// or `None` when it has none. Reads through a Proxy's
3837/// `getOwnPropertyDescriptor` trap, so it answers for any object.
3838pub fn own_prop_facts(obj: &Value, key: &str) -> Option<(Value, bool, bool, bool)> {
3839 let k = with_host(|h| h.new_str(key.to_string()));
3840 let d = own_descriptor_pub(obj, k).ok()?;
3841 if matches!(d, Value::Undef) {
3842 return None;
3843 }
3844 let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
3845 // Each read is hoisted out of the `with_host` borrow: `get_property` takes
3846 // the host itself, so reading inside the closure double-borrows.
3847 let value = field("value");
3848 let writable = field("writable");
3849 let configurable = field("configurable");
3850 let truthy = |v: &Value| with_host(|h| h.truthy(v));
3851 let is_accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
3852 Some((value, truthy(&writable), truthy(&configurable), is_accessor))
3853}
3854
3855/// `OrdinarySetWithOwnDescriptor` (10.1.9.2) with a receiver distinct from the
3856/// object the lookup started on — what `Reflect.set(t, k, v, receiver)` and a
3857/// proxy `set` trap forwarding to it both need.
3858///
3859/// The distinction that matters: an accessor found on `target`'s chain RUNS,
3860/// with `receiver` as `this`; a data property does not write to `target` at all
3861/// but is CREATED on `receiver` through its `[[DefineOwnProperty]]`. Routing
3862/// that second case back through `[[Set]]` made a proxy receiver re-enter its
3863/// own `set` trap forever — the trap body `Reflect.set(t, k, v, recv)` is the
3864/// documented way to forward a write, so the recursion hit every faithful
3865/// handler.
3866pub fn set_with_receiver(
3867 target: &Value,
3868 key: &str,
3869 val: Value,
3870 receiver: &Value,
3871) -> Result<bool, String> {
3872 // A proxy target answers through its own trap, which re-enters here with
3873 // whatever receiver the handler passes on.
3874 if crate::proxy::parts(target).is_some() {
3875 return crate::proxy::set(target, key, &val, receiver);
3876 }
3877 // An accessor anywhere on the target's chain wins, and sees `receiver`.
3878 if let Some((_, setter)) = with_host(|h| host::lookup_accessor(h, target, key)) {
3879 return match setter {
3880 Some(s) => {
3881 host::invoke(&s, vec![val], Some(receiver.clone()))?;
3882 Ok(true)
3883 }
3884 // A getter with no setter refuses the write rather than shadowing it.
3885 None => Ok(false),
3886 };
3887 }
3888 if !with_host(|h| h.can_write_prop(target, key)) {
3889 return Ok(false);
3890 }
3891 // Steps 3.b-3.d: only an object can receive the property, and its OWN
3892 // property decides — an accessor or a read-only slot refuses, and every
3893 // other case defines a plain data property.
3894 //
3895 // `is_object_like`, not a shape test: a string, a symbol and a bigint are
3896 // PRIMITIVES that ride as `Value::Obj` handles here, so the shape check
3897 // passed them through to `defineProperty`, which then threw `called on
3898 // non-object` where 10.1.9.2 step 3.b simply reports `false`.
3899 if !with_host(|h| is_object_like(h, receiver)) {
3900 return Ok(false);
3901 }
3902 if let Some((_, writable, _, is_accessor)) = own_prop_facts(receiver, key) {
3903 if is_accessor || !writable {
3904 return Ok(false);
3905 }
3906 }
3907 // Steps 3.d.iii and 3.e both DEFINE, they do not assign: a setter inherited
3908 // by the receiver must not run, and a proxy receiver must reach its
3909 // `defineProperty` trap rather than its `set` trap.
3910 let desc = with_host(|h| {
3911 let mut m: IndexMap<String, Value> = IndexMap::new();
3912 m.insert("value".into(), val);
3913 m.insert("writable".into(), Value::Bool(true));
3914 m.insert("enumerable".into(), Value::Bool(true));
3915 m.insert("configurable".into(), Value::Bool(true));
3916 h.new_object(m)
3917 });
3918 if crate::proxy::parts(receiver).is_some() {
3919 return crate::proxy::define_property(receiver, key, &desc);
3920 }
3921 let k = with_host(|h| h.new_str(key.to_string()));
3922 define_property_pub(receiver, k, desc)?;
3923 Ok(true)
3924}
3925
3926/// Whether the first argument is a PRIMITIVE — including the three that ride as
3927/// heap handles, which a shape test misses.
3928fn is_primitive_arg(args: &[Value]) -> bool {
3929 let v = arg0(args);
3930 with_host(|h| host::is_primitive(h, &v))
3931}
3932
3933/// The `TypeError` a refused write raises in strict code, worded as V8 does.
3934///
3935/// Adding a key to a non-extensible object reports differently from assigning
3936/// to a read-only one, and the object is named by its brand — `#<Object>` for a
3937/// plain object, `[object Array]` for an array.
3938fn write_refused(recv: &Value, name: &str) -> String {
3939 let extensible = with_host(|h| h.is_extensible(recv));
3940 // Which of the two messages applies turns on whether the key already
3941 // EXISTS. Every shape that keeps its own properties in the fn-prop side
3942 // table answered a blanket `true` here, so adding a key to a frozen
3943 // function reported "read only" where node reports "not extensible".
3944 let has_own = with_host(|h| match h.get(recv) {
3945 Some(JsObj::Object(p)) => p.contains_key(name),
3946 Some(JsObj::Array(items)) => {
3947 name.parse::<usize>()
3948 .map(|i| i < items.len())
3949 .unwrap_or(false)
3950 || h.fn_prop(recv, name).is_some()
3951 }
3952 Some(JsObj::RegExp(_)) => name == "lastIndex" || h.fn_prop(recv, name).is_some(),
3953 _ => h.fn_prop(recv, name).is_some(),
3954 });
3955 if !extensible && !has_own {
3956 return host::type_error(&format!(
3957 "Cannot add property {name}, object is not extensible"
3958 ));
3959 }
3960 // The receiver renders the way every other brand-check message renders one
3961 // — `#<Object>`, `[object Array]`, `[object RegExp]`, `#<Map>`, `#<C>` for a
3962 // class instance, `Error: m` for an error. Only Array was special-cased, so
3963 // every other exotic reported `#<Object>`.
3964 host::type_error(&format!(
3965 "Cannot assign to read only property '{name}' of object '{}'",
3966 no_side_effects_string(recv)
3967 ))
3968}
3969
3970fn set_property(recv: &Value, name: &str, val: Value) -> Result<(), String> {
3971 // 6.2.5.6 `PutValue` begins with `RequireObjectCoercible`: writing any
3972 // property of `undefined` or `null` throws, naming the key. Every such
3973 // write was silently discarded, so `u.x = 1` — the mirror of the single
3974 // most common runtime fault in JS, which the READ side already reports —
3975 // looked like it had succeeded.
3976 if with_host(|h| h.is_nullish(recv)) {
3977 return Err(host::type_error(&format!(
3978 "Cannot set properties of {} (setting '{name}')",
3979 with_host(|h| h.str_of(recv))
3980 )));
3981 }
3982 // A write to a PRIMITIVE receiver has no target — `ToObject` makes a
3983 // throwaway wrapper — so it is discarded in sloppy code and throws in
3984 // strict (10.1.9.2 / 6.2.5.6 again). The refusal was silent in both.
3985 // `is_primitive` rather than a shape test: a string, a symbol and a bigint
3986 // ride as `Value::Obj` handles in this host, so a check for a non-`Obj`
3987 // value caught only numbers and booleans.
3988 if with_host(|h| host::is_primitive(h, recv)) && with_host(|h| h.current_strict()) {
3989 return Err(host::type_error(&format!(
3990 "Cannot create property '{name}' on {} '{}'",
3991 with_host(|h| h.type_of(recv)),
3992 with_host(|h| h.str_of(recv))
3993 )));
3994 }
3995 // `[[PrivateSet]]` (7.3.32) refuses a receiver that carries no such private
3996 // element. The class's own field initializers install theirs directly
3997 // (`host::init_one_field`), so a declaration never reaches this check.
3998 if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
3999 return Err(private_brand_message(name, true));
4000 }
4001 // `[[Set]]` on a Proxy: the handler's `set` trap, or a forward to the target.
4002 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
4003 // A `set` trap that returns falsish REFUSED the write: silent in sloppy
4004 // code, a TypeError in strict, exactly as an ordinary refused write is.
4005 if crate::proxy::set(recv, name, &val, recv)? {
4006 return Ok(());
4007 }
4008 if with_host(|h| h.current_strict()) {
4009 return Err(host::type_error(&format!(
4010 "'set' on proxy: trap returned falsish for property '{name}'"
4011 )));
4012 }
4013 return Ok(());
4014 }
4015 // `globalThis.x = 1` creates a real global binding, so the bare `x` reads it
4016 // back. Writing only the own property left the two views disagreeing:
4017 // `globalThis.zz` was 7 while `zz` was still a `ReferenceError`.
4018 if with_host(|h| h.is_global_object(recv)) && !name.starts_with("@@") {
4019 with_host(|h| h.set_name(name, val.clone()));
4020 }
4021 // `obj.__proto__ = p` re-links the prototype — but only for the two values
4022 // the Annex B setter accepts, an Object or `null`. Everything else is a
4023 // silent no-op in Node (`o.__proto__ = 5` leaves `Object.getPrototypeOf(o)`
4024 // untouched and creates no own key), and a null-prototype object inherits
4025 // no such setter at all, so there the assignment is an ORDINARY own
4026 // property write. Re-linking unconditionally made `o.__proto__ = 5` set the
4027 // prototype to the number 5.
4028 if name == "__proto__" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
4029 if with_host(|h| h.has_null_proto(recv)) {
4030 // falls through to the ordinary own-property write below
4031 } else {
4032 let assignable =
4033 with_host(|h| h.is_null(&val) || matches!(h.kind_of(&val), Some(ObjKind::Object)));
4034 if assignable {
4035 // The `__proto__` setter runs `[[SetPrototypeOf]]`, which a
4036 // NON-EXTENSIBLE object refuses — and unlike an ordinary
4037 // refused write, the setter throws in sloppy code too. It was
4038 // rewriting the link of a frozen object.
4039 if would_cycle(recv, &val) {
4040 return Err(host::type_error("Cyclic __proto__ value"));
4041 }
4042 if !with_host(|h| h.is_extensible(recv)) && !same_prototype(recv, &val) {
4043 return Err(host::type_error(&format!(
4044 "{} is not extensible",
4045 no_side_effects_string(recv)
4046 )));
4047 }
4048 with_host(|h| h.set_proto(recv, val));
4049 }
4050 return Ok(());
4051 }
4052 }
4053 // Every environment value is a STRING. `process.env.PORT = 8080` stores
4054 // "8080", so `process.env.PORT + 1` concatenates the way it does in a real
4055 // process; storing the number made it add instead.
4056 if !name.starts_with("@@")
4057 && with_host(
4058 |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4059 )
4060 {
4061 let text = with_host(|h| h.str_of(&val));
4062 // Write THROUGH to the real environment as well. `process.env` is not a
4063 // private map: node applies the change to the process, so a child
4064 // spawned afterwards inherits it. Keeping it only in the JS object meant
4065 // `process.env.NODE_ENV = 'production'` was invisible to every
4066 // `spawnSync`/`execSync` that followed.
4067 std::env::set_var(name, &text);
4068 let sv = with_host(|h| h.new_str(text));
4069 with_host(|h| {
4070 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4071 p.insert(name.to_string(), sv);
4072 }
4073 });
4074 return Ok(());
4075 }
4076 // Assigning `e.stack` wins permanently: drop the not-yet-formatted marker so
4077 // no later read re-derives a header over the top of the assigned value.
4078 if name == "stack" {
4079 with_host(|h| {
4080 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
4081 p.shift_remove("@@stackRaw");
4082 }
4083 });
4084 }
4085 // An inherited/own setter accessor intercepts the write. This is checked
4086 // BEFORE the writable test because 10.1.9.2 branches on the descriptor
4087 // kind first: `writable` is a data-property attribute and means nothing on
4088 // an accessor, where the setter alone decides. Testing it first meant an
4089 // accessor defined through `Object.defineProperty` — which leaves
4090 // `writable` false, having no such field — silently swallowed every write
4091 // instead of calling its setter, so the standard clone idiom
4092 // `Object.create(proto, Object.getOwnPropertyDescriptors(src))` produced an
4093 // object whose setters did nothing. An accessor from an object literal
4094 // carries all-true attributes, which is why only the former broke.
4095 if let Some((getter, setter)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
4096 if let Some(setter) = setter {
4097 let _ = host::invoke(&setter, vec![val], Some(recv.clone()));
4098 return Ok(());
4099 }
4100 // Only a getter: the write is refused — silent in sloppy mode, a
4101 // TypeError in strict code. The `return` above matters, since a
4102 // successful setter call must not fall into this.
4103 let _ = getter;
4104 if with_host(|h| h.current_strict()) {
4105 return Err(host::type_error(&format!(
4106 "Cannot set property {name} of #<Object> which has only a getter"
4107 )));
4108 }
4109 return Ok(());
4110 }
4111 // A non-writable property, or a new key on a non-extensible object, refuses
4112 // the write. In SLOPPY mode that is silent; in strict code it is a
4113 // TypeError, and the ASSIGNMENT SITE decides which — not the object. Every
4114 // refusal used to be silent, so `'use strict'` did not catch a write to a
4115 // frozen object, which is most of the reason to freeze one.
4116 if !with_host(|h| h.can_write_prop(recv, name)) {
4117 if with_host(|h| h.current_strict()) {
4118 return Err(write_refused(recv, name));
4119 }
4120 return Ok(());
4121 }
4122 // Writing `name`/`prototype`/statics on a function value.
4123 if matches!(
4124 with_host(|h| h.kind_of(recv)),
4125 Some(ObjKind::Func) | Some(ObjKind::Class)
4126 ) {
4127 with_host(|h| h.set_fn_prop(recv, name, val));
4128 return Ok(());
4129 }
4130 // Writing a static onto a builtin namespace/ctor (`Error.prepareStackTrace`).
4131 // Each bare reference is a fresh `Builtin` handle, so route to the stable
4132 // per-namespace side table rather than the per-index `fn_props`.
4133 if let Some(ns) = peek(recv, |o| match o {
4134 JsObj::Builtin(ns) => Some(ns.clone()),
4135 _ => None,
4136 }) {
4137 // `process.exitCode` is an accessor in Node, not a data property: the
4138 // setter validates and stores the code the process will finally exit
4139 // with. Landing it in the generic static table made it a write-only
4140 // decoration — `process.exitCode = 3` read back as 3 and the process
4141 // still exited 0.
4142 if ns == "process" && name == "exitCode" {
4143 return crate::stdlib::process::set_exit_code(&val);
4144 }
4145 with_host(|h| h.set_builtin_static(&ns, name, val));
4146 return Ok(());
4147 }
4148 // A write onto a REAL intrinsic prototype object (`Object.prototype`,
4149 // `String.prototype`, `TypeError.prototype`) is mirrored into the
4150 // per-namespace side table as well as the object's own map. Instances are
4151 // not linked to these objects by `proto_of` — the chain walk never reaches
4152 // them — so the mirror is what makes `String.prototype.pad = f` visible as
4153 // `"x".pad`. The own-map write below still happens, so reading the
4154 // prototype itself and enumerating it keep working unchanged.
4155 if let Some(ns) = with_host(|h| {
4156 h.intrinsic_proto_ctor(recv)
4157 .map(str::to_string)
4158 .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4159 }) {
4160 with_host(|h| h.set_builtin_static(&format!("{ns}.prototype"), name, val.clone()));
4161 }
4162 // `re.lastIndex = n` on a RegExp advances/resets its match cursor. The
4163 // writability check above already refused it on a FROZEN regexp, which it
4164 // could only do once `integrity_keys` learned that `lastIndex` is an own
4165 // property.
4166 if name == "lastIndex" {
4167 if let Some(n) = with_host(|h| match h.get(recv) {
4168 Some(JsObj::RegExp(_)) => Some(h.to_number(&val)),
4169 _ => None,
4170 }) {
4171 with_host(|h| {
4172 if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
4173 r.last_index = if n.is_finite() && n >= 0.0 {
4174 crate::utf16::U16Index::new(n as usize)
4175 } else {
4176 crate::utf16::U16Index::ZERO
4177 };
4178 }
4179 });
4180 return Ok(());
4181 }
4182 }
4183 // An `arguments` object is an ORDINARY object with a `length` data property,
4184 // not an array: a write PAST the end adds an index and leaves `length`
4185 // alone. The array backing grew it instead, so `f(1)` followed by
4186 // `arguments[1] = 9` reported `arguments.length` as 2.
4187 if let Ok(i) = name.parse::<usize>() {
4188 if is_arguments(recv) && i >= array_len(recv) {
4189 with_host(|h| h.set_fn_prop(recv, name, val));
4190 return Ok(());
4191 }
4192 }
4193 // Typed-array element write (`ta[i] = v`): coerce + store into `@@elems`.
4194 if !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
4195 let is_ta = crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray");
4196 if is_ta && crate::stdlib::typedarray::elem_set(recv, name, &val)? {
4197 return Ok(());
4198 }
4199 // An index write to a view over a DETACHED buffer is DROPPED. Falling
4200 // through would store it as an ordinary own property, which then showed
4201 // up in `getOwnPropertyDescriptor` over a buffer with no bytes.
4202 if is_ta && crate::stdlib::typedarray::view_detached(recv) {
4203 return Ok(());
4204 }
4205 // `buf[i] = n` writes through to the Buffer's hidden byte array.
4206 if crate::stdlib::buffer::byte_set(recv, name, &val) {
4207 return Ok(());
4208 }
4209 }
4210 // Any own property on an exotic with no property map of its own. This sits
4211 // BELOW the exotic-specific writes above, so a RegExp's `lastIndex` still
4212 // moves its match cursor rather than being shadowed by a side-table entry.
4213 if uses_side_table(recv) {
4214 with_host(|h| h.set_fn_prop(recv, name, val));
4215 return Ok(());
4216 }
4217 // An arbitrary own prop on an array (e.g. exec-result `.index`/`.input`).
4218 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array)
4219 && name != "length"
4220 && name.parse::<usize>().is_err()
4221 {
4222 with_host(|h| h.set_fn_prop(recv, name, val));
4223 return Ok(());
4224 }
4225 // `arr.length = n` (10.4.2.4 `ArraySetLength`) validates BEFORE it resizes,
4226 // and does so outside the host borrow because `ToNumber` may run a user
4227 // `valueOf`. An invalid length throws instead of being silently coerced to 0.
4228 let new_len = if name == "length" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array) {
4229 let want = host::to_array_length(&val)?;
4230 // 10.4.2.4 steps 15-17: shrinking deletes from the END downwards and
4231 // STOPS at the first element that cannot be deleted, leaving the length
4232 // just past it. Truncating regardless discarded a non-configurable
4233 // element and reported a length node would not have accepted.
4234 let floor = with_host(|h| {
4235 let old = match h.get(recv) {
4236 Some(JsObj::Array(items)) => items.len(),
4237 _ => 0,
4238 };
4239 let mut stop = want;
4240 for i in (want..old).rev() {
4241 if !h.prop_attrs(recv, &i.to_string()).configurable {
4242 stop = i + 1;
4243 break;
4244 }
4245 }
4246 stop
4247 });
4248 Some(floor.max(want))
4249 } else {
4250 None
4251 };
4252 with_host(|h| match h.get_mut(recv) {
4253 Some(JsObj::Object(props)) => {
4254 // Adding a *new* array-index key must re-place it into ascending
4255 // integer-key order (updating an existing key keeps its position).
4256 let is_new = !props.contains_key(name);
4257 props.insert(name.to_string(), val);
4258 if is_new && host::array_index(name).is_some() {
4259 host::canonicalize_own_keys(props);
4260 }
4261 }
4262 Some(JsObj::Array(items)) => {
4263 if let Some(n) = new_len {
4264 // Growing `length` appends HOLES (`a=[1]; a.length=3` still has
4265 // just the one own key); shrinking drops any hole past the end.
4266 let old = items.len();
4267 items.resize(n, Value::Undef);
4268 if n > old {
4269 h.mark_hole_range(recv, old..n);
4270 } else {
4271 h.truncate_holes(recv, n);
4272 }
4273 } else if let Ok(i) = name.parse::<usize>() {
4274 // A write PAST the end leaves the skipped positions elided.
4275 let old = items.len();
4276 if i >= old {
4277 items.resize(i + 1, Value::Undef);
4278 }
4279 items[i] = val;
4280 if i > old {
4281 h.mark_hole_range(recv, old..i);
4282 }
4283 // …and the written index itself is no longer one. This is the
4284 // single site that keeps a hole record from outliving the
4285 // elision it describes: every array element write in the
4286 // language reaches it.
4287 h.clear_hole(recv, i);
4288 }
4289 }
4290 _ => {}
4291 });
4292 Ok(())
4293}
4294
4295fn b_getitem(vm: &mut VM, _: u8) -> Value {
4296 let idx = vm.pop();
4297 let recv = vm.pop();
4298 let key = match host::to_property_key(&idx) {
4299 Ok(k) => k,
4300 Err(e) => return abort(vm, e),
4301 };
4302 match get_property(&recv, &key) {
4303 Ok(v) => v,
4304 Err(e) => abort(vm, e),
4305 }
4306}
4307
4308fn b_setitem(vm: &mut VM, _: u8) -> Value {
4309 let val = vm.pop();
4310 let idx = vm.pop();
4311 let recv = vm.pop();
4312 let key = match host::to_property_key(&idx) {
4313 Ok(k) => k,
4314 Err(e) => return abort(vm, e),
4315 };
4316 if let Err(e) = set_property(&recv, &key, val.clone()) {
4317 return abort(vm, e);
4318 }
4319 val
4320}
4321
4322/// `[[Delete]]` (10.1.10) for an already-resolved property key: the one place
4323/// `delete o[k]`, `delete o.k` and `Reflect.deleteProperty` all go through, so
4324/// the three cannot drift. Reports `false` for a non-configurable property
4325/// (sloppy mode ignores the failure rather than throwing) and `true` otherwise,
4326/// which is also what deleting an absent key reports.
4327pub fn delete_property(recv: &Value, key: &str) -> Result<bool, String> {
4328 // 13.5.1.2 step 5 runs `ToObject` on the base, which a nullish one refuses.
4329 // `delete u.x` reported success instead.
4330 if with_host(|h| h.is_nullish(recv)) {
4331 return Err(host::type_error(
4332 "Cannot convert undefined or null to object",
4333 ));
4334 }
4335 // `[[Delete]]` on a Proxy runs the handler's `deleteProperty` trap, which may
4336 // throw — the reason this reports a `Result` rather than a bare `bool`.
4337 if let Some(b) = crate::proxy::delete(recv, key)? {
4338 return Ok(b);
4339 }
4340 // `delete globalThis.x` removes a global a script created. It lives in the
4341 // globals map, not the object's property map, so the ordinary path reported
4342 // success and removed nothing — the binding stayed readable afterwards.
4343 if with_host(|h| h.is_global_object(recv)) && with_host(|h| h.remove_global(key)) {
4344 return Ok(true);
4345 }
4346 // `delete require.cache[id]` drops the module so the next `require` of that
4347 // file runs it again — the whole point of exposing the cache.
4348 if peek(recv, |o| match o {
4349 JsObj::Builtin(ns) => Some(ns == REQUIRE_CACHE),
4350 _ => None,
4351 }) == Some(true)
4352 {
4353 return Ok(crate::module::cache_delete(key));
4354 }
4355 // `delete process.env.X` unsets the variable in the PROCESS, not just in the
4356 // JS view, so a child spawned afterwards no longer sees it.
4357 if !key.starts_with("@@")
4358 && with_host(
4359 |h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
4360 )
4361 {
4362 std::env::remove_var(key);
4363 }
4364 // A member of a builtin NAMESPACE (`Math.PI`, `Number.MAX_VALUE`,
4365 // `Object.prototype`) is non-configurable when it is a constant or a
4366 // constructor's `prototype`, and `delete` of one answers false without
4367 // removing anything. There is no property map behind a namespace, so the
4368 // ordinary attribute lookup below cannot tell — it reported success for
4369 // every one of them.
4370 // A REAL intrinsic prototype object carries the write in its own map AND in
4371 // the side table the instance read consults, so the delete has to clear
4372 // both. Clearing only the map left `Object.prototype.patch` deleted as far
4373 // as the prototype was concerned and still inherited by every object.
4374 if let Some(ns) = with_host(|h| {
4375 h.intrinsic_proto_ctor(recv)
4376 .map(str::to_string)
4377 .or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
4378 }) {
4379 with_host(|h| h.remove_builtin_static(&format!("{ns}.prototype"), key));
4380 }
4381 if let Some(ns) = peek(recv, |o| match o {
4382 JsObj::Builtin(ns) => Some(ns.clone()),
4383 _ => None,
4384 }) {
4385 // A script-assigned static is an ordinary configurable property and is
4386 // removed from the side table the assignment landed in. Falling through
4387 // to the attribute check below answered true and deleted nothing, so a
4388 // patch survived its own `delete`.
4389 if with_host(|h| h.remove_builtin_static(&ns, key)) {
4390 return Ok(true);
4391 }
4392 if ns != REQUIRE_CACHE && !builtin_member_configurable(&ns, key) {
4393 return Ok(false);
4394 }
4395 }
4396 if !with_host(|h| h.prop_attrs(recv, key).configurable) {
4397 return Ok(false);
4398 }
4399 // An accessor lives in its own table, not the property map, so removing it
4400 // has to be explicit — otherwise `delete` reported success while the getter
4401 // kept answering and `in` kept reporting the key.
4402 if with_host(|h| h.own_accessor(recv, key).is_some()) {
4403 with_host(|h| h.remove_accessor(recv, key));
4404 return Ok(true);
4405 }
4406 with_host(|h| {
4407 let index = key.parse::<usize>();
4408 match h.get_mut(recv) {
4409 Some(JsObj::Object(props)) => {
4410 props.shift_remove(key);
4411 return;
4412 }
4413 Some(JsObj::Array(items)) => {
4414 if let Ok(i) = index {
4415 if i < items.len() {
4416 // `delete a[i]` punches a HOLE: the length is unchanged
4417 // but the index stops being an own property.
4418 items[i] = Value::Undef;
4419 h.mark_hole(recv, i);
4420 }
4421 return;
4422 }
4423 }
4424 _ => {}
4425 }
4426 // A non-index key on an array (`arr.foo`, `arr[sym]`), or any own key on
4427 // a function/class, is an ordinary own property kept in the side table.
4428 h.remove_fn_prop(recv, key);
4429 });
4430 Ok(true)
4431}
4432
4433fn b_delitem(vm: &mut VM, _: u8) -> Value {
4434 let strict = vm.pop();
4435 let idx = vm.pop();
4436 let recv = vm.pop();
4437 // `delete o[k]` keys through ToPropertyKey (7.1.19), exactly as the read and
4438 // the write do: `String(k)` would turn a Symbol into its `Symbol(desc)`
4439 // description and delete a key nothing ever wrote.
4440 let key = match host::to_property_key(&idx) {
4441 Ok(k) => k,
4442 Err(e) => return abort(vm, e),
4443 };
4444 match delete_property(&recv, &key) {
4445 Ok(false) if with_host(|h| h.truthy(&strict)) => {
4446 abort(vm, refused_delete_error(&recv, &key))
4447 }
4448 Ok(b) => Value::Bool(b),
4449 Err(e) => abort(vm, e),
4450 }
4451}
4452
4453fn b_delprop_name(vm: &mut VM, _: u8) -> Value {
4454 let strict = vm.pop();
4455 let name = sval(&vm.pop());
4456 let recv = vm.pop();
4457 match delete_property(&recv, &name) {
4458 Ok(false) if with_host(|h| h.truthy(&strict)) => {
4459 abort(vm, refused_delete_error(&recv, &name))
4460 }
4461 Ok(b) => Value::Bool(b),
4462 Err(e) => abort(vm, e),
4463 }
4464}
4465
4466/// The TypeError a STRICT `delete` of a non-configurable property raises. The
4467/// receiver renders the way every other brand-check message renders one.
4468fn refused_delete_error(recv: &Value, key: &str) -> String {
4469 // A PROXY names the trap that refused. Only the `delete` OPERATOR reports
4470 // it; `Reflect.deleteProperty` answers `false`, which is why this lives
4471 // here rather than in the shared `[[Delete]]`.
4472 if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
4473 return host::type_error(&format!(
4474 "'deleteProperty' on proxy: trap returned falsish for property '{key}'"
4475 ));
4476 }
4477 // A non-callable builtin NAMESPACE renders as a plain object here — node
4478 // reports `#<Object>` for `Math`, not its `[object Math]` brand.
4479 let shown = match peek(recv, |o| match o {
4480 JsObj::Builtin(ns) => Some(ns.clone()),
4481 _ => None,
4482 }) {
4483 Some(ns) if !host::builtin_is_callable(&ns) => "#<Object>".to_string(),
4484 _ => no_side_effects_string(recv),
4485 };
4486 host::type_error(&format!("Cannot delete property '{key}' of {shown}"))
4487}
4488
4489// ── constructors ──────────────────────────────────────────────────────────────
4490
4491fn b_mkstr(vm: &mut VM, argc: u8) -> Value {
4492 let parts = pop_n(vm, argc as usize);
4493 let s: String = with_host(|h| parts.iter().map(|p| h.str_of(p)).collect());
4494 with_host(|h| h.new_str(s))
4495}
4496
4497fn b_mkarr(vm: &mut VM, argc: u8) -> Value {
4498 let items = pop_n(vm, argc as usize);
4499 with_host(|h| h.new_array(items))
4500}
4501
4502/// `MARK_HOLE [arr, index]`: record `arr[index]` as an ELIDED element. Emitted
4503/// only for an array literal that actually contains an elision, so a dense
4504/// literal costs nothing. Returns `undefined`; the array stays on the stack
4505/// underneath (the compiler `Dup`s it).
4506fn b_mark_hole(vm: &mut VM, _: u8) -> Value {
4507 let idx = vm.pop();
4508 let arr = vm.pop();
4509 let i = match idx {
4510 Value::Int(i) if i >= 0 => i as usize,
4511 _ => return Value::Undef,
4512 };
4513 with_host(|h| h.mark_hole(&arr, i));
4514 Value::Undef
4515}
4516
4517fn b_mkobj(vm: &mut VM, argc: u8) -> Value {
4518 let flat = pop_n(vm, argc as usize);
4519 let mut props: IndexMap<String, Value> = IndexMap::new();
4520 // A literal `__proto__: x` key sets the object's prototype (not an own prop).
4521 let mut proto_override: Option<Value> = None;
4522 let mut method_keys: Vec<String> = Vec::new();
4523 let mut i = 0;
4524 while i + 2 < flat.len() || (i + 2 == flat.len() && flat.len() % 3 == 0 && i < flat.len()) {
4525 if i + 2 >= flat.len() {
4526 break;
4527 }
4528 // Tag 2: an ACCESSOR's position. An accessor lives in its own table, so
4529 // the literal reserves its slot here with the `@@ord:` marker key that
4530 // `own_enum_data_keys` resolves back — otherwise `{ get g(){}, d: 2 }`
4531 // enumerated `d, g`, because `DEF_ACCESSOR` runs after `MKOBJ` and its
4532 // marker landed at the end.
4533 if matches!(flat[i], Value::Int(2)) {
4534 let key = with_host(|h| h.str_of(&flat[i + 1]));
4535 props
4536 .entry(format!("{}{key}", host::ORD_MARKER))
4537 .or_insert(Value::Undef);
4538 i += 3;
4539 continue;
4540 }
4541 // Tag 3: a METHOD DEFINITION — an ordinary property whose key is also
4542 // recorded so the literal can become its `[[HomeObject]]` below.
4543 if matches!(flat[i], Value::Int(3)) {
4544 let key = with_host(|h| h.str_of(&flat[i + 1]));
4545 method_keys.push(key.clone());
4546 props.insert(key, flat[i + 2].clone());
4547 i += 3;
4548 continue;
4549 }
4550 let spread = matches!(flat[i], Value::Int(1));
4551 if spread {
4552 let src = flat[i + 1].clone();
4553 // A STRING source spreads its index properties (`{..."ab"}` is
4554 // `{0:'a',1:'b'}`): CopyDataProperties (7.3.25) calls ToObject, and a
4555 // String exotic object owns one enumerable property per UTF-16 code
4556 // UNIT (10.4.3). `own_enum_entries_deep` only walks heap objects, so
4557 // a string source contributed nothing and `{..."ab"}` was `{}`.
4558 // Every other primitive (number/boolean/symbol) boxes to an object
4559 // with no own enumerable properties, and null/undefined are ignored,
4560 // so those correctly stay no-ops on the path below.
4561 if let Some(s) = with_host(|h| h.as_str(&src)) {
4562 for idx in 0..crate::utf16::len(&s) {
4563 if let Ok(ch) = get_property(&src, &idx.to_string()) {
4564 props.insert(idx.to_string(), ch);
4565 }
4566 }
4567 i += 3;
4568 continue;
4569 }
4570 // Object spread copies own *enumerable* properties only — never the
4571 // hidden `@@…` slots (copying `@@native` used to turn `{...buf}`
4572 // into something that still claimed to be a Buffer) and never a
4573 // property a descriptor marked non-enumerable.
4574 // A getter that throws during spread propagates as a thrown value,
4575 // which in the VM means aborting the frame.
4576 let entries = match host::own_enum_entries_deep(&src) {
4577 Ok(e) => e,
4578 Err(e) => return abort(vm, e),
4579 };
4580 for (k, v) in entries {
4581 props.insert(k, v);
4582 }
4583 // `CopyDataProperties` (7.3.25) copies own enumerable SYMBOL keys
4584 // too — only `Object.keys`/`for-in`/`JSON.stringify` skip them.
4585 for (k, v) in with_host(|h| h.own_symbol_entries(&src)) {
4586 props.insert(k, v);
4587 }
4588 } else {
4589 let key = with_host(|h| h.str_of(&flat[i + 1]));
4590 if key == "__proto__" {
4591 proto_override = Some(flat[i + 2].clone());
4592 } else {
4593 props.insert(key, flat[i + 2].clone());
4594 }
4595 }
4596 i += 3;
4597 }
4598 with_host(|h| {
4599 let o = h.new_object(props);
4600 if let Some(p) = proto_override {
4601 if matches!(p, Value::Obj(_)) {
4602 h.set_proto(&o, p);
4603 }
4604 }
4605 // A method DEFINED here takes the literal as its `[[HomeObject]]`, which
4606 // is what `super` inside it resolves through. The home object is fixed
4607 // at definition, so a method that merely arrives as a value
4608 // (`{ m: other.m }`) keeps the one it was defined with — stamping every
4609 // method-valued property instead rebound the original and changed what
4610 // IT resolved.
4611 for key in &method_keys {
4612 let m = match h.get(&o) {
4613 Some(JsObj::Object(p)) => p.get(key).cloned(),
4614 _ => None,
4615 };
4616 if let Some(m) = m {
4617 if let Some(JsObj::Func(f)) = h.get_mut(&m) {
4618 f.home_object = Some(o.clone());
4619 }
4620 }
4621 }
4622 o
4623 })
4624}
4625
4626fn b_mkfunc(vm: &mut VM, _: u8) -> Value {
4627 let def_id = match vm.pop() {
4628 Value::Int(n) => n as usize,
4629 Value::Float(f) => f as usize,
4630 _ => return abort(vm, "internal: MKFUNC id".into()),
4631 };
4632 let (is_arrow, self_name) = with_host(|h| match h.funcs.get(def_id) {
4633 Some(d) => (
4634 d.is_arrow,
4635 (d.self_name && !d.name.is_empty()).then(|| d.name.clone()),
4636 ),
4637 None => (false, None),
4638 });
4639 with_host(|h| {
4640 let mut env = h.current_env_capture();
4641 let this = h.current_this();
4642 // An arrow has no `super` of its own: it uses the enclosing METHOD's,
4643 // exactly as it uses the enclosing `this`. Nothing was captured, so
4644 // `super.m()` inside an arrow reported the method missing — in a class
4645 // method as well as an object literal.
4646 let (home_class, home_static, home_object) = if is_arrow {
4647 h.current_home()
4648 } else {
4649 (None, false, None)
4650 };
4651 // A named function expression closes over an extra scope holding its own
4652 // name, so the body can recurse through it (`function f(){ … f() … }`)
4653 // independently of whatever the outer binding is later set to.
4654 if self_name.is_some() {
4655 env = host::child_env(env);
4656 }
4657 let f = h.alloc(JsObj::Func(FuncVal {
4658 def_id,
4659 env: Some(env.clone()),
4660 this,
4661 is_arrow,
4662 home_class,
4663 home_static,
4664 home_object,
4665 }));
4666 if let Some(n) = self_name {
4667 env.borrow_mut().vars.insert(n, f.clone());
4668 }
4669 f
4670 })
4671}
4672
4673// ── truthiness / coercion / equality ──────────────────────────────────────────
4674
4675fn b_truthy(vm: &mut VM, _: u8) -> Value {
4676 let v = vm.pop();
4677 Value::Bool(with_host(|h| h.truthy(&v)))
4678}
4679
4680fn b_nullish(vm: &mut VM, _: u8) -> Value {
4681 let v = vm.pop();
4682 Value::Bool(with_host(|h| h.is_nullish(&v)))
4683}
4684
4685fn b_tostr(vm: &mut VM, _: u8) -> Value {
4686 let v = vm.pop();
4687 // ToString with user-`toString`/`valueOf` dispatch (template interpolation,
4688 // `String(x)`, object keys).
4689 match host::to_string_value(&v) {
4690 Ok(s) => s,
4691 Err(e) => abort(vm, e),
4692 }
4693}
4694
4695fn b_typeof(vm: &mut VM, _: u8) -> Value {
4696 let v = vm.pop();
4697 with_host(|h| {
4698 let t = h.type_of(&v);
4699 h.new_str(t)
4700 })
4701}
4702
4703/// `typeof <bare ident>`: read the name like `b_getlocal` but return "undefined"
4704/// (never a ReferenceError) when the name is unbound — JS `typeof` semantics.
4705fn b_typeof_name(vm: &mut VM, _: u8) -> Value {
4706 let name = sval(&vm.pop());
4707 // `typeof` does NOT excuse the temporal dead zone: it answers "undefined"
4708 // for an UNBOUND name, but a `let` above its declaration is bound and
4709 // throws. Reading the marker's type answered "function".
4710 if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
4711 return abort(vm, host::tdz_error(&name));
4712 }
4713 if let Some(v) = with_host(|h| h.read_name(&name)) {
4714 if with_host(|h| h.is_tdz(&v)) {
4715 return abort(vm, host::tdz_error(&name));
4716 }
4717 }
4718 // Bound name (user variable) → typeof its value.
4719 if let Some(v) = with_host(|h| h.read_name(&name)) {
4720 return with_host(|h| {
4721 let t = h.type_of(&v);
4722 h.new_str(t)
4723 });
4724 }
4725 // Lazily-bound globals mirror `b_getlocal`: resolve to the same value it
4726 // would produce, then take its type (so object-namespaces like `console`/
4727 // `Math`/`JSON`/`process` report "object", constructors report "function").
4728 let t = match name.as_str() {
4729 "undefined" => "undefined".to_string(),
4730 "NaN" | "Infinity" => "number".to_string(),
4731 "globalThis" | "global" => "object".to_string(),
4732 n if is_namespace(n) || is_known_builtin(n) => {
4733 let v = with_host(|h| h.alloc(JsObj::Builtin(name.clone())));
4734 with_host(|h| h.type_of(&v)).to_string()
4735 }
4736 _ => "undefined".to_string(), // genuinely unbound → JS returns "undefined"
4737 };
4738 with_host(|h| h.new_str(t))
4739}
4740
4741fn b_strict_eq(vm: &mut VM, _: u8) -> Value {
4742 let b = vm.pop();
4743 let a = vm.pop();
4744 Value::Bool(with_host(|h| h.strict_eq(&a, &b)))
4745}
4746
4747fn b_loose_eq(vm: &mut VM, _: u8) -> Value {
4748 let b = vm.pop();
4749 let a = vm.pop();
4750 // Abstract Equality steps 10-11 (7.2.15): object ⇄ primitive converts the
4751 // object with `ToPrimitive` — a JS `valueOf`/`Symbol.toPrimitive` call, so it
4752 // runs before the host borrow. Object ⇄ object stays a reference check.
4753 let (a, b) = match with_host(|h| (host::is_primitive(h, &a), host::is_primitive(h, &b))) {
4754 (false, true) if coerces_against_object(&b) => match host::to_primitive(&a, "default") {
4755 Ok(p) => (p, b),
4756 Err(e) => return abort(vm, e),
4757 },
4758 (true, false) if coerces_against_object(&a) => match host::to_primitive(&b, "default") {
4759 Ok(p) => (a, p),
4760 Err(e) => return abort(vm, e),
4761 },
4762 _ => (a, b),
4763 };
4764 Value::Bool(with_host(|h| h.loose_eq(&a, &b)))
4765}
4766
4767fn b_instanceof(vm: &mut VM, _: u8) -> Value {
4768 let ctor = vm.pop();
4769 let obj = vm.pop();
4770 match host::instance_of(&obj, &ctor) {
4771 Ok(b) => Value::Bool(b),
4772 Err(e) => abort(vm, e),
4773 }
4774}
4775
4776// ── bitwise / unary ───────────────────────────────────────────────────────────
4777
4778fn b_binop(vm: &mut VM, _: u8) -> Value {
4779 let b = vm.pop();
4780 let a = vm.pop();
4781 let tag = match vm.pop() {
4782 Value::Int(n) => n,
4783 _ => 0,
4784 };
4785 // Both operands are ToPrimitive-d with the number hint before ToInt32
4786 // (ECMA-262 13.12.1), which has to happen outside the host borrow.
4787 let r = host::to_primitive(&a, "number")
4788 .and_then(|a| host::to_primitive(&b, "number").map(|b| (a, b)))
4789 .and_then(|(a, b)| with_host(|h| h.bitwise(tag, &a, &b)));
4790 finish(vm, r)
4791}
4792
4793fn b_unary(vm: &mut VM, _: u8) -> Value {
4794 let v = vm.pop();
4795 let tag = match vm.pop() {
4796 Value::Int(n) => n,
4797 _ => 0,
4798 };
4799 // Unary `+`/`~` on a BigInt: `+` is a hard TypeError in JS; `~x` is `-x - 1`
4800 // computed in arbitrary precision.
4801 if with_host(|h| h.is_bigint_val(&v)) {
4802 return match tag {
4803 host::unop::POS => abort(
4804 vm,
4805 host::type_error("Cannot convert a BigInt value to a number"),
4806 ),
4807 host::unop::BITNOT => {
4808 let b = with_host(|h| h.as_bigint(&v)).unwrap();
4809 let r = -(b + num_bigint::BigInt::from(1));
4810 with_host(|h| h.new_bigint(r))
4811 }
4812 _ => Value::Undef,
4813 };
4814 }
4815 // `ToNumber` outside the host borrow: an object operand's `valueOf` /
4816 // `Symbol.toPrimitive` is a JS call, so it cannot run under `with_host`.
4817 let n = match host::to_number_value(&v) {
4818 Ok(n) => n,
4819 Err(e) => return abort(vm, e),
4820 };
4821 match tag {
4822 host::unop::POS => Value::Float(n),
4823 host::unop::BITNOT => {
4824 let i = if n.is_finite() {
4825 n.trunc() as i64 as i32
4826 } else {
4827 0
4828 };
4829 Value::Float(!i as f64)
4830 }
4831 _ => Value::Undef,
4832 }
4833}
4834
4835// ── membership ────────────────────────────────────────────────────────────────
4836
4837fn b_contains(vm: &mut VM, _: u8) -> Value {
4838 let container = vm.pop();
4839 let key = vm.pop();
4840 // `x in y` requires y to be an object. V8 names both operands:
4841 // `Cannot use 'in' operator to search for 'a' in 5`.
4842 // A heap-backed PRIMITIVE — a string, a symbol, a bigint — is a
4843 // `Value::Obj` in this host but is not an object, so the shape test alone
4844 // let `'length' in 'ab'` and `'description' in Symbol('x')` answer `true`
4845 // where node throws. `is_primitive` is the same predicate `ToObject` and
4846 // `typeof` use, so the three cannot disagree about what an object is.
4847 if !matches!(container, Value::Obj(_)) || with_host(|h| host::is_primitive(h, &container)) {
4848 let (k, c) = with_host(|h| (h.property_key(&key), h.str_of(&container)));
4849 return abort(
4850 vm,
4851 host::type_error(&format!(
4852 "Cannot use 'in' operator to search for '{k}' in {c}"
4853 )),
4854 );
4855 }
4856 let k = match host::to_property_key(&key) {
4857 Ok(k) => k,
4858 Err(e) => return abort(vm, e),
4859 };
4860 match has_property(&container, &k) {
4861 Ok(b) => Value::Bool(b),
4862 Err(e) => abort(vm, e),
4863 }
4864}
4865
4866// ── control ───────────────────────────────────────────────────────────────────
4867
4868fn b_sig_return(vm: &mut VM, _: u8) -> Value {
4869 let v = vm.pop();
4870 with_host(|h| h.signal = Some(host::Signal::Return(v.clone())));
4871 vm.ip = vm.chunk.ops.len();
4872 v
4873}
4874
4875/// `break [label]` whose target loop lives in an enclosing chunk (the statement is
4876/// inside a `try` block, which the host runs as its own chunk). Raise the signal
4877/// and halt this chunk; `SIG_UNWIND` after the `TRY` op re-dispatches it.
4878fn b_sig_break(vm: &mut VM, _: u8) -> Value {
4879 let label = sval(&vm.pop());
4880 let label = (!label.is_empty()).then_some(label);
4881 with_host(|h| h.signal = Some(host::Signal::Break(label)));
4882 vm.ip = vm.chunk.ops.len();
4883 Value::Undef
4884}
4885
4886/// `continue [label]` out of a `try` block — see [`b_sig_break`].
4887fn b_sig_continue(vm: &mut VM, _: u8) -> Value {
4888 let label = sval(&vm.pop());
4889 let label = (!label.is_empty()).then_some(label);
4890 with_host(|h| h.signal = Some(host::Signal::Continue(label)));
4891 vm.ip = vm.chunk.ops.len();
4892 Value::Undef
4893}
4894
4895/// Dispatch a pending control signal at the instruction after a `TRY`. `tag`
4896/// describes what the `try` is nested in (see [`host::unwind`]):
4897///
4898/// * no signal → `NONE`, execution continues normally;
4899/// * `Return`, or no enclosing loop in this chunk → halt the chunk so the signal
4900/// keeps travelling outward;
4901/// * `break`/`continue` targeting the enclosing loop → consume it and report
4902/// `BREAK`/`CONTINUE` so the compiler-emitted jump lands on the loop's exit /
4903/// continue target;
4904/// * a LABELED `break`/`continue` for some outer loop → report `BREAK` but leave
4905/// the signal pending, so leaving this loop re-dispatches it one level out.
4906fn b_sig_unwind(vm: &mut VM, _: u8) -> Value {
4907 let cont_tag = sval(&vm.pop());
4908 let brk_tag = sval(&vm.pop());
4909 let sig = match with_host(|h| h.signal.clone()) {
4910 Some(s) => s,
4911 None => return Value::Int(host::unwind::NONE),
4912 };
4913 // Nothing in this chunk can catch a `break`: halt so the signal keeps going.
4914 let propagate = |vm: &mut VM| {
4915 vm.ip = vm.chunk.ops.len();
4916 Value::Int(host::unwind::NONE)
4917 };
4918 match &sig {
4919 host::Signal::Return(_) => propagate(vm),
4920 host::Signal::Break(label) => {
4921 if brk_tag == host::unwind::NO_LOOP {
4922 return propagate(vm);
4923 }
4924 let mine = match label {
4925 None => true, // unlabeled: always the innermost enclosing context
4926 Some(l) => brk_tag == *l,
4927 };
4928 if mine {
4929 with_host(|h| h.signal = None);
4930 }
4931 // Not ours: still leave this context by its break exit, keeping the
4932 // signal pending for the next dispatch point one level out.
4933 Value::Int(host::unwind::BREAK)
4934 }
4935 host::Signal::Continue(label) => {
4936 let mine = match label {
4937 // Unlabeled `continue` binds to the innermost continue-catching
4938 // loop — which a `switch` between here and it is NOT.
4939 None => cont_tag != host::unwind::NO_LOOP,
4940 Some(l) => cont_tag == *l,
4941 };
4942 if mine {
4943 with_host(|h| h.signal = None);
4944 return Value::Int(host::unwind::CONTINUE);
4945 }
4946 if brk_tag == host::unwind::NO_LOOP {
4947 return propagate(vm);
4948 }
4949 // The target loop is further out: exit the innermost context here and
4950 // re-dispatch there.
4951 Value::Int(host::unwind::BREAK)
4952 }
4953 }
4954}
4955
4956fn b_throw(vm: &mut VM, _: u8) -> Value {
4957 let v = vm.pop();
4958 let msg = with_host(|h| {
4959 h.exc = Some(v.clone());
4960 // Prefer an error object's message for the top-level report.
4961 error_display(h, &v)
4962 });
4963 abort(vm, msg)
4964}
4965
4966fn error_display(h: &host::JsHost, v: &Value) -> String {
4967 if let Some(JsObj::Object(props)) = h.get(v) {
4968 let name = props
4969 .get("name")
4970 .map(|x| h.str_of(x))
4971 .unwrap_or_else(|| "Error".into());
4972 if let Some(m) = props.get("message") {
4973 return format!("Uncaught {name}: {}", h.str_of(m));
4974 }
4975 }
4976 format!("Uncaught {}", h.str_of(v))
4977}
4978
4979fn b_try(vm: &mut VM, _: u8) -> Value {
4980 let id = match vm.pop() {
4981 Value::Int(n) => n as usize,
4982 _ => return abort(vm, "internal: TRY id".into()),
4983 };
4984 // Shape only. Running a `try` used to clone the whole `TryDef` — its block,
4985 // its handler and its finalizer bytecode — every time control entered it,
4986 // which for a `try` inside a loop is once per iteration.
4987 let (has_handler, catch_bind, has_finalizer) = match with_host(|h| h.try_shape(id)) {
4988 Some(t) => t,
4989 None => return abort(vm, "internal: unknown try id".into()),
4990 };
4991 let mut pending: Option<String> = None;
4992 // Each sub-block runs as its own chunk on THIS frame, so a throw part-way
4993 // through can leave block scopes open. Snapshot the scope and restore it
4994 // before the handler and after the whole statement.
4995 let scope = with_host(|h| h.scope_snapshot());
4996
4997 with_host(|h| h.push_scope()); // the try block is its own block scope
4998 let body_res = host::run_chunk_keyed(host::try_key(id, 0), || {
4999 with_host(|h| h.try_chunk(id, 0)).expect("try block exists")
5000 });
5001 with_host(|h| h.restore_scope(scope.clone()));
5002 let signal_after = with_host(|h| h.signal.is_some());
5003 if let Err(e) = body_res {
5004 if signal_after {
5005 pending = Some(e);
5006 } else if has_handler {
5007 // Bind the thrown value (or a synthesized error) to the catch param.
5008 let thrown =
5009 with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
5010 with_host(|h| {
5011 h.error = None;
5012 h.exc = None;
5013 });
5014 // The catch parameter is block-scoped to the handler.
5015 with_host(|h| h.push_scope());
5016 if let Some(name) = &catch_bind {
5017 with_host(|h| h.declare_name(name, thrown));
5018 }
5019 let hres = host::run_chunk_keyed(host::try_key(id, 1), || {
5020 with_host(|h| h.try_chunk(id, 1)).expect("handler exists")
5021 });
5022 with_host(|h| h.restore_scope(scope.clone()));
5023 if let Err(e2) = hres {
5024 pending = Some(e2);
5025 }
5026 } else {
5027 pending = Some(e);
5028 }
5029 }
5030
5031 // finally always runs; a finally error/signal supersedes.
5032 if has_finalizer {
5033 let sig_before = with_host(|h| h.signal.take());
5034 with_host(|h| h.push_scope()); // ditto for `finally`
5035 let fres = host::run_chunk_keyed(host::try_key(id, 2), || {
5036 with_host(|h| h.try_chunk(id, 2)).expect("finalizer exists")
5037 });
5038 with_host(|h| h.restore_scope(scope.clone()));
5039 match fres {
5040 Ok(_) => {
5041 if with_host(|h| h.signal.is_none()) {
5042 // The finalizer completed normally: the try/catch block's own
5043 // abrupt completion resumes.
5044 with_host(|h| h.signal = sig_before);
5045 } else {
5046 // ECMA-262 14.15.3 TryStatement evaluation: when the finalizer's
5047 // completion is abrupt (`return`/`break`/`continue` inside
5048 // `finally`), that completion REPLACES the try/catch block's —
5049 // including a pending throw, which is discarded, not rethrown.
5050 pending = None;
5051 with_host(|h| {
5052 h.error = None;
5053 h.exc = None;
5054 });
5055 }
5056 }
5057 Err(e) => pending = Some(e),
5058 }
5059 }
5060
5061 if let Some(e) = pending {
5062 return abort(vm, e);
5063 }
5064 Value::Undef
5065}
5066
5067/// Synthesize an `Error`-shaped object from an internal error string, linked to
5068/// the matching builtin error prototype so `instanceof`/`.constructor` work.
5069pub(crate) fn synth_error(h: &mut host::JsHost, e: &str) -> Value {
5070 h.ensure_error_protos();
5071 // A `DOMException` marker: the WHATWG error NAME rides in the string, since
5072 // it is not one of the ECMAScript error classes below.
5073 if let Some(rest) = e.strip_prefix(host::DOM_MARK) {
5074 if let Some((name, msg)) = rest.split_once('\u{1}') {
5075 return dom_exception_with(h, name, msg);
5076 }
5077 }
5078 // A `Name [ERR_CODE]: message` head carries a Node error `code` next to the
5079 // error class, exactly as Node's internal errors render it in `.stack`.
5080 let (head, rest) = match e.split_once(": ") {
5081 Some((n, m)) => (n, m.to_string()),
5082 None => ("", e.to_string()),
5083 };
5084 let (base, code) = match head.split_once(" [") {
5085 Some((n, c)) if c.ends_with(']') => (n, Some(c[..c.len() - 1].to_string())),
5086 _ => (head, None),
5087 };
5088 let (name, mut message) = if host::ERROR_NAMES.contains(&base) {
5089 (base.to_string(), rest)
5090 } else {
5091 ("Error".to_string(), e.to_string())
5092 };
5093 // A `host::plain_coded_error` marker: the code rides at the head of the
5094 // MESSAGE rather than in the class, because Node's native-layer errors set
5095 // `.code` while leaving `String(err)` unbracketed (`TypeError: Invalid URL`
5096 // with `code === 'ERR_INVALID_URL'`). Strip it back off here — the marker is
5097 // internal and must never reach a user-visible `.message`.
5098 let mut code = code;
5099 // Whether `String(err)`/`err.stack` show `Name [CODE]:` — true for the
5100 // bracketed head, false for the marker form.
5101 let mut bracketed = code.is_some();
5102 // Extra own properties (`input`, `base`) from `host::plain_coded_error_with`.
5103 let mut fields: Vec<(String, String)> = Vec::new();
5104 if let Some(rest) = message.strip_prefix(host::CODE_MARK) {
5105 if let Some((c, m)) = rest.split_once('\u{1}') {
5106 code = Some(c.to_string());
5107 bracketed = false;
5108 let (m, fs) = host::split_error_fields(m);
5109 fields = fs
5110 .into_iter()
5111 .map(|(k, v)| (k.to_string(), v.to_string()))
5112 .collect();
5113 message = m.to_string();
5114 }
5115 }
5116 let mut props: IndexMap<String, Value> = IndexMap::new();
5117 let mv = h.new_str(message.clone());
5118 props.insert("message".into(), mv);
5119 if let Some(c) = &code {
5120 let cv = h.new_str(c.clone());
5121 props.insert("code".into(), cv);
5122 for (k, v) in fields {
5123 let fv = h.new_str(v);
5124 props.insert(k, fv);
5125 }
5126 if bracketed {
5127 // Marks this as a Node JS-layer error, whose `toString` brackets the
5128 // code. A native-layer error has the same `.code` and does not.
5129 props.insert("@@nodeError".into(), Value::Bool(true));
5130 }
5131 }
5132 let label = match (&code, bracketed) {
5133 (Some(c), true) => format!("{name} [{c}]"),
5134 _ => name.clone(),
5135 };
5136 let frames = h.stack_frames();
5137 let stack = if message.is_empty() {
5138 format!("{label}{frames}")
5139 } else {
5140 format!("{label}: {message}{frames}")
5141 };
5142 let sv = h.new_str(stack);
5143 props.insert("stack".into(), sv);
5144 // A libuv system-error message is itself the canonical encoding of the
5145 // error's metadata — `ENOENT: no such file or directory, open '/x'` — so a
5146 // filesystem/network failure recovers the enumerable `code`/`errno`/
5147 // `syscall`/`path` own properties that `err.code === 'ENOENT'` checks (the
5148 // single most common error-handling idiom in Node packages) depend on.
5149 for (k, v) in syscall_error_fields(&message) {
5150 let sv = match v {
5151 SysField::Str(s) => h.new_str(s),
5152 SysField::Num(n) => Value::Float(n),
5153 };
5154 props.insert(k.into(), sv);
5155 }
5156 let obj = h.new_object(props);
5157 if let Some(p) = host::error_proto_of(h, &name) {
5158 h.set_proto(&obj, p);
5159 }
5160 // `message`/`stack` are non-enumerable; a Node `ERR_*` error's `code` is not
5161 // (`Object.keys(e)` on an `ERR_INVALID_ARG_TYPE` reads `["code"]`).
5162 h.hide_prop(&obj, "message");
5163 h.hide_prop(&obj, "stack");
5164 obj
5165}
5166
5167enum SysField {
5168 Str(String),
5169 Num(f64),
5170}
5171
5172/// Decompose a libuv-shaped message (`ECODE: reason, syscall 'path'`) into the
5173/// own properties Node hangs off a system error. Returns empty for any message
5174/// that is not in that shape.
5175fn syscall_error_fields(message: &str) -> Vec<(&'static str, SysField)> {
5176 let (code, rest) = match message.split_once(": ") {
5177 Some((c, r))
5178 if c.len() >= 2
5179 && c.starts_with('E')
5180 && c.bytes()
5181 .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()) =>
5182 {
5183 (c, r)
5184 }
5185 _ => return Vec::new(),
5186 };
5187 let mut out: Vec<(&'static str, SysField)> = vec![
5188 ("errno", SysField::Num(errno_for(code))),
5189 ("code", SysField::Str(code.to_string())),
5190 ];
5191 // `reason, syscall 'path'` — the path is optional (`EPIPE: …, write`).
5192 if let Some((_, tail)) = rest.split_once(", ") {
5193 let (syscall, path) = match tail.split_once(" '") {
5194 // A two-path message ends `'from' -> 'to'`; `err.path` is the FIRST
5195 // one, so the scan stops at its closing quote rather than at the
5196 // end of the line — which had been swallowing `' -> 'dest` into the
5197 // path for every `rename` and `copyFile` failure.
5198 Some((s, p)) => (s, p.split_once('\'').map(|(first, _)| first)),
5199 None => (tail, None),
5200 };
5201 out.push(("syscall", SysField::Str(syscall.to_string())));
5202 if let Some(p) = path {
5203 out.push(("path", SysField::Str(p.to_string())));
5204 }
5205 }
5206 out
5207}
5208
5209/// The negative `errno` Node reports for a libuv error code on this platform.
5210/// Only the codes `err_str` can produce are mapped; anything else reports the
5211/// generic `EIO` number rather than inventing a value.
5212fn errno_for(code: &str) -> f64 {
5213 let n: i32 = match code {
5214 "ENOENT" => 2,
5215 "EACCES" => 13,
5216 "EEXIST" => 17,
5217 "ENOTDIR" => 20,
5218 "EISDIR" => 21,
5219 "EINVAL" => 22,
5220 "EPIPE" => 32,
5221 "ENOTEMPTY" => 66,
5222 _ => 5, // EIO
5223 };
5224 -f64::from(n)
5225}
5226
5227// ── iteration ─────────────────────────────────────────────────────────────────
5228
5229fn b_getiter(vm: &mut VM, _: u8) -> Value {
5230 let v = vm.pop();
5231 // A generator is its own iterator (resumed lazily by FORITER).
5232 if with_host(|h| h.is_generator_val(&v)) {
5233 return v;
5234 }
5235 // A Proxy's iterator comes from its traps, materialized eagerly: the
5236 // `lookup_chain` probe below reads the property map a proxy does not have.
5237 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5238 return match crate::proxy::iterate(&v) {
5239 Ok(Some(items)) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
5240 Ok(None) => abort(vm, "internal: kind_of said Proxy".into()),
5241 Err(e) => abort(vm, e),
5242 };
5243 }
5244 // Arrays and strings take the direct path below: they have no iterator
5245 // state to preserve and are the hot case, so they must not pay a property
5246 // lookup and a call per loop.
5247 let direct = matches!(
5248 with_host(|h| h.kind_of(&v)),
5249 Some(ObjKind::Array) | Some(ObjKind::Str)
5250 );
5251 // …but only while their `Symbol.iterator` is still reachable. It comes from
5252 // the intrinsic prototype, so replacing the link takes it away: node reports
5253 // `a is not iterable` for an array whose prototype is a plain object, where
5254 // the fast path below iterated the backing vector regardless.
5255 if !own_intrinsic_reachable(&v)
5256 && !matches!(
5257 get_property(&v, "@@iterator"),
5258 Ok(ref f) if with_host(|h| host::is_callable(h, f))
5259 )
5260 {
5261 let shown = with_host(|h| h.inspect(&v));
5262 let msg = host::type_error(&format!("{shown} is not iterable"));
5263 return abort(vm, host::name_call_site(vm, &shown, msg));
5264 }
5265 // Anything else with a `Symbol.iterator`: call it for the iterator object.
5266 //
5267 // Resolved as a full property READ, not a stored-property lookup. A
5268 // NATIVE-tagged object (`URLSearchParams`, `Headers`, `Map`, `Set`)
5269 // dispatches its methods through the stdlib table rather than a property
5270 // map, so a `lookup_chain` probe found nothing and the loop fell through to
5271 // materializing the value — which threw for `URLSearchParams` and
5272 // snapshotted for `Map`. Spreading the same object already worked, because
5273 // that path had been fixed and this one had not.
5274 if !direct {
5275 if let Ok(iter_fn) = get_property(&v, "@@iterator") {
5276 if with_host(|h| host::is_callable(h, &iter_fn)) {
5277 return match host::invoke(&iter_fn, Vec::new(), Some(v.clone())) {
5278 Ok(it) => it,
5279 Err(e) => abort(vm, e),
5280 };
5281 }
5282 }
5283 }
5284 match with_host(|h| h.iter_vec(&v)) {
5285 Ok(items) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
5286 // V8 names the SOURCE EXPRESSION, not the value: `for (const x of a)`
5287 // reports `a is not iterable`. The text was recorded for this op.
5288 Err(e) => {
5289 let shown = with_host(|h| h.inspect(&v));
5290 let named = host::name_call_site(vm, &shown, e);
5291 abort(vm, named)
5292 }
5293 }
5294}
5295
5296fn b_forin_keys(vm: &mut VM, _: u8) -> Value {
5297 let v = vm.pop();
5298 // `for-in` over a Proxy is 14.7.5.9 `EnumerateObjectProperties`: the
5299 // `ownKeys` trap filtered by `[[GetOwnProperty]]`'s `enumerable`. Both traps
5300 // are user code, so this cannot run inside `enum_keys`'s `&mut` host borrow.
5301 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
5302 // `ownKeys` ONLY. The `enumerable` filter is 14.7.5.10's per-key
5303 // `[[GetOwnProperty]]`, which `FORIN_ALIVE` runs at the moment each key
5304 // is visited — so the `getOwnPropertyDescriptor` traps interleave with
5305 // the body the way node's do, instead of all firing up front.
5306 return match crate::proxy::own_keys(&v) {
5307 Ok(keys) => with_host(|h| {
5308 let out: Vec<Value> = keys
5309 .unwrap_or_default()
5310 .into_iter()
5311 .filter(|k| !host::is_symbol_key(k))
5312 .map(|k| h.new_str(k))
5313 .collect();
5314 h.new_array(out)
5315 }),
5316 Err(e) => abort(vm, e),
5317 };
5318 }
5319 let mut keys = with_host(|h| h.enum_keys(&v));
5320 // A member patched onto the receiver's INTRINSIC prototype is enumerable
5321 // and inherited, so `for-in` visits it after the own keys — but the
5322 // intrinsic prototypes are not links `enum_keys` can walk, so its chain
5323 // pass never reaches them.
5324 if !with_host(|h| h.has_null_proto(&v)) {
5325 let seen: Vec<String> = keys.iter().map(|k| with_host(|h| h.str_of(k))).collect();
5326 for ns in intrinsic_proto_namespaces(&v) {
5327 for k in with_host(|h| h.builtin_static_keys(&ns)) {
5328 if !seen.contains(&k) && !intrinsic_proto_member(&ns, &k) {
5329 keys.push(with_host(|h| h.new_str(k)));
5330 }
5331 }
5332 }
5333 }
5334 with_host(|h| h.new_array(keys))
5335}
5336
5337/// The intrinsic prototype namespaces `v` inherits from, nearest first — its
5338/// own constructor's and then `Object`'s, the same two steps
5339/// `inherited_builtin_static` looks a value up in.
5340fn intrinsic_proto_namespaces(v: &Value) -> Vec<String> {
5341 let ctor = match wrapped_primitive(v).as_ref().and_then(wrapper_ctor_of) {
5342 Some(c) => Some(c),
5343 None if is_arguments(v) => Some("Object"),
5344 None => with_host(|h| default_ctor_name(h, v)),
5345 };
5346 let mut out: Vec<String> = ctor
5347 .filter(|c| *c != "Object")
5348 .map(|c| format!("{c}.prototype"))
5349 .into_iter()
5350 .collect();
5351 out.push("Object.prototype".to_string());
5352 out
5353}
5354
5355/// `FORIN_ALIVE` — is `key` STILL an enumerable property of `obj`?
5356///
5357/// `for-in` takes its key list once (14.7.5.10 builds it lazily, but a snapshot
5358/// of the enumerable keys is observationally the same for everything except
5359/// this), and the body can delete a key before the loop reaches it. Node does
5360/// not visit a key deleted that way; without this check `delete d.z` inside the
5361/// loop still produced `x,y,z`.
5362///
5363/// The check is `[[GetOwnProperty]]`-shaped rather than `in`: on a Proxy it runs
5364/// the `getOwnPropertyDescriptor` trap, which is what node runs, and NOT the
5365/// `has` trap, which node never fires for `for-in`. That also puts each trap
5366/// call immediately before its visit, matching node's interleaving — the trap
5367/// log used to show every `gopd` up front because the key list was filtered
5368/// eagerly.
5369fn b_forin_alive(vm: &mut VM, _: u8) -> Value {
5370 let key = vm.pop();
5371 let obj = vm.pop();
5372 let name = with_host(|h| h.str_of(&key));
5373 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
5374 return match crate::proxy::own_enumerable(&obj, &name) {
5375 Ok(b) => Value::Bool(b),
5376 Err(e) => abort(vm, e),
5377 };
5378 }
5379 // A STRING's keys are its character indices. `in` is not defined on a string
5380 // primitive at all, so the ordinary path below has no answer for one and
5381 // `for (const i in 'abc')` came back empty.
5382 if let Some(s) = with_host(|h| h.as_str(&obj)) {
5383 let len = crate::utf16::len(&s);
5384 return Value::Bool(name.parse::<usize>().is_ok_and(|i| i < len));
5385 }
5386 // Any other receiver: EXISTENCE only. Node re-checks that the key is still
5387 // there and does NOT re-check enumerability — making one non-enumerable
5388 // mid-loop still visits it, where re-filtering on `enumerable` dropped it.
5389 // (The Proxy branch above does re-check, because there the answer comes from
5390 // the trap node itself calls.)
5391 Value::Bool(has_property_ordinary(&obj, &name))
5392}
5393
5394fn b_foriter(vm: &mut VM, _: u8) -> Value {
5395 let it = match vm.stack.last() {
5396 Some(v) => v.clone(),
5397 None => return abort(vm, "internal: FORITER with empty stack".into()),
5398 };
5399 // Eager array-backed iterator (arrays/strings/Map/Set).
5400 let eager = with_host(|h| {
5401 if let Some(JsObj::Iter { items, idx }) = h.get_mut(&it) {
5402 if *idx < items.len() {
5403 let v = items[*idx].clone();
5404 *idx += 1;
5405 return Some(Some(v));
5406 }
5407 return Some(None);
5408 }
5409 None
5410 });
5411 if let Some(step) = eager {
5412 return match step {
5413 Some(v) => {
5414 vm.push(v);
5415 Value::Bool(true)
5416 }
5417 None => Value::Bool(false),
5418 };
5419 }
5420 // Generator: resume one step.
5421 if with_host(|h| h.is_generator_val(&it)) {
5422 return match host::gen_resume(&it, Value::Undef) {
5423 Ok(host::GenStep::Yield(v)) => {
5424 vm.push(v);
5425 Value::Bool(true)
5426 }
5427 Ok(host::GenStep::Done(_)) => Value::Bool(false),
5428 Err(e) => abort(vm, e),
5429 };
5430 }
5431 // A user iterator object with a `.next()` returning `{ value, done }`.
5432 match host::call_method(&it, "next", Vec::new()) {
5433 Ok(step) => {
5434 let done = get_property(&step, "done")
5435 .map(|d| with_host(|h| h.truthy(&d)))
5436 .unwrap_or(true);
5437 if done {
5438 Value::Bool(false)
5439 } else {
5440 match get_property(&step, "value") {
5441 Ok(v) => {
5442 vm.push(v);
5443 Value::Bool(true)
5444 }
5445 Err(e) => abort(vm, e),
5446 }
5447 }
5448 }
5449 Err(e) => abort(vm, e),
5450 }
5451}
5452
5453fn b_unpack(vm: &mut VM, _: u8) -> Value {
5454 let star = match vm.pop() {
5455 Value::Int(n) => n,
5456 _ => -1,
5457 };
5458 let count = match vm.pop() {
5459 Value::Int(n) => n as usize,
5460 _ => 0,
5461 };
5462 let iterable = vm.pop();
5463 // Without a `...rest` element the pattern needs exactly `count` values and
5464 // must then close the iterator; draining hung on an unbounded source.
5465 let items = match if star < 0 {
5466 host::iter_take(&iterable, count)
5467 } else {
5468 host::iter_all(&iterable)
5469 } {
5470 Ok(v) => v,
5471 // Destructuring a non-iterable names the SOURCE EXPRESSION, the way
5472 // `for-of` does: `const [x] = o` reports `o is not iterable`. The text
5473 // was recorded for this op at compile time.
5474 Err(e) => {
5475 // Node names the source only when the pattern's right-hand side is
5476 // a plain IDENTIFIER — `const [x] = o` is `o is not iterable`.
5477 // Anything else (a member, a call, a nested pattern, a parameter)
5478 // reports the TYPE instead, with the property note. Measured across
5479 // twelve shapes rather than guessed.
5480 let msg = match host::call_site_text(vm) {
5481 Some(text) => host::type_error(&format!("{text} is not iterable")),
5482 None if e.ends_with(" is not iterable") => {
5483 host::type_error(¬_iterable_typed(&iterable))
5484 }
5485 None => e,
5486 };
5487 return abort(vm, msg);
5488 }
5489 };
5490 let ordered: Vec<Value> = if star < 0 {
5491 (0..count)
5492 .map(|i| items.get(i).cloned().unwrap_or(Value::Undef))
5493 .collect()
5494 } else {
5495 let si = star as usize;
5496 let after = count.saturating_sub(si + 1);
5497 let rest_end = items.len().saturating_sub(after).max(si);
5498 let mut out: Vec<Value> = Vec::with_capacity(count);
5499 for i in 0..si {
5500 out.push(items.get(i).cloned().unwrap_or(Value::Undef));
5501 }
5502 let rest: Vec<Value> = items
5503 .get(si..rest_end)
5504 .map(|s| s.to_vec())
5505 .unwrap_or_default();
5506 out.push(with_host(|h| h.new_array(rest)));
5507 for j in 0..after {
5508 out.push(items.get(rest_end + j).cloned().unwrap_or(Value::Undef));
5509 }
5510 out
5511 };
5512 if ordered.is_empty() {
5513 return Value::Undef;
5514 }
5515 for it in ordered[1..].iter().rev().cloned() {
5516 vm.push(it);
5517 }
5518 ordered[0].clone()
5519}
5520
5521fn b_build_args(vm: &mut VM, argc: u8) -> Value {
5522 let flat = pop_n(vm, argc as usize);
5523 let mut out = Vec::new();
5524 // Elided positions of an array literal (tag 2), recorded as the run-time
5525 // index each lands on — which only this walk knows, because a preceding
5526 // spread contributes an unknown number of elements. Call-argument lists,
5527 // the other `BUILD_ARGS` caller, cannot contain an elision, so this stays
5528 // empty for them.
5529 let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
5530 let mut i = 0;
5531 while i + 1 < flat.len() {
5532 let val = flat[i + 1].clone();
5533 match flat[i] {
5534 // Tag 1 is an ARRAY-LITERAL spread, tag 3 a CALL-ARGUMENT one. They
5535 // report a non-iterable differently, which is the only reason the
5536 // two are told apart here.
5537 Value::Int(1) => match host::iter_all(&val).map_err(|e| {
5538 let shown = with_host(|h| h.inspect(&val));
5539 host::name_call_site(vm, &shown, e)
5540 }) {
5541 Ok(items) => out.extend(items),
5542 Err(e) => return abort(vm, e),
5543 },
5544 Value::Int(3) => match host::iter_all(&val) {
5545 Ok(items) => out.extend(items),
5546 Err(e) => {
5547 // A NULLISH spread names the value and what could not be
5548 // read off it; anything else names the missing protocol.
5549 let shown = with_host(|h| h.is_nullish(&val).then(|| h.str_of(&val)));
5550 return abort(
5551 vm,
5552 match shown {
5553 Some(s) => host::type_error(&format!(
5554 "{s} is not iterable (cannot read property {s})"
5555 )),
5556 None if e.ends_with(" is not iterable") => host::type_error(
5557 "Spread syntax requires ...iterable[Symbol.iterator] to be a function",
5558 ),
5559 None => e,
5560 },
5561 );
5562 }
5563 },
5564 Value::Int(2) => {
5565 holes.insert(out.len());
5566 out.push(Value::Undef);
5567 }
5568 _ => out.push(val),
5569 }
5570 i += 2;
5571 }
5572 with_host(|h| {
5573 let arr = h.new_array(out);
5574 h.install_holes(&arr, holes);
5575 arr
5576 })
5577}
5578
5579// ── calls ──────────────────────────────────────────────────────────────────────
5580
5581fn b_call(vm: &mut VM, argc: u8) -> Value {
5582 let mut args = pop_n(vm, argc as usize);
5583 let name = sval(&args.remove(0));
5584 let r = host::call_named(&name, args);
5585 // A bare name that resolved to a non-callable reports the VALUE
5586 // (`undefined is not a function`); node names the identifier. Resolving it
5587 // again to learn what the message said costs nothing off the error path.
5588 let r = r.map_err(|e| {
5589 let shown = global_binding(&name)
5590 .map(|v| with_host(|h| h.str_of(&v)))
5591 .unwrap_or_default();
5592 host::name_call_site(vm, &shown, e)
5593 });
5594 finish(vm, r)
5595}
5596
5597/// `recv[0](…)` — a computed call whose key is an ARRAY INDEX rather than a
5598/// method name. `call_method` resolves by name and bottoms out in
5599/// `call_type_method`, which knows `sort`/`slice` and not `"0"`, so an element
5600/// that happens to be a function reported "is not a function". Read the element
5601/// and invoke it with `recv` as `this`, which is the receiver 13.3.6 gives it.
5602/// A computed call's key is a property key, so it goes through ToPropertyKey:
5603/// `arr[0](…)` looks up `"0"`. `sval` only unwraps an existing `Value::Str` and
5604/// answers "" for a number, which turned `arr[0]()` into a call to the method
5605/// named "" — so the key is stringified here instead.
5606fn call_key_of(v: &Value) -> String {
5607 if let Value::Str(s) = v {
5608 return (**s).clone();
5609 }
5610 // `ToPropertyKey`, not `ToString`. A SYMBOL key has an internal `@@name`
5611 // spelling that `str_of` does not produce — it renders
5612 // `Symbol(Symbol.iterator)` — so `obj[Symbol.iterator]()` dispatched a
5613 // method by that display text and reported it was not a function, for every
5614 // object including a plain literal with a computed symbol method. Reading
5615 // the same property without calling it worked, which is what hid this.
5616 with_host(|h| h.property_key(v))
5617}
5618
5619fn index_element_call(recv: &Value, name: &str, args: &[Value]) -> Option<Result<Value, String>> {
5620 if name.is_empty() || !name.bytes().all(|b| b.is_ascii_digit()) {
5621 return None;
5622 }
5623 let f = get_property(recv, name).ok()?;
5624 with_host(|h| host::is_callable(h, &f))
5625 .then(|| host::invoke(&f, args.to_vec(), Some(recv.clone())))
5626}
5627
5628fn b_call_method(vm: &mut VM, argc: u8) -> Value {
5629 let mut args = pop_n(vm, argc as usize);
5630 let recv = args.remove(0);
5631 let name = call_key_of(&args.remove(0));
5632 if let Some(r) = index_element_call(&recv, &name, &args) {
5633 return finish(vm, r);
5634 }
5635 let r = host::call_method(&recv, &name, args);
5636 // `z.f()` on a missing method is `z.f is not a function` in node, not
5637 // `f is not a function`: V8 names the callee as the source wrote it. The
5638 // text was recorded for this op at compile time.
5639 let r = r.map_err(|e| host::name_call_site(vm, &name, e));
5640 finish(vm, r)
5641}
5642
5643fn b_call_value(vm: &mut VM, argc: u8) -> Value {
5644 let mut args = pop_n(vm, argc as usize);
5645 let callable = args.remove(0);
5646 let r = host::invoke(&callable, args, None);
5647 // The callee here is an expression, not a name, so the message it produced
5648 // describes the VALUE (`undefined is not a function`); node names the
5649 // expression. Same site table, keyed on that rendering.
5650 let r = r.map_err(|e| {
5651 let shown = with_host(|h| h.str_of(&callable));
5652 host::name_call_site(vm, &shown, e)
5653 });
5654 finish(vm, r)
5655}
5656
5657/// `NEW_SPREAD` — `new C(...xs)`, where the argument list is a run-time array
5658/// rather than a fixed count of stack slots.
5659///
5660/// `compile_new` used to compile each argument with `compile_expr`, and a
5661/// spread there evaluates to the SPREAD OBJECT itself — so `new C(...[1, 2])`
5662/// passed the array as one argument and `new Date(...[2020, 0, 1])` built an
5663/// Invalid Date.
5664fn b_new_spread(vm: &mut VM, _: u8) -> Value {
5665 let args_arr = vm.pop();
5666 let ctor = vm.pop();
5667 let args = host::iter_all(&args_arr).unwrap_or_default();
5668 let r = host::construct(&ctor, args).map_err(|e| {
5669 let shown = with_host(|h| h.str_of(&ctor));
5670 host::name_call_site(vm, &shown, e)
5671 });
5672 finish(vm, r)
5673}
5674
5675fn b_new(vm: &mut VM, argc: u8) -> Value {
5676 let mut args = pop_n(vm, argc as usize);
5677 let ctor = args.remove(0);
5678 let r = host::construct(&ctor, args);
5679 // `new (o.a.b.c)()` on a non-constructor names the expression, as a failed
5680 // call does.
5681 let r = r.map_err(|e| {
5682 let shown = with_host(|h| h.str_of(&ctor));
5683 host::name_call_site(vm, &shown, e)
5684 });
5685 finish(vm, r)
5686}
5687
5688fn b_apply(vm: &mut VM, _: u8) -> Value {
5689 let args_arr = vm.pop();
5690 let callable = vm.pop();
5691 let args = host::iter_all(&args_arr).unwrap_or_default();
5692 let r = host::invoke(&callable, args, None);
5693 finish(vm, r)
5694}
5695
5696fn b_apply_method(vm: &mut VM, _: u8) -> Value {
5697 let args_arr = vm.pop();
5698 let name = call_key_of(&vm.pop());
5699 let recv = vm.pop();
5700 let args = host::iter_all(&args_arr).unwrap_or_default();
5701 if let Some(r) = index_element_call(&recv, &name, &args) {
5702 return finish(vm, r);
5703 }
5704 let r = host::call_method(&recv, &name, args);
5705 finish(vm, r)
5706}
5707
5708// ── numeric hook ──────────────────────────────────────────────────────────────
5709
5710/// Host callback for arithmetic fusevm cannot complete natively (a non-`Int`/
5711/// non-`Float` operand). Supplies JavaScript `+` concatenation and coercion.
5712///
5713/// Every operand is run through `ToPrimitive` FIRST (ECMA-262 13.15.3 for `+`,
5714/// 13.6.3 for the other arithmetic ops, 13.10.1 for the relational ones), which
5715/// is what invokes a user `valueOf`/`Symbol.toPrimitive`. It has to happen here
5716/// rather than inside `JsHost::arith`, because calling back into JS re-enters
5717/// the VM and `arith` runs under the host's `RefCell` borrow.
5718pub fn numeric_hook(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
5719 use NumOp::*;
5720 let (a, b) = match op {
5721 // `==`/`!=` only convert when the OTHER side is a primitive that can be
5722 // compared numerically or textually; `{} == {}` stays a reference check.
5723 Eq | Ne => {
5724 let (pa, pb) = with_host(|h| (host::is_primitive(h, a), host::is_primitive(h, b)));
5725 match (pa, pb) {
5726 (false, true) if coerces_against_object(b) => {
5727 (host::to_primitive(a, "default")?, b.clone())
5728 }
5729 (true, false) if coerces_against_object(a) => {
5730 (a.clone(), host::to_primitive(b, "default")?)
5731 }
5732 _ => (a.clone(), b.clone()),
5733 }
5734 }
5735 // `+` uses the default hint (`valueOf` first, but a string result still
5736 // selects concatenation); everything else uses the number hint.
5737 Add => (
5738 host::to_primitive(a, "default")?,
5739 host::to_primitive(b, "default")?,
5740 ),
5741 _ => (
5742 host::to_primitive(a, "number")?,
5743 host::to_primitive(b, "number")?,
5744 ),
5745 };
5746 reject_symbol_operand(op, &a, &b)?;
5747 with_host(|h| h.arith(op, &a, &b))
5748}
5749
5750/// A symbol has no `ToNumber` and no `ToString`, so every operator except the
5751/// equality family rejects it (7.1.4 step 2, 7.1.17 step 2). node-js instead
5752/// concatenated `Symbol(desc)` into the result.
5753///
5754/// Which of the two messages V8 uses is decided by whether the operation is
5755/// STRING concatenation — measured on node v26.7.0, `Symbol() + ''` is
5756/// `Cannot convert a Symbol value to a string` while `Symbol() + 1`,
5757/// `Symbol() + Symbol()` and `Symbol() * 1` are all
5758/// `Cannot convert a Symbol value to a number`. `==`/`===` never convert
5759/// (`Symbol() == 1` is `false`), so they are left alone.
5760fn reject_symbol_operand(op: NumOp, a: &Value, b: &Value) -> Result<(), String> {
5761 use NumOp::*;
5762 if matches!(op, Eq | Ne) {
5763 return Ok(());
5764 }
5765 let (sym, concat) = with_host(|h| {
5766 let is_sym = |v: &Value| matches!(h.get(v), Some(JsObj::Symbol { .. }));
5767 let is_str =
5768 |v: &Value| matches!(v, Value::Str(_)) || matches!(h.get(v), Some(JsObj::Str(_)));
5769 (is_sym(a) || is_sym(b), is_str(a) || is_str(b))
5770 });
5771 if !sym {
5772 return Ok(());
5773 }
5774 Err(host::type_error(if matches!(op, Add) && concat {
5775 "Cannot convert a Symbol value to a string"
5776 } else {
5777 "Cannot convert a Symbol value to a number"
5778 }))
5779}
5780
5781/// Whether a primitive `v` makes `==` against an object convert that object
5782/// (7.2.15 steps 10-11): numbers, strings, bigints and symbols do; `null`,
5783/// `undefined` and booleans are settled without a `ToPrimitive` call
5784/// (a boolean is coerced to a number first, and then it does).
5785fn coerces_against_object(v: &Value) -> bool {
5786 match v {
5787 Value::Undef => false,
5788 Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_) => true,
5789 _ => with_host(|h| !h.is_null(v)),
5790 }
5791}
5792
5793// ══ standard library ═══════════════════════════════════════════════════════════
5794
5795/// Namespaces reachable as bare globals.
5796fn is_namespace(name: &str) -> bool {
5797 matches!(
5798 name,
5799 "console"
5800 | "Math"
5801 | "JSON"
5802 | "Object"
5803 | "Array"
5804 | "Number"
5805 | "String"
5806 | "Boolean"
5807 | "Symbol"
5808 | "Reflect"
5809 | "Promise"
5810 | "process"
5811 | "Buffer"
5812 | "URL"
5813 | "URLSearchParams"
5814 )
5815}
5816
5817const GLOBAL_FUNCS: &[&str] = &[
5818 "parseInt",
5819 "parseFloat",
5820 "isNaN",
5821 "isFinite",
5822 "encodeURIComponent",
5823 "decodeURIComponent",
5824 "encodeURI",
5825 "decodeURI",
5826 // Annex B legacy encoders. Still globals on every engine, and still called
5827 // by pre-`encodeURIComponent` library code.
5828 "escape",
5829 "unescape",
5830 "eval",
5831 "String",
5832 "Number",
5833 "Boolean",
5834 "Array",
5835 "Object",
5836 "Function",
5837 "Symbol",
5838 "Map",
5839 "Set",
5840 "WeakMap",
5841 "WeakSet",
5842 "Promise",
5843 "Error",
5844 "TypeError",
5845 "RangeError",
5846 "SyntaxError",
5847 "ReferenceError",
5848 "EvalError",
5849 "URIError",
5850 "AggregateError",
5851 "DOMException",
5852 "Iterator",
5853 "BigInt",
5854 "RegExp",
5855 "Date",
5856 "ArrayBuffer",
5857 "DataView",
5858 "Uint8Array",
5859 "Int8Array",
5860 "Uint8ClampedArray",
5861 "Int16Array",
5862 "Uint16Array",
5863 "Int32Array",
5864 "Uint32Array",
5865 "Float32Array",
5866 "Float64Array",
5867 "BigInt64Array",
5868 "BigUint64Array",
5869 "WeakRef",
5870 "FinalizationRegistry",
5871 "TextEncoder",
5872 "TextDecoder",
5873 // WHATWG Fetch globals (see `stdlib::fetch`).
5874 "fetch",
5875 "Headers",
5876 "Request",
5877 "Response",
5878 "Blob",
5879 "File",
5880 "FormData",
5881 "AbortController",
5882 "AbortSignal",
5883 "queueMicrotask",
5884 "setTimeout",
5885 "setInterval",
5886 "setImmediate",
5887 "clearTimeout",
5888 "clearInterval",
5889 "clearImmediate",
5890 "structuredClone",
5891 // Base64 helpers. They existed only as `require('buffer').btoa`, but node
5892 // exposes both as globals, so `btoa('abc')` was a ReferenceError.
5893 "btoa",
5894 "atob",
5895 "Proxy",
5896 "require",
5897 // CommonJS loader dispatch targets referenced by per-module `require`
5898 // closures (see `module.rs`); never written by user code.
5899 "__cjs_require",
5900 "__cjs_resolve",
5901 "__cjs_cache",
5902];
5903
5904const NS_METHODS: &[&str] = &[
5905 "console.log",
5906 "console.error",
5907 "console.warn",
5908 "console.info",
5909 "console.debug",
5910 "Math.abs",
5911 "Math.acos",
5912 "Math.acosh",
5913 "Math.asin",
5914 "Math.asinh",
5915 "Math.atan",
5916 "Math.atanh",
5917 "Math.atan2",
5918 "Math.ceil",
5919 "Math.cbrt",
5920 "Math.expm1",
5921 "Math.clz32",
5922 "Math.cos",
5923 "Math.cosh",
5924 "Math.exp",
5925 "Math.floor",
5926 "Math.fround",
5927 "Math.hypot",
5928 "Math.imul",
5929 "Math.log",
5930 "Math.log1p",
5931 "Math.log2",
5932 "Math.log10",
5933 "Math.max",
5934 "Math.min",
5935 "Math.pow",
5936 "Math.random",
5937 "Math.round",
5938 "Math.sign",
5939 "Math.sin",
5940 "Math.sinh",
5941 "Math.sqrt",
5942 "Math.tan",
5943 "Math.tanh",
5944 "Math.trunc",
5945 "JSON.stringify",
5946 "JSON.parse",
5947 "JSON.rawJSON",
5948 "JSON.isRawJSON",
5949 "Object.keys",
5950 "Object.values",
5951 "Object.entries",
5952 "Object.assign",
5953 "Object.freeze",
5954 "Object.is",
5955 "Object.fromEntries",
5956 "Object.getPrototypeOf",
5957 "Object.setPrototypeOf",
5958 "Object.create",
5959 "Object.getOwnPropertyNames",
5960 "Object.getOwnPropertySymbols",
5961 "Object.defineProperty",
5962 "Object.getOwnPropertyDescriptor",
5963 "Object.getOwnPropertyDescriptors",
5964 "Object.defineProperties",
5965 "Object.isFrozen",
5966 "Object.isSealed",
5967 "Object.seal",
5968 "Object.preventExtensions",
5969 "Object.isExtensible",
5970 "Object.hasOwn",
5971 "Object.groupBy",
5972 "Array.isArray",
5973 "Array.from",
5974 "Array.fromAsync",
5975 "Array.of",
5976 "Number.isFinite",
5977 "Number.isInteger",
5978 "Number.isNaN",
5979 "Number.isSafeInteger",
5980 "Number.parseFloat",
5981 "Number.parseInt",
5982 "String.fromCharCode",
5983 "String.fromCodePoint",
5984 "String.raw",
5985 "Symbol.for",
5986 "Symbol.keyFor",
5987 "BigInt.asIntN",
5988 "BigInt.asUintN",
5989 "Proxy.revocable",
5990 "Reflect.defineProperty",
5991 "Reflect.deleteProperty",
5992 "Reflect.apply",
5993 "Reflect.construct",
5994 "Reflect.get",
5995 "Reflect.getOwnPropertyDescriptor",
5996 "Reflect.getPrototypeOf",
5997 "Reflect.has",
5998 "Reflect.isExtensible",
5999 "Reflect.ownKeys",
6000 "Reflect.preventExtensions",
6001 "Reflect.set",
6002 "Reflect.setPrototypeOf",
6003 "Promise.resolve",
6004 "Promise.reject",
6005 "Promise.all",
6006 "Promise.allSettled",
6007 "Promise.race",
6008 "Promise.any",
6009 "Promise.withResolvers",
6010 "Promise.try",
6011 "RegExp.escape",
6012 "Error.isError",
6013 "Map.groupBy",
6014 "Response.json",
6015 "Response.error",
6016 "Response.redirect",
6017 "AbortSignal.abort",
6018 "AbortSignal.timeout",
6019 "process.nextTick",
6020 "Error.captureStackTrace",
6021 "require.resolve",
6022 "require.resolve.paths",
6023 "process.memoryUsage.rss",
6024];
6025
6026/// The `name` and `length` a builtin function reports, from the generated
6027/// intrinsic table ([`crate::arity::BUILTIN_ARITY`]). `None` for a key the table
6028/// does not cover — every non-function namespace (`Math`, `require('fs')`),
6029/// and the core-module functions, whose arity is not specified anywhere.
6030pub fn builtin_meta(key: &str) -> Option<(&'static str, u32)> {
6031 crate::arity::BUILTIN_ARITY
6032 .binary_search_by(|(k, _, _)| (*k).cmp(key))
6033 .ok()
6034 .map(|i| {
6035 let (_, name, len) = crate::arity::BUILTIN_ARITY[i];
6036 (name, len)
6037 })
6038}
6039
6040/// The `name` a builtin function reports. The table answers for an intrinsic;
6041/// anything else falls back to the last segment of the key, which is what the
6042/// name is for every builtin this frontend synthesizes: `@proto:TypedArray:set`
6043/// is `set` and `fs.readFileSync` is `readFileSync`. Reporting the whole key was
6044/// how `[Function: @proto:TypedArray:set]` reached `console.log`.
6045pub fn builtin_name(key: &str) -> &str {
6046 if let Some((name, _)) = builtin_meta(key) {
6047 return name;
6048 }
6049 match key.strip_prefix("@proto:") {
6050 Some(rest) => rest.rsplit(':').next().unwrap_or(rest),
6051 // An accessor's getter is named `get <member>` (10.2.9 SetFunctionName
6052 // with a `get` prefix), which is what `util.inspect` prints for it and
6053 // what a library reads to identify one.
6054 None => key.rsplit('.').next().unwrap_or(key),
6055 }
6056}
6057
6058/// The `name` of an intrinsic accessor's getter thunk, or `None` for anything
6059/// else. Kept out of `builtin_name`'s `&str` return, which cannot own the
6060/// `"get size"` it has to build.
6061pub fn proto_getter_name(key: &str) -> Option<String> {
6062 let (verb, rest) = match key.strip_prefix("@protoget:") {
6063 Some(rest) => ("get", rest),
6064 None => ("set", key.strip_prefix("@protoset:")?),
6065 };
6066 let (_, member) = rest.split_once(':')?;
6067 Some(format!("{verb} {member}"))
6068}
6069
6070pub fn is_known_builtin(name: &str) -> bool {
6071 // Binary search over a sorted INDEX of the two tables rather than a scan of
6072 // both. This runs on every call whose callee is a builtin — `call_method`
6073 // asks it before dispatching `Math.max(…)` or `JSON.parse(…)` — and the
6074 // answer came only after a full scan of `GLOBAL_FUNCS` (77) plus a scan of
6075 // `NS_METHODS` up to the entry — 106 string comparisons for `Math.max`, 120
6076 // for `Object.keys` — because those tables are ordered for ENUMERATION (V8's
6077 // own order for `Math`/`Number`/`Reflect`), not for lookup. Eight probes
6078 // now. The index is built once per process and derived FROM those tables, so
6079 // it cannot drift from them.
6080 //
6081 // That is an operation count, not a measured time, and NO wall-clock win is
6082 // claimed. Re-measured in isolation (this hunk alone applied to the previous
6083 // commit, interleaved against it, minimums over ten rounds each): the A/B
6084 // ratio came out 0.753, 1.072, 0.994 and 0.744 across four repeats, while
6085 // the A/A control — the SAME binary under both labels — came out 1.084,
6086 // 1.072, 0.787 and 1.093. The A/B spread lies inside the A/A spread, so on
6087 // this machine the change is not distinguishable from noise. It is kept for
6088 // the comparison count and because it cannot drift from the tables it is
6089 // derived from, not because anything got faster.
6090 static SORTED: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
6091 let sorted = SORTED.get_or_init(|| {
6092 let mut v: Vec<&'static str> = GLOBAL_FUNCS
6093 .iter()
6094 .chain(NS_METHODS.iter())
6095 .copied()
6096 .collect();
6097 v.sort_unstable();
6098 v
6099 });
6100 sorted.binary_search(&name).is_ok() || is_namespace(name) || crate::stdlib::is_method(name)
6101}
6102
6103// ── dynamic functions (runtime source → callable) ────────────────────────────
6104
6105/// Build a callable from a complete function-expression source text — the ONE
6106/// dynamic-function generator on this frontend.
6107///
6108/// `src` is the exact source V8 synthesizes for the construct, WITHOUT the
6109/// wrapping parentheses needed to parse it as an expression: those are added
6110/// here, and `src` itself is retained so `Function.prototype.toString` reports
6111/// what V8 reports. The two callers synthesize different text and both shapes
6112/// are observable — see `stdlib::vm::compile_function` for the measured diff.
6113///
6114/// The body runs in the MODULE scope, never the constructing function's scope
6115/// (20.2.1.1.1 step 26 instantiates a dynamic function's body against the
6116/// *global* environment). That also makes a `var` inside the body a function
6117/// local: measured on node v26.7.0, `new Function('a','var zz = 5; return zz + a')`
6118/// returns 6 and leaves `globalThis.zz` `undefined`.
6119pub fn dynamic_function(src: &str) -> Result<Value, String> {
6120 let f = crate::eval_in_global_scope(&format!("({src})"))?;
6121 with_host(|h| {
6122 let s = h.new_str(src.to_string());
6123 h.set_fn_prop(&f, "@@source", s);
6124 });
6125 Ok(f)
6126}
6127
6128/// `new Function(p1, …, pN, body)` / `Function(p1, …, pN, body)`.
6129///
6130/// Argument convention (20.2.1.1.1): the LAST argument is the body and the rest
6131/// are parameter-list fragments joined with `,` — so a fragment may itself hold
6132/// several parameters (`new Function('a,b', 'c', …)` takes three). With no
6133/// arguments at all, both the parameter list and the body are empty.
6134///
6135/// Measured on node v26.7.0:
6136///
6137/// ```text
6138/// new Function('a','b','return a+b').toString() === 'function anonymous(a,b\n) {\nreturn a+b\n}'
6139/// new Function().toString() === 'function anonymous(\n) {\n\n}'
6140/// new Function('a,b','c','return [a,b,c]').length === 3
6141/// new Function('a','b','return a+b').name === 'anonymous'
6142/// ```
6143pub fn function_ctor(args: &[Value]) -> Result<Value, String> {
6144 let parts: Vec<String> = args.iter().map(|a| with_host(|h| h.str_of(a))).collect();
6145 let (params, body) = match parts.split_last() {
6146 Some((body, params)) => (params.join(","), body.clone()),
6147 None => (String::new(), String::new()),
6148 };
6149 dynamic_function(&format!("function anonymous({params}\n) {{\n{body}\n}}"))
6150}
6151
6152/// `eval(src)`. `direct` selects the scope the source runs in: a DIRECT eval —
6153/// the literal `eval(...)` call form — evaluates in the CALLER's scope, every
6154/// other route to the same function value is an INDIRECT eval and evaluates in
6155/// the global scope (ECMA-262 19.2.1.1 `PerformEval`). The two are told apart in
6156/// `host::call_named`, which `ops::CALL` reaches and `ops::CALL_VALUE`/`APPLY`
6157/// do not.
6158///
6159/// A non-string argument is returned unchanged (19.2.1.1 step 2).
6160pub fn eval_source(arg: Option<&Value>, direct: bool) -> Result<Value, String> {
6161 let v = arg.cloned().unwrap_or(Value::Undef);
6162 let is_string =
6163 matches!(v, Value::Str(_)) || with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_))));
6164 if !is_string {
6165 return Ok(v);
6166 }
6167 let src = with_host(|h| h.str_of(&v));
6168 // A DIRECT eval inherits the caller's strictness (19.2.1.1 step 10), which
6169 // decides both the early errors the COMPILE raises and the variable
6170 // environment below. An INDIRECT one is global-scope sloppy code.
6171 let caller_strict = direct && with_host(|h| h.current_strict());
6172 let chunk = crate::load_merged(crate::compile_completion_strict(&src, caller_strict)?);
6173 if !direct {
6174 return host::run_chunk_in_global_scope(chunk);
6175 }
6176 // A STRICT direct eval gets its OWN variable environment (19.2.1.1 step 12),
6177 // so its `var`s and function declarations die with it. Only a SLOPPY one
6178 // shares the caller's, which is the form that can inject a binding — and
6179 // sharing it unconditionally meant `eval('var x=1')` inside strict code
6180 // left `x` behind.
6181 let strict = caller_strict
6182 || src.trim_start().starts_with("'use strict'")
6183 || src.trim_start().starts_with("\"use strict\"");
6184 if !strict {
6185 // 19.2.1.1 steps 12-13: a SLOPPY direct eval shares the caller's
6186 // VARIABLE environment — which is what lets `eval('var x=1')` inject a
6187 // binding — but gets a fresh LEXICAL one of its own. A `let`, `const`
6188 // or `class` declared inside therefore dies with the eval; every one of
6189 // them was landing in the caller's scope, so `eval('let a=1')` left `a`
6190 // behind and `let a=1; eval('let a=2')` overwrote it.
6191 //
6192 // `push_scope` is exactly that split: `var` and a hoisted function
6193 // declaration bind to `base_env`, which this does not touch.
6194 with_host(|h| h.push_scope());
6195 let out = host::run_chunk_on(chunk);
6196 with_host(|h| h.pop_scope());
6197 return out;
6198 }
6199 let prev = with_host(|h| h.push_var_scope());
6200 let out = host::run_chunk_on(chunk);
6201 with_host(|h| h.pop_var_scope(prev));
6202 out
6203}
6204
6205/// Call a resolved builtin function (global or `namespace.method`).
6206pub fn call_builtin_function(name: &str, args: Vec<Value>) -> Result<Value, String> {
6207 // `require(spec)`: the ENTRY script's top-level require — core module first,
6208 // else the CommonJS loader resolving from the entry file's directory.
6209 if name == "require" {
6210 let spec = with_host(|h| h.str_of(&arg0(&args)));
6211 return crate::module::require(&spec, &crate::module::entry_dir());
6212 }
6213 // `__cjs_require(spec, fromDir)`: a per-module `require` closure's dispatch
6214 // into the loader, resolving `spec` against the module's own directory.
6215 if name == "__cjs_require" {
6216 let spec = with_host(|h| h.str_of(&arg0(&args)));
6217 let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6218 return crate::module::require(&spec, std::path::Path::new(&from));
6219 }
6220 if name == "process.memoryUsage.rss" {
6221 return Ok(crate::stdlib::process::memory_usage_rss());
6222 }
6223 if name == "require.resolve.paths" {
6224 let spec = with_host(|h| h.str_of(&arg0(&args)));
6225 // A core module is not looked up on disk at all.
6226 if crate::stdlib::is_core(&spec) {
6227 return Ok(with_host(|h| h.null()));
6228 }
6229 let dirs = crate::module::resolve_paths(&spec, &crate::module::entry_dir());
6230 return Ok(with_host(|h| {
6231 let items: Vec<Value> = dirs.into_iter().map(|d| h.new_str(d)).collect();
6232 h.new_array(items)
6233 }));
6234 }
6235 // A `require.extensions` entry. This runtime's loader does not dispatch
6236 // through the map, so calling one is the loader's own behaviour for that
6237 // extension rather than a hook point.
6238 if let Some(ext) = name.strip_prefix("@@extension:") {
6239 let _ = ext;
6240 return Ok(Value::Undef);
6241 }
6242 // `require.resolve(spec)` at the ENTRY level: resolve from the entry dir.
6243 if name == "require.resolve" {
6244 let spec = with_host(|h| h.str_of(&arg0(&args)));
6245 if crate::stdlib::is_core(&spec) {
6246 return Ok(with_host(|h| h.new_str(spec)));
6247 }
6248 return match crate::module::resolve(&spec, &crate::module::entry_dir()) {
6249 Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6250 None => Err(crate::host::plain_coded_error(
6251 "Error",
6252 "MODULE_NOT_FOUND",
6253 &format!("Cannot find module '{spec}'"),
6254 )),
6255 };
6256 }
6257 // `__cjs_resolve(spec, fromDir)`: `require.resolve` — the resolved absolute
6258 // path (core modules resolve to the bare specifier, as in Node).
6259 if name == "__cjs_resolve" {
6260 let spec = with_host(|h| h.str_of(&arg0(&args)));
6261 let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
6262 if crate::stdlib::is_core(&spec) {
6263 return Ok(with_host(|h| h.new_str(spec)));
6264 }
6265 return match crate::module::resolve(&spec, std::path::Path::new(&from)) {
6266 Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
6267 None => Err(crate::host::plain_coded_error(
6268 "Error",
6269 "MODULE_NOT_FOUND",
6270 &format!("Cannot find module '{spec}'"),
6271 )),
6272 };
6273 }
6274 // `Error.captureStackTrace(target[, ctor])`: V8's stack capture. Sets
6275 // `target.stack`; when a custom `Error.prepareStackTrace` is installed (the
6276 // stack-introspection pattern used by `depd`), it is called with a synthetic
6277 // CallSite array and its result becomes `.stack`, else `.stack` is a string.
6278 if name == "Error.captureStackTrace" {
6279 let target = arg0(&args);
6280 let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
6281 let stack = match prep {
6282 Some(f)
6283 if matches!(
6284 with_host(|h| h.get(&f).cloned()),
6285 Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
6286 ) =>
6287 {
6288 let sites = crate::module::callsite_stack(10)?;
6289 host::invoke(&f, vec![target.clone(), sites], None)?
6290 }
6291 _ => with_host(|h| h.new_str("")),
6292 };
6293 let _ = set_property(&target, "stack", stack);
6294 return Ok(Value::Undef);
6295 }
6296 // Native stdlib module methods (path/os/fs/util/assert/crypto/buffer/url).
6297 if let Some(r) = crate::stdlib::call(name, &args) {
6298 return r;
6299 }
6300 match name {
6301 // Node's DEFAULT `Error.prepareStackTrace`: the `Name: message` header
6302 // followed by one ` at <site>` line per call site. Reachable because
6303 // the read hands the hook out, and a library may call it directly to
6304 // render a stack it captured.
6305 DEFAULT_PREPARE => {
6306 let err = arg0(&args);
6307 let header = with_host(|h| {
6308 let name = host::lookup_chain(h, &err, "name")
6309 .map(|v| h.str_of(&v))
6310 .unwrap_or_else(|| "Error".to_string());
6311 let msg = host::lookup_chain(h, &err, "message")
6312 .map(|v| h.str_of(&v))
6313 .unwrap_or_default();
6314 if msg.is_empty() {
6315 name
6316 } else {
6317 format!("{name}: {msg}")
6318 }
6319 });
6320 let sites = args.get(1).cloned().unwrap_or(Value::Undef);
6321 let lines = with_host(|h| match h.get(&sites) {
6322 Some(JsObj::Array(items)) => items.clone(),
6323 _ => Vec::new(),
6324 });
6325 let mut out = header;
6326 for s in lines {
6327 let rendered = host::to_string_value(&s)
6328 .map(|v| with_host(|h| h.str_of(&v)))
6329 .unwrap_or_default();
6330 out.push_str("\n at ");
6331 out.push_str(&rendered);
6332 }
6333 Ok(with_host(|h| h.new_str(out)))
6334 }
6335 "console.log" | "console.info" | "console.debug" => {
6336 print_line(&args, false)?;
6337 Ok(Value::Undef)
6338 }
6339 "console.error" | "console.warn" => {
6340 print_line(&args, true)?;
6341 Ok(Value::Undef)
6342 }
6343 "parseInt" | "Number.parseInt" => Ok(Value::Float(parse_int(&args)?)),
6344 "parseFloat" | "Number.parseFloat" => Ok(Value::Float(parse_float(&args)?)),
6345 // `isNaN`/`isFinite` are `ToNumber(x)` too (19.2.3/4).
6346 "isNaN" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_nan())),
6347 "isFinite" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_finite())),
6348 "encodeURIComponent" => uri_encode(&arg_to_string(&args, 0)?, false),
6349 "encodeURI" => uri_encode(&arg_to_string(&args, 0)?, true),
6350 "decodeURIComponent" => uri_decode(&arg_to_string(&args, 0)?, false),
6351 "decodeURI" => uri_decode(&arg_to_string(&args, 0)?, true),
6352 "escape" => legacy_escape(&with_host(|h| h.str_of(&arg0(&args)))),
6353 "unescape" => legacy_unescape(&with_host(|h| h.str_of(&arg0(&args)))),
6354 // Reaching `eval` through this table means the eval FUNCTION VALUE was
6355 // called — `(0, eval)(src)`, `const e = eval; e(src)`, `[eval][0](src)`.
6356 // Those are INDIRECT evals and run in the global scope. A literal
6357 // `eval(src)` is intercepted earlier, in `host::call_named`.
6358 "eval" => eval_source(args.first(), false),
6359 // `new Function(...)` and `Function(...)` are the same operation
6360 // (20.2.1.1 `CreateDynamicFunction` is reached from both [[Call]] and
6361 // [[Construct]]), so both route to the one generator.
6362 "Function" => function_ctor(&args),
6363 // `Buffer(arg[, encodingOrOffset[, length]])` — the deprecated call form
6364 // (DEP0005). Node still supports it and still routes it to the same place
6365 // `new Buffer` goes, which is why `safe-buffer`'s legacy `SafeBuffer`
6366 // wrapper is just `return Buffer(arg, encodingOrOffset, length)`. Measured
6367 // on node v26.7.0: `Buffer('abc').toString() === 'abc'`,
6368 // `Buffer([1,2]).toString('hex') === '0102'`, `Buffer(3).length === 3`.
6369 // Node emits DEP0005 once, on stderr, through the same one-shot machinery
6370 // `url.parse`'s DEP0169 uses, so this does too rather than staying silent
6371 // where Node warns.
6372 "Buffer" => {
6373 crate::stdlib::process::emit_deprecation_warning(
6374 "DEP0005",
6375 "Buffer() is deprecated due to security and usability issues. \
6376 Please use the Buffer.alloc(), Buffer.allocUnsafe(), or \
6377 Buffer.from() methods instead.",
6378 );
6379 crate::stdlib::construct("Buffer", &args)
6380 .unwrap_or_else(|| Err(host::type_error("Buffer is not a function")))
6381 }
6382 "Number.isInteger" => Ok(Value::Bool(is_integer(arg0(&args)))),
6383 "Number.isSafeInteger" => Ok(Value::Bool(is_safe_integer(arg0(&args)))),
6384 "Number.isNaN" => Ok(Value::Bool(
6385 matches!(arg0(&args), Value::Float(f) if f.is_nan()),
6386 )),
6387 "Number.isFinite" => Ok(Value::Bool(
6388 matches!(arg0(&args), Value::Float(f) if f.is_finite())
6389 || matches!(arg0(&args), Value::Int(_)),
6390 )),
6391 "String" => {
6392 if args.is_empty() {
6393 Ok(with_host(|h| h.new_str("")))
6394 } else {
6395 // A symbol argument stringifies to `Symbol(desc)` (explicit String()
6396 // is allowed); everything else via ToString method dispatch.
6397 host::string_ctor_value(&args[0])
6398 }
6399 }
6400 // `Number(v)` is NOT plain ToNumber: 21.1.1.1 step 2 converts the object
6401 // first and then explicitly ACCEPTS a BigInt, returning its mathematical
6402 // value as a Number. Only `Number` does — `+v` and `Math.abs(v)` reject
6403 // one — which is why this cannot just call `to_number_value`.
6404 "Number" => Ok(Value::Float(if args.is_empty() {
6405 0.0
6406 } else {
6407 let prim = host::to_primitive(&args[0], "number")?;
6408 match with_host(|h| h.as_bigint(&prim)) {
6409 Some(b) => host::bigint_to_f64(&b),
6410 None => host::to_number_value(&prim)?,
6411 }
6412 })),
6413 "BigInt" => bigint_ctor(&arg0(&args)),
6414 "RegExp" => regexp_ctor(&args),
6415 "BigInt.asIntN" | "BigInt.asUintN" => bigint_as_n(name.ends_with("asUintN"), &args),
6416 "Boolean" => Ok(Value::Bool(with_host(|h| h.truthy(&arg0(&args))))),
6417 // Each argument is truncated to a uint16 and taken as one code UNIT, so
6418 // `String.fromCharCode(0x1D4B3)` is U+D4B3, NOT the astral U+1D4B3, and
6419 // a surrogate PAIR of arguments composes into one character.
6420 "String.fromCharCode" => Ok(with_host(|h| {
6421 let units: Vec<u16> = args
6422 .iter()
6423 .map(|a| crate::utf16::to_uint16(h.to_number(a)))
6424 .collect();
6425 let s = crate::utf16::to_string_lossy(&units);
6426 h.new_str(s)
6427 })),
6428 // `fromCodePoint` takes whole code POINTS and rejects anything that is
6429 // not one — including a lone surrogate, which `fromCharCode` accepts.
6430 "String.fromCodePoint" => {
6431 let mut s = String::new();
6432 for a in &args {
6433 let n = with_host(|h| h.to_number(a));
6434 let cp = if n.is_finite() && n.trunc() == n && (0.0..=0x10FFFF as f64).contains(&n)
6435 {
6436 char::from_u32(n as u32)
6437 } else {
6438 None
6439 };
6440 match cp {
6441 Some(c) => s.push(c),
6442 None => {
6443 return Err(format!(
6444 "RangeError: Invalid code point {}",
6445 with_host(|h| h.str_of(a))
6446 ))
6447 }
6448 }
6449 }
6450 Ok(new_s(s))
6451 }
6452 "String.raw" => string_raw(&args),
6453 // `Array(5)` === `new Array(5)` (length-5 empty), but `Array.of(5)` is `[5]`.
6454 "Array" => construct_builtin("Array", args),
6455 "Array.of" => construct_array_like(host::current_static_this(), args),
6456 // 23.1.2.2 `IsArray` follows a Proxy to its `[[ProxyTarget]]` rather than
6457 // consulting any trap, so `Array.isArray(new Proxy([], {}))` is `true`.
6458 "Array.isArray" => {
6459 let v = arg0(&args);
6460 let subject = crate::proxy::ultimate_target(&v).unwrap_or(v);
6461 Ok(Value::Bool(
6462 matches!(
6463 with_host(|h| h.get(&subject).cloned()),
6464 Some(JsObj::Array(_))
6465 ) && !is_arguments(&subject),
6466 ))
6467 }
6468 "Array.from" => array_from(args),
6469 "Array.fromAsync" => array_from_async(args),
6470 "Object" => Ok(object_call(args)),
6471 "Object.keys" => object_keys(args, 0),
6472 "Object.values" => object_keys(args, 1),
6473 "Object.entries" => object_keys(args, 2),
6474 "Object.assign" => object_assign(args),
6475 "Object.freeze" => {
6476 let v = arg0(&args);
6477 reject_sealing_a_view(&v, "freeze")?;
6478 if seal_proxy(&v, true)? {
6479 return Ok(v);
6480 }
6481 with_host(|h| h.seal_object(&v, true));
6482 Ok(v)
6483 }
6484 "Object.seal" => {
6485 let v = arg0(&args);
6486 reject_sealing_a_view(&v, "seal")?;
6487 if seal_proxy(&v, false)? {
6488 return Ok(v);
6489 }
6490 with_host(|h| h.seal_object(&v, false));
6491 Ok(v)
6492 }
6493 "Object.preventExtensions" => {
6494 let v = arg0(&args);
6495 if crate::proxy::prevent_extensions(&v)? {
6496 return Ok(v);
6497 }
6498 with_host(|h| h.prevent_extensions(&v));
6499 Ok(v)
6500 }
6501 // A PRIMITIVE has no integrity to speak of and 7.3.15/16 answer for it
6502 // without coercion: it is not extensible, and vacuously frozen and
6503 // sealed. Reporting it extensible and unfrozen was the opposite of
6504 // every one of the three.
6505 "Object.isFrozen" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6506 "Object.isSealed" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
6507 "Object.isExtensible" if is_primitive_arg(&args) => Ok(Value::Bool(false)),
6508 "Object.isFrozen" => integrity_level(&arg0(&args), true),
6509 "Object.isSealed" => integrity_level(&arg0(&args), false),
6510 "Object.isExtensible" => {
6511 let v = arg0(&args);
6512 match crate::proxy::is_extensible(&v)? {
6513 Some(b) => Ok(Value::Bool(b)),
6514 None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6515 }
6516 }
6517 // Object.is — SameValue: like `===` but NaN is equal to NaN and +0 is
6518 // distinct from -0.
6519 "Object.is" => {
6520 let a = arg0(&args);
6521 let b = args.get(1).cloned().unwrap_or(Value::Undef);
6522 let num = |v: &Value| match v {
6523 Value::Int(n) => Some(*n as f64),
6524 Value::Float(f) => Some(*f),
6525 _ => None,
6526 };
6527 let r = match (num(&a), num(&b)) {
6528 (Some(x), Some(y)) => {
6529 if x.is_nan() && y.is_nan() {
6530 true
6531 } else if x == 0.0 && y == 0.0 {
6532 x.is_sign_negative() == y.is_sign_negative()
6533 } else {
6534 x == y
6535 }
6536 }
6537 _ => with_host(|h| h.strict_eq(&a, &b)),
6538 };
6539 Ok(Value::Bool(r))
6540 }
6541 "Object.fromEntries" => object_from_entries(args),
6542 // `[[GetPrototypeOf]]`: a Proxy answers from its trap (which may throw),
6543 // so the proxy form cannot share `prototype_of`'s infallible signature.
6544 // `Object.getPrototypeOf` coerces a primitive to its wrapper and
6545 // answers; `Reflect.getPrototypeOf` requires an object (28.1.8).
6546 "Object.getPrototypeOf" | "Reflect.getPrototypeOf" => {
6547 if name == "Reflect.getPrototypeOf" {
6548 reflect_require_object(&arg0(&args), "getPrototypeOf")?;
6549 }
6550 let v = arg0(&args);
6551 match crate::proxy::get_prototype_of(&v)? {
6552 Some(p) => Ok(p),
6553 None => Ok(prototype_of(&v)),
6554 }
6555 }
6556 "Object.setPrototypeOf" => {
6557 let obj = arg0(&args);
6558 let proto = args.get(1).cloned().unwrap_or(Value::Undef);
6559 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6560 reject_bad_prototype(&proto)?;
6561 crate::proxy::set_prototype_of(&obj, &proto)?;
6562 return Ok(obj);
6563 }
6564 // 20.1.2.23: `RequireObjectCoercible` on the target, then the
6565 // prototype type check, then — only for an actual object target —
6566 // the extensibility check. A PRIMITIVE target is returned untouched
6567 // (`Object.setPrototypeOf(1, {})` is `1`), which is why the
6568 // extensibility test cannot come first.
6569 if with_host(|h| matches!(obj, Value::Undef) || h.is_null(&obj)) {
6570 return Err(host::type_error(
6571 "Object.setPrototypeOf called on null or undefined",
6572 ));
6573 }
6574 reject_bad_prototype(&proto)?;
6575 if with_host(|h| is_object_like(h, &obj)) {
6576 // Setting the SAME prototype is a no-op and stays legal even on a
6577 // frozen object: node v26.7.0 accepts
6578 // `Object.setPrototypeOf(Object.freeze({}), Object.prototype)`.
6579 // `prototype_of`, not `proto_of`: an object with no EXPLICIT
6580 // link still has `Object.prototype`, and comparing against the
6581 // absent link would call that a change.
6582 if would_cycle(&obj, &proto) {
6583 return Err(host::type_error("Cyclic __proto__ value"));
6584 }
6585 if !same_prototype(&obj, &proto) && !with_host(|h| h.is_extensible(&obj)) {
6586 // The receiver is named by its brand, as every other
6587 // refusal names it — a NULL-PROTOTYPE object is
6588 // `[object Object]`, not `#<Object>`, because it has no
6589 // constructor to name.
6590 return Err(host::type_error(&format!(
6591 "{} is not extensible",
6592 no_side_effects_string(&obj)
6593 )));
6594 }
6595 with_host(|h| h.set_proto(&obj, proto));
6596 }
6597 Ok(obj)
6598 }
6599 "Object.create" => object_create(args),
6600 "Object.getOwnPropertyNames" => object_keys(args, 3),
6601 "Object.getOwnPropertySymbols" => {
6602 let v = arg0(&args);
6603 require_object_coercible(&v)?;
6604 let syms = proxy_or_own_symbol_keys(&v)?;
6605 Ok(with_host(|h| h.new_array(syms)))
6606 }
6607 // `Object.hasOwn(obj, key)` — the static form of `hasOwnProperty`.
6608 "Object.hasOwn" => {
6609 let obj = arg0(&args);
6610 let key = args.get(1).cloned().unwrap_or(Value::Undef);
6611 object_builtin_method(&obj, "hasOwnProperty", vec![key])
6612 }
6613 "Object.defineProperty" => object_define_property(args),
6614 "Object.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6615 "Object.getOwnPropertyDescriptors" => object_get_own_descriptors(args),
6616 "Object.defineProperties" => object_define_properties(args),
6617 // `Object.groupBy(items, cb)` (ES2024): group into a null-prototype object
6618 // keyed by `ToPropertyKey(cb(item, i))`, each value an array of members.
6619 "Object.groupBy" => object_group_by(args),
6620 "Symbol" => Ok(with_host(|h| {
6621 let desc = args
6622 .first()
6623 .filter(|a| !matches!(a, Value::Undef))
6624 .map(|a| h.str_of(a));
6625 h.new_symbol(desc)
6626 })),
6627 "Symbol.for" => Ok(with_host(|h| {
6628 let key = h.str_of(&arg0(&args));
6629 h.symbol_for(&key)
6630 })),
6631 // `Symbol.keyFor(sym)` (20.4.2.6) is a REGISTRY lookup, not a
6632 // description read: it answers only for symbols `Symbol.for` created.
6633 // Returning the description made every symbol look registered —
6634 // `Symbol.keyFor(Symbol("k"))` was `"k"` where node says `undefined`.
6635 "Symbol.keyFor" => Ok(with_host(|h| h.symbol_registry_key(&arg0(&args)))),
6636 "Map" | "WeakMap" | "Set" | "WeakSet" | "Promise" => construct_builtin(name, args),
6637 // `Proxy` has no `[[Call]]` slot: it is constructor-only (28.2.1).
6638 "Proxy" => Err(host::type_error("Constructor Proxy requires 'new'")),
6639 "Proxy.revocable" => crate::proxy::revocable(&args),
6640 // `Reflect.ownKeys` reports EVERY own key, non-enumerable included —
6641 // the same set as `getOwnPropertyNames` (node-js has no symbol-keyed
6642 // own properties, so there is no second half to append).
6643 // `Reflect.ownKeys` is `OwnPropertyKeys` (7.3.23): every own key,
6644 // non-enumerable included, strings first and then the SYMBOLS.
6645 "Reflect.ownKeys" => {
6646 let v = arg0(&args);
6647 reflect_require_object(&v, "ownKeys")?;
6648 let names = object_keys(args, 3)?;
6649 let syms = proxy_or_own_symbol_keys(&v)?;
6650 if syms.is_empty() {
6651 return Ok(names);
6652 }
6653 let mut all = with_host(|h| h.iter_vec(&names)).unwrap_or_default();
6654 all.extend(syms);
6655 Ok(with_host(|h| h.new_array(all)))
6656 }
6657 "Reflect.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
6658 // `Reflect.defineProperty` REPORTS success as a boolean where
6659 // `Object.defineProperty` throws (28.1.3). It was propagating the
6660 // throw, so the whole point of the reflective form was lost.
6661 "Reflect.defineProperty" => {
6662 reflect_require_object(&arg0(&args), "defineProperty")?;
6663 Ok(Value::Bool(object_define_property(args).is_ok()))
6664 }
6665 "Reflect.deleteProperty" => {
6666 let obj = arg0(&args);
6667 reflect_require_object(&obj, "deleteProperty")?;
6668 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6669 Ok(Value::Bool(delete_property(&obj, &k)?))
6670 }
6671 "Reflect.setPrototypeOf" => {
6672 let obj = arg0(&args);
6673 let p = args.get(1).cloned().unwrap_or(Value::Undef);
6674 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
6675 crate::proxy::set_prototype_of(&obj, &p)?;
6676 return Ok(Value::Bool(true));
6677 }
6678 // 10.1.2.1: a NON-EXTENSIBLE object refuses a prototype change —
6679 // unless the new one is what it already has, which is a no-op. It
6680 // reported success and rewrote the link.
6681 // `Reflect` reports a refusal rather than throwing, for a cycle as
6682 // for a non-extensible receiver.
6683 if would_cycle(&obj, &p) {
6684 return Ok(Value::Bool(false));
6685 }
6686 if !with_host(|h| h.is_extensible(&obj)) {
6687 return Ok(Value::Bool(same_prototype(&obj, &p)));
6688 }
6689 with_host(|h| h.set_proto(&obj, p));
6690 Ok(Value::Bool(true))
6691 }
6692 "Reflect.isExtensible" => {
6693 let v = arg0(&args);
6694 match crate::proxy::is_extensible(&v)? {
6695 Some(b) => Ok(Value::Bool(b)),
6696 None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
6697 }
6698 }
6699 "Reflect.preventExtensions" => {
6700 let v = arg0(&args);
6701 if crate::proxy::prevent_extensions(&v)? {
6702 return Ok(Value::Bool(true));
6703 }
6704 with_host(|h| h.prevent_extensions(&v));
6705 Ok(Value::Bool(true))
6706 }
6707 // `Reflect.apply(target, thisArg, argsList)` / `Reflect.construct(t, a)`.
6708 "Reflect.apply" => {
6709 let f = arg0(&args);
6710 let this = args.get(1).cloned();
6711 let list = create_list_from_array_like(&args.get(2).cloned().unwrap_or(Value::Undef))?;
6712 host::invoke(&f, list, this.filter(|t| !with_host(|h| h.is_nullish(t))))
6713 }
6714 // `Reflect.construct(target, args, newTarget)` — the optional third
6715 // argument decides which constructor's `prototype` the instance gets
6716 // (28.1.2). It was ignored, so the result always inherited from
6717 // `target` and `instanceof newTarget` was false.
6718 "Reflect.construct" => {
6719 let f = arg0(&args);
6720 let list = create_list_from_array_like(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6721 let new_target = args.get(2).cloned().unwrap_or_else(|| f.clone());
6722 host::construct_nt(&f, list, new_target)
6723 }
6724 "Reflect.has" => {
6725 let obj = arg0(&args);
6726 reflect_require_object(&obj, "has")?;
6727 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6728 Ok(Value::Bool(has_property(&obj, &k)?))
6729 }
6730 // `Reflect.get(target, key, receiver)` — the optional third argument is
6731 // what a getter sees as `this` (28.1.6). Defaults to the target.
6732 "Reflect.get" => {
6733 let obj = arg0(&args);
6734 reflect_require_object(&obj, "get")?;
6735 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6736 let receiver = args.get(2).cloned().unwrap_or_else(|| obj.clone());
6737 get_property_recv(&obj, &k, &receiver)
6738 }
6739 // `Reflect.set(target, key, value, receiver)` — the optional fourth
6740 // argument is what a setter sees as `this`, and where a DATA property
6741 // lands (28.1.13). It was ignored: the setter ran against the target
6742 // and the property was written there.
6743 "Reflect.set" => {
6744 let obj = arg0(&args);
6745 reflect_require_object(&obj, "set")?;
6746 let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
6747 let v = args.get(2).cloned().unwrap_or(Value::Undef);
6748 let receiver = args.get(3).cloned().unwrap_or_else(|| obj.clone());
6749 Ok(Value::Bool(set_with_receiver(&obj, &k, v, &receiver)?))
6750 }
6751 "JSON.stringify" => json_stringify(args),
6752 "JSON.parse" => json_parse(args),
6753 "JSON.rawJSON" => json_raw(args),
6754 "JSON.isRawJSON" => json_is_raw(args),
6755 "structuredClone" => structured_clone(args),
6756 // The deferred drain a `Readable.from` schedules; the suffix is the
6757 // stream's heap index.
6758 _ if name.starts_with("@@transformCb:") => {
6759 let idx: u32 = name["@@transformCb:".len()..].parse().unwrap_or(0);
6760 crate::stdlib::stream::transform_callback(&Value::Obj(idx), &args)?;
6761 Ok(Value::Undef)
6762 }
6763 _ if name.starts_with("@@streamFlush:") => {
6764 let idx: u32 = name["@@streamFlush:".len()..].parse().unwrap_or(0);
6765 crate::stdlib::stream::flush_from(&Value::Obj(idx))?;
6766 Ok(Value::Undef)
6767 }
6768 // Same implementation the `buffer` module exposes; only the binding was
6769 // missing.
6770 "btoa" | "atob" => crate::stdlib::buffer::module_call(name, &args)
6771 .unwrap_or_else(|| Err(host::type_error(&format!("{name} is not a function")))),
6772 "fetch" => crate::stdlib::fetch::fetch(&args),
6773 // An `AbortSignal.timeout` deadline reached its macrotask: the thunk's
6774 // suffix is the signal's heap index.
6775 _ if name.starts_with("@@aborttimeout:") => {
6776 let idx: u32 = name["@@aborttimeout:".len()..].parse().unwrap_or(0);
6777 crate::stdlib::fetch::fire_timeout_abort(idx)
6778 }
6779 // The `callback` handed to a `new Writable({ write(chunk, enc, cb) })`
6780 // implementation. Nothing here waits on backpressure, so it only has to
6781 // BE callable — an implementation that ends with `cb()`, which the
6782 // stream contract requires, would otherwise throw.
6783 "@@streamWriteCallback" => Ok(Value::Undef),
6784 "queueMicrotask" | "process.nextTick" => {
6785 let cb = arg0(&args);
6786 require_callback(&cb)?;
6787 let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
6788 enqueue_microtask(name == "process.nextTick", cb, rest);
6789 Ok(Value::Undef)
6790 }
6791 "setTimeout" | "setInterval" | "setImmediate" => {
6792 require_callback(&arg0(&args))?;
6793 Ok(schedule_timer(name, args))
6794 }
6795 "clearTimeout" | "clearInterval" | "clearImmediate" => {
6796 clear_timer(&arg0(&args));
6797 Ok(Value::Undef)
6798 }
6799 "Promise.resolve" => promise_resolve(arg0(&args)),
6800 "Promise.reject" => promise_reject(arg0(&args)),
6801 "Promise.all" => promise_all(args, AllMode::All),
6802 "Promise.allSettled" => promise_all(args, AllMode::AllSettled),
6803 "Promise.race" => promise_race(args, false),
6804 "Promise.any" => promise_race(args, true),
6805 // `Promise.withResolvers()` (ES2024): a new pending promise plus its own
6806 // resolve/reject functions, returned as `{ promise, resolve, reject }`.
6807 "Promise.withResolvers" => promise_with_resolvers(),
6808 "Promise.try" => promise_try(args),
6809 "RegExp.escape" => regexp_escape(args),
6810 "Error.isError" => error_is_error(args),
6811 // `Map.groupBy(items, cb)` (ES2024): group into a `Map` keyed by the raw
6812 // `cb(item, i)` result (SameValueZero), each value an array of members.
6813 "Map.groupBy" => map_group_by(args),
6814 n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
6815 _ if name.starts_with("Math.") => math_fn(&name[5..], &args),
6816 // Internal continuations (Promise resolve/reject fns, `.finally` wrappers).
6817 // The executor a species-constructed promise is built with: it does
6818 // nothing, because the caller settles the result through its id.
6819 "@@pnoop" => Ok(Value::Undef),
6820 _ if name.starts_with("@@presolve:") => {
6821 let id: u32 = name[11..].parse().unwrap_or(0);
6822 host::resolve_promise_val(id, arg0(&args));
6823 Ok(Value::Undef)
6824 }
6825 _ if name.starts_with("@@preject:") => {
6826 let id: u32 = name[10..].parse().unwrap_or(0);
6827 host::reject_promise_val(id, arg0(&args));
6828 Ok(Value::Undef)
6829 }
6830 // The revoker `Proxy.revocable` hands back, keyed by the proxy's heap
6831 // index so calling it twice is the spec's no-op rather than a re-tear.
6832 _ if name.starts_with("@@prevoke:") => {
6833 let i: u32 = name[10..].parse().unwrap_or(0);
6834 Ok(crate::proxy::revoke(i))
6835 }
6836 _ if name.starts_with("@@finpass:") => {
6837 // finally(cb) on fulfill: run cb, await whatever it returned, then
6838 // pass the original value through.
6839 let i: u32 = name["@@finpass:".len()..].parse().unwrap_or(0);
6840 let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6841 Ok(finally_chain(result, arg0(&args), false))
6842 }
6843 _ if name.starts_with("@@finthrow:") => {
6844 // finally(cb) on reject: same, then re-throw the original reason.
6845 let i: u32 = name["@@finthrow:".len()..].parse().unwrap_or(0);
6846 let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
6847 Ok(finally_chain(result, arg0(&args), true))
6848 }
6849 // The two thunks `finally_chain` hangs off that awaited promise. Each
6850 // carries the value it must reinstate in a one-slot cell, since a
6851 // builtin is identified only by its name and cannot close over one.
6852 _ if name.starts_with("@@finret:") => {
6853 let i: u32 = name["@@finret:".len()..].parse().unwrap_or(0);
6854 get_property(&Value::Obj(i), "0")
6855 }
6856 _ if name.starts_with("@@finrethrow:") => {
6857 let i: u32 = name["@@finrethrow:".len()..].parse().unwrap_or(0);
6858 let reason = get_property(&Value::Obj(i), "0")?;
6859 with_host(|h| h.exc = Some(reason.clone()));
6860 Err(with_host(|h| error_string(h, &reason)))
6861 }
6862 _ => Err(host::type_error(&format!("{name} is not a function"))),
6863 }
6864}
6865
6866/// `BigInt(x)`: convert a boolean/number/string/bigint to a BigInt. A
6867/// non-integer number is a `RangeError`; an unparseable string a `SyntaxError`
6868/// (matching Node's messages).
6869/// V8 names the offending value: `BigInt(undefined)` is `Cannot convert
6870/// undefined to a BigInt`, `BigInt({})` is `Cannot convert [object Object] to a
6871/// BigInt`. The old text said "value" literally, for every input.
6872fn bigint_convert_error(v: &Value) -> String {
6873 let shown = with_host(|h| h.str_of(v));
6874 host::type_error(&format!("Cannot convert {shown} to a BigInt"))
6875}
6876
6877/// `ToBigInt(v)` — 7.1.13. The conversion every BigInt-typed SINK performs: a
6878/// 64-bit typed array's element write, `DataView.prototype.setBigInt64`, and
6879/// BigInt arithmetic's operand check.
6880///
6881/// It is NOT `BigInt(v)`: a Number is a `TypeError` here (`BigInt(1)` is `1n`,
6882/// but `new BigInt64Array(1)[0] = 1` throws), which is the whole point of the
6883/// separate abstract op. Everything else follows `ToPrimitive(v, number)` then
6884/// the type table — booleans convert (`true` → `1n`), strings parse with a
6885/// `SyntaxError` on failure, and `undefined`/`null`/symbols throw.
6886///
6887/// Measured on node v26.8.1, receiver `new BigInt64Array(1)`:
6888///
6889/// ```text
6890/// a[0] = true → 1n
6891/// a[0] = '12' → 12n
6892/// a[0] = [] → 0n (ToPrimitive → "" → 0n)
6893/// a[0] = ['3'] → 3n
6894/// a[0] = 1 → TypeError: Cannot convert 1 to a BigInt
6895/// a[0] = new Number(3) → TypeError: Cannot convert 3 to a BigInt
6896/// a[0] = 'a' → SyntaxError: Cannot convert a to a BigInt
6897/// a[0] = {} → SyntaxError: Cannot convert [object Object] to a BigInt
6898/// ```
6899pub fn to_bigint(v: &Value) -> Result<num_bigint::BigInt, String> {
6900 let prim = host::to_primitive(v, "number")?;
6901 if let Some(b) = with_host(|h| match h.get(&prim) {
6902 Some(JsObj::BigInt(b)) => Some(b.clone()),
6903 _ => None,
6904 }) {
6905 return Ok(b);
6906 }
6907 match &prim {
6908 Value::Bool(b) => Ok(num_bigint::BigInt::from(*b as i64)),
6909 Value::Str(s) => host::parse_bigint_str(s)
6910 .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt")),
6911 _ if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) => {
6912 let s = with_host(|h| h.str_of(&prim));
6913 host::parse_bigint_str(&s)
6914 .ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt"))
6915 }
6916 _ => Err(bigint_convert_error(&prim)),
6917 }
6918}
6919
6920fn bigint_ctor(v: &Value) -> Result<Value, String> {
6921 use num_bigint::BigInt;
6922 let big = match v {
6923 Value::Bool(b) => BigInt::from(*b as i64),
6924 Value::Int(n) => BigInt::from(*n),
6925 Value::Float(f) => {
6926 if !f.is_finite() || f.fract() != 0.0 {
6927 let disp = with_host(|h| h.str_of(v));
6928 return Err(format!(
6929 "RangeError: The number {disp} cannot be converted to a BigInt because it is not an integer"
6930 ));
6931 }
6932 // The decimal EXPANSION, not `fmt_number`: `Number.prototype
6933 // .toString` switches to exponential notation at 1e21, and
6934 // `BigInt::parse_bytes` cannot read `"1e+21"` — so `BigInt(1e21)`
6935 // threw `Cannot convert value to a BigInt` where node returns
6936 // `1000000000000000000000n`. `{:.0}` prints an integral f64's exact
6937 // value, which is also what node reports for a magnitude past the
6938 // exactly-representable range (`BigInt(1e30)` is
6939 // `1000000000000000019884624838656n` in both).
6940 match BigInt::parse_bytes(format!("{f:.0}").as_bytes(), 10) {
6941 Some(b) => b,
6942 None => return Err(bigint_convert_error(v)),
6943 }
6944 }
6945 Value::Str(s) => match host::parse_bigint_str(s) {
6946 Some(b) => b,
6947 None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
6948 },
6949 Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
6950 Some(JsObj::BigInt(b)) => b,
6951 Some(JsObj::Str(s)) => match host::parse_bigint_str(&s) {
6952 Some(b) => b,
6953 None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
6954 },
6955 _ => return Err(bigint_convert_error(v)),
6956 },
6957 _ => return Err(bigint_convert_error(v)),
6958 };
6959 Ok(with_host(|h| h.new_bigint(big)))
6960}
6961
6962/// `new RegExp(source[, flags])` / `RegExp(...)`. A first `RegExp` argument copies
6963/// its source (and flags, unless new ones are given).
6964fn regexp_ctor(args: &[Value]) -> Result<Value, String> {
6965 let (source, existing_flags) = match with_host(|h| h.get(&arg0(args)).cloned()) {
6966 Some(JsObj::RegExp(r)) => (r.source.clone(), Some(r.flags.clone())),
6967 _ => {
6968 let a0 = arg0(args);
6969 // 22.2.4.1 step 9 is `ToString(pattern)`, which a SYMBOL refuses —
6970 // `new RegExp(sym)` was compiling the text `Symbol(d)` into a
6971 // pattern instead of throwing.
6972 let src = if matches!(a0, Value::Undef) {
6973 String::new()
6974 } else {
6975 arg_to_string(args, 0)?
6976 };
6977 (src, None)
6978 }
6979 };
6980 let flags = match args.get(1) {
6981 Some(v) if !matches!(v, Value::Undef) => arg_to_string(args, 1)?,
6982 _ => existing_flags.unwrap_or_default(),
6983 };
6984 // An empty source compiles as the JS canonical `(?:)`.
6985 let src = if source.is_empty() {
6986 "(?:)".to_string()
6987 } else {
6988 source
6989 };
6990 crate::regexp::build_regexp(&src, &flags)
6991}
6992
6993/// `BigInt.asIntN(bits, x)` / `BigInt.asUintN(bits, x)`: wrap `x` to a `bits`-wide
6994/// two's-complement (signed) or unsigned integer.
6995fn bigint_as_n(unsigned: bool, args: &[Value]) -> Result<Value, String> {
6996 use num_bigint::BigInt;
6997 use num_traits::Signed;
6998 let bits = with_host(|h| h.to_number(&arg0(args))) as i64;
6999 if bits < 0 {
7000 return Err("RangeError: Invalid value: not (convertible to) a safe integer".into());
7001 }
7002 let x = match with_host(|h| h.as_bigint(&args.get(1).cloned().unwrap_or(Value::Undef))) {
7003 Some(b) => b,
7004 None => return Err(host::type_error("Cannot convert to a BigInt")),
7005 };
7006 let bits = bits as u32;
7007 if bits == 0 {
7008 return Ok(with_host(|h| h.new_bigint(BigInt::from(0))));
7009 }
7010 let modulus = BigInt::from(1) << bits; // 2^bits
7011 // Reduce into [0, 2^bits); for the signed form fold the top half negative.
7012 let mut r = &x % &modulus;
7013 if r.is_negative() {
7014 r += &modulus;
7015 }
7016 if !unsigned {
7017 let half = BigInt::from(1) << (bits - 1);
7018 if r >= half {
7019 r -= &modulus;
7020 }
7021 }
7022 Ok(with_host(|h| h.new_bigint(r)))
7023}
7024
7025/// `String.raw(callSite, ...subs)`: concatenate the raw quasis (`callSite.raw`)
7026/// interleaved with the substitutions.
7027fn string_raw(args: &[Value]) -> Result<Value, String> {
7028 let call_site = arg0(args);
7029 let raw = get_property(&call_site, "raw")?;
7030 let raws = with_host(|h| h.iter_vec(&raw)).unwrap_or_default();
7031 let mut out = String::new();
7032 for (i, r) in raws.iter().enumerate() {
7033 out.push_str(&with_host(|h| h.str_of(r)));
7034 if i + 1 < raws.len() {
7035 if let Some(sub) = args.get(i + 1) {
7036 out.push_str(&with_host(|h| h.str_of(sub)));
7037 }
7038 }
7039 }
7040 Ok(with_host(|h| h.new_str(out)))
7041}
7042
7043/// `Object(x)`: box/pass-through — for our model, non-object args just return a
7044/// fresh object; objects pass through.
7045/// Whether `v`'s own properties live in the fn-prop SIDE TABLE rather than in a
7046/// property map. A `Map`/`Set`/`Promise`/`RegExp`/generator/symbol/bigint is an
7047/// ordinary object that also has internal slots, so it can carry own properties
7048/// like anything else — but its heap variant holds only those slots, so a write
7049/// had nowhere to go and vanished: `m.x = 5` left `m.x` undefined.
7050pub fn uses_side_table(v: &Value) -> bool {
7051 matches!(
7052 with_host(|h| h.kind_of(v)),
7053 Some(
7054 ObjKind::Map
7055 | ObjKind::Set
7056 | ObjKind::Promise
7057 | ObjKind::RegExp
7058 | ObjKind::Generator
7059 | ObjKind::Symbol
7060 | ObjKind::BigInt
7061 | ObjKind::Iter
7062 )
7063 )
7064}
7065
7066fn object_call(args: Vec<Value>) -> Value {
7067 let a = arg0(&args);
7068 // `Object(v)` is `ToObject(v)` (20.1.1.1): a primitive comes back BOXED,
7069 // not replaced by an empty object. `Object(1).valueOf()` was `undefined`.
7070 if matches!(a, Value::Undef) || with_host(|h| h.is_null(&a)) {
7071 return with_host(|h| h.new_object(IndexMap::new()));
7072 }
7073 to_object(&a)
7074}
7075
7076/// The name of the wrapper a primitive boxes into, or `None` when the value is
7077/// already an object.
7078fn wrapper_ctor_of(v: &Value) -> Option<&'static str> {
7079 match v {
7080 Value::Int(_) | Value::Float(_) => Some("Number"),
7081 Value::Bool(_) => Some("Boolean"),
7082 Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
7083 Some(JsObj::Str(_)) => Some("String"),
7084 Some(JsObj::Symbol { .. }) => Some("Symbol"),
7085 Some(JsObj::BigInt(_)) => Some("BigInt"),
7086 _ => None,
7087 },
7088 _ => None,
7089 }
7090}
7091
7092/// The primitive a wrapper object boxes (`new String("a")` → `"a"`), or `None`
7093/// for every other value. The slot is a hidden `@@primitive` own property —
7094/// the same `@@` marker convention the engine already uses for internal state,
7095/// so it stays out of `Object.keys` and `JSON.stringify` on its own.
7096pub fn wrapped_primitive(v: &Value) -> Option<Value> {
7097 with_host(|h| match h.get(v) {
7098 Some(JsObj::Object(p)) => p.get("@@primitive").cloned(),
7099 _ => None,
7100 })
7101}
7102
7103/// `ToObject(v)` (7.1.18) for a primitive: the wrapper object with the matching
7104/// prototype and a `[[StringData]]`/`[[NumberData]]`/`[[BooleanData]]` slot.
7105///
7106/// A String wrapper also owns its index properties and `length`, which is what
7107/// makes `w[0]`, `w.length` and `Object.keys(w)` answer; all of them are
7108/// non-writable and non-configurable, as the exotic `String` object's are.
7109pub fn to_object(v: &Value) -> Value {
7110 let Some(ctor) = wrapper_ctor_of(v) else {
7111 return v.clone();
7112 };
7113 with_host(|h| h.ensure_wrapper_protos());
7114 let chars: Vec<String> = if ctor == "String" {
7115 with_host(|h| h.str_of(v))
7116 .chars()
7117 .map(|c| c.to_string())
7118 .collect()
7119 } else {
7120 Vec::new()
7121 };
7122 with_host(|h| {
7123 let mut m: IndexMap<String, Value> = IndexMap::new();
7124 for (i, c) in chars.iter().enumerate() {
7125 let s = h.new_str(c.clone());
7126 m.insert(i.to_string(), s);
7127 }
7128 let w = h.new_object(m);
7129 if ctor == "String" {
7130 for i in 0..chars.len() {
7131 h.set_prop_attrs(
7132 &w,
7133 &i.to_string(),
7134 host::PropAttrs {
7135 writable: false,
7136 enumerable: true,
7137 configurable: false,
7138 },
7139 );
7140 }
7141 let len = Value::Float(chars.len() as f64);
7142 if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7143 p.insert("length".into(), len);
7144 }
7145 h.set_prop_attrs(
7146 &w,
7147 "length",
7148 host::PropAttrs {
7149 writable: false,
7150 enumerable: false,
7151 configurable: false,
7152 },
7153 );
7154 }
7155 if let Some(JsObj::Object(p)) = h.get_mut(&w) {
7156 p.insert("@@primitive".into(), v.clone());
7157 }
7158 if let Some(proto) = h.native_proto(ctor) {
7159 h.set_proto(&w, proto);
7160 }
7161 w
7162 })
7163}
7164
7165/// Construct via `new` for the builtin constructors.
7166pub fn construct_builtin(name: &str, args: Vec<Value>) -> Result<Value, String> {
7167 // Native stdlib constructors (`new URL(...)`, `new EventEmitter()`, `new Buffer(...)`).
7168 if let Some(r) = crate::stdlib::construct(name, &args) {
7169 return r;
7170 }
7171 match name {
7172 "Array" => {
7173 // `new Array(n)` -> length-n array; `new Array(a, b)` -> [a, b].
7174 // A single NUMBER argument is a length and is validated as one
7175 // (23.1.1.1 step 6), so `new Array(-1)` / `new Array(1.5)` /
7176 // `new Array(2**32)` are all `RangeError: Invalid array length` on
7177 // node v26.7.0; only a non-number single argument is an element.
7178 if args.len() == 1 {
7179 if let Value::Float(_) | Value::Int(_) = args[0] {
7180 let n = host::to_array_length(&args[0])?;
7181 // Every element of `new Array(n)` is a HOLE, not a stored
7182 // `undefined`: `Object.keys(Array(3))` is `[]`.
7183 return Ok(with_host(|h| {
7184 let a = h.new_array(vec![Value::Undef; n]);
7185 h.mark_hole_range(&a, 0..n);
7186 a
7187 }));
7188 }
7189 }
7190 Ok(with_host(|h| h.new_array(args)))
7191 }
7192 "Object" => Ok(object_call(args)),
7193 // `new String(v)` / `new Number(v)` / `new Boolean(v)` — the wrapper
7194 // form. These were not constructors at all, so every one threw.
7195 "String" => Ok(to_object(&host::to_string_value(
7196 &args
7197 .first()
7198 .cloned()
7199 .unwrap_or_else(|| with_host(|h| h.new_str(String::new()))),
7200 )?)),
7201 "Number" => Ok(to_object(&Value::Float(match args.first() {
7202 Some(a) => host::to_number_value(a)?,
7203 None => 0.0,
7204 }))),
7205 "Boolean" => Ok(to_object(&Value::Bool(with_host(|h| {
7206 h.truthy(&arg0(&args))
7207 })))),
7208 "Map" | "WeakMap" => {
7209 let weak = name == "WeakMap";
7210 let m = with_host(|h| {
7211 h.alloc(JsObj::Map {
7212 entries: indexmap::IndexMap::new(),
7213 weak,
7214 })
7215 });
7216 if let Some(init) = args
7217 .first()
7218 .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7219 {
7220 // Stepped, not drained: an entry that is not a pair has to
7221 // stop the construction at that element and CLOSE the iterator
7222 // (24.1.1.2 step 8). Materializing first meant a bad entry in an
7223 // infinite source was never reached and the constructor HUNG.
7224 host::iter_for_each(init, |p, _| {
7225 // 24.1.1.2 step 8.d: each entry must be an OBJECT. A string
7226 // is iterable, so without this check `new Map(["ab"])`
7227 // happily stored `'a' => 'b'` instead of throwing — and over
7228 // an infinite source it never stopped.
7229 if !with_host(|h| is_object_like(h, &p)) {
7230 let shown = with_host(|h| h.str_of(&p));
7231 return Err(host::type_error(&format!(
7232 "Iterator value {shown} is not an entry object"
7233 )));
7234 }
7235 // The entry is read by INDEX with `[[Get]]` (step 8.e), not
7236 // iterated: an object with a `Symbol.iterator` but no `0`/`1`
7237 // gives `undefined => undefined`, and an array-LIKE entry
7238 // works. Iterating it instead accepted a string as a pair
7239 // and rejected the array-like.
7240 let k = get_property(&p, "0")?;
7241 let v = get_property(&p, "1")?;
7242 map_method(&m, "set", vec![k, v])?;
7243 Ok(())
7244 })?;
7245 }
7246 Ok(m)
7247 }
7248 "Set" | "WeakSet" => {
7249 let weak = name == "WeakSet";
7250 let s = with_host(|h| {
7251 h.alloc(JsObj::Set {
7252 entries: indexmap::IndexMap::new(),
7253 weak,
7254 })
7255 });
7256 if let Some(init) = args
7257 .first()
7258 .filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
7259 {
7260 host::iter_for_each(init, |v, _| {
7261 set_method(&s, "add", vec![v])?;
7262 Ok(())
7263 })?;
7264 }
7265 Ok(s)
7266 }
7267 "Promise" => new_promise(arg0(&args)),
7268 "Proxy" => crate::proxy::create(&args),
7269 // `new Function(p…, body)` — the same `CreateDynamicFunction` the plain
7270 // call form runs (20.2.1.1). `depd`'s `wrapfunction` builds its
7271 // deprecation wrapper this way, so `require('body-parser')` — and with it
7272 // `require('express')` — dies at load without it.
7273 "Function" => function_ctor(&args),
7274 "RegExp" => regexp_ctor(&args),
7275 "BigInt" => Err(host::type_error("BigInt is not a constructor")),
7276 "Error" => make_error_checked(name, &args),
7277 // `new DOMException(message, name)` — the name is an ARGUMENT, and the
7278 // legacy numeric `code` follows from it.
7279 "DOMException" => Ok(dom_exception(&args)),
7280 n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
7281 _ => Err(host::type_error(&format!("{name} is not a constructor"))),
7282 }
7283}
7284
7285/// The legacy numeric `DOMException.code` a WHATWG error name maps to. A name
7286/// outside the table — including the default `"Error"` — reports 0.
7287pub const DOM_EXCEPTION_CODES: &[(&str, f64)] = &[
7288 ("IndexSizeError", 1.0),
7289 ("DOMStringSizeError", 2.0),
7290 ("HierarchyRequestError", 3.0),
7291 ("WrongDocumentError", 4.0),
7292 ("InvalidCharacterError", 5.0),
7293 ("NoDataAllowedError", 6.0),
7294 ("NoModificationAllowedError", 7.0),
7295 ("NotFoundError", 8.0),
7296 ("NotSupportedError", 9.0),
7297 ("InUseAttributeError", 10.0),
7298 ("InvalidStateError", 11.0),
7299 ("SyntaxError", 12.0),
7300 ("InvalidModificationError", 13.0),
7301 ("NamespaceError", 14.0),
7302 ("InvalidAccessError", 15.0),
7303 ("ValidationError", 16.0),
7304 ("TypeMismatchError", 17.0),
7305 ("SecurityError", 18.0),
7306 ("NetworkError", 19.0),
7307 ("AbortError", 20.0),
7308 ("URLMismatchError", 21.0),
7309 ("QuotaExceededError", 22.0),
7310 ("TimeoutError", 23.0),
7311 ("InvalidNodeTypeError", 24.0),
7312 ("DataCloneError", 25.0),
7313];
7314
7315/// The static name a `DOMException` code is exposed under: the error name minus
7316/// its `Error` suffix, upper-snake-cased, plus `_ERR` — `AbortError` becomes
7317/// `ABORT_ERR`, `IndexSizeError` becomes `INDEX_SIZE_ERR`.
7318fn legacy_code_name(error_name: &str) -> String {
7319 let stem = error_name.strip_suffix("Error").unwrap_or(error_name);
7320 let mut out = String::new();
7321 for (i, c) in stem.chars().enumerate() {
7322 if c.is_ascii_uppercase() && i > 0 {
7323 out.push('_');
7324 }
7325 out.push(c.to_ascii_uppercase());
7326 }
7327 out.push_str("_ERR");
7328 out
7329}
7330
7331/// `new DOMException(message, name)`.
7332///
7333/// The class node's `AbortSignal.reason` rejects with. Its `name` is the second
7334/// ARGUMENT (defaulting to `"Error"`), not the class name, and its `code` is the
7335/// legacy number that name maps to.
7336pub fn dom_exception(args: &[Value]) -> Value {
7337 let message = match args.first() {
7338 None | Some(Value::Undef) => String::new(),
7339 Some(v) => with_host(|h| h.str_of(v)),
7340 };
7341 let name = match args.get(1) {
7342 None | Some(Value::Undef) => "Error".to_string(),
7343 Some(v) => with_host(|h| h.str_of(v)),
7344 };
7345 with_host(|h| dom_exception_with(h, &name, &message))
7346}
7347
7348/// `dom_exception` for a caller that already holds the host borrow.
7349pub(crate) fn dom_exception_with(h: &mut host::JsHost, name: &str, message: &str) -> Value {
7350 let name = name.to_string();
7351 let message = message.to_string();
7352 let code = DOM_EXCEPTION_CODES
7353 .iter()
7354 .find(|(n, _)| *n == name)
7355 .map(|(_, c)| *c)
7356 .unwrap_or(0.0);
7357 let head = if message.is_empty() {
7358 name.clone()
7359 } else {
7360 format!("{name}: {message}")
7361 };
7362 let e = synth_error(h, &head);
7363 {
7364 let nv = h.new_str(name);
7365 let mv = h.new_str(message);
7366 let sv = h.new_str(head);
7367 if let Some(JsObj::Object(p)) = h.get_mut(&e) {
7368 // `name`, `message` and `code` are PROTOTYPE accessors over internal
7369 // slots in node, so `stack` is the instance's only own property.
7370 // Storing them as own keys would show up in
7371 // `Object.getOwnPropertyNames`, which reports just `['stack']`.
7372 p.shift_remove("message");
7373 p.insert("@@domName".into(), nv);
7374 p.insert("@@domMessage".into(), mv);
7375 p.insert("@@domCode".into(), Value::Float(code));
7376 p.insert("stack".into(), sv);
7377 }
7378 h.ensure_error_protos();
7379 if let Some(proto) = host::error_proto_of(h, "DOMException") {
7380 h.set_proto(&e, proto);
7381 }
7382 }
7383 e
7384}
7385
7386/// A `DOMException`'s `name`/`message`/`code`, which live in internal slots
7387/// rather than as own properties. `None` for anything else.
7388pub fn dom_exception_slot(recv: &Value, name: &str) -> Option<Value> {
7389 let slot = match name {
7390 "name" => "@@domName",
7391 "message" => "@@domMessage",
7392 "code" => "@@domCode",
7393 _ => return None,
7394 };
7395 with_host(|h| match h.get(recv) {
7396 Some(JsObj::Object(p)) if p.contains_key("@@domName") => p.get(slot).cloned(),
7397 _ => None,
7398 })
7399}
7400
7401/// Build an `Error` object carrying `msg`, for stdlib callers that need to
7402/// throw a value with extra own properties on it.
7403pub(crate) fn make_error_pub(name: &str, msg: &str) -> Value {
7404 let m = with_host(|h| h.new_str(msg.to_string()));
7405 make_error_inner(name, &[m])
7406}
7407
7408/// [`make_error`] with the message's `ToString` allowed to FAIL. A symbol
7409/// refuses it (20.5.1.1 step 3), so `new Error(sym)` is a TypeError where this
7410/// rendered `Symbol(desc)` into `.message`.
7411fn make_error_checked(name: &str, args: &[Value]) -> Result<Value, String> {
7412 if let Some(m) = args.first().filter(|m| !matches!(m, Value::Undef)) {
7413 // AggregateError's message is its SECOND argument.
7414 let idx = usize::from(name == "AggregateError");
7415 if idx == 0 {
7416 host::to_string_value(m)?;
7417 } else if let Some(m2) = args.get(idx).filter(|m| !matches!(m, Value::Undef)) {
7418 host::to_string_value(m2)?;
7419 }
7420 }
7421 Ok(make_error_inner(name, args))
7422}
7423
7424fn make_error_inner(name: &str, args: &[Value]) -> Value {
7425 // `new AggregateError(errors, message)` takes the causes FIRST; every other
7426 // error constructor takes the message first.
7427 let agg = name == "AggregateError";
7428 let (errors, args) = if agg {
7429 (
7430 Some(args.first().cloned().unwrap_or(Value::Undef)),
7431 args.get(1..).unwrap_or(&[]),
7432 )
7433 } else {
7434 (None, args)
7435 };
7436 with_host(|h| {
7437 h.ensure_error_protos();
7438 let mut props: IndexMap<String, Value> = IndexMap::new();
7439 let msg = args
7440 .first()
7441 .filter(|a| !matches!(a, Value::Undef))
7442 .map(|a| h.str_of(a));
7443 if let Some(m) = &msg {
7444 let mv = h.new_str(m.clone());
7445 props.insert("message".into(), mv);
7446 }
7447 // `.stack` is engine-specific; a simple `Name: message` header line
7448 // suffices for parity (the fuzzer never prints raw stacks).
7449 //
7450 // V8 formats that header LAZILY, on the first read, from whatever `name`
7451 // and `message` the error carries at that moment — which is why the
7452 // near-universal
7453 //
7454 // class MyErr extends Error { constructor(m) { super(m); this.name = 'MyErr'; } }
7455 //
7456 // reports `MyErr: boom` and not the `Error: boom` this built eagerly,
7457 // inside `super()`, before the subclass had renamed anything. `@@stackRaw`
7458 // carries the frames so the read can redo it; see `materialize_stack`.
7459 let frames = h.stack_frames();
7460 let stack = match &msg {
7461 Some(m) if !m.is_empty() => format!("{name}: {m}{frames}"),
7462 _ => format!("{name}{frames}"),
7463 };
7464 let sv = h.new_str(stack);
7465 props.insert("stack".into(), sv);
7466 let raw = h.new_str(frames);
7467 props.insert("@@stackRaw".into(), raw);
7468 if let Some(errs) = errors {
7469 // Materialize the iterable into the own `errors` array property.
7470 let items = h.iter_vec(&errs).unwrap_or_default();
7471 let arr = h.new_array(items);
7472 props.insert("errors".into(), arr);
7473 }
7474 // `new Error(msg, { cause })` (ES2022): installed only when the options
7475 // bag actually has a `cause` key, so `new Error(m, {})` leaves none.
7476 let opts = args.get(1);
7477 if let Some(cause) = opts.and_then(|o| match h.get(o) {
7478 Some(JsObj::Object(p)) => p.get("cause").cloned(),
7479 _ => None,
7480 }) {
7481 props.insert("cause".into(), cause);
7482 }
7483 let e = h.new_object(props);
7484 if let Some(p) = host::error_proto_of(h, name) {
7485 h.set_proto(&e, p);
7486 }
7487 // Every own slot an error constructor installs is non-enumerable in V8,
7488 // which is why `Object.keys(err)` is `[]` and `JSON.stringify(err)` is
7489 // `{}` — properties a *script* later assigns stay enumerable.
7490 for k in ["message", "stack", "errors", "cause", "@@stackRaw"] {
7491 h.hide_prop(&e, k);
7492 }
7493 e
7494 })
7495}
7496
7497fn print_line(args: &[Value], stderr: bool) -> Result<(), String> {
7498 // Node's console.log(...args) === util.format(...args): printf-style
7499 // substitution when the first arg is a format string, else inspect-and-join.
7500 // A directive can THROW (`console.log('%j', 1n)`), and node lets that reach
7501 // the caller instead of printing a line — so nothing is written on failure.
7502 let line: String = crate::stdlib::util::format(args)?;
7503 with_host(|h| h.write_out(&format!("{line}\n"), stderr));
7504 Ok(())
7505}
7506
7507fn arg0(args: &[Value]) -> Value {
7508 args.first().cloned().unwrap_or(Value::Undef)
7509}
7510/// `ToString(arg)` for a builtin's argument — fallible, because a SYMBOL
7511/// refuses the conversion (7.1.17). Every site that reached for `str_of`
7512/// instead rendered `Symbol(desc)` into its result and reported nothing.
7513fn arg_to_string(args: &[Value], i: usize) -> Result<String, String> {
7514 let v = args.get(i).cloned().unwrap_or(Value::Undef);
7515 let sv = host::to_string_value(&v)?;
7516 Ok(with_host(|h| h.str_of(&sv)))
7517}
7518
7519fn arg_num(args: &[Value], i: usize) -> f64 {
7520 with_host(|h| h.to_number(&args.get(i).cloned().unwrap_or(Value::Undef)))
7521}
7522
7523fn is_integer(v: Value) -> bool {
7524 match v {
7525 Value::Int(_) => true,
7526 Value::Float(f) => f.is_finite() && f.fract() == 0.0,
7527 _ => false,
7528 }
7529}
7530fn is_safe_integer(v: Value) -> bool {
7531 match v {
7532 Value::Float(f) => f.is_finite() && f.fract() == 0.0 && f.abs() <= 9007199254740991.0,
7533 Value::Int(_) => true,
7534 _ => false,
7535 }
7536}
7537
7538/// `encodeURI`/`encodeURIComponent`: percent-encode `s`'s UTF-8 bytes, leaving
7539/// the unreserved set unescaped. `encodeURI` additionally preserves the reserved
7540/// URI characters (`;,/?:@&=+$#`) that delimit a URI's structure.
7541fn uri_encode(s: &str, uri: bool) -> Result<Value, String> {
7542 // Always-unescaped (`encodeURIComponent`'s unreserved set), per the spec.
7543 const UNRESERVED: &[u8] =
7544 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
7545 // Reserved characters `encodeURI` leaves intact on top of the unreserved set.
7546 const RESERVED: &[u8] = b";,/?:@&=+$#";
7547 let mut out = String::with_capacity(s.len());
7548 for &b in s.as_bytes() {
7549 if UNRESERVED.contains(&b) || (uri && RESERVED.contains(&b)) {
7550 out.push(b as char);
7551 } else {
7552 out.push('%');
7553 out.push(
7554 char::from_digit((b >> 4) as u32, 16)
7555 .unwrap()
7556 .to_ascii_uppercase(),
7557 );
7558 out.push(
7559 char::from_digit((b & 0xf) as u32, 16)
7560 .unwrap()
7561 .to_ascii_uppercase(),
7562 );
7563 }
7564 }
7565 Ok(with_host(|h| h.new_str(out)))
7566}
7567
7568/// `decodeURI`/`decodeURIComponent`: reverse `%XX` escapes back to UTF-8 text.
7569/// For `decodeURI`, escapes of the reserved delimiters are left as-is (the spec's
7570/// asymmetry with `encodeURI`). Throws `URIError` on a malformed escape.
7571fn uri_decode(s: &str, uri: bool) -> Result<Value, String> {
7572 const RESERVED: &[u8] = b";,/?:@&=+$#";
7573 let bytes = s.as_bytes();
7574 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
7575 let mut i = 0;
7576 while i < bytes.len() {
7577 if bytes[i] == b'%' {
7578 if i + 2 >= bytes.len() {
7579 return Err("URIError: URI malformed".into());
7580 }
7581 let hi = (bytes[i + 1] as char).to_digit(16);
7582 let lo = (bytes[i + 2] as char).to_digit(16);
7583 match (hi, lo) {
7584 (Some(h), Some(l)) => {
7585 let byte = (h * 16 + l) as u8;
7586 // decodeURI keeps reserved-delimiter escapes literal.
7587 if uri && RESERVED.contains(&byte) {
7588 out.extend_from_slice(&bytes[i..i + 3]);
7589 } else {
7590 out.push(byte);
7591 }
7592 i += 3;
7593 }
7594 _ => return Err("URIError: URI malformed".into()),
7595 }
7596 } else {
7597 out.push(bytes[i]);
7598 i += 1;
7599 }
7600 }
7601 match String::from_utf8(out) {
7602 Ok(decoded) => Ok(with_host(|h| h.new_str(decoded))),
7603 Err(_) => Err("URIError: URI malformed".into()),
7604 }
7605}
7606
7607/// `escape` (Annex B.2.1.1) — the pre-`encodeURIComponent` legacy encoder, still
7608/// present in every engine and still reached by old libraries (jQuery's cookie
7609/// plugin, `querystring`-era code). It works on UTF-16 CODE UNITS, not UTF-8
7610/// bytes, which is what separates it from `encodeURIComponent`: a unit below
7611/// `0x100` becomes `%XX`, anything above becomes `%uXXXX`, so an astral
7612/// character yields the two escapes of its surrogate pair
7613/// (`escape("\u{1D4B3}")` is `"%uD835%uDCB3"` on node v26.7.0).
7614///
7615/// The unescaped set is frozen by the spec and is NOT the URI unreserved set —
7616/// it keeps `@*_+-./` and drops `!~'()`.
7617fn legacy_escape(s: &str) -> Result<Value, String> {
7618 const KEEP: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";
7619 let mut out = String::with_capacity(s.len());
7620 for u in s.encode_utf16() {
7621 if u < 0x100 {
7622 if KEEP.contains(&(u as u8)) {
7623 out.push(u as u8 as char);
7624 } else {
7625 out.push_str(&format!("%{u:02X}"));
7626 }
7627 } else {
7628 out.push_str(&format!("%u{u:04X}"));
7629 }
7630 }
7631 Ok(with_host(|h| h.new_str(out)))
7632}
7633
7634/// `unescape` (Annex B.2.1.2) — the inverse of [`legacy_escape`]. Unlike
7635/// `decodeURIComponent` it never throws: a `%` that does not begin a well-formed
7636/// `%XX` or `%uXXXX` escape is passed through literally
7637/// (`unescape("%u0041%42%zz%2")` is `"AB%zz%2"` on node v26.7.0).
7638///
7639/// Decoding is done in code-unit space and re-joined at the end so a
7640/// `%uD835%uDCB3` pair recomposes into the one astral character it came from.
7641fn legacy_unescape(s: &str) -> Result<Value, String> {
7642 let b = s.as_bytes();
7643 let hex = |i: usize, n: usize| -> Option<u16> {
7644 if i + n > b.len() {
7645 return None;
7646 }
7647 let mut v: u16 = 0;
7648 for &c in &b[i..i + n] {
7649 v = v.checked_mul(16)? + (c as char).to_digit(16)? as u16;
7650 }
7651 Some(v)
7652 };
7653 let units: Vec<u16> = s.encode_utf16().collect();
7654 let mut out: Vec<u16> = Vec::with_capacity(units.len());
7655 let mut i = 0;
7656 while i < b.len() {
7657 // Escapes are pure ASCII, so a byte index is a unit index up to here —
7658 // but the tail may not be, so non-`%` bytes are re-decoded as chars.
7659 if b[i] == b'%' {
7660 if let Some(u) = hex(i + 1, 2) {
7661 out.push(u);
7662 i += 3;
7663 continue;
7664 }
7665 if b.get(i + 1) == Some(&b'u') {
7666 if let Some(u) = hex(i + 2, 4) {
7667 out.push(u);
7668 i += 6;
7669 continue;
7670 }
7671 }
7672 }
7673 let c = s[i..].chars().next().unwrap_or('%');
7674 let mut buf = [0u16; 2];
7675 out.extend_from_slice(c.encode_utf16(&mut buf));
7676 i += c.len_utf8();
7677 }
7678 Ok(with_host(|h| {
7679 h.new_str(crate::utf16::to_string_lossy(&out))
7680 }))
7681}
7682
7683/// `parseInt` begins with `ToString(argument)` (19.2.5 step 1), and that step can
7684/// THROW — a Symbol has no string form, so `parseInt([Symbol()])` is a TypeError
7685/// rather than `NaN`. Reading the argument with `str_of` took the object's brand
7686/// instead of converting it, which both swallowed that throw and ignored any
7687/// `toString` the value defines.
7688fn parse_int(args: &[Value]) -> Result<f64, String> {
7689 // Converted BEFORE the host borrow: `to_string_value` can call back into JS.
7690 let sv = host::to_string_value(&arg0(args))?;
7691 // 19.2.5 step 2 is `ToInt32(radix)`, which runs a user `valueOf` — the
7692 // infallible read below does no `ToPrimitive`, so an object radix came out
7693 // as NaN and the parse silently fell back to auto-detection.
7694 let radix = match args.get(1) {
7695 Some(r) if !matches!(r, Value::Undef) => {
7696 vec![arg0(args), Value::Float(to_number_arg(args, 1)?)]
7697 }
7698 _ => args.to_vec(),
7699 };
7700 Ok(parse_int_str(&with_host(|h| h.str_of(&sv)), &radix))
7701}
7702
7703fn parse_int_str(s: &str, args: &[Value]) -> f64 {
7704 // 19.2.5 step 8: an EXPLICIT radix outside 2..=36 is `NaN`, it does not fall
7705 // back to auto-detection. The old `.filter()` silently discarded a bad radix,
7706 // so `parseInt("10", 37)` answered 10 where every engine says NaN.
7707 let radix_arg = args
7708 .get(1)
7709 .map(|r| with_host(|h| host::to_int32(h.to_number(r))));
7710 let radix = match radix_arg {
7711 Some(0) | None => None,
7712 Some(r) if (2..=36).contains(&r) => Some(r as u32),
7713 Some(_) => return f64::NAN,
7714 };
7715 let t = crate::utf16::js_trim_start(s);
7716 let (neg, digits) = match t.strip_prefix('-') {
7717 Some(rest) => (true, rest),
7718 None => (false, t.strip_prefix('+').unwrap_or(t)),
7719 };
7720 let (radix, digits) = match radix {
7721 Some(16) => (
7722 16u32,
7723 digits
7724 .strip_prefix("0x")
7725 .or_else(|| digits.strip_prefix("0X"))
7726 .unwrap_or(digits),
7727 ),
7728 Some(r) => (r, digits),
7729 None => {
7730 if let Some(hex) = digits
7731 .strip_prefix("0x")
7732 .or_else(|| digits.strip_prefix("0X"))
7733 {
7734 (16, hex)
7735 } else {
7736 (10, digits)
7737 }
7738 }
7739 };
7740 let valid: String = digits.chars().take_while(|c| c.is_digit(radix)).collect();
7741 if valid.is_empty() {
7742 return f64::NAN;
7743 }
7744 // Accumulate in `f64`, not `i64`. `i64::from_str_radix` OVERFLOWS past ~19
7745 // digits and the error was mapped to `NaN`, so
7746 // `parseInt("999999999999999999999999")` was NaN instead of 1e+24. The spec
7747 // asks for the mathematical value rounded to a Number, which is what
7748 // repeated multiply-accumulate in `f64` produces.
7749 let n = if radix == 10 {
7750 // Rust's decimal float parser is correctly rounded; digit-by-digit
7751 // multiply-accumulate is not, and drifted a ULP on long inputs
7752 // (`parseInt("999999999999999999999999")` came out
7753 // 1.0000000000000003e+24 rather than 1e+24).
7754 valid.parse::<f64>().unwrap_or(f64::NAN)
7755 } else {
7756 let mut n = 0.0f64;
7757 for c in valid.chars() {
7758 n = n * radix as f64 + c.to_digit(radix).unwrap_or(0) as f64;
7759 }
7760 n
7761 };
7762 if neg {
7763 -n
7764 } else {
7765 n
7766 }
7767}
7768
7769/// `parseFloat` likewise starts from `ToString(argument)`; see `parse_int`.
7770fn parse_float(args: &[Value]) -> Result<f64, String> {
7771 let sv = host::to_string_value(&arg0(args))?;
7772 Ok(parse_float_str(&with_host(|h| h.str_of(&sv))))
7773}
7774
7775fn parse_float_str(s: &str) -> f64 {
7776 let t = crate::utf16::js_trim_start(s);
7777 // `Infinity` / `+Infinity` / `-Infinity` are valid parseFloat prefixes.
7778 let inf_body = t
7779 .strip_prefix('+')
7780 .or_else(|| t.strip_prefix('-'))
7781 .unwrap_or(t);
7782 if inf_body.starts_with("Infinity") {
7783 return if t.starts_with('-') {
7784 f64::NEG_INFINITY
7785 } else {
7786 f64::INFINITY
7787 };
7788 }
7789 // The LONGEST prefix that is itself a complete `StrDecimalLiteral`, which is
7790 // not the same as the longest run of characters that could appear in one:
7791 // `"1e"` and `"1e+"` are `1` in every engine, because the exponent part is
7792 // only valid once a digit follows `e`. Tracking `end` at every character
7793 // accepted the dangling `e`, `parse::<f64>` then failed, and the whole call
7794 // came back NaN.
7795 let mut end = 0;
7796 let bytes = t.as_bytes();
7797 let mut seen_dot = false;
7798 let mut seen_e = false;
7799 let mut digits_before_dot = false;
7800 for (i, &c) in bytes.iter().enumerate() {
7801 match c {
7802 b'0'..=b'9' => {
7803 if !seen_dot && !seen_e {
7804 digits_before_dot = true;
7805 }
7806 end = i + 1;
7807 }
7808 // A sign is only meaningful leading, or straight after the exponent
7809 // marker; it never completes a literal on its own.
7810 b'+' | b'-' if i == 0 || bytes[i - 1] == b'e' || bytes[i - 1] == b'E' => {}
7811 // `1.` is a complete literal; a bare `.` is not.
7812 b'.' if !seen_dot && !seen_e => {
7813 seen_dot = true;
7814 if digits_before_dot {
7815 end = i + 1;
7816 }
7817 }
7818 b'e' | b'E' if !seen_e && end > 0 => seen_e = true,
7819 _ => break,
7820 }
7821 }
7822 if end == 0 {
7823 return f64::NAN;
7824 }
7825 t[..end].parse::<f64>().unwrap_or(f64::NAN)
7826}
7827
7828/// ECMA-262 `Number::exponentiate` (6.1.6.1.3), backing both `Math.pow` and the
7829/// `**` operator. Three clauses differ from IEEE-754 `pow`, which is what Rust's
7830/// `powf` implements: a NaN exponent is NaN even for base 1, a NaN base is NaN
7831/// for any non-zero exponent, and `|base| == 1` with an infinite exponent is NaN
7832/// rather than 1.
7833pub(crate) fn js_pow(base: f64, exp: f64) -> f64 {
7834 if exp == 0.0 {
7835 return 1.0;
7836 }
7837 if base.is_nan() || exp.is_nan() {
7838 return f64::NAN;
7839 }
7840 if base.abs() == 1.0 && exp.is_infinite() {
7841 return f64::NAN;
7842 }
7843 base.powf(exp)
7844}
7845
7846fn math_fn(fname: &str, args: &[Value]) -> Result<Value, String> {
7847 // Every `Math` function coerces its arguments with `ToNumber`, and `ToNumber`
7848 // of a BigInt is a TypeError (7.1.4 step 2) — the whole point of BigInt being
7849 // a separate numeric type. `arg_num` reads a BigInt's magnitude instead, so
7850 // `Math.max(1n)` quietly answered 1 where V8 throws. `Math.random` is the one
7851 // exception: it never reads an argument, so `Math.random(1n)` is fine.
7852 // A BigInt WRAPPER converts to a BigInt and is rejected just as the
7853 // primitive is: `Math.abs(Object(9n))` is a TypeError where it answered NaN.
7854 // The boxed value is read BEFORE the borrow — `wrapped_primitive` borrows
7855 // the host itself and cannot run inside another borrow.
7856 let is_bigint = |a: &Value| {
7857 if with_host(|h| matches!(h.get(a), Some(JsObj::BigInt(_)))) {
7858 return true;
7859 }
7860 match wrapped_primitive(a) {
7861 Some(p) => with_host(|h| matches!(h.get(&p), Some(JsObj::BigInt(_)))),
7862 None => false,
7863 }
7864 };
7865 if fname != "random" && args.iter().any(is_bigint) {
7866 return Err(host::type_error(
7867 "Cannot convert a BigInt value to a number",
7868 ));
7869 }
7870 // Every argument is `ToNumber`d (21.3.2.x), which runs a user `valueOf` and
7871 // can throw from it. `arg_num` does no `ToPrimitive` at all, so
7872 // `Math.max({valueOf: () => 1}, 0)` answered NaN.
7873 // EVERY argument, not a fixed prefix: `Math.max`/`min`/`hypot` are
7874 // variadic, and coercing only the first few silently DROPPED the rest —
7875 // `Math.max(...gen)` over five values answered for four of them.
7876 let mut coerced = Vec::with_capacity(args.len());
7877 for a in args {
7878 if matches!(a, Value::Undef) {
7879 coerced.push(a.clone());
7880 continue;
7881 }
7882 let p = host::to_primitive(a, "number")?;
7883 coerced.push(Value::Float(with_host(|h| h.to_number(&p))));
7884 }
7885 let args: &[Value] = &coerced;
7886 let x = arg_num(args, 0);
7887 let r = match fname {
7888 "floor" => x.floor(),
7889 "ceil" => x.ceil(),
7890 // ECMA-262 `Math.round` (21.3.2.28) transcribed clause by clause. The
7891 // obvious `(x + 0.5).floor()` is NOT this function: the addition rounds
7892 // before the floor sees it, so it answers 1 for the largest double below
7893 // 0.5 (`Math.round(0.49999999999999994)` is 0 in every engine) and it
7894 // perturbs integers above 2^52, where `x + 0.5` is no longer
7895 // representable (`Math.round(4503599627370497)` must be the input).
7896 // Splitting the zero-band cases out first also carries the signed zero
7897 // the spec asks for without a post-hoc patch.
7898 "round" => {
7899 if !x.is_finite() || x == 0.0 {
7900 x
7901 } else if x > 0.0 && x < 0.5 {
7902 0.0
7903 } else if (-0.5..0.0).contains(&x) {
7904 -0.0
7905 } else {
7906 // |x| >= 0.5, so `floor` and the subtraction are both exact
7907 // (every double >= 2^52 is already an integer and yields 0 here).
7908 let f = x.floor();
7909 if x - f >= 0.5 {
7910 f + 1.0
7911 } else {
7912 f
7913 }
7914 }
7915 }
7916 "trunc" => x.trunc(),
7917 "abs" => x.abs(),
7918 "sign" => {
7919 if x.is_nan() {
7920 f64::NAN
7921 } else if x > 0.0 {
7922 1.0
7923 } else if x < 0.0 {
7924 -1.0
7925 } else {
7926 x
7927 }
7928 }
7929 "sqrt" => x.sqrt(),
7930 "cbrt" => x.cbrt(),
7931 "exp" => x.exp(),
7932 "log" => x.ln(),
7933 "log2" => x.log2(),
7934 "log10" => x.log10(),
7935 "sin" => x.sin(),
7936 "cos" => x.cos(),
7937 "tan" => x.tan(),
7938 "asin" => x.asin(),
7939 "acos" => x.acos(),
7940 "atan" => x.atan(),
7941 "atan2" => x.atan2(arg_num(args, 1)),
7942 // Rust `powf` is IEEE-754 `pow`, which is NOT JS `**`/`Math.pow`: IEEE
7943 // makes `pow(x, ±0)` and `pow(±1, y)` return 1 unconditionally, so
7944 // `(-1) ** Infinity` and `1 ** NaN` come back 1 where the spec
7945 // (6.1.6.1.3 Number::exponentiate) says NaN. Only the exponent-is-zero
7946 // clause is shared.
7947 "pow" => js_pow(x, arg_num(args, 1)),
7948 // Hyperbolics and the two precision-preserving log/exp forms.
7949 "sinh" => x.sinh(),
7950 "cosh" => x.cosh(),
7951 "tanh" => x.tanh(),
7952 "asinh" => x.asinh(),
7953 "acosh" => x.acosh(),
7954 "atanh" => x.atanh(),
7955 "log1p" => x.ln_1p(),
7956 "expm1" => x.exp_m1(),
7957 // C-style 32-bit integer multiply: ToInt32 both operands, multiply with
7958 // wraparound, reinterpret as a signed 32-bit result.
7959 "imul" => (host::to_int32(x).wrapping_mul(host::to_int32(arg_num(args, 1)))) as f64,
7960 "hypot" => {
7961 // Scale by the largest magnitude before squaring — this avoids the
7962 // last-ULP error of the naive `sqrt(Σ xᵢ²)` and matches V8's result.
7963 let xs: Vec<f64> = args.iter().map(|a| with_host(|h| h.to_number(a))).collect();
7964 let mut max = 0.0f64;
7965 for x in &xs {
7966 if x.abs() > max {
7967 max = x.abs();
7968 }
7969 }
7970 if xs.iter().any(|x| x.is_infinite()) {
7971 f64::INFINITY
7972 } else if max == 0.0 || !max.is_finite() {
7973 max
7974 } else {
7975 let s: f64 = xs.iter().map(|x| (x / max) * (x / max)).sum();
7976 max * s.sqrt()
7977 }
7978 }
7979 "random" => pseudo_random(),
7980 "max" => {
7981 if args.is_empty() {
7982 f64::NEG_INFINITY
7983 } else {
7984 let mut m = f64::NEG_INFINITY;
7985 for a in args {
7986 let n = with_host(|h| h.to_number(a));
7987 if n.is_nan() {
7988 return Ok(Value::Float(f64::NAN));
7989 }
7990 // `>` cannot separate the zeroes (`0.0 > -0.0` is false), but
7991 // the spec ranks +0 above -0, so `Math.max(-0, 0)` is +0 and
7992 // must not keep the -0 the first iteration installed.
7993 if n > m || (n == m && n == 0.0 && n.is_sign_positive()) {
7994 m = n;
7995 }
7996 }
7997 m
7998 }
7999 }
8000 "min" => {
8001 if args.is_empty() {
8002 f64::INFINITY
8003 } else {
8004 let mut m = f64::INFINITY;
8005 for a in args {
8006 let n = with_host(|h| h.to_number(a));
8007 if n.is_nan() {
8008 return Ok(Value::Float(f64::NAN));
8009 }
8010 // Mirror of `max`: -0 ranks below +0 even though `<` says
8011 // they are equal, so `Math.min(0, -0)` is -0.
8012 if n < m || (n == m && n == 0.0 && n.is_sign_negative()) {
8013 m = n;
8014 }
8015 }
8016 m
8017 }
8018 }
8019 // Count leading zero bits of ToUint32(x) (Math.clz32(1) === 31).
8020 "clz32" => {
8021 let u = if x.is_finite() {
8022 x.trunc().rem_euclid(4294967296.0) as u32
8023 } else {
8024 0
8025 };
8026 u.leading_zeros() as f64
8027 }
8028 // Round to the nearest single-precision float.
8029 "fround" => (x as f32) as f64,
8030 _ => return Err(host::type_error(&format!("Math.{fname} is not a function"))),
8031 };
8032 Ok(Value::Float(r))
8033}
8034
8035/// A small deterministic PRNG for `Math.random` (output is non-reproducible vs
8036/// Node by nature; kept simple).
8037fn pseudo_random() -> f64 {
8038 use std::cell::Cell;
8039 thread_local!(static SEED: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
8040 SEED.with(|s| {
8041 let mut x = s.get();
8042 x ^= x << 13;
8043 x ^= x >> 7;
8044 x ^= x << 17;
8045 s.set(x);
8046 (x >> 11) as f64 / (1u64 << 53) as f64
8047 })
8048}
8049
8050// ── Object.* ──────────────────────────────────────────────────────────────────
8051
8052/// The characters of a string PRIMITIVE, as the `ToObject` wrapper's own index
8053/// properties (10.4.3 `StringExoticObject`).
8054///
8055/// `getOwnPropertyDescriptor` begins with `ToObject`, which boxes a string into
8056/// an exotic object whose own keys are its code-unit indices plus `length`;
8057/// this is the descriptor half of that. (The KEY half lives in
8058/// `JsHost::own_enum_data_keys`, the single source every enumeration path
8059/// reads.) Indices are UTF-16 code units, matching `.length` and `s[i]`.
8060///
8061/// A boxed `String` object is deliberately NOT routed here: it can carry
8062/// ordinary own properties too (`const s = new String('ab'); s.x = 1`), and its
8063/// existing path already reports them alongside the indices.
8064fn string_primitive_units(v: &Value) -> Option<Vec<String>> {
8065 // A JS string primitive rides as a `Value::Obj` handle to `JsObj::Str` (see
8066 // `host.rs`); a BOXED `new String(...)` is a different heap object, so this
8067 // never catches one.
8068 let s = match v {
8069 Value::Str(s) => (**s).clone(),
8070 _ => with_host(|h| match h.get(v) {
8071 Some(JsObj::Str(s)) => Some(s.clone()),
8072 _ => None,
8073 })?,
8074 };
8075 let units = crate::utf16::Units::of(&s);
8076 Some((0..units.len()).filter_map(|i| units.unit_str(i)).collect())
8077}
8078
8079fn object_keys(args: Vec<Value>, mode: u8) -> Result<Value, String> {
8080 let v = arg0(&args);
8081 require_object_coercible(&v)?;
8082 // A Proxy answers from its `ownKeys` trap. `getOwnPropertyNames` (mode 3)
8083 // reports every own STRING key the trap named; the enumerating modes
8084 // additionally filter by each key's `[[GetOwnProperty]]`, so both traps run.
8085 if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8086 if mode == 3 {
8087 let keys = crate::proxy::own_keys(&v)?.unwrap_or_default();
8088 return Ok(with_host(|h| {
8089 let out: Vec<Value> = keys
8090 .into_iter()
8091 .filter(|k| !host::is_symbol_key(k))
8092 .map(|k| h.new_str(k))
8093 .collect();
8094 h.new_array(out)
8095 }));
8096 }
8097 // `Object.keys` (mode 0) must run `ownKeys` and `getOwnPropertyDescriptor`
8098 // and STOP — 7.3.23 never performs `[[Get]]` when only keys are wanted.
8099 // Going through `own_enum_entries` fired the `get` trap once per key, so
8100 // the observable trap sequence carried a trailing `get` node does not
8101 // emit, and a trap with side effects ran when it should not have.
8102 if mode == 0 {
8103 let keys = crate::proxy::own_enum_string_keys(&v)?;
8104 return Ok(with_host(|h| {
8105 let out: Vec<Value> = keys.into_iter().map(|k| h.new_str(k)).collect();
8106 h.new_array(out)
8107 }));
8108 }
8109 let entries = crate::proxy::own_enum_entries(&v)?;
8110 return Ok(with_host(|h| {
8111 let out: Vec<Value> = entries
8112 .into_iter()
8113 .map(|(k, val)| match mode {
8114 0 => h.new_str(k),
8115 1 => val,
8116 _ => {
8117 let ks = h.new_str(k);
8118 h.new_array(vec![ks, val])
8119 }
8120 })
8121 .collect();
8122 h.new_array(out)
8123 }));
8124 }
8125 // An intrinsic prototype this host built as a REAL OBJECT — `Symbol
8126 // .prototype`, `String.prototype`, the error hierarchy — answers from the
8127 // generated table too. It was answering from its own property map instead,
8128 // which carries neither the right names nor V8's order: `Symbol.prototype`
8129 // reported `toLocaleString` and omitted `description`, and
8130 // `String.prototype` omitted `length` and every Annex B HTML method.
8131 //
8132 // `ns` is the namespace SPELLING, so the arm below is shared verbatim —
8133 // the two representations of a prototype cannot answer differently.
8134 let real_proto_ns = with_host(|h| h.intrinsic_proto_ctor(&v).map(|c| format!("{c}.prototype")))
8135 .filter(|ns| intrinsic_proto_members(ns).is_some());
8136 // A builtin prototype namespace that exposes enumerable methods for copying
8137 // (`Object.getOwnPropertyNames(EventEmitter.prototype)` — express's mixin).
8138 if let Some(ns) = real_proto_ns.or_else(|| {
8139 with_host(|h| match h.get(&v) {
8140 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
8141 _ => None,
8142 })
8143 }) {
8144 // An INTRINSIC prototype (`Map.prototype`, `URL.prototype`). Members are
8145 // non-enumerable on an ECMAScript builtin and enumerable on a WebIDL
8146 // interface, which the table records per name.
8147 if let Some(members) = intrinsic_proto_members(&ns) {
8148 let ctor = ns.trim_end_matches(".prototype");
8149 let mut names: Vec<String> = members
8150 .iter()
8151 .filter(|m| mode == 3 || m.starts_with('+'))
8152 .map(|m| m.strip_prefix('+').unwrap_or(m).to_string())
8153 // `getOwnPropertyNames` reports STRING keys only; the table's
8154 // `@@` entries are symbol-keyed members and belong to
8155 // `getOwnPropertySymbols` instead.
8156 .filter(|m| !m.starts_with("@@"))
8157 .collect();
8158 // Plus whatever a script patched onto this prototype under a NEW
8159 // name — an ordinary enumerable own property, so it lists in every
8160 // mode. Without it `Object.keys(Array.prototype)` stayed `[]` after
8161 // an assignment that `Array.prototype.patch` read back happily.
8162 //
8163 // Assigning over an EXISTING member is a `[[Set]]`, which leaves
8164 // that member's attributes alone: restoring a saved `join` must not
8165 // turn it into an enumerable key.
8166 for k in with_host(|h| h.builtin_static_keys(&ns)) {
8167 if !intrinsic_proto_member(&ns, &k) && !names.contains(&k) {
8168 names.push(k);
8169 }
8170 }
8171 return Ok(with_host(|h| {
8172 let out: Vec<Value> = names
8173 .iter()
8174 .map(|name| {
8175 // An accessor member has no thunk — `Map.prototype.size`
8176 // is not a function — so a VALUE read of one answers
8177 // undefined rather than synthesizing a callable.
8178 let val = |h: &mut host::JsHost| {
8179 if let Some(v) = h.builtin_static(&ns, name) {
8180 return v;
8181 }
8182 let key = format!("@proto:{ctor}:{name}");
8183 if builtin_meta(&key).is_some() {
8184 h.alloc(JsObj::Builtin(key))
8185 } else {
8186 Value::Undef
8187 }
8188 };
8189 match mode {
8190 1 => val(h),
8191 2 => {
8192 let ks = h.new_str(name.clone());
8193 let v = val(h);
8194 h.new_array(vec![ks, v])
8195 }
8196 _ => h.new_str(name.clone()),
8197 }
8198 })
8199 .collect();
8200 h.new_array(out)
8201 }));
8202 }
8203 if let Some(names) = builtin_proto_method_names(&ns) {
8204 return Ok(with_host(|h| {
8205 let out: Vec<Value> = names
8206 .iter()
8207 .map(|name| match mode {
8208 1 => h.alloc(JsObj::Builtin(format!(
8209 "@proto:{}:{name}",
8210 ns.trim_end_matches(".prototype")
8211 ))),
8212 2 => {
8213 let ks = h.new_str(*name);
8214 let val = h.alloc(JsObj::Builtin(format!(
8215 "@proto:{}:{name}",
8216 ns.trim_end_matches(".prototype")
8217 )));
8218 h.new_array(vec![ks, val])
8219 }
8220 _ => h.new_str(*name),
8221 })
8222 .collect();
8223 h.new_array(out)
8224 }));
8225 }
8226 // A stdlib namespace (`Buffer`, `require('buffer')`): its own enumerable
8227 // keys are the members node-js implements, each resolved to the same
8228 // first-class value a property read would give.
8229 let mut names = crate::stdlib::namespace_keys(&ns);
8230 // A core namespace (`Reflect`, `Math`, `JSON`) has no stdlib key list —
8231 // its members live in the builtin dispatch table. They are
8232 // non-enumerable in V8, so they surface only under
8233 // `getOwnPropertyNames`/`Reflect.ownKeys` (mode 3), never `Object.keys`.
8234 if names.is_empty() && mode == 3 {
8235 let prefix = format!("{ns}.");
8236 // A builtin constructor's own `length`/`name`/`prototype` come
8237 // first, as they do in V8.
8238 if is_builtin_ctor(&ns) {
8239 names.extend(["length", "name", "prototype"].map(str::to_string));
8240 }
8241 names.extend(
8242 NS_METHODS
8243 .iter()
8244 .filter_map(|q| q.strip_prefix(&prefix))
8245 .map(|m| m.to_string()),
8246 );
8247 // The numeric constants are members too. Without them
8248 // `getOwnPropertyNames(Math)` reported 35 of the 43 names node-js
8249 // actually answers — the eight it dropped being `PI` and its
8250 // siblings, which read fine and now own a descriptor as well.
8251 names.extend(
8252 namespace_constants(&ns)
8253 .iter()
8254 .map(|(k, _)| (*k).to_string()),
8255 );
8256 }
8257 // A builtin FUNCTION owns exactly `length` and `name` (10.3.3-4), so
8258 // `Object.getOwnPropertyNames(Math.max)` is `[ 'length', 'name' ]` — it
8259 // was `[]`, which said the function had no properties at all while both
8260 // of them read back a value. `length` is listed only where the intrinsic
8261 // table has an arity, so the names never advertise a read that answers
8262 // `undefined`.
8263 if names.is_empty() && mode == 3 && host::builtin_is_callable(&ns) {
8264 if builtin_meta(&ns).is_some() {
8265 names.push("length".to_string());
8266 }
8267 names.push("name".to_string());
8268 }
8269 // Whatever a script assigned onto the namespace, in assignment order and
8270 // after the built-in members — an ordinary enumerable own property, so
8271 // it surfaces under `Object.keys` too and not only `ownKeys`. These were
8272 // missing from every listing, which made a patched prototype read as
8273 // unpatched to any code that enumerates rather than reads.
8274 for k in with_host(|h| h.builtin_static_keys(&ns)) {
8275 if !names.contains(&k) {
8276 names.push(k);
8277 }
8278 }
8279 if !names.is_empty() {
8280 let entries: Vec<(String, Value)> = names
8281 .into_iter()
8282 .map(|k| {
8283 let val = namespace_property(&ns, &k);
8284 (k, val)
8285 })
8286 .collect();
8287 return Ok(with_host(|h| {
8288 let out: Vec<Value> = entries
8289 .into_iter()
8290 .map(|(k, val)| match mode {
8291 1 => val,
8292 2 => {
8293 let ks = h.new_str(k);
8294 h.new_array(vec![ks, val])
8295 }
8296 _ => h.new_str(k),
8297 })
8298 .collect();
8299 h.new_array(out)
8300 }));
8301 }
8302 }
8303 // mode 3 (`getOwnPropertyNames`) reports every own string key including the
8304 // non-enumerable ones, plus the exotic `length` an array carries.
8305 let entries: Vec<(String, Value)> = with_host(|h| {
8306 if mode == 3 {
8307 // An array's exotic `length` is already placed (after the indices,
8308 // before the ordinary string keys) by `own_key_names`.
8309 return h
8310 .own_key_names(&v, false)
8311 .into_iter()
8312 .map(|k| (k, Value::Undef))
8313 .collect();
8314 }
8315 Vec::new()
8316 });
8317 // `Object.keys` (mode 0) wants NAMES. `own_enum_entries_deep` returns
8318 // key/value pairs, so asking it for them ran every enumerable getter —
8319 // 20.1.2.17 -> 7.3.23 EnumerableOwnProperties only needs `[[GetOwnProperty]]`
8320 // for the enumerable flag, never `[[Get]]`, and a getter can throw or have
8321 // side effects:
8322 //
8323 // let n = 0; const o = { get g() { n++; return 1 } };
8324 // Object.keys(o); n // was 1, node says 0
8325 //
8326 // `values`/`entries` (modes 1 and 2) do read, and still do.
8327 let entries = match mode {
8328 3 => entries,
8329 0 => with_host(|h| h.own_enum_key_names(&v))
8330 .into_iter()
8331 .map(|k| (k, Value::Undef))
8332 .collect(),
8333 _ => host::own_enum_entries_deep(&v)?,
8334 };
8335 Ok(with_host(|h| {
8336 let out: Vec<Value> = entries
8337 .into_iter()
8338 .map(|(k, val)| match mode {
8339 0 | 3 => h.new_str(k),
8340 1 => val,
8341 _ => {
8342 let ks = h.new_str(k);
8343 h.new_array(vec![ks, val])
8344 }
8345 })
8346 .collect();
8347 h.new_array(out)
8348 }))
8349}
8350
8351fn object_assign(args: Vec<Value>) -> Result<Value, String> {
8352 let target = arg0(&args);
8353 // 20.1.2.1 step 1 is `ToObject(target)`, so a nullish TARGET throws while a
8354 // nullish SOURCE is skipped (`Object.assign({}, null)` is `{}`).
8355 require_object_coercible(&target)?;
8356 for src in args.iter().skip(1) {
8357 // `Object.assign` copies own *enumerable* properties, running any getter
8358 // — symbol-keyed ones included (7.3.25).
8359 let entries = host::own_enum_entries_deep(src)?;
8360 let syms = with_host(|h| h.own_symbol_entries(src));
8361 // A plain object target is filled in place (one borrow, then a single
8362 // re-canonicalization of the integer-index keys).
8363 let filled = with_host(|h| {
8364 if let Some(JsObj::Object(p)) = h.get_mut(&target) {
8365 for (k, v) in entries.iter().cloned().chain(syms.iter().cloned()) {
8366 p.insert(k, v);
8367 }
8368 host::canonicalize_own_keys(p);
8369 return true;
8370 }
8371 false
8372 });
8373 // Any OTHER target — an array being the common one — goes through the
8374 // ordinary Set path. The in-place branch above matched `JsObj::Object`
8375 // only, so `Object.assign([1,2], {extra:9})` silently copied NOTHING and
8376 // returned the untouched array: no error, just a missing property. The
8377 // Set path is what an `arr.extra = 9` assignment already used, so index
8378 // and non-index keys land where they do for a direct write.
8379 if !filled {
8380 for (k, v) in entries.into_iter().chain(syms) {
8381 set_property(&target, &k, v)?;
8382 }
8383 }
8384 }
8385 Ok(target)
8386}
8387
8388fn object_from_entries(args: Vec<Value>) -> Result<Value, String> {
8389 let pairs = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
8390 let mut props: IndexMap<String, Value> = IndexMap::new();
8391 for p in pairs {
8392 let kv = with_host(|h| h.iter_vec(&p)).unwrap_or_default();
8393 let key = with_host(|h| h.str_of(&kv.first().cloned().unwrap_or(Value::Undef)));
8394 let val = kv.get(1).cloned().unwrap_or(Value::Undef);
8395 props.insert(key, val);
8396 }
8397 Ok(with_host(|h| h.new_object(props)))
8398}
8399
8400/// `Object.groupBy(items, cb)` — group the iterable `items` into a null-prototype
8401/// object. Keys are `ToPropertyKey(cb(item, index))`; values are arrays of the
8402/// members mapped to that key, in first-seen key order.
8403fn object_group_by(args: Vec<Value>) -> Result<Value, String> {
8404 group_by_check_iterable(&arg0(&args), "Object.groupBy")?;
8405 let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8406 let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
8407 // Stepped, not drained: the callback runs per element, so a throwing one
8408 // stops at the first. Draining first meant an infinite source never reached
8409 // the callback at all and the call HUNG.
8410 host::iter_for_each(&arg0(&args), |item, i| {
8411 let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8412 let key = with_host(|h| h.property_key(&key_v));
8413 groups.entry(key).or_default().push(item);
8414 Ok(())
8415 })?;
8416 let props: IndexMap<String, Value> = with_host(|h| {
8417 groups
8418 .into_iter()
8419 .map(|(k, v)| (k, h.new_array(v)))
8420 .collect()
8421 });
8422 let obj = with_host(|h| h.new_object(props));
8423 // A null-prototype object (as Node returns), so it has no inherited members.
8424 with_host(|h| {
8425 let nv = h.null();
8426 h.set_proto(&obj, nv);
8427 });
8428 Ok(obj)
8429}
8430
8431/// The `groupBy` family words a non-iterable argument its OWN way — a third
8432/// vocabulary, alongside the array-literal spread's and the call spread's:
8433///
8434/// ```text
8435/// null / undefined "<Name> called on null or undefined"
8436/// anything else "<typeof> [value ]is not iterable (cannot read property
8437/// Symbol(Symbol.iterator))"
8438/// ```
8439///
8440/// A plain object, a symbol and a bigint name only their TYPE; a number, a
8441/// string and a boolean name the value too.
8442fn group_by_check_iterable(v: &Value, name: &str) -> Result<(), String> {
8443 if with_host(|h| h.is_nullish(v)) {
8444 return Err(host::type_error(&format!(
8445 "{name} called on null or undefined"
8446 )));
8447 }
8448 // Asked WITHOUT consuming anything: `iter_all` would drain the iterator
8449 // here, so the stepping loop below then saw an exhausted one — the finite
8450 // case returned an empty group and the infinite case was back to hanging.
8451 let iter_fn = get_property(v, "@@iterator").unwrap_or(Value::Undef);
8452 if with_host(|h| host::is_callable(h, &iter_fn)) {
8453 return Ok(());
8454 }
8455 Err(host::type_error(¬_iterable_typed(v)))
8456}
8457
8458/// The `<type> <value> is not iterable (cannot read property
8459/// Symbol(Symbol.iterator))` wording, which node uses wherever the source has
8460/// no name to report: a plain object, a symbol and a bigint name only their
8461/// TYPE; a number, a string and a boolean name the value too.
8462pub(crate) fn not_iterable_typed(v: &Value) -> String {
8463 let shown = with_host(|h| {
8464 let kind = h.type_of(v);
8465 match kind {
8466 "object" | "symbol" | "bigint" => kind.to_string(),
8467 "string" => format!("string \"{}\"", h.str_of(v)),
8468 _ => format!("{kind} {}", h.str_of(v)),
8469 }
8470 });
8471 format!("{shown} is not iterable (cannot read property Symbol(Symbol.iterator))")
8472}
8473
8474/// `Map.groupBy(items, cb)` — like `Object.groupBy` but returns a `Map` keyed by
8475/// the raw `cb(item, index)` value under SameValueZero (so object/any keys work).
8476fn map_group_by(args: Vec<Value>) -> Result<Value, String> {
8477 group_by_check_iterable(&arg0(&args), "Map.groupBy")?;
8478 let cb = args.get(1).cloned().unwrap_or(Value::Undef);
8479 let m = with_host(|h| {
8480 h.alloc(JsObj::Map {
8481 entries: IndexMap::new(),
8482 weak: false,
8483 })
8484 });
8485 // Stepped for the same reason `Object.groupBy` is.
8486 host::iter_for_each(&arg0(&args), |item, i| {
8487 let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
8488 let existing = map_method(&m, "get", vec![key_v.clone()])?;
8489 if matches!(existing, Value::Undef) {
8490 let arr = with_host(|h| h.new_array(vec![item]));
8491 map_method(&m, "set", vec![key_v, arr])?;
8492 } else {
8493 with_host(|h| {
8494 if let Some(JsObj::Array(a)) = h.get_mut(&existing) {
8495 a.push(item);
8496 }
8497 });
8498 }
8499 Ok(())
8500 })?;
8501 Ok(m)
8502}
8503
8504/// `Array.fromAsync(items[, mapFn])` — a Promise for an array, awaiting each
8505/// element and each `mapFn` result.
8506///
8507/// Written in JavaScript and compiled once, because the operation IS an async
8508/// function: a Rust builtin runs outside any coroutine and has no way to await,
8509/// so draining a promise from there would mean running the microtask queue by
8510/// hand. Delegating to the engine's own `async`/`for await` keeps the
8511/// suspension semantics — and the ordering they imply — exactly the language's.
8512///
8513/// The source may be an async iterable, a sync iterable, a bare iterator, or an
8514/// array-like. Everything iterable goes through `for await`, which awaits a sync
8515/// source's elements individually — that is what makes
8516/// `Array.fromAsync([1, Promise.resolve(2)])` answer `[1, 2]`. A bare `.next` is
8517/// accepted because an async generator object does not expose
8518/// `Symbol.asyncIterator` on this frontend.
8519fn array_from_async(args: Vec<Value>) -> Result<Value, String> {
8520 thread_local! {
8521 static IMPL: std::cell::RefCell<Option<Value>> = const { std::cell::RefCell::new(None) };
8522 }
8523 const SRC: &str = "(async function (items, mapFn, thisArg) {\n\
8524 const out = []; let i = 0;\n\
8525 const step = async (v) => { const a = await v; out.push(mapFn ? await mapFn.call(thisArg, a, i) : a); i++; };\n\
8526 const iterable = items != null && (typeof items[Symbol.asyncIterator] === 'function'\n\
8527 || typeof items[Symbol.iterator] === 'function' || typeof items.next === 'function');\n\
8528 if (iterable) {\n\
8529 for await (const v of items) { out.push(mapFn ? await mapFn.call(thisArg, v, i) : v); i++; }\n\
8530 return out;\n\
8531 }\n\
8532 const len = items == null ? 0 : (Math.trunc(Number(items.length)) || 0);\n\
8533 while (i < len) { await step(items[i]); }\n\
8534 return out;\n\
8535 })";
8536 let f = IMPL.with(|c| c.borrow().clone());
8537 let f = match f {
8538 Some(f) => f,
8539 None => {
8540 let f = crate::eval_in_global_scope(SRC)?;
8541 IMPL.with(|c| *c.borrow_mut() = Some(f.clone()));
8542 f
8543 }
8544 };
8545 host::invoke(&f, args, None)
8546}
8547
8548fn array_from(args: Vec<Value>) -> Result<Value, String> {
8549 // `Array.from` accepts generators and user iterables, plus array-likes with a
8550 // numeric `.length`.
8551 let src = arg0(&args);
8552 if let Some(cb) = args.get(1).cloned() {
8553 // Stepped, not drained: the mapper runs per element as the iterator
8554 // yields it (23.1.2.1 step 6.e). Materializing the whole sequence first
8555 // meant `Array.from(infiniteIterator, fn)` never reached the mapper at
8556 // all and HUNG, and a throwing mapper could not close the iterator.
8557 let this = this_arg(&args, 2);
8558 let mut out = Vec::new();
8559 let mapped = host::iter_for_each(&src, |v, i| {
8560 out.push(host::invoke(
8561 &cb,
8562 vec![v, Value::Float(i as f64)],
8563 this.clone(),
8564 )?);
8565 Ok(())
8566 });
8567 match mapped {
8568 Ok(()) => {}
8569 // An array-LIKE has no iterator; fall back to its indexed items.
8570 Err(e) if host::user_iterator_fn(&src).is_none() && e.ends_with(" is not iterable") => {
8571 out.clear();
8572 for (i, it) in array_like_items(&src).into_iter().enumerate() {
8573 out.push(host::invoke(
8574 &cb,
8575 vec![it, Value::Float(i as f64)],
8576 this.clone(),
8577 )?);
8578 }
8579 }
8580 Err(e) => return Err(e),
8581 }
8582 return construct_array_like(host::current_static_this(), out);
8583 }
8584 let items = match host::iter_all(&src) {
8585 Ok(v) => v,
8586 Err(_) => array_like_items(&src),
8587 };
8588 // 23.1.2.1 step 5: `Array.from` builds through `this`, so on a subclass the
8589 // result is an instance of it. It always allocated a plain array, which is
8590 // also why `A.from([1]).map(f) instanceof A` was false — the species chain
8591 // never started.
8592 construct_array_like(host::current_static_this(), items)
8593}
8594
8595/// Items of an array-like `{ length, 0, 1, … }` object (for `Array.from`).
8596pub(crate) fn array_like_items(src: &Value) -> Vec<Value> {
8597 // `LengthOfArrayLike` is `ToLength(Get(O, "length"))`, and `ToNumber` runs a
8598 // user `valueOf` — `Array.from({length: {valueOf: () => 1}})` was empty
8599 // because the infallible read does no `ToPrimitive`. A throw from it is
8600 // swallowed here for the same reason the `length` read is: this helper has
8601 // no way to report one, and every caller treats an unreadable length as 0.
8602 let len = get_property(src, "length")
8603 .ok()
8604 .and_then(|l| host::to_primitive(&l, "number").ok())
8605 .map(|l| with_host(|h| h.to_number(&l)))
8606 .unwrap_or(0.0);
8607 if !len.is_finite() || len <= 0.0 {
8608 return Vec::new();
8609 }
8610 (0..len as usize)
8611 .map(|i| get_property(src, &i.to_string()).unwrap_or(Value::Undef))
8612 .collect()
8613}
8614
8615// ── JSON ──────────────────────────────────────────────────────────────────────
8616
8617fn json_stringify(args: Vec<Value>) -> Result<Value, String> {
8618 // A CALLABLE second argument is the replacer function, and it is checked
8619 // before the array form (`IsCallable` precedes `IsArray` in the spec), so a
8620 // callable never also reaches the key-filter path below.
8621 let replacer = args
8622 .get(1)
8623 .filter(|r| with_host(|h| host::is_callable(h, r)))
8624 .cloned();
8625 // `toJSON` and the replacer run BEFORE serialization and are user code, so
8626 // the tree is rewritten first — outside the host borrow `json_str` holds,
8627 // and before the BigInt walk, which has no cycle guard of its own.
8628 //
8629 // The top-level value is a property of a synthetic wrapper `{ "": value }`
8630 // under key `""`, which is exactly the holder the replacer receives as
8631 // `this` on its first call.
8632 let root = arg0(&args);
8633 let wrapper = with_host(|h| {
8634 let mut m: IndexMap<String, Value> = IndexMap::new();
8635 m.insert(String::new(), root.clone());
8636 h.new_object(m)
8637 });
8638 let v = apply_to_json(&wrapper, "", &root, &mut Vec::new(), replacer.as_ref())?;
8639 // A BigInt anywhere in a serializable position is a TypeError (JSON has no
8640 // bigint form), matching Node's exact message.
8641 if with_host(|h| json_has_bigint(h, &v)) {
8642 return Err(host::type_error("Do not know how to serialize a BigInt"));
8643 }
8644 let indent = match args.get(2) {
8645 Some(Value::Float(f)) => " ".repeat((*f as usize).min(10)),
8646 Some(other) => with_host(|h| h.as_str(other)).unwrap_or_default(),
8647 None => String::new(),
8648 };
8649 // A replacer array (args[1]) restricts which object keys are serialized.
8650 let keys: Option<Vec<String>> = args.get(1).and_then(|r| {
8651 with_host(|h| match h.get(r) {
8652 Some(JsObj::Array(items)) => {
8653 Some(items.iter().map(|k| h.str_of(k)).collect::<Vec<_>>())
8654 }
8655 _ => None,
8656 })
8657 });
8658 let s = with_host(|h| json_str(h, &v, &indent, 0, keys.as_deref()));
8659 match s {
8660 Some(s) => Ok(with_host(|h| h.new_str(s))),
8661 None => Ok(Value::Undef),
8662 }
8663}
8664
8665/// One `SerializeJSONProperty(key, holder)` step: rewrite `v` (the value read
8666/// from `holder[key]`) by calling its `toJSON(key)` and then the replacer
8667/// function as `replacer.call(holder, key, value)`, then recurse into whatever
8668/// object survives. Applies to user methods, class methods, and the native
8669/// `Date`/`Buffer`/`URL` accessors alike.
8670///
8671/// Returns a fresh tree; the input is never mutated. `path` carries the chain of
8672/// objects currently being walked so a cyclic structure is reported rather than
8673/// spinning forever.
8674///
8675/// `toJSON` is called on the value ONCE and is NOT re-applied to its own result
8676/// — `{toJSON(){ return {toJSON(){ return 1 }} }}` serializes as `{}` in Node,
8677/// because the inner method is a plain (unserializable) function property of the
8678/// returned object, not a second conversion hook.
8679fn apply_to_json(
8680 holder: &Value,
8681 key: &str,
8682 v: &Value,
8683 path: &mut JsonPath,
8684 rep: Option<&Value>,
8685) -> Result<Value, String> {
8686 let mut v = v.clone();
8687 if matches!(v, Value::Obj(_)) {
8688 let tag = crate::stdlib::native_tag(&v);
8689 // 25.5.2.1 step 2: `toJSON` is looked up with `[[Get]]`, so a PROXY
8690 // supplies one through its `get` trap. `lookup_chain` walks the
8691 // property map and never asks the handler, so a proxy carrying a
8692 // `toJSON` was serialized as a plain object instead of by its own
8693 // method — and node's trap log starts with that `get`.
8694 let to_json = if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
8695 get_property(&v, "toJSON")?
8696 } else {
8697 with_host(|h| host::lookup_chain(h, &v, "toJSON")).unwrap_or(Value::Undef)
8698 };
8699 let has_to_json = with_host(|h| host::is_callable(h, &to_json))
8700 || tag
8701 .as_deref()
8702 .map(crate::stdlib::has_to_json)
8703 .unwrap_or(false);
8704 if has_to_json {
8705 let k = with_host(|h| h.new_str(key.to_string()));
8706 v = host::call_method(&v, "toJSON", vec![k])?;
8707 }
8708 }
8709 if let Some(rep) = rep {
8710 let k = with_host(|h| h.new_str(key.to_string()));
8711 v = host::invoke(rep, vec![k, v.clone()], Some(holder.clone()))?;
8712 }
8713 // How `v` was reached from its holder, as V8 names the step in a
8714 // circular-structure message: `index 1` under an array, else `property 'k'`.
8715 let via = if matches!(with_host(|h| h.get(holder).cloned()), Some(JsObj::Array(_))) {
8716 format!("index {key}")
8717 } else {
8718 format!("property '{key}'")
8719 };
8720 json_walk_children(&v, path, &via, rep)
8721}
8722
8723/// The objects `JSON.stringify` is inside of, outermost first, each with the
8724/// step that reached it from its holder (`property 'x'` / `index 1`).
8725type JsonPath = Vec<(String, Value)>;
8726
8727/// V8's `ConstructCircularStructureErrorMessage`: the cycle from the object it
8728/// starts at to the key that closes it. At most the first two and the last one
8729/// intermediate step are listed, with `| ...` standing for the rest.
8730fn circular_json_message(path: &JsonPath, start: usize, closing: &str) -> String {
8731 const PREFIX: usize = 2;
8732 const POSTFIX: usize = 1;
8733 let ctor = |v: &Value| -> String {
8734 with_host(|h| match h.get(v) {
8735 Some(JsObj::Array(_)) if h.proto_of(v).is_none() => "Array".to_string(),
8736 _ => match h.ctor_name(v) {
8737 n if n.is_empty() => "Object".to_string(),
8738 n => n,
8739 },
8740 })
8741 };
8742 let line = |i: usize| {
8743 format!(
8744 "\n | {} -> object with constructor '{}'",
8745 path[i].0,
8746 ctor(&path[i].1)
8747 )
8748 };
8749 let mut msg = format!(
8750 "Converting circular structure to JSON\n --> starting at object with constructor '{}'",
8751 ctor(&path[start].1)
8752 );
8753 let prefix_end = path.len().min(start + 1 + PREFIX);
8754 for i in start + 1..prefix_end {
8755 msg.push_str(&line(i));
8756 }
8757 if path.len() > prefix_end + POSTFIX {
8758 msg.push_str("\n | ...");
8759 }
8760 for i in prefix_end.max(path.len().saturating_sub(POSTFIX))..path.len() {
8761 msg.push_str(&line(i));
8762 }
8763 msg.push_str(&format!("\n --- {closing} closes the circle"));
8764 msg
8765}
8766
8767/// Whether a raw property key of a host object is one `json_str` serializes. The
8768/// internal slots (`@@`-prefixed symbol keys, `#`-prefixed private fields) are
8769/// invisible to JSON, so the replacer must not be invoked for them either.
8770fn json_visible_key(k: &str) -> bool {
8771 !k.starts_with("@@") && !k.starts_with('#')
8772}
8773
8774/// Recurse into the elements/properties of an already-converted value, running
8775/// `apply_to_json` for each with this value as the holder.
8776fn json_walk_children(
8777 v: &Value,
8778 path: &mut JsonPath,
8779 via: &str,
8780 rep: Option<&Value>,
8781) -> Result<Value, String> {
8782 if !matches!(v, Value::Obj(_)) {
8783 return Ok(v.clone());
8784 }
8785 // A value that contains itself has no JSON form.
8786 if let Some(start) = with_host(|h| path.iter().position(|(_, p)| h.strict_eq(p, v))) {
8787 return Err(host::type_error(&circular_json_message(path, start, via)));
8788 }
8789 // A Proxy owns no property map, so it is snapshotted through its traps into
8790 // the plain array/object `SerializeJSONArray`/`SerializeJSONObject` describe
8791 // — which read every member through `[[Get]]`, exactly as the snapshot does.
8792 if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
8793 let snap = crate::proxy::json_snapshot(v)?;
8794 path.push((via.to_string(), v.clone()));
8795 let out = json_walk_children(&snap, path, via, rep);
8796 path.pop();
8797 return out;
8798 }
8799 let obj = with_host(|h| h.get(v).cloned());
8800 path.push((via.to_string(), v.clone()));
8801 let out = (|| match obj {
8802 Some(JsObj::Array(items)) => {
8803 // Read the elements through the accessor-aware funnel: an index
8804 // with a getter must be SERIALIZED as what the getter returns, and
8805 // the backing vector still holds the stale slot.
8806 let mut resolved = items;
8807 // An index with a getter must be SERIALIZED as what the getter
8808 // returns, and it also forces a rebuild below: keeping the original
8809 // array would hand the serializer back the stale backing vector.
8810 let had_accessor = resolve_index_accessors(v, &mut resolved);
8811 let items = resolved;
8812 let mut out = Vec::with_capacity(items.len());
8813 let mut changed = had_accessor;
8814 for (i, it) in items.iter().enumerate() {
8815 let nv = apply_to_json(v, &i.to_string(), it, path, rep)?;
8816 changed |= !with_host(|h| h.strict_eq(&nv, it));
8817 out.push(nv);
8818 }
8819 // Keep identity when nothing changed, so an enclosing object is not
8820 // needlessly rebuilt (which would drop its property attributes).
8821 if changed {
8822 Ok(with_host(|h| h.new_array(out)))
8823 } else {
8824 Ok(v.clone())
8825 }
8826 }
8827 Some(JsObj::Object(props)) => {
8828 // An enumerable own accessor must have its getter RUN and the result
8829 // serialized. That cannot happen inside `json_str` (which holds the
8830 // host borrow), so materialize here — the same reason `toJSON` is
8831 // applied in this pass.
8832 let has_accessor = with_host(|h| {
8833 h.own_accessor_keys(v)
8834 .iter()
8835 .any(|k| h.prop_attrs(v, k).enumerable)
8836 });
8837 if has_accessor {
8838 let mut next: IndexMap<String, Value> = IndexMap::new();
8839 for (k, val) in host::own_enum_entries_deep(v)? {
8840 let nv = if json_visible_key(&k) {
8841 apply_to_json(v, &k, &val, path, rep)?
8842 } else {
8843 val
8844 };
8845 next.insert(k, nv);
8846 }
8847 return Ok(with_host(|h| h.new_object(next)));
8848 }
8849 // Only rebuild when a descendant actually changed, so plain data keeps
8850 // its identity (and its prototype / native tag).
8851 let mut next: IndexMap<String, Value> = IndexMap::new();
8852 let mut changed = false;
8853 for (k, val) in &props {
8854 let nv = if json_visible_key(k) {
8855 apply_to_json(v, k, val, path, rep)?
8856 } else {
8857 val.clone()
8858 };
8859 changed |= !with_host(|h| h.strict_eq(&nv, val));
8860 next.insert(k.clone(), nv);
8861 }
8862 if changed {
8863 Ok(with_host(|h| {
8864 let o = h.new_object(next);
8865 h.copy_prop_attrs(v, &o);
8866 o
8867 }))
8868 } else {
8869 Ok(v.clone())
8870 }
8871 }
8872 _ => Ok(v.clone()),
8873 })();
8874 path.pop();
8875 out
8876}
8877
8878/// Whether a value tree contains a `BigInt` in a position `JSON.stringify` would
8879/// try to serialize (a value in an array/object) — such a value throws.
8880fn json_has_bigint(h: &host::JsHost, v: &Value) -> bool {
8881 match h.get(v) {
8882 Some(JsObj::BigInt(_)) => true,
8883 Some(JsObj::Array(items)) => items.iter().any(|x| json_has_bigint(h, x)),
8884 Some(JsObj::Object(props)) => props
8885 .iter()
8886 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
8887 .any(|(_, val)| json_has_bigint(h, val)),
8888 _ => false,
8889 }
8890}
8891
8892fn json_str(
8893 h: &host::JsHost,
8894 v: &Value,
8895 indent: &str,
8896 depth: usize,
8897 keys: Option<&[String]>,
8898) -> Option<String> {
8899 let sep = if indent.is_empty() { ":" } else { ": " };
8900 match v {
8901 Value::Undef => None,
8902 Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
8903 Value::Int(n) => Some(n.to_string()),
8904 Value::Float(f) => Some(if f.is_finite() {
8905 host::fmt_number(*f)
8906 } else {
8907 "null".into()
8908 }),
8909 Value::Str(s) => Some(json_quote(s)),
8910 Value::Obj(_) => match h.get(v) {
8911 Some(JsObj::Str(s)) => Some(json_quote(s)),
8912 Some(JsObj::Null) => Some("null".into()),
8913 // A `JSON.rawJSON` marker contributes its text VERBATIM — that is the
8914 // whole point of it, and it is why a number wider than a `double`
8915 // can survive a round trip.
8916 _ if h.fn_prop(v, "@@rawJSON").is_some() => match h.get(v) {
8917 Some(JsObj::Object(p)) => p.get("rawJSON").map(|r| h.str_of(r)),
8918 _ => None,
8919 },
8920 // A Map/Set has no ENTRIES to serialize (they are internal slots),
8921 // but any own property a script attached is serialized like an
8922 // ordinary object's: `JSON.stringify(Object.assign(new Map(), {a:1}))`
8923 // is `{"a":1}`.
8924 Some(JsObj::Map { .. })
8925 | Some(JsObj::Set { .. })
8926 | Some(JsObj::RegExp(_))
8927 // A Promise and a generator are ORDINARY objects to the serializer:
8928 // their state is internal slots, so they contribute no entries and
8929 // render as `{}`. They were being omitted entirely instead, so a
8930 // promise in an array became `null` and one in an object vanished.
8931 | Some(JsObj::Promise { .. })
8932 | Some(JsObj::Generator { .. }) => {
8933 let parts: Vec<String> = h
8934 .own_enum_entries(v)
8935 .into_iter()
8936 .filter(|(k, _)| !k.starts_with("@@") && !host::is_symbol_key(k))
8937 .filter_map(|(k, val)| {
8938 json_str(h, &val, indent, depth + 1, keys)
8939 .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8940 })
8941 .collect();
8942 Some(wrap(&parts, "{", "}", indent, depth))
8943 }
8944 // A NON-callable builtin is a namespace object, not a function, so
8945 // it serializes as one: `JSON.stringify(Math)` is `{}` (its members
8946 // are all non-enumerable), where omitting it made the whole property
8947 // disappear from its holder.
8948 Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
8949 let parts: Vec<String> = crate::stdlib::namespace_keys(n)
8950 .into_iter()
8951 .filter_map(|k| {
8952 let val = h.builtin_static(n, &k)?;
8953 json_str(h, &val, indent, depth + 1, keys)
8954 .map(|s| format!("{}{sep}{s}", json_quote(&k)))
8955 })
8956 .collect();
8957 Some(wrap(&parts, "{", "}", indent, depth))
8958 }
8959 // Functions and symbols are omitted (undefined) as values.
8960 Some(JsObj::Func(_))
8961 | Some(JsObj::Builtin(_))
8962 | Some(JsObj::BoundMethod { .. })
8963 | Some(JsObj::BoundFunc { .. })
8964 | Some(JsObj::Class(_))
8965 | Some(JsObj::Symbol { .. }) => None,
8966 Some(JsObj::Array(items)) => {
8967 if items.is_empty() {
8968 return Some("[]".into());
8969 }
8970 let parts: Vec<String> = items
8971 .iter()
8972 .map(|x| {
8973 json_str(h, x, indent, depth + 1, keys).unwrap_or_else(|| "null".into())
8974 })
8975 .collect();
8976 Some(wrap(&parts, "[", "]", indent, depth))
8977 }
8978 Some(JsObj::Object(props)) if props.contains_key("@@primitive") => {
8979 // 25.5.2.2 step 4: a String/Number/Boolean wrapper serializes as
8980 // the primitive it boxes, not as the object holding it —
8981 // `JSON.stringify(new Number(1))` is `1`, not `{}`.
8982 json_str(h, &props["@@primitive"].clone(), indent, depth, keys)
8983 }
8984 Some(JsObj::Object(props)) => {
8985 // A replacer array restricts (and orders) which keys are emitted.
8986 let parts: Vec<String> = match keys {
8987 Some(allow) => allow
8988 .iter()
8989 .filter_map(|k| {
8990 props.get(k).and_then(|val| {
8991 json_str(h, val, indent, depth + 1, keys)
8992 .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
8993 })
8994 })
8995 .collect(),
8996 None => h
8997 .own_enum_entries(v)
8998 .iter()
8999 .filter_map(|(k, val)| {
9000 json_str(h, val, indent, depth + 1, keys)
9001 .map(|vs| format!("{}{sep}{vs}", json_quote(k)))
9002 })
9003 .collect(),
9004 };
9005 if parts.is_empty() {
9006 return Some("{}".into());
9007 }
9008 Some(wrap(&parts, "{", "}", indent, depth))
9009 }
9010 _ => Some("null".into()),
9011 },
9012 _ => Some("null".into()),
9013 }
9014}
9015
9016fn wrap(parts: &[String], open: &str, close: &str, indent: &str, depth: usize) -> String {
9017 if indent.is_empty() {
9018 format!("{open}{}{close}", parts.join(","))
9019 } else {
9020 let pad = indent.repeat(depth + 1);
9021 let pad_close = indent.repeat(depth);
9022 format!(
9023 "{open}\n{pad}{}\n{pad_close}{close}",
9024 parts.join(&format!(",\n{pad}"))
9025 )
9026 }
9027}
9028
9029fn json_quote(s: &str) -> String {
9030 let mut out = String::from("\"");
9031 for c in s.chars() {
9032 match c {
9033 '"' => out.push_str("\\\""),
9034 '\\' => out.push_str("\\\\"),
9035 '\n' => out.push_str("\\n"),
9036 '\t' => out.push_str("\\t"),
9037 '\r' => out.push_str("\\r"),
9038 // QuoteJSONString (25.5.2.2) names SIX short escapes, not four.
9039 // Backspace and form feed were missing, so they fell through to the
9040 // `\uXXXX` arm below and `JSON.stringify("\b")` produced
9041 // `""` where node produces `"\b"`. Both parse back to the same
9042 // string, so the difference is invisible to a round trip and shows
9043 // up only as a byte mismatch against a fixture or a checksum.
9044 '\u{8}' => out.push_str("\\b"),
9045 '\u{c}' => out.push_str("\\f"),
9046 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
9047 _ => out.push(c),
9048 }
9049 }
9050 out.push('"');
9051 out
9052}
9053
9054fn json_parse(args: Vec<Value>) -> Result<Value, String> {
9055 let s = with_host(|h| h.str_of(&arg0(&args)));
9056 let mut p = JsonParser {
9057 chars: s.chars().collect(),
9058 pos: 0,
9059 prims: Vec::new(),
9060 record: args
9061 .get(1)
9062 .is_some_and(|r| with_host(|h| host::is_callable(h, r))),
9063 };
9064 p.skip_ws();
9065 if p.peek().is_none() {
9066 return Err("SyntaxError: Unexpected end of JSON input".into());
9067 }
9068 let v = p.parse_value()?;
9069 let value_end = p.pos;
9070 p.skip_ws();
9071 // Anything after the top-level value is an error — the parser used to accept
9072 // and silently discard it, so `JSON.parse('{"a":1}x')` succeeded.
9073 if let Some(c) = p.peek() {
9074 // V8 names the token kind only when it butts directly against the value
9075 // (`01` -> "Unexpected number at position 1"); with whitespace between
9076 // it is just a non-whitespace character (`1 2`).
9077 // Only a digit butted directly against a completed number literal —
9078 // V8's number scanner is still in number context there. `5"x"` and
9079 // `[0,1]0` exit the scanner cleanly and get the generic message.
9080 let after_number = value_end > 0
9081 && p.pos == value_end
9082 && p.chars[value_end - 1].is_ascii_digit()
9083 && c.is_ascii_digit();
9084 return Err(if after_number {
9085 p.err_at("Unexpected number", p.pos)
9086 } else {
9087 p.err_trailing(p.pos)
9088 });
9089 }
9090 // Optional reviver: walk bottom-up, transforming each (key, value).
9091 if let Some(reviver) = args
9092 .get(1)
9093 .filter(|r| with_host(|h| host::is_callable(h, r)))
9094 .cloned()
9095 {
9096 // The top-level holder is a fresh `{ "": value }` wrapper, as the spec
9097 // constructs before the walk.
9098 let root = with_host(|h| {
9099 let mut m: IndexMap<String, Value> = IndexMap::new();
9100 m.insert(String::new(), v.clone());
9101 h.new_object(m)
9102 });
9103 return json_revive("", v, &reviver, &root, &p.prims, &mut 0);
9104 }
9105 Ok(v)
9106}
9107
9108/// `JSON.parse` reviver walk: recurse into children first, then call
9109/// `reviver(key, value)`; a returned `undefined` drops the property.
9110///
9111/// The reviver runs with the HOLDER as `this` (25.5.1.1
9112/// InternalizeJSONProperty) — the object or array the key lives in, and at the
9113/// top level a wrapper `{ "": value }`. It was being called with no receiver,
9114/// so `this` was undefined and a reviver could not reach its siblings.
9115/// `JSON.rawJSON(text)` — a marker object whose text `JSON.stringify` emits
9116/// VERBATIM, so a number too large for a `double` survives a round trip
9117/// (`JSON.stringify({n: JSON.rawJSON("12345678901234567890")})`).
9118///
9119/// The validation is not "does `JSON.parse` accept it": node's rule, measured
9120/// across the whole matrix, is
9121///
9122/// ```text
9123/// "" -> SyntaxError: Invalid value for JSON.rawJSON
9124/// leading whitespace -> the parse error for that first character
9125/// a complete literal -> ok
9126/// anything left over -> SyntaxError: Invalid value for JSON.rawJSON
9127/// a broken literal -> the parse error the scanner raised
9128/// ```
9129///
9130/// so `" 1"` reports an unexpected token while `"1 "` and `"1,2"` report the
9131/// invalid-value message even though `JSON.parse` accepts the former and gives
9132/// a token error for the latter.
9133fn json_raw(args: Vec<Value>) -> Result<Value, String> {
9134 const INVALID: &str = "SyntaxError: Invalid value for JSON.rawJSON";
9135 let s = with_host(|h| h.str_of(&arg0(&args)));
9136 if s.is_empty() {
9137 return Err(INVALID.into());
9138 }
9139 let mut p = JsonParser {
9140 chars: s.chars().collect(),
9141 pos: 0,
9142 prims: Vec::new(),
9143 record: false,
9144 };
9145 // An object or an array is rejected where it starts, as leading whitespace
9146 // is — both are "not a primitive", but node reports the token.
9147 if matches!(p.peek(), Some('{') | Some('[')) || p.peek().is_some_and(|c| c.is_whitespace()) {
9148 return Err(p.err_token(0));
9149 }
9150 p.parse_value()?;
9151 if p.pos != p.chars.len() {
9152 // A digit butted against a completed number is still in the number
9153 // scanner, so `"01"` reports the scanner's error rather than leftover
9154 // input — the same distinction `json_parse` draws for trailing text.
9155 if p.chars[p.pos - 1].is_ascii_digit() && p.chars[p.pos].is_ascii_digit() {
9156 return Err(p.err_at("Unexpected number", p.pos));
9157 }
9158 return Err(INVALID.into());
9159 }
9160 // A null prototype and one own `rawJSON` property, frozen — the brand is a
9161 // hidden slot so `Object.keys` stays `["rawJSON"]`.
9162 Ok(with_host(|h| {
9163 let mut m: IndexMap<String, Value> = IndexMap::new();
9164 let text = h.new_str(s);
9165 m.insert("rawJSON".into(), text);
9166 let o = h.new_object(m);
9167 let null = h.null();
9168 h.set_proto(&o, null);
9169 h.set_fn_prop(&o, "@@rawJSON", Value::Bool(true));
9170 h.seal_object(&o, true);
9171 o
9172 }))
9173}
9174
9175/// `JSON.isRawJSON(v)` — the brand check. A hand-built `{ rawJSON: "1" }` is
9176/// NOT one, which is why the marker is a hidden slot rather than the property.
9177fn json_is_raw(args: Vec<Value>) -> Result<Value, String> {
9178 Ok(Value::Bool(is_raw_json(&arg0(&args))))
9179}
9180
9181fn is_raw_json(v: &Value) -> bool {
9182 with_host(|h| h.fn_prop(v, "@@rawJSON")).is_some()
9183}
9184
9185fn json_revive(
9186 key: &str,
9187 val: Value,
9188 reviver: &Value,
9189 holder: &Value,
9190 prims: &[String],
9191 next: &mut usize,
9192) -> Result<Value, String> {
9193 // A PRIMITIVE claims the next recorded source slice before its children
9194 // would — it has none — and a container claims nothing. The walk descends in
9195 // the same order the parse produced them, so one cursor lines the two up.
9196 let is_container =
9197 with_host(|h| matches!(h.get(&val), Some(JsObj::Array(_)) | Some(JsObj::Object(_))));
9198 let source = if !is_container {
9199 let s = prims.get(*next).cloned();
9200 if s.is_some() {
9201 *next += 1;
9202 }
9203 s
9204 } else {
9205 None
9206 };
9207 match with_host(|h| h.get(&val).cloned()) {
9208 Some(JsObj::Array(items)) => {
9209 for i in 0..items.len() {
9210 let elem = with_host(|h| match h.get(&val) {
9211 Some(JsObj::Array(it)) => it[i].clone(),
9212 _ => Value::Undef,
9213 });
9214 let nv = json_revive(&i.to_string(), elem, reviver, &val, prims, next)?;
9215 with_host(|h| {
9216 if let Some(JsObj::Array(it)) = h.get_mut(&val) {
9217 it[i] = nv;
9218 }
9219 });
9220 }
9221 }
9222 Some(JsObj::Object(props)) => {
9223 let keys: Vec<String> = props
9224 .keys()
9225 .filter(|k| !k.starts_with("@@"))
9226 .cloned()
9227 .collect();
9228 for k in keys {
9229 let elem = with_host(|h| match h.get(&val) {
9230 Some(JsObj::Object(p)) => p.get(&k).cloned().unwrap_or(Value::Undef),
9231 _ => Value::Undef,
9232 });
9233 let nv = json_revive(&k, elem, reviver, &val, prims, next)?;
9234 with_host(|h| {
9235 if let Some(JsObj::Object(p)) = h.get_mut(&val) {
9236 if matches!(nv, Value::Undef) {
9237 p.shift_remove(&k);
9238 } else {
9239 p.insert(k.clone(), nv);
9240 }
9241 }
9242 });
9243 }
9244 }
9245 _ => {}
9246 }
9247 let kv = with_host(|h| h.new_str(key.to_string()));
9248 // 25.5.1.1 step 2.b: the reviver's THIRD argument. `{ source }` for a
9249 // primitive, an empty object for an array or an object — node passes it
9250 // either way, and code reading `ctx.source` used to die on `undefined`
9251 // because only two arguments were passed.
9252 let ctx = with_host(|h| {
9253 let mut m: IndexMap<String, Value> = IndexMap::new();
9254 if let Some(s) = source {
9255 let sv = h.new_str(s);
9256 m.insert("source".into(), sv);
9257 }
9258 h.new_object(m)
9259 });
9260 host::invoke(reviver, vec![kv, val, ctx], Some(holder.clone()))
9261}
9262
9263struct JsonParser {
9264 chars: Vec<char>,
9265 pos: usize,
9266 /// Source text of each PRIMITIVE value, in parse order — what the reviver's
9267 /// third argument reports as `context.source` (25.5.1.1). Only collected
9268 /// when a reviver was supplied.
9269 ///
9270 /// A flat list rather than a parallel tree because the reviver walk visits
9271 /// primitives in the same depth-first order the parse produced them, so an
9272 /// index into this is enough to line them up.
9273 prims: Vec<String>,
9274 record: bool,
9275}
9276impl JsonParser {
9277 fn peek(&self) -> Option<char> {
9278 self.chars.get(self.pos).copied()
9279 }
9280
9281 /// `at position N (line L column C)` — the location suffix V8 appends to the
9282 /// positional JSON parse errors. Positions are in UTF-16-ish code units;
9283 /// node-js counts `char`s, which agree for the BMP.
9284 fn at(&self, pos: usize) -> String {
9285 let mut line = 1usize;
9286 let mut col = 1usize;
9287 for c in &self.chars[..pos.min(self.chars.len())] {
9288 if *c == '\n' {
9289 line += 1;
9290 col = 1;
9291 } else {
9292 col += 1;
9293 }
9294 }
9295 format!(" at position {pos} (line {line} column {col})")
9296 }
9297
9298 /// A positional error (`Expected ':' after property name in JSON at …`).
9299 fn err_at(&self, what: &str, pos: usize) -> String {
9300 format!("SyntaxError: {what} in JSON{}", self.at(pos))
9301 }
9302
9303 /// The one positional message V8 does NOT suffix with `in JSON`.
9304 fn err_trailing(&self, pos: usize) -> String {
9305 format!(
9306 "SyntaxError: Unexpected non-whitespace character after JSON{}",
9307 self.at(pos)
9308 )
9309 }
9310
9311 /// V8's default parse error: the offending character plus a window of the
9312 /// source. The whole input is quoted when it is short (<= 20 chars);
9313 /// otherwise a 10-character context window either side of `pos` is shown,
9314 /// elided with `...` on whichever side was cut.
9315 fn err_token(&self, pos: usize) -> String {
9316 const MAX_WHOLE: usize = 20;
9317 const CONTEXT: usize = 10;
9318 let len = self.chars.len();
9319 let Some(c) = self.chars.get(pos) else {
9320 return "SyntaxError: Unexpected end of JSON input".into();
9321 };
9322 // V8 reports the whole input for the JS literals that are famously not
9323 // JSON, without naming an offending character.
9324 let whole: String = self.chars.iter().collect();
9325 if matches!(
9326 whole.as_str(),
9327 "undefined" | "NaN" | "Infinity" | "-Infinity"
9328 ) {
9329 return format!("SyntaxError: \"{whole}\" is not valid JSON");
9330 }
9331 let snippet = if len <= MAX_WHOLE {
9332 format!("\"{whole}\"")
9333 } else {
9334 let start = pos.saturating_sub(CONTEXT);
9335 let end = (pos + CONTEXT).min(len);
9336 let body: String = self.chars[start..end].iter().collect();
9337 let head = if start > 0 { "..." } else { "" };
9338 let tail = if end < len { "..." } else { "" };
9339 format!("{head}\"{body}\"{tail}")
9340 };
9341 format!("SyntaxError: Unexpected token '{c}', {snippet} is not valid JSON")
9342 }
9343
9344 fn skip_ws(&mut self) {
9345 while matches!(
9346 self.peek(),
9347 Some(' ') | Some('\n') | Some('\t') | Some('\r')
9348 ) {
9349 self.pos += 1;
9350 }
9351 }
9352 fn parse_value(&mut self) -> Result<Value, String> {
9353 self.skip_ws();
9354 let start = self.pos;
9355 let prim = matches!(self.peek(), Some(c) if c != '{' && c != '[');
9356 let v = match self.peek() {
9357 Some('{') => self.parse_object(),
9358 Some('[') => self.parse_array(),
9359 Some('"') => {
9360 let s = self.parse_string()?;
9361 Ok(with_host(|h| h.new_str(s)))
9362 }
9363 Some('t') | Some('f') => self.parse_bool(),
9364 Some('n') => {
9365 self.expect_lit("null")?;
9366 Ok(with_host(|h| h.null()))
9367 }
9368 Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
9369 None => Err("SyntaxError: Unexpected end of JSON input".into()),
9370 _ => Err(self.err_token(self.pos)),
9371 }?;
9372 if prim && self.record {
9373 self.prims
9374 .push(self.chars[start..self.pos].iter().collect());
9375 }
9376 Ok(v)
9377 }
9378 fn expect_lit(&mut self, lit: &str) -> Result<(), String> {
9379 for ch in lit.chars() {
9380 match self.peek() {
9381 Some(c) if c == ch => self.pos += 1,
9382 // V8 reports the first character that broke the literal, which is
9383 // why `foo` complains about `'o'` (index 2) and not `'f'`.
9384 None => return Err("SyntaxError: Unexpected end of JSON input".into()),
9385 _ => return Err(self.err_token(self.pos)),
9386 }
9387 }
9388 Ok(())
9389 }
9390 fn parse_bool(&mut self) -> Result<Value, String> {
9391 if self.peek() == Some('t') {
9392 self.expect_lit("true")?;
9393 Ok(Value::Bool(true))
9394 } else {
9395 self.expect_lit("false")?;
9396 Ok(Value::Bool(false))
9397 }
9398 }
9399 /// JSON's number grammar: `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?`.
9400 /// A leading zero does NOT swallow the following digits — `01` parses as `0`
9401 /// and the stray `1` becomes a trailing-token error, which is how V8 reports
9402 /// it. Each way the grammar can run out has its own message.
9403 fn parse_number(&mut self) -> Result<Value, String> {
9404 let start = self.pos;
9405 if self.peek() == Some('-') {
9406 self.pos += 1;
9407 if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9408 return Err(self.err_at("No number after minus sign", self.pos));
9409 }
9410 }
9411 if self.peek() == Some('0') {
9412 self.pos += 1;
9413 } else {
9414 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9415 self.pos += 1;
9416 }
9417 }
9418 if self.peek() == Some('.') {
9419 self.pos += 1;
9420 if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9421 return Err(self.err_at("Unterminated fractional number", self.pos));
9422 }
9423 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9424 self.pos += 1;
9425 }
9426 }
9427 if matches!(self.peek(), Some('e') | Some('E')) {
9428 self.pos += 1;
9429 if matches!(self.peek(), Some('+') | Some('-')) {
9430 self.pos += 1;
9431 }
9432 if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9433 return Err(self.err_at("Exponent part is missing a number", self.pos));
9434 }
9435 while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
9436 self.pos += 1;
9437 }
9438 }
9439 let s: String = self.chars[start..self.pos].iter().collect();
9440 s.parse::<f64>()
9441 .map(Value::Float)
9442 .map_err(|_| self.err_at("Unexpected number", start))
9443 }
9444 fn parse_string(&mut self) -> Result<String, String> {
9445 self.pos += 1; // opening quote
9446 let mut out = String::new();
9447 loop {
9448 match self.peek() {
9449 None => return Err(self.err_at("Unterminated string", self.pos)),
9450 Some('"') => {
9451 self.pos += 1;
9452 break;
9453 }
9454 Some('\\') => {
9455 self.pos += 1;
9456 match self.peek() {
9457 Some('n') => out.push('\n'),
9458 Some('t') => out.push('\t'),
9459 Some('r') => out.push('\r'),
9460 Some('"') => out.push('"'),
9461 Some('\\') => out.push('\\'),
9462 Some('/') => out.push('/'),
9463 Some('b') => out.push('\u{08}'),
9464 Some('f') => out.push('\u{0C}'),
9465 Some('u') => {
9466 let h: String = self.chars
9467 [self.pos + 1..(self.pos + 5).min(self.chars.len())]
9468 .iter()
9469 .collect();
9470 if let Ok(n) = u32::from_str_radix(&h, 16) {
9471 if let Some(ch) = char::from_u32(n) {
9472 out.push(ch);
9473 }
9474 }
9475 self.pos += 4;
9476 }
9477 _ => {}
9478 }
9479 self.pos += 1;
9480 }
9481 // A raw control character is not legal inside a JSON string; it
9482 // has to be escaped. V8 rejects it rather than passing it through.
9483 Some(c) if (c as u32) < 0x20 => {
9484 return Err(self.err_at("Bad control character in string literal", self.pos))
9485 }
9486 Some(c) => {
9487 out.push(c);
9488 self.pos += 1;
9489 }
9490 }
9491 }
9492 Ok(out)
9493 }
9494 fn parse_array(&mut self) -> Result<Value, String> {
9495 self.pos += 1; // [
9496 let mut items = Vec::new();
9497 self.skip_ws();
9498 if self.peek() == Some(']') {
9499 self.pos += 1;
9500 return Ok(with_host(|h| h.new_array(items)));
9501 }
9502 loop {
9503 items.push(self.parse_value()?);
9504 self.skip_ws();
9505 match self.peek() {
9506 Some(',') => {
9507 self.pos += 1;
9508 }
9509 Some(']') => {
9510 self.pos += 1;
9511 break;
9512 }
9513 _ => return Err(self.err_at("Expected ',' or ']' after array element", self.pos)),
9514 }
9515 }
9516 Ok(with_host(|h| h.new_array(items)))
9517 }
9518 fn parse_object(&mut self) -> Result<Value, String> {
9519 self.pos += 1; // {
9520 let mut props: IndexMap<String, Value> = IndexMap::new();
9521 self.skip_ws();
9522 if self.peek() == Some('}') {
9523 self.pos += 1;
9524 return Ok(with_host(|h| h.new_object(props)));
9525 }
9526 loop {
9527 self.skip_ws();
9528 if self.peek() != Some('"') {
9529 // The first key uses the "or '}'" wording (an empty object is
9530 // still legal there); a key after a comma does not. End of input
9531 // reports the same expectation, at the end position.
9532 return Err(if props.is_empty() {
9533 self.err_at("Expected property name or '}'", self.pos)
9534 } else {
9535 self.err_at("Expected double-quoted property name", self.pos)
9536 });
9537 }
9538 let key = self.parse_string()?;
9539 self.skip_ws();
9540 if self.peek() != Some(':') {
9541 return Err(match self.peek() {
9542 None => "SyntaxError: Unexpected end of JSON input".into(),
9543 _ => self.err_at("Expected ':' after property name", self.pos),
9544 });
9545 }
9546 self.pos += 1;
9547 let val = self.parse_value()?;
9548 props.insert(key, val);
9549 self.skip_ws();
9550 match self.peek() {
9551 Some(',') => {
9552 self.pos += 1;
9553 }
9554 Some('}') => {
9555 self.pos += 1;
9556 break;
9557 }
9558 _ => return Err(self.err_at("Expected ',' or '}' after property value", self.pos)),
9559 }
9560 }
9561 Ok(with_host(|h| h.new_object(props)))
9562 }
9563}
9564
9565// ══ type methods (array / string / number) ═══════════════════════════════════
9566
9567fn is_array_method(name: &str) -> bool {
9568 matches!(
9569 name,
9570 "push"
9571 | "pop"
9572 | "shift"
9573 | "unshift"
9574 | "map"
9575 | "filter"
9576 | "forEach"
9577 | "join"
9578 | "slice"
9579 | "indexOf"
9580 | "lastIndexOf"
9581 | "includes"
9582 | "reduce"
9583 | "concat"
9584 | "reverse"
9585 | "sort"
9586 | "find"
9587 | "findIndex"
9588 | "some"
9589 | "every"
9590 | "flat"
9591 | "fill"
9592 | "splice"
9593 | "keys"
9594 | "values"
9595 | "entries"
9596 | "flatMap"
9597 | "at"
9598 | "toString"
9599 | "reduceRight"
9600 | "findLast"
9601 | "findLastIndex"
9602 | "copyWithin"
9603 )
9604}
9605/// Every `String.prototype` method node-js implements.
9606///
9607/// A LIST rather than a `matches!` arm because the same set has to be installed
9608/// on the real `String.prototype` object: a method read off the prototype
9609/// (`String.prototype.trim.call(s)`, the generic-borrowing idiom libraries use)
9610/// found nothing there, so the two views of "which methods exist" would drift
9611/// if they were written twice.
9612pub(crate) const STRING_PROTO_METHODS: &[&str] = &[
9613 "toUpperCase",
9614 "toLowerCase",
9615 "charAt",
9616 "charCodeAt",
9617 "codePointAt",
9618 "indexOf",
9619 "lastIndexOf",
9620 "includes",
9621 "slice",
9622 "substring",
9623 "substr",
9624 "split",
9625 "trim",
9626 "trimStart",
9627 "trimEnd",
9628 "replace",
9629 "replaceAll",
9630 "repeat",
9631 "startsWith",
9632 "endsWith",
9633 "padStart",
9634 "padEnd",
9635 "concat",
9636 "at",
9637 "toString",
9638 "toLocaleString",
9639 "valueOf",
9640 "match",
9641 "matchAll",
9642 "search",
9643 "normalize",
9644 "localeCompare",
9645 "toLocaleUpperCase",
9646 "toLocaleLowerCase",
9647 "isWellFormed",
9648 "toWellFormed",
9649];
9650
9651fn is_string_method(name: &str) -> bool {
9652 STRING_PROTO_METHODS.contains(&name)
9653}
9654
9655/// Every SYMBOL-keyed intrinsic method the generated table lists for `ctor`,
9656/// spelled the way this frontend spells the key (`@@iterator`).
9657///
9658/// A prototype built as a REAL object (`String.prototype`, `URLSearchParams
9659/// .prototype`) installs its methods from a list, and only the string-keyed
9660/// list was walked — so `String.prototype[Symbol.iterator]` read `undefined`
9661/// while `Array.prototype[Symbol.iterator]`, which resolves through the
9662/// `Builtin` namespace and its table gate, answered a function. Derived from
9663/// the table rather than written out, so it cannot name a method node does not
9664/// define nor miss one it does.
9665pub(crate) fn proto_symbol_methods(ctor: &str) -> Vec<&'static str> {
9666 let prefix = format!("@proto:{ctor}:");
9667 crate::arity::BUILTIN_ARITY
9668 .iter()
9669 .filter_map(|(k, _, _)| k.strip_prefix(prefix.as_str()))
9670 .filter(|m| m.starts_with("@@"))
9671 .collect()
9672}
9673
9674/// The builtin constructors whose `.prototype` object is BRANDED — every other
9675/// `<C>.prototype` is an ordinary object and reports `[object Object]`.
9676///
9677/// Measured on node v26.8.1 over every constructor this frontend knows:
9678///
9679/// ```text
9680/// Array/Object/Number/String/Boolean/Function the ES5 legacy slot prototypes
9681/// Symbol/BigInt/Map/Set/WeakMap/WeakSet carry an own @@toStringTag
9682/// Promise/Iterator/ArrayBuffer/DataView "
9683/// WeakRef/FinalizationRegistry/URL "
9684/// URLSearchParams/TextEncoder/TextDecoder "
9685/// Date/RegExp/Error/TypeError/Uint8Array/… [object Object]
9686/// ```
9687///
9688/// The rule this replaces branded EVERY `<C>.prototype` as `C`, so
9689/// `Object.prototype.toString.call(Date.prototype)` read `[object Date]` — and
9690/// a `Date.prototype.toString` call on a plain object named `[object Date]` in
9691/// its own failure message where node names `[object Object]`.
9692pub(crate) const BRANDED_PROTOS: &[&str] = &[
9693 "Array",
9694 "ArrayBuffer",
9695 "BigInt",
9696 "Boolean",
9697 "DataView",
9698 "FinalizationRegistry",
9699 "Function",
9700 "Iterator",
9701 "Map",
9702 "Number",
9703 "Object",
9704 "Promise",
9705 "Set",
9706 "SharedArrayBuffer",
9707 "String",
9708 "Symbol",
9709 "TextDecoder",
9710 "TextEncoder",
9711 "URL",
9712 "URLSearchParams",
9713 "WeakMap",
9714 "WeakRef",
9715 "WeakSet",
9716];
9717
9718/// Whether `v` is a `RegExp` value (drives the regex path of `match`/`replace`/…).
9719/// A user `Symbol.match`/`replace`/`search`/`split`/`matchAll` method on the
9720/// ARGUMENT, which the string method must delegate to (22.1.3.x step 2).
9721///
9722/// `"abc".match(o)` where `o` defines `Symbol.match` calls that method rather
9723/// than coercing `o` to a pattern — the protocol every regexp-like library
9724/// implements. None of the five were consulted, so a custom matcher was
9725/// silently stringified instead.
9726fn symbol_protocol(arg: &Value, sym: &str) -> Option<Value> {
9727 if matches!(arg, Value::Undef) || with_host(|h| h.is_null(arg)) {
9728 return None;
9729 }
9730 let f = get_property(arg, sym).ok()?;
9731 with_host(|h| host::is_callable(h, &f)).then_some(f)
9732}
9733
9734fn is_regexp_arg(v: &Value) -> bool {
9735 // 7.2.8 `IsRegExp` asks `Symbol.match` FIRST, so an object can declare
9736 // itself a regexp — or a real one can disown the label. Only the heap kind
9737 // was checked, so `"a".startsWith({[Symbol.match]: true})` did not throw
9738 // the TypeError the spec requires.
9739 if let Ok(m) = get_property(v, "@@match") {
9740 if !matches!(m, Value::Undef) {
9741 return with_host(|h| h.truthy(&m));
9742 }
9743 }
9744 with_host(|h| h.kind_of(v)) == Some(ObjKind::RegExp)
9745}
9746
9747/// `str.replace(strPattern, fn)` — a function replacer against a literal (string)
9748/// pattern: replace the first (or all) occurrence, calling `fn(match, offset, s)`.
9749fn replace_str_fn(s: &str, pat: &str, repl: &Value, all: bool) -> Result<String, String> {
9750 if pat.is_empty() {
9751 return Ok(s.to_string());
9752 }
9753 let mut out = String::new();
9754 let mut rest = s;
9755 let mut base = 0usize;
9756 while let Some(pos) = rest.find(pat) {
9757 out.push_str(&rest[..pos]);
9758 let offset = base + pos;
9759 let m = with_host(|h| h.new_str(pat.to_string()));
9760 let str_arg = with_host(|h| h.new_str(s.to_string()));
9761 let r = host::invoke(repl, vec![m, Value::Float(offset as f64), str_arg], None)?;
9762 out.push_str(&with_host(|h| h.str_of(&r)));
9763 let consumed = pos + pat.len();
9764 base += consumed;
9765 rest = &rest[consumed..];
9766 if !all {
9767 break;
9768 }
9769 }
9770 out.push_str(rest);
9771 Ok(out)
9772}
9773/// Every `Number.prototype` method node-js implements — a list for the same
9774/// reason [`STRING_PROTO_METHODS`] is one.
9775pub(crate) const NUMBER_PROTO_METHODS: &[&str] = &[
9776 "toFixed",
9777 "toExponential",
9778 "toString",
9779 "toPrecision",
9780 "toLocaleString",
9781 "valueOf",
9782];
9783
9784fn is_number_method(name: &str) -> bool {
9785 NUMBER_PROTO_METHODS.contains(&name)
9786}
9787
9788/// The exotic kinds whose own dispatch table does NOT already reach the
9789/// `Object.prototype` methods, so the inherited ones have to be routed to.
9790///
9791/// An allowlist rather than a catch-all: a primitive receiver also reaches this
9792/// function, and a Number's `toString` is `Number.prototype.toString` — routing
9793/// it to the object form made `(255).toString(16)` report `[object Number]`.
9794fn inherits_object_methods(recv: &Value) -> bool {
9795 matches!(
9796 with_host(|h| h.kind_of(recv)),
9797 Some(
9798 ObjKind::Map
9799 | ObjKind::Set
9800 | ObjKind::Promise
9801 | ObjKind::RegExp
9802 | ObjKind::Generator
9803 | ObjKind::Symbol
9804 | ObjKind::BigInt
9805 | ObjKind::Iter
9806 )
9807 )
9808}
9809
9810/// Whether `recv`'s own prototype defines `name`, shadowing the
9811/// `Object.prototype` method of that name — `RegExp.prototype.toString` does,
9812/// `Map.prototype` does not.
9813fn overrides_object_method(recv: &Value, name: &str) -> bool {
9814 match with_host(|h| h.kind_of(recv)) {
9815 Some(ObjKind::Map) => is_map_method(name),
9816 Some(ObjKind::Set) => is_set_method(name),
9817 Some(ObjKind::RegExp) => crate::regexp::is_regexp_method(name),
9818 // A Symbol has its own `toString`; `valueOf` is the inherited one,
9819 // which returns the receiver — exactly what a symbol needs.
9820 Some(ObjKind::Symbol) => matches!(name, "toString" | "valueOf" | "@@toPrimitive"),
9821 Some(ObjKind::BigInt) => matches!(name, "toString" | "valueOf" | "toLocaleString"),
9822 _ => false,
9823 }
9824}
9825
9826/// Dispatch `recv.name(args)` for the built-in prototype methods.
9827pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
9828 // A USER method on the receiver's prototype chain wins over the builtin of
9829 // the same name — that is how a `class X extends Array` method is reached,
9830 // since the dispatch below goes straight to the builtin table and has no
9831 // entry for it.
9832 //
9833 // Deliberately restricted to a user function: the shared `Object.prototype`
9834 // carries real `@proto:Object:*` thunks, so accepting any callable made a
9835 // bare `map.toString()` resolve to the object form instead of the builtin
9836 // one the exotic is supposed to use.
9837 if let Some(f) = with_host(|h| host::lookup_chain(h, recv, name)) {
9838 if matches!(
9839 with_host(|h| h.kind_of(&f)),
9840 Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc)
9841 ) {
9842 return host::invoke(&f, args, Some(recv.clone()));
9843 }
9844 }
9845 // A method synthesized from the receiver's KIND is unreachable once its
9846 // intrinsic prototype is off the chain. The read already answers
9847 // `undefined` for one; dispatch has its own table and would still have
9848 // called it, so `Object.setPrototypeOf(a, {}); a.join()` returned "1,2"
9849 // while `a.join` was `undefined` — the read and the call disagreeing again,
9850 // in the opposite direction from the monkey-patch case below.
9851 if !own_intrinsic_reachable(recv)
9852 && inherited_method_owner(recv, name).is_none()
9853 && !has_own_for_shadow(recv, name)
9854 && inherited_builtin_static(recv, name).is_none()
9855 && with_host(|h| host::lookup_chain(h, recv, name)).is_none()
9856 {
9857 return Err(host::type_error(&format!("{name} is not a function")));
9858 }
9859 // A method monkey-patched onto the receiver's intrinsic prototype. The READ
9860 // path resolves these, but dispatch goes straight to the builtin table and
9861 // never consults it, so `Array.prototype.last = f; [1].last()` threw "is not
9862 // a function" while `[1].last` WAS `f` — the read and the call disagreeing
9863 // about the same name, on the one path a polyfill actually uses.
9864 if !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
9865 if let Some(f) = inherited_builtin_static(recv, name) {
9866 if with_host(|h| host::is_callable(h, &f)) {
9867 return host::invoke(&f, args, Some(recv.clone()));
9868 }
9869 }
9870 }
9871 // Every object INHERITS the `Object.prototype` methods, and an exotic that
9872 // does not define its own reaches them the same way. Each kind's dispatch
9873 // table below only knows its own methods, so `new Map().toString()`,
9874 // `promise.hasOwnProperty(k)` and `sym.toLocaleString()` all reported "is
9875 // not a function" — `Object.prototype.toString.call(m)` worked while
9876 // `m.toString()` did not.
9877 // The allowlist is the kinds whose own dispatch table below would otherwise
9878 // claim the name. Every OTHER receiver reaches an `Object.prototype` method
9879 // the same way — a function, a class and a bound function included, where
9880 // `f.hasOwnProperty(k)` reported "is not a function" even though the READ
9881 // resolved it. `inherited_method_owner` decides which prototype owns the
9882 // name, so a kind that defines its own still gets its own.
9883 if is_object_builtin_method(name)
9884 && (inherited_method_owner(recv, name) == Some("Object")
9885 || (inherits_object_methods(recv) && !overrides_object_method(recv, name)))
9886 {
9887 // `toString` goes through the branded form (20.1.3.6), which reads
9888 // `Symbol.toStringTag` and falls back to the receiver's own brand —
9889 // `[object Map]`, not the generic stringification.
9890 if name == "toString" {
9891 return proto_method(recv, "Object:toString", args);
9892 }
9893 return object_builtin_method(recv, name, args);
9894 }
9895 // `Object.prototype.valueOf` is inherited by every exotic that does not
9896 // override it (an Array does not), and returns the receiver. Without this
9897 // the `ToPrimitive` probe on `[o] + ''` reached `array_method("valueOf")`
9898 // and threw `valueOf is not a function`.
9899 if name == "valueOf"
9900 && matches!(
9901 with_host(|h| h.kind_of(recv)),
9902 Some(
9903 ObjKind::Array
9904 | ObjKind::Map
9905 | ObjKind::Set
9906 | ObjKind::Generator
9907 | ObjKind::Promise
9908 | ObjKind::Iter
9909 | ObjKind::RegExp
9910 )
9911 )
9912 {
9913 return Ok(recv.clone());
9914 }
9915 // Only the tag is needed to pick the branch — cloning the receiver here made
9916 // every `arr.push(x)` copy the whole array, so a fill loop was O(n^2).
9917 match with_host(|h| h.kind_of(recv)) {
9918 Some(ObjKind::Array) => array_method(recv, name, args),
9919 Some(ObjKind::Str) => {
9920 // `string_method` consumes the text itself, so this clone is the
9921 // payload, not a tag probe.
9922 let s = peek(recv, |o| match o {
9923 JsObj::Str(s) => Some(s.clone()),
9924 _ => None,
9925 })
9926 .unwrap_or_default();
9927 string_method(&s, name, args)
9928 }
9929 Some(ObjKind::Map) => map_method(recv, name, args),
9930 Some(ObjKind::Set) => set_method(recv, name, args),
9931 Some(ObjKind::Generator) if crate::stdlib::iterator::is_helper(name) => {
9932 crate::stdlib::iterator::call(recv, name, &args)
9933 }
9934 Some(ObjKind::Generator) => generator_method(recv, name, args),
9935 Some(ObjKind::Promise) => promise_method(recv, name, args),
9936 Some(ObjKind::Iter) if crate::stdlib::iterator::is_helper(name) => {
9937 crate::stdlib::iterator::call(recv, name, &args)
9938 }
9939 Some(ObjKind::Iter) => iter_method(recv, name, args),
9940 Some(ObjKind::Symbol) => symbol_method(recv, name, args),
9941 Some(ObjKind::BigInt) => {
9942 let b = peek(recv, |o| match o {
9943 JsObj::BigInt(b) => Some(b.clone()),
9944 _ => None,
9945 })
9946 .unwrap_or_default();
9947 bigint_method(&b, name, args)
9948 }
9949 Some(ObjKind::RegExp) => crate::regexp::regexp_method(recv, name, args),
9950 Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc) => {
9951 match function_builtin_method(recv, name, &args)? {
9952 Some(v) => Ok(v),
9953 None => Err(host::type_error(&format!("{name} is not a function"))),
9954 }
9955 }
9956 Some(ObjKind::Object) => {
9957 if let Some(f) = peek(recv, |o| match o {
9958 JsObj::Object(p) => p.get(name).cloned(),
9959 _ => None,
9960 }) {
9961 host::invoke(&f, args, Some(recv.clone()))
9962 } else if name == "hasOwnProperty" {
9963 let k = with_host(|h| h.str_of(&arg0(&args)));
9964 let has = peek(recv, |o| match o {
9965 JsObj::Object(p) => Some(p.contains_key(&k)),
9966 _ => None,
9967 })
9968 .unwrap_or(false);
9969 Ok(Value::Bool(has))
9970 } else if name == "toString" {
9971 Ok(with_host(|h| h.new_str("[object Object]")))
9972 } else {
9973 Err(host::type_error(&format!("{} is not a function", name)))
9974 }
9975 }
9976 _ => {
9977 // Primitive number/bool/string coercions.
9978 if let Value::Float(_) | Value::Int(_) = recv {
9979 return number_method(with_host(|h| h.to_number(recv)), name, args);
9980 }
9981 if let Some(s) = with_host(|h| h.as_str(recv)) {
9982 return string_method(&s, name, args);
9983 }
9984 // `Boolean.prototype` (20.3.3): a boolean is not a heap object here,
9985 // so it reached no branch at all and `true.toString()` threw `is not
9986 // a function`. Its three methods are `toString`, `valueOf`, and the
9987 // inherited `Object.prototype.toLocaleString` — which
9988 // `[1,'a',true].toLocaleString()` invokes per element, so the hole
9989 // was reachable from the array form too.
9990 if let Value::Bool(b) = recv {
9991 return match name {
9992 "toString" | "toLocaleString" => {
9993 Ok(new_s(if *b { "true" } else { "false" }.to_string()))
9994 }
9995 "valueOf" => Ok(Value::Bool(*b)),
9996 _ => Err(host::type_error(&format!("{name} is not a function"))),
9997 };
9998 }
9999 Err(host::type_error(&format!("{} is not a function", name)))
10000 }
10001 }
10002}
10003
10004/// A copy of the whole backing store, for the methods that genuinely consume
10005/// every element (`map`, `filter`, `join`, …). Never call it just to read
10006/// `.len()` — use [`array_len`], or `push`/`unshift` become O(n) per call.
10007/// A LIVE iterator over a `Map` or `Set`.
10008///
10009/// Node's collection iterators see the collection as it is at each step: an
10010/// entry added during iteration IS visited, and one deleted before it is
10011/// reached is NOT. Ours materialized every entry up front, so both were wrong —
10012/// a loop that deletes as it goes still processed the entries it had removed.
10013///
10014/// The cursor is the last key yielded plus the index it was at. On each step
10015/// the key is located again in the CURRENT order: if it is still there the next
10016/// entry follows it, and if it was itself deleted the stored index now names
10017/// the entry that shifted into its place. That reproduces node for the cases
10018/// its own tests turn on — add-during, delete-ahead, delete-self,
10019/// delete-behind, delete-the-rest and clear — without giving `Map` the
10020/// tombstoned entry list node uses internally.
10021fn collection_iterator(coll: &Value, kind: &str) -> Value {
10022 with_host(|h| {
10023 let mut m = IndexMap::new();
10024 m.insert(
10025 "@@native".into(),
10026 h.new_str("CollectionIterator".to_string()),
10027 );
10028 m.insert("@@coll".into(), coll.clone());
10029 m.insert("@@kind".into(), h.new_str(kind.to_string()));
10030 m.insert("@@started".into(), Value::Bool(false));
10031 m.insert("@@lastIdx".into(), Value::Float(0.0));
10032 h.new_object(m)
10033 })
10034}
10035
10036/// One step of a live collection iterator.
10037pub(crate) fn collection_iterator_next(recv: &Value) -> Result<Value, String> {
10038 let slot = |k: &str| {
10039 with_host(|h| match h.get(recv) {
10040 Some(JsObj::Object(p)) => p.get(k).cloned(),
10041 _ => None,
10042 })
10043 };
10044 let coll = slot("@@coll").unwrap_or(Value::Undef);
10045 let kind = slot("@@kind")
10046 .map(|v| with_host(|h| h.str_of(&v)))
10047 .unwrap_or_default();
10048 let started = slot("@@started").is_some_and(|v| with_host(|h| h.truthy(&v)));
10049 let last_idx = slot("@@lastIdx")
10050 .map(|v| with_host(|h| h.to_number(&v)) as usize)
10051 .unwrap_or(0);
10052 let last_key = slot("@@lastKey");
10053
10054 let next_idx = if !started {
10055 0
10056 } else {
10057 match last_key
10058 .as_ref()
10059 .and_then(|k| with_host(|h| collection_index_of(h, &coll, k)))
10060 {
10061 // Still present: continue after it.
10062 Some(i) => i + 1,
10063 // Deleted since: whatever shifted into its slot is next.
10064 None => last_idx,
10065 }
10066 };
10067 let entry = with_host(|h| collection_entry_at(h, &coll, next_idx));
10068 let Some((k, v)) = entry else {
10069 return Ok(iter_result(Value::Undef, true));
10070 };
10071 with_host(|h| {
10072 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
10073 p.insert("@@started".into(), Value::Bool(true));
10074 p.insert("@@lastIdx".into(), Value::Float(next_idx as f64));
10075 p.insert("@@lastKey".into(), k.clone());
10076 }
10077 });
10078 let out = match kind.as_str() {
10079 "keys" => k,
10080 "values" => v,
10081 _ => with_host(|h| h.new_array(vec![k, v])),
10082 };
10083 Ok(iter_result(out, false))
10084}
10085
10086/// The (key, value) at `idx` in a Map, or (value, value) in a Set.
10087fn collection_entry_at(h: &host::JsHost, coll: &Value, idx: usize) -> Option<(Value, Value)> {
10088 match h.get(coll) {
10089 Some(JsObj::Map { entries, .. }) => entries.get_index(idx).map(|(_, kv)| kv.clone()),
10090 Some(JsObj::Set { entries, .. }) => {
10091 entries.get_index(idx).map(|(_, v)| (v.clone(), v.clone()))
10092 }
10093 _ => None,
10094 }
10095}
10096
10097/// Where `key` currently sits in the collection's order.
10098fn collection_index_of(h: &host::JsHost, coll: &Value, key: &Value) -> Option<usize> {
10099 let mk = host::map_key(h, key);
10100 match h.get(coll) {
10101 Some(JsObj::Map { entries, .. }) => entries.get_index_of(&mk),
10102 Some(JsObj::Set { entries, .. }) => entries.get_index_of(&mk),
10103 _ => None,
10104 }
10105}
10106
10107/// The `thisArg` an iteration method was given, if any.
10108///
10109/// `[1].forEach(fn, thisArg)` binds `thisArg` as the callback's `this`, and so
10110/// do `map`/`filter`/`some`/`every`/`find`/`findIndex`/`findLast`/
10111/// `findLastIndex`/`flatMap`, `Map`/`Set`/TypedArray `forEach`, and
10112/// `Array.from`'s map function. Every one of them was invoking the callback
10113/// with no receiver, so `this` inside it was undefined and the argument did
10114/// nothing.
10115fn this_arg(args: &[Value], idx: usize) -> Option<Value> {
10116 args.get(idx)
10117 .filter(|v| !matches!(v, Value::Undef))
10118 .cloned()
10119}
10120
10121/// The elements of an array, with any INDEX ACCESSOR resolved.
10122///
10123/// `Object.defineProperty(arr, 1, { get })` stores the getter in the accessor
10124/// table, and an array's elements live in a backing vector — so every method
10125/// reading that vector directly (`join`, `map`, `indexOf`, …) saw the stale
10126/// slot and never called the getter, while a plain `arr[1]` read did.
10127///
10128/// An array with no accessors pays one lookup returning an empty list, so the
10129/// ordinary case is unchanged. The getters are invoked OUTSIDE the host borrow,
10130/// since calling one re-enters.
10131/// Walk `recv` the way an `Array.prototype` iteration method does: the LENGTH
10132/// is captured once at entry (LengthOfArrayLike, step 3), but each element is
10133/// read LIVE at its index, and an index that no longer exists is skipped.
10134///
10135/// Snapshotting the whole array instead meant a callback that mutated it was
10136/// not observed: `[1,2,3].forEach(v => a.shift())` visited 1, 2, 3 where node
10137/// visits 1 and 3, and `filter` kept elements the callback had already removed.
10138///
10139/// `f` returns `Some(x)` to stop early with `x`.
10140fn array_walk<T>(
10141 recv: &Value,
10142 mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10143) -> Result<Option<T>, String> {
10144 let len = array_len(recv);
10145 for i in 0..len {
10146 // A HOLE — and an index a shrinking mutation has dropped — is skipped
10147 // without calling the callback.
10148 if index_absent(recv, i) || i >= array_len(recv) {
10149 continue;
10150 }
10151 let v = get_property(recv, &i.to_string())?;
10152 if let Some(out) = f(i, v)? {
10153 return Ok(Some(out));
10154 }
10155 }
10156 Ok(None)
10157}
10158
10159/// `array_walk`'s descending twin, for `reduceRight`/`findLast*`: the same
10160/// capture-length-once, read-each-element-live rule walked from the end. A
10161/// callback that SHRINKS the array is observed by every later step, so the
10162/// indices it drops are skipped rather than served from a stale copy.
10163fn array_walk_rev<T>(
10164 recv: &Value,
10165 from: usize,
10166 mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
10167) -> Result<Option<T>, String> {
10168 for i in (0..from).rev() {
10169 if index_absent(recv, i) || i >= array_len(recv) {
10170 continue;
10171 }
10172 let v = get_property(recv, &i.to_string())?;
10173 if let Some(out) = f(i, v)? {
10174 return Ok(Some(out));
10175 }
10176 }
10177 Ok(None)
10178}
10179
10180/// The live read behind `indexOf`/`includes`/`join`: the element at `i`, or
10181/// `undefined` once a mutation has shrunk the array past it.
10182fn array_elem_live(recv: &Value, i: usize) -> Result<Value, String> {
10183 if i >= array_len(recv) {
10184 return Ok(Value::Undef);
10185 }
10186 get_property(recv, &i.to_string())
10187}
10188
10189fn array_items(recv: &Value) -> Vec<Value> {
10190 let mut items = with_host(|h| match h.get(recv) {
10191 Some(JsObj::Array(items)) => items.clone(),
10192 _ => Vec::new(),
10193 });
10194 resolve_index_accessors(recv, &mut items);
10195 items
10196}
10197
10198/// Replace each slot that has an own accessor with what its getter returns.
10199pub(crate) fn resolve_index_accessors_pub(recv: &Value, items: &mut [Value]) {
10200 resolve_index_accessors(recv, items);
10201}
10202
10203/// Returns whether any slot was replaced, which the JSON walk needs: it keeps
10204/// the ORIGINAL array when nothing changed, and the original still holds the
10205/// stale slots.
10206fn resolve_index_accessors(recv: &Value, items: &mut [Value]) -> bool {
10207 let mut indices: Vec<usize> = with_host(|h| h.own_accessor_keys(recv))
10208 .into_iter()
10209 .filter_map(|k| k.parse::<usize>().ok())
10210 .filter(|i| *i < items.len())
10211 .collect();
10212 // An ELIDED index the prototype chain supplies is stale in the backing
10213 // vector too — it holds `undefined` where `[[Get]]` answers the inherited
10214 // value. Spread and `JSON.stringify` both read through here, and both
10215 // rendered the hole rather than what `a[i]` reads.
10216 let inherited: Vec<usize> = with_host(|h| h.hole_indices(recv))
10217 .into_iter()
10218 .filter(|i| *i < items.len() && !indices.contains(i))
10219 .filter(|i| has_property(recv, &i.to_string()).unwrap_or(false))
10220 .collect();
10221 indices.extend(inherited);
10222 let mut replaced = false;
10223 for i in indices {
10224 if let Ok(v) = get_property(recv, &i.to_string()) {
10225 items[i] = v;
10226 replaced = true;
10227 }
10228 }
10229 replaced
10230}
10231
10232/// The ELIDED positions of array `recv` as a membership set. A dense array —
10233/// which is nearly every array — answers with an empty set after a single
10234/// negative hash probe and allocates nothing.
10235///
10236/// The iteration methods split into two groups, and the split is not a matter of
10237/// taste: the ones spec'd through `HasProperty` (`forEach`, `map`, `filter`,
10238/// `some`, `every`, `reduce`, `indexOf`, `flat`, `sort`) SKIP a hole, while the
10239/// ones spec'd through a bare `Get` (`for…of`, spread, `find`, `includes`,
10240/// `join`, `entries`, `Array.from`) see the `undefined` a hole reads back as.
10241fn hole_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10242 with_host(|h| h.hole_indices(recv)).into_iter().collect()
10243}
10244
10245/// The indices `recv` genuinely has NO property at — the elided ones the
10246/// prototype chain does not supply either.
10247///
10248/// Every array method tests `HasProperty` before deciding to skip a position
10249/// (23.1.3.x, uniformly), and `HasProperty` walks the chain. Testing elision
10250/// alone made an inherited element invisible to all of them: with
10251/// `Array.prototype[1] = 'p'`, `[1,,3].map(v => v)` produced a hole where node
10252/// produces `'p'`, and `flat`/`concat`/`slice`/`sort`/`indexOf` each dropped
10253/// the same position.
10254///
10255/// `hole_set` remains the elision record itself, which is what `splice` moves
10256/// around — that bookkeeping is about the array's OWN storage and must not
10257/// consult the chain.
10258fn absent_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
10259 hole_set(recv)
10260 .into_iter()
10261 .filter(|i| !has_property(recv, &i.to_string()).unwrap_or(false))
10262 .collect()
10263}
10264
10265/// The single-index form of [`absent_set`], for the walkers that test one
10266/// position at a time.
10267fn index_absent(recv: &Value, i: usize) -> bool {
10268 with_host(|h| h.is_hole(recv, i)) && !has_property(recv, &i.to_string()).unwrap_or(false)
10269}
10270
10271/// The element count, without copying the elements.
10272fn array_len(recv: &Value) -> usize {
10273 peek(recv, |o| match o {
10274 JsObj::Array(items) => Some(items.len()),
10275 _ => None,
10276 })
10277 .unwrap_or(0)
10278}
10279
10280/// `ArraySpeciesCreate(originalArray, length)` (23.1.3.4) — the constructor an
10281/// array method builds its RESULT with.
10282///
10283/// `map`, `filter`, `slice`, `concat`, `splice`, `flat` and `flatMap` all
10284/// produce an array of the receiver's own species, so on a `class A extends
10285/// Array` the result is an `A`. Every one of them allocated a plain array
10286/// instead, so `A.from([1]).map(x => x) instanceof A` was false.
10287///
10288/// The default `get [Symbol.species]() { return this }` is what makes the
10289/// subclass the species; a class overriding it with `Array` gets a plain array
10290/// back, which is the documented way to opt out.
10291/// Build an array-shaped result through `ctor`, or a plain array when there is
10292/// none to build through.
10293///
10294/// The constructor is called with the LENGTH and the elements written after, as
10295/// 23.1.2.1 and 23.1.3.4 both specify — which is what lets a subclass
10296/// constructor observe the allocation.
10297fn construct_array_like(ctor: Option<Value>, items: Vec<Value>) -> Result<Value, String> {
10298 let Some(ctor) = ctor.filter(|c| {
10299 matches!(
10300 with_host(|h| h.kind_of(c)),
10301 Some(ObjKind::Class) | Some(ObjKind::Func)
10302 )
10303 }) else {
10304 return Ok(with_host(|h| h.new_array(items)));
10305 };
10306 let out = host::construct(&ctor, vec![Value::Float(items.len() as f64)])?;
10307 write_elements(&out, items);
10308 Ok(out)
10309}
10310
10311/// Write `items` into a freshly constructed array-shaped `out`, clearing the
10312/// hole marks the length-only construction left behind.
10313///
10314/// `new A(3)` on `class A extends Array` really does produce three HOLES, and
10315/// the elements written over them stayed marked — so every subclass result of
10316/// `map`/`filter`/`flat` read back as holes: `A.from([1,2,3]).map(x => x * 2)`
10317/// had length 3 and printed `[null,null,null]`, and `0 in` it was false.
10318fn write_elements(out: &Value, items: Vec<Value>) {
10319 with_host(|h| {
10320 h.clear_holes(out);
10321 if let Some(JsObj::Array(dst)) = h.get_mut(out) {
10322 *dst = items;
10323 }
10324 });
10325}
10326
10327fn array_species_create(recv: &Value, items: Vec<Value>) -> Result<Value, String> {
10328 let plain = || with_host(|h| h.new_array(items.clone()));
10329 // Only a subclass instance can have a species of its own: a plain array's
10330 // `constructor` is the `Array` builtin, whose species is `Array`.
10331 // A chain lookup, not `get_property`: an Array receiver resolves its
10332 // properties through the stdlib funnel, which has no `constructor` entry,
10333 // so the read alone reports `undefined` for every subclass instance. A
10334 // Proxy is the exception — it has no property map to walk, and its
10335 // `constructor` comes from the `get` trap, so a proxied subclass array
10336 // produced plain arrays.
10337 let ctor = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
10338 get_property(recv, "constructor").unwrap_or(Value::Undef)
10339 } else {
10340 with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef)
10341 };
10342 if !matches!(
10343 with_host(|h| h.kind_of(&ctor)),
10344 Some(ObjKind::Class) | Some(ObjKind::Func)
10345 ) {
10346 return Ok(plain());
10347 }
10348 // An explicit `@@species` wins; absent one, the constructor itself is the
10349 // species, as the inherited accessor returns `this`.
10350 let species = match get_property(&ctor, "@@species") {
10351 Ok(Value::Undef) => ctor,
10352 Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(plain()),
10353 Ok(s) => s,
10354 Err(_) => ctor,
10355 };
10356 if !matches!(
10357 with_host(|h| h.kind_of(&species)),
10358 Some(ObjKind::Class) | Some(ObjKind::Func)
10359 ) {
10360 return Ok(plain());
10361 }
10362 let out = host::construct(&species, vec![Value::Float(items.len() as f64)])?;
10363 // The constructor is called with the LENGTH, so the elements are written
10364 // afterwards — which is also what lets a subclass constructor observe the
10365 // allocation, as node's does.
10366 write_elements(&out, items);
10367 Ok(out)
10368}
10369
10370fn array_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
10371 array_method_on(recv, recv, name, args)
10372}
10373
10374/// The `Array.prototype` methods that WRITE to their receiver, and so need the
10375/// generic path to copy the result back onto the array-like.
10376const ARRAY_MUTATORS: &[&str] = &[
10377 "push",
10378 "pop",
10379 "shift",
10380 "unshift",
10381 "splice",
10382 "sort",
10383 "reverse",
10384 "fill",
10385 "copyWithin",
10386];
10387
10388/// Run `Array.prototype.<method>` against an array-LIKE (`{0: 'a', length: 1}`,
10389/// a DOM-ish collection, `arguments`).
10390///
10391/// 23.1.3 defines every one of these over `LengthOfArrayLike(O)` and `Get(O, k)`
10392/// rather than over an Array's element vector, so the receiver only has to have
10393/// a `length`. The elements are read out into a temporary Array, the ordinary
10394/// implementation runs on that, and a MUTATING method writes the result back —
10395/// which keeps one implementation of each method rather than a second, generic
10396/// one that could drift from it.
10397///
10398/// An index the receiver does not own is a HOLE in the temporary, so the
10399/// methods that skip holes skip it here too, exactly as `HasProperty` makes them.
10400fn array_generic(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
10401 let len = match get_property(recv, "length") {
10402 Ok(v) => host::to_array_length(&v).unwrap_or(0),
10403 Err(_) => 0,
10404 };
10405 // A STRING receiver owns every index of its length; `has_property` answers
10406 // for objects and reports none of them, which made `[].map.call('abc', f)`
10407 // an array of three holes.
10408 let dense = with_host(|h| h.as_str(recv)).is_some();
10409 let mut items = Vec::with_capacity(len);
10410 let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
10411 for i in 0..len {
10412 let k = i.to_string();
10413 if dense || has_property(recv, &k)? {
10414 items.push(get_property(recv, &k)?);
10415 } else {
10416 holes.insert(i);
10417 items.push(Value::Undef);
10418 }
10419 }
10420 let tmp = with_host(|h| {
10421 let a = h.new_array(items);
10422 h.install_holes(&a, holes);
10423 a
10424 });
10425 let out = array_method_on(&tmp, recv, method, args)?;
10426 if ARRAY_MUTATORS.contains(&method) {
10427 let result = with_host(|h| match h.get(&tmp) {
10428 Some(JsObj::Array(items)) => items.clone(),
10429 _ => Vec::new(),
10430 });
10431 for (i, v) in result.iter().enumerate() {
10432 set_property(recv, &i.to_string(), v.clone())?;
10433 }
10434 set_property(recv, "length", Value::Float(result.len() as f64))?;
10435 }
10436 Ok(out)
10437}
10438
10439/// `Array.prototype.<name>` on `recv`.
10440///
10441/// `this_value` is what a callback receives as its third argument and what a
10442/// mutating method returns — the same object as `recv` for an ordinary array
10443/// call, but the ORIGINAL array-like when `array_generic` runs a method against
10444/// a temporary copy (`Array.prototype.slice.call(arguments)`).
10445fn array_method_on(
10446 recv: &Value,
10447 this_value: &Value,
10448 name: &str,
10449 args: Vec<Value>,
10450) -> Result<Value, String> {
10451 let args = coerce_numeric_args(ARRAY_METHOD_NUMERIC_ARGS, name, args)?;
10452 match name {
10453 "push" => {
10454 // 23.1.3.23 step 4 defines each new element through
10455 // `CreateDataPropertyOrThrow`, so a NON-EXTENSIBLE array refuses it:
10456 // `Object.seal(a)` / `preventExtensions(a)` then `a.push(x)` is a
10457 // TypeError. The elements were appended to the backing vector
10458 // regardless, so sealing an array did not seal it.
10459 if !args.is_empty() && !with_host(|h| h.is_extensible(recv)) {
10460 let at = array_len(recv);
10461 return Err(host::type_error(&format!(
10462 "Cannot add property {at}, object is not extensible"
10463 )));
10464 }
10465 // Step 5 then SETS `length`, so a non-writable one refuses the push
10466 // too — `defineProperty(a, 'length', {writable: false})` makes an
10467 // array append-proof without sealing it.
10468 if !args.is_empty() && !with_host(|h| h.prop_attrs(recv, "length").writable) {
10469 return Err(host::type_error(
10470 "Cannot assign to read only property 'length' of object '[object Array]'",
10471 ));
10472 }
10473 // `push` returns the new length; take it from the same mutable
10474 // borrow rather than copying the array back out to count it.
10475 let len = with_host(|h| {
10476 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10477 items.extend(args.iter().cloned());
10478 items.len()
10479 } else {
10480 0
10481 }
10482 });
10483 Ok(Value::Float(len as f64))
10484 }
10485 "pop" => Ok(with_host(|h| {
10486 let popped = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10487 items.pop().unwrap_or(Value::Undef)
10488 } else {
10489 Value::Undef
10490 };
10491 let len = match h.get(recv) {
10492 Some(JsObj::Array(items)) => items.len(),
10493 _ => 0,
10494 };
10495 h.truncate_holes(recv, len);
10496 popped
10497 })),
10498 "shift" => Ok(with_host(|h| {
10499 let shifted = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10500 if items.is_empty() {
10501 Value::Undef
10502 } else {
10503 items.remove(0)
10504 }
10505 } else {
10506 Value::Undef
10507 };
10508 h.remap_holes(recv, |i| i.checked_sub(1));
10509 shifted
10510 })),
10511 "unshift" => {
10512 with_host(|h| {
10513 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10514 for (i, a) in args.iter().enumerate() {
10515 items.insert(i, a.clone());
10516 }
10517 }
10518 let n = args.len();
10519 h.remap_holes(recv, |i| Some(i + n));
10520 });
10521 Ok(Value::Float(array_len(recv) as f64))
10522 }
10523 "join" => {
10524 let sep = if args.is_empty() || matches!(args[0], Value::Undef) {
10525 ",".to_string()
10526 } else {
10527 arg_to_string(&args, 0)?
10528 };
10529 join_array(recv, &sep)
10530 }
10531 // `Array.prototype.toLocaleString` (23.1.3.32): comma-join the elements'
10532 // OWN `toLocaleString` results, with `null`/`undefined` contributing the
10533 // empty string. It threw `is not a function` — the whole method was
10534 // missing — so `[1234.5, 'x'].toLocaleString()` was unreachable.
10535 "toLocaleString" => {
10536 // Shares `join`'s JoinStack: measured on node v26.7.0, `h=[1]`
10537 // `h.push(h)` makes `h.toLocaleString()` `"1,"`, not a stack overflow.
10538 if !host::join_stack_push(recv) {
10539 return Ok(with_host(|h| h.new_str(String::new())));
10540 }
10541 let items = array_items(recv);
10542 let mut parts: Vec<String> = Vec::with_capacity(items.len());
10543 for it in &items {
10544 if with_host(|h| h.is_nullish(it)) {
10545 parts.push(String::new());
10546 continue;
10547 }
10548 let v = match host::call_method(it, "toLocaleString", Vec::new()) {
10549 Ok(v) => v,
10550 Err(e) => {
10551 host::join_stack_pop();
10552 return Err(e);
10553 }
10554 };
10555 parts.push(with_host(|h| h.str_of(&v)));
10556 }
10557 host::join_stack_pop();
10558 Ok(with_host(|h| h.new_str(parts.join(","))))
10559 }
10560 // `indexOf`/`lastIndexOf` are spec'd through `HasProperty`, so a hole is
10561 // never a match: `[1,,3].indexOf(undefined)` is `-1`, while the
10562 // `Get`-based `includes` reports `true` for the same array.
10563 "indexOf" => {
10564 let target = arg0(&args);
10565 let len = array_len(recv);
10566 let start = search_start(arg_num(&args, 1), len);
10567 let mut idx = None;
10568 for i in start..len {
10569 // 23.1.3.17 steps 8a-8b: HasProperty first, so a hole — and an
10570 // index a mutation has since dropped — is skipped, not compared.
10571 if index_absent(recv, i) || i >= array_len(recv) {
10572 continue;
10573 }
10574 let x = get_property(recv, &i.to_string())?;
10575 if with_host(|h| h.strict_eq(&x, &target)) {
10576 idx = Some(i);
10577 break;
10578 }
10579 }
10580 Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10581 }
10582 "lastIndexOf" => {
10583 let items = array_items(recv);
10584 let holes = absent_set(recv);
10585 let target = arg0(&args);
10586 let from = (args.len() > 1).then(|| arg_num(&args, 1));
10587 let idx = match search_start_last(from, items.len()) {
10588 None => None,
10589 Some(start) => with_host(|h| {
10590 items[..=start]
10591 .iter()
10592 .enumerate()
10593 .rev()
10594 .find(|(i, x)| !holes.contains(i) && h.strict_eq(x, &target))
10595 .map(|(i, _)| i)
10596 }),
10597 };
10598 Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
10599 }
10600 "includes" => {
10601 // Array.includes uses SameValueZero: unlike `===`, NaN matches NaN.
10602 // Unlike `indexOf` it has no HasProperty step (23.1.3.16 step 5b), so
10603 // a hole reads as `undefined` and `[,].includes(undefined)` is true.
10604 let target = arg0(&args);
10605 let tnan = matches!(target, Value::Float(f) if f.is_nan());
10606 let len = array_len(recv);
10607 let start = search_start(arg_num(&args, 1), len);
10608 let mut found = false;
10609 for i in start..len {
10610 let x = array_elem_live(recv, i)?;
10611 if (tnan && matches!(x, Value::Float(f) if f.is_nan()))
10612 || with_host(|h| h.strict_eq(&x, &target))
10613 {
10614 found = true;
10615 break;
10616 }
10617 }
10618 Ok(Value::Bool(found))
10619 }
10620 "slice" => {
10621 let items = array_items(recv);
10622 let (lo, hi) = slice_bounds(&args, items.len());
10623 let out = array_species_create(this_value, items[lo..hi].to_vec())?;
10624 with_host(|h| h.copy_holes(recv, &out, |i| (i >= lo && i < hi).then(|| i - lo)));
10625 Ok(out)
10626 }
10627 "concat" => {
10628 // `Symbol.isConcatSpreadable` (23.1.3.1) decides whether a value
10629 // is spread, overriding `IsArray` in BOTH directions: a plain
10630 // array-like opts IN, and an array opts OUT.
10631 let spreadable = |a: &Value| -> bool {
10632 let flag = get_property(a, "@@isConcatSpreadable").unwrap_or(Value::Undef);
10633 if matches!(flag, Value::Undef) {
10634 matches!(with_host(|h| h.get(a).cloned()), Some(JsObj::Array(_)))
10635 && !is_arguments(a)
10636 } else {
10637 with_host(|h| h.truthy(&flag))
10638 }
10639 };
10640 // Step 5 iterates `« O » ++ items`, so the receiver takes the same
10641 // test: a non-spreadable `this` (`concat.call("ab", 1)`) is ONE
10642 // element, its `ToObject` box, not the characters `array_generic`
10643 // read out of it. A hole in a spread receiver or argument stays a
10644 // hole in the result, at its shifted position.
10645 let (mut out, mut holes) = if spreadable(this_value) {
10646 (array_items(recv), absent_set(recv))
10647 } else {
10648 (vec![to_object(this_value)], Default::default())
10649 };
10650 let mut sources: Vec<(Value, usize)> = Vec::new();
10651 for a in &args {
10652 if !spreadable(a) {
10653 out.push(a.clone());
10654 continue;
10655 }
10656 match with_host(|h| h.get(a).cloned()) {
10657 // Read off the backing vector rather than through
10658 // `array_items`, so the resolve that does for the receiver
10659 // has to be done here too: 23.1.3.1 step 5.c.iv is a
10660 // `[[Get]]`, and an index with a getter — or an elided one
10661 // the chain supplies — is stale in that vector.
10662 Some(JsObj::Array(mut items)) => {
10663 resolve_index_accessors(a, &mut items);
10664 sources.push((a.clone(), out.len()));
10665 out.extend(items);
10666 }
10667 // An opted-in array-LIKE spreads by its `length` and index
10668 // properties rather than by a backing vector it has none of.
10669 _ => {
10670 let len = get_property(a, "length").unwrap_or(Value::Undef);
10671 let n = with_host(|h| h.to_number(&len));
10672 let n = if n.is_finite() {
10673 n.max(0.0) as usize
10674 } else {
10675 0
10676 };
10677 for i in 0..n {
10678 out.push(get_property(a, &i.to_string()).unwrap_or(Value::Undef));
10679 }
10680 }
10681 }
10682 }
10683
10684 for (src, base) in sources {
10685 holes.extend(
10686 with_host(|h| h.hole_indices(&src))
10687 .into_iter()
10688 .map(|i| i + base),
10689 );
10690 }
10691 let arr = array_species_create(this_value, out)?;
10692 with_host(|h| h.install_holes(&arr, holes));
10693 Ok(arr)
10694 }
10695 "reverse" => {
10696 let len = array_len(recv);
10697 with_host(|h| {
10698 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10699 items.reverse();
10700 }
10701 h.remap_holes(recv, |i| Some(len - 1 - i));
10702 });
10703 Ok(this_value.clone())
10704 }
10705 "fill" => {
10706 // fill(value[, start[, end]]) — negative indices count from the end.
10707 let val = arg0(&args);
10708 let len = array_len(recv) as i64;
10709 let norm =
10710 |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10711 let start = if args.len() >= 2 {
10712 norm(arg_num(&args, 1) as i64)
10713 } else {
10714 0
10715 };
10716 let end = if args.len() >= 3 {
10717 norm(arg_num(&args, 2) as i64)
10718 } else {
10719 len as usize
10720 };
10721 with_host(|h| {
10722 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
10723 for it in items.iter_mut().take(end).skip(start) {
10724 *it = val.clone();
10725 }
10726 }
10727 // Every filled position now holds a real value.
10728 h.remap_holes(recv, |i| (i < start || i >= end).then_some(i));
10729 });
10730 Ok(this_value.clone())
10731 }
10732 "copyWithin" => {
10733 // copyWithin(target, start[, end]) — copy a slice within the array.
10734 let items = array_items(recv);
10735 let len = items.len() as i64;
10736 let norm =
10737 |v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
10738 let target = norm(arg_num(&args, 0) as i64);
10739 let start = if args.len() >= 2 {
10740 norm(arg_num(&args, 1) as i64)
10741 } else {
10742 0
10743 };
10744 let end = if args.len() >= 3 {
10745 norm(arg_num(&args, 2) as i64)
10746 } else {
10747 len as usize
10748 };
10749 let slice: Vec<Value> = items[start..end.max(start)].to_vec();
10750 let copied = slice.len();
10751 // A copied position takes its SOURCE's hole-ness (10.4.2 copyWithin
10752 // deletes the target when the source has no such property);
10753 // everything outside the written range keeps its own.
10754 let src_holes = absent_set(recv);
10755 with_host(|h| {
10756 if let Some(JsObj::Array(a)) = h.get_mut(recv) {
10757 for (k, v) in slice.into_iter().enumerate() {
10758 if target + k < a.len() {
10759 a[target + k] = v;
10760 }
10761 }
10762 }
10763 let len = len as usize;
10764 let mut holes: rustc_hash::FxHashSet<usize> = src_holes
10765 .iter()
10766 .copied()
10767 .filter(|i| *i < target || *i >= (target + copied).min(len))
10768 .collect();
10769 for k in 0..copied {
10770 if target + k < len && src_holes.contains(&(start + k)) {
10771 holes.insert(target + k);
10772 }
10773 }
10774 h.install_holes(recv, holes);
10775 });
10776 Ok(this_value.clone())
10777 }
10778 "at" => {
10779 let items = array_items(recv);
10780 let mut i = arg_num(&args, 0) as i64;
10781 if i < 0 {
10782 i += items.len() as i64;
10783 }
10784 Ok(if i >= 0 && (i as usize) < items.len() {
10785 items[i as usize].clone()
10786 } else {
10787 Value::Undef
10788 })
10789 }
10790 // 23.1.3.21: the callback runs only where `HasProperty` holds, and the
10791 // result array is created with the SAME holes — `[1,,3].map(f)` calls `f`
10792 // twice and yields `[2, <1 empty item>, 6]`.
10793 "map" => {
10794 let holes = absent_set(recv);
10795 let cb = arg0(&args);
10796 // The result keeps the source's LENGTH, so a skipped index still
10797 // occupies a slot; `array_walk` only tells us which ones ran.
10798 let mut out = vec![Value::Undef; array_len(recv)];
10799 array_walk(recv, |i, it| {
10800 let v = host::invoke(
10801 &cb,
10802 vec![it, Value::Float(i as f64), this_value.clone()],
10803 this_arg(&args, 1),
10804 )?;
10805 if i < out.len() {
10806 out[i] = v;
10807 }
10808 Ok(None::<()>)
10809 })?;
10810 let arr = array_species_create(this_value, out)?;
10811 with_host(|h| h.install_holes(&arr, holes));
10812 Ok(arr)
10813 }
10814 "flatMap" => {
10815 let cb = arg0(&args);
10816 let thisarg = this_arg(&args, 1);
10817 let mut out = Vec::new();
10818 array_walk(recv, |i, v| {
10819 let r = host::invoke(
10820 &cb,
10821 vec![v, Value::Float(i as f64), this_value.clone()],
10822 thisarg.clone(),
10823 )?;
10824 match with_host(|h| h.get(&r).cloned()) {
10825 Some(JsObj::Array(inner)) => out.extend(inner),
10826 _ => out.push(r),
10827 }
10828 Ok(None::<()>)
10829 })?;
10830 array_species_create(this_value, out)
10831 }
10832 "filter" => {
10833 let cb = arg0(&args);
10834 let mut out = Vec::new();
10835 array_walk(recv, |i, it| {
10836 let keep = host::invoke(
10837 &cb,
10838 vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10839 this_arg(&args, 1),
10840 )?;
10841 if with_host(|h| h.truthy(&keep)) {
10842 out.push(it);
10843 }
10844 Ok(None::<()>)
10845 })?;
10846 array_species_create(this_value, out)
10847 }
10848 "forEach" => {
10849 let cb = arg0(&args);
10850 array_walk(recv, |i, it| {
10851 host::invoke(
10852 &cb,
10853 vec![it, Value::Float(i as f64), this_value.clone()],
10854 this_arg(&args, 1),
10855 )?;
10856 Ok(None::<()>)
10857 })?;
10858 Ok(Value::Undef)
10859 }
10860 "find" => {
10861 let items = array_items(recv);
10862 let cb = arg0(&args);
10863 for (i, it) in items.iter().enumerate() {
10864 let m = host::invoke(
10865 &cb,
10866 vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10867 this_arg(&args, 1),
10868 )?;
10869 if with_host(|h| h.truthy(&m)) {
10870 return Ok(it.clone());
10871 }
10872 }
10873 Ok(Value::Undef)
10874 }
10875 "findIndex" => {
10876 let items = array_items(recv);
10877 let cb = arg0(&args);
10878 for (i, it) in items.iter().enumerate() {
10879 let m = host::invoke(
10880 &cb,
10881 vec![it.clone(), Value::Float(i as f64), this_value.clone()],
10882 this_arg(&args, 1),
10883 )?;
10884 if with_host(|h| h.truthy(&m)) {
10885 return Ok(Value::Float(i as f64));
10886 }
10887 }
10888 Ok(Value::Float(-1.0))
10889 }
10890 "some" => {
10891 let cb = arg0(&args);
10892 let thisarg = this_arg(&args, 1);
10893 let hit = array_walk(recv, |i, v| {
10894 let m = host::invoke(
10895 &cb,
10896 vec![v, Value::Float(i as f64), this_value.clone()],
10897 thisarg.clone(),
10898 )?;
10899 Ok(with_host(|h| h.truthy(&m)).then_some(()))
10900 })?;
10901 Ok(Value::Bool(hit.is_some()))
10902 }
10903 "every" => {
10904 let cb = arg0(&args);
10905 let failed = array_walk(recv, |i, it| {
10906 let m = host::invoke(
10907 &cb,
10908 vec![it, Value::Float(i as f64), this_value.clone()],
10909 this_arg(&args, 1),
10910 )?;
10911 Ok((!with_host(|h| h.truthy(&m))).then_some(()))
10912 })?;
10913 Ok(Value::Bool(failed.is_none()))
10914 }
10915 "reduce" => {
10916 let items = array_items(recv);
10917 let holes = absent_set(recv);
10918 let cb = arg0(&args);
10919 let acc;
10920 let mut start = 0;
10921 if args.len() >= 2 {
10922 acc = args[1].clone();
10923 } else {
10924 // With no seed the accumulator is the first PRESENT element, so a
10925 // leading run of holes is skipped rather than seeding `undefined`.
10926 match (0..items.len()).find(|i| !holes.contains(i)) {
10927 Some(i) => {
10928 acc = items[i].clone();
10929 start = i + 1;
10930 }
10931 None => {
10932 return Err(host::type_error(
10933 "Reduce of empty array with no initial value",
10934 ))
10935 }
10936 }
10937 }
10938 // Each element is read LIVE at its index, so a callback that
10939 // shrinks the array is observed — the tail is skipped rather than
10940 // folded from a stale snapshot.
10941 let mut cur = acc;
10942 array_walk(recv, |i, it| {
10943 if i < start {
10944 return Ok(None::<()>);
10945 }
10946 cur = host::invoke(
10947 &cb,
10948 vec![
10949 std::mem::replace(&mut cur, Value::Undef),
10950 it,
10951 Value::Float(i as f64),
10952 this_value.clone(),
10953 ],
10954 this_arg(&args, 1),
10955 )?;
10956 Ok(None::<()>)
10957 })?;
10958 Ok(cur)
10959 }
10960 "reduceRight" => {
10961 let cb = arg0(&args);
10962 let n = array_len(recv);
10963 let mut acc;
10964 let mut from = n; // one past the next index to process (walking down)
10965 if args.len() >= 2 {
10966 acc = args[1].clone();
10967 } else {
10968 let holes = absent_set(recv);
10969 match (0..n).rev().find(|i| !holes.contains(i)) {
10970 Some(k) => {
10971 acc = get_property(recv, &k.to_string())?;
10972 from = k;
10973 }
10974 None => {
10975 return Err(host::type_error(
10976 "Reduce of empty array with no initial value",
10977 ))
10978 }
10979 }
10980 }
10981 // `acc` moves into the closure and back out on every step, so it
10982 // lives in an Option the closure can take from and refill.
10983 let mut slot = Some(acc);
10984 array_walk_rev(recv, from, |i, v| {
10985 let prev = slot.take().expect("accumulator is refilled each step");
10986 slot = Some(host::invoke(
10987 &cb,
10988 vec![prev, v, Value::Float(i as f64), this_value.clone()],
10989 None,
10990 )?);
10991 Ok(None::<()>)
10992 })?;
10993 acc = slot.expect("accumulator is refilled each step");
10994 Ok(acc)
10995 }
10996 "findLast" => {
10997 let items = array_items(recv);
10998 let cb = arg0(&args);
10999 for i in (0..items.len()).rev() {
11000 let m = host::invoke(
11001 &cb,
11002 vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
11003 this_arg(&args, 1),
11004 )?;
11005 if with_host(|h| h.truthy(&m)) {
11006 return Ok(items[i].clone());
11007 }
11008 }
11009 Ok(Value::Undef)
11010 }
11011 "findLastIndex" => {
11012 let items = array_items(recv);
11013 let cb = arg0(&args);
11014 for i in (0..items.len()).rev() {
11015 let m = host::invoke(
11016 &cb,
11017 vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
11018 this_arg(&args, 1),
11019 )?;
11020 if with_host(|h| h.truthy(&m)) {
11021 return Ok(Value::Float(i as f64));
11022 }
11023 }
11024 Ok(Value::Float(-1.0))
11025 }
11026 // 23.1.3.30: `SortIndexedProperties` collects only the PRESENT elements,
11027 // and the holes are re-created at the tail — `[3,,1].sort()` is
11028 // `[1, 3, <1 empty item>]` with own keys `['0','1']`.
11029 "sort" => {
11030 let all = array_items(recv);
11031 let holes = absent_set(recv);
11032 let mut items: Vec<Value> = all
11033 .iter()
11034 .enumerate()
11035 .filter(|(i, _)| !holes.contains(i))
11036 .map(|(_, v)| v.clone())
11037 .collect();
11038 sort_values(&mut items, args.first())?;
11039 let present = items.len();
11040 // 23.1.3.30 steps 4-5 write back only the indices BELOW the length
11041 // captured at step 1: `Set` for each sorted element, then `Delete`
11042 // for the holes that followed them. Replacing the whole backing
11043 // vector instead discarded anything the COMPARATOR appended —
11044 // `a.sort((x, y) => { a.push(0); return x - y })` came back at its
11045 // original length with every pushed element gone.
11046 with_host(|h| {
11047 let len = all.len();
11048 if let Some(JsObj::Array(a)) = h.get_mut(recv) {
11049 if a.len() < len {
11050 a.resize(len, Value::Undef);
11051 }
11052 for (i, v) in items.into_iter().enumerate() {
11053 a[i] = v;
11054 }
11055 for slot in a[present..len].iter_mut() {
11056 *slot = Value::Undef;
11057 }
11058 }
11059 h.install_holes(recv, (present..len).collect());
11060 });
11061 Ok(this_value.clone())
11062 }
11063 // ES2023 change-by-copy: sort a fresh copy, leaving the receiver untouched.
11064 "toSorted" => {
11065 let mut items = array_items(recv);
11066 sort_values(&mut items, args.first())?;
11067 Ok(with_host(|h| h.new_array(items)))
11068 }
11069 "toReversed" => {
11070 let mut items = array_items(recv);
11071 items.reverse();
11072 Ok(with_host(|h| h.new_array(items)))
11073 }
11074 "toSpliced" => {
11075 let mut items = array_items(recv);
11076 let len = items.len();
11077 let start = {
11078 let s = arg_num(&args, 0);
11079 if s < 0.0 {
11080 ((len as f64 + s).max(0.0)) as usize
11081 } else {
11082 (s as usize).min(len)
11083 }
11084 };
11085 let delete = if args.len() >= 2 {
11086 (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11087 } else if args.is_empty() {
11088 0
11089 } else {
11090 len - start
11091 };
11092 let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11093 items.splice(start..start + delete, inserts);
11094 Ok(with_host(|h| h.new_array(items)))
11095 }
11096 "with" => {
11097 let mut items = array_items(recv);
11098 let len = items.len() as i64;
11099 let rel = arg_num(&args, 0) as i64;
11100 let idx = if rel < 0 { len + rel } else { rel };
11101 if idx < 0 || idx >= len {
11102 return Err(host::range_error(&format!("Invalid index : {rel}")));
11103 }
11104 items[idx as usize] = args.get(1).cloned().unwrap_or(Value::Undef);
11105 Ok(with_host(|h| h.new_array(items)))
11106 }
11107 "flat" => {
11108 // depth defaults to 1; `Infinity` flattens fully. ToIntegerOrInfinity:
11109 // NaN → 0, otherwise truncate toward zero (negatives act as 0).
11110 let raw = if args.is_empty() {
11111 1.0
11112 } else {
11113 arg_num(&args, 0)
11114 };
11115 let depth = if raw.is_nan() {
11116 0.0
11117 } else if raw.is_infinite() {
11118 raw
11119 } else {
11120 raw.trunc()
11121 };
11122 let mut out = Vec::new();
11123 flatten_into(recv, depth, &mut out)?;
11124 array_species_create(this_value, out)
11125 }
11126 "keys" => {
11127 let n = array_len(recv);
11128 let items: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
11129 Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
11130 }
11131 "values" | "@@iterator" => {
11132 let items = array_items(recv);
11133 Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
11134 }
11135 "entries" => {
11136 let items = array_items(recv);
11137 let pairs: Vec<Value> = items
11138 .into_iter()
11139 .enumerate()
11140 .map(|(i, v)| with_host(|h| h.new_array(vec![Value::Float(i as f64), v])))
11141 .collect();
11142 Ok(with_host(|h| {
11143 h.alloc(JsObj::Iter {
11144 items: pairs,
11145 idx: 0,
11146 })
11147 }))
11148 }
11149 "splice" => array_splice(recv, args),
11150 // `Array.prototype.toString` IS `join()` with the default separator
11151 // (23.1.3.36), so it converts each element with `ToString` too — and
11152 // shares its cycle cut, which is the whole reason it must not call
11153 // `join_parts` directly: `ToString` of a nested array lands back here.
11154 "toString" => join_array(recv, ","),
11155 // An Array inherits from `Object.prototype` too, so the methods it does
11156 // not override resolve there. `[].hasOwnProperty` already read back as a
11157 // function through the property path, but CALLING it landed here and
11158 // threw `is not a function`.
11159 _ if is_object_builtin_method(name) => object_builtin_method(recv, name, args),
11160 _ => Err(host::type_error(&format!("{name} is not a function"))),
11161 }
11162}
11163
11164/// `Array.prototype.join` (23.1.3.18) and, with the default separator,
11165/// `Array.prototype.toString` (23.1.3.36) — one body so both share the cycle
11166/// cut, which is not optional here: `ToString` of an element that is itself an
11167/// array re-enters through `toString`, so guarding only `join` left
11168/// `a=[]; a.push(a); a.join('-')` recursing until the native stack aborted the
11169/// process. On node v26.7.0 that expression is `""`.
11170fn join_array(recv: &Value, sep: &str) -> Result<Value, String> {
11171 if !host::join_stack_push(recv) {
11172 return Ok(with_host(|h| h.new_str(String::new())));
11173 }
11174 // 23.1.3.18 step 6: the length is captured once, then each element is read
11175 // and STRINGIFIED before the next is read. Both halves are observable —
11176 // a getter or a `toString` that shrinks the array is seen by every later
11177 // element, which a read-all-then-convert pass misses.
11178 let parts = (|| -> Result<Vec<String>, String> {
11179 let len = array_len(recv);
11180 let mut out = Vec::with_capacity(len);
11181 for i in 0..len {
11182 let v = array_elem_live(recv, i)?;
11183 out.push(join_parts(std::slice::from_ref(&v))?.remove(0));
11184 }
11185 Ok(out)
11186 })();
11187 host::join_stack_pop();
11188 let s = parts?.join(sep);
11189 Ok(with_host(|h| h.new_str(s)))
11190}
11191
11192/// `Array.prototype.join`'s per-element conversion (23.1.3.18 step 4): a
11193/// `null`/`undefined` element contributes the empty string, every other element
11194/// is `ToString(element)` — which for an object means invoking its `toString`,
11195/// so `[{ toString() { return 'x' } }].join()` is `"x"` and not
11196/// `"[object Object]"`.
11197///
11198/// The all-primitive array — the overwhelmingly common one — is rendered under
11199/// a single host borrow; only an array actually holding an object pays for the
11200/// re-entrant per-element conversion.
11201fn join_parts(items: &[Value]) -> Result<Vec<String>, String> {
11202 let fast = with_host(|h| {
11203 items
11204 .iter()
11205 .map(|x| match x {
11206 Value::Undef => Some(String::new()),
11207 _ if h.is_null(x) => Some(String::new()),
11208 // A SYMBOL element is primitive but has no `ToString`, so it must
11209 // fall through to the fallible path and throw there:
11210 // `[Symbol()].join()` is a TypeError on node v26.7.0.
11211 _ if matches!(h.get(x), Some(JsObj::Symbol { .. })) => None,
11212 _ if host::is_primitive(h, x) => Some(h.str_of(x)),
11213 _ => None,
11214 })
11215 .collect::<Vec<_>>()
11216 });
11217 if fast.iter().all(Option::is_some) {
11218 return Ok(fast.into_iter().flatten().collect());
11219 }
11220 let mut out = Vec::with_capacity(items.len());
11221 for (x, p) in items.iter().zip(fast) {
11222 match p {
11223 Some(s) => out.push(s),
11224 None => {
11225 let s = host::to_string_value(x)?;
11226 out.push(with_host(|h| h.str_of(&s)));
11227 }
11228 }
11229 }
11230 Ok(out)
11231}
11232
11233/// In-place sort of `items` (shared by `sort` and `toSorted`). Stable merge
11234/// sort — O(n log n) comparisons — with the fallible JS comparator called from
11235/// the merge step; default order is by the string form of each element.
11236/// Propagates a comparator error.
11237///
11238/// This was an insertion sort, which is O(n²): sorting 200k numbers with a
11239/// comparator did not finish inside 120s (node v26.7.0: 70ms), and each
11240/// doubling of the input quadrupled the time — 1k/2k/4k/8k/16k measured at
11241/// 0.21/0.81/3.39/12.94/51.36s. The comparator contract is unchanged; only the
11242/// number of times it is called is.
11243pub(crate) fn sort_values(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11244 // 23.1.3.30 step 1: a comparator that is neither `undefined` nor callable is
11245 // rejected BEFORE any comparison runs. `[2,1].sort(null)` was reaching the
11246 // invoke path and reporting the generic `null is not a function`.
11247 let cmp = match cmp {
11248 Some(Value::Undef) => None,
11249 Some(v) if !with_host(|h| host::is_callable(h, v)) => {
11250 // V8 renders the offending value with `NoSideEffectsToString`, not
11251 // with `util.inspect`: a string appears bare (`: x`) rather than
11252 // quoted, and an array is `[object Array]` rather than `[ 1, 2 ]`.
11253 let shown = no_side_effects_string(v);
11254 return Err(host::type_error(&format!(
11255 "The comparison function must be either a function or undefined: {shown}"
11256 )));
11257 }
11258 other => other,
11259 };
11260 // 23.1.3.30.1 SortIndexedProperties: `undefined` is never handed to the
11261 // comparator — it sorts to the end after the defined values are ordered.
11262 // `[3,undefined,1].sort((x,y)=>x-y)` is `[1,3,undefined]` with ONE call on
11263 // node v26.7.0; the insertion sort called the comparator twice, on
11264 // `undefined`, and left `[3,undefined,1]`. Every element passed over here
11265 // is `undefined`, so swapping keeps the defined values in input order.
11266 let mut defined = 0;
11267 for i in 0..items.len() {
11268 if !matches!(items[i], Value::Undef) {
11269 items.swap(defined, i);
11270 defined += 1;
11271 }
11272 }
11273 merge_sort(&mut items[..defined], cmp)
11274}
11275
11276/// One SortCompare: `> 0` means `b` sorts before `a`. A comparator result runs
11277/// through ToNumber, so a NaN (or a comparator returning `undefined`) is not
11278/// `> 0` and the pair keeps its input order.
11279fn sort_compare(a: &Value, b: &Value, cmp: Option<&Value>) -> Result<f64, String> {
11280 match cmp {
11281 Some(cb) => {
11282 let v = host::invoke(cb, vec![a.clone(), b.clone()], None)?;
11283 Ok(with_host(|h| h.to_number(&v)))
11284 }
11285 None => {
11286 // 23.1.3.30.2 SortCompare with no comparator: compare the ToString
11287 // of each element by CODE UNIT (`utf16::cmp_units`), which differs
11288 // from Rust's `String` order off the BMP.
11289 let x = with_host(|h| h.str_of(a));
11290 let y = with_host(|h| h.str_of(b));
11291 if crate::utf16::cmp_units(&x, &y) == std::cmp::Ordering::Greater {
11292 Ok(1.0)
11293 } else {
11294 Ok(-1.0)
11295 }
11296 }
11297 }
11298}
11299
11300/// Bottom-up stable merge sort. Bottom-up rather than recursive so a large
11301/// array cannot walk the native stack the JS comparator also runs on, and the
11302/// two buffers are swapped each pass instead of copied back.
11303fn merge_sort(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
11304 let n = items.len();
11305 if n < 2 {
11306 return Ok(());
11307 }
11308 let mut src = items.to_vec();
11309 let mut dst = src.clone();
11310 let mut width = 1;
11311 while width < n {
11312 let mut lo = 0;
11313 while lo < n {
11314 let mid = (lo + width).min(n);
11315 let hi = (lo + 2 * width).min(n);
11316 merge(&src[lo..mid], &src[mid..hi], &mut dst[lo..hi], cmp)?;
11317 lo = hi;
11318 }
11319 std::mem::swap(&mut src, &mut dst);
11320 width *= 2;
11321 }
11322 items.clone_from_slice(&src);
11323 Ok(())
11324}
11325
11326/// Merge two sorted runs into `out`. Ties take from `left` first, which is what
11327/// makes the sort stable — `[{k:1},{k:0},{k:1},{k:0}].sort((x,y)=>x.k-y.k)`
11328/// keeps the two `k:0` entries in input order, as node does.
11329fn merge(
11330 left: &[Value],
11331 right: &[Value],
11332 out: &mut [Value],
11333 cmp: Option<&Value>,
11334) -> Result<(), String> {
11335 let (mut i, mut j, mut k) = (0, 0, 0);
11336 while i < left.len() && j < right.len() {
11337 if sort_compare(&left[i], &right[j], cmp)? > 0.0 {
11338 out[k] = right[j].clone();
11339 j += 1;
11340 } else {
11341 out[k] = left[i].clone();
11342 i += 1;
11343 }
11344 k += 1;
11345 }
11346 for v in left[i..].iter().chain(&right[j..]) {
11347 out[k] = v.clone();
11348 k += 1;
11349 }
11350 Ok(())
11351}
11352
11353/// Recursively flatten `items` up to `depth` levels into `out`. `depth` is an
11354/// f64 so `Infinity` (full flatten) and finite counts share one path.
11355///
11356/// `flat` has NO cycle cut — unlike `join`, V8 lets it run out of stack, and
11357/// `a=[1]; a.push(a); a.flat(Infinity)` is `RangeError: Maximum call stack size
11358/// exceeded` on node v26.7.0. That is reproduced by checking the same native
11359/// stack floor the VM does, so the answer is a catchable error rather than the
11360/// `fatal runtime error: stack overflow` abort this used to produce.
11361/// `FlattenIntoArray` (23.1.3.13.1). Takes the source ARRAY rather than its
11362/// elements because each level tests `HasProperty` before recursing, so a hole
11363/// contributes nothing at any depth: `[1,,3].flat()` is the dense `[1, 3]`.
11364fn flatten_into(src: &Value, depth: f64, out: &mut Vec<Value>) -> Result<(), String> {
11365 if host::stack_exhausted() {
11366 return Err(host::stack_overflow_error());
11367 }
11368 let items = array_items(src);
11369 let holes = absent_set(src);
11370 for (i, it) in items.into_iter().enumerate() {
11371 if holes.contains(&i) {
11372 continue;
11373 }
11374 let nested = depth > 0.0 && with_host(|h| h.kind_of(&it)) == Some(ObjKind::Array);
11375 if nested {
11376 flatten_into(&it, depth - 1.0, out)?;
11377 } else {
11378 out.push(it);
11379 }
11380 }
11381 Ok(())
11382}
11383
11384fn array_splice(recv: &Value, args: Vec<Value>) -> Result<Value, String> {
11385 let len = array_len(recv);
11386 let start = {
11387 let s = arg_num(&args, 0);
11388 if s < 0.0 {
11389 ((len as f64 + s).max(0.0)) as usize
11390 } else {
11391 (s as usize).min(len)
11392 }
11393 };
11394 let delete = if args.len() >= 2 {
11395 (arg_num(&args, 1).max(0.0) as usize).min(len - start)
11396 } else {
11397 len - start
11398 };
11399 let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
11400 let inserted = inserts.len();
11401 // The receiver's holes shift by (inserted - deleted) past the cut, and the
11402 // ones inside the cut move into the RETURNED array at their offset there.
11403 let holes = hole_set(recv);
11404 let removed = with_host(|h| {
11405 if let Some(JsObj::Array(items)) = h.get_mut(recv) {
11406 let removed: Vec<Value> = items.splice(start..start + delete, inserts).collect();
11407 removed
11408 } else {
11409 Vec::new()
11410 }
11411 });
11412 let spliced = with_host(|h| {
11413 h.install_holes(
11414 recv,
11415 holes
11416 .iter()
11417 .filter_map(|&i| {
11418 if i < start {
11419 Some(i)
11420 } else if i < start + delete {
11421 None
11422 } else {
11423 Some(i - delete + inserted)
11424 }
11425 })
11426 .collect(),
11427 );
11428 (removed, holes.clone())
11429 });
11430 // The REMOVED elements come back as an array of the receiver's species
11431 // (23.1.3.31 step 8), so a subclass gets one of its own kind.
11432 let (removed, holes) = spliced;
11433 let out = array_species_create(recv, removed)?;
11434 with_host(|h| {
11435 h.install_holes(
11436 &out,
11437 holes
11438 .iter()
11439 .filter(|&&i| i >= start && i < start + delete)
11440 .map(|&i| i - start)
11441 .collect(),
11442 );
11443 });
11444 Ok(out)
11445}
11446
11447fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
11448 let norm = |v: f64| -> usize {
11449 if v < 0.0 {
11450 ((len as f64 + v).max(0.0)) as usize
11451 } else {
11452 (v as usize).min(len)
11453 }
11454 };
11455 let lo = if args.is_empty() || matches!(args[0], Value::Undef) {
11456 0
11457 } else {
11458 norm(arg_num(args, 0))
11459 };
11460 let hi = if args.len() < 2 || matches!(args[1], Value::Undef) {
11461 len
11462 } else {
11463 norm(arg_num(args, 1))
11464 };
11465 // A start at or past the end (`'World'.slice(2, 1)`) yields the empty range,
11466 // never a reversed one: JS `slice` clamps `end` up to `start`.
11467 (lo, hi.max(lo))
11468}
11469
11470/// The argument positions each `String.prototype` method coerces with
11471/// `ToNumber` rather than `ToString`. Everything not listed is a string
11472/// position — which matters only for a SYMBOL argument, the one value both
11473/// conversions refuse, and refuse with different wording.
11474///
11475/// Measured per method and per position: `'x'.indexOf(sym)` reports the STRING
11476/// message and `'x'.indexOf('a', sym)` the NUMBER one, and `padStart` is the
11477/// pair the other way round (a length then a pad string).
11478const STRING_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11479 ("at", &[0]),
11480 ("charAt", &[0]),
11481 ("charCodeAt", &[0]),
11482 ("codePointAt", &[0]),
11483 ("endsWith", &[1]),
11484 ("includes", &[1]),
11485 ("indexOf", &[1]),
11486 ("lastIndexOf", &[1]),
11487 ("padEnd", &[0]),
11488 ("padStart", &[0]),
11489 ("repeat", &[0]),
11490 ("slice", &[0, 1]),
11491 ("split", &[1]),
11492 ("startsWith", &[1]),
11493 ("substr", &[0, 1]),
11494 ("substring", &[0, 1]),
11495];
11496
11497/// Reject a SYMBOL argument before any string method coerces it. 7.1.17 and
11498/// 7.1.4 both refuse one, so `'x'.padStart(3, sym)` is a TypeError where this
11499/// rendered `Symbol(d)` into the result — silently, which is the shape of
11500/// mistake that makes a symbol key leak into text.
11501fn reject_symbol_args(name: &str, args: &[Value]) -> Result<(), String> {
11502 let numeric = STRING_METHOD_NUMERIC_ARGS
11503 .iter()
11504 .find(|(m, _)| *m == name)
11505 .map(|(_, ps)| *ps)
11506 .unwrap_or(&[]);
11507 for (i, a) in args.iter().enumerate() {
11508 if with_host(|h| matches!(h.get(a), Some(JsObj::Symbol { .. }))) {
11509 let kind = if numeric.contains(&i) {
11510 "number"
11511 } else {
11512 "string"
11513 };
11514 return Err(host::type_error(&format!(
11515 "Cannot convert a Symbol value to a {kind}"
11516 )));
11517 }
11518 }
11519 Ok(())
11520}
11521
11522/// Coerce a string method's arguments the way 22.1.3.x does, BEFORE any arm
11523/// reads them: a numeric position through `ToNumber`, every other through
11524/// `ToString`. Both run a user `valueOf`/`toString`, and none of them ran —
11525/// `'x'.padStart({valueOf: () => 3})` produced `"x"` and
11526/// `'x'.concat({toString: () => 'y'})` produced `"x[object Object]"`.
11527///
11528/// The positions that must NOT be coerced are the ones with their own protocol:
11529/// a RegExp or a `Symbol.replace`/`split`/`match`/`search` carrier at position
11530/// 0 of the method that honours it, and a callable REPLACEMENT at position 1 of
11531/// `replace`/`replaceAll`. Each of those already has a path that handles the
11532/// value as an object, and stringifying it first would take that path away.
11533/// The argument positions each `Array.prototype` method coerces with
11534/// `ToNumber` (23.1.3.x). Everything not listed is a VALUE position and must be
11535/// left alone: `fill`'s first argument, `with`'s second and `splice`'s items
11536/// are stored as given, and `indexOf`/`includes` compare their first argument
11537/// without converting it.
11538const ARRAY_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11539 ("at", &[0]),
11540 ("copyWithin", &[0, 1, 2]),
11541 ("fill", &[1, 2]),
11542 ("flat", &[0]),
11543 ("includes", &[1]),
11544 ("indexOf", &[1]),
11545 ("lastIndexOf", &[1]),
11546 ("slice", &[0, 1]),
11547 ("splice", &[0, 1]),
11548 ("toSpliced", &[0, 1]),
11549 ("with", &[0]),
11550];
11551
11552/// The same for `Number.prototype`. `toLocaleString` takes a LOCALE, not a
11553/// number, and is deliberately absent.
11554const NUMBER_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
11555 ("toExponential", &[0]),
11556 ("toFixed", &[0]),
11557 ("toPrecision", &[0]),
11558 ("toString", &[0]),
11559];
11560
11561/// Replace the listed argument positions with their `ToNumber` value, running a
11562/// user `valueOf` and propagating a throw from it. Every one of these read the
11563/// argument with an INFALLIBLE conversion that does no `ToPrimitive` at all, so
11564/// `[1,2,3].slice({valueOf: () => 1})` sliced from 0 and `(1.234).toFixed(obj)`
11565/// was a RangeError.
11566/// `ToNumber(args[i])`, running a user `valueOf` and propagating its throw.
11567fn to_number_arg(args: &[Value], i: usize) -> Result<f64, String> {
11568 let v = args.get(i).cloned().unwrap_or(Value::Undef);
11569 let p = host::to_primitive(&v, "number")?;
11570 Ok(with_host(|h| h.to_number(&p)))
11571}
11572
11573fn coerce_numeric_args(
11574 table: &[(&str, &[usize])],
11575 name: &str,
11576 mut args: Vec<Value>,
11577) -> Result<Vec<Value>, String> {
11578 let Some((_, positions)) = table.iter().find(|(m, _)| *m == name) else {
11579 return Ok(args);
11580 };
11581 for &i in *positions {
11582 let Some(a) = args.get(i) else { continue };
11583 if matches!(a, Value::Undef) {
11584 continue;
11585 }
11586 let p = host::to_primitive(a, "number")?;
11587 args[i] = Value::Float(with_host(|h| h.to_number(&p)));
11588 }
11589 Ok(args)
11590}
11591
11592/// `RegExpCreate(v, flags)` — the regexp a string method builds from a
11593/// non-RegExp argument. An empty/absent argument makes the empty pattern, which
11594/// matches at position 0.
11595fn regexp_from_arg(v: &Value, flags: &str) -> Result<Value, String> {
11596 let src = if matches!(v, Value::Undef) {
11597 String::new()
11598 } else {
11599 with_host(|h| h.str_of(v))
11600 };
11601 let fv = with_host(|h| h.new_str(flags.to_string()));
11602 let sv = with_host(|h| h.new_str(src));
11603 regexp_ctor(&[sv, fv])
11604}
11605
11606fn coerce_string_args(name: &str, args: Vec<Value>) -> Result<Vec<Value>, String> {
11607 let numeric = STRING_METHOD_NUMERIC_ARGS
11608 .iter()
11609 .find(|(m, _)| *m == name)
11610 .map(|(_, ps)| *ps)
11611 .unwrap_or(&[]);
11612 let protocol = match name {
11613 "replace" | "replaceAll" => Some("@@replace"),
11614 "split" => Some("@@split"),
11615 "match" => Some("@@match"),
11616 "matchAll" => Some("@@matchAll"),
11617 "search" => Some("@@search"),
11618 // These three do not CONSUME `Symbol.match`, they reject a value that
11619 // carries it (22.1.3.7/23/24 step 3 — `IsRegExp`). Exempting it keeps
11620 // the object intact so that check still sees one; stringifying first
11621 // turned the TypeError into an ordinary search.
11622 "startsWith" | "endsWith" | "includes" => Some("@@match"),
11623 _ => None,
11624 };
11625 let mut out = Vec::with_capacity(args.len());
11626 for (i, a) in args.into_iter().enumerate() {
11627 if matches!(a, Value::Undef) {
11628 out.push(a);
11629 continue;
11630 }
11631 if numeric.contains(&i) {
11632 let p = host::to_primitive(&a, "number")?;
11633 out.push(Value::Float(with_host(|h| h.to_number(&p))));
11634 continue;
11635 }
11636 // The IsRegExp trio tests `Symbol.match` for TRUTHINESS (7.2.8 step 2),
11637 // not for presence: an object carrying `[Symbol.match]: false` is NOT a
11638 // regexp and coerces like anything else. The consuming protocols use
11639 // `GetMethod`, which additionally requires a callable.
11640 let is_regexp_like = matches!(name, "startsWith" | "endsWith" | "includes");
11641 let carries = |p: &str| match host::protocol_lookup(&a, p) {
11642 Ok(Some(m)) => {
11643 if is_regexp_like {
11644 with_host(|h| h.truthy(&m))
11645 } else {
11646 with_host(|h| host::is_callable(h, &m))
11647 }
11648 }
11649 _ => false,
11650 };
11651 let exempt = with_host(|h| matches!(h.get(&a), Some(JsObj::RegExp(_))))
11652 || (i == 0 && protocol.is_some_and(carries))
11653 || (i == 1
11654 && matches!(name, "replace" | "replaceAll")
11655 && with_host(|h| host::is_callable(h, &a)));
11656 if exempt {
11657 out.push(a);
11658 continue;
11659 }
11660 out.push(host::to_string_value(&a)?);
11661 }
11662 Ok(out)
11663}
11664
11665fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
11666 reject_symbol_args(name, &args)?;
11667 let args = coerce_string_args(name, args)?;
11668 // Every index-bearing method below counts UTF-16 code units, so they all
11669 // work off this one decoding rather than off `s.chars()` (code points),
11670 // which agrees only on the BMP. `@@iterator` is the deliberate exception.
11671 let u = crate::utf16::Units::of(s);
11672 match name {
11673 // `for…of` / spread over a string iterates CODE POINTS, not code units:
11674 // `[..."𝒳"]` is one element in node even though `"𝒳".length` is 2. This
11675 // is the one string operation that is specified in chars, so it stays
11676 // on `s.chars()` on purpose — do not "fix" it to match the others.
11677 "@@iterator" => {
11678 let items: Vec<Value> = s.chars().map(|c| new_s(c.to_string())).collect();
11679 Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
11680 }
11681 "toUpperCase" => Ok(new_s(s.to_uppercase())),
11682 "toLowerCase" => Ok(new_s(s.to_lowercase())),
11683 // `toLocaleUpperCase`/`toLocaleLowerCase` (22.1.3.26/22.1.3.24) differ
11684 // from the plain forms only for the locale-specific mappings (Turkish
11685 // dotless i, Lithuanian accents); with no locale argument they are the
11686 // Unicode Default Case Conversion, which is exactly `to_uppercase`/
11687 // `to_lowercase`. They threw `is not a function` before, so the common
11688 // no-argument call — the only form this runtime can answer, since it
11689 // carries no ICU — failed outright rather than agreeing with node.
11690 // A locale ARGUMENT is accepted and ignored; `'I'.toLocaleLowerCase('tr')`
11691 // is `'i'` here and `'ı'` in node.
11692 // `String.prototype.toLocaleString` (22.1.3.27) is `toString` — a string
11693 // has no locale rendering. Missing it made an ARRAY of strings fail too,
11694 // since `Array.prototype.toLocaleString` invokes it per element.
11695 "toLocaleString" => Ok(new_s(s.to_string())),
11696 "toLocaleUpperCase" => Ok(new_s(s.to_uppercase())),
11697 "toLocaleLowerCase" => Ok(new_s(s.to_lowercase())),
11698 // Locale comparison (ASCII approximation of ICU collation): primary by
11699 // case-folded order, then lowercase sorts before uppercase at a tie.
11700 "localeCompare" => {
11701 let other = with_host(|h| h.str_of(&arg0(&args)));
11702 let (la, lb) = (s.to_lowercase(), other.to_lowercase());
11703 let r = match la.cmp(&lb) {
11704 std::cmp::Ordering::Less => -1.0,
11705 std::cmp::Ordering::Greater => 1.0,
11706 std::cmp::Ordering::Equal => {
11707 let mut t = 0.0;
11708 for (ca, cb) in s.chars().zip(other.chars()) {
11709 if ca != cb {
11710 t = if ca.is_lowercase() { -1.0 } else { 1.0 };
11711 break;
11712 }
11713 }
11714 t
11715 }
11716 };
11717 Ok(Value::Float(r))
11718 }
11719 // `String.prototype.normalize` (22.1.3.15) — real UAX-15 normalization.
11720 //
11721 // This used to return the receiver unchanged and only validate the FORM
11722 // argument, which made every one of the four forms a no-op: `"Å"` (NFC,
11723 // one code point) and `"Å"` (NFD, two) stayed distinct under
11724 // `.normalize()`, so the standard way to compare Unicode text for
11725 // canonical equivalence silently answered `false`, and `NFKC` never
11726 // folded a compatibility character (`"fi"` stayed one code point instead
11727 // of becoming `"fi"`). The tables come from `unicode-normalization`.
11728 "normalize" => {
11729 use unicode_normalization::UnicodeNormalization;
11730 let form = match args.first() {
11731 Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
11732 _ => "NFC".to_string(),
11733 };
11734 let out = match form.as_str() {
11735 "NFC" => s.nfc().collect::<String>(),
11736 "NFD" => s.nfd().collect::<String>(),
11737 "NFKC" => s.nfkc().collect::<String>(),
11738 "NFKD" => s.nfkd().collect::<String>(),
11739 _ => {
11740 return Err(host::range_error(
11741 "The normalization form should be one of NFC, NFD, NFKC, NFKD.",
11742 ))
11743 }
11744 };
11745 Ok(new_s(out))
11746 }
11747 // ES2024 well-formedness (22.1.3.9 / 22.1.3.29). A `String` here is a
11748 // Rust `String`, whose `char` type EXCLUDES `U+D800..=U+DFFF`, so every
11749 // value this runtime can hold is well-formed by construction and
11750 // `toWellFormed` has nothing to replace. Both answers are therefore
11751 // exact for every string that survives storage; the one case node
11752 // answers differently is a surrogate half extracted by `charAt`/`slice`,
11753 // which is already `U+FFFD` here — the documented lone-surrogate
11754 // boundary in `utf16`, not a separate gap.
11755 "isWellFormed" => Ok(Value::Bool(true)),
11756 "toWellFormed" => Ok(new_s(s.to_string())),
11757 // The JS `WhiteSpace` set, not Rust's — they differ on `U+FEFF`.
11758 "trim" => Ok(new_s(crate::utf16::js_trim(s).to_string())),
11759 "trimStart" => Ok(new_s(crate::utf16::js_trim_start(s).to_string())),
11760 "trimEnd" => Ok(new_s(crate::utf16::js_trim_end(s).to_string())),
11761 "toString" | "valueOf" => Ok(new_s(s.to_string())),
11762 "charAt" => {
11763 let at = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit_str(i));
11764 Ok(new_s(at.unwrap_or_default()))
11765 }
11766 "at" => {
11767 let n = arg_num(&args, 0);
11768 // A negative position counts back from the end; `NaN` is 0. An
11769 // infinite position is out of range in either direction.
11770 let i = if n.is_nan() {
11771 Some(0i64)
11772 } else if n.is_finite() {
11773 let i = n.trunc() as i64;
11774 Some(if i < 0 { i + u.len() as i64 } else { i })
11775 } else {
11776 None
11777 };
11778 match i
11779 .and_then(|i| usize::try_from(i).ok())
11780 .and_then(|i| u.unit_str(i))
11781 {
11782 Some(c) => Ok(new_s(c)),
11783 None => Ok(Value::Undef),
11784 }
11785 }
11786 // `charCodeAt` reports the bare code UNIT — the high surrogate of an
11787 // astral character, not the character. `codePointAt` looks ahead one
11788 // unit and reports the whole scalar when the pair is well formed. They
11789 // agree everywhere on the BMP, which is why they used to share an arm.
11790 // They also disagree OUT of range: `charCodeAt` yields `NaN` while
11791 // `codePointAt` yields `undefined` (measured on node v26.7.0).
11792 "charCodeAt" => {
11793 let unit = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit(i));
11794 Ok(Value::Float(unit.map(f64::from).unwrap_or(f64::NAN)))
11795 }
11796 "codePointAt" => match unit_pos(arg_num(&args, 0)).and_then(|i| u.code_point(i)) {
11797 Some(cp) => Ok(Value::Float(f64::from(cp))),
11798 None => Ok(Value::Undef),
11799 },
11800 // The search quartet all honor their optional position argument.
11801 // `"a&b&c".indexOf("&", 2)` must be 3, not 1 — body-parser's
11802 // parameterCount walks a query string with exactly that call.
11803 "indexOf" => {
11804 let needle = needle_units(&args);
11805 let from = clamp_pos(arg_num(&args, 1), u.len());
11806 Ok(Value::Float(
11807 search_from(u.as_slice(), needle.as_slice(), from)
11808 .map(|i| i as f64)
11809 .unwrap_or(-1.0),
11810 ))
11811 }
11812 "lastIndexOf" => {
11813 let needle = needle_units(&args);
11814 // An absent or NaN position means "search the whole string".
11815 let n = arg_num(&args, 1);
11816 let upto = if n.is_nan() {
11817 u.len()
11818 } else {
11819 clamp_pos(n, u.len())
11820 };
11821 Ok(Value::Float(
11822 search_last(u.as_slice(), needle.as_slice(), upto)
11823 .map(|i| i as f64)
11824 .unwrap_or(-1.0),
11825 ))
11826 }
11827 // 22.1.3.7/22.1.3.23/22.1.3.14 step 2: these three reject a REGEXP
11828 // argument outright, and `IsRegExp` is what decides — so an object
11829 // advertising `Symbol.match` is rejected too. None of them checked.
11830 "startsWith" | "endsWith" | "includes" if is_regexp_arg(&arg0(&args)) => {
11831 Err(host::type_error(&format!(
11832 "First argument to String.prototype.{name} must not be a regular expression"
11833 )))
11834 }
11835 "includes" => {
11836 let needle = needle_units(&args);
11837 let from = clamp_pos(arg_num(&args, 1), u.len());
11838 Ok(Value::Bool(
11839 search_from(u.as_slice(), needle.as_slice(), from).is_some(),
11840 ))
11841 }
11842 "startsWith" => {
11843 let needle = needle_units(&args);
11844 let from = clamp_pos(arg_num(&args, 1), u.len());
11845 Ok(Value::Bool(
11846 u.as_slice()[from..].starts_with(needle.as_slice()),
11847 ))
11848 }
11849 "endsWith" => {
11850 let needle = needle_units(&args);
11851 // The 2nd argument is where the string is treated as ENDING.
11852 let end = if args.len() < 2 || matches!(args[1], Value::Undef) {
11853 u.len()
11854 } else {
11855 clamp_pos(arg_num(&args, 1), u.len())
11856 };
11857 Ok(Value::Bool(
11858 u.as_slice()[..end].ends_with(needle.as_slice()),
11859 ))
11860 }
11861 "slice" => {
11862 let (lo, hi) = slice_bounds(&args, u.len());
11863 Ok(new_s(u.slice(lo, hi)))
11864 }
11865 "substring" => {
11866 let mut a = arg_num(&args, 0).max(0.0) as usize;
11867 let mut b = if args.len() < 2 || matches!(args[1], Value::Undef) {
11868 u.len()
11869 } else {
11870 (arg_num(&args, 1).max(0.0) as usize).min(u.len())
11871 };
11872 a = a.min(u.len());
11873 if a > b {
11874 std::mem::swap(&mut a, &mut b);
11875 }
11876 Ok(new_s(u.slice(a, b)))
11877 }
11878 "substr" => {
11879 // A negative start counts from the end: max(len + start, 0).
11880 let len = u.len() as i64;
11881 let mut start = arg_num(&args, 0) as i64;
11882 if start < 0 {
11883 start = (len + start).max(0);
11884 }
11885 let start = (start as usize).min(u.len());
11886 let count = if args.len() >= 2 {
11887 arg_num(&args, 1).max(0.0) as usize
11888 } else {
11889 u.len()
11890 };
11891 let end = start.saturating_add(count).min(u.len());
11892 Ok(new_s(u.slice(start, end)))
11893 }
11894 "repeat" => {
11895 let n = arg_num(&args, 0);
11896 // `RangeError`, not `TypeError`, and the count is named:
11897 // `"x".repeat(-1)` is `RangeError: Invalid count value: -1`.
11898 if n < 0.0 || !n.is_finite() {
11899 return Err(host::range_error(&format!(
11900 "Invalid count value: {}",
11901 host::fmt_number(n)
11902 )));
11903 }
11904 // The PRODUCT is what V8 bounds, so `''.repeat(2**53)` is legal (and
11905 // `''`) while `'ab'.repeat(268435445)` is not: measured on node
11906 // v26.7.0, `'ab'.repeat(268435444).length` is 536870888 and one more
11907 // is `RangeError: Invalid string length`.
11908 if n * crate::utf16::len(s) as f64 > host::MAX_STRING_LENGTH as f64 {
11909 return Err(host::invalid_string_length());
11910 }
11911 Ok(new_s(s.repeat(n as usize)))
11912 }
11913 "concat" => {
11914 let mut out = s.to_string();
11915 for a in &args {
11916 out.push_str(&with_host(|h| h.str_of(a)));
11917 }
11918 Ok(new_s(out))
11919 }
11920 "padStart" => Ok(new_s(pad(s, &args, true)?)),
11921 "padEnd" => Ok(new_s(pad(s, &args, false)?)),
11922 // Regex-taking string methods: dispatch to the regexp module when the
11923 // argument is a RegExp; otherwise keep the plain-string behavior.
11924 // 22.1.3.20 step 2.a: `replaceAll` validates the `g` flag BEFORE it
11925 // consults `Symbol.replace`, so a non-global regexp is a TypeError even
11926 // though a RegExp does define that method. Delegating first skipped the
11927 // check and silently did a single replacement.
11928 "replaceAll"
11929 if is_regexp_arg(&arg0(&args))
11930 && !with_host(
11931 |h| matches!(h.get(&arg0(&args)), Some(JsObj::RegExp(r)) if r.global),
11932 ) =>
11933 {
11934 Err(host::type_error(
11935 "String.prototype.replaceAll called with a non-global RegExp argument",
11936 ))
11937 }
11938 "match" | "matchAll" | "search" | "split" | "replace" | "replaceAll"
11939 if symbol_protocol(
11940 &arg0(&args),
11941 match name {
11942 "match" => "@@match",
11943 "matchAll" => "@@matchAll",
11944 "search" => "@@search",
11945 "split" => "@@split",
11946 _ => "@@replace",
11947 },
11948 )
11949 .is_some() =>
11950 {
11951 let sym = match name {
11952 "match" => "@@match",
11953 "matchAll" => "@@matchAll",
11954 "search" => "@@search",
11955 "split" => "@@split",
11956 _ => "@@replace",
11957 };
11958 let f = symbol_protocol(&arg0(&args), sym).expect("guard checked");
11959 let sv = with_host(|h| h.new_str(s.to_string()));
11960 let mut rest = vec![sv];
11961 rest.extend(args.iter().skip(1).cloned());
11962 host::invoke(&f, rest, Some(arg0(&args)))
11963 }
11964 // 22.1.3.13/14: a non-RegExp argument is turned INTO one
11965 // (`RegExpCreate(regexp, …)`), so `'abc'.match('b')` matches. It
11966 // answered `null` for every string argument, which reads as "no match"
11967 // — the one answer a caller cannot tell from a real failure.
11968 // `matchAll` builds its with `g`, which 22.1.3.14 requires.
11969 "match" => {
11970 let a = arg0(&args);
11971 let re = if is_regexp_arg(&a) {
11972 a
11973 } else {
11974 regexp_from_arg(&a, "")?
11975 };
11976 crate::regexp::str_match(s, &re)
11977 }
11978 "matchAll" => {
11979 let a = arg0(&args);
11980 let re = if is_regexp_arg(&a) {
11981 a
11982 } else {
11983 regexp_from_arg(&a, "g")?
11984 };
11985 crate::regexp::str_match_all(s, &re)
11986 }
11987 "search" => {
11988 if is_regexp_arg(&arg0(&args)) {
11989 crate::regexp::str_search(s, &arg0(&args))
11990 } else {
11991 // 22.1.3.17 builds a RegExp from the argument, so a
11992 // METACHARACTER matches as one: `'a.c'.search('.')` is 0, not
11993 // 1. The substring approximation this replaces agreed only for
11994 // a literal needle, and answered -1 for an absent argument
11995 // where the empty pattern matches at 0.
11996 let re = regexp_from_arg(&arg0(&args), "")?;
11997 crate::regexp::str_search(s, &re)
11998 }
11999 }
12000 "replace" => {
12001 let pat = arg0(&args);
12002 let repl = args.get(1).cloned().unwrap_or(Value::Undef);
12003 if is_regexp_arg(&pat) {
12004 crate::regexp::str_replace_regex(s, &pat, &repl, false)
12005 } else if with_host(|h| host::is_callable(h, &repl)) {
12006 Ok(new_s(replace_str_fn(
12007 s,
12008 &with_host(|h| h.str_of(&pat)),
12009 &repl,
12010 false,
12011 )?))
12012 } else {
12013 let from = with_host(|h| h.str_of(&pat));
12014 let to = with_host(|h| h.str_of(&repl));
12015 Ok(new_s(replace_str_plain(s, &from, &to, false)))
12016 }
12017 }
12018 "replaceAll" => {
12019 let pat = arg0(&args);
12020 let repl = args.get(1).cloned().unwrap_or(Value::Undef);
12021 if is_regexp_arg(&pat) {
12022 // 22.1.3.20 step 2: a non-global regexp is a TypeError here,
12023 // because `replaceAll` cannot honour "all" without `g`. This
12024 // used to replace only the first match and say nothing.
12025 let global = with_host(|h| match h.get(&pat) {
12026 Some(JsObj::RegExp(r)) => r.global,
12027 _ => true,
12028 });
12029 if !global {
12030 return Err(host::type_error(
12031 "String.prototype.replaceAll called with a non-global RegExp argument",
12032 ));
12033 }
12034 crate::regexp::str_replace_regex(s, &pat, &repl, true)
12035 } else if with_host(|h| host::is_callable(h, &repl)) {
12036 Ok(new_s(replace_str_fn(
12037 s,
12038 &with_host(|h| h.str_of(&pat)),
12039 &repl,
12040 true,
12041 )?))
12042 } else {
12043 let from = with_host(|h| h.str_of(&pat));
12044 let to = with_host(|h| h.str_of(&repl));
12045 Ok(new_s(replace_str_plain(s, &from, &to, true)))
12046 }
12047 }
12048 "split" => {
12049 if is_regexp_arg(&arg0(&args)) {
12050 let limit = args
12051 .get(1)
12052 .filter(|v| !matches!(v, Value::Undef))
12053 .map(|v| with_host(|h| h.to_number(v)) as usize);
12054 return crate::regexp::str_split_regex(s, &arg0(&args), limit);
12055 }
12056 let mut parts: Vec<Value> = if args.is_empty() || matches!(args[0], Value::Undef) {
12057 vec![new_s(s.to_string())]
12058 } else {
12059 let sep = with_host(|h| h.str_of(&args[0]));
12060 if sep.is_empty() {
12061 // `split('')` yields one element per code UNIT, so an astral
12062 // character becomes its two surrogate halves.
12063 (0..u.len())
12064 .filter_map(|i| u.unit_str(i))
12065 .map(new_s)
12066 .collect()
12067 } else {
12068 s.split(&sep as &str)
12069 .map(|p| new_s(p.to_string()))
12070 .collect()
12071 }
12072 };
12073 // Optional limit: keep at most `limit` substrings.
12074 if let Some(lim) = args.get(1).filter(|v| !matches!(v, Value::Undef)) {
12075 let n = with_host(|h| h.to_number(lim));
12076 if n.is_finite() && n >= 0.0 {
12077 parts.truncate(n as usize);
12078 }
12079 }
12080 Ok(with_host(|h| h.new_array(parts)))
12081 }
12082 _ => Err(host::type_error(&format!("{name} is not a function"))),
12083 }
12084}
12085
12086/// GetSubstitution (22.1.3.19) for a STRING search value.
12087///
12088/// `String.prototype.replace`/`replaceAll` expand the same `$` patterns whether
12089/// the pattern is a regexp or a plain string, but the string path here did a
12090/// raw `str::replace` and passed the template through verbatim — so
12091/// `'abc'.replace('b', '[$&]')` produced `a[$&]c` instead of `a[b]c`. The
12092/// regexp path has always expanded them.
12093///
12094/// A string search captures nothing, so only `$$`, `$&`, `` $` `` and `$'`
12095/// apply; `$1` and `$<name>` have no referent and stay literal, which is also
12096/// what node does.
12097fn substitute_plain(templ: &str, matched: &str, position: usize, subject: &str) -> String {
12098 let chars: Vec<char> = templ.chars().collect();
12099 let mut out = String::new();
12100 let mut i = 0;
12101 while i < chars.len() {
12102 if chars[i] == '$' && i + 1 < chars.len() {
12103 match chars[i + 1] {
12104 '$' => {
12105 out.push('$');
12106 i += 2;
12107 continue;
12108 }
12109 '&' => {
12110 out.push_str(matched);
12111 i += 2;
12112 continue;
12113 }
12114 '`' => {
12115 out.push_str(&subject[..position]);
12116 i += 2;
12117 continue;
12118 }
12119 '\'' => {
12120 out.push_str(&subject[position + matched.len()..]);
12121 i += 2;
12122 continue;
12123 }
12124 _ => {}
12125 }
12126 }
12127 out.push(chars[i]);
12128 i += 1;
12129 }
12130 out
12131}
12132
12133/// `replace`/`replaceAll` with a string pattern and a string replacement,
12134/// expanding each match's `$` patterns against its own position.
12135fn replace_str_plain(s: &str, from: &str, to: &str, all: bool) -> String {
12136 if from.is_empty() && !all {
12137 return format!("{}{s}", substitute_plain(to, "", 0, s));
12138 }
12139 let mut out = String::new();
12140 let mut rest = 0usize;
12141 while let Some(rel) = s[rest..].find(from) {
12142 let at = rest + rel;
12143 out.push_str(&s[rest..at]);
12144 out.push_str(&substitute_plain(to, from, at, s));
12145 rest = at + from.len();
12146 if !all {
12147 break;
12148 }
12149 // An empty pattern matches between every character; step one along so
12150 // the scan terminates.
12151 if from.is_empty() {
12152 if rest >= s.len() {
12153 break;
12154 }
12155 let step = s[rest..].chars().next().map(|c| c.len_utf8()).unwrap_or(1);
12156 out.push_str(&s[rest..rest + step]);
12157 rest += step;
12158 }
12159 }
12160 out.push_str(&s[rest..]);
12161 out
12162}
12163
12164fn new_s(s: String) -> Value {
12165 with_host(|h| h.new_str(s))
12166}
12167
12168/// Where a forward `indexOf`/`includes` search starts, given the optional
12169/// `fromIndex` (23.1.3.17 steps 4-6, 23.1.3.16 steps 5-7). A negative value
12170/// counts back from the end and clamps at 0; absent or `NaN` is 0. A start at
12171/// or past the end finds nothing, which callers report as `-1` / `false`.
12172pub(crate) fn search_start(n: f64, len: usize) -> usize {
12173 if n.is_nan() {
12174 return 0;
12175 }
12176 let n = n.trunc();
12177 if n >= 0.0 {
12178 if n >= len as f64 {
12179 len
12180 } else {
12181 n as usize
12182 }
12183 } else {
12184 let from_end = len as f64 + n;
12185 if from_end <= 0.0 {
12186 0
12187 } else {
12188 from_end as usize
12189 }
12190 }
12191}
12192
12193/// The INCLUSIVE index a backward `lastIndexOf` starts at (23.1.3.20 steps
12194/// 4-6), or `None` when `fromIndex` places it before the array. Absent means
12195/// the last element — which is why this takes an `Option` rather than reading
12196/// `NaN` as "absent" the way the forward form can: an explicit `NaN` is
12197/// `ToIntegerOrInfinity`'d to 0 and searches only index 0.
12198pub(crate) fn search_start_last(from: Option<f64>, len: usize) -> Option<usize> {
12199 if len == 0 {
12200 return None;
12201 }
12202 let n = match from {
12203 None => return Some(len - 1),
12204 Some(v) if v.is_nan() => 0.0,
12205 Some(v) => v.trunc(),
12206 };
12207 if n >= 0.0 {
12208 Some(if n >= len as f64 { len - 1 } else { n as usize })
12209 } else {
12210 let k = len as f64 + n;
12211 if k < 0.0 {
12212 None
12213 } else {
12214 Some(k as usize)
12215 }
12216 }
12217}
12218
12219/// `ToIntegerOrInfinity(n)` clamped into `0..=len` — the position argument of
12220/// the `String.prototype` search methods. `NaN` (an absent argument) is `0`.
12221fn clamp_pos(n: f64, len: usize) -> usize {
12222 if n.is_nan() || n <= 0.0 {
12223 0
12224 } else if n >= len as f64 {
12225 len
12226 } else {
12227 n.trunc() as usize
12228 }
12229}
12230
12231/// `ToIntegerOrInfinity(n)` as a code-unit position, or `None` when there can be
12232/// no such unit. `NaN` (an absent argument) is 0; a negative or infinite
12233/// position is out of range — `"abc".charCodeAt(-1)` is `NaN`, not `'a'`.
12234fn unit_pos(n: f64) -> Option<usize> {
12235 if n.is_nan() {
12236 Some(0)
12237 } else if n < 0.0 || !n.is_finite() {
12238 None
12239 } else {
12240 Some(n.trunc() as usize)
12241 }
12242}
12243
12244/// The search argument of `indexOf`/`includes`/`startsWith`/… as code units, so
12245/// the needle is compared in the same alphabet the haystack is indexed by.
12246fn needle_units(args: &[Value]) -> crate::utf16::Units {
12247 crate::utf16::Units::of(&with_host(|h| h.str_of(&arg0(args))))
12248}
12249
12250/// The lowest index `>= from` at which `needle` occurs in `hay`. An empty
12251/// needle matches at `from` itself, as JS specifies.
12252fn search_from(hay: &[u16], needle: &[u16], from: usize) -> Option<usize> {
12253 if needle.is_empty() {
12254 return Some(from.min(hay.len()));
12255 }
12256 if needle.len() > hay.len() {
12257 return None;
12258 }
12259 (from..=hay.len().saturating_sub(needle.len())).find(|&i| &hay[i..i + needle.len()] == needle)
12260}
12261
12262/// The highest index `<= upto` at which `needle` occurs in `hay`.
12263fn search_last(hay: &[u16], needle: &[u16], upto: usize) -> Option<usize> {
12264 if needle.is_empty() {
12265 return Some(upto.min(hay.len()));
12266 }
12267 if needle.len() > hay.len() {
12268 return None;
12269 }
12270 let last = hay.len() - needle.len();
12271 (0..=upto.min(last))
12272 .rev()
12273 .find(|&i| &hay[i..i + needle.len()] == needle)
12274}
12275
12276fn pad(s: &str, args: &[Value], start: bool) -> Result<String, String> {
12277 let target_f = arg_num(args, 0);
12278 let target = if target_f.is_finite() && target_f > 0.0 {
12279 target_f as usize
12280 } else {
12281 0
12282 };
12283 // `targetLength` and the padding both count code units: `'𝒳'.padStart(3,'-')`
12284 // is `'-𝒳'` in node, not `'--𝒳'`.
12285 let cur = crate::utf16::len(s);
12286 if cur >= target {
12287 return Ok(s.to_string());
12288 }
12289 let filler = if args.len() >= 2 {
12290 with_host(|h| h.str_of(&args[1]))
12291 } else {
12292 " ".to_string()
12293 };
12294 if filler.is_empty() {
12295 return Ok(s.to_string());
12296 }
12297 // Checked only AFTER the two short-circuits, which is the order V8 uses:
12298 // measured on node v26.7.0, `'ab'.padStart(2**40, '')` is `'ab'` while
12299 // `'ab'.padStart(536870889, 'x')` is `RangeError: Invalid string length`.
12300 if target_f > host::MAX_STRING_LENGTH as f64 {
12301 return Err(host::invalid_string_length());
12302 }
12303 let need = target - cur;
12304 let fill = crate::utf16::Units::of(&filler);
12305 // The filler repeats and is TRUNCATED to the exact unit count, which can cut
12306 // a surrogate pair — node yields a lone surrogate there, we yield U+FFFD
12307 // (see src/utf16.rs).
12308 let units: Vec<u16> = (0..need)
12309 .filter_map(|i| fill.unit(i % fill.len()))
12310 .collect();
12311 let padding = crate::utf16::to_string_lossy(&units);
12312 Ok(if start {
12313 format!("{padding}{s}")
12314 } else {
12315 format!("{s}{padding}")
12316 })
12317}
12318
12319/// V8's radix rejection, shared by `Number.prototype.toString` and
12320/// `BigInt.prototype.toString` — one string, because they are one message and
12321/// the two sites had drifted apart ("radix must be" vs V8's "radix argument
12322/// must be").
12323const RADIX_RANGE: &str = "toString() radix argument must be between 2 and 36";
12324
12325/// `BigInt.prototype` methods: `toString([radix])`, `valueOf`, `toLocaleString`.
12326fn bigint_method(b: &num_bigint::BigInt, name: &str, args: Vec<Value>) -> Result<Value, String> {
12327 match name {
12328 "toString" => {
12329 let radix = match args.first() {
12330 None | Some(Value::Undef) => 10,
12331 Some(_) => {
12332 let t = arg_num(&args, 0).trunc();
12333 if !(2.0..=36.0).contains(&t) {
12334 return Err(host::range_error(RADIX_RANGE));
12335 }
12336 t as u32
12337 }
12338 };
12339 Ok(new_s(b.to_str_radix(radix)))
12340 }
12341 // `BigInt.prototype.toLocaleString` groups thousands like the Number
12342 // one does — `(1234567n).toLocaleString()` is `1,234,567` in node, and
12343 // returning the bare digits made it the only numeric type that skipped
12344 // grouping. Same en-US-shaped output as `Number.prototype`; the
12345 // `locales`/`options` arguments are ignored (no ICU here).
12346 "toLocaleString" => {
12347 let digits = b.magnitude().to_string();
12348 let sign = if b.sign() == num_bigint::Sign::Minus {
12349 "-"
12350 } else {
12351 ""
12352 };
12353 Ok(new_s(format!("{sign}{}", group_thousands(&digits))))
12354 }
12355 "valueOf" => Ok(with_host(|h| h.new_bigint(b.clone()))),
12356 _ => Err(host::type_error(&format!("{name} is not a function"))),
12357 }
12358}
12359
12360fn number_method(n: f64, name: &str, args: Vec<Value>) -> Result<Value, String> {
12361 let args = coerce_numeric_args(NUMBER_METHOD_NUMERIC_ARGS, name, args)?;
12362 match name {
12363 "toFixed" => {
12364 let digits = arg_num(&args, 0);
12365 if !(0.0..=100.0).contains(&digits.trunc()) {
12366 return Err(host::range_error(
12367 "toFixed() digits argument must be between 0 and 100",
12368 ));
12369 }
12370 Ok(new_s(to_fixed(n, digits as usize)))
12371 }
12372 "toExponential" => {
12373 // `undefined` (or a missing argument) selects the shortest form.
12374 let f = match args.first() {
12375 None | Some(Value::Undef) => None,
12376 Some(_) => {
12377 let d = arg_num(&args, 0).trunc();
12378 if !(0.0..=100.0).contains(&d) {
12379 return Err(host::range_error(
12380 "toExponential() argument must be between 0 and 100",
12381 ));
12382 }
12383 Some(d as usize)
12384 }
12385 };
12386 Ok(new_s(to_exponential(n, f)))
12387 }
12388 "toString" => {
12389 // An out-of-range radix THROWS; it does not silently fall back to
12390 // base 10. `(1).toString(37)` returned "1" here, so a support probe
12391 // was told every radix worked.
12392 let radix = match args.first() {
12393 None | Some(Value::Undef) => 10,
12394 Some(_) => {
12395 let r = arg_num(&args, 0);
12396 let t = r.trunc();
12397 if !(2.0..=36.0).contains(&t) {
12398 return Err(host::range_error(RADIX_RANGE));
12399 }
12400 t as u32
12401 }
12402 };
12403 if radix == 10 {
12404 Ok(new_s(host::fmt_number(n)))
12405 } else {
12406 Ok(new_s(to_radix(n, radix)))
12407 }
12408 }
12409 "toPrecision" => {
12410 // `undefined` (or a missing argument) behaves like `toString()`.
12411 match args.first() {
12412 None | Some(Value::Undef) => Ok(new_s(host::fmt_number(n))),
12413 Some(_) => {
12414 let p = arg_num(&args, 0).trunc();
12415 if !(1.0..=100.0).contains(&p) {
12416 return Err(host::range_error(
12417 "toPrecision() argument must be between 1 and 100",
12418 ));
12419 }
12420 Ok(new_s(to_precision(n, p as usize)))
12421 }
12422 }
12423 }
12424 "toLocaleString" => Ok(new_s(to_locale_string(n))),
12425 "valueOf" => Ok(Value::Float(n)),
12426 _ => Err(host::type_error(&format!("{name} is not a function"))),
12427 }
12428}
12429
12430/// `Number.prototype.toLocaleString()` with the default locale and options:
12431/// integer part grouped in threes with `,`, up to 3 fraction digits (rounded
12432/// half away from zero), trailing fractional zeros dropped. Mirrors V8's default
12433/// `Intl.NumberFormat().format` output (`(12345.678).toLocaleString()` ⇒
12434/// `"12,345.678"`; `(1234.5678)` ⇒ `"1,234.568"`). `NaN`, `±Infinity`, and `-0`
12435/// render as `"NaN"`, `"∞"`/`"-∞"`, and `"-0"`.
12436fn to_locale_string(n: f64) -> String {
12437 if n.is_nan() {
12438 return "NaN".to_string();
12439 }
12440 if n.is_infinite() {
12441 return if n < 0.0 { "-∞" } else { "∞" }.to_string();
12442 }
12443 let neg = n.is_sign_negative();
12444 // Round the magnitude to at most 3 fraction digits, then drop trailing zeros
12445 // (and a bare trailing point). `to_fixed` rounds half away from zero.
12446 // `to_fixed` falls back to `ToString` at |x| ≥ 1e21 (spec 21.1.3.3 step 6),
12447 // which is exponential — and the grouping below then chopped up the
12448 // exponent, so `(1e21).toLocaleString()` was `1e,+21` instead of node's
12449 // `1,000,000,000,000,000,000,000`. Expanding the SHORTEST repr is the right
12450 // source: node groups the shortest decimal form, so `(1e100)
12451 // .toLocaleString()` is 1 followed by a hundred zeros rather than the exact
12452 // binary value `1000…159028911…`. (`BigInt(1e100)` is the exact value, a
12453 // deliberately different rule — see `bigint_ctor`.)
12454 let fixed = expand_exponential(&to_fixed(n.abs(), 3));
12455 let trimmed = match fixed.split_once('.') {
12456 Some(_) => fixed.trim_end_matches('0').trim_end_matches('.'),
12457 None => fixed.as_str(),
12458 };
12459 let (int_part, frac_part) = match trimmed.split_once('.') {
12460 Some((i, f)) => (i, Some(f)),
12461 None => (trimmed, None),
12462 };
12463 let mut out = String::new();
12464 if neg {
12465 out.push('-'); // Intl keeps the sign even for -0.
12466 }
12467 out.push_str(&group_thousands(int_part));
12468 if let Some(f) = frac_part {
12469 out.push('.');
12470 out.push_str(f);
12471 }
12472 out
12473}
12474
12475/// Write a nonnegative decimal string in plain positional form, expanding an
12476/// `e+NN` exponent into zeros. `"1e+21"` → `"1000000000000000000000"`,
12477/// `"1.5e+21"` → `"1500000000000000000000"`. A string with no exponent, or a
12478/// negative exponent (a magnitude below 1, which the caller has already rounded
12479/// to zero), is returned unchanged.
12480fn expand_exponential(s: &str) -> String {
12481 let Some((mantissa, exp)) = s.split_once(['e', 'E']) else {
12482 return s.to_string();
12483 };
12484 let Ok(exp) = exp.trim_start_matches('+').parse::<i32>() else {
12485 return s.to_string();
12486 };
12487 if exp <= 0 {
12488 return s.to_string();
12489 }
12490 let (int_digits, frac_digits) = match mantissa.split_once('.') {
12491 Some((i, f)) => (i.to_string(), f.to_string()),
12492 None => (mantissa.to_string(), String::new()),
12493 };
12494 let mut digits = int_digits;
12495 digits.push_str(&frac_digits);
12496 // The exponent consumes the fractional digits first; whatever is left
12497 // becomes trailing zeros.
12498 let zeros = exp as usize - frac_digits.len().min(exp as usize);
12499 digits.push_str(&"0".repeat(zeros));
12500 digits
12501}
12502
12503/// Insert `,` as a thousands separator into a nonnegative integer digit string.
12504fn group_thousands(int_part: &str) -> String {
12505 let bytes = int_part.as_bytes();
12506 let n = bytes.len();
12507 let mut out = String::with_capacity(n + n / 3);
12508 for (i, &b) in bytes.iter().enumerate() {
12509 if i > 0 && (n - i) % 3 == 0 {
12510 out.push(',');
12511 }
12512 out.push(b as char);
12513 }
12514 out
12515}
12516
12517/// `Number.prototype.toFixed(f)`: fixed-point with `f` fractional digits, rounding
12518/// half away from zero on the actual IEEE-754 value (so `(1.005).toFixed(2)` is
12519/// `"1.00"` because 1.005 is really 1.00499…). The sign of a negative input is
12520/// preserved even when the rounded magnitude is zero: `(-0.4).toFixed(0) === "-0"`.
12521///
12522/// The rounding is done on the value's EXACT decimal expansion (Rust's fixed
12523/// formatting is exact), not on `x * 10^f` — the latter loses precision for large
12524/// magnitudes (`(9.999999e20).toFixed(4)` must keep every integer digit).
12525fn to_fixed(n: f64, f: usize) -> String {
12526 if !n.is_finite() {
12527 return host::fmt_number(n);
12528 }
12529 // Spec: for |x| ≥ 10^21, toFixed falls back to ToString(x).
12530 if n.abs() >= 1e21 {
12531 return host::fmt_number(n);
12532 }
12533 let neg = n < 0.0;
12534 // Exact decimal with guard digits past the rounding position; then round the
12535 // digit string half-away-from-zero (nonneg operand ⇒ round-half-up).
12536 let full = format!("{:.*}", f + 25, n.abs());
12537 let mut body = round_decimal_string(&full, f);
12538 if neg {
12539 body.insert(0, '-'); // JS keeps the sign even for "-0" / "-0.00".
12540 }
12541 body
12542}
12543
12544/// Round the exact decimal string `s` (`"int.frac"`, nonnegative) to `f`
12545/// fractional digits, half away from zero, propagating carry across the point.
12546fn round_decimal_string(s: &str, f: usize) -> String {
12547 let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
12548 let mut digits: Vec<u8> = int_part
12549 .bytes()
12550 .chain(frac_part.bytes())
12551 .map(|b| b - b'0')
12552 .collect();
12553 let point = int_part.len(); // digits before the decimal point
12554 let keep = point + f; // number of leading digits to keep
12555
12556 // Round up if the first dropped digit is ≥ 5 (exact-half ⇒ up).
12557 if digits.get(keep).map(|&d| d >= 5).unwrap_or(false) {
12558 let mut i = keep;
12559 loop {
12560 if i == 0 {
12561 digits.insert(0, 1);
12562 // A new leading digit shifts the decimal point right by one.
12563 return assemble_decimal(&digits, point + 1, f);
12564 }
12565 i -= 1;
12566 if digits[i] == 9 {
12567 digits[i] = 0;
12568 } else {
12569 digits[i] += 1;
12570 break;
12571 }
12572 }
12573 }
12574 assemble_decimal(&digits, point, f)
12575}
12576
12577/// Reassemble `digits` into `"int.frac"` keeping `f` fractional digits, given that
12578/// `point` digits precede the decimal point.
12579fn assemble_decimal(digits: &[u8], point: usize, f: usize) -> String {
12580 let int_str: String = digits[..point].iter().map(|d| (d + b'0') as char).collect();
12581 let int_str = int_str.trim_start_matches('0');
12582 let int_str = if int_str.is_empty() { "0" } else { int_str };
12583 if f == 0 {
12584 return int_str.to_string();
12585 }
12586 let frac: String = digits[point..point + f]
12587 .iter()
12588 .map(|d| (d + b'0') as char)
12589 .collect();
12590 format!("{int_str}.{frac}")
12591}
12592
12593/// Round the nonnegative finite `a` to `p` significant decimal digits, half away
12594/// from zero, returning the `p` digits and the decimal exponent `e` such that the
12595/// value is `0.d…d × 10^(e+1)` (i.e. `d.d…d e±e`). Rust's `{:.*e}` rounds half to
12596/// EVEN (`(2.5)` at 1 digit would give "2"), but JS rounds half up ("3"), so the
12597/// exact digits are taken with guard positions and rounded here.
12598fn round_significant(a: f64, p: usize) -> (String, i32) {
12599 let sci = format!("{a:.*e}", p - 1 + 25);
12600 let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12601 let mut e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
12602 let all: Vec<u8> = mant
12603 .chars()
12604 .filter(|c| c.is_ascii_digit())
12605 .map(|c| c as u8 - b'0')
12606 .collect();
12607 let mut s: String = all[..p].iter().map(|d| (d + b'0') as char).collect();
12608 if all.get(p).map(|&d| d >= 5).unwrap_or(false) {
12609 // Round the p-digit mantissa up, propagating carry; a carry out of the
12610 // leading digit (`9.99 → 10`) bumps the decimal exponent by one.
12611 let mut d: Vec<u8> = all[..p].to_vec();
12612 let mut i = p;
12613 loop {
12614 if i == 0 {
12615 d.insert(0, 1);
12616 d.truncate(p);
12617 e += 1;
12618 break;
12619 }
12620 i -= 1;
12621 if d[i] == 9 {
12622 d[i] = 0;
12623 } else {
12624 d[i] += 1;
12625 break;
12626 }
12627 }
12628 s = d.iter().map(|x| (x + b'0') as char).collect();
12629 }
12630 (s, e)
12631}
12632
12633/// `Number.prototype.toExponential(f)`: one digit before the point and `f` after,
12634/// with a signed decimal exponent (`(100).toExponential(2) === "1.00e+2"`). With
12635/// `f` omitted, as many digits as uniquely identify the value are used
12636/// (`(123456).toExponential() === "1.23456e+5"`). Rounding is half away from zero
12637/// on the exact value, matching `toPrecision`.
12638fn to_exponential(n: f64, f: Option<usize>) -> String {
12639 if !n.is_finite() {
12640 return host::fmt_number(n);
12641 }
12642 let neg = n < 0.0;
12643 let a = n.abs();
12644 let (s, e) = if a == 0.0 {
12645 // Zero has no significant digits: emit "0" padded to the requested width.
12646 ("0".repeat(f.unwrap_or(0) + 1), 0)
12647 } else {
12648 match f {
12649 Some(f) => round_significant(a, f + 1),
12650 None => {
12651 // Shortest round-tripping digits (Rust's `{:e}` is shortest).
12652 let sci = format!("{a:e}");
12653 let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
12654 let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
12655 let trimmed = digits.trim_end_matches('0');
12656 let digits = if trimmed.is_empty() { "0" } else { trimmed };
12657 (digits.to_string(), exp_str.parse().unwrap_or(0))
12658 }
12659 }
12660 };
12661 let sign = if e >= 0 { '+' } else { '-' };
12662 let mag = e.abs();
12663 let body = if s.len() == 1 {
12664 format!("{s}e{sign}{mag}")
12665 } else {
12666 format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12667 };
12668 if neg {
12669 format!("-{body}")
12670 } else {
12671 body
12672 }
12673}
12674
12675/// `Number.prototype.toPrecision(p)`: `p` significant digits, switching to
12676/// exponential form when the decimal exponent `e` satisfies `e < -6` or `e ≥ p`
12677/// (ECMAScript Number.prototype.toPrecision). Trailing zeros are significant and
12678/// retained (`(100).toPrecision(5) === "100.00"`).
12679fn to_precision(n: f64, p: usize) -> String {
12680 if !n.is_finite() {
12681 return host::fmt_number(n);
12682 }
12683 if n == 0.0 {
12684 return if p == 1 {
12685 "0".into()
12686 } else {
12687 format!("0.{}", "0".repeat(p - 1))
12688 };
12689 }
12690 let neg = n < 0.0;
12691 let (s, e) = round_significant(n.abs(), p);
12692 let pp = p as i32;
12693
12694 let body = if e < -6 || e >= pp {
12695 // Exponential: first digit, optional '.rest', signed exponent.
12696 let sign = if e >= 0 { '+' } else { '-' };
12697 let mag = e.abs();
12698 if p == 1 {
12699 format!("{s}e{sign}{mag}")
12700 } else {
12701 format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
12702 }
12703 } else if e >= 0 {
12704 // e in 0..p-1: (e+1) integer digits, then any remaining as fraction.
12705 let ip = (e + 1) as usize;
12706 if ip == p {
12707 s
12708 } else {
12709 format!("{}.{}", &s[..ip], &s[ip..])
12710 }
12711 } else {
12712 // -6 ≤ e < 0: "0." then (−e−1) zeros then all p digits.
12713 format!("0.{}{}", "0".repeat((-e - 1) as usize), s)
12714 };
12715 if neg {
12716 format!("-{body}")
12717 } else {
12718 body
12719 }
12720}
12721
12722/// `Number.prototype.toString(radix)` for radix 2..=36 (radix 10 goes through
12723/// `fmt_number`). Faithful port of V8's `DoubleToRadixCString`: the integer part
12724/// is emitted exact, and fractional digits are produced up to the input double's
12725/// precision (terminating via a ULP-sized `delta`), with round-half-to-even and
12726/// carry-over back into already-written digits (and into the integer part).
12727fn to_radix(n: f64, radix: u32) -> String {
12728 if !n.is_finite() {
12729 return host::fmt_number(n);
12730 }
12731 let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
12732 let rf = radix as f64;
12733 let neg = n < 0.0;
12734 let value = n.abs();
12735
12736 let mut integer = value.floor();
12737 let mut fraction = value - integer;
12738
12739 // Fraction digits, most-significant first.
12740 let mut frac: Vec<u8> = Vec::new();
12741 // Only compute fractional digits down to the input double's precision.
12742 let mut delta = 0.5 * (next_up(value) - value);
12743 delta = delta.max(next_up(0.0));
12744 if fraction >= delta {
12745 loop {
12746 // Shift up by one digit.
12747 fraction *= rf;
12748 delta *= rf;
12749 let digit = fraction as usize;
12750 frac.push(digits[digit]);
12751 fraction -= digit as f64;
12752 // Round to even.
12753 if (fraction > 0.5 || (fraction == 0.5 && (digit & 1) == 1)) && fraction + delta > 1.0 {
12754 // Carry-over: back-trace already-written fraction digits.
12755 loop {
12756 match frac.pop() {
12757 None => {
12758 // Carried past the point into the integer part.
12759 integer += 1.0;
12760 break;
12761 }
12762 Some(c) => {
12763 let d = if c > b'9' {
12764 (c - b'a' + 10) as u32
12765 } else {
12766 (c - b'0') as u32
12767 };
12768 if d + 1 < radix {
12769 frac.push(digits[(d + 1) as usize]);
12770 break;
12771 }
12772 // digit was radix-1: drop it and keep carrying.
12773 }
12774 }
12775 }
12776 break;
12777 }
12778 if fraction < delta {
12779 break;
12780 }
12781 }
12782 }
12783
12784 // Integer digits, least-significant first (reversed at the end).
12785 let mut int_out: Vec<u8> = Vec::new();
12786 // For magnitudes ≥ 2^53, `fmod` loses low bits: pre-fill trailing zeros.
12787 while v8_exponent(integer / rf) > 0 {
12788 integer /= rf;
12789 int_out.push(b'0');
12790 }
12791 loop {
12792 let remainder = integer % rf;
12793 int_out.push(digits[remainder as usize]);
12794 integer = (integer - remainder) / rf;
12795 if integer <= 0.0 {
12796 break;
12797 }
12798 }
12799 int_out.reverse();
12800
12801 let mut out: Vec<u8> = Vec::new();
12802 if neg {
12803 out.push(b'-');
12804 }
12805 out.extend_from_slice(&int_out);
12806 if !frac.is_empty() {
12807 out.push(b'.');
12808 out.extend_from_slice(&frac);
12809 }
12810 String::from_utf8(out).unwrap()
12811}
12812
12813/// Next representable f64 above `x` (`x` finite, `x ≥ 0`) — V8's `NextDouble`.
12814fn next_up(x: f64) -> f64 {
12815 f64::from_bits(x.to_bits() + 1)
12816}
12817
12818/// V8's `Double::Exponent`: the binary exponent of the significand-scaled value
12819/// (`> 0` iff |x| ≥ 2^53). Used to detect integers past `fmod`'s exact range.
12820fn v8_exponent(x: f64) -> i32 {
12821 let biased = ((x.to_bits() >> 52) & 0x7ff) as i32;
12822 if biased == 0 {
12823 -1074 // denormal
12824 } else {
12825 biased - 1075
12826 }
12827}
12828
12829// ══ Map / Set / Symbol / generator methods ═══════════════════════════════════
12830
12831/// `Map.prototype.set` step 6 and `Set.prototype.add` step 4: a key of `-0` is
12832/// STORED as `+0`. `map_key` already treats the two as one key (SameValueZero),
12833/// but the value kept alongside it is what iteration and `console.log` report,
12834/// and node shows `0` there — `new Map().set(-0, 1)` renders `Map(1) { 0 => 1 }`.
12835fn normalize_zero_key(v: Value) -> Value {
12836 match v {
12837 Value::Float(f) if f == 0.0 && f.is_sign_negative() => Value::Float(0.0),
12838 other => other,
12839 }
12840}
12841
12842fn map_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
12843 match name {
12844 "get" => {
12845 let key = with_host(|h| host::map_key(h, &arg0(&args)));
12846 Ok(with_host(|h| match h.get(recv) {
12847 Some(JsObj::Map { entries, .. }) => entries
12848 .get(&key)
12849 .map(|(_, v)| v.clone())
12850 .unwrap_or(Value::Undef),
12851 _ => Value::Undef,
12852 }))
12853 }
12854 "set" => {
12855 let kv = normalize_zero_key(arg0(&args));
12856 let vv = args.get(1).cloned().unwrap_or(Value::Undef);
12857 reject_non_object_weak_key(recv, &kv, "WeakMap")?;
12858 let key = with_host(|h| host::map_key(h, &kv));
12859 with_host(|h| {
12860 if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12861 entries.insert(key, (kv, vv));
12862 }
12863 });
12864 Ok(recv.clone())
12865 }
12866 "has" => {
12867 let key = with_host(|h| host::map_key(h, &arg0(&args)));
12868 Ok(Value::Bool(with_host(
12869 |h| matches!(h.get(recv), Some(JsObj::Map { entries, .. }) if entries.contains_key(&key)),
12870 )))
12871 }
12872 "delete" => {
12873 let key = with_host(|h| host::map_key(h, &arg0(&args)));
12874 Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
12875 Some(JsObj::Map { entries, .. }) => entries.shift_remove(&key).is_some(),
12876 _ => false,
12877 })))
12878 }
12879 "clear" => {
12880 with_host(|h| {
12881 if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
12882 entries.clear();
12883 }
12884 });
12885 Ok(Value::Undef)
12886 }
12887 "forEach" => {
12888 let cb = arg0(&args);
12889 let pairs: Vec<(Value, Value)> = with_host(|h| match h.get(recv) {
12890 Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
12891 _ => Vec::new(),
12892 });
12893 for (k, v) in pairs {
12894 host::invoke(&cb, vec![v, k, recv.clone()], this_arg(&args, 1))?;
12895 }
12896 Ok(Value::Undef)
12897 }
12898 // LIVE, not a snapshot: an entry added during iteration is visited and
12899 // one deleted before it is reached is not.
12900 "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
12901 recv,
12902 if name == "@@iterator" {
12903 "entries"
12904 } else {
12905 name
12906 },
12907 )),
12908 _ => Err(host::type_error(&format!("map.{name} is not a function"))),
12909 }
12910}
12911
12912/// A weak collection can only hold objects (and unregistered symbols) — a
12913/// primitive key is a `TypeError`, which is how packages probe for weak support.
12914fn reject_non_object_weak_key(recv: &Value, key: &Value, kind: &str) -> Result<(), String> {
12915 let weak = with_host(|h| {
12916 matches!(
12917 h.get(recv),
12918 Some(JsObj::Map { weak: true, .. }) | Some(JsObj::Set { weak: true, .. })
12919 )
12920 });
12921 if !weak {
12922 return Ok(());
12923 }
12924 let is_object = with_host(|h| match key {
12925 Value::Obj(_) => !h.is_null(key) && h.as_str(key).is_none() && h.as_bigint(key).is_none(),
12926 _ => false,
12927 });
12928 if is_object {
12929 return Ok(());
12930 }
12931 Err(host::type_error(if kind == "WeakMap" {
12932 "Invalid value used as weak map key"
12933 } else {
12934 "Invalid value used in weak set"
12935 }))
12936}
12937
12938/// A `Set`-like operand of the ES2025 set methods — 24.2.1.2 `GetSetRecord`.
12939///
12940/// The seven set operations do NOT require a real `Set` on the right-hand side:
12941/// anything with a numeric `size` and callable `has`/`keys` participates, which
12942/// is what lets a `Map`'s key view or a user-written set stand in. The reads
12943/// happen in this order (`size`, `has`, `keys`) and each failure has its own
12944/// diagnostic, so a bad operand reports which field was wrong rather than
12945/// failing later inside the iteration.
12946struct SetRecord {
12947 obj: Value,
12948 /// `size` truncated toward zero, as the spec's `intSize` is; the fractional
12949 /// part is dropped BEFORE the negative check, so `size: -0.5` truncates to
12950 /// `-0` and is accepted while `-1.5` reports `'-1' is an invalid size`.
12951 size: f64,
12952 has: Value,
12953 keys: Value,
12954}
12955
12956fn get_set_record(other: &Value, method: &str) -> Result<SetRecord, String> {
12957 if !with_host(|h| is_object_like(h, other)) {
12958 return Err(host::type_error(&format!(
12959 "Set.prototype.{method} argument must be an object"
12960 )));
12961 }
12962 let raw = get_property(other, "size")?;
12963 let num = host::to_number_value(&raw)?;
12964 if num.is_nan() {
12965 return Err(host::type_error("The .size property is NaN"));
12966 }
12967 let size = num.trunc();
12968 if size < 0.0 {
12969 return Err(host::range_error(&format!("'{size}' is an invalid size")));
12970 }
12971 let has = get_property(other, "has")?;
12972 if !with_host(|h| host::is_callable(h, &has)) {
12973 return Err(host::type_error("string \"has\" is not a function"));
12974 }
12975 let keys = get_property(other, "keys")?;
12976 if !with_host(|h| host::is_callable(h, &keys)) {
12977 return Err(host::type_error("string \"keys\" is not a function"));
12978 }
12979 Ok(SetRecord {
12980 obj: other.clone(),
12981 size,
12982 has,
12983 keys,
12984 })
12985}
12986
12987impl SetRecord {
12988 /// `Call(has, obj, [v])`, coerced to a boolean the way the spec's
12989 /// `ToBoolean(Call(...))` is — a set-like may answer with anything truthy.
12990 fn has(&self, v: &Value) -> Result<bool, String> {
12991 let r = host::invoke(&self.has, vec![v.clone()], Some(self.obj.clone()))?;
12992 Ok(with_host(|h| h.truthy(&r)))
12993 }
12994
12995 /// The operand's elements, drained from the iterator its `keys` method
12996 /// returns. A non-object result is the spec's `Result of the keys method is
12997 /// not an object`, reported before anything is iterated.
12998 fn keys(&self) -> Result<Vec<Value>, String> {
12999 let it = host::invoke(&self.keys, Vec::new(), Some(self.obj.clone()))?;
13000 if !with_host(|h| is_object_like(h, &it)) {
13001 return Err(host::type_error(
13002 "Result of the keys method is not an object",
13003 ));
13004 }
13005 host::drain_iterator(&it)
13006 }
13007}
13008
13009/// The receiver of a set operation must be a real (non-weak) `Set`: these seven
13010/// methods read `[[SetData]]` directly, so a look-alike cannot stand in on the
13011/// LEFT even though it can on the right.
13012fn require_set_receiver(recv: &Value, method: &str) -> Result<(), String> {
13013 if with_host(|h| matches!(h.get(recv), Some(JsObj::Set { weak: false, .. }))) {
13014 return Ok(());
13015 }
13016 Err(host::type_error(&format!(
13017 "Method Set.prototype.{method} called on incompatible receiver {}",
13018 with_host(|h| object_tag(h, recv))
13019 )))
13020}
13021
13022/// The receiver's elements, READ AT THE POINT THE SPEC READS THEM.
13023///
13024/// Every one of these operations copies `[[SetData]]` *after* it has touched
13025/// the operand — `union` and `symmetricDifference` call the operand's `keys`
13026/// first — so a `keys` (or a `has`) that mutates the receiver is visible in the
13027/// result. Snapshotting the receiver up front instead dropped such an element:
13028/// node's `s.union({ keys(){ s.add(99); … } })` contains `99`.
13029fn set_values(recv: &Value) -> Vec<Value> {
13030 with_host(|h| match h.get(recv) {
13031 Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13032 _ => Vec::new(),
13033 })
13034}
13035
13036fn set_size(recv: &Value) -> f64 {
13037 with_host(|h| match h.get(recv) {
13038 Some(JsObj::Set { entries, .. }) => entries.len() as f64,
13039 _ => 0.0,
13040 })
13041}
13042
13043/// A fresh, ordinary `Set`. The set operations are NOT species-aware: on node
13044/// `class S extends Set {}`, `new S([1]).union(other).constructor` is `Set`.
13045fn new_set(items: Vec<Value>) -> Result<Value, String> {
13046 let s = with_host(|h| {
13047 h.alloc(JsObj::Set {
13048 entries: IndexMap::new(),
13049 weak: false,
13050 })
13051 });
13052 for v in items {
13053 set_method(&s, "add", vec![v])?;
13054 }
13055 Ok(s)
13056}
13057
13058fn set_contains(s: &Value, v: &Value) -> bool {
13059 let key = with_host(|h| host::map_key(h, v));
13060 with_host(
13061 |h| matches!(h.get(s), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13062 )
13063}
13064
13065/// The seven ES2025 set operations (24.2.4.3, .8, .5, .16, .10, .12, .7).
13066///
13067/// Each one branches on the two sizes and iterates the SMALLER side — not an
13068/// optimization but observable behaviour: which side is walked decides the
13069/// result's order and whether the operand's `has` or its `keys` is the method
13070/// that runs. `intersection` of a 3-element receiver with a 2-element operand
13071/// yields the operand's order, and its `keys` (never its `has`) is called.
13072fn set_operation(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13073 require_set_receiver(recv, name)?;
13074 let other = get_set_record(&arg0(&args), name)?;
13075 let my_size = set_size(recv);
13076 match name {
13077 "union" => {
13078 let keys = other.keys()?;
13079 let mut out = set_values(recv);
13080 out.extend(keys);
13081 new_set(out)
13082 }
13083 "intersection" => {
13084 let mut out = Vec::new();
13085 if my_size <= other.size {
13086 for v in set_values(recv) {
13087 if other.has(&v)? {
13088 out.push(v);
13089 }
13090 }
13091 } else {
13092 for k in other.keys()? {
13093 if set_contains(recv, &k) {
13094 out.push(k);
13095 }
13096 }
13097 }
13098 new_set(out)
13099 }
13100 "difference" => {
13101 if my_size <= other.size {
13102 let mut out = Vec::new();
13103 for v in set_values(recv) {
13104 if !other.has(&v)? {
13105 out.push(v);
13106 }
13107 }
13108 return new_set(out);
13109 }
13110 let out = new_set(set_values(recv))?;
13111 for k in other.keys()? {
13112 set_method(&out, "delete", vec![k])?;
13113 }
13114 Ok(out)
13115 }
13116 "symmetricDifference" => {
13117 // The operand is drained FIRST — the spec takes the iterator before
13118 // it copies `[[SetData]]`, so a `keys` that mutates the receiver is
13119 // reflected in the result.
13120 let keys = other.keys()?;
13121 let out = new_set(set_values(recv))?;
13122 for k in keys {
13123 if set_contains(recv, &k) {
13124 set_method(&out, "delete", vec![k])?;
13125 } else {
13126 set_method(&out, "add", vec![k])?;
13127 }
13128 }
13129 Ok(out)
13130 }
13131 "isSubsetOf" => {
13132 if my_size > other.size {
13133 return Ok(Value::Bool(false));
13134 }
13135 for v in set_values(recv) {
13136 if !other.has(&v)? {
13137 return Ok(Value::Bool(false));
13138 }
13139 }
13140 Ok(Value::Bool(true))
13141 }
13142 "isSupersetOf" => {
13143 if my_size < other.size {
13144 return Ok(Value::Bool(false));
13145 }
13146 for k in other.keys()? {
13147 if !set_contains(recv, &k) {
13148 return Ok(Value::Bool(false));
13149 }
13150 }
13151 Ok(Value::Bool(true))
13152 }
13153 "isDisjointFrom" => {
13154 if my_size <= other.size {
13155 for v in set_values(recv) {
13156 if other.has(&v)? {
13157 return Ok(Value::Bool(false));
13158 }
13159 }
13160 } else {
13161 for k in other.keys()? {
13162 if set_contains(recv, &k) {
13163 return Ok(Value::Bool(false));
13164 }
13165 }
13166 }
13167 Ok(Value::Bool(true))
13168 }
13169 _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13170 }
13171}
13172
13173fn set_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13174 match name {
13175 "add" => {
13176 let vv = normalize_zero_key(arg0(&args));
13177 reject_non_object_weak_key(recv, &vv, "WeakSet")?;
13178 let key = with_host(|h| host::map_key(h, &vv));
13179 with_host(|h| {
13180 if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13181 entries.insert(key, vv);
13182 }
13183 });
13184 Ok(recv.clone())
13185 }
13186 "has" => {
13187 let key = with_host(|h| host::map_key(h, &arg0(&args)));
13188 Ok(Value::Bool(with_host(
13189 |h| matches!(h.get(recv), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
13190 )))
13191 }
13192 "delete" => {
13193 let key = with_host(|h| host::map_key(h, &arg0(&args)));
13194 Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
13195 Some(JsObj::Set { entries, .. }) => entries.shift_remove(&key).is_some(),
13196 _ => false,
13197 })))
13198 }
13199 "clear" => {
13200 with_host(|h| {
13201 if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
13202 entries.clear();
13203 }
13204 });
13205 Ok(Value::Undef)
13206 }
13207 "forEach" => {
13208 let cb = arg0(&args);
13209 let vals: Vec<Value> = with_host(|h| match h.get(recv) {
13210 Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
13211 _ => Vec::new(),
13212 });
13213 for v in vals {
13214 host::invoke(&cb, vec![v.clone(), v, recv.clone()], this_arg(&args, 1))?;
13215 }
13216 Ok(Value::Undef)
13217 }
13218 "union"
13219 | "intersection"
13220 | "difference"
13221 | "symmetricDifference"
13222 | "isSubsetOf"
13223 | "isSupersetOf"
13224 | "isDisjointFrom" => set_operation(recv, name, args),
13225 // LIVE, as for `Map`. A Set's `keys` and `values` are the same thing.
13226 "keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
13227 recv,
13228 if name == "entries" {
13229 "entries"
13230 } else {
13231 "values"
13232 },
13233 )),
13234 _ => Err(host::type_error(&format!("set.{name} is not a function"))),
13235 }
13236}
13237
13238fn generator_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13239 // A generator IS its own iterator: both symbol forms return the receiver.
13240 if matches!(name, "@@iterator" | "@@asyncIterator") {
13241 return Ok(recv.clone());
13242 }
13243 // An `async function*` object's methods return PROMISES of the record, and
13244 // its body has to be driven through the await-aware stepper (a plain
13245 // `gen_resume` would surface an internal `await` suspension as a bogus yield).
13246 if host::is_async_generator(recv) {
13247 // All three go through `[[AsyncGeneratorQueue]]` (ECMA-262 27.6.3.6):
13248 // `.return`/`.throw` must wait behind a `.next()` that is still
13249 // suspended on an internal `await`, or that `.next()` would report
13250 // `{done: true}` for a value the body had not yet reached. An uncaught
13251 // `.throw(e)` rejects the returned promise; it does not throw here.
13252 return match name {
13253 "next" => Ok(host::async_gen_enqueue(
13254 recv,
13255 host::GenReq::Next(arg0(&args)),
13256 )),
13257 "return" => Ok(host::async_gen_enqueue(
13258 recv,
13259 host::GenReq::Return(arg0(&args)),
13260 )),
13261 "throw" => Ok(host::async_gen_enqueue(
13262 recv,
13263 host::GenReq::Throw(arg0(&args)),
13264 )),
13265 "@@asyncIterator" => Ok(recv.clone()),
13266 _ => Err(host::type_error(&format!(
13267 "asyncGenerator.{name} is not a function"
13268 ))),
13269 };
13270 }
13271 match name {
13272 "next" => {
13273 let send = arg0(&args);
13274 match host::gen_resume(recv, send)? {
13275 host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13276 host::GenStep::Done(v) => Ok(iter_result(v, true)),
13277 }
13278 }
13279 "return" => {
13280 // Resume with an injected return so any pending `finally` runs; the
13281 // completion may itself be a `finally` yield (not-done) or the value.
13282 match host::gen_return(recv, arg0(&args))? {
13283 host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13284 host::GenStep::Done(v) => Ok(iter_result(v, true)),
13285 }
13286 }
13287 "throw" => {
13288 // Inject a throw at the suspension point: an enclosing `try/catch` in
13289 // the body can handle it (and any `finally` runs); otherwise it
13290 // propagates to the caller.
13291 match host::gen_throw(recv, arg0(&args))? {
13292 host::GenStep::Yield(v) => Ok(iter_result(v, false)),
13293 host::GenStep::Done(v) => Ok(iter_result(v, true)),
13294 }
13295 }
13296 _ => Err(host::type_error(&format!(
13297 "generator.{name} is not a function"
13298 ))),
13299 }
13300}
13301
13302/// A `{ value, done }` iterator-result object.
13303fn iter_result(value: Value, done: bool) -> Value {
13304 with_host(|h| {
13305 let mut m: IndexMap<String, Value> = IndexMap::new();
13306 m.insert("value".into(), value);
13307 m.insert("done".into(), Value::Bool(done));
13308 h.new_object(m)
13309 })
13310}
13311
13312/// Built-in iterator object (`arr.values()`, `arr[Symbol.iterator]()`): a lazy
13313/// cursor over a materialized item list.
13314fn iter_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
13315 match name {
13316 "next" => {
13317 let step = with_host(|h| {
13318 if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
13319 if *idx < items.len() {
13320 let v = items[*idx].clone();
13321 *idx += 1;
13322 return Some(v);
13323 }
13324 }
13325 None
13326 });
13327 Ok(match step {
13328 Some(v) => iter_result(v, false),
13329 None => iter_result(Value::Undef, true),
13330 })
13331 }
13332 "return" => {
13333 // Exhaust the cursor and report done.
13334 with_host(|h| {
13335 if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
13336 *idx = items.len();
13337 }
13338 });
13339 Ok(iter_result(arg0(&args), true))
13340 }
13341 // An iterator is its own iterable.
13342 "@@iterator" => Ok(recv.clone()),
13343 _ => Err(host::type_error(&format!(
13344 "iterator.{name} is not a function"
13345 ))),
13346 }
13347}
13348
13349fn symbol_method(recv: &Value, name: &str, _args: Vec<Value>) -> Result<Value, String> {
13350 match name {
13351 "toString" => Ok(with_host(|h| {
13352 let s = h.str_of(recv);
13353 h.new_str(s)
13354 })),
13355 // 20.4.3.5: `Symbol.prototype[@@toPrimitive]` returns the symbol
13356 // itself for EVERY hint — it ignores its argument. That is what makes
13357 // `sym + ''` a TypeError rather than a concatenation: the conversion
13358 // succeeds and hands back a symbol, and it is `+` that then rejects it.
13359 "@@toPrimitive" | "valueOf" => Ok(recv.clone()),
13360 _ => Err(host::type_error(&format!(
13361 "symbol.{name} is not a function"
13362 ))),
13363 }
13364}
13365
13366// ══ Object.* prototype helpers, `in`, deep clone ═════════════════════════════
13367
13368fn object_create(args: Vec<Value>) -> Result<Value, String> {
13369 let proto = arg0(&args);
13370 // 20.1.2.2 step 1: the prototype must be an Object or exactly `null`.
13371 // `undefined` is NOT accepted — measured on node v26.7.0,
13372 // `Object.create(undefined)` is
13373 // `TypeError: Object prototype may only be an Object or null: undefined`,
13374 // where node-js quietly built a normal object.
13375 reject_bad_prototype(&proto)?;
13376 let obj = with_host(|h| h.new_object(IndexMap::new()));
13377 // `set_proto` records a null proto as an explicit null-prototype object.
13378 with_host(|h| h.set_proto(&obj, proto));
13379 // Optional second arg: a property-descriptor map.
13380 if let Some(descs) = args.get(1).filter(|d| !matches!(d, Value::Undef)) {
13381 let entries: Vec<(String, Value)> = with_host(|h| match h.get(descs) {
13382 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
13383 _ => Vec::new(),
13384 });
13385 for (k, d) in entries {
13386 apply_descriptor(&obj, &k, &d)?;
13387 }
13388 }
13389 Ok(obj)
13390}
13391
13392/// The enumerable method names of a builtin `<Ctor>.prototype` namespace that
13393/// supports being copied via `mixin`/`getOwnPropertyNames`. Currently only
13394/// `EventEmitter.prototype` (the one express mixes onto its app function).
13395/// The own property names of `<Ctor>.prototype`, and whether each is
13396/// enumerable, from the generated [`crate::arity::PROTO_MEMBERS`] table.
13397///
13398/// `Object.getOwnPropertyNames(Map.prototype)` answered `[]` for every
13399/// intrinsic — the members are reachable by NAME through the `@proto:` thunks
13400/// but were not enumerable, so feature detection that walks a prototype found
13401/// nothing there. The table is read from the reference engine rather than
13402/// derived from the arity table because the arity table holds functions only:
13403/// `Map.prototype.size`, `RegExp.prototype.source` and the twelve
13404/// `URL.prototype` components are accessors.
13405fn intrinsic_proto_members(ns: &str) -> Option<&'static [&'static str]> {
13406 let ctor = ns.strip_suffix(".prototype")?;
13407 crate::arity::PROTO_MEMBERS
13408 .binary_search_by(|(k, _)| (*k).cmp(ctor))
13409 .ok()
13410 .map(|i| crate::arity::PROTO_MEMBERS[i].1)
13411}
13412
13413fn builtin_proto_method_names(ns: &str) -> Option<&'static [&'static str]> {
13414 match ns {
13415 "EventEmitter.prototype" => Some(crate::stdlib::events::METHODS),
13416 _ => None,
13417 }
13418}
13419
13420/// The own SYMBOL-keyed property keys of `v` as symbol values. A Proxy's come
13421/// from its `ownKeys` trap (the symbol half of the same list the string keys are
13422/// filtered out of); every other receiver answers from its property map.
13423fn proxy_or_own_symbol_keys(v: &Value) -> Result<Vec<Value>, String> {
13424 if let Some(keys) = crate::proxy::own_keys(v)? {
13425 return Ok(keys
13426 .iter()
13427 .filter(|k| host::is_symbol_key(k))
13428 .map(|k| crate::proxy::key_value(k))
13429 .collect());
13430 }
13431 // An intrinsic prototype's symbol-keyed members come from the generated
13432 // table, which is the only record of them: they own no map entry, so
13433 // `Object.getOwnPropertySymbols(Array.prototype)` was `[]` where node
13434 // reports `Symbol.iterator` and `Symbol.unscopables`.
13435 if let Some(ns) = intrinsic_proto_of(v).map(|c| format!("{c}.prototype")) {
13436 if let Some(members) = intrinsic_proto_members(&ns) {
13437 return Ok(with_host(|h| {
13438 members
13439 .iter()
13440 .filter_map(|m| m.strip_prefix('+').unwrap_or(m).strip_prefix("@@"))
13441 .map(|name| h.well_known_symbol(name))
13442 .collect()
13443 }));
13444 }
13445 }
13446 Ok(with_host(|h| h.own_symbol_keys(v)))
13447}
13448
13449/// `[[DefineOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13450pub fn define_property_pub(obj: &Value, key: Value, desc: Value) -> Result<Value, String> {
13451 object_define_property(vec![obj.clone(), key, desc])
13452}
13453
13454/// `[[GetOwnProperty]]` reachable from `crate::proxy`'s no-trap forward.
13455pub fn own_descriptor_pub(obj: &Value, key: Value) -> Result<Value, String> {
13456 object_get_own_descriptor(vec![obj.clone(), key])
13457}
13458
13459fn object_define_property(args: Vec<Value>) -> Result<Value, String> {
13460 let obj = arg0(&args);
13461 // A Proxy defines through its `defineProperty` trap; the target it forwards
13462 // to is where the ordinary path below finally runs.
13463 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
13464 let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13465 let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13466 if !with_host(|h| is_object_like(h, &desc)) {
13467 return Err(host::type_error(&format!(
13468 "Property description must be an object: {}",
13469 with_host(|h| h.str_of(&desc))
13470 )));
13471 }
13472 // `Object.defineProperty` THROWS on a refusing trap — in sloppy code
13473 // too. `Reflect.defineProperty` is the form that reports `false`.
13474 if !crate::proxy::define_property(&obj, &key, &desc)? {
13475 return Err(host::type_error(&format!(
13476 "'defineProperty' on proxy: trap returned falsish for property '{key}'"
13477 )));
13478 }
13479 return Ok(obj);
13480 }
13481 // 20.1.2.4 steps 1-3, both of which node-js skipped entirely: a non-object
13482 // target and a non-object descriptor each throw before anything is written.
13483 if !with_host(|h| is_object_like(h, &obj)) {
13484 return Err(host::type_error(
13485 "Object.defineProperty called on non-object",
13486 ));
13487 }
13488 let desc = args.get(2).cloned().unwrap_or(Value::Undef);
13489 if !with_host(|h| is_object_like(h, &desc)) {
13490 return Err(host::type_error(&format!(
13491 "Property description must be an object: {}",
13492 with_host(|h| h.str_of(&desc))
13493 )));
13494 }
13495 let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
13496 apply_descriptor(&obj, &key, &desc)?;
13497 Ok(obj)
13498}
13499
13500/// Every `Reflect` method requires an OBJECT target and reports a `TypeError`
13501/// for anything else (28.1). A primitive was being accepted and silently
13502/// producing nothing.
13503/// `CreateListFromArrayLike` (7.3.18) — the argument list `Reflect.apply` and
13504/// `Reflect.construct` take.
13505///
13506/// An ARRAY-LIKE counts: `{length: 2, 0: 1, 1: 5}` is a two-element list. The
13507/// iterator was being used instead, so an array-like produced nothing and a
13508/// primitive produced nothing rather than the TypeError node raises.
13509/// Whether `p` is ALREADY `obj`'s prototype — the one case a non-extensible
13510/// object still accepts, because it changes nothing.
13511///
13512/// The observable prototype, not the stored link: an ordinary object has no
13513/// explicit link and inherits `Object.prototype`, so comparing the raw slot
13514/// reported "different" for `setPrototypeOf(frozen, Object.prototype)`.
13515/// Whether making `p` the prototype of `obj` would create a CYCLE — 10.1.2.1
13516/// step 8 walks up from `p` looking for `obj`.
13517///
13518/// Without the check `Object.setPrototypeOf(a, b)` followed by the reverse
13519/// built a ring. Nothing hung, because every chain walk in this host carries a
13520/// hop limit, but a lookup then silently gave up instead of finding a property
13521/// that really was there.
13522fn would_cycle(obj: &Value, p: &Value) -> bool {
13523 let mut cur = Some(p.clone());
13524 for _ in 0..1000 {
13525 let Some(c) = cur else { return false };
13526 if with_host(|h| h.strict_eq(&c, obj)) {
13527 return true;
13528 }
13529 // A PROXY's prototype is its handler's business; the spec skips the
13530 // walk entirely when one is in the chain.
13531 if with_host(|h| h.kind_of(&c)) == Some(ObjKind::Proxy) {
13532 return false;
13533 }
13534 cur = with_host(|h| h.proto_of(&c));
13535 }
13536 false
13537}
13538
13539fn same_prototype(obj: &Value, p: &Value) -> bool {
13540 let cur = prototype_of(obj);
13541 with_host(|h| h.strict_eq(&cur, p) || (h.is_null(&cur) && h.is_null(p)))
13542}
13543
13544fn create_list_from_array_like(v: &Value) -> Result<Vec<Value>, String> {
13545 if !with_host(|h| is_object_like(h, v)) {
13546 return Err(host::type_error(
13547 "CreateListFromArrayLike called on non-object",
13548 ));
13549 }
13550 let len = get_property(v, "length")?;
13551 let n = with_host(|h| h.to_number(&len));
13552 let n = if n.is_finite() && n > 0.0 {
13553 n as usize
13554 } else {
13555 0
13556 };
13557 (0..n).map(|i| get_property(v, &i.to_string())).collect()
13558}
13559
13560fn reflect_require_object(v: &Value, method: &str) -> Result<(), String> {
13561 if with_host(|h| is_object_like(h, v)) {
13562 return Ok(());
13563 }
13564 Err(host::type_error(&format!(
13565 "Reflect.{method} called on non-object"
13566 )))
13567}
13568
13569/// Whether `v` is an Object in the language sense — anything `typeof` calls
13570/// `"object"` (bar `null`) or `"function"`. Used by the argument checks that
13571/// distinguish "an object" from a primitive.
13572fn is_object_like(h: &host::JsHost, v: &Value) -> bool {
13573 matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v)
13574}
13575
13576/// `RequireObjectCoercible(v)` — 7.2.1. The check in front of every `ToObject`,
13577/// which node-js was missing on the whole `Object.keys`/`values`/`entries`/
13578/// `getOwnPropertyNames`/`getOwnPropertySymbols`/`getOwnPropertyDescriptor`/
13579/// `assign` family: each returned an empty result for `null` where node v26.7.0
13580/// throws `TypeError: Cannot convert undefined or null to object`. A PRIMITIVE
13581/// is coercible and keeps working (`Object.keys(1)` is `[]`).
13582fn require_object_coercible(v: &Value) -> Result<(), String> {
13583 if with_host(|h| matches!(v, Value::Undef) || h.is_null(v)) {
13584 return Err(host::type_error(
13585 "Cannot convert undefined or null to object",
13586 ));
13587 }
13588 Ok(())
13589}
13590
13591/// 10.1.2 / 20.1.2.2 step 1: reject a `[[Prototype]]` that is neither an Object
13592/// nor `null`, with V8's wording. Measured on node v26.7.0:
13593/// `Object.create("s")` is
13594/// `TypeError: Object prototype may only be an Object or null: s`.
13595fn reject_bad_prototype(proto: &Value) -> Result<(), String> {
13596 if with_host(|h| h.is_null(proto) || is_object_like(h, proto)) {
13597 return Ok(());
13598 }
13599 Err(host::type_error(&format!(
13600 "Object prototype may only be an Object or null: {}",
13601 with_host(|h| h.str_of(proto))
13602 )))
13603}
13604
13605/// Apply a `{ value | get | set }` descriptor object to `obj[key]`.
13606///
13607/// Per ECMAScript `ToPropertyDescriptor`, an omitted `writable`/`enumerable`/
13608/// `configurable` field defaults to **false** — which is why a `defineProperty`
13609/// data property is invisible to `Object.keys` unless the caller opts in. That
13610/// asymmetry against plain assignment is the whole reason the attribute table
13611/// exists.
13612/// The requested fields of a property descriptor — 10.1.6.2
13613/// `ToPropertyDescriptor`. Each is `None` when the descriptor omits it, which
13614/// is the distinction the merge below turns on: an omitted field LEAVES an
13615/// existing attribute alone rather than resetting it.
13616struct Requested {
13617 value: Option<Value>,
13618 get: Option<Option<Value>>,
13619 set: Option<Option<Value>>,
13620 writable: Option<bool>,
13621 enumerable: Option<bool>,
13622 configurable: Option<bool>,
13623}
13624
13625impl Requested {
13626 /// Reads through the prototype chain, as `ToPropertyDescriptor`'s
13627 /// `HasProperty`/`Get` pairs do — a descriptor built with
13628 /// `Object.create({ value: 1 })` is legal.
13629 fn read(desc: &Value) -> Self {
13630 let has = |k: &str| {
13631 with_host(|h| {
13632 host::lookup_chain(h, desc, k).is_some()
13633 || host::lookup_accessor(h, desc, k).is_some()
13634 })
13635 };
13636 let val = |k: &str| get_property(desc, k).unwrap_or(Value::Undef);
13637 // Resolve the value BEFORE the borrow: `val` re-enters the host, and
13638 // doing it inside the `with_host` closure aborts on the double borrow.
13639 let flag = |k: &str| {
13640 has(k).then(|| {
13641 let v = val(k);
13642 with_host(|h| h.truthy(&v))
13643 })
13644 };
13645 Requested {
13646 value: has("value").then(|| val("value")),
13647 get: has("get").then(|| match val("get") {
13648 Value::Undef => None,
13649 g => Some(g),
13650 }),
13651 set: has("set").then(|| match val("set") {
13652 Value::Undef => None,
13653 st => Some(st),
13654 }),
13655 writable: flag("writable"),
13656 enumerable: flag("enumerable"),
13657 configurable: flag("configurable"),
13658 }
13659 }
13660
13661 fn is_accessor(&self) -> bool {
13662 self.get.is_some() || self.set.is_some()
13663 }
13664
13665 fn is_data(&self) -> bool {
13666 self.value.is_some() || self.writable.is_some()
13667 }
13668}
13669
13670/// The own property already at `key`, if any, read back through
13671/// `Object.getOwnPropertyDescriptor` so every object kind (array indices, the
13672/// fn-prop side table, Buffer bytes) is covered by one code path.
13673struct Existing {
13674 accessor: bool,
13675 value: Value,
13676 get: Option<Value>,
13677 set: Option<Value>,
13678 writable: bool,
13679 enumerable: bool,
13680 configurable: bool,
13681}
13682
13683fn existing_property(obj: &Value, key: &str) -> Option<Existing> {
13684 let k = with_host(|h| h.new_str(key.to_string()));
13685 let d = own_descriptor_pub(obj, k).ok()?;
13686 if matches!(d, Value::Undef) {
13687 return None;
13688 }
13689 let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
13690 let truthy = |n: &str| {
13691 let v = field(n);
13692 with_host(|h| h.truthy(&v))
13693 };
13694 let accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
13695 Some(Existing {
13696 accessor,
13697 value: field("value"),
13698 get: match field("get") {
13699 Value::Undef => None,
13700 g => Some(g),
13701 },
13702 set: match field("set") {
13703 Value::Undef => None,
13704 st => Some(st),
13705 },
13706 writable: truthy("writable"),
13707 enumerable: truthy("enumerable"),
13708 configurable: truthy("configurable"),
13709 })
13710}
13711
13712/// SameValue (7.2.11) — `===` except that `NaN` equals itself and `+0` and
13713/// `-0` are distinct. 10.1.6.3 compares a redefined value against the current
13714/// one with this, not with strict equality.
13715pub(crate) fn same_value(a: &Value, b: &Value) -> bool {
13716 let num = |v: &Value| match v {
13717 Value::Int(n) => Some(*n as f64),
13718 Value::Float(f) => Some(*f),
13719 _ => None,
13720 };
13721 match (num(a), num(b)) {
13722 (Some(x), Some(y)) => {
13723 if x.is_nan() && y.is_nan() {
13724 true
13725 } else if x == 0.0 && y == 0.0 {
13726 x.is_sign_negative() == y.is_sign_negative()
13727 } else {
13728 x == y
13729 }
13730 }
13731 _ => with_host(|h| h.strict_eq(a, b)),
13732 }
13733}
13734
13735/// 10.1.6.3 `ValidateAndApplyPropertyDescriptor`.
13736///
13737/// None of the validation existed: every `Object.defineProperty` was applied
13738/// unconditionally, so redefining a non-configurable property silently
13739/// succeeded where node throws. Worse in practice, an OMITTED field was read as
13740/// `false` rather than "leave alone", so the ordinary
13741/// `Object.defineProperty(o, 'k', { enumerable: false })` also stripped
13742/// `writable` and `configurable` from a property that had both.
13743///
13744/// Converting an accessor to a data property did not take effect at all: the
13745/// value was written but the accessor stayed in its side table, and accessors
13746/// win on read, so the getter kept answering.
13747fn apply_descriptor(obj: &Value, key: &str, desc: &Value) -> Result<(), String> {
13748 let req = Requested::read(desc);
13749 let cur = existing_property(obj, key);
13750
13751 // An array's `length` is the exotic own property whose write resizes the
13752 // array (10.4.2.1); routing it through the ordinary path stored a shadowing
13753 // key and left the elements untouched.
13754 if key == "length" && with_host(|h| h.kind_of(obj)) == Some(ObjKind::Array) {
13755 if let Some(v) = req.value.clone() {
13756 return set_property_pub(obj, "length", v);
13757 }
13758 }
13759
13760 // The other exotics whose own properties are SYNTHESIZED rather than stored
13761 // in a property map: a typed array's elements and a RegExp's `lastIndex`.
13762 // The ordinary path below writes a shadowing map entry the read never
13763 // consults, so `Object.defineProperty(u8, '0', {value: 9})` left `u8[0]`
13764 // unchanged.
13765 // A builtin namespace/prototype has no property map either, so a data
13766 // descriptor has to reach the same side table an assignment does.
13767 // `Object.defineProperty(Array.prototype, 'at', {value: impl})` — how a
13768 // careful polyfill installs itself, precisely to avoid the enumerable
13769 // property a bare assignment creates — wrote a map entry nothing read.
13770 if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Builtin) {
13771 if let Some(v) = req.value.clone() {
13772 return set_property_pub(obj, key, v);
13773 }
13774 }
13775 let exotic_own = (crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray")
13776 && key.parse::<usize>().is_ok())
13777 || (key == "lastIndex" && with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))));
13778 if exotic_own {
13779 if let Some(v) = req.value.clone() {
13780 return set_property_pub(obj, key, v);
13781 }
13782 }
13783
13784 // 10.1.6.3 step 2: a NEW property cannot be added to a non-extensible
13785 // object. Only an existing property's attributes were being validated, so
13786 // `defineProperty(Object.freeze({}), 'z', …)` silently added one.
13787 if cur.is_none() && !with_host(|h| h.is_extensible(obj)) {
13788 return Err(host::type_error(&format!(
13789 "Cannot define property {key}, object is not extensible"
13790 )));
13791 }
13792 if let Some(c) = &cur {
13793 if !c.configurable {
13794 let rejected = req.configurable == Some(true)
13795 || req.enumerable.is_some_and(|e| e != c.enumerable)
13796 || (req.is_accessor() && !c.accessor)
13797 || (req.is_data() && c.accessor)
13798 || (c.accessor
13799 && ((req.get.is_some() && req.get.clone().flatten() != c.get)
13800 || (req.set.is_some() && req.set.clone().flatten() != c.set)))
13801 || (!c.accessor
13802 && !c.writable
13803 && (req.writable == Some(true)
13804 || req.value.as_ref().is_some_and(|v| !same_value(v, &c.value))));
13805 if rejected {
13806 return Err(host::type_error(&format!(
13807 "Cannot redefine property: {key}"
13808 )));
13809 }
13810 }
13811 }
13812
13813 // An omitted field keeps what the property already had; a brand-new
13814 // property defaults every one of them to false.
13815 let attrs = host::PropAttrs {
13816 writable: req
13817 .writable
13818 .unwrap_or(cur.as_ref().is_some_and(|c| c.writable)),
13819 enumerable: req
13820 .enumerable
13821 .unwrap_or(cur.as_ref().is_some_and(|c| c.enumerable)),
13822 configurable: req
13823 .configurable
13824 .unwrap_or(cur.as_ref().is_some_and(|c| c.configurable)),
13825 };
13826 with_host(|h| h.set_prop_attrs(obj, key, attrs));
13827
13828 if req.is_accessor() {
13829 let get = req
13830 .get
13831 .clone()
13832 .unwrap_or_else(|| cur.as_ref().and_then(|c| c.get.clone()));
13833 let set = req
13834 .set
13835 .clone()
13836 .unwrap_or_else(|| cur.as_ref().and_then(|c| c.set.clone()));
13837 // An ACCESSOR at an index past the end still extends the array
13838 // (10.4.2.1): `Object.defineProperty([1], '4', {get})` gives
13839 // `length === 5` with holes between. Only the DATA path grew it, so
13840 // the accessor landed in the side table while `length` stayed put —
13841 // and with it out of range, `Object.keys` and `JSON.stringify` never
13842 // saw the index at all.
13843 if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>())
13844 {
13845 with_host(|h| {
13846 let old_len = match h.get(obj) {
13847 Some(JsObj::Array(items)) => items.len(),
13848 _ => 0,
13849 };
13850 if i >= old_len {
13851 if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13852 items.resize(i + 1, Value::Undef);
13853 }
13854 h.mark_hole_range(obj, old_len..i + 1);
13855 }
13856 });
13857 }
13858 with_host(|h| h.set_accessor(obj, key, get, set));
13859 return Ok(());
13860 }
13861
13862 if let Some(c) = &cur {
13863 if c.accessor {
13864 if !req.is_data() {
13865 // A generic descriptor — flags only — leaves an accessor an
13866 // accessor. They were already applied above.
13867 return Ok(());
13868 }
13869 let v = req.value.clone().unwrap_or(Value::Undef);
13870 with_host(|h| h.accessor_to_data(obj, key, v));
13871 return Ok(());
13872 }
13873 }
13874
13875 let Some(v) = req.value else {
13876 // Nothing to write: a flags-only redefinition of a data property.
13877 return Ok(());
13878 };
13879 write_data_slot(obj, key, v);
13880 Ok(())
13881}
13882
13883/// Store `v` as an own data property, in whichever slot the object kind keeps
13884/// its own properties.
13885fn write_data_slot(obj: &Value, key: &str, v: Value) {
13886 // A function/class receiver stores its own props in the fn-prop side table
13887 // (express `mixin(app, proto)` defines methods onto the `app` *function*).
13888 if matches!(
13889 with_host(|h| h.get(obj).cloned()),
13890 Some(JsObj::Func(_)) | Some(JsObj::Class(_))
13891 ) || uses_side_table(obj)
13892 {
13893 with_host(|h| h.set_fn_prop(obj, key, v));
13894 return;
13895 }
13896 if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>()) {
13897 // An array's index keys ARE its elements, and defining one past the end
13898 // grows the array with holes in between (10.4.2.1). This whole branch
13899 // used to be missing: `Object.defineProperty(arr, 1, {value})` wrote
13900 // into the ordinary property map an array does not have, so it was a
13901 // silent no-op.
13902 with_host(|h| {
13903 let old = match h.get(obj) {
13904 Some(JsObj::Array(items)) => items.len(),
13905 _ => 0,
13906 };
13907 if let Some(JsObj::Array(items)) = h.get_mut(obj) {
13908 if i >= old {
13909 items.resize(i + 1, Value::Undef);
13910 }
13911 items[i] = v;
13912 }
13913 if i > old {
13914 h.mark_hole_range(obj, old..i);
13915 }
13916 h.clear_hole(obj, i);
13917 });
13918 return;
13919 }
13920 with_host(|h| {
13921 if let Some(JsObj::Object(p)) = h.get_mut(obj) {
13922 p.insert(key.to_string(), v);
13923 host::canonicalize_own_keys(p);
13924 }
13925 });
13926}
13927
13928/// `Object.defineProperties(obj, descriptorMap)`.
13929fn object_define_properties(args: Vec<Value>) -> Result<Value, String> {
13930 let obj = arg0(&args);
13931 let descs = args.get(1).cloned().unwrap_or(Value::Undef);
13932 let entries: Vec<(String, Value)> = with_host(|h| match h.get(&descs) {
13933 Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
13934 _ => Vec::new(),
13935 });
13936 for (k, d) in entries {
13937 apply_descriptor(&obj, &k, &d)?;
13938 }
13939 Ok(obj)
13940}
13941
13942/// The descriptor of an own property a function, a typed array or a RegExp
13943/// SYNTHESIZES rather than keeping in a property map.
13944///
13945/// These read back through the ordinary path but owned no descriptor and did
13946/// not appear under `hasOwnProperty` or `getOwnPropertyNames`, so the five
13947/// views of "does this property exist" disagreed — a read said yes while
13948/// `Object.getOwnPropertyDescriptor(f, 'name')` said no such property, which is
13949/// what a shim checks before patching.
13950fn synthesized_own_descriptor(obj: &Value, key: &str) -> Option<(Value, host::PropAttrs)> {
13951 let ro_configurable = host::PropAttrs {
13952 writable: false,
13953 enumerable: false,
13954 configurable: true,
13955 };
13956 // A callable's `length`/`name` are read-only but configurable; its
13957 // `prototype` is writable and NOT configurable, and a class's is neither.
13958 // An arrow, a method and a bound function own no `prototype` at all.
13959 if with_host(|h| host::is_callable(h, obj)) && !matches!(key, "length" | "name" | "prototype") {
13960 return None;
13961 }
13962 if with_host(|h| host::is_callable(h, obj)) {
13963 if key == "prototype" {
13964 let p = get_property(obj, "prototype").ok()?;
13965 if matches!(p, Value::Undef) {
13966 return None;
13967 }
13968 return Some((
13969 p,
13970 host::PropAttrs {
13971 writable: with_host(|h| h.kind_of(obj)) != Some(ObjKind::Class),
13972 enumerable: false,
13973 configurable: false,
13974 },
13975 ));
13976 }
13977 return Some((get_property(obj, key).ok()?, ro_configurable));
13978 }
13979 // A typed array's elements are own, enumerable, writable, configurable
13980 // properties; an index past the end owns nothing.
13981 if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") {
13982 let v = crate::stdlib::typedarray::elem_get(obj, key)?;
13983 return Some((
13984 v,
13985 host::PropAttrs {
13986 writable: true,
13987 enumerable: true,
13988 configurable: true,
13989 },
13990 ));
13991 }
13992 // A RegExp's `lastIndex` is its own, writable, non-configurable cursor.
13993 if with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))) && key == "lastIndex" {
13994 return Some((
13995 get_property(obj, "lastIndex").ok()?,
13996 host::PropAttrs {
13997 writable: true,
13998 enumerable: false,
13999 configurable: false,
14000 },
14001 ));
14002 }
14003 None
14004}
14005
14006fn object_get_own_descriptor(args: Vec<Value>) -> Result<Value, String> {
14007 let obj = arg0(&args);
14008 require_object_coercible(&obj)?;
14009 let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
14010 // A string primitive's boxed own properties: each code-unit index is an
14011 // enumerable, non-writable, non-configurable data property, and `length` is
14012 // the same minus enumerable.
14013 if let Some(units) = string_primitive_units(&obj) {
14014 let entry = match key.parse::<usize>() {
14015 Ok(i) => units
14016 .get(i)
14017 .map(|c| (with_host(|h| h.new_str(c.clone())), true)),
14018 Err(_) if key == "length" => Some((Value::Float(units.len() as f64), false)),
14019 Err(_) => None,
14020 };
14021 return Ok(match entry {
14022 Some((value, enumerable)) => with_host(|h| {
14023 let mut m: IndexMap<String, Value> = IndexMap::new();
14024 m.insert("value".into(), value);
14025 m.insert("writable".into(), Value::Bool(false));
14026 m.insert("enumerable".into(), Value::Bool(enumerable));
14027 m.insert("configurable".into(), Value::Bool(false));
14028 h.new_object(m)
14029 }),
14030 None => Value::Undef,
14031 });
14032 }
14033 if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
14034 return Ok(crate::proxy::get_own_descriptor(&obj, &key)?.unwrap_or(Value::Undef));
14035 }
14036 // A method read off an enumerable builtin prototype (`EventEmitter.prototype`)
14037 // yields a `{ value: <method thunk> }` data descriptor so `mixin` can copy it.
14038 if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&obj).cloned()) {
14039 if let Some(names) = builtin_proto_method_names(&ns) {
14040 if names.contains(&key.as_str()) {
14041 return Ok(with_host(|h| {
14042 let thunk = h.alloc(JsObj::Builtin(format!(
14043 "@proto:{}:{key}",
14044 ns.trim_end_matches(".prototype")
14045 )));
14046 let mut m: IndexMap<String, Value> = IndexMap::new();
14047 m.insert("value".into(), thunk);
14048 m.insert("writable".into(), Value::Bool(true));
14049 m.insert("enumerable".into(), Value::Bool(true));
14050 m.insert("configurable".into(), Value::Bool(true));
14051 h.new_object(m)
14052 }));
14053 }
14054 }
14055 }
14056 // A global the object does not own outright is still an own property of the
14057 // global object — the same lazy binding the bare identifier resolves to.
14058 // Every one of them reported `undefined`, so a feature probe written as
14059 // `getOwnPropertyDescriptor(globalThis, 'structuredClone')` concluded the
14060 // global was absent. The immutable trio (11.1.1 / 19.1.1-3) is frozen; the
14061 // rest are ordinary writable, non-enumerable, configurable bindings.
14062 if with_host(|h| h.is_global_object(&obj)) {
14063 let owned = with_host(|h| match h.get(&obj) {
14064 Some(JsObj::Object(p)) => p.contains_key(&key),
14065 _ => false,
14066 });
14067 if !owned && !CJS_WRAPPER_LOCALS.contains(&key.as_str()) {
14068 // A global a SCRIPT created — `x = 1` with no declaration — is an
14069 // ordinary enumerable property, unlike the builtins.
14070 let script_made = with_host(|h| h.read_global(&key).is_some());
14071 if let Some(v) = global_object_binding(&key) {
14072 let frozen = matches!(key.as_str(), "undefined" | "NaN" | "Infinity");
14073 return Ok(with_host(|h| {
14074 let mut m: IndexMap<String, Value> = IndexMap::new();
14075 m.insert("value".into(), v);
14076 m.insert("writable".into(), Value::Bool(!frozen));
14077 m.insert(
14078 "enumerable".into(),
14079 Value::Bool(script_made || ENUMERABLE_GLOBALS.contains(&key.as_str())),
14080 );
14081 m.insert("configurable".into(), Value::Bool(!frozen));
14082 h.new_object(m)
14083 }));
14084 }
14085 }
14086 }
14087 // Any other member of a builtin namespace (`Math.PI`, `Math.floor`,
14088 // `Array.prototype.slice`, a builtin function's own `name`/`length`). Every
14089 // one of these reads back a value, but none owned a DESCRIPTOR:
14090 // `Object.getOwnPropertyDescriptor(Math, 'PI')` was `undefined`, which reads
14091 // as "no such property" to the shim/polyfill family that probes a namespace
14092 // before patching it.
14093 // An ACCESSOR member describes itself with a `get`, never a `value` — and
14094 // it must do so without READING the property, since running the getter
14095 // against the prototype is exactly what throws. Both prototype
14096 // representations are covered, so `Symbol.prototype.description` and
14097 // `Map.prototype.size` answer alike; both were `undefined`, which reads as
14098 // "no such property" to anything that probes before patching.
14099 if let Some(ctor) = intrinsic_proto_of(&obj) {
14100 if is_proto_accessor(&ctor, &key) {
14101 let getter = proto_getter(&ctor, &key);
14102 // The poison pair is the only ECMAScript accessor here with a
14103 // SETTER, but a WebIDL class has plenty: `URL.prototype.href`,
14104 // `hostname` and the rest are all writable, and reporting them as
14105 // read-only made `Object.getOwnPropertyDescriptor(URL.prototype,
14106 // 'href').set` read `undefined` for a setter that runs.
14107 let writable = (ctor == "Function" && matches!(key.as_str(), "arguments" | "caller"))
14108 || crate::stdlib::instance_accessors(&ctor)
14109 .0
14110 .iter()
14111 .any(|(k, settable)| *k == key && *settable);
14112 let setter = writable
14113 .then(|| with_host(|h| h.alloc(JsObj::Builtin(format!("@protoset:{ctor}:{key}")))));
14114 return Ok(with_host(|h| {
14115 let mut m: IndexMap<String, Value> = IndexMap::new();
14116 m.insert("get".into(), getter);
14117 // `undefined`, not null: a read-only accessor has no setter at
14118 // all, and `JSON.stringify` of the descriptor must drop the key
14119 // rather than report `"set": null`.
14120 m.insert("set".into(), setter.unwrap_or(Value::Undef));
14121 m.insert("enumerable".into(), Value::Bool(is_webidl_proto(&ctor)));
14122 m.insert("configurable".into(), Value::Bool(true));
14123 h.new_object(m)
14124 }));
14125 }
14126 }
14127 if let Some(ns) = with_host(|h| match h.get(&obj) {
14128 Some(JsObj::Builtin(ns)) => Some(ns.clone()),
14129 _ => None,
14130 }) {
14131 let value = namespace_property(&ns, &key);
14132 if !matches!(value, Value::Undef) {
14133 return Ok(builtin_member_descriptor(&ns, &key, value));
14134 }
14135 }
14136 if let Some((value, attrs)) = synthesized_own_descriptor(&obj, &key) {
14137 return Ok(with_host(|h| {
14138 let mut m: IndexMap<String, Value> = IndexMap::new();
14139 m.insert("value".into(), value);
14140 m.insert("writable".into(), Value::Bool(attrs.writable));
14141 m.insert("enumerable".into(), Value::Bool(attrs.enumerable));
14142 m.insert("configurable".into(), Value::Bool(attrs.configurable));
14143 h.new_object(m)
14144 }));
14145 }
14146 // Accessor descriptor?
14147 if let Some((get, set)) = with_host(|h| h.own_accessor(&obj, &key)) {
14148 return Ok(with_host(|h| {
14149 let a = h.prop_attrs(&obj, &key);
14150 let mut m: IndexMap<String, Value> = IndexMap::new();
14151 m.insert("get".into(), get.unwrap_or(Value::Undef));
14152 m.insert("set".into(), set.unwrap_or(Value::Undef));
14153 m.insert("enumerable".into(), Value::Bool(a.enumerable));
14154 m.insert("configurable".into(), Value::Bool(a.configurable));
14155 h.new_object(m)
14156 }));
14157 }
14158 let val = with_host(|h| match h.get(&obj) {
14159 // A Buffer's own properties are exactly its byte indices, read out of the
14160 // hidden `@@bytes` slot; `length`/`byteLength` are internal bookkeeping
14161 // that V8 keeps on the prototype, so they own no descriptor.
14162 Some(JsObj::Object(p))
14163 if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("Buffer") =>
14164 {
14165 match (
14166 p.get("@@bytes").and_then(|b| h.get(b)),
14167 key.parse::<usize>(),
14168 ) {
14169 (Some(JsObj::Array(items)), Ok(i)) => items.get(i).cloned(),
14170 _ => None,
14171 }
14172 }
14173 Some(JsObj::Object(p)) => p.get(&key).cloned(),
14174 // An array's index keys read the elements; `length` is the exotic own
14175 // property; anything else is an ordinary own key in the side table.
14176 Some(JsObj::Array(items)) => match key.parse::<usize>() {
14177 // An ELIDED index owns no property at all, so it has no descriptor.
14178 Ok(i) if h.is_hole(&obj, i) => None,
14179 Ok(i) => items.get(i).cloned(),
14180 Err(_) if key == "length" => Some(Value::Float(items.len() as f64)),
14181 Err(_) => h.fn_prop(&obj, &key),
14182 },
14183 // A function/class own prop lives in the fn-prop side table.
14184 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(&obj, &key),
14185 _ => None,
14186 });
14187 match val {
14188 Some(v) => Ok(with_host(|h| {
14189 let a = h.prop_attrs(&obj, &key);
14190 let mut m: IndexMap<String, Value> = IndexMap::new();
14191 m.insert("value".into(), v);
14192 m.insert("writable".into(), Value::Bool(a.writable));
14193 m.insert("enumerable".into(), Value::Bool(a.enumerable));
14194 m.insert("configurable".into(), Value::Bool(a.configurable));
14195 h.new_object(m)
14196 })),
14197 None => Ok(Value::Undef),
14198 }
14199}
14200
14201/// `Object.getOwnPropertyDescriptors(obj)` — the descriptor of every own string
14202/// key, keyed by name. `Object.create(proto, getOwnPropertyDescriptors(src))` is
14203/// the standard "clone with accessors intact" idiom, so this must agree
14204/// key-for-key with `getOwnPropertyNames`.
14205fn object_get_own_descriptors(args: Vec<Value>) -> Result<Value, String> {
14206 let obj = arg0(&args);
14207 let names = object_keys(vec![obj.clone()], 3)?;
14208 let keys: Vec<String> = with_host(|h| match h.get(&names) {
14209 Some(JsObj::Array(items)) => items.iter().map(|k| h.str_of(k)).collect(),
14210 _ => Vec::new(),
14211 });
14212 let mut out: IndexMap<String, Value> = IndexMap::new();
14213 for k in keys {
14214 let ks = with_host(|h| h.new_str(k.clone()));
14215 let d = object_get_own_descriptor(vec![obj.clone(), ks])?;
14216 if !matches!(d, Value::Undef) {
14217 out.insert(k, d);
14218 }
14219 }
14220 Ok(with_host(|h| h.new_object(out)))
14221}
14222
14223/// `key in obj` respecting the prototype chain. Reports a `Result` because a
14224/// Proxy's `has` trap is user code and may throw.
14225pub fn has_property(obj: &Value, key: &str) -> Result<bool, String> {
14226 if let Some(b) = crate::proxy::has(obj, key)? {
14227 return Ok(b);
14228 }
14229 Ok(has_property_ordinary(obj, key))
14230}
14231
14232/// `[[HasProperty]]` for every non-Proxy receiver.
14233fn has_property_ordinary(obj: &Value, key: &str) -> bool {
14234 // `key in globalThis`: membership matches what the READ answers, which for
14235 // the global object includes every lazily-bound builtin and every global a
14236 // script created. `'Math' in globalThis` and `'x' in globalThis` after
14237 // `x = 1` both answered FALSE while `globalThis.Math` and `globalThis.x`
14238 // read back fine.
14239 if with_host(|h| h.is_global_object(obj))
14240 && !CJS_WRAPPER_LOCALS.contains(&key)
14241 && global_object_binding(key).is_some()
14242 {
14243 return true;
14244 }
14245 // `key in <builtin namespace/prototype>`: membership matches what a property
14246 // read would yield. `String.prototype.indexOf` (and the rest of the builtin
14247 // prototype methods) resolve as callable thunks via `namespace_property`, so
14248 // `'indexOf' in String.prototype` must report true (get-intrinsic probes this
14249 // with the `in` operator before reading the intrinsic).
14250 if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(obj).cloned()) {
14251 return !matches!(namespace_property(&ns, key), Value::Undef);
14252 }
14253 // An integer index of a typed array / Buffer is an own property, and lives
14254 // in the hidden element array rather than the property map — the same
14255 // question `hasOwnProperty` answers, through the same helper. Only a hit
14256 // short-circuits: a non-index key like `'length'` must still fall through
14257 // to the ordinary chain lookup below.
14258 if crate::stdlib::typedarray::has_index(obj, key) == Some(true) {
14259 return true;
14260 }
14261 if with_host(|h| host::lookup_chain(h, obj, key)).is_some() {
14262 return true;
14263 }
14264 if with_host(|h| host::lookup_accessor(h, obj, key)).is_some() {
14265 return true;
14266 }
14267 // A member patched onto the receiver's intrinsic prototype. The READ
14268 // resolves it, so without this `Array.prototype.at = f` made `[].at` a
14269 // function while `'at' in []` stayed false.
14270 if !key.starts_with('#') && inherited_builtin_static(obj, key).is_some() {
14271 return true;
14272 }
14273 if with_host(|h| match h.get(obj) {
14274 Some(JsObj::Object(p)) => p.contains_key(key),
14275 Some(JsObj::Array(items)) => {
14276 key == "length"
14277 || key
14278 .parse::<usize>()
14279 .map(|i| i < items.len() && !h.is_hole(obj, i))
14280 .unwrap_or(false)
14281 // A non-index own property (`arr.foo`, `arr[sym]`) lives in the
14282 // side table, and `in` must see it.
14283 || h.fn_prop(obj, key).is_some()
14284 }
14285 Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(obj, key).is_some(),
14286 // A RegExp's `lastIndex` is an OWN property in node. Here it lives in
14287 // the `RegExpObj` struct rather than a property map, so nothing above
14288 // can see it.
14289 Some(JsObj::RegExp(_)) => key == "lastIndex" || h.fn_prop(obj, key).is_some(),
14290 _ => false,
14291 }) {
14292 return true;
14293 }
14294 // An INHERITED builtin prototype method. These are not objects on the
14295 // prototype chain — they are synthesized by the read path from the
14296 // intrinsic table — so neither `lookup_chain` nor the property map above
14297 // can see them, and `'toString' in {}`, `'push' in []` and `'then' in
14298 // Promise.resolve()` all answered false. That last one is the standard
14299 // thenable test, so the `in` operator disagreed with what a read gives for
14300 // every builtin method of every builtin kind.
14301 inherited_builtin_method(obj, key)
14302}
14303
14304/// Whether a READ of `key` on `obj` would resolve to an inherited builtin
14305/// prototype method. Asked by `in` and `hasOwnProperty`'s negative case; it
14306/// performs no read, so a getter cannot fire.
14307/// A property a script MONKEY-PATCHED onto the intrinsic prototype `obj`
14308/// inherits from (`Array.prototype.at = impl`, `Object.prototype.foo = 1`), or
14309/// `None`.
14310///
14311/// The intrinsic prototypes are namespace handles rather than real objects on
14312/// the chain, so an assignment onto one lands in `builtin_statics` and no
14313/// ordinary chain walk can see it. This is the read side: the receiver's own
14314/// constructor's prototype first, then `Object.prototype`, mirroring
14315/// `inherited_method_owner`'s two-step.
14316pub(crate) fn inherited_builtin_static(obj: &Value, key: &str) -> Option<Value> {
14317 if with_host(|h| h.has_null_proto(obj)) {
14318 return None;
14319 }
14320 let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14321 Some(c) => Some(c),
14322 None if is_arguments(obj) => Some("Object"),
14323 None => with_host(|h| default_ctor_name(h, obj)),
14324 };
14325 // Only the side table is consulted, never the real prototype OBJECT's map:
14326 // `String.prototype` and friends are materialized with their intrinsic
14327 // members present, so reading their maps here would re-route every ordinary
14328 // `"a".toString()` through this path — which recursed until the stack blew.
14329 // `set_property` mirrors a write onto a real intrinsic prototype INTO this
14330 // table precisely so the read side can stay this narrow.
14331 let on = |c: &str| with_host(|h| h.builtin_static(&format!("{c}.prototype"), key));
14332 let found = ctor.and_then(on).or_else(|| on("Object"))?;
14333 // Restoring a saved intrinsic (`const orig = Array.prototype.join; …;
14334 // Array.prototype.join = orig`) stores the SYNTHESIZED thunk for this very
14335 // name back into the table. Dispatching to it would re-enter this lookup
14336 // and recurse until the stack blew, so a thunk that is already this key's
14337 // own intrinsic reports nothing and the ordinary builtin path answers.
14338 let self_thunk = with_host(
14339 |h| matches!(h.get(&found), Some(JsObj::Builtin(s)) if s.starts_with("@proto:") && s.ends_with(&format!(":{key}"))),
14340 );
14341 (!self_thunk).then_some(found)
14342}
14343
14344/// Whether `recv` carries `key` as an OWN property — the guard on
14345/// [`inherited_builtin_static`], since an own property shadows anything
14346/// patched onto a prototype.
14347fn has_own_for_shadow(recv: &Value, key: &str) -> bool {
14348 with_host(|h| {
14349 if h.fn_prop(recv, key).is_some() || h.own_accessor(recv, key).is_some() {
14350 return true;
14351 }
14352 match h.get(recv) {
14353 Some(JsObj::Object(p)) => p.contains_key(key),
14354 // An ELIDED index owns nothing — the whole point of a hole is that
14355 // the lookup continues up the chain — so it must not count as a
14356 // shadow here or an inherited value at that index stays invisible.
14357 Some(JsObj::Array(items)) => {
14358 key == "length" || {
14359 key.parse::<usize>()
14360 .is_ok_and(|i| i < items.len() && !h.is_hole(recv, i))
14361 }
14362 }
14363 _ => false,
14364 }
14365 })
14366}
14367
14368fn inherited_builtin_method(obj: &Value, key: &str) -> bool {
14369 if with_host(|h| h.has_null_proto(obj)) {
14370 return false;
14371 }
14372 if let Some(tag) = crate::stdlib::native_tag(obj) {
14373 if crate::stdlib::instance_has_method(&tag, key) {
14374 return true;
14375 }
14376 }
14377 inherited_method_owner(obj, key).is_some()
14378}
14379
14380/// Whether `recv`'s intrinsic prototype is still on its chain — that is,
14381/// whether `Array.prototype`'s methods are still reachable from an array.
14382///
14383/// A builtin's methods are synthesized from the receiver's KIND rather than
14384/// found on a chain, so replacing the prototype could not take them away:
14385/// `Object.setPrototypeOf(a, {})` left `a.join` a function where node reports
14386/// `undefined`, and `Object.setPrototypeOf(a, null)` did too. The exotic
14387/// storage is unaffected either way — `Array.isArray`, `a.length` and `a[0]`
14388/// all still answer, as they do in node.
14389///
14390/// The overwhelmingly common case is the DEFAULT link, which is recorded as no
14391/// link at all, so this answers true after one map probe and allocates nothing.
14392pub(crate) fn own_intrinsic_reachable_pub(recv: &Value) -> bool {
14393 own_intrinsic_reachable(recv)
14394}
14395
14396fn own_intrinsic_reachable(recv: &Value) -> bool {
14397 // A BOXED primitive needs no special case here: its methods resolve through
14398 // `inherited_method_owner`, which applies the wrapper rule itself.
14399 with_host(|h| default_ctor_name(h, recv)).map_or(true, |c| intrinsic_reachable(recv, c))
14400}
14401
14402/// Whether the intrinsic prototype for `ctor` is still on `recv`'s chain.
14403fn intrinsic_reachable(recv: &Value, ctor: &str) -> bool {
14404 let own = Some(ctor);
14405 let mut cur = recv.clone();
14406 for _ in 0..100 {
14407 let explicit = with_host(|h| h.proto_of(&cur));
14408 let Some(p) = explicit else {
14409 // No explicit link: the implicit prototype is this object's own
14410 // kind's, which is what `recv` is asking about only while `cur` is
14411 // still `recv` itself.
14412 if with_host(|h| h.has_null_proto(&cur)) {
14413 return false;
14414 }
14415 let implicit = with_host(|h| default_ctor_name(h, &cur));
14416 // Every implicit prototype chain ends at `Object.prototype`, so a
14417 // question about `Object` is answered yes by any of them.
14418 return implicit == own || ctor == "Object";
14419 };
14420 if with_host(|h| h.is_null(&p)) {
14421 return false;
14422 }
14423 let hit = with_host(|h| {
14424 own.is_some_and(|c| {
14425 matches!(h.get(&p), Some(JsObj::Builtin(ns)) if *ns == format!("{c}.prototype"))
14426 || h.intrinsic_proto_ctor(&p) == Some(c)
14427 || (c == "Object" && h.object_proto() == p)
14428 })
14429 });
14430 if hit {
14431 return true;
14432 }
14433 // A CLASS prototype object is not linked to the builtin its class
14434 // extends — the `extends` relationship is recorded on the class value —
14435 // so the walk has to cross over there or `class D extends Array {}` ends
14436 // it, and every inherited method of every subclass instance vanishes.
14437 if let Some(builtin) = with_host(|h| {
14438 h.class_owning_proto(&p)
14439 .and_then(|c| h.class_builtin_ancestor(&c))
14440 .map(|b| h.callable_name(&b))
14441 }) {
14442 if own == Some(builtin.as_str()) || ctor == "Object" {
14443 return true;
14444 }
14445 }
14446 cur = p;
14447 }
14448 false
14449}
14450
14451/// The intrinsic prototypes actually ON `recv`'s explicit chain, nearest first
14452/// — the complement of [`intrinsic_reachable`], which asks about one known
14453/// constructor.
14454///
14455/// `Object.create(Array.prototype)` is an ordinary object whose chain reaches
14456/// `Array.prototype`, and node resolves the whole of `Array.prototype` through
14457/// it: `o.push(1)` works, because those methods are generic over their receiver
14458/// (which is also why `Array.prototype.push.call({length: 0}, 1)` already
14459/// worked here). Deciding the owner from the receiver's KIND alone made every
14460/// one of them `undefined` — the same "methods come from the kind, not the
14461/// chain" mistake as the detachment case, in the opposite direction.
14462pub(crate) fn chain_intrinsic_ctors_pub(recv: &Value) -> Vec<&'static str> {
14463 chain_intrinsic_ctors(recv)
14464}
14465
14466fn chain_intrinsic_ctors(recv: &Value) -> Vec<&'static str> {
14467 with_host(|h| chain_intrinsic_ctors_h(h, recv))
14468}
14469
14470/// [`chain_intrinsic_ctors`] against an already-held host borrow, for the
14471/// callers that are inside one — `can_write_prop` takes `&JsHost`, so going
14472/// back through `with_host` there aborts the process on a double borrow.
14473pub(crate) fn chain_intrinsic_ctors_h(h: &host::JsHost, recv: &Value) -> Vec<&'static str> {
14474 let mut out: Vec<&'static str> = Vec::new();
14475 let mut cur = recv.clone();
14476 for _ in 0..100 {
14477 let Some(p) = h.proto_of(&cur) else {
14478 break;
14479 };
14480 if h.is_null(&p) {
14481 break;
14482 }
14483 let name = match h.get(&p) {
14484 Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
14485 _ => h.intrinsic_proto_ctor(&p).map(str::to_string),
14486 };
14487 if let Some(n) = name {
14488 if let Some(c) = crate::arity::PROTO_MEMBERS
14489 .iter()
14490 .map(|(k, _)| *k)
14491 .find(|k| *k == n)
14492 {
14493 if !out.contains(&c) {
14494 out.push(c);
14495 }
14496 }
14497 }
14498 cur = p;
14499 }
14500 out
14501}
14502
14503/// The constructor whose prototype defines `key` for `obj` — its own if that
14504/// prototype has it, otherwise `Object` — or `None` when neither does.
14505///
14506/// Used both by `in` and by the READ, so the two cannot disagree about which
14507/// prototype a name comes from. `new Map().toString` is `Map.prototype`'s and
14508/// `new Map().hasOwnProperty` is `Object.prototype`'s.
14509pub(crate) fn inherited_method_owner_pub(obj: &Value, key: &str) -> Option<&'static str> {
14510 inherited_method_owner(obj, key)
14511}
14512
14513fn inherited_method_owner(obj: &Value, key: &str) -> Option<&'static str> {
14514 if with_host(|h| h.has_null_proto(obj)) {
14515 return None;
14516 }
14517 // The generated prototype-member table, which unlike the arity table knows
14518 // about the ACCESSORS — `size` on a Map, `source` on a RegExp, `description`
14519 // on a Symbol are members but not functions — and about `constructor`.
14520 // A BOXED primitive reports its wrapper's constructor, not `Object` —
14521 // `'description' in Object(Symbol())` is true. The box is an ordinary
14522 // object carrying the primitive in a slot, so the ctor comes from what it
14523 // holds rather than from the box itself.
14524 let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
14525 Some(c) => Some(c),
14526 // An `arguments` object is ARRAY-BACKED here so that indices, `length`,
14527 // spread and `for-of` work, but node's is an exotic that inherits from
14528 // `Object.prototype` — `typeof arguments.map` is `undefined`. Reporting
14529 // its backing kind would hand it the whole `Array.prototype`.
14530 None if is_arguments(obj) => Some("Object"),
14531 None => with_host(|h| default_ctor_name(h, obj)),
14532 };
14533 let on_proto = |c: &str| {
14534 crate::arity::PROTO_MEMBERS
14535 .binary_search_by(|(k, _)| (*k).cmp(c))
14536 .ok()
14537 .is_some_and(|i| {
14538 crate::arity::PROTO_MEMBERS[i]
14539 .1
14540 .iter()
14541 .any(|m| m.strip_prefix('+').unwrap_or(m) == key)
14542 })
14543 };
14544 // `PROTO_MEMBERS` is generated from the prototypes' STRING keys, so a
14545 // well-known symbol member is absent from it. For an object whose CHAIN
14546 // reaches an intrinsic prototype the intrinsic table has to be consulted as
14547 // well, or `[...Object.create(Array.prototype)]` finds no `Symbol.iterator`
14548 // at all. It is deliberately NOT consulted for the receiver's own kind:
14549 // there a thunk would be minted for every `@@` member the table names,
14550 // including ones whose dispatch has no implementation for that receiver,
14551 // and `[...buffer]` then failed with `@@iterator is not a function`.
14552 //
14553 // It is narrowed further to an ORDINARY object: a natively-tagged receiver
14554 // (a typed array, a Buffer) is linked to a real intrinsic prototype too,
14555 // and minting a thunk there produced `@@iterator is not a function` for
14556 // `[...new Uint8Array(ab)]` — those kinds reach their iterator by their own
14557 // fast path, which the table entry would shadow.
14558 let plain = with_host(|h| h.kind_of(obj)) == Some(ObjKind::Object)
14559 && crate::stdlib::native_tag(obj).is_none();
14560 let on_proto_or_symbol =
14561 |c: &str| on_proto(c) || (plain && builtin_meta(&format!("@proto:{c}:{key}")).is_some());
14562 // Each candidate is only an answer while ITS prototype is still on the
14563 // receiver's chain. The two are asked separately: replacing an array's
14564 // prototype with a plain object takes `Array.prototype`'s methods away and
14565 // leaves `Object.prototype`'s, since the replacement inherits from it.
14566 if let Some(c) = ctor.filter(|c| on_proto(c) && intrinsic_reachable(obj, c)) {
14567 return Some(c);
14568 }
14569 // An intrinsic prototype the receiver's chain passes THROUGH, which its own
14570 // kind does not account for.
14571 if let Some(c) = chain_intrinsic_ctors(obj)
14572 .into_iter()
14573 .find(|c| on_proto_or_symbol(c))
14574 {
14575 return Some(c);
14576 }
14577 // Everything else inherits `Object.prototype`'s.
14578 if on_proto("Object") && intrinsic_reachable(obj, "Object") {
14579 return Some("Object");
14580 }
14581 None
14582}
14583
14584/// `structuredClone` — a deep copy of plain data (objects/arrays/primitives).
14585/// `structuredClone` — the HTML structured-clone algorithm's shape: a deep copy
14586/// that preserves the *reference graph*. Two properties pointing at the same
14587/// object clone to two properties pointing at the same clone, and a cycle clones
14588/// to a cycle instead of recursing forever. `seen` maps each source heap index
14589/// to its clone, which is what buys both.
14590/// The rendering node puts in a `DataCloneError` for a value the structured
14591/// clone algorithm refuses, or `None` when the value IS cloneable.
14592///
14593/// Refusing at all is the point: these used to be copied through by reference,
14594/// so `structuredClone({f: () => 1})` handed back an object sharing the
14595/// original's function and `structuredClone(new WeakMap())` returned the very
14596/// same WeakMap. Node throws on every one of them.
14597fn clone_refusal(v: &Value) -> Option<String> {
14598 let kind = with_host(|h| h.kind_of(v))?;
14599 let render = |ctor: &str| Some(format!("#<{ctor}>"));
14600 match kind {
14601 // A function renders as its SOURCE TEXT here, which each FuncDef
14602 // keeps as a span into its script (`JsHost::func_source`).
14603 ObjKind::Func | ObjKind::Class | ObjKind::BoundFunc | ObjKind::BoundMethod => {
14604 Some(with_host(|h| h.str_of(v)))
14605 }
14606 ObjKind::Builtin if with_host(|h| host::is_callable(h, v)) => {
14607 Some(with_host(|h| h.str_of(v)))
14608 }
14609 ObjKind::Symbol => Some(with_host(|h| h.str_of(v))),
14610 ObjKind::Promise => render("Promise"),
14611 ObjKind::Generator => Some("[object Generator]".to_string()),
14612 // A proxy is refused by its TARGET's shape: a callable one renders like
14613 // the function it wraps, everything else as a plain object.
14614 ObjKind::Proxy => Some(if with_host(|h| host::is_callable(h, v)) {
14615 with_host(|h| h.str_of(v))
14616 } else {
14617 "#<Object>".to_string()
14618 }),
14619 ObjKind::Map if with_host(|h| matches!(h.get(v), Some(JsObj::Map { weak: true, .. }))) => {
14620 render("WeakMap")
14621 }
14622 ObjKind::Set if with_host(|h| matches!(h.get(v), Some(JsObj::Set { weak: true, .. }))) => {
14623 render("WeakSet")
14624 }
14625 _ => match crate::stdlib::native_tag(v).as_deref() {
14626 Some(t @ ("WeakRef" | "FinalizationRegistry")) => render(t),
14627 _ => None,
14628 },
14629 }
14630}
14631
14632/// `structuredClone(value[, { transfer }])`.
14633///
14634/// Everything in `transfer` must be an `ArrayBuffer`, and each one is DETACHED
14635/// after the clone — its bytes belong to the copy. The option used to be
14636/// ignored entirely, so the source buffer stayed usable where node leaves it
14637/// with zero length.
14638fn structured_clone(args: Vec<Value>) -> Result<Value, String> {
14639 let list: Vec<Value> = match args.get(1).filter(|v| !matches!(v, Value::Undef)) {
14640 Some(opts) => {
14641 let t = get_property(opts, "transfer")?;
14642 if matches!(t, Value::Undef) {
14643 Vec::new()
14644 } else {
14645 host::iter_all(&t)?
14646 }
14647 }
14648 None => Vec::new(),
14649 };
14650 for item in &list {
14651 if crate::stdlib::native_tag(item).as_deref() != Some("ArrayBuffer") {
14652 return Err(host::dom_error(
14653 "DataCloneError",
14654 "Found invalid value in transferList.",
14655 ));
14656 }
14657 }
14658 let out = deep_clone(&arg0(&args))?;
14659 for item in &list {
14660 crate::stdlib::typedarray::detach_buffer(item);
14661 }
14662 Ok(out)
14663}
14664
14665pub(crate) fn deep_clone(v: &Value) -> Result<Value, String> {
14666 deep_clone_seen(v, &mut std::collections::HashMap::new())
14667}
14668
14669fn deep_clone_seen(
14670 v: &Value,
14671 seen: &mut std::collections::HashMap<u32, Value>,
14672) -> Result<Value, String> {
14673 let idx = match v {
14674 Value::Obj(i) => *i,
14675 _ => return Ok(v.clone()),
14676 };
14677 if let Some(done) = seen.get(&idx) {
14678 return Ok(done.clone());
14679 }
14680 if crate::stdlib::typedarray::is_detached(v) {
14681 return Err(host::dom_error(
14682 "DataCloneError",
14683 "An ArrayBuffer is detached and could not be cloned.",
14684 ));
14685 }
14686 if let Some(render) = clone_refusal(v) {
14687 return Err(host::dom_error(
14688 "DataCloneError",
14689 &format!("{render} could not be cloned."),
14690 ));
14691 }
14692 // A REGEXP is cloned, not shared: it carries a mutable `lastIndex`, so
14693 // handing back the same object let a write through the clone move the
14694 // original's match cursor.
14695 if let Some((src, flags)) = with_host(|h| match h.get(v) {
14696 Some(JsObj::RegExp(r)) => Some((r.source.clone(), r.flags.clone())),
14697 _ => None,
14698 }) {
14699 let args = with_host(|h| vec![h.new_str(src), h.new_str(flags)]);
14700 let out = regexp_ctor(&args)?;
14701 seen.insert(idx, out.clone());
14702 return Ok(out);
14703 }
14704 Ok(match with_host(|h| h.get(v).cloned()) {
14705 Some(JsObj::Array(items)) => {
14706 // Register the (empty) clone BEFORE recursing so a self-reference
14707 // resolves to it.
14708 let out = with_host(|h| h.new_array(Vec::new()));
14709 seen.insert(idx, out.clone());
14710 let mut cloned: Vec<Value> = Vec::with_capacity(items.len());
14711 for x in &items {
14712 cloned.push(deep_clone_seen(x, seen)?);
14713 }
14714 with_host(|h| {
14715 if let Some(JsObj::Array(a)) = h.get_mut(&out) {
14716 *a = cloned;
14717 }
14718 // A sparse source clones to an equally sparse array: the clone
14719 // walks own properties, so a hole is nothing to copy.
14720 h.copy_holes(v, &out, Some);
14721 });
14722 out
14723 }
14724 Some(JsObj::Object(_)) => {
14725 let out = with_host(|h| h.new_object(IndexMap::new()));
14726 seen.insert(idx, out.clone());
14727 // Own ENUMERABLE string keys, read THROUGH any accessor: the clone
14728 // walked the property map, where an accessor stores nothing, so
14729 // `structuredClone({get p(){return 1}})` silently lost `p`. A symbol
14730 // key and a non-enumerable one are dropped, as node drops them.
14731 let is_error = with_host(|h| h.error_to_string(v)).is_some();
14732 let proto = clone_proto(v);
14733 let keeps_proto = !matches!(proto, CloneProto::Plain);
14734 // An ERROR clones its name, message and stack and NOTHING else —
14735 // node drops any other own property, even an enumerable one.
14736 let keys: Vec<String> = if is_error {
14737 // An ERROR clones its name, message and stack and NOTHING else —
14738 // node drops any other own property, even an enumerable one.
14739 ["name", "message", "stack"]
14740 .iter()
14741 .filter(|k| has_property(v, k).unwrap_or(false))
14742 .map(|k| (*k).to_string())
14743 .collect()
14744 } else if keeps_proto {
14745 // A preserved exotic keeps EVERY own property, including the
14746 // non-enumerable ones and the internal slots — a Date's time
14747 // value, an ArrayBuffer's `byteLength` and byte store, a typed
14748 // array's view. The enumerable-only walk dropped all of those,
14749 // so a cloned Date read `Invalid Date` and a cloned
14750 // ArrayBuffer had no `byteLength`.
14751 with_host(|h| match h.get(v) {
14752 Some(JsObj::Object(p)) => p.keys().cloned().collect(),
14753 _ => Vec::new(),
14754 })
14755 } else {
14756 with_host(|h| h.own_enum_key_names(v))
14757 };
14758 let mut cloned: IndexMap<String, Value> = IndexMap::new();
14759 for k in keys {
14760 // An internal slot is read straight out of the map: it is not a
14761 // property, so a `[[Get]]` would not find it.
14762 let val = if k.starts_with("@@") {
14763 match with_host(|h| match h.get(v) {
14764 Some(JsObj::Object(p)) => p.get(&k).cloned(),
14765 _ => None,
14766 }) {
14767 Some(val) => val,
14768 None => continue,
14769 }
14770 } else {
14771 get_property(v, &k)?
14772 };
14773 cloned.insert(k, deep_clone_seen(&val, seen)?);
14774 }
14775 // The prototype survives only for the exotics the algorithm knows —
14776 // a Date, an Error, a typed array, a boxed primitive. A USER class
14777 // instance becomes a plain object, which is what node produces;
14778 // keeping every prototype made `structuredClone(new K())
14779 // instanceof K` true.
14780 with_host(|h| {
14781 if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14782 *p = cloned;
14783 }
14784 match &proto {
14785 CloneProto::Same => {
14786 if let Some(p) = h.proto_of(v) {
14787 h.set_proto(&out, p);
14788 }
14789 }
14790 CloneProto::Ctor(c) => {
14791 h.ensure_error_protos();
14792 let p = h.error_proto(c).or_else(|| h.ensure_ctor_proto(c));
14793 if let Some(p) = p {
14794 h.set_proto(&out, p);
14795 }
14796 // A Buffer clones to a plain `Uint8Array`, so the native
14797 // tag has to change with the prototype — left alone,
14798 // `Buffer.isBuffer` still answered true for the clone.
14799 // A Buffer clones to a plain `Uint8Array`, so the native
14800 // tag has to change with the prototype — left alone,
14801 // `Buffer.isBuffer` answered true for the clone and the
14802 // brand stayed `[object Object]`. A typed array is
14803 // tagged `TypedArray` and names its element type in
14804 // `@@kind`; `@@native = "Uint8Array"` matches no arm.
14805 if c == "Uint8Array" {
14806 let tag = h.new_str("TypedArray");
14807 let kind = h.new_str("Uint8Array");
14808 if let Some(JsObj::Object(p)) = h.get_mut(&out) {
14809 p.insert("@@native".into(), tag);
14810 p.insert("@@kind".into(), kind);
14811 }
14812 }
14813 }
14814 CloneProto::Plain => {}
14815 }
14816 h.copy_prop_attrs(v, &out);
14817 });
14818 out
14819 }
14820 // Map/Set are structured types: clone the entries, keep the kind.
14821 Some(JsObj::Map { entries, weak }) => {
14822 let out = with_host(|h| {
14823 h.alloc(JsObj::Map {
14824 entries: IndexMap::new(),
14825 weak,
14826 })
14827 });
14828 seen.insert(idx, out.clone());
14829 let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
14830 for (k, val) in pairs {
14831 let ck = deep_clone_seen(&k, seen)?;
14832 let cv = deep_clone_seen(&val, seen)?;
14833 let _ = map_method(&out, "set", vec![ck, cv]);
14834 }
14835 out
14836 }
14837 Some(JsObj::Set { entries, weak }) => {
14838 let out = with_host(|h| {
14839 h.alloc(JsObj::Set {
14840 entries: IndexMap::new(),
14841 weak,
14842 })
14843 });
14844 seen.insert(idx, out.clone());
14845 let vals: Vec<Value> = entries.values().cloned().collect();
14846 for x in vals {
14847 let cx = deep_clone_seen(&x, seen)?;
14848 let _ = set_method(&out, "add", vec![cx]);
14849 }
14850 out
14851 }
14852 // A string, a BigInt and a boxed primitive are immutable enough to
14853 // share; anything left is a value type.
14854 _ => v.clone(),
14855 })
14856}
14857
14858/// Whether a cloned object keeps the source's prototype.
14859///
14860/// The structured clone algorithm reproduces the exotics it knows and turns
14861/// everything else into a plain object — so a `Date` clones to a `Date` and a
14862/// user class instance clones to an `Object`.
14863fn clone_proto(v: &Value) -> CloneProto {
14864 // An ERROR clones to the BUILT-IN class its `name` selects, so a subclass
14865 // flattens: `structuredClone(new (class E extends Error{})('m'))` reports
14866 // `Error`, not `E`.
14867 if with_host(|h| h.error_to_string(v)).is_some() {
14868 let name = get_property(v, "name")
14869 .map(|n| with_host(|h| h.str_of(&n)))
14870 .unwrap_or_else(|_| "Error".into());
14871 let class = if host::ERROR_NAMES.contains(&name.as_str()) {
14872 name
14873 } else {
14874 "Error".to_string()
14875 };
14876 return CloneProto::Ctor(class);
14877 }
14878 match crate::stdlib::native_tag(v).as_deref() {
14879 // A Buffer is not reproduced as a Buffer: node hands back a plain
14880 // `Uint8Array` over the same bytes.
14881 Some("Buffer") => CloneProto::Ctor("Uint8Array".into()),
14882 Some(_) => CloneProto::Same,
14883 // A boxed primitive keeps its wrapper; anything else — a user class
14884 // instance included — becomes a plain object.
14885 None if wrapped_primitive(v).is_some() => CloneProto::Same,
14886 None => CloneProto::Plain,
14887 }
14888}
14889
14890/// Which prototype a clone gets: the source's, a named builtin's, or none.
14891enum CloneProto {
14892 Same,
14893 Ctor(String),
14894 Plain,
14895}
14896
14897// ══ Promises, timers, microtasks (event-loop-driven) ═════════════════════════
14898
14899/// A short `Name: message` string for an error value (used when an await
14900/// rejection unwinds as a thrown error).
14901pub fn error_string(h: &host::JsHost, v: &Value) -> String {
14902 if let Some(JsObj::Object(props)) = h.get(v) {
14903 let name = props
14904 .get("name")
14905 .map(|x| h.str_of(x))
14906 .or_else(|| host::lookup_chain(h, v, "name").map(|x| h.str_of(&x)))
14907 .unwrap_or_else(|| "Error".into());
14908 if let Some(m) = props.get("message") {
14909 return format!("{name}: {}", h.str_of(m));
14910 }
14911 return name;
14912 }
14913 h.str_of(v)
14914}
14915
14916/// 27.2.5.3 `thenFinally`/`catchFinally`: `PromiseResolve(result).then(() =>
14917/// value)`, or `() => { throw reason }` on the reject path.
14918///
14919/// Returning the carried value directly — what this used to do — skipped both
14920/// halves. A promise returned by the callback was never awaited, so the
14921/// ordinary async-cleanup shape
14922///
14923/// ```text
14924/// work().finally(() => closeConnection()).then(next)
14925/// ```
14926///
14927/// ran `next` before the connection had closed. And the chain settled three
14928/// microtask ticks early, which is observable in ordering against any other
14929/// chain, not just against a timer.
14930///
14931/// A rejection from the callback's own promise wins over the carried value, so
14932/// no reject handler is attached here: it propagates on its own.
14933fn finally_chain(result: Value, carried: Value, rethrow: bool) -> Value {
14934 // PromiseResolve (27.2.4.7) returns an argument that is already a promise
14935 // UNCHANGED. Wrapping it anyway costs the extra tick that resolving with a
14936 // thenable takes to adopt it, which showed up as a callback returning a
14937 // rejected promise settling one tick late against every other chain.
14938 let p = match with_host(|h| h.promise_id(&result)) {
14939 Some(_) => result,
14940 None => {
14941 let fresh = with_host(|h| h.new_promise());
14942 if let Some(pid) = with_host(|h| h.promise_id(&fresh)) {
14943 host::resolve_promise_val(pid, result);
14944 }
14945 fresh
14946 }
14947 };
14948 let cell = with_host(|h| h.new_array(vec![carried]));
14949 let idx = match cell {
14950 Value::Obj(i) => i,
14951 _ => 0,
14952 };
14953 let tag = if rethrow { "finrethrow" } else { "finret" };
14954 let thunk = make_builtin(format!("@@{tag}:{idx}"));
14955 host::promise_then(&p, thunk, Value::Undef)
14956}
14957
14958fn make_builtin(name: String) -> Value {
14959 with_host(|h| h.alloc(JsObj::Builtin(name)))
14960}
14961
14962/// `[[GetPrototypeOf]]` (10.1.1) — the answer `Object.getPrototypeOf`,
14963/// `Reflect.getPrototypeOf` and a `__proto__` READ all have to agree on.
14964///
14965/// `__proto__` used to answer from `JsHost::proto_of` alone, which records only
14966/// an EXPLICIT link, so an object on the default prototype reported `null`:
14967/// `({}).__proto__ === Object.prototype` was false while
14968/// `Object.getPrototypeOf({}) === Object.prototype` was true. One function, so
14969/// the three cannot drift apart again.
14970pub fn prototype_of(v: &Value) -> Value {
14971 // Constructor-side inheritance: `Buffer extends Uint8Array`, so
14972 // `Object.getPrototypeOf(Buffer)` is the `Uint8Array` constructor itself,
14973 // not `Function.prototype`. This is the class-side half of the subclass
14974 // link — the instance-side half is `Buffer.prototype`'s `[[Prototype]]`.
14975 if matches!(with_host(|h| h.get(v).cloned()), Some(JsObj::Builtin(ref n)) if n == "Buffer") {
14976 return with_host(|h| h.alloc(JsObj::Builtin("Uint8Array".into())));
14977 }
14978 // Constructor-side inheritance for a `class B extends A` (ClassDefinition
14979 // 15.7.14 step 6.d: the constructor's `[[Prototype]]` is the parent
14980 // CONSTRUCTOR, not `Function.prototype`). Statics already resolved through
14981 // `ClassVal.parent`, but the link itself was invisible, so
14982 // `Object.getPrototypeOf(B) === A` read false and any library walking the
14983 // constructor chain — rather than calling a static — saw a base class.
14984 // A base class keeps the default answer below (`Function.prototype`).
14985 if let Some(JsObj::Class(c)) = with_host(|h| h.get(v).cloned()) {
14986 if let Some(parent) = c.parent {
14987 return parent;
14988 }
14989 }
14990 // `Object.create(null)` and friends really do have a null prototype.
14991 if with_host(|h| h.has_null_proto(v)) {
14992 return with_host(|h| h.null());
14993 }
14994 // `Object.prototype` is the CHAIN ROOT, so its own prototype is `null`. It
14995 // reported itself, because the fallback below answers by constructor name
14996 // and a plain object's is `Object` — an infinite chain to anything walking
14997 // it.
14998 if with_host(|h| h.strict_eq(v, &h.object_proto())) {
14999 return with_host(|h| h.null());
15000 }
15001 // Every OTHER builtin prototype namespace (`Array.prototype`,
15002 // `Function.prototype`, …) inherits from `Object.prototype`; the fallback
15003 // would send it back to a namespace handle for its own constructor.
15004 if matches!(
15005 with_host(|h| h.get(v).cloned()),
15006 Some(JsObj::Builtin(ref n)) if n.ends_with(".prototype")
15007 ) {
15008 return with_host(|h| h.object_proto());
15009 }
15010 if let Some(p) = with_host(|h| h.proto_of(v)) {
15011 return p;
15012 }
15013 // A builtin exotic with no explicit `[[Prototype]]` link reports its
15014 // constructor's prototype namespace (`Object.getPrototypeOf([]) ===
15015 // Array.prototype`), which `strict_eq` compares by name. A plain object
15016 // reports the one real `Object.prototype` object.
15017 with_host(|h| {
15018 h.ensure_native_protos();
15019 match default_ctor_name(h, v) {
15020 Some("Object") => h.object_proto(),
15021 // `String`/`Number`/`Boolean` own REAL prototype objects, so a
15022 // primitive must report that object and not a fresh namespace
15023 // thunk — otherwise `Object.getPrototypeOf(1) === Number.prototype`
15024 // compares a thunk against the real object and reads false.
15025 Some(c) => h
15026 .native_proto(c)
15027 .unwrap_or_else(|| h.alloc(JsObj::Builtin(format!("{c}.prototype")))),
15028 None => h.null(),
15029 }
15030 })
15031}
15032
15033/// `new Promise((resolve, reject) => …)` — run the executor synchronously with
15034/// internal resolve/reject functions.
15035/// A fresh promise built through the SPECIES constructor, when a `Promise`
15036/// static was reached through a subclass.
15037///
15038/// `class P extends Promise {}` makes `P.resolve(1)` a `P`, because every
15039/// combinator builds its result with `this` (27.2.4.x). They all allocated a
15040/// plain promise, so nothing a subclass produced was an instance of it. The
15041/// executor is a no-op: the result is settled through its promise id, which is
15042/// what the ordinary path does too.
15043fn promise_species_create() -> Result<Option<Value>, String> {
15044 let Some(ctor) = host::current_static_this() else {
15045 return Ok(None);
15046 };
15047 if !matches!(
15048 with_host(|h| h.kind_of(&ctor)),
15049 Some(ObjKind::Class) | Some(ObjKind::Func)
15050 ) {
15051 return Ok(None);
15052 }
15053 let species = match get_property(&ctor, "@@species") {
15054 Ok(Value::Undef) => ctor,
15055 Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15056 Ok(s) => s,
15057 Err(_) => ctor,
15058 };
15059 if !matches!(
15060 with_host(|h| h.kind_of(&species)),
15061 Some(ObjKind::Class) | Some(ObjKind::Func)
15062 ) {
15063 return Ok(None);
15064 }
15065 let noop = make_builtin("@@pnoop".to_string());
15066 let p = host::construct(&species, vec![noop])?;
15067 // Only usable if the subclass really produced a promise; a constructor that
15068 // returned something else has no id to settle.
15069 Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15070}
15071
15072/// The species constructor of a promise RECEIVER — what `then`/`catch`/`finally`
15073/// build their result with (`SpeciesConstructor(p, %Promise%)`, 27.2.5.4 step 3).
15074///
15075/// Distinct from `promise_species_create`, which answers for a STATIC reached
15076/// through a subclass. Here the subclass comes from the receiver itself, so
15077/// `P.resolve(1).then(f)` is also a `P`.
15078pub fn promise_species_from(recv: &Value) -> Result<Option<Value>, String> {
15079 // A chain lookup: a Promise receiver resolves through the stdlib funnel,
15080 // which has no `constructor` entry of its own.
15081 let ctor = with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef);
15082 if !matches!(
15083 with_host(|h| h.kind_of(&ctor)),
15084 Some(ObjKind::Class) | Some(ObjKind::Func)
15085 ) {
15086 return Ok(None);
15087 }
15088 let species = match get_property(&ctor, "@@species") {
15089 Ok(Value::Undef) => ctor,
15090 Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
15091 Ok(s) => s,
15092 Err(_) => ctor,
15093 };
15094 if !matches!(
15095 with_host(|h| h.kind_of(&species)),
15096 Some(ObjKind::Class) | Some(ObjKind::Func)
15097 ) {
15098 return Ok(None);
15099 }
15100 let noop = make_builtin("@@pnoop".to_string());
15101 let p = host::construct(&species, vec![noop])?;
15102 Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
15103}
15104
15105fn new_promise(executor: Value) -> Result<Value, String> {
15106 let p = with_host(|h| h.new_promise());
15107 let id = with_host(|h| h.promise_id(&p).unwrap());
15108 let res = make_builtin(format!("@@presolve:{id}"));
15109 let rej = make_builtin(format!("@@preject:{id}"));
15110 if let Err(e) = host::invoke(&executor, vec![res, rej], None) {
15111 // A throw in the executor rejects the promise.
15112 let ev = host::take_exc_or_error(&e);
15113 host::reject_promise_val(id, ev);
15114 }
15115 Ok(p)
15116}
15117
15118/// `Promise.resolve(v)` for stdlib callers that need to hand back an
15119/// already-settled promise.
15120pub fn promise_resolve_pub(v: Value) -> Result<Value, String> {
15121 promise_resolve(v)
15122}
15123
15124fn promise_resolve(v: Value) -> Result<Value, String> {
15125 if let Some(p) = promise_species_create()? {
15126 let id = with_host(|h| h.promise_id(&p).unwrap());
15127 host::resolve_promise_val(id, v);
15128 return Ok(p);
15129 }
15130 Ok(host::promise_of(&v))
15131}
15132fn promise_reject(v: Value) -> Result<Value, String> {
15133 let p = match promise_species_create()? {
15134 Some(p) => p,
15135 None => with_host(|h| h.new_promise()),
15136 };
15137 let id = with_host(|h| h.promise_id(&p).unwrap());
15138 host::reject_promise_val(id, v);
15139 Ok(p)
15140}
15141
15142/// `Promise.withResolvers()` — a fresh pending promise paired with its own
15143/// resolve/reject continuations (the same `@@presolve`/`@@preject` thunks the
15144/// executor receives), returned as a plain `{ promise, resolve, reject }` object.
15145/// A fresh pending promise paired with the thunk that resolves it, for stdlib
15146/// callers that hand the resolver to an event listener.
15147pub fn pending_promise_with_resolver() -> (Value, Value) {
15148 let p = with_host(|h| h.new_promise());
15149 let id = with_host(|h| h.promise_id(&p).unwrap());
15150 let resolve = make_builtin(format!("@@presolve:{id}"));
15151 (p, resolve)
15152}
15153
15154/// `RegExp.escape(s)` (22.2.4.2) — a string that matches `s` literally.
15155///
15156/// The rule is not "backslash the syntax characters": it also escapes a LEADING
15157/// ASCII alphanumeric, so the result can be concatenated after a `\` or a `{`
15158/// without the two running together, and it escapes the punctuation that is
15159/// meaningful inside a character class or a group name.
15160fn regexp_escape(args: Vec<Value>) -> Result<Value, String> {
15161 let v = arg0(&args);
15162 if !matches!(v, Value::Str(_)) && !with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_)))) {
15163 return Err(host::type_error("input argument must be a string"));
15164 }
15165 let s = with_host(|h| h.str_of(&v));
15166 // Punctuation that is escaped by CODE POINT rather than with a backslash.
15167 // Measured against node over the whole ASCII range, not taken from a list:
15168 // `-` and `=` are here, `$` and `*` are syntax characters and are not.
15169 const OTHER_PUNCTUATORS: &str = " !\"#%&',-:;<=>@`~";
15170 const SYNTAX: &str = "^$\\.*+?()[]{}|/";
15171 let mut out = String::with_capacity(s.len());
15172 for (i, c) in s.chars().enumerate() {
15173 // A leading ASCII alphanumeric, and only a leading one.
15174 if i == 0 && c.is_ascii_alphanumeric() {
15175 out.push_str(&format!("\\x{:02x}", c as u32));
15176 continue;
15177 }
15178 if SYNTAX.contains(c) {
15179 out.push('\\');
15180 out.push(c);
15181 continue;
15182 }
15183 match c {
15184 '\t' => out.push_str("\\t"),
15185 '\n' => out.push_str("\\n"),
15186 '\u{b}' => out.push_str("\\v"),
15187 '\u{c}' => out.push_str("\\f"),
15188 '\r' => out.push_str("\\r"),
15189 _ if OTHER_PUNCTUATORS.contains(c) || is_regex_escape_space(c) => {
15190 let n = c as u32;
15191 if n <= 0xff {
15192 out.push_str(&format!("\\x{n:02x}"));
15193 } else {
15194 out.push_str(&format!("\\u{n:04x}"));
15195 }
15196 }
15197 _ => out.push(c),
15198 }
15199 }
15200 Ok(with_host(|h| h.new_str(out)))
15201}
15202
15203/// The WhiteSpace and LineTerminator code points `RegExp.escape` spells out.
15204/// Deliberately NOT `char::is_whitespace`: U+180E and U+200B are whitespace to
15205/// Unicode but not to ECMAScript, and node leaves both alone.
15206fn is_regex_escape_space(c: char) -> bool {
15207 matches!(
15208 c,
15209 '\u{a0}' | '\u{1680}' | '\u{2000}'
15210 ..='\u{200a}'
15211 | '\u{2028}'
15212 | '\u{2029}'
15213 | '\u{202f}'
15214 | '\u{205f}'
15215 | '\u{3000}'
15216 | '\u{feff}'
15217 )
15218}
15219
15220/// `Error.isError(v)` (20.5.2.1) — a brand check for `[[ErrorData]]`, so an
15221/// object that merely INHERITS from `Error.prototype` is not one.
15222fn error_is_error(args: Vec<Value>) -> Result<Value, String> {
15223 let v = arg0(&args);
15224 Ok(Value::Bool(with_host(|h| has_error_data(h, &v))))
15225}
15226
15227/// Whether `v` carries `[[ErrorData]]` — the slot `Error.isError` (20.5.2.1)
15228/// and `Object.prototype.toString`'s step 9 both test.
15229///
15230/// The brand is the OWN `stack` an error is built with (a `DOMException`
15231/// carries `@@domName` instead); a plain `Object.create(Error.prototype)` has
15232/// neither, which is why inheriting from an error prototype does not make a
15233/// value an error. Shared so the two cannot disagree — branding by a chain
15234/// lookup for `name`/`message` made `Object.create(Error.prototype)` report
15235/// `[object Error]` where node says `[object Object]`, while `Error.isError`
15236/// on the same value already said false.
15237pub(crate) fn has_error_data(h: &host::JsHost, v: &Value) -> bool {
15238 match h.get(v) {
15239 Some(JsObj::Object(p)) => {
15240 p.contains_key("stack") || p.contains_key("@@stackRaw") || p.contains_key("@@domName")
15241 }
15242 _ => false,
15243 }
15244}
15245
15246/// `Promise.try(fn, ...args)` (27.2.4.6) — call `fn` and settle the promise with
15247/// what it does, so a SYNCHRONOUS throw becomes a rejection instead of
15248/// propagating. `Promise.resolve().then(fn)` is the shape it replaces, and it
15249/// costs a tick that this does not.
15250fn promise_try(args: Vec<Value>) -> Result<Value, String> {
15251 let f = arg0(&args);
15252 // A non-callable argument REJECTS, it does not throw: `Promise.try(5)`
15253 // returns a rejected promise, so the surrounding `try` never sees it.
15254 if !with_host(|h| host::is_callable(h, &f)) {
15255 // Node names the TYPE alongside the value — `number 5 is not a
15256 // function` — which the ordinary call-site message does not. A plain
15257 // object and a symbol name only the type; `null` names both.
15258 let shown = with_host(|h| {
15259 let kind = h.type_of(&f);
15260 match kind {
15261 "undefined" => "undefined".to_string(),
15262 "symbol" | "bigint" => kind.to_string(),
15263 "object" if h.is_null(&f) => "object null".to_string(),
15264 "object" => "object".to_string(),
15265 "string" => format!("string \"{}\"", h.str_of(&f)),
15266 _ => format!("{kind} {}", h.str_of(&f)),
15267 }
15268 });
15269 let p = with_host(|h| h.new_promise());
15270 let id = with_host(|h| h.promise_id(&p).unwrap());
15271 let reject = make_builtin(format!("@@preject:{id}"));
15272 let err =
15273 with_host(|h| synth_error(h, &host::type_error(&format!("{shown} is not a function"))));
15274 host::invoke(&reject, vec![err], None)?;
15275 return Ok(p);
15276 }
15277 let rest: Vec<Value> = args.iter().skip(1).cloned().collect();
15278 let p = with_host(|h| h.new_promise());
15279 let id = with_host(|h| h.promise_id(&p).unwrap());
15280 let resolve = make_builtin(format!("@@presolve:{id}"));
15281 let reject = make_builtin(format!("@@preject:{id}"));
15282 let promise = p;
15283 match host::invoke(&f, rest, None) {
15284 Ok(v) => {
15285 host::invoke(&resolve, vec![v], None)?;
15286 }
15287 Err(e) => {
15288 // The thrown VALUE, not a re-synthesis of its rendering: a callback
15289 // that throws a `TypeError` must reject with that object, and
15290 // rebuilding it from the message string flattened it to a plain
15291 // `Error` whose message was the rendered `Uncaught TypeError: t`.
15292 let err =
15293 with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15294 with_host(|h| {
15295 h.error = None;
15296 h.exc = None;
15297 });
15298 host::invoke(&reject, vec![err], None)?;
15299 }
15300 }
15301 Ok(promise)
15302}
15303
15304fn promise_with_resolvers() -> Result<Value, String> {
15305 let p = with_host(|h| h.new_promise());
15306 let id = with_host(|h| h.promise_id(&p).unwrap());
15307 let resolve = make_builtin(format!("@@presolve:{id}"));
15308 let reject = make_builtin(format!("@@preject:{id}"));
15309 let mut props: IndexMap<String, Value> = IndexMap::new();
15310 props.insert("promise".into(), p);
15311 props.insert("resolve".into(), resolve);
15312 props.insert("reject".into(), reject);
15313 Ok(with_host(|h| h.new_object(props)))
15314}
15315
15316/// A promise already rejected with `e` — what every combinator hands back when
15317/// the ITERABLE misbehaves.
15318///
15319/// 27.2.4.1 step 4 catches an abrupt completion from the iteration and rejects
15320/// rather than letting it propagate, so `Promise.all(badIterable)` returns a
15321/// rejected promise. Throwing synchronously meant a `.catch()` never attached
15322/// and the caller saw the error at the call site instead.
15323fn rejected_promise(e: String) -> Value {
15324 let p = with_host(|h| h.new_promise());
15325 let id = with_host(|h| h.promise_id(&p).unwrap());
15326 let reject = make_builtin(format!("@@preject:{id}"));
15327 let err = with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
15328 with_host(|h| {
15329 h.error = None;
15330 h.exc = None;
15331 });
15332 let _ = host::invoke(&reject, vec![err], None);
15333 p
15334}
15335
15336#[derive(Clone, Copy)]
15337enum AllMode {
15338 All,
15339 AllSettled,
15340}
15341
15342/// `Promise.all` / `Promise.allSettled`.
15343fn promise_all(args: Vec<Value>, mode: AllMode) -> Result<Value, String> {
15344 let items = match host::iter_all(&arg0(&args)) {
15345 Ok(v) => v,
15346 Err(e) => return Ok(rejected_promise(e)),
15347 };
15348 // 27.2.4.1 step 3: the combinator builds its result with `this`, so on a
15349 // subclass the promise it hands back is an instance of that subclass.
15350 let result = match promise_species_create()? {
15351 Some(p) => p,
15352 None => with_host(|h| h.new_promise()),
15353 };
15354 let rid = with_host(|h| h.promise_id(&result).unwrap());
15355 let n = items.len();
15356 if n == 0 {
15357 let empty = with_host(|h| h.new_array(Vec::new()));
15358 host::resolve_promise_val(rid, empty);
15359 return Ok(result);
15360 }
15361 // Shared mutable accumulator via Rc<RefCell<…>>.
15362 let slots = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15363 let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15364 for (i, it) in items.into_iter().enumerate() {
15365 let ap = host::promise_of(&it);
15366 let aid = with_host(|h| h.promise_id(&ap).unwrap());
15367 let slots = slots.clone();
15368 let remaining = remaining.clone();
15369 host::subscribe_native(
15370 aid,
15371 Box::new(move |state, val| {
15372 let settled = match mode {
15373 AllMode::All => {
15374 if state == host::PromiseState::Rejected {
15375 host::reject_promise_val(rid, val);
15376 return Ok(());
15377 }
15378 val
15379 }
15380 AllMode::AllSettled => with_host(|h| {
15381 let mut m: IndexMap<String, Value> = IndexMap::new();
15382 if state == host::PromiseState::Rejected {
15383 m.insert("status".into(), h.new_str("rejected"));
15384 m.insert("reason".into(), val);
15385 } else {
15386 m.insert("status".into(), h.new_str("fulfilled"));
15387 m.insert("value".into(), val);
15388 }
15389 h.new_object(m)
15390 }),
15391 };
15392 slots.borrow_mut()[i] = settled;
15393 let mut r = remaining.borrow_mut();
15394 *r -= 1;
15395 if *r == 0 {
15396 let arr = with_host(|h| h.new_array(slots.borrow().clone()));
15397 host::resolve_promise_val(rid, arr);
15398 }
15399 Ok(())
15400 }),
15401 );
15402 }
15403 Ok(result)
15404}
15405
15406/// `Promise.race` (first to settle wins) / `Promise.any` (first to fulfill wins).
15407fn promise_race(args: Vec<Value>, any: bool) -> Result<Value, String> {
15408 let items = match host::iter_all(&arg0(&args)) {
15409 Ok(v) => v,
15410 Err(e) => return Ok(rejected_promise(e)),
15411 };
15412 // Built with `this`, as every combinator is (27.2.4.5 / 27.2.4.3).
15413 let result = match promise_species_create()? {
15414 Some(p) => p,
15415 None => with_host(|h| h.new_promise()),
15416 };
15417 let rid = with_host(|h| h.promise_id(&result).unwrap());
15418 let n = items.len();
15419 let errors = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
15420 let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
15421 for (i, it) in items.into_iter().enumerate() {
15422 let ap = host::promise_of(&it);
15423 let aid = with_host(|h| h.promise_id(&ap).unwrap());
15424 let errors = errors.clone();
15425 let remaining = remaining.clone();
15426 host::subscribe_native(
15427 aid,
15428 Box::new(move |state, val| {
15429 if any {
15430 if state == host::PromiseState::Fulfilled {
15431 host::resolve_promise_val(rid, val);
15432 } else {
15433 errors.borrow_mut()[i] = val;
15434 let mut r = remaining.borrow_mut();
15435 *r -= 1;
15436 if *r == 0 {
15437 // All rejected → AggregateError carrying every reason.
15438 let reasons = with_host(|h| h.new_array(errors.borrow().clone()));
15439 let msg = with_host(|h| h.new_str("All promises were rejected"));
15440 let agg = make_error_inner("AggregateError", &[reasons, msg]);
15441 host::reject_promise_val(rid, agg);
15442 }
15443 }
15444 } else if state == host::PromiseState::Rejected {
15445 host::reject_promise_val(rid, val);
15446 } else {
15447 host::resolve_promise_val(rid, val);
15448 }
15449 Ok(())
15450 }),
15451 );
15452 }
15453 Ok(result)
15454}
15455
15456/// `.then` / `.catch` / `.finally` on a promise.
15457fn promise_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
15458 match name {
15459 "then" => Ok(host::promise_then(
15460 recv,
15461 args.first().cloned().unwrap_or(Value::Undef),
15462 args.get(1).cloned().unwrap_or(Value::Undef),
15463 )),
15464 "catch" => Ok(host::promise_then(
15465 recv,
15466 Value::Undef,
15467 args.first().cloned().unwrap_or(Value::Undef),
15468 )),
15469 "finally" => {
15470 let cb = arg0(&args);
15471 // 27.2.5.3 step 3: a non-callable `onFinally` is handed to `then`
15472 // as BOTH handlers, and `then` ignores a non-callable one — so the
15473 // value or reason simply passes through. Building the thunks
15474 // regardless meant `p.finally(null)` tried to call `null`.
15475 if !with_host(|h| host::is_callable(h, &cb)) {
15476 return Ok(host::promise_then(recv, cb.clone(), cb));
15477 }
15478 let i = match cb {
15479 Value::Obj(i) => i,
15480 _ => 0,
15481 };
15482 let pass = make_builtin(format!("@@finpass:{i}"));
15483 let throw = make_builtin(format!("@@finthrow:{i}"));
15484 Ok(host::promise_then(recv, pass, throw))
15485 }
15486 _ => Err(host::type_error(&format!(
15487 "promise.{name} is not a function"
15488 ))),
15489 }
15490}
15491
15492fn enqueue_microtask(next_tick: bool, cb: Value, args: Vec<Value>) {
15493 with_host(|h| {
15494 if next_tick {
15495 h.queue_nexttick(cb, args);
15496 } else {
15497 h.queue_micro(cb, args);
15498 }
15499 });
15500}
15501
15502/// `setTimeout`/`setInterval`/`setImmediate` — register a macrotask and return
15503/// the handle object Node returns (`Timeout` for the first two, `Immediate` for
15504/// the third), carrying `ref`/`unref`/`hasRef`/`refresh`.
15505///
15506/// `setInterval` schedules a *repeating* timer: the loop re-arms it each time it
15507/// fires, so it runs until cleared and — being referenced — holds the process
15508/// open exactly as in Node.
15509fn schedule_timer(name: &str, args: Vec<Value>) -> Value {
15510 let cb = arg0(&args);
15511 let delay = if name == "setImmediate" {
15512 -1.0 // before any 0ms timeout
15513 } else {
15514 args.get(1)
15515 .map(|d| with_host(|h| h.to_number(d)))
15516 .unwrap_or(0.0)
15517 .max(0.0)
15518 };
15519 let extra = if name == "setImmediate" {
15520 args.get(1..).map(|s| s.to_vec()).unwrap_or_default()
15521 } else {
15522 args.get(2..).map(|s| s.to_vec()).unwrap_or_default()
15523 };
15524 // Node clamps a sub-1ms interval to 1ms, so `setInterval(fn, 0)` yields a
15525 // ~1000Hz timer rather than a busy loop that starves the rest of the queue.
15526 let interval = (name == "setInterval").then(|| delay.max(1.0));
15527 let id = with_host(|h| h.add_timer(delay, cb, extra, interval));
15528 let tag = if name == "setImmediate" {
15529 "Immediate"
15530 } else {
15531 "Timeout"
15532 };
15533 crate::stdlib::timers::new_handle(id, tag)
15534}
15535
15536/// `clearTimeout`/`clearInterval`/`clearImmediate` — cancel by handle object or
15537/// by the bare id it coerces to (code that stored `+timer` still works).
15538fn clear_timer(v: &Value) {
15539 let id =
15540 crate::stdlib::timers::handle_id(v).unwrap_or_else(|| with_host(|h| h.to_number(v)) as u64);
15541 with_host(|h| h.cancel_timer(id));
15542}