Skip to main content

nodejs/
host.rs

1//! The JavaScript object heap and runtime, reached from fusevm through
2//! registered builtins (`register_builtin`) and the strict numeric hook.
3//!
4//! node-js owns no VM and no JIT: the compiler lowers JS to `fusevm::Chunk`, and
5//! every JS-specific operation the VM can't do natively is a builtin call that
6//! lands here. Local variables live in `Rc<RefCell>` environments chained
7//! parent-to-child, so a nested function/closure captures its enclosing scope by
8//! reference.
9//!
10//! Value representation:
11//!   - immediate: `Value::Float` (every JS number — one IEEE-754 f64 type),
12//!     `Value::Bool` (true/false), `Value::Undef` (undefined);
13//!   - heap `Value::Obj(u32)` handles: string, array, object, function,
14//!     builtin-namespace, and the canonical `null` — the reference types.
15
16use fusevm::{Chunk, NumOp, VMResult, Value, VM};
17use indexmap::IndexMap;
18use std::cell::RefCell;
19use std::rc::Rc;
20
21/// Builtin ids emitted by the compiler and registered on every VM. The compiler
22/// (`compiler.rs`) and the handler table (`builtins.rs::install`) must agree on
23/// these exactly.
24pub mod ops {
25    pub const GETLOCAL: u16 = 1; // [name] -> value (scope-chain read)
26    pub const SETLOCAL: u16 = 2; // [name, value] -> value (assignment)
27    pub const DECLARE: u16 = 3; // [name, value] -> value (let/const/var into current scope)
28    pub const DELNAME: u16 = 4; // [name]
29    pub const GETATTR: u16 = 5; // [recv, name] -> value (member .x)
30    pub const SETATTR: u16 = 6; // [recv, name, value]
31    pub const GETITEM: u16 = 7; // [recv, idx] -> value (computed [k])
32    pub const SETITEM: u16 = 8; // [recv, idx, value]
33    pub const DELITEM: u16 = 9; // [recv, idx] -> Bool (delete obj[k])
34    pub const MKSTR: u16 = 10; // [parts...] -> str (concat)
35    pub const MKARR: u16 = 11; // [items...] -> array
36    pub const MKOBJ: u16 = 12; // [tag,k,v,...] -> object (tag 1 = ...spread of k)
37    pub const CALL: u16 = 13; // [name, args...] -> resolve name & call
38    pub const CALL_METHOD: u16 = 14; // [recv, name, args...]
39    pub const CALL_VALUE: u16 = 15; // [callable, args...]
40    pub const NEW: u16 = 16; // [ctor, args...] -> instance
41    pub const TRUTHY: u16 = 17; // [v] -> Bool (JS truthiness)
42    pub const TOSTR: u16 = 18; // [v] -> str via String(v)
43    pub const MKFUNC: u16 = 19; // [func_id, defaults...] -> closure
44    pub const GETITER: u16 = 20; // [iterable] -> iterator (left on stack)
45    pub const FORITER: u16 = 21; // peek iterator -> pushes value + Bool(has_next)
46    pub const FORIN_KEYS: u16 = 22; // [obj] -> array of enumerable keys
47    pub const CONTAINS: u16 = 23; // [key, obj] -> Bool (`in`)
48    pub const SIG_RETURN: u16 = 24; // [v] -> return v from the function
49    pub const BINOP: u16 = 25; // [tag, a, b] -> bitwise/shift result (JS int32 semantics)
50    pub const UNARY: u16 = 26; // [tag, v] -> unary +/~ result
51    pub const STRICT_EQ: u16 = 27; // [a, b] -> Bool (===)
52    pub const LOOSE_EQ: u16 = 28; // [a, b] -> Bool (==)
53    pub const TYPEOF: u16 = 29; // [v] -> str
54    pub const LOAD_NULL: u16 = 30; // [] -> the canonical null
55    pub const THROW: u16 = 31; // [v] -> throw
56    pub const TRY: u16 = 32; // [try_id] -> run a try/catch/finally block
57    pub const NULLISH: u16 = 33; // [v] -> Bool (v is null or undefined)
58    pub const UNPACK: u16 = 34; // [iterable, count, star] -> pushes count values
59    pub const BUILD_ARGS: u16 = 35; // [tag,val,...] -> flat array (tag 1 = ...spread)
60    pub const THIS: u16 = 36; // [] -> current `this`
61    pub const INSTANCEOF: u16 = 37; // [a, b] -> Bool
62    pub const DELPROP_NAME: u16 = 38; // [recv, name] -> Bool (delete obj.name)
63    pub const APPLY: u16 = 39; // [callable, argsArray] -> call with spread args
64    pub const APPLY_METHOD: u16 = 40; // [recv, name, argsArray] -> method call with spread
65    pub const OBJ_REST: u16 = 41; // [obj, excludedKeys] -> object of remaining keys
66    pub const DIV: u16 = 42; // [a, b] -> IEEE `a / b` (JS: x/0 = ±Infinity, 0/0 = NaN)
67}
68
69/// Bitwise/shift op tags carried by `ops::BINOP` (JS ToInt32/ToUint32 rules).
70pub mod binop {
71    pub const BITAND: i64 = 0;
72    pub const BITOR: i64 = 1;
73    pub const BITXOR: i64 = 2;
74    pub const SHL: i64 = 3;
75    pub const SHR: i64 = 4;
76    pub const USHR: i64 = 5;
77}
78
79/// Unary op tags carried by `ops::UNARY`.
80pub mod unop {
81    pub const POS: i64 = 0; // unary +
82    pub const BITNOT: i64 = 1; // ~
83}
84
85// ── heap objects ───────────────────────────────────────────────────────────
86
87/// A compiled function template: parameter shape + body chunk. Shared by every
88/// closure created from the same function/arrow.
89#[derive(Clone)]
90pub struct FuncDef {
91    pub name: String,
92    /// Parameter binding templates (destructuring lowered by the compiler into
93    /// the body prologue; here we only track the simple arg slots).
94    pub params: Vec<ParamSlot>,
95    pub chunk: Chunk,
96    pub is_arrow: bool,
97}
98
99/// One parameter slot. `name` is the simple bound name; a destructuring pattern
100/// is lowered to a synthetic `.arg{i}` name plus body prologue code.
101#[derive(Clone)]
102pub struct ParamSlot {
103    pub name: String,
104    /// True for the `...rest` collector.
105    pub rest: bool,
106    /// True if this slot has a default expression (applied in the body prologue).
107    pub has_default: bool,
108}
109
110/// A compiled `try`/`catch`/`finally` block. Bodies are bare chunks run in the
111/// current scope.
112#[derive(Clone)]
113pub struct TryDef {
114    pub block: Chunk,
115    /// `(catch_param_name, catch_body)`.
116    pub handler: Option<(Option<String>, Chunk)>,
117    pub finalizer: Option<Chunk>,
118}
119
120/// A live closure value.
121#[derive(Clone)]
122pub struct FuncVal {
123    pub def_id: usize,
124    /// Captured lexical environment (enclosing scope chain), for free vars.
125    pub env: Option<Env>,
126    /// `this` captured at definition time (arrow functions).
127    pub this: Option<Value>,
128    pub is_arrow: bool,
129}
130
131/// A heap object.
132#[derive(Clone)]
133pub enum JsObj {
134    Str(String),
135    Array(Vec<Value>),
136    Object(IndexMap<String, Value>),
137    Func(FuncVal),
138    /// A first-class reference to a builtin function or namespace
139    /// (`console.log`, `Math`, `parseInt`).
140    Builtin(String),
141    /// A bound method value (`obj.method` captured then called): dispatches
142    /// through `call_method(recv, name, args)` when invoked.
143    BoundMethod { recv: Value, name: String },
144    /// The single canonical `null`.
145    Null,
146    /// A live iterator over a sequence, with a cursor.
147    Iter { items: Vec<Value>, idx: usize },
148}
149
150// ── environments ─────────────────────────────────────────────────────────────
151
152/// A local-variable environment, shared (by `Rc`) between a frame and any nested
153/// function that captures it.
154pub struct EnvData {
155    pub vars: IndexMap<String, Value>,
156    pub parent: Option<Env>,
157}
158pub type Env = Rc<RefCell<EnvData>>;
159
160fn new_env(parent: Option<Env>) -> Env {
161    Rc::new(RefCell::new(EnvData {
162        vars: IndexMap::new(),
163        parent,
164    }))
165}
166
167/// One function activation.
168pub struct Frame {
169    pub env: Env,
170    pub this_obj: Option<Value>,
171}
172
173/// A non-local control signal.
174#[derive(Clone)]
175pub enum Signal {
176    Return(Value),
177    Break,
178    Continue,
179}
180
181/// The JavaScript runtime.
182pub struct JsHost {
183    heap: Vec<JsObj>,
184    /// Function templates, indexed by def id.
185    pub funcs: Vec<FuncDef>,
186    /// try/catch/finally block templates, indexed by try id.
187    pub tries: Vec<TryDef>,
188    /// Module-level (global) names.
189    globals: IndexMap<String, Value>,
190    /// The frame stack (bottom = module).
191    frames: Vec<Frame>,
192    pub error: Option<String>,
193    /// The in-flight thrown value, if any (JS `throw`).
194    pub exc: Option<Value>,
195    pub signal: Option<Signal>,
196    /// The canonical `null` handle (allocated once).
197    null_val: Value,
198}
199
200thread_local! {
201    static HOST: RefCell<JsHost> = RefCell::new(JsHost::new());
202}
203
204/// Run `f` with mutable access to the thread-local host.
205pub fn with_host<R>(f: impl FnOnce(&mut JsHost) -> R) -> R {
206    HOST.with(|h| f(&mut h.borrow_mut()))
207}
208
209/// Reset the host to a clean slate (fresh module frame).
210pub fn reset_host() {
211    with_host(|h| *h = JsHost::new());
212}
213
214impl Default for JsHost {
215    fn default() -> Self {
216        Self::new()
217    }
218}
219
220impl JsHost {
221    pub fn new() -> JsHost {
222        let module_env = new_env(None);
223        let mut h = JsHost {
224            heap: Vec::new(),
225            funcs: Vec::new(),
226            tries: Vec::new(),
227            globals: IndexMap::new(),
228            frames: vec![Frame {
229                env: module_env,
230                this_obj: None,
231            }],
232            error: None,
233            exc: None,
234            signal: None,
235            null_val: Value::Undef,
236        };
237        h.null_val = h.alloc(JsObj::Null);
238        h
239    }
240
241    pub fn null(&self) -> Value {
242        self.null_val.clone()
243    }
244    pub fn is_null(&self, v: &Value) -> bool {
245        matches!(self.get(v), Some(JsObj::Null))
246    }
247
248    // ── program loading ──────────────────────────────────────────────────
249    pub fn program_offsets(&self) -> (usize, usize) {
250        (self.funcs.len(), self.tries.len())
251    }
252    pub fn load_program(&mut self, funcs: Vec<FuncDef>, tries: Vec<TryDef>) {
253        self.funcs.extend(funcs);
254        self.tries.extend(tries);
255    }
256    pub fn try_def(&self, id: usize) -> Option<TryDef> {
257        self.tries.get(id).cloned()
258    }
259
260    // ── heap allocation / accessors ──────────────────────────────────────
261    pub fn alloc(&mut self, obj: JsObj) -> Value {
262        self.heap.push(obj);
263        Value::Obj((self.heap.len() - 1) as u32)
264    }
265    pub fn get(&self, v: &Value) -> Option<&JsObj> {
266        if let Value::Obj(i) = v {
267            self.heap.get(*i as usize)
268        } else {
269            None
270        }
271    }
272    pub fn get_mut(&mut self, v: &Value) -> Option<&mut JsObj> {
273        if let Value::Obj(i) = v {
274            self.heap.get_mut(*i as usize)
275        } else {
276            None
277        }
278    }
279    pub fn new_str(&mut self, s: impl Into<String>) -> Value {
280        self.alloc(JsObj::Str(s.into()))
281    }
282    pub fn new_array(&mut self, items: Vec<Value>) -> Value {
283        self.alloc(JsObj::Array(items))
284    }
285    pub fn new_object(&mut self, props: IndexMap<String, Value>) -> Value {
286        self.alloc(JsObj::Object(props))
287    }
288    pub fn as_str(&self, v: &Value) -> Option<String> {
289        match v {
290            Value::Str(s) => Some((**s).clone()),
291            Value::Obj(_) => match self.get(v) {
292                Some(JsObj::Str(s)) => Some(s.clone()),
293                _ => None,
294            },
295            _ => None,
296        }
297    }
298
299    // ── scope / names ────────────────────────────────────────────────────
300    fn frame(&self) -> &Frame {
301        self.frames.last().unwrap()
302    }
303    fn cur_env(&self) -> Env {
304        self.frame().env.clone()
305    }
306
307    /// Scope-chain read: local + enclosing chain, then globals.
308    pub fn read_name(&self, name: &str) -> Option<Value> {
309        let mut env = Some(self.cur_env());
310        while let Some(e) = env {
311            if let Some(v) = e.borrow().vars.get(name) {
312                return Some(v.clone());
313            }
314            env = e.borrow().parent.clone();
315        }
316        self.globals.get(name).cloned()
317    }
318    pub fn read_global(&self, name: &str) -> Option<Value> {
319        self.globals.get(name).cloned()
320    }
321
322    /// Assign to an existing binding up the scope chain, else create a global
323    /// (JS assignment to an undeclared name targets the global object).
324    pub fn set_name(&mut self, name: &str, val: Value) {
325        let mut env = Some(self.cur_env());
326        while let Some(e) = env {
327            if e.borrow().vars.contains_key(name) {
328                e.borrow_mut().vars.insert(name.to_string(), val);
329                return;
330            }
331            env = e.borrow().parent.clone();
332        }
333        self.globals.insert(name.to_string(), val);
334    }
335
336    /// Declare a new binding in the current scope (`let`/`const`/`var`).
337    pub fn declare_name(&mut self, name: &str, val: Value) {
338        if self.frames.len() == 1 {
339            self.globals.insert(name.to_string(), val);
340        } else {
341            self.cur_env().borrow_mut().vars.insert(name.to_string(), val);
342        }
343    }
344    pub fn set_global(&mut self, name: &str, val: Value) {
345        self.globals.insert(name.to_string(), val);
346    }
347    pub fn del_name(&mut self, name: &str) {
348        if self.cur_env().borrow_mut().vars.shift_remove(name).is_some() {
349            return;
350        }
351        self.globals.shift_remove(name);
352    }
353
354    pub fn current_this(&self) -> Option<Value> {
355        self.frame().this_obj.clone()
356    }
357    pub fn current_env_capture(&self) -> Env {
358        self.frame().env.clone()
359    }
360
361    // ── signals / errors ─────────────────────────────────────────────────
362    pub fn take_error(&mut self) -> Option<String> {
363        self.error.take()
364    }
365    pub fn raise_str(&mut self, class: &str, msg: &str) -> String {
366        let s = if msg.is_empty() {
367            class.to_string()
368        } else {
369            format!("{class}: {msg}")
370        };
371        self.error = Some(s.clone());
372        s
373    }
374}
375
376// ── error constructors ───────────────────────────────────────────────────────
377
378pub fn type_error(msg: &str) -> String {
379    format!("TypeError: {msg}")
380}
381pub fn ref_error(name: &str) -> String {
382    format!("ReferenceError: {name} is not defined")
383}
384
385// ── the fusevm run plumbing ──────────────────────────────────────────────────
386
387/// Register every node-js builtin + the numeric hook on a VM, then run it.
388pub fn run_chunk_on(chunk: Chunk) -> Result<Value, String> {
389    let mut vm = VM::new(chunk);
390    crate::builtins::install(&mut vm);
391    vm.set_numeric_hook(std::sync::Arc::new(|op, a, b| {
392        crate::builtins::numeric_hook(op, a, b)
393    }));
394    vm.enable_tracing_jit();
395    let outcome = vm.run();
396    if let Some(e) = with_host(|h| h.take_error()) {
397        return Err(e);
398    }
399    match outcome {
400        VMResult::Ok(v) => Ok(v),
401        VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
402        VMResult::Error(e) => Err(e),
403    }
404}
405
406/// Run the top-level program chunk.
407pub fn run_main(chunk: Chunk) -> Result<Value, String> {
408    let r = run_chunk_on(chunk);
409    with_host(|h| h.signal = None);
410    r
411}
412
413// ── formatting ───────────────────────────────────────────────────────────────
414
415/// Format a JS number exactly as `Number.prototype.toString` does for the common
416/// range (no exponential-notation threshold handling for very large/small).
417pub fn fmt_number(f: f64) -> String {
418    if f.is_nan() {
419        return "NaN".into();
420    }
421    if f.is_infinite() {
422        return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
423    }
424    if f == 0.0 {
425        // Covers -0.0 too: (-0).toString() === "0".
426        return "0".into();
427    }
428    if f < 0.0 {
429        return format!("-{}", js_number_repr(-f));
430    }
431    js_number_repr(f)
432}
433
434/// ECMAScript `Number::toString` layout for a positive, finite, nonzero value.
435///
436/// Rust's `Display`/`LowerExp` give the shortest round-trip decimal digits, but
437/// NOT JavaScript's exponential-vs-fixed threshold: Rust prints `1e21` as
438/// `1000000000000000000000` and `1e-7` as `0.0000001`, whereas JS prints `1e+21`
439/// and `1e-7`. So we take the shortest digits from `{:e}` and re-lay them out per
440/// the spec (steps 5–10 of Number::toString): `k` significant digits `s` with
441/// decimal exponent `n` (value = s × 10^(n−k)); exponential form only when
442/// `n > 21` or `n ≤ -6`.
443fn js_number_repr(a: f64) -> String {
444    // `{:e}` yields `d[.ddd]e<exp>` with the mantissa in [1, 10) and shortest
445    // round-trip digits. Split it into the digit string `s` and exponent `E`.
446    let sci = format!("{a:e}");
447    let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
448    let e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
449    let s: String = mant.chars().filter(|c| *c != '.').collect();
450    let k = s.len() as i32; // number of significant digits
451    let n = e + 1; // value = s × 10^(n−k), 10^(k−1) ≤ s < 10^k
452
453    if k <= n && n <= 21 {
454        // Integer with trailing zeros: all digits, then n−k zeros.
455        let mut out = s;
456        out.push_str(&"0".repeat((n - k) as usize));
457        out
458    } else if 0 < n && n <= 21 {
459        // Decimal point inside the digit run: n digits, '.', the rest.
460        format!("{}.{}", &s[..n as usize], &s[n as usize..])
461    } else if -6 < n && n <= 0 {
462        // Leading "0." then (−n) zeros then all digits.
463        format!("0.{}{}", "0".repeat((-n) as usize), s)
464    } else {
465        // Exponential form. Exponent digit is n−1, always signed.
466        let exp = n - 1;
467        let sign = if exp >= 0 { '+' } else { '-' };
468        let mag = exp.abs();
469        if k == 1 {
470            format!("{s}e{sign}{mag}")
471        } else {
472            format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
473        }
474    }
475}
476
477impl JsHost {
478    /// The `typeof` string for `v`.
479    pub fn type_of(&self, v: &Value) -> &'static str {
480        match v {
481            Value::Undef => "undefined",
482            Value::Bool(_) => "boolean",
483            Value::Int(_) | Value::Float(_) => "number",
484            Value::Str(_) => "string",
485            Value::Obj(_) => match self.get(v) {
486                Some(JsObj::Str(_)) => "string",
487                Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundMethod { .. }) => {
488                    "function"
489                }
490                _ => "object", // arrays, objects, null
491            },
492            _ => "object",
493        }
494    }
495
496    /// JS truthiness: false / 0 / -0 / NaN / "" / null / undefined are falsy.
497    pub fn truthy(&self, v: &Value) -> bool {
498        match v {
499            Value::Undef => false,
500            Value::Bool(b) => *b,
501            Value::Int(n) => *n != 0,
502            Value::Float(f) => *f != 0.0 && !f.is_nan(),
503            Value::Str(s) => !s.is_empty(),
504            Value::Obj(_) => match self.get(v) {
505                Some(JsObj::Str(s)) => !s.is_empty(),
506                Some(JsObj::Null) => false,
507                _ => true, // arrays, objects, functions
508            },
509            _ => true,
510        }
511    }
512
513    /// Coerce to a number (`ToNumber`): the arithmetic-context conversion.
514    pub fn to_number(&self, v: &Value) -> f64 {
515        match v {
516            Value::Undef => f64::NAN,
517            Value::Bool(b) => {
518                if *b {
519                    1.0
520                } else {
521                    0.0
522                }
523            }
524            Value::Int(n) => *n as f64,
525            Value::Float(f) => *f,
526            Value::Str(s) => str_to_number(s),
527            Value::Obj(_) => match self.get(v) {
528                Some(JsObj::Str(s)) => str_to_number(s),
529                Some(JsObj::Null) => 0.0,
530                Some(JsObj::Array(items)) => {
531                    // [] -> 0, [x] -> ToNumber(x), else NaN.
532                    if items.is_empty() {
533                        0.0
534                    } else if items.len() == 1 {
535                        self.to_number(&items[0])
536                    } else {
537                        f64::NAN
538                    }
539                }
540                _ => f64::NAN,
541            },
542            _ => f64::NAN,
543        }
544    }
545
546    /// `String(v)` — the string-coercion form (raw, unquoted).
547    pub fn str_of(&self, v: &Value) -> String {
548        match v {
549            Value::Undef => "undefined".into(),
550            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
551            Value::Int(n) => n.to_string(),
552            Value::Float(f) => fmt_number(*f),
553            Value::Str(s) => (**s).clone(),
554            Value::Obj(_) => match self.get(v) {
555                Some(JsObj::Str(s)) => s.clone(),
556                Some(JsObj::Null) => "null".into(),
557                Some(JsObj::Array(items)) => {
558                    // Array.prototype.toString: comma-join, null/undefined -> "".
559                    let parts: Vec<String> = items
560                        .iter()
561                        .map(|x| match x {
562                            Value::Undef => String::new(),
563                            _ if self.is_null(x) => String::new(),
564                            _ => self.str_of(x),
565                        })
566                        .collect();
567                    parts.join(",")
568                }
569                Some(JsObj::Object(_)) => "[object Object]".into(),
570                Some(JsObj::Func(f)) => {
571                    let name = self.funcs.get(f.def_id).map(|d| d.name.clone()).unwrap_or_default();
572                    format!("function {name}() {{ [code] }}")
573                }
574                Some(JsObj::Builtin(n)) => format!("function {n}() {{ [native code] }}"),
575                Some(JsObj::BoundMethod { .. }) => "function () { [native code] }".into(),
576                _ => "[object Object]".into(),
577            },
578            _ => "[object Object]".into(),
579        }
580    }
581
582    /// `console.log`-style rendering of a top-level argument: bare strings print
583    /// raw; everything else uses `inspect`.
584    pub fn console_format(&self, v: &Value) -> String {
585        match v {
586            Value::Str(_) => self.str_of(v),
587            Value::Obj(_) if matches!(self.get(v), Some(JsObj::Str(_))) => self.str_of(v),
588            _ => self.inspect(v),
589        }
590    }
591
592    /// `util.inspect`-style rendering (nested; strings quoted).
593    pub fn inspect(&self, v: &Value) -> String {
594        match v {
595            Value::Undef => "undefined".into(),
596            Value::Bool(b) => if *b { "true" } else { "false" }.into(),
597            Value::Int(n) => n.to_string(),
598            // `util.inspect` distinguishes negative zero; `String(-0)` does not.
599            Value::Float(f) if *f == 0.0 && f.is_sign_negative() => "-0".into(),
600            Value::Float(f) => fmt_number(*f),
601            Value::Str(s) => quote_str(s),
602            Value::Obj(_) => match self.get(v) {
603                Some(JsObj::Str(s)) => quote_str(s),
604                Some(JsObj::Null) => "null".into(),
605                Some(JsObj::Array(items)) => {
606                    if items.is_empty() {
607                        return "[]".into();
608                    }
609                    let inner: Vec<String> = items.iter().map(|x| self.inspect(x)).collect();
610                    format!("[ {} ]", inner.join(", "))
611                }
612                Some(JsObj::Object(props)) => {
613                    if props.is_empty() {
614                        return "{}".into();
615                    }
616                    let inner: Vec<String> = props
617                        .iter()
618                        .map(|(k, val)| format!("{}: {}", fmt_key(k), self.inspect(val)))
619                        .collect();
620                    format!("{{ {} }}", inner.join(", "))
621                }
622                Some(JsObj::Func(f)) => {
623                    let name = self.funcs.get(f.def_id).map(|d| d.name.clone()).unwrap_or_default();
624                    if name.is_empty() {
625                        "[Function (anonymous)]".into()
626                    } else {
627                        format!("[Function: {name}]")
628                    }
629                }
630                Some(JsObj::Builtin(n)) => {
631                    let short = n.rsplit('.').next().unwrap_or(n);
632                    format!("[Function: {short}]")
633                }
634                Some(JsObj::BoundMethod { .. }) => "[Function (anonymous)]".into(),
635                _ => "undefined".into(),
636            },
637            _ => "undefined".into(),
638        }
639    }
640
641    // ── equality / comparison / arithmetic (numeric-hook + builtin paths) ──
642
643    /// Strict equality (`===`): same type and same value, no coercion.
644    pub fn strict_eq(&self, a: &Value, b: &Value) -> bool {
645        match (a, b) {
646            (Value::Undef, Value::Undef) => true,
647            (Value::Bool(x), Value::Bool(y)) => x == y,
648            (Value::Str(x), Value::Str(y)) => x == y,
649            _ => {
650                // Numbers (NaN !== NaN, +0 === -0).
651                let an = matches!(a, Value::Int(_) | Value::Float(_));
652                let bn = matches!(b, Value::Int(_) | Value::Float(_));
653                if an && bn {
654                    let x = self.to_number(a);
655                    let y = self.to_number(b);
656                    return x == y;
657                }
658                // Heap values.
659                if let (Some(sa), Some(sb)) = (self.as_str(a), self.as_str(b)) {
660                    return sa == sb;
661                }
662                let na = self.is_null(a);
663                let nb = self.is_null(b);
664                if na || nb {
665                    return na && nb;
666                }
667                // Reference identity for arrays/objects/functions.
668                matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
669            }
670        }
671    }
672
673    /// Whether `v` is `null` or `undefined`.
674    pub fn is_nullish(&self, v: &Value) -> bool {
675        matches!(v, Value::Undef) || self.is_null(v)
676    }
677
678    /// The ECMAScript "loose type" of `v` for the `==` algorithm: `"number"`,
679    /// `"string"` (primitive or heap string), `"boolean"`, `"undefined"`,
680    /// `"null"`, or `"object"` (array / plain object / function).
681    fn js_type(&self, v: &Value) -> &'static str {
682        match v {
683            Value::Undef => "undefined",
684            Value::Bool(_) => "boolean",
685            Value::Int(_) | Value::Float(_) => "number",
686            Value::Str(_) => "string",
687            Value::Obj(_) => match self.get(v) {
688                Some(JsObj::Str(_)) => "string",
689                Some(JsObj::Null) => "null",
690                _ => "object",
691            },
692            _ => "object",
693        }
694    }
695
696    /// Loose equality (`==`) following the ECMAScript Abstract Equality Comparison.
697    /// Objects reduce via `ToPrimitive` (which for our heap objects is always their
698    /// string `toString`), so `[0] == "0"` is `true` (string compare of `"0"`) but
699    /// `[0] == ""` is `false` — never a number coercion of the object.
700    pub fn loose_eq(&self, a: &Value, b: &Value) -> bool {
701        // Same type: identical to `===` (number==number, string==string, etc.).
702        if self.strict_eq(a, b) {
703            return true;
704        }
705        let ta = self.js_type(a);
706        let tb = self.js_type(b);
707        // null and undefined are loosely equal only to each other.
708        if self.is_nullish(a) || self.is_nullish(b) {
709            return self.is_nullish(a) && self.is_nullish(b);
710        }
711        if ta == tb {
712            // Same type but not strict-equal (and not nullish) ⇒ not equal.
713            return false;
714        }
715        // number ⇄ string: compare as numbers.
716        if (ta == "number" && tb == "string") || (ta == "string" && tb == "number") {
717            return self.to_number(a) == self.to_number(b);
718        }
719        // boolean side coerces to number, then recompares.
720        if ta == "boolean" {
721            return self.loose_eq(&Value::Float(self.to_number(a)), b);
722        }
723        if tb == "boolean" {
724            return self.loose_eq(a, &Value::Float(self.to_number(b)));
725        }
726        // object ⇄ (number|string): ToPrimitive the object (→ its string form),
727        // then recompare as string==string or number==string.
728        if ta == "object" && (tb == "number" || tb == "string") {
729            let pa = self.str_of(a);
730            return if tb == "string" {
731                pa == self.str_of(b)
732            } else {
733                str_to_number(&pa) == self.to_number(b)
734            };
735        }
736        if tb == "object" && (ta == "number" || ta == "string") {
737            let pb = self.str_of(b);
738            return if ta == "string" {
739                self.str_of(a) == pb
740            } else {
741                self.to_number(a) == str_to_number(&pb)
742            };
743        }
744        false
745    }
746
747    /// The numeric-hook arithmetic/relational fallback for non-native operands
748    /// (called by fusevm when at least one operand isn't `Int`/`Float`).
749    pub fn arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
750        use NumOp::*;
751        match op {
752            Add => {
753                // `+`: if either operand is a string, concatenate string forms;
754                // otherwise numeric addition.
755                let a_str = self.prefers_string(a);
756                let b_str = self.prefers_string(b);
757                if a_str || b_str {
758                    let s = format!("{}{}", self.str_of(a), self.str_of(b));
759                    Ok(self.new_str(s))
760                } else {
761                    Ok(Value::Float(self.to_number(a) + self.to_number(b)))
762                }
763            }
764            Sub => Ok(Value::Float(self.to_number(a) - self.to_number(b))),
765            Mul => Ok(Value::Float(self.to_number(a) * self.to_number(b))),
766            Div => Ok(Value::Float(self.to_number(a) / self.to_number(b))),
767            Mod => Ok(Value::Float(js_mod(self.to_number(a), self.to_number(b)))),
768            Pow => Ok(Value::Float(self.to_number(a).powf(self.to_number(b)))),
769            Neg => Ok(Value::Float(-self.to_number(a))),
770            Lt | Le | Gt | Ge => Ok(Value::Bool(self.relational(op, a, b))),
771            Eq => Ok(Value::Bool(self.loose_eq(a, b))),
772            Ne => Ok(Value::Bool(!self.loose_eq(a, b))),
773        }
774    }
775
776    /// Whether `v`'s primitive (`ToPrimitive` with the default hint) is a string,
777    /// which drives `+` toward concatenation. Primitive strings qualify, and so
778    /// do heap objects whose default `ToPrimitive` is their (string) `toString`:
779    /// arrays (`[1,2,3]+3 → "1,2,33"`), plain objects (`{}+[] → "[object Object]"`),
780    /// and functions. `null`/`undefined`/`boolean`/`number` do not.
781    fn prefers_string(&self, v: &Value) -> bool {
782        match v {
783            Value::Str(_) => true,
784            Value::Obj(_) => !matches!(self.get(v), Some(JsObj::Null) | None),
785            _ => false,
786        }
787    }
788
789    /// Relational comparison (`< <= > >=`) with JS coercion: string/string is
790    /// lexicographic, otherwise numeric (NaN yields false).
791    fn relational(&self, op: NumOp, a: &Value, b: &Value) -> bool {
792        use std::cmp::Ordering;
793        let ord = if let (Some(x), Some(y)) = (self.as_str(a), self.as_str(b)) {
794            x.cmp(&y)
795        } else {
796            let x = self.to_number(a);
797            let y = self.to_number(b);
798            match x.partial_cmp(&y) {
799                Some(o) => o,
800                None => return false, // NaN operand
801            }
802        };
803        match op {
804            NumOp::Lt => ord == Ordering::Less,
805            NumOp::Le => ord != Ordering::Greater,
806            NumOp::Gt => ord == Ordering::Greater,
807            NumOp::Ge => ord != Ordering::Less,
808            _ => false,
809        }
810    }
811
812    /// Bitwise/shift ops with JS ToInt32/ToUint32 semantics.
813    pub fn bitwise(&self, tag: i64, a: &Value, b: &Value) -> Value {
814        let x = to_int32(self.to_number(a));
815        let y = to_int32(self.to_number(b));
816        let r: i64 = match tag {
817            binop::BITAND => (x & y) as i64,
818            binop::BITOR => (x | y) as i64,
819            binop::BITXOR => (x ^ y) as i64,
820            binop::SHL => (x.wrapping_shl((y as u32) & 31)) as i64,
821            binop::SHR => (x >> ((y as u32) & 31)) as i64,
822            binop::USHR => (to_uint32(self.to_number(a)) >> ((y as u32) & 31)) as i64,
823            _ => 0,
824        };
825        Value::Float(r as f64)
826    }
827}
828
829/// JS `%` remainder (sign follows the dividend; matches `f64::rem`).
830fn js_mod(a: f64, b: f64) -> f64 {
831    a % b
832}
833
834fn to_int32(f: f64) -> i32 {
835    if !f.is_finite() {
836        return 0;
837    }
838    let n = f.trunc();
839    (n as i64 as u32) as i32
840}
841fn to_uint32(f: f64) -> u32 {
842    if !f.is_finite() {
843        return 0;
844    }
845    f.trunc() as i64 as u32
846}
847
848/// Parse a string in numeric context (`ToNumber`): trimmed, empty -> 0.
849fn str_to_number(s: &str) -> f64 {
850    let t = s.trim();
851    if t.is_empty() {
852        return 0.0;
853    }
854    if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
855        return i64::from_str_radix(hex, 16).map(|n| n as f64).unwrap_or(f64::NAN);
856    }
857    match t {
858        "Infinity" | "+Infinity" => f64::INFINITY,
859        "-Infinity" => f64::NEG_INFINITY,
860        _ => t.parse::<f64>().unwrap_or(f64::NAN),
861    }
862}
863
864/// Quote a string the way `util.inspect` does (single quotes, escaped).
865fn quote_str(s: &str) -> String {
866    let mut out = String::from("'");
867    for c in s.chars() {
868        match c {
869            '\'' => out.push_str("\\'"),
870            '\\' => out.push_str("\\\\"),
871            '\n' => out.push_str("\\n"),
872            '\t' => out.push_str("\\t"),
873            '\r' => out.push_str("\\r"),
874            _ => out.push(c),
875        }
876    }
877    out.push('\'');
878    out
879}
880
881/// Render an object key: bare if it is a valid identifier, quoted otherwise.
882fn fmt_key(k: &str) -> String {
883    let ok = !k.is_empty()
884        && k.chars().next().map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$').unwrap_or(false)
885        && k.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
886    if ok {
887        k.to_string()
888    } else {
889        quote_str(k)
890    }
891}
892
893// ── iteration ────────────────────────────────────────────────────────────────
894
895impl JsHost {
896    /// Collect an iterable into a vector of values (arrays and strings).
897    pub fn iter_vec(&mut self, v: &Value) -> Result<Vec<Value>, String> {
898        match self.get(v) {
899            Some(JsObj::Array(items)) => Ok(items.clone()),
900            Some(JsObj::Str(s)) => {
901                let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
902                Ok(chars.into_iter().map(|c| self.new_str(c)).collect())
903            }
904            Some(JsObj::Iter { items, idx }) => Ok(items[*idx..].to_vec()),
905            _ => Err(type_error(&format!(
906                "{} is not iterable",
907                self.type_of(v)
908            ))),
909        }
910    }
911
912    /// Enumerable keys of an object/array (for `for-in`).
913    pub fn enum_keys(&mut self, v: &Value) -> Vec<Value> {
914        let keys: Vec<String> = match self.get(v) {
915            Some(JsObj::Object(props)) => props.keys().cloned().collect(),
916            Some(JsObj::Array(items)) => (0..items.len()).map(|i| i.to_string()).collect(),
917            _ => Vec::new(),
918        };
919        keys.into_iter().map(|k| self.new_str(k)).collect()
920    }
921}
922
923// ── function invocation ──────────────────────────────────────────────────────
924
925/// Resolve a bare name and call it (`f(args)`, `parseInt(args)`).
926pub fn call_named(name: &str, args: Vec<Value>) -> Result<Value, String> {
927    if let Some(v) = with_host(|h| h.read_name(name)) {
928        return invoke(&v, args, None);
929    }
930    if crate::builtins::is_known_builtin(name) {
931        return crate::builtins::call_builtin_function(name, args);
932    }
933    Err(ref_error(name))
934}
935
936/// `recv.name(args)`.
937pub fn call_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
938    // Namespace builtins (`console`, `Math`, `JSON`, ...): dispatch by qualified
939    // name.
940    if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(recv).cloned()) {
941        let qualified = format!("{ns}.{name}");
942        if crate::builtins::is_known_builtin(&qualified) {
943            return crate::builtins::call_builtin_function(&qualified, args);
944        }
945    }
946    // A method stored as a property (object method, `this` = recv).
947    if let Some(JsObj::Object(props)) = with_host(|h| h.get(recv).cloned()) {
948        if let Some(f) = props.get(name).cloned() {
949            return invoke(&f, args, Some(recv.clone()));
950        }
951    }
952    // Type methods (array/string/number methods).
953    crate::builtins::call_type_method(recv, name, args)
954}
955
956/// Call any callable value.
957pub fn invoke(callable: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
958    let obj = with_host(|h| h.get(callable).cloned());
959    match obj {
960        Some(JsObj::Builtin(name)) => crate::builtins::call_builtin_function(&name, args),
961        Some(JsObj::Func(fv)) => run_user_func(&fv, args, this),
962        Some(JsObj::BoundMethod { recv, name }) => call_method(&recv, &name, args),
963        _ => Err(type_error(&format!(
964            "{} is not a function",
965            with_host(|h| h.str_of(callable))
966        ))),
967    }
968}
969
970/// Execute a user function/closure body on a fresh frame.
971pub fn run_user_func(fv: &FuncVal, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
972    let def = with_host(|h| h.funcs[fv.def_id].clone());
973    let env = new_env(fv.env.clone());
974    // Bind the simple/rest arg slots; destructuring + defaults run in the body
975    // prologue (compiled ahead of the user statements).
976    bind_params(&env, &def, args);
977    // Arrow functions capture `this` lexically; regular functions receive it.
978    let this_val = if fv.is_arrow {
979        fv.this.clone()
980    } else {
981        this
982    };
983    with_host(|h| {
984        h.frames.push(Frame {
985            env,
986            this_obj: this_val,
987        })
988    });
989    let r = run_chunk_on(def.chunk.clone());
990    let sig = with_host(|h| {
991        h.frames.pop();
992        h.signal.take()
993    });
994    match r {
995        Err(e) => Err(e),
996        Ok(_) => Ok(match sig {
997            Some(Signal::Return(v)) => v,
998            _ => Value::Undef,
999        }),
1000    }
1001}
1002
1003/// Bind positional args into a fresh call environment. The compiler emits the
1004/// param names in `def.params`; a `...rest` slot collects the tail as an array.
1005fn bind_params(env: &Env, def: &FuncDef, args: Vec<Value>) {
1006    let mut vars: IndexMap<String, Value> = IndexMap::new();
1007    let mut i = 0;
1008    for slot in &def.params {
1009        if slot.rest {
1010            let rest: Vec<Value> = args.get(i..).map(|s| s.to_vec()).unwrap_or_default();
1011            let arr = with_host(|h| h.new_array(rest));
1012            vars.insert(slot.name.clone(), arr);
1013        } else {
1014            let v = args.get(i).cloned().unwrap_or(Value::Undef);
1015            vars.insert(slot.name.clone(), v);
1016            i += 1;
1017        }
1018    }
1019    // `arguments` array (simple approximation).
1020    let args_arr = with_host(|h| h.new_array(args));
1021    vars.entry("arguments".to_string()).or_insert(args_arr);
1022    env.borrow_mut().vars = vars;
1023}
1024
1025/// Construct an instance with `new` — creates a fresh object, binds it as
1026/// `this`, runs the constructor, and returns the object (unless the constructor
1027/// returns its own object).
1028pub fn construct(ctor: &Value, args: Vec<Value>) -> Result<Value, String> {
1029    let inst = with_host(|h| h.new_object(IndexMap::new()));
1030    let obj = with_host(|h| h.get(ctor).cloned());
1031    match obj {
1032        Some(JsObj::Func(fv)) => {
1033            let r = run_user_func(&fv, args, Some(inst.clone()))?;
1034            // If the constructor returned an object/array, use it; else the instance.
1035            if matches!(
1036                with_host(|h| h.get(&r).cloned()),
1037                Some(JsObj::Object(_)) | Some(JsObj::Array(_))
1038            ) {
1039                Ok(r)
1040            } else {
1041                Ok(inst)
1042            }
1043        }
1044        Some(JsObj::Builtin(name)) => crate::builtins::construct_builtin(&name, args),
1045        _ => Err(type_error("not a constructor")),
1046    }
1047}