we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement tri-color mark-and-sweep garbage collector for JS engine

Add a generic GC module (gc.rs) with tri-color marking (white/gray/black),
heap allocation via Vec with free-list reuse, and stop-the-world collection
triggered when live object count exceeds a configurable threshold.

Refactor VM to use GcRef handles for heap objects (plain objects and
functions) instead of owned values. This gives correct JS reference
semantics — object assignment shares identity rather than deep-cloning.
The GC traces through object properties and prototype chains to find all
reachable objects, and correctly collects cycles.

Key changes:
- gc.rs: Generic Gc<T: Traceable> with alloc/get/collect, HeapSlot with
color metadata, mark phase with gray stack, sweep with free-list
- vm.rs: Value::Object(GcRef) and Value::Function(GcRef) replace owned
data; VM allocates through self.gc and triggers collection after
allocations; root collection scans registers and globals
- lib.rs: evaluate() uses to_js_string(&gc) for GC-aware string conversion

8 new GC tests (alloc, collect, transitive reachability, cycle collection,
free-list reuse, threshold, stress) plus 3 integration tests. All 226
existing tests continue to pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+870 -273
+401
crates/js/src/gc.rs
··· 1 + //! Tri-color mark-and-sweep garbage collector. 2 + //! 3 + //! Manages heap-allocated JS objects (plain objects, arrays, functions) using 4 + //! the tri-color invariant: white (unreached), gray (reached, children not yet 5 + //! scanned), black (fully scanned). The collector is stop-the-world and 6 + //! triggered when live object count exceeds a configurable threshold. 7 + 8 + /// Mark color for tri-color marking. 9 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 10 + enum Color { 11 + /// Not yet visited — candidate for collection. 12 + White, 13 + /// Visited but children not yet scanned. 14 + Gray, 15 + /// Fully scanned — reachable and all children traced. 16 + Black, 17 + } 18 + 19 + /// A reference (handle) to a GC-managed object. 20 + /// 21 + /// This is a lightweight index into the GC heap. It is `Copy` so it can be 22 + /// freely duplicated — the GC is responsible for tracking liveness. 23 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 24 + pub struct GcRef(u32); 25 + 26 + /// Trait for types that can be traced by the garbage collector. 27 + /// 28 + /// Implementors must report all `GcRef` values they hold so the GC can 29 + /// traverse the object graph. 30 + pub trait Traceable { 31 + /// Call `visitor` once for each `GcRef` this object holds. 32 + fn trace(&self, visitor: &mut dyn FnMut(GcRef)); 33 + } 34 + 35 + /// A heap slot containing GC metadata and the object data. 36 + struct HeapSlot<T> { 37 + color: Color, 38 + data: T, 39 + } 40 + 41 + /// Statistics about the GC heap. 42 + #[derive(Debug, Clone)] 43 + pub struct GcStats { 44 + /// Number of live (allocated) objects. 45 + pub live_count: usize, 46 + /// Total heap slots (including free slots). 47 + pub total_slots: usize, 48 + /// Number of collections performed so far. 49 + pub collections: usize, 50 + } 51 + 52 + /// The garbage collector. 53 + /// 54 + /// Manages a heap of `T` objects. Objects are allocated via [`alloc`] and 55 + /// accessed via [`get`]/[`get_mut`]. Collection is triggered manually or 56 + /// when [`should_collect`] returns true. 57 + pub struct Gc<T: Traceable> { 58 + /// The heap: a vector of optional slots (None = free). 59 + heap: Vec<Option<HeapSlot<T>>>, 60 + /// Free slot indices for O(1) allocation. 61 + free_list: Vec<u32>, 62 + /// Number of currently live objects. 63 + live_count: usize, 64 + /// Collection is suggested when live_count exceeds this. 65 + threshold: usize, 66 + /// Total collections performed. 67 + collections: usize, 68 + } 69 + 70 + /// Initial collection threshold. 71 + const INITIAL_THRESHOLD: usize = 256; 72 + 73 + impl<T: Traceable> Gc<T> { 74 + /// Create a new, empty GC heap. 75 + pub fn new() -> Self { 76 + Self { 77 + heap: Vec::new(), 78 + free_list: Vec::new(), 79 + live_count: 0, 80 + threshold: INITIAL_THRESHOLD, 81 + collections: 0, 82 + } 83 + } 84 + 85 + /// Allocate a new object on the heap, returning a `GcRef` handle. 86 + pub fn alloc(&mut self, data: T) -> GcRef { 87 + let slot = HeapSlot { 88 + color: Color::White, 89 + data, 90 + }; 91 + let idx = if let Some(free_idx) = self.free_list.pop() { 92 + let i = free_idx as usize; 93 + self.heap[i] = Some(slot); 94 + free_idx 95 + } else { 96 + let i = self.heap.len(); 97 + self.heap.push(Some(slot)); 98 + i as u32 99 + }; 100 + self.live_count += 1; 101 + GcRef(idx) 102 + } 103 + 104 + /// Get an immutable reference to the object at `r`. 105 + /// 106 + /// Returns `None` if the slot has been freed (stale reference). 107 + pub fn get(&self, r: GcRef) -> Option<&T> { 108 + let idx = r.0 as usize; 109 + self.heap 110 + .get(idx) 111 + .and_then(|slot| slot.as_ref().map(|s| &s.data)) 112 + } 113 + 114 + /// Get a mutable reference to the object at `r`. 115 + /// 116 + /// Returns `None` if the slot has been freed (stale reference). 117 + pub fn get_mut(&mut self, r: GcRef) -> Option<&mut T> { 118 + let idx = r.0 as usize; 119 + self.heap 120 + .get_mut(idx) 121 + .and_then(|slot| slot.as_mut().map(|s| &mut s.data)) 122 + } 123 + 124 + /// Returns `true` if a collection should be triggered. 125 + pub fn should_collect(&self) -> bool { 126 + self.live_count >= self.threshold 127 + } 128 + 129 + /// Perform a garbage collection pass. 130 + /// 131 + /// `roots` must contain every `GcRef` reachable from the mutator (VM 132 + /// registers, globals, call stack). Any object not transitively reachable 133 + /// from a root is freed. 134 + pub fn collect(&mut self, roots: &[GcRef]) { 135 + self.mark(roots); 136 + self.sweep(); 137 + self.collections += 1; 138 + 139 + // Grow threshold so we don't collect too often. 140 + if self.live_count >= self.threshold { 141 + self.threshold = self.live_count * 2; 142 + } 143 + } 144 + 145 + /// Return heap statistics. 146 + pub fn stats(&self) -> GcStats { 147 + GcStats { 148 + live_count: self.live_count, 149 + total_slots: self.heap.len(), 150 + collections: self.collections, 151 + } 152 + } 153 + 154 + /// Mark phase: starting from roots, color all reachable objects black. 155 + fn mark(&mut self, roots: &[GcRef]) { 156 + let mut gray_stack: Vec<GcRef> = Vec::new(); 157 + 158 + // Color roots gray. 159 + for &root in roots { 160 + let idx = root.0 as usize; 161 + if idx < self.heap.len() { 162 + if let Some(entry) = &mut self.heap[idx] { 163 + if entry.color == Color::White { 164 + entry.color = Color::Gray; 165 + gray_stack.push(root); 166 + } 167 + } 168 + } 169 + } 170 + 171 + // Process the gray stack. 172 + while let Some(gc_ref) = gray_stack.pop() { 173 + let idx = gc_ref.0 as usize; 174 + 175 + // Collect child references (scoped borrow). 176 + let children = { 177 + let mut c = Vec::new(); 178 + if let Some(entry) = &self.heap[idx] { 179 + entry.data.trace(&mut |child| c.push(child)); 180 + } 181 + c 182 + }; 183 + 184 + // Mark current object black. 185 + if let Some(entry) = &mut self.heap[idx] { 186 + entry.color = Color::Black; 187 + } 188 + 189 + // Mark white children gray. 190 + for child in children { 191 + let cidx = child.0 as usize; 192 + if cidx < self.heap.len() { 193 + if let Some(entry) = &mut self.heap[cidx] { 194 + if entry.color == Color::White { 195 + entry.color = Color::Gray; 196 + gray_stack.push(child); 197 + } 198 + } 199 + } 200 + } 201 + } 202 + } 203 + 204 + /// Sweep phase: free all white objects, reset black objects to white. 205 + fn sweep(&mut self) { 206 + for i in 0..self.heap.len() { 207 + let should_free = match &self.heap[i] { 208 + Some(entry) => entry.color == Color::White, 209 + None => false, 210 + }; 211 + if should_free { 212 + self.heap[i] = None; 213 + self.free_list.push(i as u32); 214 + self.live_count -= 1; 215 + } else if let Some(entry) = &mut self.heap[i] { 216 + // Reset black → white for next cycle. 217 + entry.color = Color::White; 218 + } 219 + } 220 + } 221 + } 222 + 223 + impl<T: Traceable> Default for Gc<T> { 224 + fn default() -> Self { 225 + Self::new() 226 + } 227 + } 228 + 229 + #[cfg(test)] 230 + mod tests { 231 + use super::*; 232 + 233 + /// A simple test object that holds GcRef children. 234 + struct TestObj { 235 + children: Vec<GcRef>, 236 + } 237 + 238 + impl Traceable for TestObj { 239 + fn trace(&self, visitor: &mut dyn FnMut(GcRef)) { 240 + for &child in &self.children { 241 + visitor(child); 242 + } 243 + } 244 + } 245 + 246 + #[test] 247 + fn test_alloc_and_get() { 248 + let mut gc = Gc::new(); 249 + let r = gc.alloc(TestObj { 250 + children: Vec::new(), 251 + }); 252 + assert!(gc.get(r).is_some()); 253 + assert_eq!(gc.stats().live_count, 1); 254 + } 255 + 256 + #[test] 257 + fn test_collect_unreachable() { 258 + let mut gc = Gc::new(); 259 + let r1 = gc.alloc(TestObj { 260 + children: Vec::new(), 261 + }); 262 + let r2 = gc.alloc(TestObj { 263 + children: Vec::new(), 264 + }); 265 + assert_eq!(gc.stats().live_count, 2); 266 + 267 + // Collect with no roots — everything is freed. 268 + gc.collect(&[]); 269 + assert_eq!(gc.stats().live_count, 0); 270 + assert!(gc.get(r1).is_none()); 271 + assert!(gc.get(r2).is_none()); 272 + } 273 + 274 + #[test] 275 + fn test_collect_reachable() { 276 + let mut gc = Gc::new(); 277 + let r1 = gc.alloc(TestObj { 278 + children: Vec::new(), 279 + }); 280 + let r2 = gc.alloc(TestObj { 281 + children: Vec::new(), 282 + }); 283 + assert_eq!(gc.stats().live_count, 2); 284 + 285 + // Only r1 is a root. 286 + gc.collect(&[r1]); 287 + assert_eq!(gc.stats().live_count, 1); 288 + assert!(gc.get(r1).is_some()); 289 + assert!(gc.get(r2).is_none()); 290 + } 291 + 292 + #[test] 293 + fn test_collect_transitive() { 294 + let mut gc = Gc::new(); 295 + let r1 = gc.alloc(TestObj { 296 + children: Vec::new(), 297 + }); 298 + let r2 = gc.alloc(TestObj { 299 + children: Vec::new(), 300 + }); 301 + let r3 = gc.alloc(TestObj { 302 + children: Vec::new(), 303 + }); 304 + 305 + // r1 -> r2 -> r3 306 + gc.get_mut(r1).unwrap().children.push(r2); 307 + gc.get_mut(r2).unwrap().children.push(r3); 308 + 309 + gc.collect(&[r1]); 310 + assert_eq!(gc.stats().live_count, 3); 311 + assert!(gc.get(r1).is_some()); 312 + assert!(gc.get(r2).is_some()); 313 + assert!(gc.get(r3).is_some()); 314 + } 315 + 316 + #[test] 317 + fn test_cycle_collection() { 318 + let mut gc = Gc::new(); 319 + let r1 = gc.alloc(TestObj { 320 + children: Vec::new(), 321 + }); 322 + let r2 = gc.alloc(TestObj { 323 + children: Vec::new(), 324 + }); 325 + 326 + // Create a cycle: r1 -> r2 -> r1. 327 + gc.get_mut(r1).unwrap().children.push(r2); 328 + gc.get_mut(r2).unwrap().children.push(r1); 329 + 330 + // With r1 as root, both survive. 331 + gc.collect(&[r1]); 332 + assert_eq!(gc.stats().live_count, 2); 333 + 334 + // With no roots, both are freed (cycle is collected). 335 + gc.collect(&[]); 336 + assert_eq!(gc.stats().live_count, 0); 337 + } 338 + 339 + #[test] 340 + fn test_free_list_reuse() { 341 + let mut gc = Gc::new(); 342 + let _r1 = gc.alloc(TestObj { 343 + children: Vec::new(), 344 + }); 345 + let _r2 = gc.alloc(TestObj { 346 + children: Vec::new(), 347 + }); 348 + 349 + // Free both. 350 + gc.collect(&[]); 351 + assert_eq!(gc.stats().live_count, 0); 352 + assert_eq!(gc.stats().total_slots, 2); 353 + 354 + // Allocate again — should reuse freed slots. 355 + let _r3 = gc.alloc(TestObj { 356 + children: Vec::new(), 357 + }); 358 + let _r4 = gc.alloc(TestObj { 359 + children: Vec::new(), 360 + }); 361 + assert_eq!(gc.stats().live_count, 2); 362 + assert_eq!(gc.stats().total_slots, 2); // No growth. 363 + } 364 + 365 + #[test] 366 + fn test_should_collect_threshold() { 367 + let mut gc: Gc<TestObj> = Gc::new(); 368 + assert!(!gc.should_collect()); 369 + 370 + // Allocate up to threshold. 371 + for _ in 0..INITIAL_THRESHOLD { 372 + gc.alloc(TestObj { 373 + children: Vec::new(), 374 + }); 375 + } 376 + assert!(gc.should_collect()); 377 + } 378 + 379 + #[test] 380 + fn test_stress() { 381 + let mut gc = Gc::new(); 382 + let mut live_refs = Vec::new(); 383 + 384 + for i in 0..1000 { 385 + let r = gc.alloc(TestObj { 386 + children: Vec::new(), 387 + }); 388 + if i % 3 == 0 { 389 + live_refs.push(r); 390 + } 391 + } 392 + 393 + gc.collect(&live_refs); 394 + // Only refs kept in live_refs should survive. 395 + assert_eq!(gc.stats().live_count, live_refs.len()); 396 + 397 + for r in &live_refs { 398 + assert!(gc.get(*r).is_some()); 399 + } 400 + } 401 + }
+2 -1
crates/js/src/lib.rs
··· 3 3 pub mod ast; 4 4 pub mod bytecode; 5 5 pub mod compiler; 6 + pub mod gc; 6 7 pub mod lexer; 7 8 pub mod parser; 8 9 pub mod vm; ··· 40 41 let result = engine 41 42 .execute(&func) 42 43 .map_err(|e| JsError::RuntimeError(e.to_string()))?; 43 - Ok(result.to_string()) 44 + Ok(result.to_js_string(&engine.gc)) 44 45 }
+467 -272
crates/js/src/vm.rs
··· 1 1 //! Register-based JavaScript virtual machine. 2 2 //! 3 3 //! Executes bytecode produced by the compiler. Each call frame has a register 4 - //! file, and the VM dispatches instructions in a loop. 4 + //! file, and the VM dispatches instructions in a loop. Heap-allocated objects 5 + //! (plain objects and functions) are managed by a tri-color mark-and-sweep 6 + //! garbage collector. 5 7 6 8 use crate::bytecode::{Constant, Function, Op, Reg}; 9 + use crate::gc::{Gc, GcRef, Traceable}; 7 10 use std::collections::HashMap; 8 11 use std::fmt; 9 12 13 + // ── Heap objects (GC-managed) ──────────────────────────────── 14 + 15 + /// A GC-managed heap object: either a plain object or a function. 16 + pub enum HeapObject { 17 + Object(ObjectData), 18 + Function(FunctionData), 19 + } 20 + 21 + impl Traceable for HeapObject { 22 + fn trace(&self, visitor: &mut dyn FnMut(GcRef)) { 23 + match self { 24 + HeapObject::Object(data) => { 25 + for val in data.properties.values() { 26 + if let Some(r) = val.gc_ref() { 27 + visitor(r); 28 + } 29 + } 30 + if let Some(proto) = data.prototype { 31 + visitor(proto); 32 + } 33 + } 34 + HeapObject::Function(_) => { 35 + // Bytecode and native callbacks don't hold GC references. 36 + } 37 + } 38 + } 39 + } 40 + 41 + /// A JS plain object (properties stored as a HashMap). 42 + pub struct ObjectData { 43 + pub properties: HashMap<String, Value>, 44 + pub prototype: Option<GcRef>, 45 + } 46 + 47 + impl ObjectData { 48 + pub fn new() -> Self { 49 + Self { 50 + properties: HashMap::new(), 51 + prototype: None, 52 + } 53 + } 54 + } 55 + 56 + impl Default for ObjectData { 57 + fn default() -> Self { 58 + Self::new() 59 + } 60 + } 61 + 62 + /// A runtime function value: either bytecode or native. 63 + pub struct FunctionData { 64 + pub name: String, 65 + pub kind: FunctionKind, 66 + } 67 + 68 + #[derive(Clone)] 69 + pub enum FunctionKind { 70 + /// Bytecode function. 71 + Bytecode(BytecodeFunc), 72 + /// Native (Rust) function. 73 + Native(NativeFunc), 74 + } 75 + 76 + #[derive(Clone)] 77 + pub struct BytecodeFunc { 78 + pub func: Function, 79 + } 80 + 81 + /// A native function callable from JS. 82 + #[derive(Clone)] 83 + pub struct NativeFunc { 84 + pub callback: fn(&[Value]) -> Result<Value, RuntimeError>, 85 + } 86 + 10 87 // ── JS Value ────────────────────────────────────────────────── 11 88 12 89 /// A JavaScript runtime value. 90 + /// 91 + /// Primitive types (Undefined, Null, Boolean, Number, String) are stored 92 + /// inline. Objects and Functions are heap-allocated via the GC and referenced 93 + /// by a [`GcRef`] handle. 13 94 #[derive(Clone)] 14 95 pub enum Value { 15 96 Undefined, ··· 17 98 Boolean(bool), 18 99 Number(f64), 19 100 String(String), 20 - Object(Object), 21 - Function(FunctionValue), 101 + /// A GC-managed plain object. 102 + Object(GcRef), 103 + /// A GC-managed function. 104 + Function(GcRef), 22 105 } 23 106 24 107 impl fmt::Debug for Value { ··· 30 113 Value::Number(n) => write!(f, "{n}"), 31 114 Value::String(s) => write!(f, "\"{}\"", s), 32 115 Value::Object(_) => write!(f, "[object Object]"), 33 - Value::Function(fv) => write!(f, "function {}()", fv.name), 116 + Value::Function(_) => write!(f, "[Function]"), 34 117 } 35 118 } 36 119 } ··· 44 127 Value::Number(n) => format_number(*n, f), 45 128 Value::String(s) => write!(f, "{s}"), 46 129 Value::Object(_) => write!(f, "[object Object]"), 47 - Value::Function(fv) => { 48 - write!(f, "function {}() {{ [native code] }}", fv.name) 49 - } 130 + Value::Function(_) => write!(f, "function() {{ [native code] }}"), 50 131 } 51 132 } 52 133 } ··· 107 188 } 108 189 109 190 /// Abstract `ToString` (ECMA-262 §7.1.12). 110 - pub fn to_js_string(&self) -> String { 191 + /// 192 + /// Requires `&Gc` to look up function names for `Value::Function`. 193 + pub fn to_js_string(&self, gc: &Gc<HeapObject>) -> String { 111 194 match self { 112 195 Value::Undefined => "undefined".to_string(), 113 196 Value::Null => "null".to_string(), ··· 116 199 Value::Number(n) => js_number_to_string(*n), 117 200 Value::String(s) => s.clone(), 118 201 Value::Object(_) => "[object Object]".to_string(), 119 - Value::Function(fv) => format!("function {}() {{ [native code] }}", fv.name), 202 + Value::Function(gc_ref) => gc 203 + .get(*gc_ref) 204 + .and_then(|obj| match obj { 205 + HeapObject::Function(f) => { 206 + Some(format!("function {}() {{ [native code] }}", f.name)) 207 + } 208 + _ => None, 209 + }) 210 + .unwrap_or_else(|| "function() { [native code] }".to_string()), 120 211 } 121 212 } 122 213 ··· 137 228 pub fn is_nullish(&self) -> bool { 138 229 matches!(self, Value::Undefined | Value::Null) 139 230 } 231 + 232 + /// Extract the `GcRef` if this value is an Object or Function. 233 + pub fn gc_ref(&self) -> Option<GcRef> { 234 + match self { 235 + Value::Object(r) | Value::Function(r) => Some(*r), 236 + _ => None, 237 + } 238 + } 140 239 } 141 240 142 241 /// Format a number as JS would. ··· 158 257 } 159 258 } 160 259 161 - // ── Object ──────────────────────────────────────────────────── 162 - 163 - /// A JS plain object (properties stored as a HashMap). 164 - #[derive(Clone, Debug)] 165 - pub struct Object { 166 - pub properties: HashMap<String, Value>, 167 - pub prototype: Option<Box<Object>>, 168 - } 169 - 170 - impl Object { 171 - pub fn new() -> Self { 172 - Self { 173 - properties: HashMap::new(), 174 - prototype: None, 175 - } 176 - } 177 - 178 - pub fn get(&self, key: &str) -> Value { 179 - if let Some(val) = self.properties.get(key) { 180 - val.clone() 181 - } else if let Some(proto) = &self.prototype { 182 - proto.get(key) 183 - } else { 184 - Value::Undefined 185 - } 186 - } 187 - 188 - pub fn set(&mut self, key: String, value: Value) { 189 - self.properties.insert(key, value); 190 - } 191 - 192 - pub fn delete(&mut self, key: &str) -> bool { 193 - self.properties.remove(key).is_some() 194 - } 195 - 196 - pub fn has(&self, key: &str) -> bool { 197 - self.properties.contains_key(key) || self.prototype.as_ref().is_some_and(|p| p.has(key)) 198 - } 199 - } 200 - 201 - impl Default for Object { 202 - fn default() -> Self { 203 - Self::new() 204 - } 205 - } 206 - 207 - // ── Function value ──────────────────────────────────────────── 208 - 209 - /// A runtime function value: either bytecode or native. 210 - #[derive(Clone)] 211 - pub struct FunctionValue { 212 - pub name: String, 213 - pub kind: FunctionKind, 214 - } 215 - 216 - #[derive(Clone)] 217 - pub enum FunctionKind { 218 - /// Bytecode function. 219 - Bytecode(BytecodeFunc), 220 - /// Native (Rust) function. 221 - Native(NativeFunc), 222 - } 223 - 224 - #[derive(Clone)] 225 - pub struct BytecodeFunc { 226 - pub func: Function, 227 - } 228 - 229 - /// A native function callable from JS. 230 - #[derive(Clone)] 231 - pub struct NativeFunc { 232 - pub callback: fn(&[Value]) -> Result<Value, RuntimeError>, 233 - } 234 - 235 260 // ── Runtime errors ──────────────────────────────────────────── 236 261 237 262 /// JavaScript runtime error types. ··· 285 310 } 286 311 } 287 312 288 - /// Convert to a JS Value (an error object). 289 - pub fn to_value(&self) -> Value { 290 - let mut obj = Object::new(); 291 - obj.set("message".to_string(), Value::String(self.message.clone())); 313 + /// Convert to a JS Value (an error object). Allocates through the GC. 314 + pub fn to_value(&self, gc: &mut Gc<HeapObject>) -> Value { 315 + let mut obj = ObjectData::new(); 316 + obj.properties 317 + .insert("message".to_string(), Value::String(self.message.clone())); 292 318 let name = match self.kind { 293 319 ErrorKind::TypeError => "TypeError", 294 320 ErrorKind::ReferenceError => "ReferenceError", ··· 296 322 ErrorKind::SyntaxError => "SyntaxError", 297 323 ErrorKind::Error => "Error", 298 324 }; 299 - obj.set("name".to_string(), Value::String(name.to_string())); 300 - Value::Object(obj) 325 + obj.properties 326 + .insert("name".to_string(), Value::String(name.to_string())); 327 + Value::Object(gc.alloc(HeapObject::Object(obj))) 328 + } 329 + } 330 + 331 + // ── Property access helpers ────────────────────────────────── 332 + 333 + /// Get a property from an object, walking the prototype chain. 334 + fn gc_get_property(gc: &Gc<HeapObject>, obj_ref: GcRef, key: &str) -> Value { 335 + let proto = { 336 + match gc.get(obj_ref) { 337 + Some(HeapObject::Object(data)) => { 338 + if let Some(val) = data.properties.get(key) { 339 + return val.clone(); 340 + } 341 + data.prototype 342 + } 343 + _ => return Value::Undefined, 344 + } 345 + }; 346 + if let Some(proto_ref) = proto { 347 + gc_get_property(gc, proto_ref, key) 348 + } else { 349 + Value::Undefined 350 + } 351 + } 352 + 353 + /// Check if an object has a property (own or inherited). 354 + fn gc_has_property(gc: &Gc<HeapObject>, obj_ref: GcRef, key: &str) -> bool { 355 + let proto = { 356 + match gc.get(obj_ref) { 357 + Some(HeapObject::Object(data)) => { 358 + if data.properties.contains_key(key) { 359 + return true; 360 + } 361 + data.prototype 362 + } 363 + _ => return false, 364 + } 365 + }; 366 + if let Some(proto_ref) = proto { 367 + gc_has_property(gc, proto_ref, key) 368 + } else { 369 + false 370 + } 371 + } 372 + 373 + /// Get a string property (length, index access). 374 + fn string_get_property(s: &str, key: &str) -> Value { 375 + if key == "length" { 376 + Value::Number(s.len() as f64) 377 + } else if let Ok(idx) = key.parse::<usize>() { 378 + s.chars() 379 + .nth(idx) 380 + .map(|c| Value::String(c.to_string())) 381 + .unwrap_or(Value::Undefined) 382 + } else { 383 + Value::Undefined 384 + } 385 + } 386 + 387 + // ── Type conversion helpers ────────────────────────────────── 388 + 389 + /// ToInt32 (ECMA-262 §7.1.5). 390 + fn to_int32(val: &Value) -> i32 { 391 + let n = val.to_number(); 392 + if n.is_nan() || n.is_infinite() || n == 0.0 { 393 + return 0; 394 + } 395 + let i = n.trunc() as i64; 396 + (i & 0xFFFF_FFFF) as i32 397 + } 398 + 399 + /// ToUint32 (ECMA-262 §7.1.6). 400 + fn to_uint32(val: &Value) -> u32 { 401 + let n = val.to_number(); 402 + if n.is_nan() || n.is_infinite() || n == 0.0 { 403 + return 0; 404 + } 405 + let i = n.trunc() as i64; 406 + (i & 0xFFFF_FFFF) as u32 407 + } 408 + 409 + // ── Equality ───────────────────────────────────────────────── 410 + 411 + /// Abstract equality comparison (==) per ECMA-262 §7.2.14. 412 + fn abstract_eq(x: &Value, y: &Value) -> bool { 413 + match (x, y) { 414 + (Value::Undefined, Value::Undefined) => true, 415 + (Value::Null, Value::Null) => true, 416 + (Value::Undefined, Value::Null) | (Value::Null, Value::Undefined) => true, 417 + (Value::Number(a), Value::Number(b)) => a == b, 418 + (Value::String(a), Value::String(b)) => a == b, 419 + (Value::Boolean(a), Value::Boolean(b)) => a == b, 420 + // Number / String → convert String to Number. 421 + (Value::Number(_), Value::String(_)) => abstract_eq(x, &Value::Number(y.to_number())), 422 + (Value::String(_), Value::Number(_)) => abstract_eq(&Value::Number(x.to_number()), y), 423 + // Boolean → Number. 424 + (Value::Boolean(_), _) => abstract_eq(&Value::Number(x.to_number()), y), 425 + (_, Value::Boolean(_)) => abstract_eq(x, &Value::Number(y.to_number())), 426 + // Same GcRef → equal. 427 + (Value::Object(a), Value::Object(b)) => a == b, 428 + (Value::Function(a), Value::Function(b)) => a == b, 429 + _ => false, 430 + } 431 + } 432 + 433 + /// Strict equality comparison (===) per ECMA-262 §7.2.15. 434 + fn strict_eq(x: &Value, y: &Value) -> bool { 435 + match (x, y) { 436 + (Value::Undefined, Value::Undefined) => true, 437 + (Value::Null, Value::Null) => true, 438 + (Value::Number(a), Value::Number(b)) => a == b, 439 + (Value::String(a), Value::String(b)) => a == b, 440 + (Value::Boolean(a), Value::Boolean(b)) => a == b, 441 + // Reference identity for heap objects. 442 + (Value::Object(a), Value::Object(b)) => a == b, 443 + (Value::Function(a), Value::Function(b)) => a == b, 444 + _ => false, 445 + } 446 + } 447 + 448 + // ── Relational comparison ──────────────────────────────────── 449 + 450 + /// Abstract relational comparison. Returns false for NaN comparisons. 451 + fn abstract_relational( 452 + lhs: &Value, 453 + rhs: &Value, 454 + predicate: fn(std::cmp::Ordering) -> bool, 455 + ) -> bool { 456 + // If both are strings, compare lexicographically. 457 + if let (Value::String(a), Value::String(b)) = (lhs, rhs) { 458 + return predicate(a.cmp(b)); 459 + } 460 + // Otherwise, compare as numbers. 461 + let a = lhs.to_number(); 462 + let b = rhs.to_number(); 463 + if a.is_nan() || b.is_nan() { 464 + return false; 465 + } 466 + predicate(a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Equal)) 467 + } 468 + 469 + // ── Addition ───────────────────────────────────────────────── 470 + 471 + /// The + operator: string concat if either operand is a string, else numeric add. 472 + fn add_values(lhs: &Value, rhs: &Value, gc: &Gc<HeapObject>) -> Value { 473 + match (lhs, rhs) { 474 + (Value::String(a), _) => Value::String(format!("{a}{}", rhs.to_js_string(gc))), 475 + (_, Value::String(b)) => Value::String(format!("{}{b}", lhs.to_js_string(gc))), 476 + _ => Value::Number(lhs.to_number() + rhs.to_number()), 301 477 } 302 478 } 303 479 ··· 335 511 frames: Vec<CallFrame>, 336 512 /// Global variables. 337 513 globals: HashMap<String, Value>, 514 + /// Garbage collector managing heap objects. 515 + pub gc: Gc<HeapObject>, 338 516 } 339 517 340 518 /// Maximum register file size. ··· 348 526 registers: vec![Value::Undefined; 256], 349 527 frames: Vec::new(), 350 528 globals: HashMap::new(), 529 + gc: Gc::new(), 351 530 } 352 531 } 353 532 ··· 407 586 i32::from_le_bytes(bytes) 408 587 } 409 588 589 + /// Collect all GcRef values reachable from the mutator (roots for GC). 590 + fn collect_roots(&self) -> Vec<GcRef> { 591 + let mut roots = Vec::new(); 592 + for val in &self.registers { 593 + if let Some(r) = val.gc_ref() { 594 + roots.push(r); 595 + } 596 + } 597 + for val in self.globals.values() { 598 + if let Some(r) = val.gc_ref() { 599 + roots.push(r); 600 + } 601 + } 602 + roots 603 + } 604 + 410 605 /// Main dispatch loop. 411 606 fn run(&mut self) -> Result<Value, RuntimeError> { 412 607 loop { ··· 506 701 let result = add_values( 507 702 &self.registers[base + lhs_r as usize], 508 703 &self.registers[base + rhs_r as usize], 704 + &self.gc, 509 705 ); 510 706 self.registers[base + dst as usize] = result; 511 707 } ··· 683 879 let key_r = Self::read_u8(&mut self.frames[fi]); 684 880 let obj_r = Self::read_u8(&mut self.frames[fi]); 685 881 let base = self.frames[fi].base; 686 - let key = self.registers[base + key_r as usize].to_js_string(); 687 - let result = match &self.registers[base + obj_r as usize] { 688 - Value::Object(obj) => Value::Boolean(obj.has(&key)), 882 + let key = self.registers[base + key_r as usize].to_js_string(&self.gc); 883 + let result = match self.registers[base + obj_r as usize] { 884 + Value::Object(gc_ref) => { 885 + Value::Boolean(gc_has_property(&self.gc, gc_ref, &key)) 886 + } 689 887 _ => { 690 888 return Err(RuntimeError::type_error( 691 889 "Cannot use 'in' operator to search for property in non-object", ··· 738 936 let args_start = Self::read_u8(&mut self.frames[fi]); 739 937 let arg_count = Self::read_u8(&mut self.frames[fi]); 740 938 let base = self.frames[fi].base; 741 - let func_val = self.registers[base + func_r as usize].clone(); 939 + 940 + // Extract function GcRef. 941 + let func_gc_ref = match self.registers[base + func_r as usize] { 942 + Value::Function(r) => r, 943 + _ => { 944 + let desc = 945 + self.registers[base + func_r as usize].to_js_string(&self.gc); 946 + let err = RuntimeError::type_error(format!("{desc} is not a function")); 947 + let err_val = err.to_value(&mut self.gc); 948 + if !self.handle_exception(err_val) { 949 + return Err(err); 950 + } 951 + continue; 952 + } 953 + }; 742 954 743 955 // Collect arguments. 744 956 let mut args = Vec::with_capacity(arg_count as usize); ··· 746 958 args.push(self.registers[base + (args_start + i) as usize].clone()); 747 959 } 748 960 749 - match func_val { 750 - Value::Function(fv) => match &fv.kind { 751 - FunctionKind::Native(native) => match (native.callback)(&args) { 752 - Ok(val) => { 753 - self.registers[base + dst as usize] = val; 754 - } 755 - Err(err) => { 756 - if !self.handle_exception(err.to_value()) { 757 - return Err(err); 758 - } 759 - } 961 + // Read function data from GC (scoped borrow). 962 + let call_info = { 963 + match self.gc.get(func_gc_ref) { 964 + Some(HeapObject::Function(fdata)) => match &fdata.kind { 965 + FunctionKind::Native(n) => CallInfo::Native(n.callback), 966 + FunctionKind::Bytecode(bc) => CallInfo::Bytecode(bc.func.clone()), 760 967 }, 761 - FunctionKind::Bytecode(bc) => { 762 - if self.frames.len() >= MAX_CALL_DEPTH { 763 - let err = RuntimeError::range_error( 764 - "Maximum call stack size exceeded", 765 - ); 766 - if !self.handle_exception(err.to_value()) { 767 - return Err(err); 768 - } 769 - continue; 968 + _ => { 969 + let err = RuntimeError::type_error("not a function"); 970 + let err_val = err.to_value(&mut self.gc); 971 + if !self.handle_exception(err_val) { 972 + return Err(err); 770 973 } 974 + continue; 975 + } 976 + } 977 + }; 771 978 772 - let callee_func = bc.func.clone(); 773 - let callee_base = 774 - base + self.frames[fi].func.register_count as usize; 775 - let callee_regs = callee_func.register_count as usize; 776 - self.ensure_registers(callee_base + callee_regs); 777 - 778 - // Copy arguments into callee's registers. 779 - for i in 0..callee_func.param_count.min(arg_count) { 780 - self.registers[callee_base + i as usize] = 781 - args[i as usize].clone(); 979 + match call_info { 980 + CallInfo::Native(callback) => match callback(&args) { 981 + Ok(val) => { 982 + self.registers[base + dst as usize] = val; 983 + } 984 + Err(err) => { 985 + let err_val = err.to_value(&mut self.gc); 986 + if !self.handle_exception(err_val) { 987 + return Err(err); 782 988 } 783 - // Fill remaining params with undefined. 784 - for i in arg_count..callee_func.param_count { 785 - self.registers[callee_base + i as usize] = Value::Undefined; 989 + } 990 + }, 991 + CallInfo::Bytecode(callee_func) => { 992 + if self.frames.len() >= MAX_CALL_DEPTH { 993 + let err = 994 + RuntimeError::range_error("Maximum call stack size exceeded"); 995 + let err_val = err.to_value(&mut self.gc); 996 + if !self.handle_exception(err_val) { 997 + return Err(err); 786 998 } 999 + continue; 1000 + } 787 1001 788 - self.frames.push(CallFrame { 789 - func: callee_func, 790 - ip: 0, 791 - base: callee_base, 792 - return_reg: base + dst as usize, 793 - exception_handlers: Vec::new(), 794 - }); 1002 + let callee_base = base + self.frames[fi].func.register_count as usize; 1003 + let callee_regs = callee_func.register_count as usize; 1004 + self.ensure_registers(callee_base + callee_regs); 1005 + 1006 + // Copy arguments into callee's registers. 1007 + for i in 0..callee_func.param_count.min(arg_count) { 1008 + self.registers[callee_base + i as usize] = args[i as usize].clone(); 795 1009 } 796 - }, 797 - _ => { 798 - let err = RuntimeError::type_error(format!( 799 - "{} is not a function", 800 - func_val.to_js_string() 801 - )); 802 - if !self.handle_exception(err.to_value()) { 803 - return Err(err); 1010 + // Fill remaining params with undefined. 1011 + for i in arg_count..callee_func.param_count { 1012 + self.registers[callee_base + i as usize] = Value::Undefined; 804 1013 } 1014 + 1015 + self.frames.push(CallFrame { 1016 + func: callee_func, 1017 + ip: 0, 1018 + base: callee_base, 1019 + return_reg: base + dst as usize, 1020 + exception_handlers: Vec::new(), 1021 + }); 805 1022 } 806 1023 } 807 1024 } ··· 824 1041 let val = self.registers[base + reg as usize].clone(); 825 1042 826 1043 if !self.handle_exception(val) { 827 - let msg = self.registers[base + reg as usize].to_js_string(); 1044 + let msg = self.registers[base + reg as usize].to_js_string(&self.gc); 828 1045 return Err(RuntimeError { 829 1046 kind: ErrorKind::Error, 830 1047 message: msg, ··· 837 1054 let base = self.frames[fi].base; 838 1055 let inner_func = self.frames[fi].func.functions[func_idx].clone(); 839 1056 let name = inner_func.name.clone(); 840 - self.registers[base + dst as usize] = Value::Function(FunctionValue { 1057 + let gc_ref = self.gc.alloc(HeapObject::Function(FunctionData { 841 1058 name, 842 1059 kind: FunctionKind::Bytecode(BytecodeFunc { func: inner_func }), 843 - }); 1060 + })); 1061 + self.registers[base + dst as usize] = Value::Function(gc_ref); 1062 + 1063 + // Trigger GC if needed. 1064 + if self.gc.should_collect() { 1065 + let roots = self.collect_roots(); 1066 + self.gc.collect(&roots); 1067 + } 844 1068 } 845 1069 846 1070 // ── Object / property ────────────────────────── ··· 849 1073 let obj_r = Self::read_u8(&mut self.frames[fi]); 850 1074 let key_r = Self::read_u8(&mut self.frames[fi]); 851 1075 let base = self.frames[fi].base; 852 - let key = self.registers[base + key_r as usize].to_js_string(); 853 - let val = match &self.registers[base + obj_r as usize] { 854 - Value::Object(obj) => obj.get(&key), 855 - Value::String(s) => string_get_property(s, &key), 1076 + let key = self.registers[base + key_r as usize].to_js_string(&self.gc); 1077 + let val = match self.registers[base + obj_r as usize] { 1078 + Value::Object(gc_ref) => gc_get_property(&self.gc, gc_ref, &key), 1079 + Value::String(ref s) => string_get_property(s, &key), 856 1080 _ => Value::Undefined, 857 1081 }; 858 1082 self.registers[base + dst as usize] = val; ··· 862 1086 let key_r = Self::read_u8(&mut self.frames[fi]); 863 1087 let val_r = Self::read_u8(&mut self.frames[fi]); 864 1088 let base = self.frames[fi].base; 865 - let key = self.registers[base + key_r as usize].to_js_string(); 1089 + let key = self.registers[base + key_r as usize].to_js_string(&self.gc); 866 1090 let val = self.registers[base + val_r as usize].clone(); 867 - if let Value::Object(ref mut obj) = self.registers[base + obj_r as usize] { 868 - obj.set(key, val); 1091 + if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { 1092 + if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { 1093 + data.properties.insert(key, val); 1094 + } 869 1095 } 870 1096 } 871 1097 Op::CreateObject => { 872 1098 let dst = Self::read_u8(&mut self.frames[fi]); 873 1099 let base = self.frames[fi].base; 874 - self.registers[base + dst as usize] = Value::Object(Object::new()); 1100 + let gc_ref = self.gc.alloc(HeapObject::Object(ObjectData::new())); 1101 + self.registers[base + dst as usize] = Value::Object(gc_ref); 1102 + 1103 + if self.gc.should_collect() { 1104 + let roots = self.collect_roots(); 1105 + self.gc.collect(&roots); 1106 + } 875 1107 } 876 1108 Op::CreateArray => { 877 1109 let dst = Self::read_u8(&mut self.frames[fi]); 878 1110 let base = self.frames[fi].base; 879 - let mut obj = Object::new(); 880 - obj.set("length".to_string(), Value::Number(0.0)); 881 - self.registers[base + dst as usize] = Value::Object(obj); 1111 + let mut obj = ObjectData::new(); 1112 + obj.properties 1113 + .insert("length".to_string(), Value::Number(0.0)); 1114 + let gc_ref = self.gc.alloc(HeapObject::Object(obj)); 1115 + self.registers[base + dst as usize] = Value::Object(gc_ref); 1116 + 1117 + if self.gc.should_collect() { 1118 + let roots = self.collect_roots(); 1119 + self.gc.collect(&roots); 1120 + } 882 1121 } 883 1122 Op::GetPropertyByName => { 884 1123 let dst = Self::read_u8(&mut self.frames[fi]); ··· 886 1125 let name_idx = Self::read_u16(&mut self.frames[fi]) as usize; 887 1126 let base = self.frames[fi].base; 888 1127 let key = self.frames[fi].func.names[name_idx].clone(); 889 - let val = match &self.registers[base + obj_r as usize] { 890 - Value::Object(obj) => obj.get(&key), 891 - Value::String(s) => string_get_property(s, &key), 1128 + let val = match self.registers[base + obj_r as usize] { 1129 + Value::Object(gc_ref) => gc_get_property(&self.gc, gc_ref, &key), 1130 + Value::String(ref s) => string_get_property(s, &key), 892 1131 _ => Value::Undefined, 893 1132 }; 894 1133 self.registers[base + dst as usize] = val; ··· 900 1139 let base = self.frames[fi].base; 901 1140 let key = self.frames[fi].func.names[name_idx].clone(); 902 1141 let val = self.registers[base + val_r as usize].clone(); 903 - if let Value::Object(ref mut obj) = self.registers[base + obj_r as usize] { 904 - obj.set(key, val); 1142 + if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { 1143 + if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { 1144 + data.properties.insert(key, val); 1145 + } 905 1146 } 906 1147 } 907 1148 ··· 911 1152 let obj_r = Self::read_u8(&mut self.frames[fi]); 912 1153 let key_r = Self::read_u8(&mut self.frames[fi]); 913 1154 let base = self.frames[fi].base; 914 - let key = self.registers[base + key_r as usize].to_js_string(); 1155 + let key = self.registers[base + key_r as usize].to_js_string(&self.gc); 915 1156 let result = 916 - if let Value::Object(ref mut obj) = self.registers[base + obj_r as usize] { 917 - obj.delete(&key) 1157 + if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { 1158 + if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { 1159 + data.properties.remove(&key).is_some() 1160 + } else { 1161 + true 1162 + } 918 1163 } else { 919 1164 true 920 1165 }; ··· 957 1202 name: &str, 958 1203 callback: fn(&[Value]) -> Result<Value, RuntimeError>, 959 1204 ) { 960 - self.globals.insert( 961 - name.to_string(), 962 - Value::Function(FunctionValue { 963 - name: name.to_string(), 964 - kind: FunctionKind::Native(NativeFunc { callback }), 965 - }), 966 - ); 1205 + let gc_ref = self.gc.alloc(HeapObject::Function(FunctionData { 1206 + name: name.to_string(), 1207 + kind: FunctionKind::Native(NativeFunc { callback }), 1208 + })); 1209 + self.globals 1210 + .insert(name.to_string(), Value::Function(gc_ref)); 967 1211 } 968 1212 969 1213 /// Get a global variable value. ··· 983 1227 } 984 1228 } 985 1229 986 - // ── String property access ─────────────────────────────────── 987 - 988 - fn string_get_property(s: &str, key: &str) -> Value { 989 - if key == "length" { 990 - Value::Number(s.len() as f64) 991 - } else if let Ok(idx) = key.parse::<usize>() { 992 - s.chars() 993 - .nth(idx) 994 - .map(|c| Value::String(c.to_string())) 995 - .unwrap_or(Value::Undefined) 996 - } else { 997 - Value::Undefined 998 - } 999 - } 1000 - 1001 - // ── Type conversion helpers ────────────────────────────────── 1002 - 1003 - /// ToInt32 (ECMA-262 §7.1.5). 1004 - fn to_int32(val: &Value) -> i32 { 1005 - let n = val.to_number(); 1006 - if n.is_nan() || n.is_infinite() || n == 0.0 { 1007 - return 0; 1008 - } 1009 - let i = n.trunc() as i64; 1010 - (i & 0xFFFF_FFFF) as i32 1011 - } 1012 - 1013 - /// ToUint32 (ECMA-262 §7.1.6). 1014 - fn to_uint32(val: &Value) -> u32 { 1015 - let n = val.to_number(); 1016 - if n.is_nan() || n.is_infinite() || n == 0.0 { 1017 - return 0; 1018 - } 1019 - let i = n.trunc() as i64; 1020 - (i & 0xFFFF_FFFF) as u32 1021 - } 1022 - 1023 - // ── Equality ───────────────────────────────────────────────── 1024 - 1025 - /// Abstract equality comparison (==) per ECMA-262 §7.2.14. 1026 - fn abstract_eq(x: &Value, y: &Value) -> bool { 1027 - match (x, y) { 1028 - (Value::Undefined, Value::Undefined) => true, 1029 - (Value::Null, Value::Null) => true, 1030 - (Value::Undefined, Value::Null) | (Value::Null, Value::Undefined) => true, 1031 - (Value::Number(a), Value::Number(b)) => a == b, 1032 - (Value::String(a), Value::String(b)) => a == b, 1033 - (Value::Boolean(a), Value::Boolean(b)) => a == b, 1034 - // Number / String → convert String to Number. 1035 - (Value::Number(_), Value::String(_)) => abstract_eq(x, &Value::Number(y.to_number())), 1036 - (Value::String(_), Value::Number(_)) => abstract_eq(&Value::Number(x.to_number()), y), 1037 - // Boolean → Number. 1038 - (Value::Boolean(_), _) => abstract_eq(&Value::Number(x.to_number()), y), 1039 - (_, Value::Boolean(_)) => abstract_eq(x, &Value::Number(y.to_number())), 1040 - _ => false, 1041 - } 1042 - } 1043 - 1044 - /// Strict equality comparison (===) per ECMA-262 §7.2.15. 1045 - fn strict_eq(x: &Value, y: &Value) -> bool { 1046 - match (x, y) { 1047 - (Value::Undefined, Value::Undefined) => true, 1048 - (Value::Null, Value::Null) => true, 1049 - (Value::Number(a), Value::Number(b)) => a == b, 1050 - (Value::String(a), Value::String(b)) => a == b, 1051 - (Value::Boolean(a), Value::Boolean(b)) => a == b, 1052 - _ => false, 1053 - } 1054 - } 1055 - 1056 - // ── Relational comparison ──────────────────────────────────── 1057 - 1058 - /// Abstract relational comparison. Returns false for NaN comparisons. 1059 - fn abstract_relational( 1060 - lhs: &Value, 1061 - rhs: &Value, 1062 - predicate: fn(std::cmp::Ordering) -> bool, 1063 - ) -> bool { 1064 - // If both are strings, compare lexicographically. 1065 - if let (Value::String(a), Value::String(b)) = (lhs, rhs) { 1066 - return predicate(a.cmp(b)); 1067 - } 1068 - // Otherwise, compare as numbers. 1069 - let a = lhs.to_number(); 1070 - let b = rhs.to_number(); 1071 - if a.is_nan() || b.is_nan() { 1072 - return false; 1073 - } 1074 - predicate(a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Equal)) 1075 - } 1076 - 1077 - // ── Addition ───────────────────────────────────────────────── 1078 - 1079 - /// The + operator: string concat if either operand is a string, else numeric add. 1080 - fn add_values(lhs: &Value, rhs: &Value) -> Value { 1081 - match (lhs, rhs) { 1082 - (Value::String(a), _) => Value::String(format!("{a}{}", rhs.to_js_string())), 1083 - (_, Value::String(b)) => Value::String(format!("{}{b}", lhs.to_js_string())), 1084 - _ => Value::Number(lhs.to_number() + rhs.to_number()), 1085 - } 1230 + /// Internal enum to avoid holding a GC borrow across the call setup. 1231 + enum CallInfo { 1232 + Native(fn(&[Value]) -> Result<Value, RuntimeError>), 1233 + Bytecode(Function), 1086 1234 } 1087 1235 1088 1236 // ── Tests ──────────────────────────────────────────────────── ··· 1106 1254 1107 1255 #[test] 1108 1256 fn test_to_boolean() { 1257 + let mut gc: Gc<HeapObject> = Gc::new(); 1109 1258 assert!(!Value::Undefined.to_boolean()); 1110 1259 assert!(!Value::Null.to_boolean()); 1111 1260 assert!(!Value::Boolean(false).to_boolean()); ··· 1115 1264 assert!(Value::Number(1.0).to_boolean()); 1116 1265 assert!(!Value::String(String::new()).to_boolean()); 1117 1266 assert!(Value::String("hello".to_string()).to_boolean()); 1118 - assert!(Value::Object(Object::new()).to_boolean()); 1267 + let obj_ref = gc.alloc(HeapObject::Object(ObjectData::new())); 1268 + assert!(Value::Object(obj_ref).to_boolean()); 1119 1269 } 1120 1270 1121 1271 #[test] ··· 1132 1282 1133 1283 #[test] 1134 1284 fn test_type_of() { 1285 + let mut gc: Gc<HeapObject> = Gc::new(); 1135 1286 assert_eq!(Value::Undefined.type_of(), "undefined"); 1136 1287 assert_eq!(Value::Null.type_of(), "object"); 1137 1288 assert_eq!(Value::Boolean(true).type_of(), "boolean"); 1138 1289 assert_eq!(Value::Number(1.0).type_of(), "number"); 1139 1290 assert_eq!(Value::String("hi".to_string()).type_of(), "string"); 1140 - assert_eq!(Value::Object(Object::new()).type_of(), "object"); 1291 + let obj_ref = gc.alloc(HeapObject::Object(ObjectData::new())); 1292 + assert_eq!(Value::Object(obj_ref).type_of(), "object"); 1141 1293 } 1142 1294 1143 1295 // ── VM bytecode-level tests ───────────────────────────── ··· 1583 1735 Value::Number(n) => assert_eq!(n, 55.0), 1584 1736 v => panic!("expected 55, got {v:?}"), 1585 1737 } 1738 + } 1739 + 1740 + // ── GC integration tests ──────────────────────────────── 1741 + 1742 + #[test] 1743 + fn test_gc_object_survives_collection() { 1744 + let src = r#" 1745 + var o = { x: 42 }; 1746 + o.x 1747 + "#; 1748 + match eval(src).unwrap() { 1749 + Value::Number(n) => assert_eq!(n, 42.0), 1750 + v => panic!("expected 42, got {v:?}"), 1751 + } 1752 + } 1753 + 1754 + #[test] 1755 + fn test_gc_many_objects() { 1756 + // Allocate many objects to trigger GC threshold. 1757 + let src = r#" 1758 + var sum = 0; 1759 + var i = 0; 1760 + while (i < 100) { 1761 + var o = { val: i }; 1762 + sum = sum + o.val; 1763 + i = i + 1; 1764 + } 1765 + sum 1766 + "#; 1767 + match eval(src).unwrap() { 1768 + Value::Number(n) => assert_eq!(n, 4950.0), 1769 + v => panic!("expected 4950, got {v:?}"), 1770 + } 1771 + } 1772 + 1773 + #[test] 1774 + fn test_gc_reference_identity() { 1775 + // With GC, object assignment is by reference. 1776 + let mut gc: Gc<HeapObject> = Gc::new(); 1777 + let r = gc.alloc(HeapObject::Object(ObjectData::new())); 1778 + let a = Value::Object(r); 1779 + let b = a.clone(); 1780 + assert!(strict_eq(&a, &b)); // Same GcRef → strict equal. 1586 1781 } 1587 1782 }