diff --git a/crates/js/src/gc.rs b/crates/js/src/gc.rs new file mode 100644 index 0000000..70f8421 --- /dev/null +++ b/crates/js/src/gc.rs @@ -0,0 +1,401 @@ +//! Tri-color mark-and-sweep garbage collector. +//! +//! Manages heap-allocated JS objects (plain objects, arrays, functions) using +//! the tri-color invariant: white (unreached), gray (reached, children not yet +//! scanned), black (fully scanned). The collector is stop-the-world and +//! triggered when live object count exceeds a configurable threshold. + +/// Mark color for tri-color marking. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Color { + /// Not yet visited — candidate for collection. + White, + /// Visited but children not yet scanned. + Gray, + /// Fully scanned — reachable and all children traced. + Black, +} + +/// A reference (handle) to a GC-managed object. +/// +/// This is a lightweight index into the GC heap. It is `Copy` so it can be +/// freely duplicated — the GC is responsible for tracking liveness. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct GcRef(u32); + +/// Trait for types that can be traced by the garbage collector. +/// +/// Implementors must report all `GcRef` values they hold so the GC can +/// traverse the object graph. +pub trait Traceable { + /// Call `visitor` once for each `GcRef` this object holds. + fn trace(&self, visitor: &mut dyn FnMut(GcRef)); +} + +/// A heap slot containing GC metadata and the object data. +struct HeapSlot { + color: Color, + data: T, +} + +/// Statistics about the GC heap. +#[derive(Debug, Clone)] +pub struct GcStats { + /// Number of live (allocated) objects. + pub live_count: usize, + /// Total heap slots (including free slots). + pub total_slots: usize, + /// Number of collections performed so far. + pub collections: usize, +} + +/// The garbage collector. +/// +/// Manages a heap of `T` objects. Objects are allocated via [`alloc`] and +/// accessed via [`get`]/[`get_mut`]. Collection is triggered manually or +/// when [`should_collect`] returns true. +pub struct Gc { + /// The heap: a vector of optional slots (None = free). + heap: Vec>>, + /// Free slot indices for O(1) allocation. + free_list: Vec, + /// Number of currently live objects. + live_count: usize, + /// Collection is suggested when live_count exceeds this. + threshold: usize, + /// Total collections performed. + collections: usize, +} + +/// Initial collection threshold. +const INITIAL_THRESHOLD: usize = 256; + +impl Gc { + /// Create a new, empty GC heap. + pub fn new() -> Self { + Self { + heap: Vec::new(), + free_list: Vec::new(), + live_count: 0, + threshold: INITIAL_THRESHOLD, + collections: 0, + } + } + + /// Allocate a new object on the heap, returning a `GcRef` handle. + pub fn alloc(&mut self, data: T) -> GcRef { + let slot = HeapSlot { + color: Color::White, + data, + }; + let idx = if let Some(free_idx) = self.free_list.pop() { + let i = free_idx as usize; + self.heap[i] = Some(slot); + free_idx + } else { + let i = self.heap.len(); + self.heap.push(Some(slot)); + i as u32 + }; + self.live_count += 1; + GcRef(idx) + } + + /// Get an immutable reference to the object at `r`. + /// + /// Returns `None` if the slot has been freed (stale reference). + pub fn get(&self, r: GcRef) -> Option<&T> { + let idx = r.0 as usize; + self.heap + .get(idx) + .and_then(|slot| slot.as_ref().map(|s| &s.data)) + } + + /// Get a mutable reference to the object at `r`. + /// + /// Returns `None` if the slot has been freed (stale reference). + pub fn get_mut(&mut self, r: GcRef) -> Option<&mut T> { + let idx = r.0 as usize; + self.heap + .get_mut(idx) + .and_then(|slot| slot.as_mut().map(|s| &mut s.data)) + } + + /// Returns `true` if a collection should be triggered. + pub fn should_collect(&self) -> bool { + self.live_count >= self.threshold + } + + /// Perform a garbage collection pass. + /// + /// `roots` must contain every `GcRef` reachable from the mutator (VM + /// registers, globals, call stack). Any object not transitively reachable + /// from a root is freed. + pub fn collect(&mut self, roots: &[GcRef]) { + self.mark(roots); + self.sweep(); + self.collections += 1; + + // Grow threshold so we don't collect too often. + if self.live_count >= self.threshold { + self.threshold = self.live_count * 2; + } + } + + /// Return heap statistics. + pub fn stats(&self) -> GcStats { + GcStats { + live_count: self.live_count, + total_slots: self.heap.len(), + collections: self.collections, + } + } + + /// Mark phase: starting from roots, color all reachable objects black. + fn mark(&mut self, roots: &[GcRef]) { + let mut gray_stack: Vec = Vec::new(); + + // Color roots gray. + for &root in roots { + let idx = root.0 as usize; + if idx < self.heap.len() { + if let Some(entry) = &mut self.heap[idx] { + if entry.color == Color::White { + entry.color = Color::Gray; + gray_stack.push(root); + } + } + } + } + + // Process the gray stack. + while let Some(gc_ref) = gray_stack.pop() { + let idx = gc_ref.0 as usize; + + // Collect child references (scoped borrow). + let children = { + let mut c = Vec::new(); + if let Some(entry) = &self.heap[idx] { + entry.data.trace(&mut |child| c.push(child)); + } + c + }; + + // Mark current object black. + if let Some(entry) = &mut self.heap[idx] { + entry.color = Color::Black; + } + + // Mark white children gray. + for child in children { + let cidx = child.0 as usize; + if cidx < self.heap.len() { + if let Some(entry) = &mut self.heap[cidx] { + if entry.color == Color::White { + entry.color = Color::Gray; + gray_stack.push(child); + } + } + } + } + } + } + + /// Sweep phase: free all white objects, reset black objects to white. + fn sweep(&mut self) { + for i in 0..self.heap.len() { + let should_free = match &self.heap[i] { + Some(entry) => entry.color == Color::White, + None => false, + }; + if should_free { + self.heap[i] = None; + self.free_list.push(i as u32); + self.live_count -= 1; + } else if let Some(entry) = &mut self.heap[i] { + // Reset black → white for next cycle. + entry.color = Color::White; + } + } + } +} + +impl Default for Gc { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A simple test object that holds GcRef children. + struct TestObj { + children: Vec, + } + + impl Traceable for TestObj { + fn trace(&self, visitor: &mut dyn FnMut(GcRef)) { + for &child in &self.children { + visitor(child); + } + } + } + + #[test] + fn test_alloc_and_get() { + let mut gc = Gc::new(); + let r = gc.alloc(TestObj { + children: Vec::new(), + }); + assert!(gc.get(r).is_some()); + assert_eq!(gc.stats().live_count, 1); + } + + #[test] + fn test_collect_unreachable() { + let mut gc = Gc::new(); + let r1 = gc.alloc(TestObj { + children: Vec::new(), + }); + let r2 = gc.alloc(TestObj { + children: Vec::new(), + }); + assert_eq!(gc.stats().live_count, 2); + + // Collect with no roots — everything is freed. + gc.collect(&[]); + assert_eq!(gc.stats().live_count, 0); + assert!(gc.get(r1).is_none()); + assert!(gc.get(r2).is_none()); + } + + #[test] + fn test_collect_reachable() { + let mut gc = Gc::new(); + let r1 = gc.alloc(TestObj { + children: Vec::new(), + }); + let r2 = gc.alloc(TestObj { + children: Vec::new(), + }); + assert_eq!(gc.stats().live_count, 2); + + // Only r1 is a root. + gc.collect(&[r1]); + assert_eq!(gc.stats().live_count, 1); + assert!(gc.get(r1).is_some()); + assert!(gc.get(r2).is_none()); + } + + #[test] + fn test_collect_transitive() { + let mut gc = Gc::new(); + let r1 = gc.alloc(TestObj { + children: Vec::new(), + }); + let r2 = gc.alloc(TestObj { + children: Vec::new(), + }); + let r3 = gc.alloc(TestObj { + children: Vec::new(), + }); + + // r1 -> r2 -> r3 + gc.get_mut(r1).unwrap().children.push(r2); + gc.get_mut(r2).unwrap().children.push(r3); + + gc.collect(&[r1]); + assert_eq!(gc.stats().live_count, 3); + assert!(gc.get(r1).is_some()); + assert!(gc.get(r2).is_some()); + assert!(gc.get(r3).is_some()); + } + + #[test] + fn test_cycle_collection() { + let mut gc = Gc::new(); + let r1 = gc.alloc(TestObj { + children: Vec::new(), + }); + let r2 = gc.alloc(TestObj { + children: Vec::new(), + }); + + // Create a cycle: r1 -> r2 -> r1. + gc.get_mut(r1).unwrap().children.push(r2); + gc.get_mut(r2).unwrap().children.push(r1); + + // With r1 as root, both survive. + gc.collect(&[r1]); + assert_eq!(gc.stats().live_count, 2); + + // With no roots, both are freed (cycle is collected). + gc.collect(&[]); + assert_eq!(gc.stats().live_count, 0); + } + + #[test] + fn test_free_list_reuse() { + let mut gc = Gc::new(); + let _r1 = gc.alloc(TestObj { + children: Vec::new(), + }); + let _r2 = gc.alloc(TestObj { + children: Vec::new(), + }); + + // Free both. + gc.collect(&[]); + assert_eq!(gc.stats().live_count, 0); + assert_eq!(gc.stats().total_slots, 2); + + // Allocate again — should reuse freed slots. + let _r3 = gc.alloc(TestObj { + children: Vec::new(), + }); + let _r4 = gc.alloc(TestObj { + children: Vec::new(), + }); + assert_eq!(gc.stats().live_count, 2); + assert_eq!(gc.stats().total_slots, 2); // No growth. + } + + #[test] + fn test_should_collect_threshold() { + let mut gc: Gc = Gc::new(); + assert!(!gc.should_collect()); + + // Allocate up to threshold. + for _ in 0..INITIAL_THRESHOLD { + gc.alloc(TestObj { + children: Vec::new(), + }); + } + assert!(gc.should_collect()); + } + + #[test] + fn test_stress() { + let mut gc = Gc::new(); + let mut live_refs = Vec::new(); + + for i in 0..1000 { + let r = gc.alloc(TestObj { + children: Vec::new(), + }); + if i % 3 == 0 { + live_refs.push(r); + } + } + + gc.collect(&live_refs); + // Only refs kept in live_refs should survive. + assert_eq!(gc.stats().live_count, live_refs.len()); + + for r in &live_refs { + assert!(gc.get(*r).is_some()); + } + } +} diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index db40ff8..5be85b9 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -3,6 +3,7 @@ pub mod ast; pub mod bytecode; pub mod compiler; +pub mod gc; pub mod lexer; pub mod parser; pub mod vm; @@ -40,5 +41,5 @@ pub fn evaluate(source: &str) -> Result { let result = engine .execute(&func) .map_err(|e| JsError::RuntimeError(e.to_string()))?; - Ok(result.to_string()) + Ok(result.to_js_string(&engine.gc)) } diff --git a/crates/js/src/vm.rs b/crates/js/src/vm.rs index a02448c..89b0551 100644 --- a/crates/js/src/vm.rs +++ b/crates/js/src/vm.rs @@ -1,15 +1,96 @@ //! 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. +//! 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 val in data.properties.values() { + if let Some(r) = val.gc_ref() { + visitor(r); + } + } + if let Some(proto) = data.prototype { + visitor(proto); + } + } + HeapObject::Function(_) => { + // Bytecode and native callbacks don't hold GC references. + } + } + } +} + +/// A JS plain object (properties stored as a HashMap). +pub struct ObjectData { + pub properties: HashMap, + pub prototype: Option, +} + +impl ObjectData { + pub fn new() -> Self { + Self { + properties: HashMap::new(), + prototype: None, + } + } +} + +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, +} + +#[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, @@ -17,8 +98,10 @@ pub enum Value { Boolean(bool), Number(f64), String(String), - Object(Object), - Function(FunctionValue), + /// A GC-managed plain object. + Object(GcRef), + /// A GC-managed function. + Function(GcRef), } impl fmt::Debug for Value { @@ -30,7 +113,7 @@ impl fmt::Debug for Value { Value::Number(n) => write!(f, "{n}"), Value::String(s) => write!(f, "\"{}\"", s), Value::Object(_) => write!(f, "[object Object]"), - Value::Function(fv) => write!(f, "function {}()", fv.name), + Value::Function(_) => write!(f, "[Function]"), } } } @@ -44,9 +127,7 @@ impl fmt::Display for Value { Value::Number(n) => format_number(*n, f), Value::String(s) => write!(f, "{s}"), Value::Object(_) => write!(f, "[object Object]"), - Value::Function(fv) => { - write!(f, "function {}() {{ [native code] }}", fv.name) - } + Value::Function(_) => write!(f, "function() {{ [native code] }}"), } } } @@ -107,7 +188,9 @@ impl Value { } /// Abstract `ToString` (ECMA-262 §7.1.12). - pub fn to_js_string(&self) -> String { + /// + /// 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(), @@ -116,7 +199,15 @@ impl Value { Value::Number(n) => js_number_to_string(*n), Value::String(s) => s.clone(), Value::Object(_) => "[object Object]".to_string(), - Value::Function(fv) => format!("function {}() {{ [native code] }}", fv.name), + 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()), } } @@ -137,6 +228,14 @@ impl Value { 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. @@ -158,80 +257,6 @@ fn js_number_to_string(n: f64) -> String { } } -// ── Object ──────────────────────────────────────────────────── - -/// A JS plain object (properties stored as a HashMap). -#[derive(Clone, Debug)] -pub struct Object { - pub properties: HashMap, - pub prototype: Option>, -} - -impl Object { - pub fn new() -> Self { - Self { - properties: HashMap::new(), - prototype: None, - } - } - - pub fn get(&self, key: &str) -> Value { - if let Some(val) = self.properties.get(key) { - val.clone() - } else if let Some(proto) = &self.prototype { - proto.get(key) - } else { - Value::Undefined - } - } - - pub fn set(&mut self, key: String, value: Value) { - self.properties.insert(key, value); - } - - pub fn delete(&mut self, key: &str) -> bool { - self.properties.remove(key).is_some() - } - - pub fn has(&self, key: &str) -> bool { - self.properties.contains_key(key) || self.prototype.as_ref().is_some_and(|p| p.has(key)) - } -} - -impl Default for Object { - fn default() -> Self { - Self::new() - } -} - -// ── Function value ──────────────────────────────────────────── - -/// A runtime function value: either bytecode or native. -#[derive(Clone)] -pub struct FunctionValue { - pub name: String, - pub kind: FunctionKind, -} - -#[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, -} - // ── Runtime errors ──────────────────────────────────────────── /// JavaScript runtime error types. @@ -285,10 +310,11 @@ impl RuntimeError { } } - /// Convert to a JS Value (an error object). - pub fn to_value(&self) -> Value { - let mut obj = Object::new(); - obj.set("message".to_string(), Value::String(self.message.clone())); + /// 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(), Value::String(self.message.clone())); let name = match self.kind { ErrorKind::TypeError => "TypeError", ErrorKind::ReferenceError => "ReferenceError", @@ -296,8 +322,158 @@ impl RuntimeError { ErrorKind::SyntaxError => "SyntaxError", ErrorKind::Error => "Error", }; - obj.set("name".to_string(), Value::String(name.to_string())); - Value::Object(obj) + obj.properties + .insert("name".to_string(), 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(val) = data.properties.get(key) { + return val.clone(); + } + data.prototype + } + _ => 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 + } +} + +/// 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()), } } @@ -335,6 +511,8 @@ pub struct Vm { frames: Vec, /// Global variables. globals: HashMap, + /// Garbage collector managing heap objects. + pub gc: Gc, } /// Maximum register file size. @@ -348,6 +526,7 @@ impl Vm { registers: vec![Value::Undefined; 256], frames: Vec::new(), globals: HashMap::new(), + gc: Gc::new(), } } @@ -407,6 +586,22 @@ impl Vm { 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 { @@ -506,6 +701,7 @@ impl Vm { 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; } @@ -683,9 +879,11 @@ impl Vm { 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(); - let result = match &self.registers[base + obj_r as usize] { - Value::Object(obj) => Value::Boolean(obj.has(&key)), + 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", @@ -738,7 +936,21 @@ impl Vm { 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; - let func_val = self.registers[base + func_r as usize].clone(); + + // 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); @@ -746,62 +958,67 @@ impl Vm { args.push(self.registers[base + (args_start + i) as usize].clone()); } - match func_val { - Value::Function(fv) => match &fv.kind { - FunctionKind::Native(native) => match (native.callback)(&args) { - Ok(val) => { - self.registers[base + dst as usize] = val; - } - Err(err) => { - if !self.handle_exception(err.to_value()) { - return Err(err); - } - } + // 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()), }, - FunctionKind::Bytecode(bc) => { - if self.frames.len() >= MAX_CALL_DEPTH { - let err = RuntimeError::range_error( - "Maximum call stack size exceeded", - ); - if !self.handle_exception(err.to_value()) { - return Err(err); - } - continue; + _ => { + 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; + } + } + }; - let callee_func = bc.func.clone(); - 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(); + 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); } - // Fill remaining params with undefined. - for i in arg_count..callee_func.param_count { - self.registers[callee_base + i as usize] = Value::Undefined; + } + }, + 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; + } - self.frames.push(CallFrame { - func: callee_func, - ip: 0, - base: callee_base, - return_reg: base + dst as usize, - exception_handlers: Vec::new(), - }); + 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(); } - }, - _ => { - let err = RuntimeError::type_error(format!( - "{} is not a function", - func_val.to_js_string() - )); - if !self.handle_exception(err.to_value()) { - return Err(err); + // 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(), + }); } } } @@ -824,7 +1041,7 @@ impl Vm { 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(); + let msg = self.registers[base + reg as usize].to_js_string(&self.gc); return Err(RuntimeError { kind: ErrorKind::Error, message: msg, @@ -837,10 +1054,17 @@ impl Vm { let base = self.frames[fi].base; let inner_func = self.frames[fi].func.functions[func_idx].clone(); let name = inner_func.name.clone(); - self.registers[base + dst as usize] = Value::Function(FunctionValue { + let gc_ref = self.gc.alloc(HeapObject::Function(FunctionData { name, kind: FunctionKind::Bytecode(BytecodeFunc { func: inner_func }), - }); + })); + 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 ────────────────────────── @@ -849,10 +1073,10 @@ impl Vm { 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(); - let val = match &self.registers[base + obj_r as usize] { - Value::Object(obj) => obj.get(&key), - Value::String(s) => string_get_property(s, &key), + 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; @@ -862,23 +1086,38 @@ impl Vm { 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(); + 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(ref mut obj) = self.registers[base + obj_r as usize] { - obj.set(key, val); + 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.properties.insert(key, val); + } } } Op::CreateObject => { let dst = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; - self.registers[base + dst as usize] = Value::Object(Object::new()); + 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 = Object::new(); - obj.set("length".to_string(), Value::Number(0.0)); - self.registers[base + dst as usize] = Value::Object(obj); + let mut obj = ObjectData::new(); + obj.properties + .insert("length".to_string(), Value::Number(0.0)); + 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]); @@ -886,9 +1125,9 @@ impl Vm { 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(obj) => obj.get(&key), - Value::String(s) => string_get_property(s, &key), + 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; @@ -900,8 +1139,10 @@ impl Vm { 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(ref mut obj) = self.registers[base + obj_r as usize] { - obj.set(key, val); + 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.properties.insert(key, val); + } } } @@ -911,10 +1152,14 @@ impl Vm { 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(); + let key = self.registers[base + key_r as usize].to_js_string(&self.gc); let result = - if let Value::Object(ref mut obj) = self.registers[base + obj_r as usize] { - obj.delete(&key) + 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.properties.remove(&key).is_some() + } else { + true + } } else { true }; @@ -957,13 +1202,12 @@ impl Vm { name: &str, callback: fn(&[Value]) -> Result, ) { - self.globals.insert( - name.to_string(), - Value::Function(FunctionValue { - name: name.to_string(), - kind: FunctionKind::Native(NativeFunc { callback }), - }), - ); + let gc_ref = self.gc.alloc(HeapObject::Function(FunctionData { + name: name.to_string(), + kind: FunctionKind::Native(NativeFunc { callback }), + })); + self.globals + .insert(name.to_string(), Value::Function(gc_ref)); } /// Get a global variable value. @@ -983,106 +1227,10 @@ impl Default for Vm { } } -// ── String property 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())), - _ => 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, - _ => 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) -> Value { - match (lhs, rhs) { - (Value::String(a), _) => Value::String(format!("{a}{}", rhs.to_js_string())), - (_, Value::String(b)) => Value::String(format!("{}{b}", lhs.to_js_string())), - _ => Value::Number(lhs.to_number() + rhs.to_number()), - } +/// Internal enum to avoid holding a GC borrow across the call setup. +enum CallInfo { + Native(fn(&[Value]) -> Result), + Bytecode(Function), } // ── Tests ──────────────────────────────────────────────────── @@ -1106,6 +1254,7 @@ mod 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()); @@ -1115,7 +1264,8 @@ mod tests { assert!(Value::Number(1.0).to_boolean()); assert!(!Value::String(String::new()).to_boolean()); assert!(Value::String("hello".to_string()).to_boolean()); - assert!(Value::Object(Object::new()).to_boolean()); + let obj_ref = gc.alloc(HeapObject::Object(ObjectData::new())); + assert!(Value::Object(obj_ref).to_boolean()); } #[test] @@ -1132,12 +1282,14 @@ mod tests { #[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"); - assert_eq!(Value::Object(Object::new()).type_of(), "object"); + let obj_ref = gc.alloc(HeapObject::Object(ObjectData::new())); + assert_eq!(Value::Object(obj_ref).type_of(), "object"); } // ── VM bytecode-level tests ───────────────────────────── @@ -1584,4 +1736,47 @@ mod tests { 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. + } }