Skip to main content

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};
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::DECLARE, b_declare);
15    vm.register_builtin(ops::DELNAME, b_delname);
16    vm.register_builtin(ops::GETATTR, b_getattr);
17    vm.register_builtin(ops::SETATTR, b_setattr);
18    vm.register_builtin(ops::GETITEM, b_getitem);
19    vm.register_builtin(ops::SETITEM, b_setitem);
20    vm.register_builtin(ops::DELITEM, b_delitem);
21    vm.register_builtin(ops::MKSTR, b_mkstr);
22    vm.register_builtin(ops::MKARR, b_mkarr);
23    vm.register_builtin(ops::MKOBJ, b_mkobj);
24    vm.register_builtin(ops::CALL, b_call);
25    vm.register_builtin(ops::CALL_METHOD, b_call_method);
26    vm.register_builtin(ops::CALL_VALUE, b_call_value);
27    vm.register_builtin(ops::NEW, b_new);
28    vm.register_builtin(ops::TRUTHY, b_truthy);
29    vm.register_builtin(ops::TOSTR, b_tostr);
30    vm.register_builtin(ops::MKFUNC, b_mkfunc);
31    vm.register_builtin(ops::GETITER, b_getiter);
32    vm.register_builtin(ops::FORITER, b_foriter);
33    vm.register_builtin(ops::FORIN_KEYS, b_forin_keys);
34    vm.register_builtin(ops::CONTAINS, b_contains);
35    vm.register_builtin(ops::SIG_RETURN, b_sig_return);
36    vm.register_builtin(ops::BINOP, b_binop);
37    vm.register_builtin(ops::UNARY, b_unary);
38    vm.register_builtin(ops::STRICT_EQ, b_strict_eq);
39    vm.register_builtin(ops::LOOSE_EQ, b_loose_eq);
40    vm.register_builtin(ops::TYPEOF, b_typeof);
41    vm.register_builtin(ops::LOAD_NULL, b_load_null);
42    vm.register_builtin(ops::THROW, b_throw);
43    vm.register_builtin(ops::TRY, b_try);
44    vm.register_builtin(ops::NULLISH, b_nullish);
45    vm.register_builtin(ops::UNPACK, b_unpack);
46    vm.register_builtin(ops::BUILD_ARGS, b_build_args);
47    vm.register_builtin(ops::THIS, b_this);
48    vm.register_builtin(ops::INSTANCEOF, b_instanceof);
49    vm.register_builtin(ops::DELPROP_NAME, b_delprop_name);
50    vm.register_builtin(ops::APPLY, b_apply);
51    vm.register_builtin(ops::APPLY_METHOD, b_apply_method);
52    vm.register_builtin(ops::OBJ_REST, b_obj_rest);
53    vm.register_builtin(ops::DIV, b_div);
54}
55
56/// `a / b` with JS/IEEE-754 semantics. fusevm's native `Op::Div` returns `Undef`
57/// for a zero divisor (so a frontend whose `/` differs must lower to a builtin —
58/// its own documented guidance), but JavaScript requires `x/0 === ±Infinity` and
59/// `0/0 === NaN`, so `/` is lowered here instead. Non-number operands are coerced
60/// via `ToNumber`, exactly as the numeric hook's `arith(Div)` path does.
61fn b_div(vm: &mut VM, _: u8) -> Value {
62    let b = vm.pop();
63    let a = vm.pop();
64    let r = with_host(|h| h.arith(NumOp::Div, &a, &b));
65    finish(vm, r)
66}
67
68/// `{ ...rest } = obj`: a new object of `obj`'s own keys minus the excluded set.
69fn b_obj_rest(vm: &mut VM, _: u8) -> Value {
70    let excluded = vm.pop();
71    let obj = vm.pop();
72    let excl: Vec<String> = with_host(|h| h.iter_vec(&excluded))
73        .unwrap_or_default()
74        .iter()
75        .map(|v| with_host(|h| h.str_of(v)))
76        .collect();
77    with_host(|h| {
78        let props: IndexMap<String, Value> = match h.get(&obj) {
79            Some(JsObj::Object(m)) => m
80                .iter()
81                .filter(|(k, _)| !excl.contains(k))
82                .map(|(k, v)| (k.clone(), v.clone()))
83                .collect(),
84            _ => IndexMap::new(),
85        };
86        h.new_object(props)
87    })
88}
89
90// ── helpers ──────────────────────────────────────────────────────────────────
91
92fn pop_n(vm: &mut VM, n: usize) -> Vec<Value> {
93    let mut v = Vec::with_capacity(n);
94    for _ in 0..n {
95        v.push(vm.pop());
96    }
97    v.reverse();
98    v
99}
100
101/// Read a compiler-internal name string (native `Value::Str` or heap `str`).
102fn sval(v: &Value) -> String {
103    if let Value::Str(s) = v {
104        return (**s).clone();
105    }
106    with_host(|h| h.as_str(v)).unwrap_or_default()
107}
108
109fn abort(vm: &mut VM, e: String) -> Value {
110    with_host(|h| h.error = Some(e));
111    vm.ip = vm.chunk.ops.len();
112    Value::Undef
113}
114
115/// Halt the chunk if a call left an error or non-local signal pending.
116fn finish(vm: &mut VM, r: Result<Value, String>) -> Value {
117    match r {
118        Ok(v) => {
119            if with_host(|h| h.error.is_some() || h.signal.is_some()) {
120                vm.ip = vm.chunk.ops.len();
121            }
122            v
123        }
124        Err(e) => abort(vm, e),
125    }
126}
127
128// ── name handlers ─────────────────────────────────────────────────────────────
129
130fn b_getlocal(vm: &mut VM, _: u8) -> Value {
131    let name = sval(&vm.pop());
132    if let Some(v) = with_host(|h| h.read_name(&name)) {
133        return v;
134    }
135    // Globals bound lazily: numeric sentinels + builtin namespaces.
136    match name.as_str() {
137        "undefined" => return Value::Undef,
138        "NaN" => return Value::Float(f64::NAN),
139        "Infinity" => return Value::Float(f64::INFINITY),
140        "globalThis" => return with_host(|h| h.new_object(IndexMap::new())),
141        _ => {}
142    }
143    if is_namespace(&name) || is_known_builtin(&name) {
144        return with_host(|h| h.alloc(JsObj::Builtin(name.clone())));
145    }
146    abort(vm, host::ref_error(&name))
147}
148
149fn b_setlocal(vm: &mut VM, _: u8) -> Value {
150    let val = vm.pop();
151    let name = sval(&vm.pop());
152    with_host(|h| h.set_name(&name, val.clone()));
153    val
154}
155
156fn b_declare(vm: &mut VM, _: u8) -> Value {
157    let val = vm.pop();
158    let name = sval(&vm.pop());
159    with_host(|h| h.declare_name(&name, val.clone()));
160    val
161}
162
163fn b_delname(vm: &mut VM, _: u8) -> Value {
164    let name = sval(&vm.pop());
165    with_host(|h| h.del_name(&name));
166    Value::Bool(true)
167}
168
169fn b_this(_vm: &mut VM, _: u8) -> Value {
170    with_host(|h| h.current_this().unwrap_or(Value::Undef))
171}
172
173fn b_load_null(_vm: &mut VM, _: u8) -> Value {
174    with_host(|h| h.null())
175}
176
177// ── attribute / item handlers ─────────────────────────────────────────────────
178
179fn b_getattr(vm: &mut VM, _: u8) -> Value {
180    let name = sval(&vm.pop());
181    let recv = vm.pop();
182    match get_property(&recv, &name) {
183        Ok(v) => v,
184        Err(e) => abort(vm, e),
185    }
186}
187
188/// Read `recv.name` (also the computed-key path for string keys).
189fn get_property(recv: &Value, name: &str) -> Result<Value, String> {
190    if with_host(|h| h.is_nullish(recv)) {
191        return Err(host::type_error(&format!(
192            "Cannot read properties of {} (reading '{name}')",
193            with_host(|h| h.str_of(recv))
194        )));
195    }
196    let obj = with_host(|h| h.get(recv).cloned());
197    Ok(match obj {
198        Some(JsObj::Object(props)) => props.get(name).cloned().unwrap_or(Value::Undef),
199        Some(JsObj::Array(items)) => {
200            if name == "length" {
201                Value::Float(items.len() as f64)
202            } else if let Ok(i) = name.parse::<usize>() {
203                items.get(i).cloned().unwrap_or(Value::Undef)
204            } else if is_array_method(name) {
205                bound_method(recv, name)
206            } else {
207                Value::Undef
208            }
209        }
210        Some(JsObj::Str(s)) => {
211            if name == "length" {
212                Value::Float(s.chars().count() as f64)
213            } else if let Ok(i) = name.parse::<usize>() {
214                match s.chars().nth(i) {
215                    Some(c) => with_host(|h| h.new_str(c.to_string())),
216                    None => Value::Undef,
217                }
218            } else if is_string_method(name) {
219                bound_method(recv, name)
220            } else {
221                Value::Undef
222            }
223        }
224        Some(JsObj::Builtin(ns)) => namespace_property(&ns, name),
225        _ => {
226            // Primitive numbers/booleans: method access -> bound method.
227            if matches!(recv, Value::Float(_) | Value::Int(_)) && is_number_method(name) {
228                bound_method(recv, name)
229            } else {
230                Value::Undef
231            }
232        }
233    })
234}
235
236fn bound_method(recv: &Value, name: &str) -> Value {
237    with_host(|h| {
238        h.alloc(JsObj::BoundMethod {
239            recv: recv.clone(),
240            name: name.to_string(),
241        })
242    })
243}
244
245/// A property on a builtin namespace object (`Math.PI`, `Number.MAX_SAFE_INTEGER`,
246/// `console.log`).
247fn namespace_property(ns: &str, name: &str) -> Value {
248    // Numeric constants.
249    let konst = match (ns, name) {
250        ("Math", "PI") => Some(std::f64::consts::PI),
251        ("Math", "E") => Some(std::f64::consts::E),
252        ("Math", "LN2") => Some(std::f64::consts::LN_2),
253        ("Math", "LN10") => Some(std::f64::consts::LN_10),
254        ("Math", "LOG2E") => Some(std::f64::consts::LOG2_E),
255        ("Math", "LOG10E") => Some(std::f64::consts::LOG10_E),
256        ("Math", "SQRT2") => Some(std::f64::consts::SQRT_2),
257        ("Math", "SQRT1_2") => Some(std::f64::consts::FRAC_1_SQRT_2),
258        ("Number", "MAX_SAFE_INTEGER") => Some(9007199254740991.0),
259        ("Number", "MIN_SAFE_INTEGER") => Some(-9007199254740991.0),
260        ("Number", "MAX_VALUE") => Some(f64::MAX),
261        ("Number", "MIN_VALUE") => Some(f64::MIN_POSITIVE),
262        ("Number", "EPSILON") => Some(f64::EPSILON),
263        ("Number", "POSITIVE_INFINITY") => Some(f64::INFINITY),
264        ("Number", "NEGATIVE_INFINITY") => Some(f64::NEG_INFINITY),
265        ("Number", "NaN") => Some(f64::NAN),
266        _ => None,
267    };
268    if let Some(k) = konst {
269        return Value::Float(k);
270    }
271    let qualified = format!("{ns}.{name}");
272    if is_known_builtin(&qualified) {
273        return with_host(|h| h.alloc(JsObj::Builtin(qualified)));
274    }
275    Value::Undef
276}
277
278fn b_setattr(vm: &mut VM, _: u8) -> Value {
279    let val = vm.pop();
280    let name = sval(&vm.pop());
281    let recv = vm.pop();
282    set_property(&recv, &name, val.clone());
283    val
284}
285
286fn set_property(recv: &Value, name: &str, val: Value) {
287    with_host(|h| match h.get_mut(recv) {
288        Some(JsObj::Object(props)) => {
289            props.insert(name.to_string(), val);
290        }
291        Some(JsObj::Array(items)) => {
292            if name == "length" {
293                let n = h_val_to_len(&val);
294                items.resize(n, Value::Undef);
295            } else if let Ok(i) = name.parse::<usize>() {
296                if i >= items.len() {
297                    items.resize(i + 1, Value::Undef);
298                }
299                items[i] = val;
300            }
301        }
302        _ => {}
303    });
304}
305
306fn h_val_to_len(v: &Value) -> usize {
307    match v {
308        Value::Float(f) if f.is_finite() && *f >= 0.0 => *f as usize,
309        Value::Int(n) if *n >= 0 => *n as usize,
310        _ => 0,
311    }
312}
313
314fn b_getitem(vm: &mut VM, _: u8) -> Value {
315    let idx = vm.pop();
316    let recv = vm.pop();
317    let key = with_host(|h| h.str_of(&idx));
318    match get_property(&recv, &key) {
319        Ok(v) => v,
320        Err(e) => abort(vm, e),
321    }
322}
323
324fn b_setitem(vm: &mut VM, _: u8) -> Value {
325    let val = vm.pop();
326    let idx = vm.pop();
327    let recv = vm.pop();
328    let key = with_host(|h| h.str_of(&idx));
329    set_property(&recv, &key, val.clone());
330    val
331}
332
333fn b_delitem(vm: &mut VM, _: u8) -> Value {
334    let idx = vm.pop();
335    let recv = vm.pop();
336    let key = with_host(|h| h.str_of(&idx));
337    with_host(|h| match h.get_mut(&recv) {
338        Some(JsObj::Object(props)) => {
339            props.shift_remove(&key);
340        }
341        Some(JsObj::Array(items)) => {
342            if let Ok(i) = key.parse::<usize>() {
343                if i < items.len() {
344                    items[i] = Value::Undef;
345                }
346            }
347        }
348        _ => {}
349    });
350    Value::Bool(true)
351}
352
353fn b_delprop_name(vm: &mut VM, _: u8) -> Value {
354    let name = sval(&vm.pop());
355    let recv = vm.pop();
356    with_host(|h| {
357        if let Some(JsObj::Object(props)) = h.get_mut(&recv) {
358            props.shift_remove(&name);
359        }
360    });
361    Value::Bool(true)
362}
363
364// ── constructors ──────────────────────────────────────────────────────────────
365
366fn b_mkstr(vm: &mut VM, argc: u8) -> Value {
367    let parts = pop_n(vm, argc as usize);
368    let s: String = with_host(|h| parts.iter().map(|p| h.str_of(p)).collect());
369    with_host(|h| h.new_str(s))
370}
371
372fn b_mkarr(vm: &mut VM, argc: u8) -> Value {
373    let items = pop_n(vm, argc as usize);
374    with_host(|h| h.new_array(items))
375}
376
377fn b_mkobj(vm: &mut VM, argc: u8) -> Value {
378    let flat = pop_n(vm, argc as usize);
379    let mut props: IndexMap<String, Value> = IndexMap::new();
380    let mut i = 0;
381    while i + 2 < flat.len() || (i + 2 == flat.len() && flat.len() % 3 == 0 && i < flat.len()) {
382        if i + 2 >= flat.len() {
383            break;
384        }
385        let spread = matches!(flat[i], Value::Int(1));
386        if spread {
387            let src = flat[i + 1].clone();
388            let entries = with_host(|h| match h.get(&src) {
389                Some(JsObj::Object(m)) => m.iter().map(|(k, v)| (k.clone(), v.clone())).collect::<Vec<_>>(),
390                Some(JsObj::Array(items)) => items
391                    .iter()
392                    .enumerate()
393                    .map(|(idx, v)| (idx.to_string(), v.clone()))
394                    .collect::<Vec<_>>(),
395                _ => Vec::new(),
396            });
397            for (k, v) in entries {
398                props.insert(k, v);
399            }
400        } else {
401            let key = with_host(|h| h.str_of(&flat[i + 1]));
402            props.insert(key, flat[i + 2].clone());
403        }
404        i += 3;
405    }
406    with_host(|h| h.new_object(props))
407}
408
409fn b_mkfunc(vm: &mut VM, _: u8) -> Value {
410    let def_id = match vm.pop() {
411        Value::Int(n) => n as usize,
412        Value::Float(f) => f as usize,
413        _ => return abort(vm, "internal: MKFUNC id".into()),
414    };
415    let is_arrow = with_host(|h| h.funcs.get(def_id).map(|d| d.is_arrow).unwrap_or(false));
416    with_host(|h| {
417        let env = h.current_env_capture();
418        let this = h.current_this();
419        h.alloc(JsObj::Func(FuncVal {
420            def_id,
421            env: Some(env),
422            this,
423            is_arrow,
424        }))
425    })
426}
427
428// ── truthiness / coercion / equality ──────────────────────────────────────────
429
430fn b_truthy(vm: &mut VM, _: u8) -> Value {
431    let v = vm.pop();
432    Value::Bool(with_host(|h| h.truthy(&v)))
433}
434
435fn b_nullish(vm: &mut VM, _: u8) -> Value {
436    let v = vm.pop();
437    Value::Bool(with_host(|h| h.is_nullish(&v)))
438}
439
440fn b_tostr(vm: &mut VM, _: u8) -> Value {
441    let v = vm.pop();
442    with_host(|h| {
443        let s = h.str_of(&v);
444        h.new_str(s)
445    })
446}
447
448fn b_typeof(vm: &mut VM, _: u8) -> Value {
449    let v = vm.pop();
450    with_host(|h| {
451        let t = h.type_of(&v);
452        h.new_str(t)
453    })
454}
455
456fn b_strict_eq(vm: &mut VM, _: u8) -> Value {
457    let b = vm.pop();
458    let a = vm.pop();
459    Value::Bool(with_host(|h| h.strict_eq(&a, &b)))
460}
461
462fn b_loose_eq(vm: &mut VM, _: u8) -> Value {
463    let b = vm.pop();
464    let a = vm.pop();
465    Value::Bool(with_host(|h| h.loose_eq(&a, &b)))
466}
467
468fn b_instanceof(vm: &mut VM, _: u8) -> Value {
469    let _ctor = vm.pop();
470    let _obj = vm.pop();
471    // Prototype chains are not modeled; report false (conservative).
472    Value::Bool(false)
473}
474
475// ── bitwise / unary ───────────────────────────────────────────────────────────
476
477fn b_binop(vm: &mut VM, _: u8) -> Value {
478    let b = vm.pop();
479    let a = vm.pop();
480    let tag = match vm.pop() {
481        Value::Int(n) => n,
482        _ => 0,
483    };
484    with_host(|h| h.bitwise(tag, &a, &b))
485}
486
487fn b_unary(vm: &mut VM, _: u8) -> Value {
488    let v = vm.pop();
489    let tag = match vm.pop() {
490        Value::Int(n) => n,
491        _ => 0,
492    };
493    with_host(|h| match tag {
494        host::unop::POS => Value::Float(h.to_number(&v)),
495        host::unop::BITNOT => {
496            let n = h.to_number(&v);
497            let i = if n.is_finite() { n.trunc() as i64 as i32 } else { 0 };
498            Value::Float(!i as f64)
499        }
500        _ => Value::Undef,
501    })
502}
503
504// ── membership ────────────────────────────────────────────────────────────────
505
506fn b_contains(vm: &mut VM, _: u8) -> Value {
507    let container = vm.pop();
508    let key = vm.pop();
509    let k = with_host(|h| h.str_of(&key));
510    Value::Bool(with_host(|h| match h.get(&container) {
511        Some(JsObj::Object(props)) => props.contains_key(&k),
512        Some(JsObj::Array(items)) => k.parse::<usize>().map(|i| i < items.len()).unwrap_or(false),
513        _ => false,
514    }))
515}
516
517// ── control ───────────────────────────────────────────────────────────────────
518
519fn b_sig_return(vm: &mut VM, _: u8) -> Value {
520    let v = vm.pop();
521    with_host(|h| h.signal = Some(host::Signal::Return(v.clone())));
522    vm.ip = vm.chunk.ops.len();
523    v
524}
525
526fn b_throw(vm: &mut VM, _: u8) -> Value {
527    let v = vm.pop();
528    let msg = with_host(|h| {
529        h.exc = Some(v.clone());
530        // Prefer an error object's message for the top-level report.
531        error_display(h, &v)
532    });
533    abort(vm, msg)
534}
535
536fn error_display(h: &host::JsHost, v: &Value) -> String {
537    if let Some(JsObj::Object(props)) = h.get(v) {
538        let name = props.get("name").map(|x| h.str_of(x)).unwrap_or_else(|| "Error".into());
539        if let Some(m) = props.get("message") {
540            return format!("Uncaught {name}: {}", h.str_of(m));
541        }
542    }
543    format!("Uncaught {}", h.str_of(v))
544}
545
546fn b_try(vm: &mut VM, _: u8) -> Value {
547    let id = match vm.pop() {
548        Value::Int(n) => n as usize,
549        _ => return abort(vm, "internal: TRY id".into()),
550    };
551    let td = match with_host(|h| h.try_def(id)) {
552        Some(t) => t,
553        None => return abort(vm, "internal: unknown try id".into()),
554    };
555    let mut pending: Option<String> = None;
556
557    let body_res = host::run_chunk_on(td.block.clone());
558    let signal_after = with_host(|h| h.signal.is_some());
559    if let Err(e) = body_res {
560        if signal_after {
561            pending = Some(e);
562        } else if let Some((bind, hbody)) = &td.handler {
563            // Bind the thrown value (or a synthesized error) to the catch param.
564            let thrown = with_host(|h| h.exc.clone()).unwrap_or_else(|| {
565                with_host(|h| synth_error(h, &e))
566            });
567            with_host(|h| {
568                h.error = None;
569                h.exc = None;
570            });
571            if let Some(name) = bind {
572                with_host(|h| h.declare_name(name, thrown));
573            }
574            if let Err(e2) = host::run_chunk_on(hbody.clone()) {
575                pending = Some(e2);
576            }
577        } else {
578            pending = Some(e);
579        }
580    }
581
582    // finally always runs; a finally error/signal supersedes.
583    if let Some(fin) = &td.finalizer {
584        let sig_before = with_host(|h| h.signal.take());
585        match host::run_chunk_on(fin.clone()) {
586            Ok(_) => {
587                if with_host(|h| h.signal.is_none()) {
588                    with_host(|h| h.signal = sig_before);
589                }
590            }
591            Err(e) => pending = Some(e),
592        }
593    }
594
595    if let Some(e) = pending {
596        return abort(vm, e);
597    }
598    Value::Undef
599}
600
601/// Synthesize an `Error`-shaped object from an internal error string.
602fn synth_error(h: &mut host::JsHost, e: &str) -> Value {
603    let (name, message) = match e.split_once(": ") {
604        Some((n, m)) => (n.to_string(), m.to_string()),
605        None => ("Error".to_string(), e.to_string()),
606    };
607    let mut props: IndexMap<String, Value> = IndexMap::new();
608    let nv = h.new_str(name);
609    let mv = h.new_str(message);
610    props.insert("name".into(), nv);
611    props.insert("message".into(), mv);
612    h.new_object(props)
613}
614
615// ── iteration ─────────────────────────────────────────────────────────────────
616
617fn b_getiter(vm: &mut VM, _: u8) -> Value {
618    let v = vm.pop();
619    match with_host(|h| h.iter_vec(&v)) {
620        Ok(items) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
621        Err(e) => abort(vm, e),
622    }
623}
624
625fn b_forin_keys(vm: &mut VM, _: u8) -> Value {
626    let v = vm.pop();
627    let keys = with_host(|h| h.enum_keys(&v));
628    with_host(|h| h.new_array(keys))
629}
630
631fn b_foriter(vm: &mut VM, _: u8) -> Value {
632    let it = match vm.stack.last() {
633        Some(v) => v.clone(),
634        None => return abort(vm, "internal: FORITER with empty stack".into()),
635    };
636    let next = with_host(|h| {
637        if let Some(JsObj::Iter { items, idx }) = h.get_mut(&it) {
638            if *idx < items.len() {
639                let v = items[*idx].clone();
640                *idx += 1;
641                return Some(v);
642            }
643        }
644        None
645    });
646    match next {
647        Some(v) => {
648            vm.push(v);
649            Value::Bool(true)
650        }
651        None => Value::Bool(false),
652    }
653}
654
655fn b_unpack(vm: &mut VM, _: u8) -> Value {
656    let star = match vm.pop() {
657        Value::Int(n) => n,
658        _ => -1,
659    };
660    let count = match vm.pop() {
661        Value::Int(n) => n as usize,
662        _ => 0,
663    };
664    let iterable = vm.pop();
665    let items = match with_host(|h| h.iter_vec(&iterable)) {
666        Ok(v) => v,
667        Err(e) => return abort(vm, e),
668    };
669    let ordered: Vec<Value> = if star < 0 {
670        (0..count).map(|i| items.get(i).cloned().unwrap_or(Value::Undef)).collect()
671    } else {
672        let si = star as usize;
673        let after = count.saturating_sub(si + 1);
674        let rest_end = items.len().saturating_sub(after).max(si);
675        let mut out: Vec<Value> = Vec::with_capacity(count);
676        for i in 0..si {
677            out.push(items.get(i).cloned().unwrap_or(Value::Undef));
678        }
679        let rest: Vec<Value> = items.get(si..rest_end).map(|s| s.to_vec()).unwrap_or_default();
680        out.push(with_host(|h| h.new_array(rest)));
681        for j in 0..after {
682            out.push(items.get(rest_end + j).cloned().unwrap_or(Value::Undef));
683        }
684        out
685    };
686    if ordered.is_empty() {
687        return Value::Undef;
688    }
689    for it in ordered[1..].iter().rev().cloned() {
690        vm.push(it);
691    }
692    ordered[0].clone()
693}
694
695fn b_build_args(vm: &mut VM, argc: u8) -> Value {
696    let flat = pop_n(vm, argc as usize);
697    let mut out = Vec::new();
698    let mut i = 0;
699    while i + 1 < flat.len() {
700        let spread = matches!(flat[i], Value::Int(1));
701        let val = flat[i + 1].clone();
702        if spread {
703            match with_host(|h| h.iter_vec(&val)) {
704                Ok(items) => out.extend(items),
705                Err(e) => return abort(vm, e),
706            }
707        } else {
708            out.push(val);
709        }
710        i += 2;
711    }
712    with_host(|h| h.new_array(out))
713}
714
715// ── calls ──────────────────────────────────────────────────────────────────────
716
717fn b_call(vm: &mut VM, argc: u8) -> Value {
718    let mut args = pop_n(vm, argc as usize);
719    let name = sval(&args.remove(0));
720    let r = host::call_named(&name, args);
721    finish(vm, r)
722}
723
724fn b_call_method(vm: &mut VM, argc: u8) -> Value {
725    let mut args = pop_n(vm, argc as usize);
726    let recv = args.remove(0);
727    let name = sval(&args.remove(0));
728    let r = host::call_method(&recv, &name, args);
729    finish(vm, r)
730}
731
732fn b_call_value(vm: &mut VM, argc: u8) -> Value {
733    let mut args = pop_n(vm, argc as usize);
734    let callable = args.remove(0);
735    let r = host::invoke(&callable, args, None);
736    finish(vm, r)
737}
738
739fn b_new(vm: &mut VM, argc: u8) -> Value {
740    let mut args = pop_n(vm, argc as usize);
741    let ctor = args.remove(0);
742    let r = host::construct(&ctor, args);
743    finish(vm, r)
744}
745
746fn b_apply(vm: &mut VM, _: u8) -> Value {
747    let args_arr = vm.pop();
748    let callable = vm.pop();
749    let args = with_host(|h| h.iter_vec(&args_arr)).unwrap_or_default();
750    let r = host::invoke(&callable, args, None);
751    finish(vm, r)
752}
753
754fn b_apply_method(vm: &mut VM, _: u8) -> Value {
755    let args_arr = vm.pop();
756    let name = sval(&vm.pop());
757    let recv = vm.pop();
758    let args = with_host(|h| h.iter_vec(&args_arr)).unwrap_or_default();
759    let r = host::call_method(&recv, &name, args);
760    finish(vm, r)
761}
762
763// ── numeric hook ──────────────────────────────────────────────────────────────
764
765/// Host callback for arithmetic fusevm cannot complete natively (a non-`Int`/
766/// non-`Float` operand). Supplies JavaScript `+` concatenation and coercion.
767pub fn numeric_hook(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
768    with_host(|h| h.arith(op, a, b))
769}
770
771// ══ standard library ═══════════════════════════════════════════════════════════
772
773/// Namespaces reachable as bare globals.
774fn is_namespace(name: &str) -> bool {
775    matches!(
776        name,
777        "console" | "Math" | "JSON" | "Object" | "Array" | "Number" | "String" | "Boolean"
778    )
779}
780
781const GLOBAL_FUNCS: &[&str] = &[
782    "parseInt",
783    "parseFloat",
784    "isNaN",
785    "isFinite",
786    "String",
787    "Number",
788    "Boolean",
789    "Array",
790    "Error",
791    "TypeError",
792    "RangeError",
793];
794
795const NS_METHODS: &[&str] = &[
796    "console.log",
797    "console.error",
798    "console.warn",
799    "console.info",
800    "console.debug",
801    "Math.floor",
802    "Math.ceil",
803    "Math.round",
804    "Math.trunc",
805    "Math.abs",
806    "Math.sign",
807    "Math.max",
808    "Math.min",
809    "Math.pow",
810    "Math.sqrt",
811    "Math.cbrt",
812    "Math.random",
813    "Math.hypot",
814    "Math.log",
815    "Math.log2",
816    "Math.log10",
817    "Math.exp",
818    "Math.sin",
819    "Math.cos",
820    "Math.tan",
821    "Math.atan",
822    "Math.atan2",
823    "Math.asin",
824    "Math.acos",
825    "JSON.stringify",
826    "JSON.parse",
827    "Object.keys",
828    "Object.values",
829    "Object.entries",
830    "Object.assign",
831    "Object.freeze",
832    "Object.fromEntries",
833    "Array.isArray",
834    "Array.from",
835    "Array.of",
836    "Number.isInteger",
837    "Number.isNaN",
838    "Number.isFinite",
839    "Number.isSafeInteger",
840    "Number.parseInt",
841    "Number.parseFloat",
842    "String.fromCharCode",
843];
844
845pub fn is_known_builtin(name: &str) -> bool {
846    GLOBAL_FUNCS.contains(&name) || NS_METHODS.contains(&name) || is_namespace(name)
847}
848
849/// Call a resolved builtin function (global or `namespace.method`).
850pub fn call_builtin_function(name: &str, args: Vec<Value>) -> Result<Value, String> {
851    match name {
852        "console.log" | "console.info" | "console.debug" => {
853            print_line(&args, false);
854            Ok(Value::Undef)
855        }
856        "console.error" | "console.warn" => {
857            print_line(&args, true);
858            Ok(Value::Undef)
859        }
860        "parseInt" | "Number.parseInt" => Ok(Value::Float(parse_int(&args))),
861        "parseFloat" | "Number.parseFloat" => Ok(Value::Float(parse_float(&args))),
862        "isNaN" => Ok(Value::Bool(arg_num(&args, 0).is_nan())),
863        "isFinite" => Ok(Value::Bool(arg_num(&args, 0).is_finite())),
864        "Number.isInteger" => Ok(Value::Bool(is_integer(arg0(&args)))),
865        "Number.isSafeInteger" => Ok(Value::Bool(is_safe_integer(arg0(&args)))),
866        "Number.isNaN" => Ok(Value::Bool(matches!(arg0(&args), Value::Float(f) if f.is_nan()))),
867        "Number.isFinite" => Ok(Value::Bool(matches!(arg0(&args), Value::Float(f) if f.is_finite()) || matches!(arg0(&args), Value::Int(_)))),
868        "String" => Ok(with_host(|h| {
869            let s = if args.is_empty() { String::new() } else { h.str_of(&args[0]) };
870            h.new_str(s)
871        })),
872        "Number" => Ok(Value::Float(if args.is_empty() { 0.0 } else { with_host(|h| h.to_number(&args[0])) })),
873        "Boolean" => Ok(Value::Bool(with_host(|h| h.truthy(&arg0(&args))))),
874        "String.fromCharCode" => Ok(with_host(|h| {
875            let s: String = args
876                .iter()
877                .filter_map(|a| char::from_u32(h.to_number(a) as u32))
878                .collect();
879            h.new_str(s)
880        })),
881        "Array" | "Array.of" => Ok(with_host(|h| h.new_array(args))),
882        "Array.isArray" => Ok(Value::Bool(matches!(with_host(|h| h.get(&arg0(&args)).cloned()), Some(JsObj::Array(_))))),
883        "Array.from" => array_from(args),
884        "Object.keys" => object_keys(args, 0),
885        "Object.values" => object_keys(args, 1),
886        "Object.entries" => object_keys(args, 2),
887        "Object.assign" => object_assign(args),
888        "Object.freeze" => Ok(arg0(&args)),
889        "Object.fromEntries" => object_from_entries(args),
890        "JSON.stringify" => json_stringify(args),
891        "JSON.parse" => json_parse(args),
892        "Error" | "TypeError" | "RangeError" => Ok(make_error(name, &args)),
893        _ if name.starts_with("Math.") => math_fn(&name[5..], &args),
894        _ => Err(host::type_error(&format!("{name} is not a function"))),
895    }
896}
897
898/// Construct via `new` for the builtin constructors.
899pub fn construct_builtin(name: &str, args: Vec<Value>) -> Result<Value, String> {
900    match name {
901        "Array" => {
902            // new Array(n) -> length-n array; new Array(a, b) -> [a, b].
903            if args.len() == 1 {
904                if let Value::Float(f) = args[0] {
905                    if f.fract() == 0.0 && f >= 0.0 {
906                        return Ok(with_host(|h| h.new_array(vec![Value::Undef; f as usize])));
907                    }
908                }
909            }
910            Ok(with_host(|h| h.new_array(args)))
911        }
912        "Object" => Ok(with_host(|h| h.new_object(IndexMap::new()))),
913        "Error" | "TypeError" | "RangeError" => Ok(make_error(name, &args)),
914        _ => Err(host::type_error(&format!("{name} is not a constructor"))),
915    }
916}
917
918fn make_error(name: &str, args: &[Value]) -> Value {
919    with_host(|h| {
920        let mut props: IndexMap<String, Value> = IndexMap::new();
921        let nv = h.new_str(name);
922        props.insert("name".into(), nv);
923        let msg = args.first().map(|a| h.str_of(a)).unwrap_or_default();
924        let mv = h.new_str(msg);
925        props.insert("message".into(), mv);
926        h.new_object(props)
927    })
928}
929
930fn print_line(args: &[Value], stderr: bool) {
931    let line: String = with_host(|h| {
932        args.iter().map(|a| h.console_format(a)).collect::<Vec<_>>().join(" ")
933    });
934    if stderr {
935        eprintln!("{line}");
936    } else {
937        println!("{line}");
938    }
939}
940
941fn arg0(args: &[Value]) -> Value {
942    args.first().cloned().unwrap_or(Value::Undef)
943}
944fn arg_num(args: &[Value], i: usize) -> f64 {
945    with_host(|h| h.to_number(&args.get(i).cloned().unwrap_or(Value::Undef)))
946}
947
948fn is_integer(v: Value) -> bool {
949    match v {
950        Value::Int(_) => true,
951        Value::Float(f) => f.is_finite() && f.fract() == 0.0,
952        _ => false,
953    }
954}
955fn is_safe_integer(v: Value) -> bool {
956    match v {
957        Value::Float(f) => f.is_finite() && f.fract() == 0.0 && f.abs() <= 9007199254740991.0,
958        Value::Int(_) => true,
959        _ => false,
960    }
961}
962
963fn parse_int(args: &[Value]) -> f64 {
964    let s = with_host(|h| h.str_of(&arg0(args)));
965    let radix = args.get(1).map(|r| with_host(|h| h.to_number(r)) as u32).filter(|r| (2..=36).contains(r));
966    let t = s.trim();
967    let (neg, digits) = match t.strip_prefix('-') {
968        Some(rest) => (true, rest),
969        None => (false, t.strip_prefix('+').unwrap_or(t)),
970    };
971    let (radix, digits) = match radix {
972        Some(16) => (16u32, digits.strip_prefix("0x").or_else(|| digits.strip_prefix("0X")).unwrap_or(digits)),
973        Some(r) => (r, digits),
974        None => {
975            if let Some(hex) = digits.strip_prefix("0x").or_else(|| digits.strip_prefix("0X")) {
976                (16, hex)
977            } else {
978                (10, digits)
979            }
980        }
981    };
982    let valid: String = digits.chars().take_while(|c| c.is_digit(radix)).collect();
983    if valid.is_empty() {
984        return f64::NAN;
985    }
986    let n = i64::from_str_radix(&valid, radix).map(|n| n as f64).unwrap_or(f64::NAN);
987    if neg {
988        -n
989    } else {
990        n
991    }
992}
993
994fn parse_float(args: &[Value]) -> f64 {
995    let s = with_host(|h| h.str_of(&arg0(args)));
996    let t = s.trim_start();
997    // `Infinity` / `+Infinity` / `-Infinity` are valid parseFloat prefixes.
998    let inf_body = t.strip_prefix('+').or_else(|| t.strip_prefix('-')).unwrap_or(t);
999    if inf_body.starts_with("Infinity") {
1000        return if t.starts_with('-') {
1001            f64::NEG_INFINITY
1002        } else {
1003            f64::INFINITY
1004        };
1005    }
1006    // Longest numeric prefix.
1007    let mut end = 0;
1008    let bytes = t.as_bytes();
1009    let mut seen_dot = false;
1010    let mut seen_e = false;
1011    for (i, &c) in bytes.iter().enumerate() {
1012        match c {
1013            b'0'..=b'9' => end = i + 1,
1014            b'+' | b'-' if i == 0 || bytes[i - 1] == b'e' || bytes[i - 1] == b'E' => end = i + 1,
1015            b'.' if !seen_dot && !seen_e => {
1016                seen_dot = true;
1017                end = i + 1;
1018            }
1019            b'e' | b'E' if !seen_e && i > 0 => {
1020                seen_e = true;
1021                end = i + 1;
1022            }
1023            _ => break,
1024        }
1025    }
1026    t[..end].parse::<f64>().unwrap_or(f64::NAN)
1027}
1028
1029fn math_fn(fname: &str, args: &[Value]) -> Result<Value, String> {
1030    let x = arg_num(args, 0);
1031    let r = match fname {
1032        "floor" => x.floor(),
1033        "ceil" => x.ceil(),
1034        "round" => {
1035            // JS rounds half up toward +Infinity, but preserves the sign of a
1036            // zero result: Math.round(-0.5) === -0, Math.round(-0.4) === -0.
1037            let r = (x + 0.5).floor();
1038            if r == 0.0 && x.is_sign_negative() {
1039                -0.0
1040            } else {
1041                r
1042            }
1043        }
1044        "trunc" => x.trunc(),
1045        "abs" => x.abs(),
1046        "sign" => {
1047            if x.is_nan() {
1048                f64::NAN
1049            } else if x > 0.0 {
1050                1.0
1051            } else if x < 0.0 {
1052                -1.0
1053            } else {
1054                x
1055            }
1056        }
1057        "sqrt" => x.sqrt(),
1058        "cbrt" => x.cbrt(),
1059        "exp" => x.exp(),
1060        "log" => x.ln(),
1061        "log2" => x.log2(),
1062        "log10" => x.log10(),
1063        "sin" => x.sin(),
1064        "cos" => x.cos(),
1065        "tan" => x.tan(),
1066        "asin" => x.asin(),
1067        "acos" => x.acos(),
1068        "atan" => x.atan(),
1069        "atan2" => x.atan2(arg_num(args, 1)),
1070        "pow" => x.powf(arg_num(args, 1)),
1071        "hypot" => {
1072            // Scale by the largest magnitude before squaring — this avoids the
1073            // last-ULP error of the naive `sqrt(Σ xᵢ²)` and matches V8's result.
1074            let xs: Vec<f64> = args.iter().map(|a| with_host(|h| h.to_number(a))).collect();
1075            let mut max = 0.0f64;
1076            for x in &xs {
1077                if x.abs() > max {
1078                    max = x.abs();
1079                }
1080            }
1081            if xs.iter().any(|x| x.is_infinite()) {
1082                f64::INFINITY
1083            } else if max == 0.0 || !max.is_finite() {
1084                max
1085            } else {
1086                let s: f64 = xs.iter().map(|x| (x / max) * (x / max)).sum();
1087                max * s.sqrt()
1088            }
1089        }
1090        "random" => pseudo_random(),
1091        "max" => {
1092            if args.is_empty() {
1093                f64::NEG_INFINITY
1094            } else {
1095                let mut m = f64::NEG_INFINITY;
1096                for a in args {
1097                    let n = with_host(|h| h.to_number(a));
1098                    if n.is_nan() {
1099                        return Ok(Value::Float(f64::NAN));
1100                    }
1101                    if n > m {
1102                        m = n;
1103                    }
1104                }
1105                m
1106            }
1107        }
1108        "min" => {
1109            if args.is_empty() {
1110                f64::INFINITY
1111            } else {
1112                let mut m = f64::INFINITY;
1113                for a in args {
1114                    let n = with_host(|h| h.to_number(a));
1115                    if n.is_nan() {
1116                        return Ok(Value::Float(f64::NAN));
1117                    }
1118                    if n < m {
1119                        m = n;
1120                    }
1121                }
1122                m
1123            }
1124        }
1125        _ => return Err(host::type_error(&format!("Math.{fname} is not a function"))),
1126    };
1127    Ok(Value::Float(r))
1128}
1129
1130/// A small deterministic PRNG for `Math.random` (output is non-reproducible vs
1131/// Node by nature; kept simple).
1132fn pseudo_random() -> f64 {
1133    use std::cell::Cell;
1134    thread_local!(static SEED: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
1135    SEED.with(|s| {
1136        let mut x = s.get();
1137        x ^= x << 13;
1138        x ^= x >> 7;
1139        x ^= x << 17;
1140        s.set(x);
1141        (x >> 11) as f64 / (1u64 << 53) as f64
1142    })
1143}
1144
1145// ── Object.* ──────────────────────────────────────────────────────────────────
1146
1147fn object_keys(args: Vec<Value>, mode: u8) -> Result<Value, String> {
1148    let v = arg0(&args);
1149    let entries: Vec<(String, Value)> = with_host(|h| match h.get(&v) {
1150        Some(JsObj::Object(props)) => props.iter().map(|(k, val)| (k.clone(), val.clone())).collect(),
1151        Some(JsObj::Array(items)) => items.iter().enumerate().map(|(i, val)| (i.to_string(), val.clone())).collect(),
1152        _ => Vec::new(),
1153    });
1154    Ok(with_host(|h| {
1155        let out: Vec<Value> = entries
1156            .into_iter()
1157            .map(|(k, val)| match mode {
1158                0 => h.new_str(k),
1159                1 => val,
1160                _ => {
1161                    let ks = h.new_str(k);
1162                    h.new_array(vec![ks, val])
1163                }
1164            })
1165            .collect();
1166        h.new_array(out)
1167    }))
1168}
1169
1170fn object_assign(args: Vec<Value>) -> Result<Value, String> {
1171    let target = arg0(&args);
1172    for src in args.iter().skip(1) {
1173        let entries: Vec<(String, Value)> = with_host(|h| match h.get(src) {
1174            Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
1175            _ => Vec::new(),
1176        });
1177        with_host(|h| {
1178            if let Some(JsObj::Object(p)) = h.get_mut(&target) {
1179                for (k, v) in entries {
1180                    p.insert(k, v);
1181                }
1182            }
1183        });
1184    }
1185    Ok(target)
1186}
1187
1188fn object_from_entries(args: Vec<Value>) -> Result<Value, String> {
1189    let pairs = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
1190    let mut props: IndexMap<String, Value> = IndexMap::new();
1191    for p in pairs {
1192        let kv = with_host(|h| h.iter_vec(&p)).unwrap_or_default();
1193        let key = with_host(|h| h.str_of(&kv.first().cloned().unwrap_or(Value::Undef)));
1194        let val = kv.get(1).cloned().unwrap_or(Value::Undef);
1195        props.insert(key, val);
1196    }
1197    Ok(with_host(|h| h.new_object(props)))
1198}
1199
1200fn array_from(args: Vec<Value>) -> Result<Value, String> {
1201    let items = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
1202    if let Some(cb) = args.get(1).cloned() {
1203        let mut out = Vec::with_capacity(items.len());
1204        for (i, it) in items.into_iter().enumerate() {
1205            out.push(host::invoke(&cb, vec![it, Value::Float(i as f64)], None)?);
1206        }
1207        return Ok(with_host(|h| h.new_array(out)));
1208    }
1209    Ok(with_host(|h| h.new_array(items)))
1210}
1211
1212// ── JSON ──────────────────────────────────────────────────────────────────────
1213
1214fn json_stringify(args: Vec<Value>) -> Result<Value, String> {
1215    let v = arg0(&args);
1216    let indent = match args.get(2) {
1217        Some(Value::Float(f)) => " ".repeat((*f as usize).min(10)),
1218        Some(other) => with_host(|h| h.as_str(other)).unwrap_or_default(),
1219        None => String::new(),
1220    };
1221    let s = with_host(|h| json_str(h, &v, &indent, 0));
1222    match s {
1223        Some(s) => Ok(with_host(|h| h.new_str(s))),
1224        None => Ok(Value::Undef),
1225    }
1226}
1227
1228fn json_str(h: &host::JsHost, v: &Value, indent: &str, depth: usize) -> Option<String> {
1229    match v {
1230        Value::Undef => None,
1231        Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
1232        Value::Int(n) => Some(n.to_string()),
1233        Value::Float(f) => Some(if f.is_finite() { host::fmt_number(*f) } else { "null".into() }),
1234        Value::Str(s) => Some(json_quote(s)),
1235        Value::Obj(_) => match h.get(v) {
1236            Some(JsObj::Str(s)) => Some(json_quote(s)),
1237            Some(JsObj::Null) => Some("null".into()),
1238            Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundMethod { .. }) => None,
1239            Some(JsObj::Array(items)) => {
1240                if items.is_empty() {
1241                    return Some("[]".into());
1242                }
1243                let parts: Vec<String> = items
1244                    .iter()
1245                    .map(|x| json_str(h, x, indent, depth + 1).unwrap_or_else(|| "null".into()))
1246                    .collect();
1247                Some(wrap(&parts, "[", "]", indent, depth))
1248            }
1249            Some(JsObj::Object(props)) => {
1250                let parts: Vec<String> = props
1251                    .iter()
1252                    .filter_map(|(k, val)| {
1253                        json_str(h, val, indent, depth + 1).map(|vs| {
1254                            let sep = if indent.is_empty() { ":" } else { ": " };
1255                            format!("{}{sep}{vs}", json_quote(k))
1256                        })
1257                    })
1258                    .collect();
1259                if parts.is_empty() {
1260                    return Some("{}".into());
1261                }
1262                Some(wrap(&parts, "{", "}", indent, depth))
1263            }
1264            _ => Some("null".into()),
1265        },
1266        _ => Some("null".into()),
1267    }
1268}
1269
1270fn wrap(parts: &[String], open: &str, close: &str, indent: &str, depth: usize) -> String {
1271    if indent.is_empty() {
1272        format!("{open}{}{close}", parts.join(","))
1273    } else {
1274        let pad = indent.repeat(depth + 1);
1275        let pad_close = indent.repeat(depth);
1276        format!("{open}\n{pad}{}\n{pad_close}{close}", parts.join(&format!(",\n{pad}")))
1277    }
1278}
1279
1280fn json_quote(s: &str) -> String {
1281    let mut out = String::from("\"");
1282    for c in s.chars() {
1283        match c {
1284            '"' => out.push_str("\\\""),
1285            '\\' => out.push_str("\\\\"),
1286            '\n' => out.push_str("\\n"),
1287            '\t' => out.push_str("\\t"),
1288            '\r' => out.push_str("\\r"),
1289            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
1290            _ => out.push(c),
1291        }
1292    }
1293    out.push('"');
1294    out
1295}
1296
1297fn json_parse(args: Vec<Value>) -> Result<Value, String> {
1298    let s = with_host(|h| h.str_of(&arg0(&args)));
1299    let mut p = JsonParser { chars: s.chars().collect(), pos: 0 };
1300    p.skip_ws();
1301    let v = p.parse_value()?;
1302    p.skip_ws();
1303    Ok(v)
1304}
1305
1306struct JsonParser {
1307    chars: Vec<char>,
1308    pos: usize,
1309}
1310impl JsonParser {
1311    fn peek(&self) -> Option<char> {
1312        self.chars.get(self.pos).copied()
1313    }
1314    fn skip_ws(&mut self) {
1315        while matches!(self.peek(), Some(' ') | Some('\n') | Some('\t') | Some('\r')) {
1316            self.pos += 1;
1317        }
1318    }
1319    fn parse_value(&mut self) -> Result<Value, String> {
1320        self.skip_ws();
1321        match self.peek() {
1322            Some('{') => self.parse_object(),
1323            Some('[') => self.parse_array(),
1324            Some('"') => {
1325                let s = self.parse_string()?;
1326                Ok(with_host(|h| h.new_str(s)))
1327            }
1328            Some('t') | Some('f') => self.parse_bool(),
1329            Some('n') => {
1330                self.expect_lit("null")?;
1331                Ok(with_host(|h| h.null()))
1332            }
1333            Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
1334            _ => Err("SyntaxError: Unexpected token in JSON".into()),
1335        }
1336    }
1337    fn expect_lit(&mut self, lit: &str) -> Result<(), String> {
1338        for ch in lit.chars() {
1339            if self.peek() != Some(ch) {
1340                return Err("SyntaxError: Unexpected token in JSON".into());
1341            }
1342            self.pos += 1;
1343        }
1344        Ok(())
1345    }
1346    fn parse_bool(&mut self) -> Result<Value, String> {
1347        if self.peek() == Some('t') {
1348            self.expect_lit("true")?;
1349            Ok(Value::Bool(true))
1350        } else {
1351            self.expect_lit("false")?;
1352            Ok(Value::Bool(false))
1353        }
1354    }
1355    fn parse_number(&mut self) -> Result<Value, String> {
1356        let start = self.pos;
1357        while matches!(self.peek(), Some(c) if c.is_ascii_digit() || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E') {
1358            self.pos += 1;
1359        }
1360        let s: String = self.chars[start..self.pos].iter().collect();
1361        s.parse::<f64>().map(Value::Float).map_err(|_| "SyntaxError: bad number in JSON".into())
1362    }
1363    fn parse_string(&mut self) -> Result<String, String> {
1364        self.pos += 1; // opening quote
1365        let mut out = String::new();
1366        loop {
1367            match self.peek() {
1368                None => return Err("SyntaxError: unterminated string in JSON".into()),
1369                Some('"') => {
1370                    self.pos += 1;
1371                    break;
1372                }
1373                Some('\\') => {
1374                    self.pos += 1;
1375                    match self.peek() {
1376                        Some('n') => out.push('\n'),
1377                        Some('t') => out.push('\t'),
1378                        Some('r') => out.push('\r'),
1379                        Some('"') => out.push('"'),
1380                        Some('\\') => out.push('\\'),
1381                        Some('/') => out.push('/'),
1382                        Some('b') => out.push('\u{08}'),
1383                        Some('f') => out.push('\u{0C}'),
1384                        Some('u') => {
1385                            let h: String = self.chars[self.pos + 1..(self.pos + 5).min(self.chars.len())].iter().collect();
1386                            if let Ok(n) = u32::from_str_radix(&h, 16) {
1387                                if let Some(ch) = char::from_u32(n) {
1388                                    out.push(ch);
1389                                }
1390                            }
1391                            self.pos += 4;
1392                        }
1393                        _ => {}
1394                    }
1395                    self.pos += 1;
1396                }
1397                Some(c) => {
1398                    out.push(c);
1399                    self.pos += 1;
1400                }
1401            }
1402        }
1403        Ok(out)
1404    }
1405    fn parse_array(&mut self) -> Result<Value, String> {
1406        self.pos += 1; // [
1407        let mut items = Vec::new();
1408        self.skip_ws();
1409        if self.peek() == Some(']') {
1410            self.pos += 1;
1411            return Ok(with_host(|h| h.new_array(items)));
1412        }
1413        loop {
1414            items.push(self.parse_value()?);
1415            self.skip_ws();
1416            match self.peek() {
1417                Some(',') => {
1418                    self.pos += 1;
1419                }
1420                Some(']') => {
1421                    self.pos += 1;
1422                    break;
1423                }
1424                _ => return Err("SyntaxError: bad array in JSON".into()),
1425            }
1426        }
1427        Ok(with_host(|h| h.new_array(items)))
1428    }
1429    fn parse_object(&mut self) -> Result<Value, String> {
1430        self.pos += 1; // {
1431        let mut props: IndexMap<String, Value> = IndexMap::new();
1432        self.skip_ws();
1433        if self.peek() == Some('}') {
1434            self.pos += 1;
1435            return Ok(with_host(|h| h.new_object(props)));
1436        }
1437        loop {
1438            self.skip_ws();
1439            let key = self.parse_string()?;
1440            self.skip_ws();
1441            if self.peek() != Some(':') {
1442                return Err("SyntaxError: expected ':' in JSON".into());
1443            }
1444            self.pos += 1;
1445            let val = self.parse_value()?;
1446            props.insert(key, val);
1447            self.skip_ws();
1448            match self.peek() {
1449                Some(',') => {
1450                    self.pos += 1;
1451                }
1452                Some('}') => {
1453                    self.pos += 1;
1454                    break;
1455                }
1456                _ => return Err("SyntaxError: bad object in JSON".into()),
1457            }
1458        }
1459        Ok(with_host(|h| h.new_object(props)))
1460    }
1461}
1462
1463// ══ type methods (array / string / number) ═══════════════════════════════════
1464
1465fn is_array_method(name: &str) -> bool {
1466    matches!(
1467        name,
1468        "push" | "pop" | "shift" | "unshift" | "map" | "filter" | "forEach" | "join" | "slice"
1469            | "indexOf" | "lastIndexOf" | "includes" | "reduce" | "concat" | "reverse" | "sort"
1470            | "find" | "findIndex" | "some" | "every" | "flat" | "fill" | "splice" | "keys"
1471            | "values" | "entries" | "flatMap" | "at" | "toString"
1472    )
1473}
1474fn is_string_method(name: &str) -> bool {
1475    matches!(
1476        name,
1477        "toUpperCase" | "toLowerCase" | "charAt" | "charCodeAt" | "codePointAt" | "indexOf"
1478            | "lastIndexOf" | "includes" | "slice" | "substring" | "substr" | "split" | "trim"
1479            | "trimStart" | "trimEnd" | "replace" | "replaceAll" | "repeat" | "startsWith"
1480            | "endsWith" | "padStart" | "padEnd" | "concat" | "at" | "toString" | "valueOf"
1481    )
1482}
1483fn is_number_method(name: &str) -> bool {
1484    matches!(name, "toFixed" | "toString" | "toPrecision" | "valueOf")
1485}
1486
1487/// Dispatch `recv.name(args)` for the built-in prototype methods.
1488pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
1489    let obj = with_host(|h| h.get(recv).cloned());
1490    match obj {
1491        Some(JsObj::Array(_)) => array_method(recv, name, args),
1492        Some(JsObj::Str(s)) => string_method(&s, name, args),
1493        Some(JsObj::Object(props)) => {
1494            if let Some(f) = props.get(name).cloned() {
1495                host::invoke(&f, args, Some(recv.clone()))
1496            } else if name == "hasOwnProperty" {
1497                let k = with_host(|h| h.str_of(&arg0(&args)));
1498                Ok(Value::Bool(props.contains_key(&k)))
1499            } else if name == "toString" {
1500                Ok(with_host(|h| h.new_str("[object Object]")))
1501            } else {
1502                Err(host::type_error(&format!(
1503                    "{} is not a function",
1504                    name
1505                )))
1506            }
1507        }
1508        _ => {
1509            // Primitive number/bool/string coercions.
1510            if let Value::Float(_) | Value::Int(_) = recv {
1511                return number_method(with_host(|h| h.to_number(recv)), name, args);
1512            }
1513            if let Some(s) = with_host(|h| h.as_str(recv)) {
1514                return string_method(&s, name, args);
1515            }
1516            Err(host::type_error(&format!("{} is not a function", name)))
1517        }
1518    }
1519}
1520
1521fn array_items(recv: &Value) -> Vec<Value> {
1522    with_host(|h| match h.get(recv) {
1523        Some(JsObj::Array(items)) => items.clone(),
1524        _ => Vec::new(),
1525    })
1526}
1527
1528fn array_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
1529    match name {
1530        "push" => {
1531            with_host(|h| {
1532                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
1533                    items.extend(args.iter().cloned());
1534                }
1535            });
1536            Ok(Value::Float(array_items(recv).len() as f64))
1537        }
1538        "pop" => Ok(with_host(|h| {
1539            if let Some(JsObj::Array(items)) = h.get_mut(recv) {
1540                items.pop().unwrap_or(Value::Undef)
1541            } else {
1542                Value::Undef
1543            }
1544        })),
1545        "shift" => Ok(with_host(|h| {
1546            if let Some(JsObj::Array(items)) = h.get_mut(recv) {
1547                if items.is_empty() {
1548                    Value::Undef
1549                } else {
1550                    items.remove(0)
1551                }
1552            } else {
1553                Value::Undef
1554            }
1555        })),
1556        "unshift" => {
1557            with_host(|h| {
1558                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
1559                    for (i, a) in args.iter().enumerate() {
1560                        items.insert(i, a.clone());
1561                    }
1562                }
1563            });
1564            Ok(Value::Float(array_items(recv).len() as f64))
1565        }
1566        "join" => {
1567            let sep = if args.is_empty() {
1568                ",".to_string()
1569            } else {
1570                with_host(|h| h.str_of(&args[0]))
1571            };
1572            let items = array_items(recv);
1573            let s = with_host(|h| {
1574                items
1575                    .iter()
1576                    .map(|x| match x {
1577                        Value::Undef => String::new(),
1578                        _ if h.is_null(x) => String::new(),
1579                        _ => h.str_of(x),
1580                    })
1581                    .collect::<Vec<_>>()
1582                    .join(&sep)
1583            });
1584            Ok(with_host(|h| h.new_str(s)))
1585        }
1586        "indexOf" => {
1587            let items = array_items(recv);
1588            let target = arg0(&args);
1589            let idx = with_host(|h| items.iter().position(|x| h.strict_eq(x, &target)));
1590            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
1591        }
1592        "lastIndexOf" => {
1593            let items = array_items(recv);
1594            let target = arg0(&args);
1595            let idx = with_host(|h| items.iter().rposition(|x| h.strict_eq(x, &target)));
1596            Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
1597        }
1598        "includes" => {
1599            let items = array_items(recv);
1600            let target = arg0(&args);
1601            Ok(Value::Bool(with_host(|h| items.iter().any(|x| h.strict_eq(x, &target)))))
1602        }
1603        "slice" => {
1604            let items = array_items(recv);
1605            let (lo, hi) = slice_bounds(&args, items.len());
1606            Ok(with_host(|h| h.new_array(items[lo..hi].to_vec())))
1607        }
1608        "concat" => {
1609            let mut out = array_items(recv);
1610            for a in &args {
1611                match with_host(|h| h.get(a).cloned()) {
1612                    Some(JsObj::Array(items)) => out.extend(items),
1613                    _ => out.push(a.clone()),
1614                }
1615            }
1616            Ok(with_host(|h| h.new_array(out)))
1617        }
1618        "reverse" => {
1619            with_host(|h| {
1620                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
1621                    items.reverse();
1622                }
1623            });
1624            Ok(recv.clone())
1625        }
1626        "fill" => {
1627            let val = arg0(&args);
1628            with_host(|h| {
1629                if let Some(JsObj::Array(items)) = h.get_mut(recv) {
1630                    for it in items.iter_mut() {
1631                        *it = val.clone();
1632                    }
1633                }
1634            });
1635            Ok(recv.clone())
1636        }
1637        "at" => {
1638            let items = array_items(recv);
1639            let mut i = arg_num(&args, 0) as i64;
1640            if i < 0 {
1641                i += items.len() as i64;
1642            }
1643            Ok(if i >= 0 && (i as usize) < items.len() {
1644                items[i as usize].clone()
1645            } else {
1646                Value::Undef
1647            })
1648        }
1649        "map" => {
1650            let items = array_items(recv);
1651            let cb = arg0(&args);
1652            let mut out = Vec::with_capacity(items.len());
1653            for (i, it) in items.iter().enumerate() {
1654                out.push(host::invoke(&cb, vec![it.clone(), Value::Float(i as f64), recv.clone()], None)?);
1655            }
1656            Ok(with_host(|h| h.new_array(out)))
1657        }
1658        "flatMap" => {
1659            let items = array_items(recv);
1660            let cb = arg0(&args);
1661            let mut out = Vec::new();
1662            for (i, it) in items.iter().enumerate() {
1663                let r = host::invoke(&cb, vec![it.clone(), Value::Float(i as f64), recv.clone()], None)?;
1664                match with_host(|h| h.get(&r).cloned()) {
1665                    Some(JsObj::Array(inner)) => out.extend(inner),
1666                    _ => out.push(r),
1667                }
1668            }
1669            Ok(with_host(|h| h.new_array(out)))
1670        }
1671        "filter" => {
1672            let items = array_items(recv);
1673            let cb = arg0(&args);
1674            let mut out = Vec::new();
1675            for (i, it) in items.iter().enumerate() {
1676                let keep = host::invoke(&cb, vec![it.clone(), Value::Float(i as f64), recv.clone()], None)?;
1677                if with_host(|h| h.truthy(&keep)) {
1678                    out.push(it.clone());
1679                }
1680            }
1681            Ok(with_host(|h| h.new_array(out)))
1682        }
1683        "forEach" => {
1684            let items = array_items(recv);
1685            let cb = arg0(&args);
1686            for (i, it) in items.iter().enumerate() {
1687                host::invoke(&cb, vec![it.clone(), Value::Float(i as f64), recv.clone()], None)?;
1688            }
1689            Ok(Value::Undef)
1690        }
1691        "find" => {
1692            let items = array_items(recv);
1693            let cb = arg0(&args);
1694            for (i, it) in items.iter().enumerate() {
1695                let m = host::invoke(&cb, vec![it.clone(), Value::Float(i as f64), recv.clone()], None)?;
1696                if with_host(|h| h.truthy(&m)) {
1697                    return Ok(it.clone());
1698                }
1699            }
1700            Ok(Value::Undef)
1701        }
1702        "findIndex" => {
1703            let items = array_items(recv);
1704            let cb = arg0(&args);
1705            for (i, it) in items.iter().enumerate() {
1706                let m = host::invoke(&cb, vec![it.clone(), Value::Float(i as f64), recv.clone()], None)?;
1707                if with_host(|h| h.truthy(&m)) {
1708                    return Ok(Value::Float(i as f64));
1709                }
1710            }
1711            Ok(Value::Float(-1.0))
1712        }
1713        "some" => {
1714            let items = array_items(recv);
1715            let cb = arg0(&args);
1716            for (i, it) in items.iter().enumerate() {
1717                let m = host::invoke(&cb, vec![it.clone(), Value::Float(i as f64), recv.clone()], None)?;
1718                if with_host(|h| h.truthy(&m)) {
1719                    return Ok(Value::Bool(true));
1720                }
1721            }
1722            Ok(Value::Bool(false))
1723        }
1724        "every" => {
1725            let items = array_items(recv);
1726            let cb = arg0(&args);
1727            for (i, it) in items.iter().enumerate() {
1728                let m = host::invoke(&cb, vec![it.clone(), Value::Float(i as f64), recv.clone()], None)?;
1729                if !with_host(|h| h.truthy(&m)) {
1730                    return Ok(Value::Bool(false));
1731                }
1732            }
1733            Ok(Value::Bool(true))
1734        }
1735        "reduce" => {
1736            let items = array_items(recv);
1737            let cb = arg0(&args);
1738            let mut acc;
1739            let mut start = 0;
1740            if args.len() >= 2 {
1741                acc = args[1].clone();
1742            } else if !items.is_empty() {
1743                acc = items[0].clone();
1744                start = 1;
1745            } else {
1746                return Err(host::type_error("Reduce of empty array with no initial value"));
1747            }
1748            for (i, it) in items.iter().enumerate().skip(start) {
1749                acc = host::invoke(&cb, vec![acc, it.clone(), Value::Float(i as f64), recv.clone()], None)?;
1750            }
1751            Ok(acc)
1752        }
1753        "sort" => {
1754            let mut items = array_items(recv);
1755            let cmp = args.first().cloned();
1756            // Insertion sort so we can call the (fallible) JS comparator.
1757            let mut err: Option<String> = None;
1758            for i in 1..items.len() {
1759                let mut j = i;
1760                while j > 0 {
1761                    let order = match &cmp {
1762                        Some(cb) => {
1763                            match host::invoke(cb, vec![items[j - 1].clone(), items[j].clone()], None) {
1764                                Ok(v) => with_host(|h| h.to_number(&v)),
1765                                Err(e) => {
1766                                    err = Some(e);
1767                                    0.0
1768                                }
1769                            }
1770                        }
1771                        None => {
1772                            let a = with_host(|h| h.str_of(&items[j - 1]));
1773                            let b = with_host(|h| h.str_of(&items[j]));
1774                            if a > b {
1775                                1.0
1776                            } else {
1777                                -1.0
1778                            }
1779                        }
1780                    };
1781                    if err.is_some() {
1782                        break;
1783                    }
1784                    if order > 0.0 {
1785                        items.swap(j - 1, j);
1786                        j -= 1;
1787                    } else {
1788                        break;
1789                    }
1790                }
1791                if err.is_some() {
1792                    break;
1793                }
1794            }
1795            if let Some(e) = err {
1796                return Err(e);
1797            }
1798            with_host(|h| {
1799                if let Some(JsObj::Array(a)) = h.get_mut(recv) {
1800                    *a = items;
1801                }
1802            });
1803            Ok(recv.clone())
1804        }
1805        "flat" => {
1806            let items = array_items(recv);
1807            let mut out = Vec::new();
1808            for it in items {
1809                match with_host(|h| h.get(&it).cloned()) {
1810                    Some(JsObj::Array(inner)) => out.extend(inner),
1811                    _ => out.push(it),
1812                }
1813            }
1814            Ok(with_host(|h| h.new_array(out)))
1815        }
1816        "keys" => {
1817            let n = array_items(recv).len();
1818            let items: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
1819            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
1820        }
1821        "values" => {
1822            let items = array_items(recv);
1823            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
1824        }
1825        "entries" => {
1826            let items = array_items(recv);
1827            let pairs: Vec<Value> = items
1828                .into_iter()
1829                .enumerate()
1830                .map(|(i, v)| with_host(|h| h.new_array(vec![Value::Float(i as f64), v])))
1831                .collect();
1832            Ok(with_host(|h| h.alloc(JsObj::Iter { items: pairs, idx: 0 })))
1833        }
1834        "splice" => array_splice(recv, args),
1835        "toString" => {
1836            let s = with_host(|h| h.str_of(recv));
1837            Ok(with_host(|h| h.new_str(s)))
1838        }
1839        _ => Err(host::type_error(&format!("{name} is not a function"))),
1840    }
1841}
1842
1843fn array_splice(recv: &Value, args: Vec<Value>) -> Result<Value, String> {
1844    let len = array_items(recv).len();
1845    let start = {
1846        let s = arg_num(&args, 0);
1847        if s < 0.0 {
1848            ((len as f64 + s).max(0.0)) as usize
1849        } else {
1850            (s as usize).min(len)
1851        }
1852    };
1853    let delete = if args.len() >= 2 {
1854        (arg_num(&args, 1).max(0.0) as usize).min(len - start)
1855    } else {
1856        len - start
1857    };
1858    let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
1859    let removed = with_host(|h| {
1860        if let Some(JsObj::Array(items)) = h.get_mut(recv) {
1861            let removed: Vec<Value> = items.splice(start..start + delete, inserts).collect();
1862            removed
1863        } else {
1864            Vec::new()
1865        }
1866    });
1867    Ok(with_host(|h| h.new_array(removed)))
1868}
1869
1870fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
1871    let norm = |v: f64| -> usize {
1872        if v < 0.0 {
1873            ((len as f64 + v).max(0.0)) as usize
1874        } else {
1875            (v as usize).min(len)
1876        }
1877    };
1878    let lo = if args.is_empty() || matches!(args[0], Value::Undef) {
1879        0
1880    } else {
1881        norm(arg_num(args, 0))
1882    };
1883    let hi = if args.len() < 2 || matches!(args[1], Value::Undef) {
1884        len
1885    } else {
1886        norm(arg_num(args, 1))
1887    };
1888    // A start at or past the end (`'World'.slice(2, 1)`) yields the empty range,
1889    // never a reversed one: JS `slice` clamps `end` up to `start`.
1890    (lo, hi.max(lo))
1891}
1892
1893fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
1894    let chars: Vec<char> = s.chars().collect();
1895    match name {
1896        "toUpperCase" => Ok(new_s(s.to_uppercase())),
1897        "toLowerCase" => Ok(new_s(s.to_lowercase())),
1898        "trim" => Ok(new_s(s.trim().to_string())),
1899        "trimStart" => Ok(new_s(s.trim_start().to_string())),
1900        "trimEnd" => Ok(new_s(s.trim_end().to_string())),
1901        "toString" | "valueOf" => Ok(new_s(s.to_string())),
1902        "charAt" => {
1903            let i = arg_num(&args, 0) as usize;
1904            Ok(new_s(chars.get(i).map(|c| c.to_string()).unwrap_or_default()))
1905        }
1906        "at" => {
1907            let mut i = arg_num(&args, 0) as i64;
1908            if i < 0 {
1909                i += chars.len() as i64;
1910            }
1911            if i >= 0 && (i as usize) < chars.len() {
1912                Ok(new_s(chars[i as usize].to_string()))
1913            } else {
1914                Ok(Value::Undef)
1915            }
1916        }
1917        "charCodeAt" | "codePointAt" => {
1918            let i = arg_num(&args, 0) as usize;
1919            match chars.get(i) {
1920                Some(c) => Ok(Value::Float(*c as u32 as f64)),
1921                None => Ok(Value::Float(f64::NAN)),
1922            }
1923        }
1924        "indexOf" => {
1925            let needle = with_host(|h| h.str_of(&arg0(&args)));
1926            Ok(Value::Float(byte_to_char_index(s, s.find(&needle))))
1927        }
1928        "lastIndexOf" => {
1929            let needle = with_host(|h| h.str_of(&arg0(&args)));
1930            Ok(Value::Float(byte_to_char_index(s, s.rfind(&needle))))
1931        }
1932        "includes" => {
1933            let needle = with_host(|h| h.str_of(&arg0(&args)));
1934            Ok(Value::Bool(s.contains(&needle)))
1935        }
1936        "startsWith" => {
1937            let needle = with_host(|h| h.str_of(&arg0(&args)));
1938            Ok(Value::Bool(s.starts_with(&needle)))
1939        }
1940        "endsWith" => {
1941            let needle = with_host(|h| h.str_of(&arg0(&args)));
1942            Ok(Value::Bool(s.ends_with(&needle)))
1943        }
1944        "slice" => {
1945            let (lo, hi) = slice_bounds(&args, chars.len());
1946            Ok(new_s(chars[lo..hi].iter().collect()))
1947        }
1948        "substring" => {
1949            let mut a = arg_num(&args, 0).max(0.0) as usize;
1950            let mut b = if args.len() < 2 || matches!(args[1], Value::Undef) {
1951                chars.len()
1952            } else {
1953                (arg_num(&args, 1).max(0.0) as usize).min(chars.len())
1954            };
1955            a = a.min(chars.len());
1956            if a > b {
1957                std::mem::swap(&mut a, &mut b);
1958            }
1959            Ok(new_s(chars[a..b].iter().collect()))
1960        }
1961        "substr" => {
1962            // A negative start counts from the end: max(len + start, 0).
1963            let len = chars.len() as i64;
1964            let mut start = arg_num(&args, 0) as i64;
1965            if start < 0 {
1966                start = (len + start).max(0);
1967            }
1968            let start = (start as usize).min(chars.len());
1969            let count = if args.len() >= 2 {
1970                arg_num(&args, 1).max(0.0) as usize
1971            } else {
1972                chars.len()
1973            };
1974            let end = (start + count).min(chars.len());
1975            Ok(new_s(chars[start..end].iter().collect()))
1976        }
1977        "repeat" => {
1978            let n = arg_num(&args, 0);
1979            if n < 0.0 || !n.is_finite() {
1980                return Err(host::type_error("Invalid count value"));
1981            }
1982            Ok(new_s(s.repeat(n as usize)))
1983        }
1984        "concat" => {
1985            let mut out = s.to_string();
1986            for a in &args {
1987                out.push_str(&with_host(|h| h.str_of(a)));
1988            }
1989            Ok(new_s(out))
1990        }
1991        "padStart" => Ok(new_s(pad(s, &args, true))),
1992        "padEnd" => Ok(new_s(pad(s, &args, false))),
1993        "replace" => {
1994            let from = with_host(|h| h.str_of(&arg0(&args)));
1995            let to = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
1996            Ok(new_s(s.replacen(&from, &to, 1)))
1997        }
1998        "replaceAll" => {
1999            let from = with_host(|h| h.str_of(&arg0(&args)));
2000            let to = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
2001            Ok(new_s(s.replace(&from, &to)))
2002        }
2003        "split" => {
2004            let parts: Vec<Value> = if args.is_empty() || matches!(args[0], Value::Undef) {
2005                vec![new_s(s.to_string())]
2006            } else {
2007                let sep = with_host(|h| h.str_of(&args[0]));
2008                if sep.is_empty() {
2009                    chars.iter().map(|c| new_s(c.to_string())).collect()
2010                } else {
2011                    s.split(&sep as &str).map(|p| new_s(p.to_string())).collect()
2012                }
2013            };
2014            Ok(with_host(|h| h.new_array(parts)))
2015        }
2016        _ => Err(host::type_error(&format!("{name} is not a function"))),
2017    }
2018}
2019
2020fn new_s(s: String) -> Value {
2021    with_host(|h| h.new_str(s))
2022}
2023
2024fn byte_to_char_index(s: &str, byte: Option<usize>) -> f64 {
2025    match byte {
2026        Some(b) => s[..b].chars().count() as f64,
2027        None => -1.0,
2028    }
2029}
2030
2031fn pad(s: &str, args: &[Value], start: bool) -> String {
2032    let target = arg_num(args, 0) as usize;
2033    let cur = s.chars().count();
2034    if cur >= target {
2035        return s.to_string();
2036    }
2037    let filler = if args.len() >= 2 {
2038        with_host(|h| h.str_of(&args[1]))
2039    } else {
2040        " ".to_string()
2041    };
2042    if filler.is_empty() {
2043        return s.to_string();
2044    }
2045    let need = target - cur;
2046    let fill_chars: Vec<char> = filler.chars().collect();
2047    let padding: String = (0..need).map(|i| fill_chars[i % fill_chars.len()]).collect();
2048    if start {
2049        format!("{padding}{s}")
2050    } else {
2051        format!("{s}{padding}")
2052    }
2053}
2054
2055fn number_method(n: f64, name: &str, args: Vec<Value>) -> Result<Value, String> {
2056    match name {
2057        "toFixed" => {
2058            let digits = arg_num(&args, 0).max(0.0) as usize;
2059            Ok(new_s(to_fixed(n, digits)))
2060        }
2061        "toString" => {
2062            let radix = args.first().map(|_| arg_num(&args, 0) as u32).unwrap_or(10);
2063            if radix == 10 || !(2..=36).contains(&radix) {
2064                Ok(new_s(host::fmt_number(n)))
2065            } else {
2066                Ok(new_s(to_radix(n, radix)))
2067            }
2068        }
2069        "toPrecision" => {
2070            if args.is_empty() {
2071                Ok(new_s(host::fmt_number(n)))
2072            } else {
2073                let p = arg_num(&args, 0) as usize;
2074                Ok(new_s(to_precision(n, p.max(1))))
2075            }
2076        }
2077        "valueOf" => Ok(Value::Float(n)),
2078        _ => Err(host::type_error(&format!("{name} is not a function"))),
2079    }
2080}
2081
2082/// `Number.prototype.toFixed(f)`: fixed-point with `f` fractional digits, rounding
2083/// half away from zero on the actual IEEE-754 value (so `(1.005).toFixed(2)` is
2084/// `"1.00"` because 1.005 is really 1.00499…). The sign of a negative input is
2085/// preserved even when the rounded magnitude is zero: `(-0.4).toFixed(0) === "-0"`.
2086///
2087/// The rounding is done on the value's EXACT decimal expansion (Rust's fixed
2088/// formatting is exact), not on `x * 10^f` — the latter loses precision for large
2089/// magnitudes (`(9.999999e20).toFixed(4)` must keep every integer digit).
2090fn to_fixed(n: f64, f: usize) -> String {
2091    if !n.is_finite() {
2092        return host::fmt_number(n);
2093    }
2094    // Spec: for |x| ≥ 10^21, toFixed falls back to ToString(x).
2095    if n.abs() >= 1e21 {
2096        return host::fmt_number(n);
2097    }
2098    let neg = n < 0.0;
2099    // Exact decimal with guard digits past the rounding position; then round the
2100    // digit string half-away-from-zero (nonneg operand ⇒ round-half-up).
2101    let full = format!("{:.*}", f + 25, n.abs());
2102    let mut body = round_decimal_string(&full, f);
2103    if neg {
2104        body.insert(0, '-'); // JS keeps the sign even for "-0" / "-0.00".
2105    }
2106    body
2107}
2108
2109/// Round the exact decimal string `s` (`"int.frac"`, nonnegative) to `f`
2110/// fractional digits, half away from zero, propagating carry across the point.
2111fn round_decimal_string(s: &str, f: usize) -> String {
2112    let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
2113    let mut digits: Vec<u8> = int_part.bytes().chain(frac_part.bytes()).map(|b| b - b'0').collect();
2114    let point = int_part.len(); // digits before the decimal point
2115    let keep = point + f; // number of leading digits to keep
2116
2117    // Round up if the first dropped digit is ≥ 5 (exact-half ⇒ up).
2118    if digits.get(keep).map(|&d| d >= 5).unwrap_or(false) {
2119        let mut i = keep;
2120        loop {
2121            if i == 0 {
2122                digits.insert(0, 1);
2123                // A new leading digit shifts the decimal point right by one.
2124                return assemble_decimal(&digits, point + 1, f);
2125            }
2126            i -= 1;
2127            if digits[i] == 9 {
2128                digits[i] = 0;
2129            } else {
2130                digits[i] += 1;
2131                break;
2132            }
2133        }
2134    }
2135    assemble_decimal(&digits, point, f)
2136}
2137
2138/// Reassemble `digits` into `"int.frac"` keeping `f` fractional digits, given that
2139/// `point` digits precede the decimal point.
2140fn assemble_decimal(digits: &[u8], point: usize, f: usize) -> String {
2141    let int_str: String = digits[..point].iter().map(|d| (d + b'0') as char).collect();
2142    let int_str = int_str.trim_start_matches('0');
2143    let int_str = if int_str.is_empty() { "0" } else { int_str };
2144    if f == 0 {
2145        return int_str.to_string();
2146    }
2147    let frac: String = digits[point..point + f].iter().map(|d| (d + b'0') as char).collect();
2148    format!("{int_str}.{frac}")
2149}
2150
2151/// `Number.prototype.toPrecision(p)`: `p` significant digits, switching to
2152/// exponential form when the decimal exponent `e` satisfies `e < -6` or `e ≥ p`
2153/// (ECMAScript Number.prototype.toPrecision). Trailing zeros are significant and
2154/// retained (`(100).toPrecision(5) === "100.00"`).
2155fn to_precision(n: f64, p: usize) -> String {
2156    if !n.is_finite() {
2157        return host::fmt_number(n);
2158    }
2159    if n == 0.0 {
2160        return if p == 1 {
2161            "0".into()
2162        } else {
2163            format!("0.{}", "0".repeat(p - 1))
2164        };
2165    }
2166    let neg = n < 0.0;
2167    let a = n.abs();
2168    // Take the EXACT digits with guard positions past the p-th, then round to p
2169    // significant digits half away from zero — Rust's `{:.*e}` rounds half to
2170    // EVEN (`(2.5).toPrecision(1)` would give "2"), but JS rounds half up ("3").
2171    let sci = format!("{a:.*e}", p - 1 + 25);
2172    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
2173    let mut e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
2174    let all: Vec<u8> = mant.chars().filter(|c| c.is_ascii_digit()).map(|c| c as u8 - b'0').collect();
2175    let mut s: String = all[..p].iter().map(|d| (d + b'0') as char).collect();
2176    if all.get(p).map(|&d| d >= 5).unwrap_or(false) {
2177        // Round the p-digit mantissa up, propagating carry; a carry out of the
2178        // leading digit (`9.99 → 10`) bumps the decimal exponent by one.
2179        let mut d: Vec<u8> = all[..p].to_vec();
2180        let mut i = p;
2181        loop {
2182            if i == 0 {
2183                d.insert(0, 1);
2184                d.truncate(p);
2185                e += 1;
2186                break;
2187            }
2188            i -= 1;
2189            if d[i] == 9 {
2190                d[i] = 0;
2191            } else {
2192                d[i] += 1;
2193                break;
2194            }
2195        }
2196        s = d.iter().map(|x| (x + b'0') as char).collect();
2197    }
2198    let pp = p as i32;
2199
2200    let body = if e < -6 || e >= pp {
2201        // Exponential: first digit, optional '.rest', signed exponent.
2202        let sign = if e >= 0 { '+' } else { '-' };
2203        let mag = e.abs();
2204        if p == 1 {
2205            format!("{s}e{sign}{mag}")
2206        } else {
2207            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
2208        }
2209    } else if e >= 0 {
2210        // e in 0..p-1: (e+1) integer digits, then any remaining as fraction.
2211        let ip = (e + 1) as usize;
2212        if ip == p {
2213            s
2214        } else {
2215            format!("{}.{}", &s[..ip], &s[ip..])
2216        }
2217    } else {
2218        // -6 ≤ e < 0: "0." then (−e−1) zeros then all p digits.
2219        format!("0.{}{}", "0".repeat((-e - 1) as usize), s)
2220    };
2221    if neg {
2222        format!("-{body}")
2223    } else {
2224        body
2225    }
2226}
2227
2228fn to_radix(n: f64, radix: u32) -> String {
2229    if !n.is_finite() {
2230        return host::fmt_number(n);
2231    }
2232    let neg = n < 0.0;
2233    let mut i = n.abs().trunc() as u64;
2234    if i == 0 {
2235        return "0".into();
2236    }
2237    let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
2238    let mut out = Vec::new();
2239    while i > 0 {
2240        out.push(digits[(i % radix as u64) as usize]);
2241        i /= radix as u64;
2242    }
2243    if neg {
2244        out.push(b'-');
2245    }
2246    out.reverse();
2247    String::from_utf8(out).unwrap()
2248}