//! Register-based JavaScript virtual machine. //! //! Executes bytecode produced by the compiler. Each call frame has a register //! file, and the VM dispatches instructions in a loop. Heap-allocated objects //! (plain objects and functions) are managed by a tri-color mark-and-sweep //! garbage collector. use crate::bytecode::{Constant, Function, Op, Reg}; use crate::gc::{Gc, GcRef, Traceable}; use std::collections::HashMap; use std::fmt; // ── Heap objects (GC-managed) ──────────────────────────────── /// A GC-managed heap object: either a plain object or a function. pub enum HeapObject { Object(ObjectData), Function(FunctionData), } impl Traceable for HeapObject { fn trace(&self, visitor: &mut dyn FnMut(GcRef)) { match self { HeapObject::Object(data) => { for prop in data.properties.values() { if let Some(r) = prop.value.gc_ref() { visitor(r); } } if let Some(proto) = data.prototype { visitor(proto); } } HeapObject::Function(fdata) => { if let Some(proto) = fdata.prototype_obj { visitor(proto); } } } } } /// A property descriptor stored in an object's property map. #[derive(Clone)] pub struct Property { /// The property's value (for data properties). pub value: Value, /// Whether the value can be changed via assignment. pub writable: bool, /// Whether the property shows up in `for...in` and `Object.keys`. pub enumerable: bool, /// Whether the property can be deleted or its attributes changed. pub configurable: bool, } impl Property { /// Create a new data property with all flags set to true (the JS default for /// properties created by assignment). pub fn data(value: Value) -> Self { Self { value, writable: true, enumerable: true, configurable: true, } } /// Create a non-enumerable, non-configurable property (e.g. built-in methods). pub fn builtin(value: Value) -> Self { Self { value, writable: true, enumerable: false, configurable: false, } } } /// A JS plain object (properties stored as a HashMap with descriptors). pub struct ObjectData { pub properties: HashMap, pub prototype: Option, /// Whether new properties can be added (Object.preventExtensions). pub extensible: bool, } impl ObjectData { pub fn new() -> Self { Self { properties: HashMap::new(), prototype: None, extensible: true, } } } impl Default for ObjectData { fn default() -> Self { Self::new() } } /// A runtime function value: either bytecode or native. pub struct FunctionData { pub name: String, pub kind: FunctionKind, /// The `.prototype` property object (for use as a constructor with `instanceof`). pub prototype_obj: Option, } #[derive(Clone)] pub enum FunctionKind { /// Bytecode function. Bytecode(BytecodeFunc), /// Native (Rust) function. Native(NativeFunc), } #[derive(Clone)] pub struct BytecodeFunc { pub func: Function, } /// A native function callable from JS. #[derive(Clone)] pub struct NativeFunc { pub callback: fn(&[Value]) -> Result, } // ── JS Value ────────────────────────────────────────────────── /// A JavaScript runtime value. /// /// Primitive types (Undefined, Null, Boolean, Number, String) are stored /// inline. Objects and Functions are heap-allocated via the GC and referenced /// by a [`GcRef`] handle. #[derive(Clone)] pub enum Value { Undefined, Null, Boolean(bool), Number(f64), String(String), /// A GC-managed plain object. Object(GcRef), /// A GC-managed function. Function(GcRef), } impl fmt::Debug for Value { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Value::Undefined => write!(f, "undefined"), Value::Null => write!(f, "null"), Value::Boolean(b) => write!(f, "{b}"), Value::Number(n) => write!(f, "{n}"), Value::String(s) => write!(f, "\"{}\"", s), Value::Object(_) => write!(f, "[object Object]"), Value::Function(_) => write!(f, "[Function]"), } } } impl fmt::Display for Value { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Value::Undefined => write!(f, "undefined"), Value::Null => write!(f, "null"), Value::Boolean(b) => write!(f, "{b}"), Value::Number(n) => format_number(*n, f), Value::String(s) => write!(f, "{s}"), Value::Object(_) => write!(f, "[object Object]"), Value::Function(_) => write!(f, "function() {{ [native code] }}"), } } } /// Format a number following JS conventions (no trailing .0 for integers). fn format_number(n: f64, f: &mut fmt::Formatter<'_>) -> fmt::Result { if n.is_nan() { write!(f, "NaN") } else if n.is_infinite() { if n.is_sign_positive() { write!(f, "Infinity") } else { write!(f, "-Infinity") } } else if n == 0.0 { write!(f, "0") } else if n.fract() == 0.0 && n.abs() < 1e20 { write!(f, "{}", n as i64) } else { write!(f, "{n}") } } impl Value { /// Abstract `ToBoolean` (ECMA-262 §7.1.2). pub fn to_boolean(&self) -> bool { match self { Value::Undefined | Value::Null => false, Value::Boolean(b) => *b, Value::Number(n) => *n != 0.0 && !n.is_nan(), Value::String(s) => !s.is_empty(), Value::Object(_) | Value::Function(_) => true, } } /// Abstract `ToNumber` (ECMA-262 §7.1.3). pub fn to_number(&self) -> f64 { match self { Value::Undefined => f64::NAN, Value::Null => 0.0, Value::Boolean(true) => 1.0, Value::Boolean(false) => 0.0, Value::Number(n) => *n, Value::String(s) => { let s = s.trim(); if s.is_empty() { 0.0 } else if s == "Infinity" || s == "+Infinity" { f64::INFINITY } else if s == "-Infinity" { f64::NEG_INFINITY } else { s.parse::().unwrap_or(f64::NAN) } } Value::Object(_) | Value::Function(_) => f64::NAN, } } /// Abstract `ToString` (ECMA-262 §7.1.12). /// /// Requires `&Gc` to look up function names for `Value::Function`. pub fn to_js_string(&self, gc: &Gc) -> String { match self { Value::Undefined => "undefined".to_string(), Value::Null => "null".to_string(), Value::Boolean(true) => "true".to_string(), Value::Boolean(false) => "false".to_string(), Value::Number(n) => js_number_to_string(*n), Value::String(s) => s.clone(), Value::Object(_) => "[object Object]".to_string(), Value::Function(gc_ref) => gc .get(*gc_ref) .and_then(|obj| match obj { HeapObject::Function(f) => { Some(format!("function {}() {{ [native code] }}", f.name)) } _ => None, }) .unwrap_or_else(|| "function() { [native code] }".to_string()), } } /// `typeof` operator result. pub fn type_of(&self) -> &'static str { match self { Value::Undefined => "undefined", Value::Null => "object", // yes, this is the spec Value::Boolean(_) => "boolean", Value::Number(_) => "number", Value::String(_) => "string", Value::Object(_) => "object", Value::Function(_) => "function", } } /// Is this value nullish (null or undefined)? pub fn is_nullish(&self) -> bool { matches!(self, Value::Undefined | Value::Null) } /// Extract the `GcRef` if this value is an Object or Function. pub fn gc_ref(&self) -> Option { match self { Value::Object(r) | Value::Function(r) => Some(*r), _ => None, } } } /// Format a number as JS would. fn js_number_to_string(n: f64) -> String { if n.is_nan() { "NaN".to_string() } else if n.is_infinite() { if n.is_sign_positive() { "Infinity".to_string() } else { "-Infinity".to_string() } } else if n == 0.0 { "0".to_string() } else if n.fract() == 0.0 && n.abs() < 1e20 { format!("{}", n as i64) } else { format!("{n}") } } // ── Runtime errors ──────────────────────────────────────────── /// JavaScript runtime error types. #[derive(Debug, Clone)] pub struct RuntimeError { pub kind: ErrorKind, pub message: String, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ErrorKind { TypeError, ReferenceError, RangeError, SyntaxError, Error, } impl fmt::Display for RuntimeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match self.kind { ErrorKind::TypeError => "TypeError", ErrorKind::ReferenceError => "ReferenceError", ErrorKind::RangeError => "RangeError", ErrorKind::SyntaxError => "SyntaxError", ErrorKind::Error => "Error", }; write!(f, "{name}: {}", self.message) } } impl RuntimeError { pub fn type_error(msg: impl Into) -> Self { Self { kind: ErrorKind::TypeError, message: msg.into(), } } pub fn reference_error(msg: impl Into) -> Self { Self { kind: ErrorKind::ReferenceError, message: msg.into(), } } pub fn range_error(msg: impl Into) -> Self { Self { kind: ErrorKind::RangeError, message: msg.into(), } } /// Convert to a JS Value (an error object). Allocates through the GC. pub fn to_value(&self, gc: &mut Gc) -> Value { let mut obj = ObjectData::new(); obj.properties.insert( "message".to_string(), Property::data(Value::String(self.message.clone())), ); let name = match self.kind { ErrorKind::TypeError => "TypeError", ErrorKind::ReferenceError => "ReferenceError", ErrorKind::RangeError => "RangeError", ErrorKind::SyntaxError => "SyntaxError", ErrorKind::Error => "Error", }; obj.properties.insert( "name".to_string(), Property::data(Value::String(name.to_string())), ); Value::Object(gc.alloc(HeapObject::Object(obj))) } } // ── Property access helpers ────────────────────────────────── /// Get a property from an object, walking the prototype chain. fn gc_get_property(gc: &Gc, obj_ref: GcRef, key: &str) -> Value { let proto = { match gc.get(obj_ref) { Some(HeapObject::Object(data)) => { if let Some(prop) = data.properties.get(key) { return prop.value.clone(); } data.prototype } Some(HeapObject::Function(fdata)) => { // Functions have a `.prototype` property. if key == "prototype" { if let Some(proto_ref) = fdata.prototype_obj { return Value::Object(proto_ref); } return Value::Undefined; } None } _ => return Value::Undefined, } }; if let Some(proto_ref) = proto { gc_get_property(gc, proto_ref, key) } else { Value::Undefined } } /// Check if an object has a property (own or inherited). fn gc_has_property(gc: &Gc, obj_ref: GcRef, key: &str) -> bool { let proto = { match gc.get(obj_ref) { Some(HeapObject::Object(data)) => { if data.properties.contains_key(key) { return true; } data.prototype } _ => return false, } }; if let Some(proto_ref) = proto { gc_has_property(gc, proto_ref, key) } else { false } } /// Collect all enumerable string keys of an object (own + inherited), in proper order. /// Integer indices first (sorted numerically), then string keys in insertion order. fn gc_enumerate_keys(gc: &Gc, obj_ref: GcRef) -> Vec { let mut seen = std::collections::HashSet::new(); let mut integer_keys: Vec<(u32, String)> = Vec::new(); let mut string_keys: Vec = Vec::new(); let mut current = Some(obj_ref); while let Some(cur_ref) = current { match gc.get(cur_ref) { Some(HeapObject::Object(data)) => { for (key, prop) in &data.properties { if prop.enumerable && seen.insert(key.clone()) { if let Ok(idx) = key.parse::() { integer_keys.push((idx, key.clone())); } else { string_keys.push(key.clone()); } } } current = data.prototype; } _ => break, } } // Integer indices sorted numerically first, then string keys in collected order. integer_keys.sort_by_key(|(idx, _)| *idx); let mut result: Vec = integer_keys.into_iter().map(|(_, k)| k).collect(); result.extend(string_keys); result } /// Check if `obj_ref` is an instance of the constructor at `ctor_ref`. /// Walks the prototype chain of `obj_ref` looking for `ctor.prototype`. fn gc_instanceof(gc: &Gc, obj_ref: GcRef, ctor_ref: GcRef) -> bool { // Get the constructor's .prototype object. let ctor_proto = match gc.get(ctor_ref) { Some(HeapObject::Function(fdata)) => match fdata.prototype_obj { Some(p) => p, None => return false, }, _ => return false, }; // Walk the prototype chain of obj_ref. let mut current = match gc.get(obj_ref) { Some(HeapObject::Object(data)) => data.prototype, _ => None, }; while let Some(proto_ref) = current { if proto_ref == ctor_proto { return true; } current = match gc.get(proto_ref) { Some(HeapObject::Object(data)) => data.prototype, _ => None, }; } false } /// Get a string property (length, index access). fn string_get_property(s: &str, key: &str) -> Value { if key == "length" { Value::Number(s.len() as f64) } else if let Ok(idx) = key.parse::() { s.chars() .nth(idx) .map(|c| Value::String(c.to_string())) .unwrap_or(Value::Undefined) } else { Value::Undefined } } // ── Type conversion helpers ────────────────────────────────── /// ToInt32 (ECMA-262 §7.1.5). fn to_int32(val: &Value) -> i32 { let n = val.to_number(); if n.is_nan() || n.is_infinite() || n == 0.0 { return 0; } let i = n.trunc() as i64; (i & 0xFFFF_FFFF) as i32 } /// ToUint32 (ECMA-262 §7.1.6). fn to_uint32(val: &Value) -> u32 { let n = val.to_number(); if n.is_nan() || n.is_infinite() || n == 0.0 { return 0; } let i = n.trunc() as i64; (i & 0xFFFF_FFFF) as u32 } // ── Equality ───────────────────────────────────────────────── /// Abstract equality comparison (==) per ECMA-262 §7.2.14. fn abstract_eq(x: &Value, y: &Value) -> bool { match (x, y) { (Value::Undefined, Value::Undefined) => true, (Value::Null, Value::Null) => true, (Value::Undefined, Value::Null) | (Value::Null, Value::Undefined) => true, (Value::Number(a), Value::Number(b)) => a == b, (Value::String(a), Value::String(b)) => a == b, (Value::Boolean(a), Value::Boolean(b)) => a == b, // Number / String → convert String to Number. (Value::Number(_), Value::String(_)) => abstract_eq(x, &Value::Number(y.to_number())), (Value::String(_), Value::Number(_)) => abstract_eq(&Value::Number(x.to_number()), y), // Boolean → Number. (Value::Boolean(_), _) => abstract_eq(&Value::Number(x.to_number()), y), (_, Value::Boolean(_)) => abstract_eq(x, &Value::Number(y.to_number())), // Same GcRef → equal. (Value::Object(a), Value::Object(b)) => a == b, (Value::Function(a), Value::Function(b)) => a == b, _ => false, } } /// Strict equality comparison (===) per ECMA-262 §7.2.15. fn strict_eq(x: &Value, y: &Value) -> bool { match (x, y) { (Value::Undefined, Value::Undefined) => true, (Value::Null, Value::Null) => true, (Value::Number(a), Value::Number(b)) => a == b, (Value::String(a), Value::String(b)) => a == b, (Value::Boolean(a), Value::Boolean(b)) => a == b, // Reference identity for heap objects. (Value::Object(a), Value::Object(b)) => a == b, (Value::Function(a), Value::Function(b)) => a == b, _ => false, } } // ── Relational comparison ──────────────────────────────────── /// Abstract relational comparison. Returns false for NaN comparisons. fn abstract_relational( lhs: &Value, rhs: &Value, predicate: fn(std::cmp::Ordering) -> bool, ) -> bool { // If both are strings, compare lexicographically. if let (Value::String(a), Value::String(b)) = (lhs, rhs) { return predicate(a.cmp(b)); } // Otherwise, compare as numbers. let a = lhs.to_number(); let b = rhs.to_number(); if a.is_nan() || b.is_nan() { return false; } predicate(a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Equal)) } // ── Addition ───────────────────────────────────────────────── /// The + operator: string concat if either operand is a string, else numeric add. fn add_values(lhs: &Value, rhs: &Value, gc: &Gc) -> Value { match (lhs, rhs) { (Value::String(a), _) => Value::String(format!("{a}{}", rhs.to_js_string(gc))), (_, Value::String(b)) => Value::String(format!("{}{b}", lhs.to_js_string(gc))), _ => Value::Number(lhs.to_number() + rhs.to_number()), } } // ── Call frame ──────────────────────────────────────────────── /// A single call frame on the VM's call stack. struct CallFrame { /// The function being executed. func: Function, /// Instruction pointer (byte offset into func.code). ip: usize, /// Base register index in the VM's register file. base: usize, /// Register to write the return value into (absolute index in register file). return_reg: usize, /// Exception handler stack for this frame. exception_handlers: Vec, } /// An exception handler entry (for try/catch). struct ExceptionHandler { /// IP to jump to on exception (the catch block start). catch_ip: usize, /// Register to store the caught exception value. catch_reg: Reg, } // ── VM ─────────────────────────────────────────────────────── /// The JavaScript virtual machine. pub struct Vm { /// Register file (flat array shared across frames via base offsets). registers: Vec, /// Call stack. frames: Vec, /// Global variables. globals: HashMap, /// Garbage collector managing heap objects. pub gc: Gc, } /// Maximum register file size. const MAX_REGISTERS: usize = 4096; /// Maximum call depth. const MAX_CALL_DEPTH: usize = 512; impl Vm { pub fn new() -> Self { Self { registers: vec![Value::Undefined; 256], frames: Vec::new(), globals: HashMap::new(), gc: Gc::new(), } } /// Execute a compiled top-level function and return the completion value. pub fn execute(&mut self, func: &Function) -> Result { let reg_count = func.register_count as usize; self.ensure_registers(reg_count); self.frames.push(CallFrame { func: func.clone(), ip: 0, base: 0, return_reg: 0, exception_handlers: Vec::new(), }); self.run() } /// Ensure the register file has at least `needed` slots. fn ensure_registers(&mut self, needed: usize) { if needed > self.registers.len() { if needed > MAX_REGISTERS { return; } self.registers.resize(needed, Value::Undefined); } } /// Read a u8 from the current frame's bytecode and advance IP. #[inline] fn read_u8(frame: &mut CallFrame) -> u8 { let b = frame.func.code[frame.ip]; frame.ip += 1; b } /// Read a u16 (little-endian) from the current frame's bytecode and advance IP. #[inline] fn read_u16(frame: &mut CallFrame) -> u16 { let lo = frame.func.code[frame.ip]; let hi = frame.func.code[frame.ip + 1]; frame.ip += 2; u16::from_le_bytes([lo, hi]) } /// Read an i32 (little-endian) from the current frame's bytecode and advance IP. #[inline] fn read_i32(frame: &mut CallFrame) -> i32 { let bytes = [ frame.func.code[frame.ip], frame.func.code[frame.ip + 1], frame.func.code[frame.ip + 2], frame.func.code[frame.ip + 3], ]; frame.ip += 4; i32::from_le_bytes(bytes) } /// Collect all GcRef values reachable from the mutator (roots for GC). fn collect_roots(&self) -> Vec { let mut roots = Vec::new(); for val in &self.registers { if let Some(r) = val.gc_ref() { roots.push(r); } } for val in self.globals.values() { if let Some(r) = val.gc_ref() { roots.push(r); } } roots } /// Main dispatch loop. fn run(&mut self) -> Result { loop { let fi = self.frames.len() - 1; // Check if we've reached the end of bytecode. if self.frames[fi].ip >= self.frames[fi].func.code.len() { if self.frames.len() == 1 { self.frames.pop(); return Ok(Value::Undefined); } let old = self.frames.pop().unwrap(); self.registers[old.return_reg] = Value::Undefined; continue; } let opcode_byte = self.frames[fi].func.code[self.frames[fi].ip]; self.frames[fi].ip += 1; let Some(op) = Op::from_byte(opcode_byte) else { return Err(RuntimeError { kind: ErrorKind::Error, message: format!("unknown opcode: 0x{opcode_byte:02X}"), }); }; match op { // ── Register loads ────────────────────────────── Op::LoadConst => { let dst = Self::read_u8(&mut self.frames[fi]); let idx = Self::read_u16(&mut self.frames[fi]) as usize; let base = self.frames[fi].base; let val = match &self.frames[fi].func.constants[idx] { Constant::Number(n) => Value::Number(*n), Constant::String(s) => Value::String(s.clone()), }; self.registers[base + dst as usize] = val; } Op::LoadNull => { let dst = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; self.registers[base + dst as usize] = Value::Null; } Op::LoadUndefined => { let dst = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; self.registers[base + dst as usize] = Value::Undefined; } Op::LoadTrue => { let dst = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; self.registers[base + dst as usize] = Value::Boolean(true); } Op::LoadFalse => { let dst = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; self.registers[base + dst as usize] = Value::Boolean(false); } Op::LoadInt8 => { let dst = Self::read_u8(&mut self.frames[fi]); let val = Self::read_u8(&mut self.frames[fi]) as i8; let base = self.frames[fi].base; self.registers[base + dst as usize] = Value::Number(val as f64); } Op::Move => { let dst = Self::read_u8(&mut self.frames[fi]); let src = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let val = self.registers[base + src as usize].clone(); self.registers[base + dst as usize] = val; } // ── Global access ────────────────────────────── Op::LoadGlobal => { let dst = Self::read_u8(&mut self.frames[fi]); let name_idx = Self::read_u16(&mut self.frames[fi]) as usize; let base = self.frames[fi].base; let name = &self.frames[fi].func.names[name_idx]; let val = self.globals.get(name).cloned().unwrap_or(Value::Undefined); self.registers[base + dst as usize] = val; } Op::StoreGlobal => { let name_idx = Self::read_u16(&mut self.frames[fi]) as usize; let src = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let name = self.frames[fi].func.names[name_idx].clone(); let val = self.registers[base + src as usize].clone(); self.globals.insert(name, val); } // ── Arithmetic ───────────────────────────────── Op::Add => { let dst = Self::read_u8(&mut self.frames[fi]); let lhs_r = Self::read_u8(&mut self.frames[fi]); let rhs_r = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let result = add_values( &self.registers[base + lhs_r as usize], &self.registers[base + rhs_r as usize], &self.gc, ); self.registers[base + dst as usize] = result; } Op::Sub => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let result = self.registers[base + lhs_r].to_number() - self.registers[base + rhs_r].to_number(); self.registers[base + dst] = Value::Number(result); } Op::Mul => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let result = self.registers[base + lhs_r].to_number() * self.registers[base + rhs_r].to_number(); self.registers[base + dst] = Value::Number(result); } Op::Div => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let result = self.registers[base + lhs_r].to_number() / self.registers[base + rhs_r].to_number(); self.registers[base + dst] = Value::Number(result); } Op::Rem => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let result = self.registers[base + lhs_r].to_number() % self.registers[base + rhs_r].to_number(); self.registers[base + dst] = Value::Number(result); } Op::Exp => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let result = self.registers[base + lhs_r] .to_number() .powf(self.registers[base + rhs_r].to_number()); self.registers[base + dst] = Value::Number(result); } Op::Neg => { let dst = Self::read_u8(&mut self.frames[fi]); let src = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let result = -self.registers[base + src as usize].to_number(); self.registers[base + dst as usize] = Value::Number(result); } // ── Bitwise ──────────────────────────────────── Op::BitAnd => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let a = to_int32(&self.registers[base + lhs_r]); let b = to_int32(&self.registers[base + rhs_r]); self.registers[base + dst] = Value::Number((a & b) as f64); } Op::BitOr => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let a = to_int32(&self.registers[base + lhs_r]); let b = to_int32(&self.registers[base + rhs_r]); self.registers[base + dst] = Value::Number((a | b) as f64); } Op::BitXor => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let a = to_int32(&self.registers[base + lhs_r]); let b = to_int32(&self.registers[base + rhs_r]); self.registers[base + dst] = Value::Number((a ^ b) as f64); } Op::ShiftLeft => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let a = to_int32(&self.registers[base + lhs_r]); let b = to_uint32(&self.registers[base + rhs_r]) & 0x1F; self.registers[base + dst] = Value::Number((a << b) as f64); } Op::ShiftRight => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let a = to_int32(&self.registers[base + lhs_r]); let b = to_uint32(&self.registers[base + rhs_r]) & 0x1F; self.registers[base + dst] = Value::Number((a >> b) as f64); } Op::UShiftRight => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let a = to_uint32(&self.registers[base + lhs_r]); let b = to_uint32(&self.registers[base + rhs_r]) & 0x1F; self.registers[base + dst] = Value::Number((a >> b) as f64); } Op::BitNot => { let dst = Self::read_u8(&mut self.frames[fi]); let src = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let result = !to_int32(&self.registers[base + src as usize]); self.registers[base + dst as usize] = Value::Number(result as f64); } // ── Comparison ───────────────────────────────── Op::Eq => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let result = abstract_eq(&self.registers[base + lhs_r], &self.registers[base + rhs_r]); self.registers[base + dst] = Value::Boolean(result); } Op::StrictEq => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let result = strict_eq(&self.registers[base + lhs_r], &self.registers[base + rhs_r]); self.registers[base + dst] = Value::Boolean(result); } Op::NotEq => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let result = !abstract_eq(&self.registers[base + lhs_r], &self.registers[base + rhs_r]); self.registers[base + dst] = Value::Boolean(result); } Op::StrictNotEq => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let result = !strict_eq(&self.registers[base + lhs_r], &self.registers[base + rhs_r]); self.registers[base + dst] = Value::Boolean(result); } Op::LessThan => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let result = abstract_relational( &self.registers[base + lhs_r], &self.registers[base + rhs_r], |ord| ord == std::cmp::Ordering::Less, ); self.registers[base + dst] = Value::Boolean(result); } Op::LessEq => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let result = abstract_relational( &self.registers[base + lhs_r], &self.registers[base + rhs_r], |ord| ord != std::cmp::Ordering::Greater, ); self.registers[base + dst] = Value::Boolean(result); } Op::GreaterThan => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let result = abstract_relational( &self.registers[base + lhs_r], &self.registers[base + rhs_r], |ord| ord == std::cmp::Ordering::Greater, ); self.registers[base + dst] = Value::Boolean(result); } Op::GreaterEq => { let (dst, lhs_r, rhs_r, base) = self.read_3reg(fi); let result = abstract_relational( &self.registers[base + lhs_r], &self.registers[base + rhs_r], |ord| ord != std::cmp::Ordering::Less, ); self.registers[base + dst] = Value::Boolean(result); } // ── Logical / unary ──────────────────────────── Op::LogicalNot => { let dst = Self::read_u8(&mut self.frames[fi]); let src = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let result = !self.registers[base + src as usize].to_boolean(); self.registers[base + dst as usize] = Value::Boolean(result); } Op::TypeOf => { let dst = Self::read_u8(&mut self.frames[fi]); let src = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let t = self.registers[base + src as usize].type_of(); self.registers[base + dst as usize] = Value::String(t.to_string()); } Op::InstanceOf => { let dst = Self::read_u8(&mut self.frames[fi]); let lhs_r = Self::read_u8(&mut self.frames[fi]); let rhs_r = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let result = match ( &self.registers[base + lhs_r as usize], &self.registers[base + rhs_r as usize], ) { (Value::Object(obj_ref), Value::Function(ctor_ref)) => { gc_instanceof(&self.gc, *obj_ref, *ctor_ref) } (_, Value::Function(_)) => false, _ => { return Err(RuntimeError::type_error( "Right-hand side of instanceof is not callable", )); } }; self.registers[base + dst as usize] = Value::Boolean(result); } Op::In => { let dst = Self::read_u8(&mut self.frames[fi]); let key_r = Self::read_u8(&mut self.frames[fi]); let obj_r = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let key = self.registers[base + key_r as usize].to_js_string(&self.gc); let result = match self.registers[base + obj_r as usize] { Value::Object(gc_ref) => { Value::Boolean(gc_has_property(&self.gc, gc_ref, &key)) } _ => { return Err(RuntimeError::type_error( "Cannot use 'in' operator to search for property in non-object", )); } }; self.registers[base + dst as usize] = result; } Op::Void => { let dst = Self::read_u8(&mut self.frames[fi]); let _src = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; self.registers[base + dst as usize] = Value::Undefined; } // ── Control flow ─────────────────────────────── Op::Jump => { let offset = Self::read_i32(&mut self.frames[fi]); self.frames[fi].ip = (self.frames[fi].ip as i64 + offset as i64) as usize; } Op::JumpIfTrue => { let reg = Self::read_u8(&mut self.frames[fi]); let offset = Self::read_i32(&mut self.frames[fi]); let base = self.frames[fi].base; if self.registers[base + reg as usize].to_boolean() { self.frames[fi].ip = (self.frames[fi].ip as i64 + offset as i64) as usize; } } Op::JumpIfFalse => { let reg = Self::read_u8(&mut self.frames[fi]); let offset = Self::read_i32(&mut self.frames[fi]); let base = self.frames[fi].base; if !self.registers[base + reg as usize].to_boolean() { self.frames[fi].ip = (self.frames[fi].ip as i64 + offset as i64) as usize; } } Op::JumpIfNullish => { let reg = Self::read_u8(&mut self.frames[fi]); let offset = Self::read_i32(&mut self.frames[fi]); let base = self.frames[fi].base; if self.registers[base + reg as usize].is_nullish() { self.frames[fi].ip = (self.frames[fi].ip as i64 + offset as i64) as usize; } } // ── Functions / calls ────────────────────────── Op::Call => { let dst = Self::read_u8(&mut self.frames[fi]); let func_r = Self::read_u8(&mut self.frames[fi]); let args_start = Self::read_u8(&mut self.frames[fi]); let arg_count = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; // Extract function GcRef. let func_gc_ref = match self.registers[base + func_r as usize] { Value::Function(r) => r, _ => { let desc = self.registers[base + func_r as usize].to_js_string(&self.gc); let err = RuntimeError::type_error(format!("{desc} is not a function")); let err_val = err.to_value(&mut self.gc); if !self.handle_exception(err_val) { return Err(err); } continue; } }; // Collect arguments. let mut args = Vec::with_capacity(arg_count as usize); for i in 0..arg_count { args.push(self.registers[base + (args_start + i) as usize].clone()); } // Read function data from GC (scoped borrow). let call_info = { match self.gc.get(func_gc_ref) { Some(HeapObject::Function(fdata)) => match &fdata.kind { FunctionKind::Native(n) => CallInfo::Native(n.callback), FunctionKind::Bytecode(bc) => CallInfo::Bytecode(bc.func.clone()), }, _ => { let err = RuntimeError::type_error("not a function"); let err_val = err.to_value(&mut self.gc); if !self.handle_exception(err_val) { return Err(err); } continue; } } }; match call_info { CallInfo::Native(callback) => match callback(&args) { Ok(val) => { self.registers[base + dst as usize] = val; } Err(err) => { let err_val = err.to_value(&mut self.gc); if !self.handle_exception(err_val) { return Err(err); } } }, CallInfo::Bytecode(callee_func) => { if self.frames.len() >= MAX_CALL_DEPTH { let err = RuntimeError::range_error("Maximum call stack size exceeded"); let err_val = err.to_value(&mut self.gc); if !self.handle_exception(err_val) { return Err(err); } continue; } let callee_base = base + self.frames[fi].func.register_count as usize; let callee_regs = callee_func.register_count as usize; self.ensure_registers(callee_base + callee_regs); // Copy arguments into callee's registers. for i in 0..callee_func.param_count.min(arg_count) { self.registers[callee_base + i as usize] = args[i as usize].clone(); } // Fill remaining params with undefined. for i in arg_count..callee_func.param_count { self.registers[callee_base + i as usize] = Value::Undefined; } self.frames.push(CallFrame { func: callee_func, ip: 0, base: callee_base, return_reg: base + dst as usize, exception_handlers: Vec::new(), }); } } } Op::Return => { let reg = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let val = self.registers[base + reg as usize].clone(); if self.frames.len() == 1 { self.frames.pop(); return Ok(val); } let old = self.frames.pop().unwrap(); self.registers[old.return_reg] = val; } Op::Throw => { let reg = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let val = self.registers[base + reg as usize].clone(); if !self.handle_exception(val) { let msg = self.registers[base + reg as usize].to_js_string(&self.gc); return Err(RuntimeError { kind: ErrorKind::Error, message: msg, }); } } Op::CreateClosure => { let dst = Self::read_u8(&mut self.frames[fi]); let func_idx = Self::read_u16(&mut self.frames[fi]) as usize; let base = self.frames[fi].base; let inner_func = self.frames[fi].func.functions[func_idx].clone(); let name = inner_func.name.clone(); // Create a .prototype object for the function (for instanceof). let proto_obj = self.gc.alloc(HeapObject::Object(ObjectData::new())); let gc_ref = self.gc.alloc(HeapObject::Function(FunctionData { name, kind: FunctionKind::Bytecode(BytecodeFunc { func: inner_func }), prototype_obj: Some(proto_obj), })); // Set .prototype.constructor = this function. if let Some(HeapObject::Object(data)) = self.gc.get_mut(proto_obj) { data.properties.insert( "constructor".to_string(), Property { value: Value::Function(gc_ref), writable: true, enumerable: false, configurable: true, }, ); } self.registers[base + dst as usize] = Value::Function(gc_ref); // Trigger GC if needed. if self.gc.should_collect() { let roots = self.collect_roots(); self.gc.collect(&roots); } } // ── Object / property ────────────────────────── Op::GetProperty => { let dst = Self::read_u8(&mut self.frames[fi]); let obj_r = Self::read_u8(&mut self.frames[fi]); let key_r = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let key = self.registers[base + key_r as usize].to_js_string(&self.gc); let val = match self.registers[base + obj_r as usize] { Value::Object(gc_ref) => gc_get_property(&self.gc, gc_ref, &key), Value::String(ref s) => string_get_property(s, &key), _ => Value::Undefined, }; self.registers[base + dst as usize] = val; } Op::SetProperty => { let obj_r = Self::read_u8(&mut self.frames[fi]); let key_r = Self::read_u8(&mut self.frames[fi]); let val_r = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let key = self.registers[base + key_r as usize].to_js_string(&self.gc); let val = self.registers[base + val_r as usize].clone(); if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { if let Some(prop) = data.properties.get_mut(&key) { if prop.writable { prop.value = val; } } else { data.properties.insert(key, Property::data(val)); } } } } Op::CreateObject => { let dst = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let gc_ref = self.gc.alloc(HeapObject::Object(ObjectData::new())); self.registers[base + dst as usize] = Value::Object(gc_ref); if self.gc.should_collect() { let roots = self.collect_roots(); self.gc.collect(&roots); } } Op::CreateArray => { let dst = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let mut obj = ObjectData::new(); obj.properties.insert( "length".to_string(), Property { value: Value::Number(0.0), writable: true, enumerable: false, configurable: false, }, ); let gc_ref = self.gc.alloc(HeapObject::Object(obj)); self.registers[base + dst as usize] = Value::Object(gc_ref); if self.gc.should_collect() { let roots = self.collect_roots(); self.gc.collect(&roots); } } Op::GetPropertyByName => { let dst = Self::read_u8(&mut self.frames[fi]); let obj_r = Self::read_u8(&mut self.frames[fi]); let name_idx = Self::read_u16(&mut self.frames[fi]) as usize; let base = self.frames[fi].base; let key = self.frames[fi].func.names[name_idx].clone(); let val = match self.registers[base + obj_r as usize] { Value::Object(gc_ref) => gc_get_property(&self.gc, gc_ref, &key), Value::String(ref s) => string_get_property(s, &key), _ => Value::Undefined, }; self.registers[base + dst as usize] = val; } Op::SetPropertyByName => { let obj_r = Self::read_u8(&mut self.frames[fi]); let name_idx = Self::read_u16(&mut self.frames[fi]) as usize; let val_r = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let key = self.frames[fi].func.names[name_idx].clone(); let val = self.registers[base + val_r as usize].clone(); if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { if let Some(prop) = data.properties.get_mut(&key) { if prop.writable { prop.value = val; } } else { data.properties.insert(key, Property::data(val)); } } } } // ── Misc ─────────────────────────────────────── Op::Delete => { let dst = Self::read_u8(&mut self.frames[fi]); let obj_r = Self::read_u8(&mut self.frames[fi]); let key_r = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let key = self.registers[base + key_r as usize].to_js_string(&self.gc); let result = if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { match data.properties.get(&key) { Some(prop) if !prop.configurable => false, Some(_) => { data.properties.remove(&key); true } None => true, } } else { true } } else { true }; self.registers[base + dst as usize] = Value::Boolean(result); } Op::ForInInit => { let dst = Self::read_u8(&mut self.frames[fi]); let obj_r = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let keys = match self.registers[base + obj_r as usize] { Value::Object(gc_ref) => gc_enumerate_keys(&self.gc, gc_ref), _ => Vec::new(), }; // Store keys as an array object. let mut arr = ObjectData::new(); for (i, key) in keys.iter().enumerate() { arr.properties .insert(i.to_string(), Property::data(Value::String(key.clone()))); } arr.properties.insert( "length".to_string(), Property { value: Value::Number(keys.len() as f64), writable: true, enumerable: false, configurable: false, }, ); let gc_ref = self.gc.alloc(HeapObject::Object(arr)); self.registers[base + dst as usize] = Value::Object(gc_ref); } Op::ForInNext => { let dst_val = Self::read_u8(&mut self.frames[fi]); let dst_done = Self::read_u8(&mut self.frames[fi]); let keys_r = Self::read_u8(&mut self.frames[fi]); let idx_r = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let idx = self.registers[base + idx_r as usize].to_number() as usize; let len = match self.registers[base + keys_r as usize] { Value::Object(gc_ref) => { gc_get_property(&self.gc, gc_ref, "length").to_number() as usize } _ => 0, }; if idx >= len { self.registers[base + dst_done as usize] = Value::Boolean(true); self.registers[base + dst_val as usize] = Value::Undefined; } else { let key_str = idx.to_string(); let key = match self.registers[base + keys_r as usize] { Value::Object(gc_ref) => gc_get_property(&self.gc, gc_ref, &key_str), _ => Value::Undefined, }; self.registers[base + dst_val as usize] = key; self.registers[base + dst_done as usize] = Value::Boolean(false); } } Op::SetPrototype => { let obj_r = Self::read_u8(&mut self.frames[fi]); let proto_r = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let proto = match &self.registers[base + proto_r as usize] { Value::Object(r) => Some(*r), Value::Null => None, _ => None, }; if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { data.prototype = proto; } } } Op::GetPrototype => { let dst = Self::read_u8(&mut self.frames[fi]); let obj_r = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let proto = match self.registers[base + obj_r as usize] { Value::Object(gc_ref) => match self.gc.get(gc_ref) { Some(HeapObject::Object(data)) => { data.prototype.map(Value::Object).unwrap_or(Value::Null) } _ => Value::Null, }, _ => Value::Null, }; self.registers[base + dst as usize] = proto; } } } } /// Read 3 register operands and return (dst, lhs, rhs, base) as usize indices. fn read_3reg(&mut self, fi: usize) -> (usize, usize, usize, usize) { let dst = Self::read_u8(&mut self.frames[fi]) as usize; let lhs = Self::read_u8(&mut self.frames[fi]) as usize; let rhs = Self::read_u8(&mut self.frames[fi]) as usize; let base = self.frames[fi].base; (dst, lhs, rhs, base) } /// Try to find an exception handler on the call stack. /// Returns true if a handler was found (execution resumes there). fn handle_exception(&mut self, value: Value) -> bool { while let Some(frame) = self.frames.last_mut() { if let Some(handler) = frame.exception_handlers.pop() { let base = frame.base; frame.ip = handler.catch_ip; self.registers[base + handler.catch_reg as usize] = value; return true; } if self.frames.len() == 1 { break; } self.frames.pop(); } false } /// Register a native function as a global. pub fn define_native( &mut self, name: &str, callback: fn(&[Value]) -> Result, ) { let gc_ref = self.gc.alloc(HeapObject::Function(FunctionData { name: name.to_string(), kind: FunctionKind::Native(NativeFunc { callback }), prototype_obj: None, })); self.globals .insert(name.to_string(), Value::Function(gc_ref)); } /// Get a global variable value. pub fn get_global(&self, name: &str) -> Option<&Value> { self.globals.get(name) } /// Set a global variable. pub fn set_global(&mut self, name: &str, val: Value) { self.globals.insert(name.to_string(), val); } } impl Default for Vm { fn default() -> Self { Self::new() } } /// Internal enum to avoid holding a GC borrow across the call setup. enum CallInfo { Native(fn(&[Value]) -> Result), Bytecode(Function), } // ── Tests ──────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; use crate::bytecode::{BytecodeBuilder, Constant, Op}; use crate::compiler; use crate::parser::Parser; /// Helper: compile and execute JS source, return the completion value. fn eval(source: &str) -> Result { let program = Parser::parse(source).expect("parse failed"); let func = compiler::compile(&program).expect("compile failed"); let mut vm = Vm::new(); vm.execute(&func) } // ── Value tests ───────────────────────────────────────── #[test] fn test_to_boolean() { let mut gc: Gc = Gc::new(); assert!(!Value::Undefined.to_boolean()); assert!(!Value::Null.to_boolean()); assert!(!Value::Boolean(false).to_boolean()); assert!(Value::Boolean(true).to_boolean()); assert!(!Value::Number(0.0).to_boolean()); assert!(!Value::Number(f64::NAN).to_boolean()); assert!(Value::Number(1.0).to_boolean()); assert!(!Value::String(String::new()).to_boolean()); assert!(Value::String("hello".to_string()).to_boolean()); let obj_ref = gc.alloc(HeapObject::Object(ObjectData::new())); assert!(Value::Object(obj_ref).to_boolean()); } #[test] fn test_to_number() { assert!(Value::Undefined.to_number().is_nan()); assert_eq!(Value::Null.to_number(), 0.0); assert_eq!(Value::Boolean(true).to_number(), 1.0); assert_eq!(Value::Boolean(false).to_number(), 0.0); assert_eq!(Value::Number(42.0).to_number(), 42.0); assert_eq!(Value::String("42".to_string()).to_number(), 42.0); assert_eq!(Value::String("".to_string()).to_number(), 0.0); assert!(Value::String("abc".to_string()).to_number().is_nan()); } #[test] fn test_type_of() { let mut gc: Gc = Gc::new(); assert_eq!(Value::Undefined.type_of(), "undefined"); assert_eq!(Value::Null.type_of(), "object"); assert_eq!(Value::Boolean(true).type_of(), "boolean"); assert_eq!(Value::Number(1.0).type_of(), "number"); assert_eq!(Value::String("hi".to_string()).type_of(), "string"); let obj_ref = gc.alloc(HeapObject::Object(ObjectData::new())); assert_eq!(Value::Object(obj_ref).type_of(), "object"); } // ── VM bytecode-level tests ───────────────────────────── #[test] fn test_load_const_number() { let mut b = BytecodeBuilder::new("".into(), 0); b.func.register_count = 1; let ci = b.add_constant(Constant::Number(42.0)); b.emit_reg_u16(Op::LoadConst, 0, ci); b.emit_reg(Op::Return, 0); let func = b.finish(); let mut vm = Vm::new(); let result = vm.execute(&func).unwrap(); match result { Value::Number(n) => assert_eq!(n, 42.0), _ => panic!("expected Number, got {result:?}"), } } #[test] fn test_arithmetic_ops() { let mut b = BytecodeBuilder::new("".into(), 0); b.func.register_count = 3; let c10 = b.add_constant(Constant::Number(10.0)); let c3 = b.add_constant(Constant::Number(3.0)); b.emit_reg_u16(Op::LoadConst, 0, c10); b.emit_reg_u16(Op::LoadConst, 1, c3); b.emit_reg3(Op::Add, 2, 0, 1); b.emit_reg(Op::Return, 2); let func = b.finish(); let mut vm = Vm::new(); match vm.execute(&func).unwrap() { Value::Number(n) => assert_eq!(n, 13.0), v => panic!("expected 13, got {v:?}"), } } #[test] fn test_string_concat() { let mut b = BytecodeBuilder::new("".into(), 0); b.func.register_count = 3; let c1 = b.add_constant(Constant::String("hello".into())); let c2 = b.add_constant(Constant::String(" world".into())); b.emit_reg_u16(Op::LoadConst, 0, c1); b.emit_reg_u16(Op::LoadConst, 1, c2); b.emit_reg3(Op::Add, 2, 0, 1); b.emit_reg(Op::Return, 2); let func = b.finish(); let mut vm = Vm::new(); match vm.execute(&func).unwrap() { Value::String(s) => assert_eq!(s, "hello world"), v => panic!("expected string, got {v:?}"), } } #[test] fn test_jump_if_false() { let mut b = BytecodeBuilder::new("".into(), 0); b.func.register_count = 2; b.emit_reg(Op::LoadFalse, 0); let patch = b.emit_cond_jump(Op::JumpIfFalse, 0); b.emit_load_int8(1, 1); let skip = b.emit_jump(Op::Jump); b.patch_jump(patch); b.emit_load_int8(1, 2); b.patch_jump(skip); b.emit_reg(Op::Return, 1); let func = b.finish(); let mut vm = Vm::new(); match vm.execute(&func).unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_globals() { let mut b = BytecodeBuilder::new("".into(), 0); b.func.register_count = 2; let name = b.add_name("x"); b.emit_load_int8(0, 42); b.emit_store_global(name, 0); b.emit_load_global(1, name); b.emit_reg(Op::Return, 1); let func = b.finish(); let mut vm = Vm::new(); match vm.execute(&func).unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_function_call() { let mut inner_b = BytecodeBuilder::new("add1".into(), 1); inner_b.func.register_count = 2; inner_b.emit_load_int8(1, 1); inner_b.emit_reg3(Op::Add, 0, 0, 1); inner_b.emit_reg(Op::Return, 0); let inner = inner_b.finish(); let mut b = BytecodeBuilder::new("".into(), 0); b.func.register_count = 4; let fi = b.add_function(inner); b.emit_reg_u16(Op::CreateClosure, 0, fi); b.emit_load_int8(1, 10); b.emit_call(2, 0, 1, 1); b.emit_reg(Op::Return, 2); let func = b.finish(); let mut vm = Vm::new(); match vm.execute(&func).unwrap() { Value::Number(n) => assert_eq!(n, 11.0), v => panic!("expected 11, got {v:?}"), } } #[test] fn test_native_function() { let mut b = BytecodeBuilder::new("".into(), 0); b.func.register_count = 3; let name = b.add_name("double"); b.emit_load_global(0, name); b.emit_load_int8(1, 21); b.emit_call(2, 0, 1, 1); b.emit_reg(Op::Return, 2); let func = b.finish(); let mut vm = Vm::new(); vm.define_native("double", |args| { let n = args.first().unwrap_or(&Value::Undefined).to_number(); Ok(Value::Number(n * 2.0)) }); match vm.execute(&func).unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_object_property() { let mut b = BytecodeBuilder::new("".into(), 0); b.func.register_count = 3; let name = b.add_name("x"); b.emit_reg(Op::CreateObject, 0); b.emit_load_int8(1, 42); b.emit_set_prop_name(0, name, 1); b.emit_get_prop_name(2, 0, name); b.emit_reg(Op::Return, 2); let func = b.finish(); let mut vm = Vm::new(); match vm.execute(&func).unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_typeof_operator() { let mut b = BytecodeBuilder::new("".into(), 0); b.func.register_count = 2; b.emit_load_int8(0, 5); b.emit_reg_reg(Op::TypeOf, 1, 0); b.emit_reg(Op::Return, 1); let func = b.finish(); let mut vm = Vm::new(); match vm.execute(&func).unwrap() { Value::String(s) => assert_eq!(s, "number"), v => panic!("expected 'number', got {v:?}"), } } #[test] fn test_comparison() { let mut b = BytecodeBuilder::new("".into(), 0); b.func.register_count = 3; b.emit_load_int8(0, 5); b.emit_load_int8(1, 10); b.emit_reg3(Op::LessThan, 2, 0, 1); b.emit_reg(Op::Return, 2); let func = b.finish(); let mut vm = Vm::new(); match vm.execute(&func).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_abstract_equality() { assert!(abstract_eq(&Value::Null, &Value::Undefined)); assert!(abstract_eq(&Value::Undefined, &Value::Null)); assert!(abstract_eq( &Value::Number(1.0), &Value::String("1".to_string()) )); assert!(abstract_eq(&Value::Boolean(true), &Value::Number(1.0))); assert!(!abstract_eq(&Value::Number(0.0), &Value::Null)); } #[test] fn test_strict_equality() { assert!(strict_eq(&Value::Number(1.0), &Value::Number(1.0))); assert!(!strict_eq( &Value::Number(1.0), &Value::String("1".to_string()) )); assert!(strict_eq(&Value::Null, &Value::Null)); assert!(!strict_eq(&Value::Null, &Value::Undefined)); } #[test] fn test_bitwise_ops() { let mut b = BytecodeBuilder::new("".into(), 0); b.func.register_count = 3; b.emit_load_int8(0, 0x0F); b.emit_load_int8(1, 0x03); b.emit_reg3(Op::BitAnd, 2, 0, 1); b.emit_reg(Op::Return, 2); let func = b.finish(); let mut vm = Vm::new(); match vm.execute(&func).unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } } #[test] fn test_throw_uncaught() { let mut b = BytecodeBuilder::new("".into(), 0); b.func.register_count = 1; let ci = b.add_constant(Constant::String("oops".into())); b.emit_reg_u16(Op::LoadConst, 0, ci); b.emit_reg(Op::Throw, 0); let func = b.finish(); let mut vm = Vm::new(); let err = vm.execute(&func).unwrap_err(); assert_eq!(err.message, "oops"); } #[test] fn test_loop_counting() { let mut b = BytecodeBuilder::new("".into(), 0); b.func.register_count = 3; b.emit_load_int8(0, 0); b.emit_load_int8(1, 5); let loop_start = b.offset(); b.emit_reg3(Op::LessThan, 2, 0, 1); let exit_patch = b.emit_cond_jump(Op::JumpIfFalse, 2); b.emit_load_int8(2, 1); b.emit_reg3(Op::Add, 0, 0, 2); b.emit_jump_to(loop_start); b.patch_jump(exit_patch); b.emit_reg(Op::Return, 0); let func = b.finish(); let mut vm = Vm::new(); match vm.execute(&func).unwrap() { Value::Number(n) => assert_eq!(n, 5.0), v => panic!("expected 5, got {v:?}"), } } // ── End-to-end (compile + execute) tests ──────────────── #[test] fn test_e2e_arithmetic() { match eval("2 + 3 * 4").unwrap() { Value::Number(n) => assert_eq!(n, 14.0), v => panic!("expected 14, got {v:?}"), } } #[test] fn test_e2e_variables() { match eval("var x = 10; var y = 20; x + y").unwrap() { Value::Number(n) => assert_eq!(n, 30.0), v => panic!("expected 30, got {v:?}"), } } #[test] fn test_e2e_if_else() { match eval("var x = 5; if (x > 3) { x = 100; } x").unwrap() { Value::Number(n) => assert_eq!(n, 100.0), v => panic!("expected 100, got {v:?}"), } } #[test] fn test_e2e_while_loop() { match eval("var i = 0; var sum = 0; while (i < 10) { sum = sum + i; i = i + 1; } sum") .unwrap() { Value::Number(n) => assert_eq!(n, 45.0), v => panic!("expected 45, got {v:?}"), } } #[test] fn test_e2e_function_call() { match eval("function add(a, b) { return a + b; } add(3, 4)").unwrap() { Value::Number(n) => assert_eq!(n, 7.0), v => panic!("expected 7, got {v:?}"), } } #[test] fn test_e2e_recursive_factorial() { let src = r#" function fact(n) { if (n <= 1) return 1; return n * fact(n - 1); } fact(6) "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 720.0), v => panic!("expected 720, got {v:?}"), } } #[test] fn test_e2e_string_concat() { match eval("'hello' + ' ' + 'world'").unwrap() { Value::String(s) => assert_eq!(s, "hello world"), v => panic!("expected 'hello world', got {v:?}"), } } #[test] fn test_e2e_comparison_coercion() { match eval("1 == '1'").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("1 === '1'").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_e2e_typeof() { match eval("typeof 42").unwrap() { Value::String(s) => assert_eq!(s, "number"), v => panic!("expected 'number', got {v:?}"), } match eval("typeof 'hello'").unwrap() { Value::String(s) => assert_eq!(s, "string"), v => panic!("expected 'string', got {v:?}"), } } #[test] fn test_e2e_object_literal() { match eval("var o = { x: 10, y: 20 }; o.x + o.y").unwrap() { Value::Number(n) => assert_eq!(n, 30.0), v => panic!("expected 30, got {v:?}"), } } #[test] fn test_e2e_for_loop() { match eval("var s = 0; for (var i = 0; i < 5; i = i + 1) { s = s + i; } s").unwrap() { Value::Number(n) => assert_eq!(n, 10.0), v => panic!("expected 10, got {v:?}"), } } #[test] fn test_e2e_nested_functions() { // Note: closures (capturing parent scope vars) not yet supported. // This test verifies nested function declarations and calls work. let src = r#" function outer(x) { function inner(y) { return y + 1; } return inner(x); } outer(5) "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 6.0), v => panic!("expected 6, got {v:?}"), } } #[test] fn test_e2e_logical_operators() { match eval("true && false").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } match eval("false || true").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_e2e_unary_neg() { match eval("-(5 + 3)").unwrap() { Value::Number(n) => assert_eq!(n, -8.0), v => panic!("expected -8, got {v:?}"), } } #[test] fn test_e2e_ternary() { match eval("true ? 1 : 2").unwrap() { Value::Number(n) => assert_eq!(n, 1.0), v => panic!("expected 1, got {v:?}"), } match eval("false ? 1 : 2").unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_e2e_fibonacci() { let src = r#" function fib(n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); } fib(10) "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 55.0), v => panic!("expected 55, got {v:?}"), } } // ── GC integration tests ──────────────────────────────── #[test] fn test_gc_object_survives_collection() { let src = r#" var o = { x: 42 }; o.x "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_gc_many_objects() { // Allocate many objects to trigger GC threshold. let src = r#" var sum = 0; var i = 0; while (i < 100) { var o = { val: i }; sum = sum + o.val; i = i + 1; } sum "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 4950.0), v => panic!("expected 4950, got {v:?}"), } } #[test] fn test_gc_reference_identity() { // With GC, object assignment is by reference. let mut gc: Gc = Gc::new(); let r = gc.alloc(HeapObject::Object(ObjectData::new())); let a = Value::Object(r); let b = a.clone(); assert!(strict_eq(&a, &b)); // Same GcRef → strict equal. } // ── Object model tests ────────────────────────────────── #[test] fn test_prototype_chain_lookup() { // Property lookup walks the prototype chain. let src = r#" function Animal() {} var a = {}; a.sound = "woof"; a.sound "#; match eval(src).unwrap() { Value::String(s) => assert_eq!(s, "woof"), v => panic!("expected 'woof', got {v:?}"), } } #[test] fn test_typeof_all_types() { // typeof returns correct strings for all types. let cases = [ ("typeof undefined", "undefined"), ("typeof null", "object"), ("typeof true", "boolean"), ("typeof 42", "number"), ("typeof 'hello'", "string"), ("typeof {}", "object"), ("typeof function(){}", "function"), ]; for (src, expected) in cases { match eval(src).unwrap() { Value::String(s) => assert_eq!(s, expected, "typeof failed for: {src}"), v => panic!("expected string for {src}, got {v:?}"), } } } #[test] fn test_instanceof_basic() { let src = r#" function Foo() {} var f = {}; f instanceof Foo "#; // Plain object without prototype link → false match eval(src).unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_in_operator() { let src = r#" var o = { x: 1, y: 2 }; var r1 = "x" in o; var r2 = "z" in o; r1 === true && r2 === false "#; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_delete_property() { let src = r#" var o = { x: 1, y: 2 }; delete o.x; typeof o.x === "undefined" && o.y === 2 "#; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_delete_computed_property() { let src = r#" var o = { a: 10, b: 20 }; var key = "a"; delete o[key]; typeof o.a === "undefined" && o.b === 20 "#; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_delete_non_configurable() { // Array length is non-configurable, delete should return false. let mut gc: Gc = Gc::new(); let mut obj = ObjectData::new(); obj.properties.insert( "x".to_string(), Property { value: Value::Number(1.0), writable: true, enumerable: true, configurable: false, }, ); let obj_ref = gc.alloc(HeapObject::Object(obj)); // Try to delete the non-configurable property. match gc.get_mut(obj_ref) { Some(HeapObject::Object(data)) => { let prop = data.properties.get("x").unwrap(); assert!(!prop.configurable); // The property should still be there. assert!(data.properties.contains_key("x")); } _ => panic!("expected object"), } } #[test] fn test_property_writable_flag() { // Setting a non-writable property should silently fail. let mut gc: Gc = Gc::new(); let mut obj = ObjectData::new(); obj.properties.insert( "frozen".to_string(), Property { value: Value::Number(42.0), writable: false, enumerable: true, configurable: false, }, ); let obj_ref = gc.alloc(HeapObject::Object(obj)); // Verify the property value. match gc.get(obj_ref) { Some(HeapObject::Object(data)) => { assert_eq!( data.properties.get("frozen").unwrap().value.to_number(), 42.0 ); } _ => panic!("expected object"), } } #[test] fn test_for_in_basic() { let src = r#" var o = { a: 1, b: 2, c: 3 }; var sum = 0; for (var key in o) { sum = sum + o[key]; } sum "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 6.0), v => panic!("expected 6, got {v:?}"), } } #[test] fn test_for_in_collects_keys() { let src = r#" var o = { x: 10, y: 20 }; var keys = ""; for (var k in o) { keys = keys + k + ","; } keys "#; match eval(src).unwrap() { Value::String(s) => { // Both keys should appear (order may vary with HashMap). assert!(s.contains("x,")); assert!(s.contains("y,")); } v => panic!("expected string, got {v:?}"), } } #[test] fn test_for_in_empty_object() { let src = r#" var o = {}; var count = 0; for (var k in o) { count = count + 1; } count "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 0.0), v => panic!("expected 0, got {v:?}"), } } #[test] fn test_property_enumerable_flag() { // Array "length" is non-enumerable, should not appear in for-in. let src = r#" var arr = [10, 20, 30]; var keys = ""; for (var k in arr) { keys = keys + k + ","; } keys "#; match eval(src).unwrap() { Value::String(s) => { // "length" should NOT be in the keys (it's non-enumerable). assert!(!s.contains("length")); // Array indices should be present. assert!(s.contains("0,")); assert!(s.contains("1,")); assert!(s.contains("2,")); } v => panic!("expected string, got {v:?}"), } } #[test] fn test_object_reference_semantics() { // Objects have reference semantics (shared via GcRef). let src = r#" var a = { x: 1 }; var b = a; b.x = 42; a.x "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_gc_enumerate_keys_order() { // Integer keys should come first, sorted numerically. let mut gc: Gc = Gc::new(); let mut obj = ObjectData::new(); obj.properties .insert("b".to_string(), Property::data(Value::Number(2.0))); obj.properties .insert("2".to_string(), Property::data(Value::Number(3.0))); obj.properties .insert("a".to_string(), Property::data(Value::Number(1.0))); obj.properties .insert("0".to_string(), Property::data(Value::Number(0.0))); let obj_ref = gc.alloc(HeapObject::Object(obj)); let keys = gc_enumerate_keys(&gc, obj_ref); // Integer keys first (sorted), then string keys. let int_part: Vec<&str> = keys.iter().take(2).map(|s| s.as_str()).collect(); assert_eq!(int_part, vec!["0", "2"]); // Remaining keys are string keys (order depends on HashMap iteration). let str_part: Vec<&str> = keys.iter().skip(2).map(|s| s.as_str()).collect(); assert!(str_part.contains(&"a")); assert!(str_part.contains(&"b")); } #[test] fn test_instanceof_with_gc() { // Direct test of gc_instanceof. let mut gc: Gc = Gc::new(); // Create a constructor function with a .prototype object. let proto = gc.alloc(HeapObject::Object(ObjectData::new())); let ctor = gc.alloc(HeapObject::Function(FunctionData { name: "Foo".to_string(), kind: FunctionKind::Native(NativeFunc { callback: |_| Ok(Value::Undefined), }), prototype_obj: Some(proto), })); // Create an object whose [[Prototype]] is the constructor's .prototype. let mut obj_data = ObjectData::new(); obj_data.prototype = Some(proto); let obj = gc.alloc(HeapObject::Object(obj_data)); assert!(gc_instanceof(&gc, obj, ctor)); // An unrelated object should not match. let other = gc.alloc(HeapObject::Object(ObjectData::new())); assert!(!gc_instanceof(&gc, other, ctor)); } }