//! 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 crate::shape::{PropertyAttrs, ShapeId, ShapeTable}; use std::cell::RefCell; use std::collections::HashMap; use std::fmt; use std::rc::Rc; use we_dom::Document; // ── Heap objects (GC-managed) ──────────────────────────────── /// A GC-managed heap object: a plain object, a function, a closure cell, or a generator. pub enum HeapObject { Object(ObjectData), Function(Box), /// A mutable cell holding one Value — used for closure-captured variables. Cell(Value), /// A suspended generator function instance. Generator(Box), } /// State of a generator object. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GeneratorState { /// Created but next() not yet called. NotStarted, /// Suspended at a yield point. Suspended, /// Currently executing (re-entrancy guard). Executing, /// Completed (returned or threw). Completed, } /// Data for a suspended generator function. pub struct GeneratorData { pub state: GeneratorState, /// The generator function's bytecode. pub func: Function, /// Captured upvalues. pub upvalues: Vec, /// Saved register file for this generator's frame. pub registers: Vec, /// Saved instruction pointer (where to resume). pub ip: usize, /// The GcRef of the result prototype (for {value, done} objects). pub prototype: Option, /// Saved exception handlers (for try/catch across await/yield points). pub exception_handlers: Vec<(usize, Reg)>, } impl Traceable for HeapObject { fn trace(&self, visitor: &mut dyn FnMut(GcRef)) { match self { HeapObject::Object(data) => { data.trace_values(visitor); if let Some(proto) = data.prototype { visitor(proto); } } HeapObject::Function(fdata) => { if let Some(proto) = fdata.prototype_obj { visitor(proto); } for prop in fdata.properties.values() { if let Some(r) = prop.value.gc_ref() { visitor(r); } } for &uv in &fdata.upvalues { visitor(uv); } } HeapObject::Cell(val) => { if let Some(r) = val.gc_ref() { visitor(r); } } HeapObject::Generator(gen) => { for &uv in &gen.upvalues { visitor(uv); } for val in &gen.registers { if let Some(r) = val.gc_ref() { visitor(r); } } if let Some(proto) = gen.prototype { 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, } } } /// Result of a property removal attempt. pub enum RemoveResult { /// Property was removed successfully. Removed, /// Property was not found. NotFound, /// Property exists but is non-configurable. NonConfigurable, } /// Internal storage representation for object properties. pub enum ObjectStorage { /// Fast mode: shape-based indexed slots. Shaped { shape: ShapeId, slots: Vec }, /// Dictionary mode: fallback after property deletion or attribute changes. Dictionary(HashMap), } /// A JS plain object. Properties are stored either in shape-indexed slots /// (fast path) or a HashMap (dictionary mode, after deletion/freeze/seal). pub struct ObjectData { pub storage: ObjectStorage, pub prototype: Option, /// Whether new properties can be added (Object.preventExtensions). pub extensible: bool, } impl ObjectData { pub fn new() -> Self { Self { storage: ObjectStorage::Shaped { shape: ShapeId::ROOT, slots: Vec::new(), }, prototype: None, extensible: true, } } // ── Core property operations ────────────────────────────── /// Get a property descriptor (cloned) by name. pub fn get_property(&self, key: &str, shapes: &ShapeTable) -> Option { match &self.storage { ObjectStorage::Shaped { shape, slots } => { let desc = shapes.lookup(*shape, key)?; Some(Property { value: slots[desc.index as usize].clone(), writable: desc.attrs.writable, enumerable: desc.attrs.enumerable, configurable: desc.attrs.configurable, }) } ObjectStorage::Dictionary(map) => map.get(key).cloned(), } } /// Check if the object has an own property with the given name. pub fn contains_key(&self, key: &str, shapes: &ShapeTable) -> bool { match &self.storage { ObjectStorage::Shaped { shape, .. } => shapes.lookup(*shape, key).is_some(), ObjectStorage::Dictionary(map) => map.contains_key(key), } } /// Insert or overwrite a property. In shaped mode, if the property already /// exists, updates its slot value (ignoring attribute differences). If it /// does not exist, transitions to a child shape. pub fn insert_property(&mut self, key: String, prop: Property, shapes: &mut ShapeTable) { let attrs = PropertyAttrs { writable: prop.writable, enumerable: prop.enumerable, configurable: prop.configurable, }; match &mut self.storage { ObjectStorage::Shaped { shape, slots } => { if let Some(desc) = shapes.lookup(*shape, &key) { // Property exists — update value in-place. slots[desc.index as usize] = prop.value; } else { // New property — transition shape. let new_shape = shapes.add_property(*shape, &key, attrs); *shape = new_shape; slots.push(prop.value); } } ObjectStorage::Dictionary(map) => { map.insert(key, prop); } } } /// Update the value of an existing property if it is writable. /// Returns true if the update happened. pub fn update_value(&mut self, key: &str, val: Value, shapes: &ShapeTable) -> bool { match &mut self.storage { ObjectStorage::Shaped { shape, slots } => { if let Some(desc) = shapes.lookup(*shape, key) { if desc.attrs.writable { slots[desc.index as usize] = val; return true; } } false } ObjectStorage::Dictionary(map) => { if let Some(prop) = map.get_mut(key) { if prop.writable { prop.value = val; return true; } } false } } } /// Set a property: update existing value if writable, or insert new. pub fn set_property(&mut self, key: String, val: Value, shapes: &mut ShapeTable) { match &mut self.storage { ObjectStorage::Shaped { shape, slots } => { if let Some(desc) = shapes.lookup(*shape, &key) { if desc.attrs.writable { slots[desc.index as usize] = val; } } else { let new_shape = shapes.add_property(*shape, &key, PropertyAttrs::DEFAULT); *shape = new_shape; slots.push(val); } } ObjectStorage::Dictionary(map) => { if let Some(prop) = map.get_mut(&key) { if prop.writable { prop.value = val; } } else { map.insert(key, Property::data(val)); } } } } /// Remove a property. Transitions to dictionary mode in shaped mode. /// Returns `None` if not found, `Some(property)` if removed. /// Returns `Some` with configurable=false if removal was blocked. pub fn remove_property(&mut self, key: &str, shapes: &ShapeTable) -> RemoveResult { match &mut self.storage { ObjectStorage::Shaped { shape, .. } => { let desc = match shapes.lookup(*shape, key) { Some(d) => d, None => return RemoveResult::NotFound, }; if !desc.attrs.configurable { return RemoveResult::NonConfigurable; } // Convert to dictionary mode, then remove. self.to_dictionary(shapes); if let ObjectStorage::Dictionary(map) = &mut self.storage { map.remove(key); } RemoveResult::Removed } ObjectStorage::Dictionary(map) => { // Check if the property exists and is configurable. let configurable = match map.get(key) { Some(prop) => { if !prop.configurable { return RemoveResult::NonConfigurable; } true } None => return RemoveResult::NotFound, }; if configurable { map.remove(key); } RemoveResult::Removed } } } // ── Iteration ───────────────────────────────────────────── /// Collect all own properties as (name, Property) pairs. pub fn property_entries(&self, shapes: &ShapeTable) -> Vec<(String, Property)> { match &self.storage { ObjectStorage::Shaped { shape, slots } => shapes .all_properties(*shape) .into_iter() .map(|(name, desc)| { ( name, Property { value: slots[desc.index as usize].clone(), writable: desc.attrs.writable, enumerable: desc.attrs.enumerable, configurable: desc.attrs.configurable, }, ) }) .collect(), ObjectStorage::Dictionary(map) => { map.iter().map(|(k, v)| (k.clone(), v.clone())).collect() } } } /// Collect all own property names. pub fn property_keys(&self, shapes: &ShapeTable) -> Vec { match &self.storage { ObjectStorage::Shaped { shape, .. } => shapes .all_properties(*shape) .into_iter() .map(|(name, _)| name) .collect(), ObjectStorage::Dictionary(map) => map.keys().cloned().collect(), } } /// Number of own properties. pub fn property_count(&self) -> usize { match &self.storage { ObjectStorage::Shaped { slots, .. } => slots.len(), ObjectStorage::Dictionary(map) => map.len(), } } /// Whether the object has no own properties. pub fn is_empty(&self) -> bool { self.property_count() == 0 } // ── Dictionary mode transitions ─────────────────────────── /// Convert from shaped mode to dictionary mode. pub fn to_dictionary(&mut self, shapes: &ShapeTable) { if let ObjectStorage::Shaped { shape, slots } = &self.storage { let props = shapes.all_properties(*shape); let mut map = HashMap::with_capacity(props.len()); for (name, desc) in props { map.insert( name, Property { value: slots[desc.index as usize].clone(), writable: desc.attrs.writable, enumerable: desc.attrs.enumerable, configurable: desc.attrs.configurable, }, ); } self.storage = ObjectStorage::Dictionary(map); } } /// Apply a function to every property (transitions to dict mode if shaped). pub fn modify_all_properties(&mut self, shapes: &ShapeTable, mut f: impl FnMut(&mut Property)) { self.to_dictionary(shapes); if let ObjectStorage::Dictionary(map) = &mut self.storage { for prop in map.values_mut() { f(prop); } } } /// Retain only properties for which the predicate returns true. pub fn retain_properties( &mut self, shapes: &ShapeTable, f: impl FnMut(&str, &Property) -> bool, ) { self.to_dictionary(shapes); if let ObjectStorage::Dictionary(map) = &mut self.storage { let mut f = f; map.retain(|k, v| f(k, v)); } } // ── GC tracing ──────────────────────────────────────────── /// Trace all values reachable from this object's properties. pub fn trace_values(&self, visitor: &mut dyn FnMut(GcRef)) { match &self.storage { ObjectStorage::Shaped { slots, .. } => { for val in slots { if let Some(r) = val.gc_ref() { visitor(r); } } } ObjectStorage::Dictionary(map) => { for prop in map.values() { if let Some(r) = prop.value.gc_ref() { visitor(r); } } } } } } impl Default for ObjectData { fn default() -> Self { Self::new() } } /// A runtime function value: either bytecode or native. /// /// In JavaScript, functions are objects and can have arbitrary properties /// (e.g. `assert.sameValue = function() {}`). pub struct FunctionData { pub name: String, pub kind: FunctionKind, /// The `.prototype` property object (for use as a constructor with `instanceof`). pub prototype_obj: Option, /// Arbitrary properties set on this function (functions are objects in JS). pub properties: HashMap, /// Captured upvalue cells (GcRefs to HeapObject::Cell values). pub upvalues: Vec, } #[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], &mut NativeContext) -> Result, } /// Trait for console output. Allows redirecting console output to a dev tools /// panel or capturing it in tests. The default implementation writes to /// stdout/stderr. pub trait ConsoleOutput { fn log(&self, message: &str); fn error(&self, message: &str); fn warn(&self, message: &str); } /// Default console output that writes to stdout/stderr. pub struct StdConsoleOutput; impl ConsoleOutput for StdConsoleOutput { fn log(&self, message: &str) { println!("{}", message); } fn error(&self, message: &str) { eprintln!("{}", message); } fn warn(&self, message: &str) { eprintln!("{}", message); } } /// A single event listener registered on a DOM node. pub struct EventListener { pub event_type: String, pub callback: GcRef, pub capture: bool, pub once: bool, } /// Bridge between JS and the DOM. Holds a shared document and a cache /// mapping `NodeId` indices to their JS wrapper `GcRef` so that the same /// DOM node always returns the same JS object (identity). pub struct DomBridge { pub document: RefCell, pub node_wrappers: RefCell>, /// Event listeners keyed by NodeId index. pub event_listeners: RefCell>>, /// The serialized origin of this document (e.g. "https://example.com"). /// Used for Same-Origin Policy enforcement on cross-origin DOM access. pub origin: RefCell, /// Cookie jar shared with the network layer for `document.cookie` access. pub cookie_jar: RefCell, /// The URL of the current document, used for cookie domain/path matching. pub document_url: RefCell>, /// localStorage area for the current origin. pub local_storage: RefCell, /// sessionStorage area for the current browsing context. pub session_storage: RefCell, /// IndexedDB state for the current origin. pub indexeddb: RefCell, /// Window proxies for iframe elements, keyed by the iframe's NodeId index. /// Used to implement `contentWindow` / `contentDocument`. pub iframe_windows: RefCell>, } /// Context passed to native functions, providing GC access and `this` binding. pub struct NativeContext<'a> { pub gc: &'a mut Gc, pub shapes: &'a mut ShapeTable, pub this: Value, pub console_output: &'a dyn ConsoleOutput, pub dom_bridge: Option<&'a DomBridge>, } // ── 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. pub(crate) 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(), } } pub fn syntax_error(msg: impl Into) -> Self { Self { kind: ErrorKind::SyntaxError, message: msg.into(), } } /// Convert to a JS Value (an error object). Allocates through the GC. pub fn to_value(&self, gc: &mut Gc, shapes: &mut ShapeTable) -> Value { let mut obj = ObjectData::new(); obj.insert_property( "message".to_string(), Property::data(Value::String(self.message.clone())), shapes, ); let name = match self.kind { ErrorKind::TypeError => "TypeError", ErrorKind::ReferenceError => "ReferenceError", ErrorKind::RangeError => "RangeError", ErrorKind::SyntaxError => "SyntaxError", ErrorKind::Error => "Error", }; obj.insert_property( "name".to_string(), Property::data(Value::String(name.to_string())), shapes, ); Value::Object(gc.alloc(HeapObject::Object(obj))) } } // ── Property access helpers ────────────────────────────────── /// Get an object's own property value using IC, also returning shape+slot for IC /// update if the property was found as an own property on a shaped object. fn gc_get_property_ic( gc: &Gc, obj_ref: GcRef, key: &str, shapes: &ShapeTable, ) -> (Value, Option<(ShapeId, u32)>) { let proto = match gc.get(obj_ref) { Some(HeapObject::Object(data)) => { if let ObjectStorage::Shaped { shape, slots } = &data.storage { if let Some(desc) = shapes.lookup(*shape, key) { return ( slots[desc.index as usize].clone(), Some((*shape, desc.index)), ); } } else if let ObjectStorage::Dictionary(map) = &data.storage { if let Some(prop) = map.get(key) { return (prop.value.clone(), None); } } data.prototype } Some(HeapObject::Function(fdata)) => { if let Some(prop) = fdata.properties.get(key) { return (prop.value.clone(), None); } if key == "prototype" { if let Some(proto_ref) = fdata.prototype_obj { return (Value::Object(proto_ref), None); } return (Value::Undefined, None); } None } _ => return (Value::Undefined, None), }; if let Some(proto_ref) = proto { // Prototype chain hit — do NOT provide IC info (only own properties). let val = gc_get_property(gc, proto_ref, key, shapes); (val, None) } else { (Value::Undefined, None) } } /// Get a property from an object, walking the prototype chain. fn gc_get_property(gc: &Gc, obj_ref: GcRef, key: &str, shapes: &ShapeTable) -> Value { let proto = { match gc.get(obj_ref) { Some(HeapObject::Object(data)) => { if let Some(prop) = data.get_property(key, shapes) { return prop.value.clone(); } data.prototype } Some(HeapObject::Function(fdata)) => { // Check user-defined properties first. if let Some(prop) = fdata.properties.get(key) { return prop.value.clone(); } // 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, shapes) } else { Value::Undefined } } /// Check if an object has a property (own or inherited). fn gc_has_property(gc: &Gc, obj_ref: GcRef, key: &str, shapes: &ShapeTable) -> bool { let proto = { match gc.get(obj_ref) { Some(HeapObject::Object(data)) => { if data.contains_key(key, shapes) { return true; } data.prototype } Some(HeapObject::Function(fdata)) => { if fdata.properties.contains_key(key) || key == "prototype" { return true; } None } _ => return false, } }; if let Some(proto_ref) = proto { gc_has_property(gc, proto_ref, key, shapes) } 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, shapes: &ShapeTable) -> 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.property_entries(shapes) { 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, /// Captured upvalue cells from the closure that created this call frame. upvalues: 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, /// Shape table for object property layout tracking. pub shapes: ShapeTable, /// Optional instruction limit. If set, the VM will return an error after /// executing this many instructions (prevents infinite loops). instruction_limit: Option, /// Number of instructions executed so far. instructions_executed: u64, /// Built-in Object.prototype (root of the prototype chain). pub object_prototype: Option, /// Built-in Array.prototype (set on newly created arrays). pub array_prototype: Option, /// Built-in String.prototype (for primitive auto-boxing). pub string_prototype: Option, /// Built-in Number.prototype (for primitive auto-boxing). pub number_prototype: Option, /// Built-in Boolean.prototype (for primitive auto-boxing). pub boolean_prototype: Option, /// Built-in Date.prototype (for Date constructor objects). pub date_prototype: Option, /// Built-in RegExp.prototype (for RegExp constructor objects). pub regexp_prototype: Option, /// Built-in Promise.prototype (for Promise objects). pub promise_prototype: Option, /// Console output sink (configurable for dev tools or testing). console_output: Box, /// DOM bridge for JS-DOM interop (set via `attach_document`). pub(crate) dom_bridge: Option>, } /// Maximum register file size. const MAX_REGISTERS: usize = 4096; /// Maximum call depth. const MAX_CALL_DEPTH: usize = 512; impl Vm { pub fn new() -> Self { let mut vm = Self { registers: vec![Value::Undefined; 256], frames: Vec::new(), globals: HashMap::new(), gc: Gc::new(), shapes: ShapeTable::new(), instruction_limit: None, instructions_executed: 0, object_prototype: None, array_prototype: None, string_prototype: None, number_prototype: None, boolean_prototype: None, date_prototype: None, regexp_prototype: None, promise_prototype: None, console_output: Box::new(StdConsoleOutput), dom_bridge: None, }; crate::builtins::init_builtins(&mut vm); vm } /// Replace the console output sink (e.g. for dev tools or testing). pub fn set_console_output(&mut self, output: Box) { self.console_output = output; } /// Attach a DOM document to this VM, registering the `document` global /// and enabling DOM-JS interop. pub fn attach_document(&mut self, doc: Document) { let bridge = Rc::new(DomBridge { document: RefCell::new(doc), node_wrappers: RefCell::new(HashMap::new()), event_listeners: RefCell::new(HashMap::new()), origin: RefCell::new(String::new()), cookie_jar: RefCell::new(we_net::cookie::CookieJar::new()), document_url: RefCell::new(None), local_storage: RefCell::new(crate::storage::StorageArea::new()), session_storage: RefCell::new(crate::storage::StorageArea::new()), indexeddb: RefCell::new(crate::indexeddb::IndexedDbState::new()), iframe_windows: RefCell::new(HashMap::new()), }); self.dom_bridge = Some(bridge); crate::dom_bridge::init_document_object(self); crate::dom_bridge::init_event_system(self); crate::dom_bridge::init_storage_objects(self); crate::indexeddb::init_indexeddb(self); } /// Set the document origin for Same-Origin Policy enforcement. /// /// This should be called after `attach_document` and before executing /// scripts. The origin is used for cross-origin DOM access checks /// (relevant when iframes are implemented). pub fn set_document_origin(&mut self, origin: &str) { if let Some(bridge) = &self.dom_bridge { *bridge.origin.borrow_mut() = origin.to_string(); } } /// Set the document URL for cookie domain/path matching. pub fn set_document_url(&mut self, url: we_url::Url) { if let Some(bridge) = &self.dom_bridge { *bridge.document_url.borrow_mut() = Some(url); } } /// Set the cookie jar on the DOM bridge (typically from the HTTP client). pub fn set_cookie_jar(&mut self, jar: we_net::cookie::CookieJar) { if let Some(bridge) = &self.dom_bridge { *bridge.cookie_jar.borrow_mut() = jar; } } /// Take the cookie jar from the DOM bridge (to return to the HTTP client). pub fn take_cookie_jar(&mut self) -> Option { self.dom_bridge .as_ref() .map(|bridge| bridge.cookie_jar.replace(we_net::cookie::CookieJar::new())) } /// Set the localStorage area (typically loaded from disk by the browser). pub fn set_local_storage(&mut self, area: crate::storage::StorageArea) { if let Some(bridge) = &self.dom_bridge { *bridge.local_storage.borrow_mut() = area; } } /// Take the localStorage area from the DOM bridge (to persist to disk). pub fn take_local_storage(&mut self) -> Option { self.dom_bridge.as_ref().map(|bridge| { bridge .local_storage .replace(crate::storage::StorageArea::new()) }) } /// Set the sessionStorage area. pub fn set_session_storage(&mut self, area: crate::storage::StorageArea) { if let Some(bridge) = &self.dom_bridge { *bridge.session_storage.borrow_mut() = area; } } /// Take the sessionStorage area from the DOM bridge. pub fn take_session_storage(&mut self) -> Option { self.dom_bridge.as_ref().map(|bridge| { bridge .session_storage .replace(crate::storage::StorageArea::new()) }) } /// Set the IndexedDB state (typically loaded from disk by the browser). pub fn set_indexeddb(&mut self, state: crate::indexeddb::IndexedDbState) { if let Some(bridge) = &self.dom_bridge { *bridge.indexeddb.borrow_mut() = state; } } /// Take the IndexedDB state from the DOM bridge (to persist to disk). pub fn take_indexeddb(&mut self) -> Option { self.dom_bridge.as_ref().map(|bridge| { bridge .indexeddb .replace(crate::indexeddb::IndexedDbState::new()) }) } /// Register an iframe's window proxy so that `contentWindow` can return it. /// /// `iframe_node_idx` is the `NodeId::index()` of the iframe element in the /// parent document. `window_ref` is the GcRef of the window object for the /// iframe's browsing context. pub fn set_iframe_window(&mut self, iframe_node_idx: usize, window_ref: GcRef) { if let Some(bridge) = &self.dom_bridge { bridge .iframe_windows .borrow_mut() .insert(iframe_node_idx, window_ref); } } /// Detach the DOM document from the VM, returning it. /// /// This removes the `document` global and disconnects the DOM bridge. /// Returns `None` if no document was attached or if there are outstanding /// references to the bridge. pub fn detach_document(&mut self) -> Option { let bridge = self.dom_bridge.take()?; self.globals.remove("document"); match Rc::try_unwrap(bridge) { Ok(bridge) => Some(bridge.document.into_inner()), Err(rc) => { // Something still holds a reference — reattach. self.dom_bridge = Some(rc); None } } } /// Set an instruction limit. The VM will return a RuntimeError after /// executing this many instructions. pub fn set_instruction_limit(&mut self, limit: u64) { self.instruction_limit = Some(limit); } /// 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(), upvalues: Vec::new(), }); let result = self.run()?; self.drain_microtasks()?; Ok(result) } /// Call a function (native or bytecode) from outside the execution loop. /// Used by the microtask drain to execute promise callbacks. pub fn call_function( &mut self, func_ref: GcRef, args: &[Value], ) -> Result { let (kind, upvalues) = match self.gc.get(func_ref) { Some(HeapObject::Function(f)) => (f.kind.clone(), f.upvalues.clone()), _ => return Err(RuntimeError::type_error("not a function")), }; match kind { FunctionKind::Native(native) => { // Set async resume data if this function has it. if let Some(HeapObject::Function(f)) = self.gc.get(func_ref) { if let Some(prop) = f.properties.get("__async_data__") { if let Value::Object(data_ref) = &prop.value { ASYNC_RESUME_DATA.with(|cell| cell.set(Some(*data_ref))); } } } let this = self .globals .get("this") .cloned() .unwrap_or(Value::Undefined); let dom_ref = self.dom_bridge.as_deref(); let mut ctx = NativeContext { gc: &mut self.gc, shapes: &mut self.shapes, this, console_output: &*self.console_output, dom_bridge: dom_ref, }; let result = (native.callback)(args, &mut ctx)?; // Check for generator resume marker. if let Value::Object(r) = &result { let is_gen_resume = matches!( gc_get_property(&self.gc, *r, "__generator_resume__", &self.shapes), Value::Boolean(true) ); if is_gen_resume { let gen_ref = match gc_get_property(&self.gc, *r, "__gen_ref__", &self.shapes) { Value::Object(gr) => gr, _ => return Ok(Value::Undefined), }; let send_val = gc_get_property(&self.gc, *r, "__send_value__", &self.shapes); let kind_str = match gc_get_property(&self.gc, *r, "__resume_kind__", &self.shapes) { Value::String(s) => s, _ => "next".to_string(), }; return match kind_str.as_str() { "next" => self.run_generator(gen_ref, send_val), "return" => { if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.state = GeneratorState::Completed; } Ok(self.make_iterator_result(send_val, true)) } "throw" => { if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.state = GeneratorState::Completed; } Err(RuntimeError::type_error("Generator throw")) } _ => Ok(Value::Undefined), }; } // Check for async resume marker. let is_async_resume = matches!( gc_get_property(&self.gc, *r, "__async_resume__", &self.shapes), Value::Boolean(true) ); if is_async_resume { let gen_ref = match gc_get_property(&self.gc, *r, "__gen_ref__", &self.shapes) { Value::Object(gr) => gr, _ => return Ok(Value::Undefined), }; let result_promise = match gc_get_property(&self.gc, *r, "__result_promise__", &self.shapes) { Value::Object(pr) => pr, _ => return Ok(Value::Undefined), }; let is_throw = matches!( gc_get_property(&self.gc, *r, "__is_throw__", &self.shapes), Value::Boolean(true) ); let value = gc_get_property(&self.gc, *r, "__value__", &self.shapes); self.drive_async_step(gen_ref, result_promise, value, is_throw); return Ok(Value::Undefined); } // Check for async generator resume marker. let is_ag_resume = matches!( gc_get_property(&self.gc, *r, "__async_generator_resume__", &self.shapes), Value::Boolean(true) ); if is_ag_resume { let gen_ref = match gc_get_property(&self.gc, *r, "__gen_ref__", &self.shapes) { Value::Object(gr) => gr, _ => return Ok(Value::Undefined), }; let send_val = gc_get_property(&self.gc, *r, "__send_value__", &self.shapes); let kind_str = match gc_get_property(&self.gc, *r, "__resume_kind__", &self.shapes) { Value::String(s) => s, _ => "next".to_string(), }; // Create a promise for the result. let promise = crate::builtins::create_promise_object_pub( &mut self.gc, &mut self.shapes, ); match kind_str.as_str() { "next" => match self.run_generator(gen_ref, send_val) { Ok(iter_result) => { crate::builtins::resolve_promise_internal( &mut self.gc, &mut self.shapes, promise, iter_result, ); } Err(err) => { let reason = err.to_value(&mut self.gc, &mut self.shapes); crate::builtins::reject_promise_internal( &mut self.gc, &mut self.shapes, promise, reason, ); } }, "return" => { if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.state = GeneratorState::Completed; } let result = self.make_iterator_result(send_val, true); crate::builtins::resolve_promise_internal( &mut self.gc, &mut self.shapes, promise, result, ); } _ => {} } return Ok(Value::Object(promise)); } // Check for event dispatch marker. let is_event_dispatch = matches!( gc_get_property(&self.gc, *r, "__event_dispatch__", &self.shapes), Value::Boolean(true) ); if is_event_dispatch { let target_idx = match gc_get_property(&self.gc, *r, "__target_id__", &self.shapes) { Value::Number(n) => n as usize, _ => return Ok(Value::Boolean(true)), }; let evt_ref = match gc_get_property(&self.gc, *r, "__event_ref__", &self.shapes) { Value::Object(er) => er, _ => return Ok(Value::Boolean(true)), }; return Ok(crate::dom_bridge::run_event_dispatch( self, target_idx, evt_ref, )); } } Ok(result) } FunctionKind::Bytecode(bc) => { let callee_func = bc.func; // Async function: create generator + promise, drive async. if callee_func.is_async && !callee_func.is_generator { let gen_ref = self.create_raw_generator(callee_func, upvalues, args); let result_promise = crate::builtins::create_promise_object_pub(&mut self.gc, &mut self.shapes); self.drive_async_step(gen_ref, result_promise, Value::Undefined, false); return Ok(Value::Object(result_promise)); } // Async generator function: create async generator wrapper. if callee_func.is_async && callee_func.is_generator { let gen_ref = self.create_raw_generator(callee_func, upvalues, args); let wrapper = self.create_async_generator_wrapper(gen_ref); return Ok(Value::Object(wrapper)); } // Generator function: create a generator object instead of executing. if callee_func.is_generator { let gen_obj = self.create_generator_object(callee_func, upvalues, args); return Ok(Value::Object(gen_obj)); } // Save current frames and run the function in isolation. let saved_frames = std::mem::take(&mut self.frames); // Compute base after any existing register usage. let base = saved_frames .last() .map(|f| f.base + f.func.register_count as usize) .unwrap_or(0); let reg_count = callee_func.register_count as usize; self.ensure_registers(base + reg_count); // Copy arguments. let param_count = callee_func.param_count as usize; for (i, arg) in args.iter().enumerate() { if i < param_count { self.registers[base + i] = arg.clone(); } } for i in args.len()..param_count { self.registers[base + i] = Value::Undefined; } self.frames.push(CallFrame { func: callee_func, ip: 0, base, return_reg: base, exception_handlers: Vec::new(), upvalues, }); let result = self.run(); // Restore saved frames. self.frames = saved_frames; result } } } /// Drain the microtask queue. Called after execute() and recursively /// until no more microtasks are pending. pub(crate) fn drain_microtasks(&mut self) -> Result<(), RuntimeError> { loop { let tasks = crate::builtins::take_microtasks(); if tasks.is_empty() { break; } for task in tasks { match task.handler { Some(handler_ref) => { // Call the handler with the value. let result = self.call_function(handler_ref, std::slice::from_ref(&task.value)); if let Some(chained) = task.chained_promise { // Check if this is a "finally" chain. let is_finally = matches!( crate::builtins::promise_get_prop_pub( &self.gc, &self.shapes, chained, "__finally__" ), Value::Boolean(true) ); if is_finally { // finally: ignore handler result, propagate parent's result. match result { Ok(_) => { let parent = crate::builtins::promise_get_prop_pub( &self.gc, &self.shapes, chained, "__finally_parent__", ); if let Some(parent_ref) = parent.gc_ref() { let parent_state = crate::builtins::promise_state_pub( &self.gc, &self.shapes, parent_ref, ); let parent_result = crate::builtins::promise_get_prop_pub( &self.gc, &self.shapes, parent_ref, crate::builtins::PROMISE_RESULT_KEY, ); if parent_state == crate::builtins::PROMISE_FULFILLED { crate::builtins::resolve_promise_internal( &mut self.gc, &mut self.shapes, chained, parent_result, ); } else { crate::builtins::reject_promise_internal( &mut self.gc, &mut self.shapes, chained, parent_result, ); } } } Err(err) => { let err_val = err.to_value(&mut self.gc, &mut self.shapes); crate::builtins::reject_promise_internal( &mut self.gc, &mut self.shapes, chained, err_val, ); } } } else { match result { Ok(val) => { // If result is a promise, chain it. if crate::builtins::is_promise_pub( &self.gc, &self.shapes, &val, ) { if let Some(val_ref) = val.gc_ref() { let state = crate::builtins::promise_state_pub( &self.gc, &self.shapes, val_ref, ); if state == crate::builtins::PROMISE_FULFILLED { let r = crate::builtins::promise_get_prop_pub( &self.gc, &self.shapes, val_ref, crate::builtins::PROMISE_RESULT_KEY, ); crate::builtins::resolve_promise_internal( &mut self.gc, &mut self.shapes, chained, r, ); } else if state == crate::builtins::PROMISE_REJECTED { let r = crate::builtins::promise_get_prop_pub( &self.gc, &self.shapes, val_ref, crate::builtins::PROMISE_RESULT_KEY, ); crate::builtins::reject_promise_internal( &mut self.gc, &mut self.shapes, chained, r, ); } else { crate::builtins::chain_promise_pub( &mut self.gc, &mut self.shapes, val_ref, chained, ); } } } else { crate::builtins::resolve_promise_internal( &mut self.gc, &mut self.shapes, chained, val, ); } } Err(err) => { let err_val = err.to_value(&mut self.gc, &mut self.shapes); crate::builtins::reject_promise_internal( &mut self.gc, &mut self.shapes, chained, err_val, ); } } } } } None => { // No handler: identity for fulfillment, thrower for rejection. if let Some(chained) = task.chained_promise { if task.is_fulfillment { crate::builtins::resolve_promise_internal( &mut self.gc, &mut self.shapes, chained, task.value, ); } else { crate::builtins::reject_promise_internal( &mut self.gc, &mut self.shapes, chained, task.value, ); } } } } } } Ok(()) } /// Execute all due timer callbacks. After each callback, drain the /// microtask queue (so Promise `.then()` runs between timer callbacks). fn drain_due_timers(&mut self) -> Result<(), RuntimeError> { let due = crate::timers::take_due_timers(); for timer in due { // For requestAnimationFrame, pass the timestamp as an argument. let args = match timer.raf_timestamp { Some(ts) => vec![Value::Number(ts)], None => vec![], }; let _ = self.call_function(timer.callback, &args); self.drain_microtasks()?; } Ok(()) } /// Resolve/reject promises for completed fetch() requests. fn drain_completed_fetches(&mut self) -> Result<(), RuntimeError> { let completed = crate::fetch::take_completed_fetches(); for fetch in completed { match fetch.result { Ok(result) => { let response = crate::fetch::create_response_object( &mut self.gc, &mut self.shapes, &result, ); crate::builtins::resolve_promise_internal( &mut self.gc, &mut self.shapes, fetch.promise, Value::Object(response), ); } Err(err_msg) => { let err_val = Value::String(err_msg); crate::builtins::reject_promise_internal( &mut self.gc, &mut self.shapes, fetch.promise, err_val, ); } } self.drain_microtasks()?; } Ok(()) } /// Pump the event loop: drain due timers, completed fetches, IDB events, /// and microtasks. pub fn pump_event_loop(&mut self) -> Result<(), RuntimeError> { self.drain_due_timers()?; self.drain_completed_fetches()?; crate::indexeddb::drain_idb_events(self) } /// Run the event loop until all pending timers, fetches, and IDB events /// have completed. Useful in tests to deterministically execute all /// scheduled work. `max_iterations` caps the loop to prevent infinite /// loops with recurring intervals; pass 0 for unlimited. pub fn run_event_loop(&mut self, max_iterations: usize) -> Result<(), RuntimeError> { let mut iterations = 0; while crate::timers::has_pending_timers() || crate::fetch::has_pending_fetches() || crate::indexeddb::has_pending_idb_events() { if max_iterations > 0 && iterations >= max_iterations { break; } self.drain_due_timers()?; self.drain_completed_fetches()?; crate::indexeddb::drain_idb_events(self)?; iterations += 1; // If work is still pending, sleep briefly to avoid spinning. if crate::timers::has_pending_timers() || crate::fetch::has_pending_fetches() { std::thread::sleep(std::time::Duration::from_millis(1)); } } Ok(()) } /// 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) } // ── Generator helpers ────────────────────────────────────── /// Create a generator object from a generator function. fn create_generator_object( &mut self, func: Function, upvalues: Vec, args: &[Value], ) -> GcRef { // Pre-fill registers with arguments. let reg_count = func.register_count as usize; let mut regs = vec![Value::Undefined; reg_count]; for (i, arg) in args.iter().enumerate() { if i < func.param_count as usize { regs[i] = arg.clone(); } } let gen_data = GeneratorData { state: GeneratorState::NotStarted, func, upvalues, registers: regs, ip: 0, prototype: self.object_prototype, exception_handlers: Vec::new(), }; let gen_ref = self.gc.alloc(HeapObject::Generator(Box::new(gen_data))); // Wrap in an object with next/return/throw methods. let mut obj = ObjectData::new(); obj.prototype = self.object_prototype; // Store the generator GcRef so methods can find it. obj.insert_property( "__gen__".to_string(), Property { value: Value::Object(gen_ref), writable: false, enumerable: false, configurable: false, }, &mut self.shapes, ); // next() method let next_fn = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "next".to_string(), kind: FunctionKind::Native(NativeFunc { callback: generator_next, }), prototype_obj: None, properties: HashMap::new(), upvalues: Vec::new(), }))); obj.insert_property( "next".to_string(), Property::builtin(Value::Function(next_fn)), &mut self.shapes, ); // return() method let return_fn = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "return".to_string(), kind: FunctionKind::Native(NativeFunc { callback: generator_return, }), prototype_obj: None, properties: HashMap::new(), upvalues: Vec::new(), }))); obj.insert_property( "return".to_string(), Property::builtin(Value::Function(return_fn)), &mut self.shapes, ); // throw() method let throw_fn = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "throw".to_string(), kind: FunctionKind::Native(NativeFunc { callback: generator_throw, }), prototype_obj: None, properties: HashMap::new(), upvalues: Vec::new(), }))); obj.insert_property( "throw".to_string(), Property::builtin(Value::Function(throw_fn)), &mut self.shapes, ); // @@iterator method (generators are iterable - returns self) let iter_fn = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "[Symbol.iterator]".to_string(), kind: FunctionKind::Native(NativeFunc { callback: generator_symbol_iterator, }), prototype_obj: None, properties: HashMap::new(), upvalues: Vec::new(), }))); obj.insert_property( "@@iterator".to_string(), Property::builtin(Value::Function(iter_fn)), &mut self.shapes, ); self.gc.alloc(HeapObject::Object(obj)) } /// Create a raw GeneratorData (HeapObject::Generator) without the wrapper object. /// Used by async functions which manage their own driving logic. fn create_raw_generator( &mut self, func: Function, upvalues: Vec, args: &[Value], ) -> GcRef { let reg_count = func.register_count as usize; let mut regs = vec![Value::Undefined; reg_count]; for (i, arg) in args.iter().enumerate() { if i < func.param_count as usize { regs[i] = arg.clone(); } } let gen_data = GeneratorData { state: GeneratorState::NotStarted, func, upvalues, registers: regs, ip: 0, prototype: self.object_prototype, exception_handlers: Vec::new(), }; self.gc.alloc(HeapObject::Generator(Box::new(gen_data))) } /// Create a {value, done} iterator result object. fn make_iterator_result(&mut self, value: Value, done: bool) -> Value { let mut obj = ObjectData::new(); obj.prototype = self.object_prototype; obj.insert_property("value".to_string(), Property::data(value), &mut self.shapes); obj.insert_property( "done".to_string(), Property::data(Value::Boolean(done)), &mut self.shapes, ); let gc_ref = self.gc.alloc(HeapObject::Object(obj)); Value::Object(gc_ref) } /// Run a generator until its next yield/return. /// Returns the yielded/returned value. pub fn run_generator( &mut self, gen_ref: GcRef, send_value: Value, ) -> Result { // Extract generator data. let (func, upvalues, mut regs, ip, state, saved_exc_handlers) = match self.gc.get(gen_ref) { Some(HeapObject::Generator(gen)) => { if gen.state == GeneratorState::Completed { return Ok(self.make_iterator_result(Value::Undefined, true)); } if gen.state == GeneratorState::Executing { return Err(RuntimeError::type_error("Generator is already executing")); } ( gen.func.clone(), gen.upvalues.clone(), gen.registers.clone(), gen.ip, gen.state, gen.exception_handlers.clone(), ) } _ => return Err(RuntimeError::type_error("not a generator")), }; // Mark as executing. if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.state = GeneratorState::Executing; } // If resuming from a yield/await, write the sent value into the dst register. if state == GeneratorState::Suspended && ip >= 3 { // The Yield/Await instruction was: op dst, src (3 bytes total: op + dst + src) // After executing, ip points past it. The dst byte is at ip - 2. let dst_reg = func.code[ip - 2] as usize; regs[dst_reg] = send_value; } // Save current VM state. let saved_frames = std::mem::take(&mut self.frames); let saved_instructions = self.instructions_executed; // Use a base past any existing register usage to avoid clobbering // the caller's register file. let base = saved_frames .last() .map(|f| f.base + f.func.register_count as usize) .unwrap_or(0); let reg_count = func.register_count as usize; self.ensure_registers(base + reg_count + 1); // Set up registers for the generator. for (i, val) in regs.iter().enumerate() { self.registers[base + i] = val.clone(); } // Push frame. return_reg points to a slot that holds the generator ref // so Yield can find it. We use a slot just past the registers. self.registers[base + reg_count] = Value::Object(gen_ref); // Restore exception handlers. let exception_handlers = saved_exc_handlers .iter() .map(|&(catch_ip, catch_reg)| ExceptionHandler { catch_ip, catch_reg, }) .collect(); self.frames.push(CallFrame { func, ip, base, return_reg: base + reg_count, exception_handlers, upvalues, }); let result = self.run(); // Restore VM state. self.frames = saved_frames; self.instructions_executed = saved_instructions; match result { Ok(val) => { // Normal return from generator (either via Return or end of function). // Check if it was a Yield (state == Suspended) or a Return (state stays Executing). let gen_state = match self.gc.get(gen_ref) { Some(HeapObject::Generator(gen)) => gen.state, _ => GeneratorState::Completed, }; if gen_state == GeneratorState::Suspended { // Yield already created the result; `val` is the {value, done} object. Ok(val) } else { // Return: mark completed and wrap result. if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.state = GeneratorState::Completed; } Ok(self.make_iterator_result(val, true)) } } Err(err) => { // Generator threw: mark completed. if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.state = GeneratorState::Completed; } Err(err) } } } /// Throw a value into a suspended generator. Used for `await` on rejected /// promises — the rejection reason is thrown so that try/catch can handle it. fn throw_into_generator( &mut self, gen_ref: GcRef, throw_value: Value, ) -> Result { // Extract generator data. let (func, upvalues, regs, ip, state, saved_exc_handlers) = match self.gc.get(gen_ref) { Some(HeapObject::Generator(gen)) => { if gen.state == GeneratorState::Completed { return Ok(self.make_iterator_result(Value::Undefined, true)); } if gen.state == GeneratorState::Executing { return Err(RuntimeError::type_error("Generator is already executing")); } ( gen.func.clone(), gen.upvalues.clone(), gen.registers.clone(), gen.ip, gen.state, gen.exception_handlers.clone(), ) } _ => return Err(RuntimeError::type_error("not a generator")), }; if state == GeneratorState::NotStarted { if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.state = GeneratorState::Completed; } return Err(RuntimeError { kind: ErrorKind::Error, message: throw_value.to_js_string(&self.gc), }); } // Mark as executing. if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.state = GeneratorState::Executing; } // Save current VM state. let saved_frames = std::mem::take(&mut self.frames); let saved_instructions = self.instructions_executed; let base = saved_frames .last() .map(|f| f.base + f.func.register_count as usize) .unwrap_or(0); let reg_count = func.register_count as usize; self.ensure_registers(base + reg_count + 1); for (i, val) in regs.iter().enumerate() { self.registers[base + i] = val.clone(); } self.registers[base + reg_count] = Value::Object(gen_ref); // Restore exception handlers from the generator. let exception_handlers = saved_exc_handlers .iter() .map(|&(catch_ip, catch_reg)| ExceptionHandler { catch_ip, catch_reg, }) .collect(); self.frames.push(CallFrame { func, ip, base, return_reg: base + reg_count, exception_handlers, upvalues, }); // Instead of writing send_value to dst register, throw the value. let caught = self.handle_exception(throw_value); let result = if caught { self.run() } else { let msg = "Uncaught (in async)".to_string(); Err(RuntimeError { kind: ErrorKind::Error, message: msg, }) }; // Restore VM state. self.frames = saved_frames; self.instructions_executed = saved_instructions; match result { Ok(val) => { let gen_state = match self.gc.get(gen_ref) { Some(HeapObject::Generator(gen)) => gen.state, _ => GeneratorState::Completed, }; if gen_state == GeneratorState::Suspended { Ok(val) } else { if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.state = GeneratorState::Completed; } Ok(self.make_iterator_result(val, true)) } } Err(err) => { if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.state = GeneratorState::Completed; } Err(err) } } } // ── Iterator protocol helpers ──────────────────────────────── /// Get an iterator from a value by calling its [Symbol.iterator]() method. pub fn get_iterator(&mut self, iterable: &Value) -> Result { // Get the @@iterator property. let iter_fn = match iterable { Value::Object(gc_ref) | Value::Function(gc_ref) => { gc_get_property(&self.gc, *gc_ref, "@@iterator", &self.shapes) } Value::String(_) => { // Strings have @@iterator on their prototype. self.string_prototype .map(|p| gc_get_property(&self.gc, p, "@@iterator", &self.shapes)) .unwrap_or(Value::Undefined) } _ => Value::Undefined, }; let iter_fn_ref = match iter_fn { Value::Function(r) => r, _ => { return Err(RuntimeError::type_error( "object is not iterable (no Symbol.iterator)", )); } }; // Call [Symbol.iterator]() with `this` set to the iterable. // We temporarily set `this` in globals for the native call. let old_this = self.globals.get("this").cloned(); self.globals.insert("this".to_string(), iterable.clone()); let result = self.call_function(iter_fn_ref, &[]); match old_this { Some(v) => self.globals.insert("this".to_string(), v), None => self.globals.remove("this"), }; result } /// Call iterator.next() and return (value, done). pub fn iterator_next(&mut self, iterator: &Value) -> Result<(Value, bool), RuntimeError> { let iter_ref = match iterator { Value::Object(r) | Value::Function(r) => *r, _ => return Err(RuntimeError::type_error("iterator is not an object")), }; let next_fn = gc_get_property(&self.gc, iter_ref, "next", &self.shapes); let next_fn_ref = match next_fn { Value::Function(r) => r, _ => return Err(RuntimeError::type_error("iterator.next is not a function")), }; // Call next() with `this` = iterator. let old_this = self.globals.get("this").cloned(); self.globals.insert("this".to_string(), iterator.clone()); let result = self.call_function(next_fn_ref, &[])?; match old_this { Some(v) => self.globals.insert("this".to_string(), v), None => self.globals.remove("this"), }; // Extract value and done from the result object. let (value, done) = match result { Value::Object(r) => { let val = gc_get_property(&self.gc, r, "value", &self.shapes); let d = gc_get_property(&self.gc, r, "done", &self.shapes); (val, d.to_boolean()) } _ => (Value::Undefined, true), }; Ok((value, done)) } // ── Async function helpers ────────────────────────────────── /// Drive one step of an async function. Runs the internal generator until /// it yields (await) or returns, then wires up promise reactions for the /// next step. fn drive_async_step( &mut self, gen_ref: GcRef, result_promise: GcRef, send_value: Value, is_throw: bool, ) { let result = if is_throw { // Throw into the generator — resume it and throw so that // try/catch inside the async function can handle it. self.throw_into_generator(gen_ref, send_value) } else { self.run_generator(gen_ref, send_value) }; match result { Ok(iter_result) => { // Extract {value, done} from the iterator result. let (value, done) = match &iter_result { Value::Object(r) => { let val = gc_get_property(&self.gc, *r, "value", &self.shapes); let d = gc_get_property(&self.gc, *r, "done", &self.shapes); (val, d.to_boolean()) } _ => (Value::Undefined, true), }; if done { // Async function returned — resolve the result promise. crate::builtins::resolve_promise_internal( &mut self.gc, &mut self.shapes, result_promise, value, ); } else { // Async function awaited — set up promise chain to resume. self.setup_async_resume(gen_ref, result_promise, value); } } Err(err) => { // Async function threw — reject the result promise. // For plain `throw expr` (ErrorKind::Error), reject with the // message string to preserve the original thrown value. // For typed errors (TypeError, etc.), wrap in an error object. let reason = if err.kind == ErrorKind::Error { Value::String(err.message.clone()) } else { err.to_value(&mut self.gc, &mut self.shapes) }; crate::builtins::reject_promise_internal( &mut self.gc, &mut self.shapes, result_promise, reason, ); } } } /// Set up promise reactions so that when the awaited value settles, the /// async function resumes. fn setup_async_resume(&mut self, gen_ref: GcRef, result_promise: GcRef, awaited_value: Value) { // Create the fulfill callback. let fulfill_data = self.make_async_resume_data(gen_ref, result_promise, false); let fulfill_fn = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "__async_fulfill__".to_string(), kind: FunctionKind::Native(NativeFunc { callback: async_resume_callback, }), prototype_obj: None, properties: { let mut m = HashMap::new(); m.insert( "__async_data__".to_string(), Property::builtin(Value::Object(fulfill_data)), ); m }, upvalues: Vec::new(), }))); // Create the reject callback. let reject_data = self.make_async_resume_data(gen_ref, result_promise, true); let reject_fn = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "__async_reject__".to_string(), kind: FunctionKind::Native(NativeFunc { callback: async_resume_callback, }), prototype_obj: None, properties: { let mut m = HashMap::new(); m.insert( "__async_data__".to_string(), Property::builtin(Value::Object(reject_data)), ); m }, upvalues: Vec::new(), }))); // If the awaited value is a promise, react to it. if crate::builtins::is_promise_pub(&self.gc, &self.shapes, &awaited_value) { let val_ref = awaited_value.gc_ref().unwrap(); let state = crate::builtins::promise_state_pub(&self.gc, &self.shapes, val_ref); if state == crate::builtins::PROMISE_FULFILLED { let r = crate::builtins::promise_get_prop_pub( &self.gc, &self.shapes, val_ref, crate::builtins::PROMISE_RESULT_KEY, ); crate::builtins::enqueue_microtask_pub(crate::builtins::Microtask { handler: Some(fulfill_fn), value: r, chained_promise: None, is_fulfillment: true, }); } else if state == crate::builtins::PROMISE_REJECTED { let r = crate::builtins::promise_get_prop_pub( &self.gc, &self.shapes, val_ref, crate::builtins::PROMISE_RESULT_KEY, ); crate::builtins::enqueue_microtask_pub(crate::builtins::Microtask { handler: Some(reject_fn), value: r, chained_promise: None, is_fulfillment: false, }); } else { // Pending promise: add reactions. crate::builtins::add_reaction_pub( &mut self.gc, &mut self.shapes, val_ref, Value::Function(fulfill_fn), Value::Function(reject_fn), ); } } else { // Not a promise: resume immediately via microtask with the value. crate::builtins::enqueue_microtask_pub(crate::builtins::Microtask { handler: Some(fulfill_fn), value: awaited_value, chained_promise: None, is_fulfillment: true, }); } } /// Create an object holding the data needed to resume an async function. fn make_async_resume_data( &mut self, gen_ref: GcRef, result_promise: GcRef, is_throw: bool, ) -> GcRef { let mut data = ObjectData::new(); data.insert_property( "__gen_ref__".to_string(), Property::builtin(Value::Object(gen_ref)), &mut self.shapes, ); data.insert_property( "__result_promise__".to_string(), Property::builtin(Value::Object(result_promise)), &mut self.shapes, ); data.insert_property( "__is_throw__".to_string(), Property::builtin(Value::Boolean(is_throw)), &mut self.shapes, ); self.gc.alloc(HeapObject::Object(data)) } /// Create an async generator wrapper object (for `async function*`). fn create_async_generator_wrapper(&mut self, gen_ref: GcRef) -> GcRef { let mut obj = ObjectData::new(); obj.prototype = self.object_prototype; obj.insert_property( "__gen__".to_string(), Property { value: Value::Object(gen_ref), writable: false, enumerable: false, configurable: false, }, &mut self.shapes, ); // next() method — returns a Promise for the next iteration result. let next_fn = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "next".to_string(), kind: FunctionKind::Native(NativeFunc { callback: async_generator_next, }), prototype_obj: None, properties: HashMap::new(), upvalues: Vec::new(), }))); obj.insert_property( "next".to_string(), Property::builtin(Value::Function(next_fn)), &mut self.shapes, ); // return() method let return_fn = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "return".to_string(), kind: FunctionKind::Native(NativeFunc { callback: async_generator_return, }), prototype_obj: None, properties: HashMap::new(), upvalues: Vec::new(), }))); obj.insert_property( "return".to_string(), Property::builtin(Value::Function(return_fn)), &mut self.shapes, ); // @@asyncIterator method — returns self. let iter_fn = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "[Symbol.asyncIterator]".to_string(), kind: FunctionKind::Native(NativeFunc { callback: generator_symbol_iterator, }), prototype_obj: None, properties: HashMap::new(), upvalues: Vec::new(), }))); obj.insert_property( "@@asyncIterator".to_string(), Property::builtin(Value::Function(iter_fn)), &mut self.shapes, ); self.gc.alloc(HeapObject::Object(obj)) } /// Resolve a dynamic DOM property for a wrapper object. /// Returns `Some(value)` if the key is a recognized DOM property, `None` otherwise. fn resolve_dom_property(&mut self, gc_ref: GcRef, key: &str) -> Option { let bridge = Rc::clone(self.dom_bridge.as_ref()?); // Try Storage proxy properties first. if let Some(val) = crate::dom_bridge::resolve_storage_get(&self.gc, &self.shapes, &bridge, gc_ref, key) { return Some(val); } // Try window properties (parent, top, frames, length). if let Some(val) = crate::iframe_bridge::resolve_window_property(&self.gc, &self.shapes, gc_ref, key) { return Some(val); } // Try iframe element properties (contentWindow, contentDocument). if let Some(node_id) = crate::dom_bridge::get_node_id_pub(&self.gc, &self.shapes, gc_ref) { if let Some(val) = crate::iframe_bridge::resolve_iframe_property( &mut self.gc, &self.shapes, &bridge, node_id, key, ) { return Some(val); } } // Try node wrapper properties. if let Some(val) = crate::dom_bridge::resolve_dom_get(&mut self.gc, &mut self.shapes, &bridge, gc_ref, key) { return Some(val); } // Try document-level dynamic properties (e.g. document.cookie). crate::dom_bridge::resolve_document_get(&self.gc, &self.shapes, &bridge, gc_ref, key) } /// Handle a DOM property set on a wrapper object. /// Returns `true` if the property was handled (caller should skip normal set). fn handle_dom_property_set(&mut self, gc_ref: GcRef, key: &str, val: &Value) -> bool { if let Some(bridge) = self.dom_bridge.clone() { // Check for Storage proxy sets (localStorage["key"] = "val"). if crate::dom_bridge::handle_storage_set( &bridge, gc_ref, key, val, &self.gc, &self.shapes, ) { return true; } // Check for document-level dynamic properties (e.g. document.cookie). if crate::dom_bridge::handle_document_set( &bridge, gc_ref, key, val, &self.gc, &self.shapes, ) { return true; } // Check for style proxy objects. if crate::dom_bridge::handle_style_set( &mut self.gc, &mut self.shapes, &bridge, gc_ref, key, val, ) { return true; } // Then check for DOM node wrapper sets. crate::dom_bridge::handle_dom_set( &mut self.gc, &mut self.shapes, &bridge, gc_ref, key, val, ) } else { false } } /// 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); } } for frame in &self.frames { for &uv in &frame.upvalues { roots.push(uv); } } // Built-in prototype roots. if let Some(r) = self.object_prototype { roots.push(r); } if let Some(r) = self.array_prototype { roots.push(r); } if let Some(r) = self.string_prototype { roots.push(r); } if let Some(r) = self.number_prototype { roots.push(r); } if let Some(r) = self.boolean_prototype { roots.push(r); } if let Some(r) = self.promise_prototype { roots.push(r); } // DOM wrapper identity cache: keep cached wrappers alive so that the // same DOM node always returns the same JS object. if let Some(bridge) = &self.dom_bridge { for &wrapper_ref in bridge.node_wrappers.borrow().values() { roots.push(wrapper_ref); } // Event listener callbacks must also be GC roots. for listeners in bridge.event_listeners.borrow().values() { for listener in listeners { roots.push(listener.callback); } } } // Pending timer callbacks must be GC roots. roots.extend(crate::timers::timer_gc_roots()); // Pending fetch promises must be GC roots. roots.extend(crate::fetch::fetch_gc_roots()); // Pending IndexedDB event targets must be GC roots. roots.extend(crate::indexeddb::idb_gc_roots()); 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; } // Instruction limit check (for test harnesses). if let Some(limit) = self.instruction_limit { self.instructions_executed += 1; if self.instructions_executed > limit { return Err(RuntimeError { kind: ErrorKind::Error, message: "instruction limit exceeded".into(), }); } } 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, &self.shapes)) } _ => { 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, &mut self.shapes); 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( Box::new(bc.func.clone()), fdata.upvalues.clone(), ), }, _ => { let err = RuntimeError::type_error("not a function"); let err_val = err.to_value(&mut self.gc, &mut self.shapes); if !self.handle_exception(err_val) { return Err(err); } continue; } } }; match call_info { CallInfo::Native(callback) => { // Set async resume data if the function has it. if let Some(HeapObject::Function(f)) = self.gc.get(func_gc_ref) { if let Some(prop) = f.properties.get("__async_data__") { if let Value::Object(data_ref) = &prop.value { ASYNC_RESUME_DATA.with(|cell| cell.set(Some(*data_ref))); } } } let this = self .globals .get("this") .cloned() .unwrap_or(Value::Undefined); let dom_ref = self.dom_bridge.as_deref(); let mut ctx = NativeContext { gc: &mut self.gc, shapes: &mut self.shapes, this, console_output: &*self.console_output, dom_bridge: dom_ref, }; match callback(&args, &mut ctx) { Ok(val) => { // Check if this is a generator resume request. if let Value::Object(r) = &val { let is_resume = matches!( gc_get_property( &self.gc, *r, "__generator_resume__", &self.shapes ), Value::Boolean(true) ); if is_resume { let gen_ref = match gc_get_property( &self.gc, *r, "__gen_ref__", &self.shapes, ) { Value::Object(gr) => gr, _ => { self.registers[base + dst as usize] = Value::Undefined; continue; } }; let send_val = gc_get_property( &self.gc, *r, "__send_value__", &self.shapes, ); let kind = match gc_get_property( &self.gc, *r, "__resume_kind__", &self.shapes, ) { Value::String(s) => s, _ => "next".to_string(), }; match kind.as_str() { "next" => { match self.run_generator(gen_ref, send_val) { Ok(result) => { self.registers[base + dst as usize] = result; } Err(err) => { let err_val = err.to_value( &mut self.gc, &mut self.shapes, ); if !self.handle_exception(err_val) { return Err(err); } } } } "return" => { // Force the generator to complete. if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.state = GeneratorState::Completed; } let result = self.make_iterator_result(send_val, true); self.registers[base + dst as usize] = result; } "throw" => { // Mark generator as completed and throw. if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.state = GeneratorState::Completed; } if !self.handle_exception(send_val) { return Err(RuntimeError::type_error( "Generator throw", )); } } _ => { self.registers[base + dst as usize] = Value::Undefined; } } continue; } // Check for async resume marker. let is_async_resume = matches!( gc_get_property( &self.gc, *r, "__async_resume__", &self.shapes ), Value::Boolean(true) ); if is_async_resume { let ar_gen = match gc_get_property( &self.gc, *r, "__gen_ref__", &self.shapes, ) { Value::Object(gr) => gr, _ => { self.registers[base + dst as usize] = Value::Undefined; continue; } }; let ar_promise = match gc_get_property( &self.gc, *r, "__result_promise__", &self.shapes, ) { Value::Object(pr) => pr, _ => { self.registers[base + dst as usize] = Value::Undefined; continue; } }; let ar_throw = matches!( gc_get_property( &self.gc, *r, "__is_throw__", &self.shapes ), Value::Boolean(true) ); let ar_value = gc_get_property( &self.gc, *r, "__value__", &self.shapes, ); self.drive_async_step( ar_gen, ar_promise, ar_value, ar_throw, ); self.registers[base + dst as usize] = Value::Undefined; continue; } // Check for async generator resume marker. let is_ag_resume = matches!( gc_get_property( &self.gc, *r, "__async_generator_resume__", &self.shapes, ), Value::Boolean(true) ); if is_ag_resume { let ag_gen = match gc_get_property( &self.gc, *r, "__gen_ref__", &self.shapes, ) { Value::Object(gr) => gr, _ => { self.registers[base + dst as usize] = Value::Undefined; continue; } }; let ag_send = gc_get_property( &self.gc, *r, "__send_value__", &self.shapes, ); let ag_kind = match gc_get_property( &self.gc, *r, "__resume_kind__", &self.shapes, ) { Value::String(s) => s, _ => "next".to_string(), }; let promise = crate::builtins::create_promise_object_pub( &mut self.gc, &mut self.shapes, ); match ag_kind.as_str() { "next" => { match self.run_generator(ag_gen, ag_send) { Ok(iter_result) => { crate::builtins::resolve_promise_internal( &mut self.gc, &mut self.shapes, promise, iter_result, ); } Err(err) => { let reason = err.to_value( &mut self.gc, &mut self.shapes, ); crate::builtins::reject_promise_internal( &mut self.gc, &mut self.shapes, promise, reason, ); } } } "return" => { if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(ag_gen) { gen.state = GeneratorState::Completed; } let result = self.make_iterator_result(ag_send, true); crate::builtins::resolve_promise_internal( &mut self.gc, &mut self.shapes, promise, result, ); } _ => {} } self.registers[base + dst as usize] = Value::Object(promise); continue; } // Check for event dispatch marker. let is_event_dispatch = matches!( gc_get_property( &self.gc, *r, "__event_dispatch__", &self.shapes ), Value::Boolean(true) ); if is_event_dispatch { let target_idx = match gc_get_property( &self.gc, *r, "__target_id__", &self.shapes, ) { Value::Number(n) => n as usize, _ => { self.registers[base + dst as usize] = Value::Boolean(true); continue; } }; let evt_ref = match gc_get_property( &self.gc, *r, "__event_ref__", &self.shapes, ) { Value::Object(er) => er, _ => { self.registers[base + dst as usize] = Value::Boolean(true); continue; } }; let result = crate::dom_bridge::run_event_dispatch( self, target_idx, evt_ref, ); self.registers[base + dst as usize] = result; continue; } } self.registers[base + dst as usize] = val; } Err(err) => { let err_val = err.to_value(&mut self.gc, &mut self.shapes); if !self.handle_exception(err_val) { return Err(err); } } } } CallInfo::Bytecode(callee_func, callee_upvalues) => { let callee_func = *callee_func; // Async function: create generator + promise, drive async. if callee_func.is_async && !callee_func.is_generator { let gen_ref = self.create_raw_generator(callee_func, callee_upvalues, &args); let result_promise = crate::builtins::create_promise_object_pub( &mut self.gc, &mut self.shapes, ); self.drive_async_step( gen_ref, result_promise, Value::Undefined, false, ); self.registers[base + dst as usize] = Value::Object(result_promise); continue; } // Async generator function: create async generator wrapper. if callee_func.is_async && callee_func.is_generator { let gen_ref = self.create_raw_generator(callee_func, callee_upvalues, &args); let wrapper = self.create_async_generator_wrapper(gen_ref); self.registers[base + dst as usize] = Value::Object(wrapper); continue; } // Generator function: create a generator object instead of executing. if callee_func.is_generator { let gen_obj = self.create_generator_object( callee_func, callee_upvalues, &args, ); self.registers[base + dst as usize] = Value::Object(gen_obj); continue; } 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, &mut self.shapes); 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(), upvalues: callee_upvalues, }); } } } 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(); // Resolve upvalues from the parent scope. let mut upvalues = Vec::with_capacity(inner_func.upvalue_defs.len()); for def in &inner_func.upvalue_defs { let cell_ref = if def.is_local { // Parent has a cell in register `def.index`. match &self.registers[base + def.index as usize] { Value::Object(r) => *r, _ => { return Err(RuntimeError { kind: ErrorKind::Error, message: "CreateClosure: upvalue register does not hold a cell" .into(), }); } } } else { // Transitive: parent's own upvalue at `def.index`. self.frames[fi].upvalues[def.index as usize] }; upvalues.push(cell_ref); } // 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(Box::new(FunctionData { name, kind: FunctionKind::Bytecode(BytecodeFunc { func: inner_func }), prototype_obj: Some(proto_obj), properties: HashMap::new(), upvalues, }))); // Set .prototype.constructor = this function. if let Some(HeapObject::Object(data)) = self.gc.get_mut(proto_obj) { data.insert_property( "constructor".to_string(), Property { value: Value::Function(gc_ref), writable: true, enumerable: false, configurable: true, }, &mut self.shapes, ); } 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); // Save gc_ref for DOM interception. let obj_gc_ref = match self.registers[base + obj_r as usize] { Value::Object(r) | Value::Function(r) => Some(r), _ => None, }; let val = match self.registers[base + obj_r as usize] { Value::Object(gc_ref) | Value::Function(gc_ref) => { gc_get_property(&self.gc, gc_ref, &key, &self.shapes) } Value::String(ref s) => { let v = string_get_property(s, &key); if matches!(v, Value::Undefined) { self.string_prototype .map(|p| gc_get_property(&self.gc, p, &key, &self.shapes)) .unwrap_or(Value::Undefined) } else { v } } Value::Number(_) => self .number_prototype .map(|p| gc_get_property(&self.gc, p, &key, &self.shapes)) .unwrap_or(Value::Undefined), Value::Boolean(_) => self .boolean_prototype .map(|p| gc_get_property(&self.gc, p, &key, &self.shapes)) .unwrap_or(Value::Undefined), _ => Value::Undefined, }; // DOM dynamic property interception. let val = match val { Value::Undefined if obj_gc_ref.is_some() => self .resolve_dom_property(obj_gc_ref.unwrap(), &key) .unwrap_or(Value::Undefined), other => other, }; 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(); let obj_gc = match self.registers[base + obj_r as usize] { Value::Object(r) => Some(r), Value::Function(r) => Some(r), _ => None, }; // DOM property set interception. let dom_handled = obj_gc .map(|r| self.handle_dom_property_set(r, &key, &val)) .unwrap_or(false); if !dom_handled { if let Some(gc_ref) = obj_gc { match self.gc.get_mut(gc_ref) { Some(HeapObject::Object(data)) => { data.set_property(key, val, &mut self.shapes); } Some(HeapObject::Function(fdata)) => { if let Some(prop) = fdata.properties.get_mut(&key) { if prop.writable { prop.value = val; } } else { fdata.properties.insert(key, Property::data(val)); } } _ => {} } } } } Op::CreateObject => { let dst = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let mut obj = ObjectData::new(); obj.prototype = self.object_prototype; 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::CreateArray => { let dst = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let mut obj = ObjectData::new(); obj.prototype = self.array_prototype; obj.insert_property( "length".to_string(), Property { value: Value::Number(0.0), writable: true, enumerable: false, configurable: false, }, &mut self.shapes, ); 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 ic_idx = Self::read_u16(&mut self.frames[fi]) as usize; let base = self.frames[fi].base; // ── IC fast path: shaped object with cached shape ── let mut ic_hit = false; if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { if let Some(HeapObject::Object(data)) = self.gc.get(gc_ref) { if let ObjectStorage::Shaped { shape, slots } = &data.storage { if let Some(slot_idx) = self.frames[fi].func.inline_caches[ic_idx].lookup(*shape) { self.registers[base + dst as usize] = slots[slot_idx as usize].clone(); ic_hit = true; } } } } if !ic_hit { let key = self.frames[fi].func.names[name_idx].clone(); let obj_gc_ref = match self.registers[base + obj_r as usize] { Value::Object(r) | Value::Function(r) => Some(r), _ => None, }; let val = match self.registers[base + obj_r as usize] { Value::Object(gc_ref) => { // Try own-property lookup for IC update. let (val, own_slot) = gc_get_property_ic(&self.gc, gc_ref, &key, &self.shapes); if let Some((shape, slot_index)) = own_slot { self.frames[fi].func.inline_caches[ic_idx] .update(shape, slot_index); } val } Value::Function(gc_ref) => { gc_get_property(&self.gc, gc_ref, &key, &self.shapes) } Value::String(ref s) => { let v = string_get_property(s, &key); if matches!(v, Value::Undefined) { self.string_prototype .map(|p| gc_get_property(&self.gc, p, &key, &self.shapes)) .unwrap_or(Value::Undefined) } else { v } } Value::Number(_) => self .number_prototype .map(|p| gc_get_property(&self.gc, p, &key, &self.shapes)) .unwrap_or(Value::Undefined), Value::Boolean(_) => self .boolean_prototype .map(|p| gc_get_property(&self.gc, p, &key, &self.shapes)) .unwrap_or(Value::Undefined), _ => Value::Undefined, }; // DOM dynamic property interception. let val = match val { Value::Undefined if obj_gc_ref.is_some() => self .resolve_dom_property(obj_gc_ref.unwrap(), &key) .unwrap_or(Value::Undefined), other => other, }; 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 ic_idx = Self::read_u16(&mut self.frames[fi]) as usize; let base = self.frames[fi].base; // ── IC fast path: shaped object, property exists ── let mut ic_hit = false; if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { // Check shape match via IC (read-only borrow first). let slot_idx_opt = if let Some(HeapObject::Object(data)) = self.gc.get(gc_ref) { if let ObjectStorage::Shaped { shape, .. } = &data.storage { self.frames[fi].func.inline_caches[ic_idx].lookup(*shape) } else { None } } else { None }; if let Some(slot_idx) = slot_idx_opt { let val = self.registers[base + val_r as usize].clone(); if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { if let ObjectStorage::Shaped { slots, .. } = &mut data.storage { slots[slot_idx as usize] = val; ic_hit = true; } } } } if !ic_hit { let key = self.frames[fi].func.names[name_idx].clone(); let val = self.registers[base + val_r as usize].clone(); let obj_gc = match self.registers[base + obj_r as usize] { Value::Object(r) => Some(r), Value::Function(r) => Some(r), _ => None, }; let dom_handled = obj_gc .map(|r| self.handle_dom_property_set(r, &key, &val)) .unwrap_or(false); if !dom_handled { if let Some(gc_ref) = obj_gc { match self.gc.get_mut(gc_ref) { Some(HeapObject::Object(data)) => { // Get shape before mutation for IC update. let shape_before = if let ObjectStorage::Shaped { shape, .. } = &data.storage { Some(*shape) } else { None }; data.set_property(key.clone(), val, &mut self.shapes); // Update IC: cache the slot if property existed // (shape didn't change). if let ObjectStorage::Shaped { shape, .. } = &data.storage { if shape_before == Some(*shape) { if let Some(desc) = self.shapes.lookup(*shape, &key) { self.frames[fi].func.inline_caches[ic_idx] .update(*shape, desc.index); } } } } Some(HeapObject::Function(fdata)) => { if let Some(prop) = fdata.properties.get_mut(&key) { if prop.writable { prop.value = val; } } else { fdata.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.remove_property(&key, &self.shapes) { RemoveResult::NonConfigurable => false, RemoveResult::Removed | RemoveResult::NotFound => 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, &self.shapes), _ => Vec::new(), }; // Store keys as an array object. let mut arr = ObjectData::new(); for (i, key) in keys.iter().enumerate() { arr.insert_property( i.to_string(), Property::data(Value::String(key.clone())), &mut self.shapes, ); } arr.insert_property( "length".to_string(), Property { value: Value::Number(keys.len() as f64), writable: true, enumerable: false, configurable: false, }, &mut self.shapes, ); 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", &self.shapes).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, &self.shapes) } _ => 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; } // ── Exception handling ───────────────────────────── Op::PushExceptionHandler => { let catch_reg = Self::read_u8(&mut self.frames[fi]); let offset = Self::read_i32(&mut self.frames[fi]); let catch_ip = (self.frames[fi].ip as i32 + offset) as usize; self.frames[fi].exception_handlers.push(ExceptionHandler { catch_ip, catch_reg, }); } Op::PopExceptionHandler => { self.frames[fi].exception_handlers.pop(); } // ── Closure / upvalue ops ───────────────────────── Op::NewCell => { let dst = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let cell = self.gc.alloc(HeapObject::Cell(Value::Undefined)); self.registers[base + dst as usize] = Value::Object(cell); if self.gc.should_collect() { let roots = self.collect_roots(); self.gc.collect(&roots); } } Op::CellLoad => { let dst = Self::read_u8(&mut self.frames[fi]); let cell_reg = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let cell_ref = match &self.registers[base + cell_reg as usize] { Value::Object(r) => *r, _ => { return Err(RuntimeError { kind: ErrorKind::Error, message: "CellLoad: register does not hold a cell".into(), }); } }; let val = match self.gc.get(cell_ref) { Some(HeapObject::Cell(v)) => v.clone(), _ => Value::Undefined, }; self.registers[base + dst as usize] = val; } Op::CellStore => { let cell_reg = Self::read_u8(&mut self.frames[fi]); let src = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let cell_ref = match &self.registers[base + cell_reg as usize] { Value::Object(r) => *r, _ => { return Err(RuntimeError { kind: ErrorKind::Error, message: "CellStore: register does not hold a cell".into(), }); } }; let val = self.registers[base + src as usize].clone(); if let Some(HeapObject::Cell(cell_val)) = self.gc.get_mut(cell_ref) { *cell_val = val; } } Op::LoadUpvalue => { let dst = Self::read_u8(&mut self.frames[fi]); let idx = Self::read_u8(&mut self.frames[fi]) as usize; let base = self.frames[fi].base; let cell_ref = self.frames[fi].upvalues[idx]; let val = match self.gc.get(cell_ref) { Some(HeapObject::Cell(v)) => v.clone(), _ => Value::Undefined, }; self.registers[base + dst as usize] = val; } Op::StoreUpvalue => { let idx = Self::read_u8(&mut self.frames[fi]) as usize; let src = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let val = self.registers[base + src as usize].clone(); let cell_ref = self.frames[fi].upvalues[idx]; if let Some(HeapObject::Cell(cell_val)) = self.gc.get_mut(cell_ref) { *cell_val = val; } } // ── Iterator / generator ───────────────────────── Op::Yield => { 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 yield_val = self.registers[base + src as usize].clone(); // Save the generator's state. let frame = &self.frames[fi]; let gen_ref = match self.registers.get(frame.return_reg) { Some(Value::Object(r)) => *r, _ => { return Err(RuntimeError { kind: ErrorKind::Error, message: "Yield outside generator".into(), }); } }; // Save registers and IP into the generator object. let saved_ip = self.frames[fi].ip; let saved_base = self.frames[fi].base; let reg_count = self.frames[fi].func.register_count as usize; let saved_regs: Vec = self.registers[saved_base..saved_base + reg_count].to_vec(); // Save exception handlers as (catch_ip, catch_reg) pairs. let saved_handlers: Vec<(usize, Reg)> = self.frames[fi] .exception_handlers .iter() .map(|h| (h.catch_ip, h.catch_reg)) .collect(); if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.ip = saved_ip; gen.registers = saved_regs; gen.state = GeneratorState::Suspended; gen.exception_handlers = saved_handlers; } // Pop the generator frame. self.frames.pop(); // Create {value, done: false} result. let result = self.make_iterator_result(yield_val, false); // Generators always run via run_generator which uses an isolated // frame stack. After Yield pops the generator frame, the stack is // empty and we return the {value, done} result to run_generator. return Ok(result); } // Await works identically to Yield at the opcode level: save state and // suspend. The async driver (not the generator protocol) handles resume. Op::Await => { 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 await_val = self.registers[base + src as usize].clone(); // Save the generator's state (same as Yield). let frame = &self.frames[fi]; let gen_ref = match self.registers.get(frame.return_reg) { Some(Value::Object(r)) => *r, _ => { return Err(RuntimeError { kind: ErrorKind::Error, message: "Await outside async function".into(), }); } }; let saved_ip = self.frames[fi].ip; let saved_base = self.frames[fi].base; let reg_count = self.frames[fi].func.register_count as usize; let saved_regs: Vec = self.registers[saved_base..saved_base + reg_count].to_vec(); let saved_handlers: Vec<(usize, Reg)> = self.frames[fi] .exception_handlers .iter() .map(|h| (h.catch_ip, h.catch_reg)) .collect(); if let Some(HeapObject::Generator(gen)) = self.gc.get_mut(gen_ref) { gen.ip = saved_ip; gen.registers = saved_regs; gen.state = GeneratorState::Suspended; gen.exception_handlers = saved_handlers; } self.frames.pop(); let result = self.make_iterator_result(await_val, false); return Ok(result); } Op::Spread => { 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 iterable = self.registers[base + src as usize].clone(); // Get the iterator from the iterable. let iterator = self.get_iterator(&iterable)?; // Iterate and push each element into the dst array. loop { let (value, done) = self.iterator_next(&iterator)?; if done { break; } // Push value into dst array. let dst_ref = match self.registers[base + dst as usize] { Value::Object(r) => r, _ => break, }; if let Some(HeapObject::Object(data)) = self.gc.get_mut(dst_ref) { let len = match data.get_property("length", &self.shapes) { Some(prop) => prop.value.to_number() as usize, None => 0, }; data.insert_property( len.to_string(), Property::data(value), &mut self.shapes, ); data.insert_property( "length".to_string(), Property { value: Value::Number((len + 1) as f64), writable: true, enumerable: false, configurable: false, }, &mut self.shapes, ); } } } } } } /// 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], &mut NativeContext) -> Result, ) { let gc_ref = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: name.to_string(), kind: FunctionKind::Native(NativeFunc { callback }), prototype_obj: None, properties: HashMap::new(), upvalues: Vec::new(), }))); 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); } /// Remove a global variable. pub fn remove_global(&mut self, name: &str) { self.globals.remove(name); } } 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], &mut NativeContext) -> Result), Bytecode(Box, Vec), } // ── Generator native callbacks ────────────────────────────── /// Native callback for generator.next(value). /// `this` is the generator wrapper object containing __gen__. fn generator_next(args: &[Value], ctx: &mut NativeContext) -> Result { // The generator wrapper stores the actual generator as __gen__. let gen_ref = match &ctx.this { Value::Object(r) => match gc_get_property(ctx.gc, *r, "__gen__", ctx.shapes) { Value::Object(gen_r) => gen_r, _ => return Err(RuntimeError::type_error("not a generator")), }, _ => return Err(RuntimeError::type_error("not a generator")), }; let send_value = args.first().cloned().unwrap_or(Value::Undefined); // We can't call run_generator from a NativeContext since we only have &mut Gc. // Instead, store the request and let the caller handle it. // This is a limitation — we need to restructure. // For now, use a different approach: store the gen_ref and value in a special // return value that the VM intercepts. // Actually, generator.next() needs to be handled specially by the VM. // Let's return a sentinel that the VM's call handling can detect. // Store gen_ref and send_value for the VM to process. // We use a special object with __generator_resume__ marker. let mut obj = ObjectData::new(); obj.insert_property( "__generator_resume__".to_string(), Property::builtin(Value::Boolean(true)), ctx.shapes, ); obj.insert_property( "__gen_ref__".to_string(), Property::builtin(Value::Object(gen_ref)), ctx.shapes, ); obj.insert_property( "__send_value__".to_string(), Property::builtin(send_value), ctx.shapes, ); obj.insert_property( "__resume_kind__".to_string(), Property::builtin(Value::String("next".to_string())), ctx.shapes, ); let r = ctx.gc.alloc(HeapObject::Object(obj)); Ok(Value::Object(r)) } /// Native callback for generator.return(value). fn generator_return(args: &[Value], ctx: &mut NativeContext) -> Result { let gen_ref = match &ctx.this { Value::Object(r) => match gc_get_property(ctx.gc, *r, "__gen__", ctx.shapes) { Value::Object(gen_r) => gen_r, _ => return Err(RuntimeError::type_error("not a generator")), }, _ => return Err(RuntimeError::type_error("not a generator")), }; let return_value = args.first().cloned().unwrap_or(Value::Undefined); let mut obj = ObjectData::new(); obj.insert_property( "__generator_resume__".to_string(), Property::builtin(Value::Boolean(true)), ctx.shapes, ); obj.insert_property( "__gen_ref__".to_string(), Property::builtin(Value::Object(gen_ref)), ctx.shapes, ); obj.insert_property( "__send_value__".to_string(), Property::builtin(return_value), ctx.shapes, ); obj.insert_property( "__resume_kind__".to_string(), Property::builtin(Value::String("return".to_string())), ctx.shapes, ); let r = ctx.gc.alloc(HeapObject::Object(obj)); Ok(Value::Object(r)) } /// Native callback for generator.throw(error). fn generator_throw(args: &[Value], ctx: &mut NativeContext) -> Result { let gen_ref = match &ctx.this { Value::Object(r) => match gc_get_property(ctx.gc, *r, "__gen__", ctx.shapes) { Value::Object(gen_r) => gen_r, _ => return Err(RuntimeError::type_error("not a generator")), }, _ => return Err(RuntimeError::type_error("not a generator")), }; let error_value = args.first().cloned().unwrap_or(Value::Undefined); let mut obj = ObjectData::new(); obj.insert_property( "__generator_resume__".to_string(), Property::builtin(Value::Boolean(true)), ctx.shapes, ); obj.insert_property( "__gen_ref__".to_string(), Property::builtin(Value::Object(gen_ref)), ctx.shapes, ); obj.insert_property( "__send_value__".to_string(), Property::builtin(error_value), ctx.shapes, ); obj.insert_property( "__resume_kind__".to_string(), Property::builtin(Value::String("throw".to_string())), ctx.shapes, ); let r = ctx.gc.alloc(HeapObject::Object(obj)); Ok(Value::Object(r)) } /// Native callback for generator[Symbol.iterator]() — returns `this`. fn generator_symbol_iterator( _args: &[Value], ctx: &mut NativeContext, ) -> Result { Ok(ctx.this.clone()) } // ── Async function native callbacks ────────────────────────── /// Native callback for async function resume. Called from microtask drain. /// Returns a marker object with `__async_resume__` so the VM can detect it /// and call `drive_async_step`. /// /// The resume data (gen_ref, result_promise, is_throw) is stored on the /// function's `__async_data__` property. The VM extracts it into the /// `ASYNC_RESUME_DATA` thread-local before invoking this callback. fn async_resume_callback(args: &[Value], ctx: &mut NativeContext) -> Result { let value = args.first().cloned().unwrap_or(Value::Undefined); let data_ref = ASYNC_RESUME_DATA.with(|cell| cell.take()); if let Some(data) = data_ref { let gen_ref = match gc_get_property(ctx.gc, data, "__gen_ref__", ctx.shapes) { Value::Object(r) => r, _ => return Ok(value), }; let result_promise = match gc_get_property(ctx.gc, data, "__result_promise__", ctx.shapes) { Value::Object(r) => r, _ => return Ok(value), }; let is_throw = matches!( gc_get_property(ctx.gc, data, "__is_throw__", ctx.shapes), Value::Boolean(true) ); // Return a marker for the VM to detect. let mut obj = ObjectData::new(); obj.insert_property( "__async_resume__".to_string(), Property::builtin(Value::Boolean(true)), ctx.shapes, ); obj.insert_property( "__gen_ref__".to_string(), Property::builtin(Value::Object(gen_ref)), ctx.shapes, ); obj.insert_property( "__result_promise__".to_string(), Property::builtin(Value::Object(result_promise)), ctx.shapes, ); obj.insert_property( "__is_throw__".to_string(), Property::builtin(Value::Boolean(is_throw)), ctx.shapes, ); obj.insert_property( "__value__".to_string(), Property::builtin(value), ctx.shapes, ); let r = ctx.gc.alloc(HeapObject::Object(obj)); Ok(Value::Object(r)) } else { Ok(value) } } thread_local! { static ASYNC_RESUME_DATA: std::cell::Cell> = const { std::cell::Cell::new(None) }; } /// Native callback for async generator `.next(value)`. fn async_generator_next(args: &[Value], ctx: &mut NativeContext) -> Result { let gen_ref = match &ctx.this { Value::Object(r) => match gc_get_property(ctx.gc, *r, "__gen__", ctx.shapes) { Value::Object(gen_r) => gen_r, _ => return Err(RuntimeError::type_error("not an async generator")), }, _ => return Err(RuntimeError::type_error("not an async generator")), }; let send_value = args.first().cloned().unwrap_or(Value::Undefined); // Return a marker for the VM to handle. let mut obj = ObjectData::new(); obj.insert_property( "__async_generator_resume__".to_string(), Property::builtin(Value::Boolean(true)), ctx.shapes, ); obj.insert_property( "__gen_ref__".to_string(), Property::builtin(Value::Object(gen_ref)), ctx.shapes, ); obj.insert_property( "__send_value__".to_string(), Property::builtin(send_value), ctx.shapes, ); obj.insert_property( "__resume_kind__".to_string(), Property::builtin(Value::String("next".to_string())), ctx.shapes, ); let r = ctx.gc.alloc(HeapObject::Object(obj)); Ok(Value::Object(r)) } /// Native callback for async generator `.return(value)`. fn async_generator_return(args: &[Value], ctx: &mut NativeContext) -> Result { let gen_ref = match &ctx.this { Value::Object(r) => match gc_get_property(ctx.gc, *r, "__gen__", ctx.shapes) { Value::Object(gen_r) => gen_r, _ => return Err(RuntimeError::type_error("not an async generator")), }, _ => return Err(RuntimeError::type_error("not an async generator")), }; let return_value = args.first().cloned().unwrap_or(Value::Undefined); let mut obj = ObjectData::new(); obj.insert_property( "__async_generator_resume__".to_string(), Property::builtin(Value::Boolean(true)), ctx.shapes, ); obj.insert_property( "__gen_ref__".to_string(), Property::builtin(Value::Object(gen_ref)), ctx.shapes, ); obj.insert_property( "__send_value__".to_string(), Property::builtin(return_value), ctx.shapes, ); obj.insert_property( "__resume_kind__".to_string(), Property::builtin(Value::String("return".to_string())), ctx.shapes, ); let r = ctx.gc.alloc(HeapObject::Object(obj)); Ok(Value::Object(r)) } // ── 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, _ctx| { 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 shapes = ShapeTable::new(); let mut obj = ObjectData::new(); obj.insert_property( "x".to_string(), Property { value: Value::Number(1.0), writable: true, enumerable: true, configurable: false, }, &mut shapes, ); 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.get_property("x", &shapes).unwrap(); assert!(!prop.configurable); // The property should still be there. assert!(data.contains_key("x", &shapes)); } _ => 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 shapes = ShapeTable::new(); let mut obj = ObjectData::new(); obj.insert_property( "frozen".to_string(), Property { value: Value::Number(42.0), writable: false, enumerable: true, configurable: false, }, &mut shapes, ); let obj_ref = gc.alloc(HeapObject::Object(obj)); // Verify the property value. match gc.get(obj_ref) { Some(HeapObject::Object(data)) => { assert_eq!( data.get_property("frozen", &shapes) .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 shapes = ShapeTable::new(); let mut obj = ObjectData::new(); obj.insert_property( "b".to_string(), Property::data(Value::Number(2.0)), &mut shapes, ); obj.insert_property( "2".to_string(), Property::data(Value::Number(3.0)), &mut shapes, ); obj.insert_property( "a".to_string(), Property::data(Value::Number(1.0)), &mut shapes, ); obj.insert_property( "0".to_string(), Property::data(Value::Number(0.0)), &mut shapes, ); let obj_ref = gc.alloc(HeapObject::Object(obj)); let keys = gc_enumerate_keys(&gc, obj_ref, &shapes); // Integer keys first (sorted), then string keys in insertion order. 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 in insertion order. 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(Box::new(FunctionData { name: "Foo".to_string(), kind: FunctionKind::Native(NativeFunc { callback: |_, _ctx| Ok(Value::Undefined), }), prototype_obj: Some(proto), properties: HashMap::new(), upvalues: Vec::new(), }))); // 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)); } #[test] fn test_try_catch_basic() { // Simple try/catch should catch a thrown value. let src = r#" var caught = false; try { throw "err"; } catch (e) { caught = true; } caught "#; match eval(src).unwrap() { Value::Boolean(true) => {} v => panic!("expected true, got {v:?}"), } } #[test] fn test_try_catch_nested_call() { // try/catch should catch errors thrown from called functions. let src = r#" function thrower() { throw "err"; } var caught = false; try { thrower(); } catch (e) { caught = true; } caught "#; match eval(src).unwrap() { Value::Boolean(true) => {} v => panic!("expected true, got {v:?}"), } } // ── Closure tests ──────────────────────────────────────── #[test] fn test_closure_basic() { // Basic closure: inner function reads outer variable. let src = r#" function outer() { var x = 10; function inner() { return x; } return inner(); } outer() "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 10.0), v => panic!("expected 10, got {v:?}"), } } #[test] fn test_closure_return_function() { // Closure survives the outer function's return. let src = r#" function makeAdder(x) { return function(y) { return x + y; }; } var add5 = makeAdder(5); add5(3) "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 8.0), v => panic!("expected 8, got {v:?}"), } } #[test] fn test_closure_mutation() { // Closures share live references — mutation is visible. let src = r#" function counter() { var n = 0; return function() { n = n + 1; return n; }; } var c = counter(); c(); c(); c() "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } } #[test] fn test_closure_shared_variable() { // Two closures from the same scope share the same variable. let src = r#" function make() { var x = 0; function inc() { x = x + 1; } function get() { return x; } inc(); inc(); return get(); } make() "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_closure_arrow() { // Arrow function captures outer variable. let src = r#" function outer() { var x = 42; var f = () => x; return f(); } outer() "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_closure_nested() { // Transitive capture: grandchild function reads grandparent variable. let src = r#" function outer() { var x = 100; function middle() { function inner() { return x; } return inner(); } return middle(); } outer() "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 100.0), v => panic!("expected 100, got {v:?}"), } } #[test] fn test_closure_param_capture() { // Closure captures a function parameter. let src = r#" function multiply(factor) { return function(x) { return x * factor; }; } var double = multiply(2); double(7) "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 14.0), v => panic!("expected 14, got {v:?}"), } } // ── const tests ────────────────────────────────────────── #[test] fn test_const_basic() { let src = "const x = 42; x"; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_const_reassignment_error() { let src = "const x = 1; x = 2;"; let program = crate::parser::Parser::parse(src).expect("parse ok"); let result = crate::compiler::compile(&program); assert!( result.is_err(), "const reassignment should be a compile error" ); } #[test] fn test_const_missing_init_error() { let src = "const x;"; let program = crate::parser::Parser::parse(src).expect("parse ok"); let result = crate::compiler::compile(&program); assert!( result.is_err(), "const without initializer should be a compile error" ); } // ── this binding tests ─────────────────────────────────── #[test] fn test_method_call_this() { let src = r#" var obj = {}; obj.x = 10; obj.getX = function() { return this.x; }; obj.getX() "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 10.0), v => panic!("expected 10, got {v:?}"), } } // ── Object built-in tests ──────────────────────────────── #[test] fn test_object_keys() { let src = r#" var obj = {}; obj.a = 1; obj.b = 2; obj.c = 3; var k = Object.keys(obj); k.length "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } } #[test] fn test_object_values() { let src = r#" var obj = {}; obj.x = 10; var v = Object.values(obj); v[0] "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 10.0), v => panic!("expected 10, got {v:?}"), } } #[test] fn test_object_entries() { let src = r#" var obj = {}; obj.x = 42; var e = Object.entries(obj); e[0][1] "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_object_assign() { let src = r#" var a = {}; a.x = 1; var b = {}; b.y = 2; var c = Object.assign(a, b); c.y "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_object_create() { let src = r#" var proto = {}; proto.greet = "hello"; var child = Object.create(proto); child.greet "#; match eval(src).unwrap() { Value::String(s) => assert_eq!(s, "hello"), v => panic!("expected 'hello', got {v:?}"), } } #[test] fn test_object_is() { let src = "Object.is(NaN, NaN)"; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_object_freeze() { let src = r#" var obj = {}; obj.x = 1; Object.freeze(obj); Object.isFrozen(obj) "#; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_object_has_own_property() { let src = r#" var obj = {}; obj.x = 1; obj.hasOwnProperty("x") "#; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } // ── Array built-in tests ───────────────────────────────── #[test] fn test_array_push_pop() { let src = r#" var arr = [1, 2, 3]; arr.push(4); arr.pop() "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 4.0), v => panic!("expected 4, got {v:?}"), } } #[test] fn test_array_push_length() { let src = r#" var arr = []; arr.push(10); arr.push(20); arr.length "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_array_shift_unshift() { let src = r#" var arr = [1, 2, 3]; arr.unshift(0); arr.shift() "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 0.0), v => panic!("expected 0, got {v:?}"), } } #[test] fn test_array_index_of() { let src = r#" var arr = [10, 20, 30]; arr.indexOf(20) "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 1.0), v => panic!("expected 1, got {v:?}"), } } #[test] fn test_array_includes() { let src = r#" var arr = [1, 2, 3]; arr.includes(2) "#; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_array_join() { let src = r#" var arr = [1, 2, 3]; arr.join("-") "#; match eval(src).unwrap() { Value::String(s) => assert_eq!(s, "1-2-3"), v => panic!("expected '1-2-3', got {v:?}"), } } #[test] fn test_array_slice() { let src = r#" var arr = [1, 2, 3, 4, 5]; var s = arr.slice(1, 3); s.length "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_array_concat() { let src = r#" var a = [1, 2]; var b = [3, 4]; var c = a.concat(b); c.length "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 4.0), v => panic!("expected 4, got {v:?}"), } } #[test] fn test_array_reverse() { let src = r#" var arr = [1, 2, 3]; arr.reverse(); arr[0] "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } } #[test] fn test_array_splice() { let src = r#" var arr = [1, 2, 3, 4, 5]; var removed = arr.splice(1, 2); removed.length "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_array_is_array() { let src = "Array.isArray([1, 2, 3])"; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_array_map() { let src = r#" var arr = [1, 2, 3]; var doubled = arr.map(function(x) { return x * 2; }); doubled[1] "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 4.0), v => panic!("expected 4, got {v:?}"), } } #[test] fn test_array_filter() { let src = r#" var arr = [1, 2, 3, 4, 5]; var evens = arr.filter(function(x) { return x % 2 === 0; }); evens.length "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_array_reduce() { let src = r#" var arr = [1, 2, 3, 4]; arr.reduce(function(acc, x) { return acc + x; }, 0) "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 10.0), v => panic!("expected 10, got {v:?}"), } } #[test] fn test_array_foreach() { let src = r#" var arr = [1, 2, 3]; var sum = 0; arr.forEach(function(x) { sum = sum + x; }); sum "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 6.0), v => panic!("expected 6, got {v:?}"), } } #[test] fn test_array_find() { let src = r#" var arr = [1, 2, 3, 4]; arr.find(function(x) { return x > 2; }) "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } } #[test] fn test_array_find_index() { let src = r#" var arr = [1, 2, 3, 4]; arr.findIndex(function(x) { return x > 2; }) "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_array_some_every() { let src = r#" var arr = [2, 4, 6]; var all_even = arr.every(function(x) { return x % 2 === 0; }); var has_big = arr.some(function(x) { return x > 10; }); all_even && !has_big "#; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_array_sort() { let src = r#" var arr = [3, 1, 2]; arr.sort(); arr[0] "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 1.0), v => panic!("expected 1, got {v:?}"), } } #[test] fn test_array_sort_custom() { let src = r#" var arr = [3, 1, 2]; arr.sort(function(a, b) { return a - b; }); arr[2] "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } } #[test] fn test_array_from() { let src = r#" var arr = Array.from("abc"); arr.length "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } } #[test] fn test_array_from_array() { let src = r#" var orig = [10, 20, 30]; var copy = Array.from(orig); copy[2] "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 30.0), v => panic!("expected 30, got {v:?}"), } } #[test] fn test_array_flat() { let src = r#" var arr = [[1, 2], [3, 4]]; var flat = arr.flat(); flat.length "#; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 4.0), v => panic!("expected 4, got {v:?}"), } } // ── Error built-in tests ───────────────────────────────── #[test] fn test_error_constructor() { let src = r#" var e = new Error("oops"); e.message "#; match eval(src).unwrap() { Value::String(s) => assert_eq!(s, "oops"), v => panic!("expected 'oops', got {v:?}"), } } #[test] fn test_type_error_constructor() { let src = r#" var e = new TypeError("bad type"); e.message "#; match eval(src).unwrap() { Value::String(s) => assert_eq!(s, "bad type"), v => panic!("expected 'bad type', got {v:?}"), } } // ── Global function tests ──────────────────────────────── #[test] fn test_parse_int() { let src = "parseInt('42')"; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_parse_int_hex() { let src = "parseInt('0xFF', 16)"; match eval(src).unwrap() { Value::Number(n) => assert_eq!(n, 255.0), v => panic!("expected 255, got {v:?}"), } } #[test] fn test_is_nan() { let src = "isNaN(NaN)"; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_is_finite() { let src = "isFinite(42)"; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } // ── String built-in tests ───────────────────────────────── #[test] fn test_string_constructor() { match eval("String(42)").unwrap() { Value::String(s) => assert_eq!(s, "42"), v => panic!("expected '42', got {v:?}"), } match eval("String(true)").unwrap() { Value::String(s) => assert_eq!(s, "true"), v => panic!("expected 'true', got {v:?}"), } match eval("String()").unwrap() { Value::String(s) => assert_eq!(s, ""), v => panic!("expected '', got {v:?}"), } } #[test] fn test_string_length() { match eval("'hello'.length").unwrap() { Value::Number(n) => assert_eq!(n, 5.0), v => panic!("expected 5, got {v:?}"), } } #[test] fn test_string_char_at() { match eval("'hello'.charAt(1)").unwrap() { Value::String(s) => assert_eq!(s, "e"), v => panic!("expected 'e', got {v:?}"), } match eval("'hello'.charAt(10)").unwrap() { Value::String(s) => assert_eq!(s, ""), v => panic!("expected '', got {v:?}"), } } #[test] fn test_string_char_code_at() { match eval("'A'.charCodeAt(0)").unwrap() { Value::Number(n) => assert_eq!(n, 65.0), v => panic!("expected 65, got {v:?}"), } } #[test] fn test_string_proto_concat() { match eval("'hello'.concat(' ', 'world')").unwrap() { Value::String(s) => assert_eq!(s, "hello world"), v => panic!("expected 'hello world', got {v:?}"), } } #[test] fn test_string_slice() { match eval("'hello world'.slice(6)").unwrap() { Value::String(s) => assert_eq!(s, "world"), v => panic!("expected 'world', got {v:?}"), } match eval("'hello'.slice(1, 3)").unwrap() { Value::String(s) => assert_eq!(s, "el"), v => panic!("expected 'el', got {v:?}"), } match eval("'hello'.slice(-3)").unwrap() { Value::String(s) => assert_eq!(s, "llo"), v => panic!("expected 'llo', got {v:?}"), } } #[test] fn test_string_substring() { match eval("'hello'.substring(1, 3)").unwrap() { Value::String(s) => assert_eq!(s, "el"), v => panic!("expected 'el', got {v:?}"), } // substring swaps args if start > end match eval("'hello'.substring(3, 1)").unwrap() { Value::String(s) => assert_eq!(s, "el"), v => panic!("expected 'el', got {v:?}"), } } #[test] fn test_string_index_of() { match eval("'hello world'.indexOf('world')").unwrap() { Value::Number(n) => assert_eq!(n, 6.0), v => panic!("expected 6, got {v:?}"), } match eval("'hello'.indexOf('xyz')").unwrap() { Value::Number(n) => assert_eq!(n, -1.0), v => panic!("expected -1, got {v:?}"), } } #[test] fn test_string_last_index_of() { match eval("'abcabc'.lastIndexOf('abc')").unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } } #[test] fn test_string_includes() { match eval("'hello world'.includes('world')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("'hello'.includes('xyz')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_string_starts_ends_with() { match eval("'hello'.startsWith('hel')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("'hello'.endsWith('llo')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_string_trim() { match eval("' hello '.trim()").unwrap() { Value::String(s) => assert_eq!(s, "hello"), v => panic!("expected 'hello', got {v:?}"), } match eval("' hello '.trimStart()").unwrap() { Value::String(s) => assert_eq!(s, "hello "), v => panic!("expected 'hello ', got {v:?}"), } match eval("' hello '.trimEnd()").unwrap() { Value::String(s) => assert_eq!(s, " hello"), v => panic!("expected ' hello', got {v:?}"), } } #[test] fn test_string_pad() { match eval("'5'.padStart(3, '0')").unwrap() { Value::String(s) => assert_eq!(s, "005"), v => panic!("expected '005', got {v:?}"), } match eval("'5'.padEnd(3, '0')").unwrap() { Value::String(s) => assert_eq!(s, "500"), v => panic!("expected '500', got {v:?}"), } } #[test] fn test_string_repeat() { match eval("'ab'.repeat(3)").unwrap() { Value::String(s) => assert_eq!(s, "ababab"), v => panic!("expected 'ababab', got {v:?}"), } } #[test] fn test_string_split() { // split returns an array; verify length and elements. match eval("'a,b,c'.split(',').length").unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } match eval("'a,b,c'.split(',')[0]").unwrap() { Value::String(s) => assert_eq!(s, "a"), v => panic!("expected 'a', got {v:?}"), } match eval("'a,b,c'.split(',')[2]").unwrap() { Value::String(s) => assert_eq!(s, "c"), v => panic!("expected 'c', got {v:?}"), } } #[test] fn test_string_replace() { match eval("'hello world'.replace('world', 'there')").unwrap() { Value::String(s) => assert_eq!(s, "hello there"), v => panic!("expected 'hello there', got {v:?}"), } } #[test] fn test_string_replace_all() { match eval("'aabbcc'.replaceAll('b', 'x')").unwrap() { Value::String(s) => assert_eq!(s, "aaxxcc"), v => panic!("expected 'aaxxcc', got {v:?}"), } } #[test] fn test_string_case() { match eval("'Hello'.toLowerCase()").unwrap() { Value::String(s) => assert_eq!(s, "hello"), v => panic!("expected 'hello', got {v:?}"), } match eval("'Hello'.toUpperCase()").unwrap() { Value::String(s) => assert_eq!(s, "HELLO"), v => panic!("expected 'HELLO', got {v:?}"), } } #[test] fn test_string_at() { match eval("'hello'.at(0)").unwrap() { Value::String(s) => assert_eq!(s, "h"), v => panic!("expected 'h', got {v:?}"), } match eval("'hello'.at(-1)").unwrap() { Value::String(s) => assert_eq!(s, "o"), v => panic!("expected 'o', got {v:?}"), } } #[test] fn test_string_from_char_code() { match eval("String.fromCharCode(72, 101, 108)").unwrap() { Value::String(s) => assert_eq!(s, "Hel"), v => panic!("expected 'Hel', got {v:?}"), } } #[test] fn test_string_from_code_point() { match eval("String.fromCodePoint(65, 66, 67)").unwrap() { Value::String(s) => assert_eq!(s, "ABC"), v => panic!("expected 'ABC', got {v:?}"), } } // ── Number built-in tests ───────────────────────────────── #[test] fn test_number_constructor() { match eval("Number('42')").unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } match eval("Number(true)").unwrap() { Value::Number(n) => assert_eq!(n, 1.0), v => panic!("expected 1, got {v:?}"), } match eval("Number()").unwrap() { Value::Number(n) => assert_eq!(n, 0.0), v => panic!("expected 0, got {v:?}"), } } #[test] fn test_number_is_nan() { match eval("Number.isNaN(NaN)").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("Number.isNaN(42)").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } // Number.isNaN doesn't coerce — string "NaN" is not NaN. match eval("Number.isNaN('NaN')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_number_is_finite() { match eval("Number.isFinite(42)").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("Number.isFinite(Infinity)").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_number_is_integer() { match eval("Number.isInteger(42)").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("Number.isInteger(42.5)").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_number_is_safe_integer() { match eval("Number.isSafeInteger(42)").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("Number.isSafeInteger(9007199254740992)").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_number_constants() { match eval("Number.MAX_SAFE_INTEGER").unwrap() { Value::Number(n) => assert_eq!(n, 9007199254740991.0), v => panic!("expected MAX_SAFE_INTEGER, got {v:?}"), } match eval("Number.EPSILON").unwrap() { Value::Number(n) => assert_eq!(n, f64::EPSILON), v => panic!("expected EPSILON, got {v:?}"), } } #[test] fn test_number_to_fixed() { match eval("var n = 3.14159; n.toFixed(2)").unwrap() { Value::String(s) => assert_eq!(s, "3.14"), v => panic!("expected '3.14', got {v:?}"), } } #[test] fn test_number_to_string_radix() { match eval("var n = 255; n.toString(16)").unwrap() { Value::String(s) => assert_eq!(s, "ff"), v => panic!("expected 'ff', got {v:?}"), } match eval("var n = 10; n.toString(2)").unwrap() { Value::String(s) => assert_eq!(s, "1010"), v => panic!("expected '1010', got {v:?}"), } } #[test] fn test_number_parse_int() { match eval("Number.parseInt('42')").unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } // ── Boolean built-in tests ──────────────────────────────── #[test] fn test_boolean_constructor() { match eval("Boolean(1)").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("Boolean(0)").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } match eval("Boolean('')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } match eval("Boolean('hello')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_boolean_to_string() { match eval("true.toString()").unwrap() { Value::String(s) => assert_eq!(s, "true"), v => panic!("expected 'true', got {v:?}"), } match eval("false.toString()").unwrap() { Value::String(s) => assert_eq!(s, "false"), v => panic!("expected 'false', got {v:?}"), } } // ── Symbol built-in tests ───────────────────────────────── #[test] fn test_symbol_uniqueness() { // Each Symbol() call should produce a unique value. match eval("var a = Symbol('x'); var b = Symbol('x'); a === b").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_symbol_well_known() { match eval("typeof Symbol.iterator").unwrap() { Value::String(s) => assert_eq!(s, "string"), v => panic!("expected 'string', got {v:?}"), } match eval("Symbol.iterator").unwrap() { Value::String(s) => assert_eq!(s, "@@iterator"), v => panic!("expected '@@iterator', got {v:?}"), } } #[test] fn test_symbol_for_and_key_for() { // "for" is a keyword, so use bracket notation: Symbol["for"](...). match eval("Symbol['for']('test') === Symbol['for']('test')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("Symbol.keyFor(Symbol['for']('mykey'))").unwrap() { Value::String(s) => assert_eq!(s, "mykey"), v => panic!("expected 'mykey', got {v:?}"), } } // ── Primitive auto-boxing tests ─────────────────────────── #[test] fn test_string_method_chaining() { match eval("' Hello World '.trim().toLowerCase()").unwrap() { Value::String(s) => assert_eq!(s, "hello world"), v => panic!("expected 'hello world', got {v:?}"), } } #[test] fn test_string_substr() { match eval("'hello world'.substr(6, 5)").unwrap() { Value::String(s) => assert_eq!(s, "world"), v => panic!("expected 'world', got {v:?}"), } } // ── Math built-in ───────────────────────────────────────── #[test] fn test_math_constants() { let r = eval("Math.PI").unwrap(); match r { Value::Number(n) => assert!((n - std::f64::consts::PI).abs() < 1e-10), _ => panic!("Expected number"), } let r = eval("Math.E").unwrap(); match r { Value::Number(n) => assert!((n - std::f64::consts::E).abs() < 1e-10), _ => panic!("Expected number"), } let r = eval("Math.SQRT2").unwrap(); match r { Value::Number(n) => assert!((n - std::f64::consts::SQRT_2).abs() < 1e-10), _ => panic!("Expected number"), } } #[test] fn test_math_abs() { assert_eq!(eval("Math.abs(-5)").unwrap().to_number(), 5.0); assert_eq!(eval("Math.abs(3)").unwrap().to_number(), 3.0); assert_eq!(eval("Math.abs(0)").unwrap().to_number(), 0.0); } #[test] fn test_math_floor_ceil_round_trunc() { assert_eq!(eval("Math.floor(4.7)").unwrap().to_number(), 4.0); assert_eq!(eval("Math.ceil(4.2)").unwrap().to_number(), 5.0); assert_eq!(eval("Math.round(4.5)").unwrap().to_number(), 5.0); assert_eq!(eval("Math.round(4.4)").unwrap().to_number(), 4.0); assert_eq!(eval("Math.trunc(4.7)").unwrap().to_number(), 4.0); assert_eq!(eval("Math.trunc(-4.7)").unwrap().to_number(), -4.0); } #[test] fn test_math_max_min() { assert_eq!(eval("Math.max(1, 3, 2)").unwrap().to_number(), 3.0); assert_eq!(eval("Math.min(1, 3, 2)").unwrap().to_number(), 1.0); assert_eq!(eval("Math.max()").unwrap().to_number(), f64::NEG_INFINITY); assert_eq!(eval("Math.min()").unwrap().to_number(), f64::INFINITY); } #[test] fn test_math_pow_sqrt() { assert_eq!(eval("Math.pow(2, 10)").unwrap().to_number(), 1024.0); assert_eq!(eval("Math.sqrt(9)").unwrap().to_number(), 3.0); let cbrt = eval("Math.cbrt(27)").unwrap().to_number(); assert!((cbrt - 3.0).abs() < 1e-10); } #[test] fn test_math_trig() { let sin = eval("Math.sin(0)").unwrap().to_number(); assert!(sin.abs() < 1e-10); let cos = eval("Math.cos(0)").unwrap().to_number(); assert!((cos - 1.0).abs() < 1e-10); let atan2 = eval("Math.atan2(1, 1)").unwrap().to_number(); assert!((atan2 - std::f64::consts::FRAC_PI_4).abs() < 1e-10); } #[test] fn test_math_log_exp() { let exp = eval("Math.exp(1)").unwrap().to_number(); assert!((exp - std::f64::consts::E).abs() < 1e-10); let log = eval("Math.log(Math.E)").unwrap().to_number(); assert!((log - 1.0).abs() < 1e-10); let log2 = eval("Math.log2(8)").unwrap().to_number(); assert!((log2 - 3.0).abs() < 1e-10); let log10 = eval("Math.log10(1000)").unwrap().to_number(); assert!((log10 - 3.0).abs() < 1e-10); } #[test] fn test_math_sign() { assert_eq!(eval("Math.sign(5)").unwrap().to_number(), 1.0); assert_eq!(eval("Math.sign(-5)").unwrap().to_number(), -1.0); assert_eq!(eval("Math.sign(0)").unwrap().to_number(), 0.0); } #[test] fn test_math_clz32() { assert_eq!(eval("Math.clz32(1)").unwrap().to_number(), 31.0); assert_eq!(eval("Math.clz32(0)").unwrap().to_number(), 32.0); } #[test] fn test_math_imul() { assert_eq!(eval("Math.imul(3, 4)").unwrap().to_number(), 12.0); assert_eq!(eval("Math.imul(0xffffffff, 5)").unwrap().to_number(), -5.0); } #[test] fn test_math_hypot() { assert_eq!(eval("Math.hypot(3, 4)").unwrap().to_number(), 5.0); assert_eq!(eval("Math.hypot()").unwrap().to_number(), 0.0); } #[test] fn test_math_random() { match eval("var r = Math.random(); r >= 0 && r < 1").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_math_fround() { let r = eval("Math.fround(5.5)").unwrap().to_number(); assert_eq!(r, 5.5f32 as f64); } // ── Date built-in ───────────────────────────────────────── #[test] fn test_date_now() { let r = eval("Date.now()").unwrap().to_number(); assert!(r > 1_577_836_800_000.0); } #[test] fn test_date_utc() { let r = eval("Date.UTC(2020, 0, 1)").unwrap().to_number(); assert_eq!(r, 1_577_836_800_000.0); } #[test] fn test_date_parse() { let r = eval("Date.parse('2020-01-01T00:00:00.000Z')") .unwrap() .to_number(); assert_eq!(r, 1_577_836_800_000.0); } #[test] fn test_date_constructor_ms() { let r = eval("var d = new Date(1577836800000); d.getFullYear()") .unwrap() .to_number(); assert_eq!(r, 2020.0); } #[test] fn test_date_constructor_components() { let r = eval("var d = new Date(2020, 0, 1, 0, 0, 0, 0); d.getTime()") .unwrap() .to_number(); assert_eq!(r, 1_577_836_800_000.0); } #[test] fn test_date_getters() { let src = r#" var d = new Date(1577836800000); var results = [ d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds(), d.getDay() ]; results[0] === 2020 && results[1] === 0 && results[2] === 1 && results[3] === 0 && results[4] === 0 && results[5] === 0 && results[6] === 0 && results[7] === 3 "#; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_date_setters() { let src = r#" var d = new Date(1577836800000); d.setFullYear(2025); d.getFullYear() "#; assert_eq!(eval(src).unwrap().to_number(), 2025.0); } #[test] fn test_date_to_iso_string() { match eval("var d = new Date(1577836800000); d.toISOString()").unwrap() { Value::String(s) => assert_eq!(s, "2020-01-01T00:00:00.000Z"), v => panic!("expected ISO string, got {v:?}"), } } #[test] fn test_date_value_of() { let r = eval("var d = new Date(1577836800000); d.valueOf()") .unwrap() .to_number(); assert_eq!(r, 1_577_836_800_000.0); } #[test] fn test_date_to_string() { let r = eval("var d = new Date(1577836800000); d.toString()").unwrap(); match r { Value::String(s) => assert!(s.contains("2020") && s.contains("GMT")), _ => panic!("Expected string"), } } #[test] fn test_date_to_json() { match eval("var d = new Date(1577836800000); d.toJSON()").unwrap() { Value::String(s) => assert_eq!(s, "2020-01-01T00:00:00.000Z"), v => panic!("expected ISO string, got {v:?}"), } } #[test] fn test_date_constructor_string() { let r = eval("var d = new Date('2020-06-15T12:30:00Z'); d.getMonth()") .unwrap() .to_number(); assert_eq!(r, 5.0); } // ── JSON built-in ───────────────────────────────────────── #[test] fn test_json_parse_primitives() { assert!(matches!(eval("JSON.parse('null')").unwrap(), Value::Null)); match eval("JSON.parse('true')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("JSON.parse('false')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } assert_eq!(eval("JSON.parse('42')").unwrap().to_number(), 42.0); match eval(r#"JSON.parse('"hello"')"#).unwrap() { Value::String(s) => assert_eq!(s, "hello"), v => panic!("expected 'hello', got {v:?}"), } } #[test] fn test_json_parse_array() { let src = r#" var a = JSON.parse('[1, 2, 3]'); a.length === 3 && a[0] === 1 && a[1] === 2 && a[2] === 3 "#; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_json_parse_object() { let src = r#" var o = JSON.parse('{"name":"test","value":42}'); o.name === "test" && o.value === 42 "#; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_json_parse_nested() { let src = r#" var o = JSON.parse('{"a":[1,{"b":2}]}'); o.a[1].b === 2 "#; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_json_parse_invalid() { assert!(eval("JSON.parse('{invalid}')").is_err()); assert!(eval("JSON.parse('')").is_err()); } #[test] fn test_json_stringify_primitives() { match eval("JSON.stringify(null)").unwrap() { Value::String(s) => assert_eq!(s, "null"), v => panic!("expected 'null', got {v:?}"), } match eval("JSON.stringify(true)").unwrap() { Value::String(s) => assert_eq!(s, "true"), v => panic!("expected 'true', got {v:?}"), } match eval("JSON.stringify(42)").unwrap() { Value::String(s) => assert_eq!(s, "42"), v => panic!("expected '42', got {v:?}"), } match eval(r#"JSON.stringify("hello")"#).unwrap() { Value::String(s) => assert_eq!(s, "\"hello\""), v => panic!("expected quoted hello, got {v:?}"), } } #[test] fn test_json_stringify_array() { match eval("JSON.stringify([1, 2, 3])").unwrap() { Value::String(s) => assert_eq!(s, "[1,2,3]"), v => panic!("expected '[1,2,3]', got {v:?}"), } } #[test] fn test_json_stringify_object() { let src = r#" var o = {a: 1, b: "hello"}; JSON.stringify(o) "#; let r = eval(src).unwrap(); match r { Value::String(s) => { assert!(s.contains("\"a\"") && s.contains("\"b\"")); assert!(s.contains('1') && s.contains("\"hello\"")); } _ => panic!("Expected string"), } } #[test] fn test_json_stringify_nested() { let src = r#" JSON.stringify({a: [1, 2], b: {c: 3}}) "#; let r = eval(src).unwrap(); match r { Value::String(s) => { assert!(s.contains("[1,2]")); assert!(s.contains("\"c\":3") || s.contains("\"c\": 3")); } _ => panic!("Expected string"), } } #[test] fn test_json_stringify_special_values() { match eval("JSON.stringify(NaN)").unwrap() { Value::String(s) => assert_eq!(s, "null"), v => panic!("expected 'null', got {v:?}"), } match eval("JSON.stringify(Infinity)").unwrap() { Value::String(s) => assert_eq!(s, "null"), v => panic!("expected 'null', got {v:?}"), } assert!(matches!( eval("JSON.stringify(undefined)").unwrap(), Value::Undefined )); } #[test] fn test_json_stringify_with_indent() { let src = r#"JSON.stringify([1, 2], null, 2)"#; let r = eval(src).unwrap(); match r { Value::String(s) => { assert!(s.contains('\n')); assert!(s.contains(" 1")); } _ => panic!("Expected string"), } } #[test] fn test_json_parse_escape_sequences() { let src = r#"JSON.parse('"hello\\nworld"')"#; match eval(src).unwrap() { Value::String(s) => assert_eq!(s, "hello\nworld"), v => panic!("expected escaped string, got {v:?}"), } } #[test] fn test_json_roundtrip() { let src = r#" var original = {name: "test", values: [1, 2, 3], nested: {ok: true}}; var json = JSON.stringify(original); var parsed = JSON.parse(json); parsed.name === "test" && parsed.values[1] === 2 && parsed.nested.ok === true "#; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_json_stringify_circular_detection() { let src = r#" var obj = {}; obj.self = obj; try { JSON.stringify(obj); false; } catch (e) { e.message.indexOf("circular") !== -1; } "#; match eval(src).unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_json_parse_unicode_escape() { let src = r#"JSON.parse('"\\u0041"')"#; match eval(src).unwrap() { Value::String(s) => assert_eq!(s, "A"), v => panic!("expected 'A', got {v:?}"), } } #[test] fn test_json_stringify_empty() { match eval("JSON.stringify([])").unwrap() { Value::String(s) => assert_eq!(s, "[]"), v => panic!("expected '[]', got {v:?}"), } match eval("JSON.stringify({})").unwrap() { Value::String(s) => assert_eq!(s, "{}"), v => panic!("expected '{{}}', got {v:?}"), } } // ── RegExp tests ──────────────────────────────────────── #[test] fn test_regexp_constructor() { match eval("var r = new RegExp('abc', 'g'); r.source").unwrap() { Value::String(s) => assert_eq!(s, "abc"), v => panic!("expected 'abc', got {v:?}"), } match eval("var r = new RegExp('abc', 'gi'); r.flags").unwrap() { Value::String(s) => assert_eq!(s, "gi"), v => panic!("expected 'gi', got {v:?}"), } match eval("var r = new RegExp('abc'); r.global").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } match eval("var r = new RegExp('abc', 'g'); r.global").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_regexp_test() { match eval("var r = new RegExp('abc'); r.test('xabcx')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("var r = new RegExp('abc'); r.test('xyz')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } match eval("var r = new RegExp('\\\\d+'); r.test('abc123')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_regexp_exec() { match eval("var r = new RegExp('(a)(b)(c)'); var m = r.exec('abc'); m[0]").unwrap() { Value::String(s) => assert_eq!(s, "abc"), v => panic!("expected 'abc', got {v:?}"), } match eval("var r = new RegExp('(a)(b)(c)'); var m = r.exec('abc'); m[1]").unwrap() { Value::String(s) => assert_eq!(s, "a"), v => panic!("expected 'a', got {v:?}"), } match eval("var r = new RegExp('b+'); var m = r.exec('aabbc'); m[0]").unwrap() { Value::String(s) => assert_eq!(s, "bb"), v => panic!("expected 'bb', got {v:?}"), } match eval("var r = new RegExp('xyz'); r.exec('abc')").unwrap() { Value::Null => {} v => panic!("expected null, got {v:?}"), } } #[test] fn test_regexp_exec_global() { let src = "var r = new RegExp('a', 'g'); r.exec('aba')[0]"; match eval(src).unwrap() { Value::String(s) => assert_eq!(s, "a"), v => panic!("expected 'a', got {v:?}"), } let src = r#" var r = new RegExp('a', 'g'); r.exec('aba'); var m = r.exec('aba'); m[0] + ',' + m.index "#; match eval(src).unwrap() { Value::String(s) => assert_eq!(s, "a,2"), v => panic!("expected 'a,2', got {v:?}"), } } #[test] fn test_regexp_to_string() { match eval("var r = new RegExp('abc', 'gi'); r.toString()").unwrap() { Value::String(s) => assert_eq!(s, "/abc/gi"), v => panic!("expected '/abc/gi', got {v:?}"), } match eval("/hello\\d+/.toString()").unwrap() { Value::String(s) => assert_eq!(s, "/hello\\d+/"), v => panic!("expected '/hello\\d+/', got {v:?}"), } } #[test] fn test_regexp_literal() { match eval("/abc/.test('abc')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("/abc/.test('xyz')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } match eval("/\\d+/.test('123')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("/abc/i.test('ABC')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_regexp_literal_exec() { match eval("var m = /([a-z]+)(\\d+)/.exec('abc123'); m[0]").unwrap() { Value::String(s) => assert_eq!(s, "abc123"), v => panic!("expected 'abc123', got {v:?}"), } match eval("var m = /([a-z]+)(\\d+)/.exec('abc123'); m[1]").unwrap() { Value::String(s) => assert_eq!(s, "abc"), v => panic!("expected 'abc', got {v:?}"), } match eval("var m = /([a-z]+)(\\d+)/.exec('abc123'); m[2]").unwrap() { Value::String(s) => assert_eq!(s, "123"), v => panic!("expected '123', got {v:?}"), } } #[test] fn test_string_match_regexp() { match eval("'hello world'.match(/world/)[0]").unwrap() { Value::String(s) => assert_eq!(s, "world"), v => panic!("expected 'world', got {v:?}"), } match eval("'aaa'.match(/a/g).length").unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } match eval("'abc'.match(/xyz/)").unwrap() { Value::Null => {} v => panic!("expected null, got {v:?}"), } } #[test] fn test_string_search_regexp() { match eval("'hello world'.search(/world/)").unwrap() { Value::Number(n) => assert_eq!(n, 6.0), v => panic!("expected 6, got {v:?}"), } match eval("'abc'.search(/xyz/)").unwrap() { Value::Number(n) => assert_eq!(n, -1.0), v => panic!("expected -1, got {v:?}"), } match eval("'abc123'.search(/\\d/)").unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } } #[test] fn test_string_replace_regexp() { match eval("'hello world'.replace(/world/, 'rust')").unwrap() { Value::String(s) => assert_eq!(s, "hello rust"), v => panic!("expected 'hello rust', got {v:?}"), } match eval("'aaa'.replace(/a/, 'b')").unwrap() { Value::String(s) => assert_eq!(s, "baa"), v => panic!("expected 'baa', got {v:?}"), } match eval("'aaa'.replace(/a/g, 'b')").unwrap() { Value::String(s) => assert_eq!(s, "bbb"), v => panic!("expected 'bbb', got {v:?}"), } } #[test] fn test_string_replace_capture_groups() { let src = r#"'John Smith'.replace(/(\w+) (\w+)/, '$2, $1')"#; match eval(src).unwrap() { Value::String(s) => assert_eq!(s, "Smith, John"), v => panic!("expected 'Smith, John', got {v:?}"), } match eval("'abc'.replace(/(b)/, '[$1]')").unwrap() { Value::String(s) => assert_eq!(s, "a[b]c"), v => panic!("expected 'a[b]c', got {v:?}"), } } #[test] fn test_string_split_regexp() { match eval("'a1b2c3'.split(/\\d/).length").unwrap() { Value::Number(n) => assert_eq!(n, 4.0), v => panic!("expected 4, got {v:?}"), } match eval("'a1b2c3'.split(/\\d/)[0]").unwrap() { Value::String(s) => assert_eq!(s, "a"), v => panic!("expected 'a', got {v:?}"), } } #[test] fn test_regexp_ignore_case() { match eval("/abc/i.exec('XAbCx')[0]").unwrap() { Value::String(s) => assert_eq!(s, "AbC"), v => panic!("expected 'AbC', got {v:?}"), } } #[test] fn test_regexp_multiline() { match eval("/^b/m.test('a\\nb')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("/^b/.test('a\\nb')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_regexp_dot_all() { match eval("/a.b/s.test('a\\nb')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("/a.b/.test('a\\nb')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_regexp_word_boundary() { match eval("/\\bfoo\\b/.test('a foo b')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("/\\bfoo\\b/.test('foobar')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_regexp_quantifiers_vm() { match eval("/a{3}/.test('aaa')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("/a{3}/.test('aa')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } match eval("/a+?/.exec('aaa')[0]").unwrap() { Value::String(s) => assert_eq!(s, "a"), v => panic!("expected 'a', got {v:?}"), } } #[test] fn test_regexp_alternation_vm() { match eval("/cat|dog/.exec('I have a dog')[0]").unwrap() { Value::String(s) => assert_eq!(s, "dog"), v => panic!("expected 'dog', got {v:?}"), } } #[test] fn test_regexp_lookahead_vm() { match eval("/a(?=b)/.test('ab')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("/a(?=b)/.test('ac')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } match eval("/a(?!b)/.test('ac')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_regexp_char_class_vm() { match eval("/[abc]/.test('b')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("/[a-z]+/.exec('Hello')[0]").unwrap() { Value::String(s) => assert_eq!(s, "ello"), v => panic!("expected 'ello', got {v:?}"), } } #[test] fn test_regexp_backreference_vm() { match eval("/(a)\\1/.test('aa')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("/(a)\\1/.test('ab')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_regexp_properties() { match eval("var r = /abc/gim; r.global").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("/abc/.lastIndex").unwrap() { Value::Number(n) => assert_eq!(n, 0.0), v => panic!("expected 0, got {v:?}"), } } #[test] fn test_string_replace_all_regexp() { match eval("'aba'.replaceAll(/a/g, 'x')").unwrap() { Value::String(s) => assert_eq!(s, "xbx"), v => panic!("expected 'xbx', got {v:?}"), } } // ── Map tests ───────────────────────────────────────────── #[test] fn test_map_basic() { match eval("var m = new Map(); m.set('a', 1); m.get('a')").unwrap() { Value::Number(n) => assert_eq!(n, 1.0), v => panic!("expected 1, got {v:?}"), } } #[test] fn test_map_size() { match eval("var m = new Map(); m.set('a', 1); m.set('b', 2); m.size").unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_map_has() { match eval("var m = new Map(); m.set('x', 10); m.has('x')").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("var m = new Map(); m.has('x')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_map_delete() { match eval("var m = new Map(); m.set('a', 1); m['delete']('a'); m.has('a')").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } match eval("var m = new Map(); m.set('a', 1); m['delete']('a'); m.size").unwrap() { Value::Number(n) => assert_eq!(n, 0.0), v => panic!("expected 0, got {v:?}"), } } #[test] fn test_map_clear() { match eval("var m = new Map(); m.set('a', 1); m.set('b', 2); m.clear(); m.size").unwrap() { Value::Number(n) => assert_eq!(n, 0.0), v => panic!("expected 0, got {v:?}"), } } #[test] fn test_map_overwrite() { match eval("var m = new Map(); m.set('a', 1); m.set('a', 2); m.get('a')").unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } // Size should still be 1 after overwriting. match eval("var m = new Map(); m.set('a', 1); m.set('a', 2); m.size").unwrap() { Value::Number(n) => assert_eq!(n, 1.0), v => panic!("expected 1, got {v:?}"), } } #[test] fn test_map_get_missing() { match eval("var m = new Map(); m.get('missing')").unwrap() { Value::Undefined => {} v => panic!("expected undefined, got {v:?}"), } } #[test] fn test_map_chaining() { // set() returns the Map for chaining. match eval("var m = new Map(); m.set('a', 1).set('b', 2); m.size").unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_map_nan_key() { // NaN === NaN for Map keys (SameValueZero). match eval("var m = new Map(); m.set(NaN, 'nan'); m.get(NaN)").unwrap() { Value::String(s) => assert_eq!(s, "nan"), v => panic!("expected 'nan', got {v:?}"), } } #[test] fn test_map_object_key() { match eval("var m = new Map(); var o = {}; m.set(o, 'val'); m.get(o)").unwrap() { Value::String(s) => assert_eq!(s, "val"), v => panic!("expected 'val', got {v:?}"), } } #[test] fn test_map_constructor_with_pairs() { match eval("var m = new Map([['a', 1], ['b', 2]]); m.get('b')").unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_map_keys_values_entries() { match eval("var m = new Map(); m.set('a', 1); m.set('b', 2); m.keys().length").unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } match eval("var m = new Map(); m.set('a', 1); m.set('b', 2); m.values()[1]").unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } match eval("var m = new Map(); m.set('a', 1); m.entries()[0][0]").unwrap() { Value::String(s) => assert_eq!(s, "a"), v => panic!("expected 'a', got {v:?}"), } } #[test] fn test_map_insertion_order() { match eval("var m = new Map(); m.set('c', 3); m.set('a', 1); m.set('b', 2); m.keys()[0]") .unwrap() { Value::String(s) => assert_eq!(s, "c"), v => panic!("expected 'c', got {v:?}"), } } // ── Set tests ───────────────────────────────────────────── #[test] fn test_set_basic() { match eval("var s = new Set(); s.add(1); s.add(2); s.size").unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_set_has() { match eval("var s = new Set(); s.add(42); s.has(42)").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("var s = new Set(); s.has(42)").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_set_delete() { match eval("var s = new Set(); s.add(1); s['delete'](1); s.has(1)").unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } match eval("var s = new Set(); s.add(1); s['delete'](1); s.size").unwrap() { Value::Number(n) => assert_eq!(n, 0.0), v => panic!("expected 0, got {v:?}"), } } #[test] fn test_set_clear() { match eval("var s = new Set(); s.add(1); s.add(2); s.clear(); s.size").unwrap() { Value::Number(n) => assert_eq!(n, 0.0), v => panic!("expected 0, got {v:?}"), } } #[test] fn test_set_uniqueness() { match eval("var s = new Set(); s.add(1); s.add(1); s.add(1); s.size").unwrap() { Value::Number(n) => assert_eq!(n, 1.0), v => panic!("expected 1, got {v:?}"), } } #[test] fn test_set_chaining() { match eval("var s = new Set(); s.add(1).add(2).add(3); s.size").unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } } #[test] fn test_set_nan() { match eval("var s = new Set(); s.add(NaN); s.add(NaN); s.size").unwrap() { Value::Number(n) => assert_eq!(n, 1.0), v => panic!("expected 1, got {v:?}"), } match eval("var s = new Set(); s.add(NaN); s.has(NaN)").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_set_constructor_from_array() { match eval("var s = new Set([1, 2, 3, 2, 1]); s.size").unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } } #[test] fn test_set_values() { match eval("var s = new Set(); s.add('a'); s.add('b'); s.values().length").unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_set_entries() { // Set.entries() returns [value, value] pairs. match eval("var s = new Set(); s.add('x'); s.entries()[0][0]").unwrap() { Value::String(s) => assert_eq!(s, "x"), v => panic!("expected 'x', got {v:?}"), } match eval("var s = new Set(); s.add('x'); s.entries()[0][1]").unwrap() { Value::String(s) => assert_eq!(s, "x"), v => panic!("expected 'x', got {v:?}"), } } #[test] fn test_set_insertion_order() { match eval("var s = new Set(); s.add('c'); s.add('a'); s.add('b'); s.values()[0]").unwrap() { Value::String(s) => assert_eq!(s, "c"), v => panic!("expected 'c', got {v:?}"), } } // ── WeakMap tests ───────────────────────────────────────── #[test] fn test_weakmap_basic() { match eval("var wm = new WeakMap(); var o = {}; wm.set(o, 'val'); wm.get(o)").unwrap() { Value::String(s) => assert_eq!(s, "val"), v => panic!("expected 'val', got {v:?}"), } } #[test] fn test_weakmap_has_delete() { match eval("var wm = new WeakMap(); var o = {}; wm.set(o, 1); wm.has(o)").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } match eval("var wm = new WeakMap(); var o = {}; wm.set(o, 1); wm['delete'](o); wm.has(o)") .unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_weakmap_rejects_primitive_key() { assert!(eval("var wm = new WeakMap(); wm.set('str', 1)").is_err()); assert!(eval("var wm = new WeakMap(); wm.set(42, 1)").is_err()); } // ── WeakSet tests ───────────────────────────────────────── #[test] fn test_weakset_basic() { match eval("var ws = new WeakSet(); var o = {}; ws.add(o); ws.has(o)").unwrap() { Value::Boolean(b) => assert!(b), v => panic!("expected true, got {v:?}"), } } #[test] fn test_weakset_delete() { match eval("var ws = new WeakSet(); var o = {}; ws.add(o); ws['delete'](o); ws.has(o)") .unwrap() { Value::Boolean(b) => assert!(!b), v => panic!("expected false, got {v:?}"), } } #[test] fn test_weakset_rejects_primitive() { assert!(eval("var ws = new WeakSet(); ws.add('str')").is_err()); assert!(eval("var ws = new WeakSet(); ws.add(42)").is_err()); } // ── Promise tests ──────────────────────────────────────────── #[test] fn test_promise_typeof() { match eval("typeof Promise").unwrap() { Value::String(s) => assert_eq!(s, "function"), v => panic!("expected 'function', got {v:?}"), } } #[test] fn test_promise_static_resolve_exists() { match eval("typeof Promise.resolve").unwrap() { Value::String(s) => assert_eq!(s, "function"), v => panic!("expected 'function', got {v:?}"), } } #[test] fn test_promise_resolve_returns_object() { match eval("typeof Promise.resolve(42)").unwrap() { Value::String(s) => assert_eq!(s, "object"), v => panic!("expected 'object', got {v:?}"), } } #[test] fn test_promise_resolve_then_exists() { match eval("typeof Promise.resolve(42).then").unwrap() { Value::String(s) => assert_eq!(s, "function"), v => panic!("expected 'function', got {v:?}"), } } /// Helper: eval JS, then read an undeclared global set by callbacks. /// NOTE: Variables set inside closures MUST NOT be declared with `var` /// in the same scope that creates the closure, because the compiler /// would put them in Cells instead of globals. fn eval_global(source: &str, name: &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)?; Ok(vm.globals.get(name).cloned().unwrap_or(Value::Undefined)) } #[test] fn test_promise_resolve_then() { // Don't use `var result` — it would be captured. Use implicit global. match eval_global( "Promise.resolve(42).then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_promise_reject_catch() { // Use bracket notation for catch (it's a keyword in the parser). match eval_global( "Promise.reject('err')['catch'](function(e) { result = e; });", "result", ) .unwrap() { Value::String(s) => assert_eq!(s, "err"), v => panic!("expected 'err', got {v:?}"), } } #[test] fn test_promise_constructor_resolve() { match eval_global( "var p = Promise(function(resolve) { resolve(10); }); p.then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 10.0), v => panic!("expected 10, got {v:?}"), } } #[test] fn test_promise_constructor_reject() { match eval_global( "var p = Promise(function(resolve, reject) { reject('fail'); }); p['catch'](function(e) { result = e; });", "result", ) .unwrap() { Value::String(s) => assert_eq!(s, "fail"), v => panic!("expected 'fail', got {v:?}"), } } #[test] fn test_promise_executor_runs_synchronously() { // Executor runs synchronously. Use eval to check completion value. match eval("var x = 'before'; Promise(function(resolve) { x = 'during'; resolve(); }); x") .unwrap() { Value::String(s) => assert_eq!(s, "during"), v => panic!("expected 'during', got {v:?}"), } } #[test] fn test_promise_then_chaining() { match eval_global( "Promise.resolve(1).then(function(v) { return v + 1; }).then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 2.0), v => panic!("expected 2, got {v:?}"), } } #[test] fn test_promise_catch_returns_to_then() { match eval_global( "Promise.reject('err')['catch'](function(e) { return 'recovered'; }).then(function(v) { result = v; });", "result", ) .unwrap() { Value::String(s) => assert_eq!(s, "recovered"), v => panic!("expected 'recovered', got {v:?}"), } } #[test] fn test_promise_then_error_goes_to_catch() { // When a then handler throws, the rejection reason is the thrown value // wrapped in an Error object by the VM. Check it's an object. match eval_global( "Promise.resolve(1).then(function(v) { throw 'oops'; })['catch'](function(e) { result = typeof e; });", "result", ) .unwrap() { // The thrown string gets wrapped in an error object by RuntimeError::to_value. Value::String(s) => assert!( s == "object" || s == "string", "expected 'object' or 'string', got '{s}'" ), v => panic!("expected string type, got {v:?}"), } } #[test] fn test_promise_resolve_with_promise() { match eval_global( "var p = Promise.resolve(99); p.then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 99.0), v => panic!("expected 99, got {v:?}"), } } #[test] fn test_promise_multiple_then() { match eval_global( "var p = Promise.resolve(5); p.then(function(v) { a = v; }); p.then(function(v) { b = v * 2; });", "a", ) .unwrap() { Value::Number(n) => assert_eq!(n, 5.0), v => panic!("expected 5, got {v:?}"), } } #[test] fn test_promise_double_resolve_ignored() { match eval_global( "var p = Promise(function(resolve) { resolve(1); resolve(2); }); p.then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 1.0), v => panic!("expected 1, got {v:?}"), } } #[test] fn test_promise_executor_throw_rejects() { match eval_global( "var p = Promise(function() { throw 'boom'; }); p['catch'](function(e) { result = e; });", "result", ) .unwrap() { Value::String(s) => assert_eq!(s, "boom"), v => panic!("expected 'boom', got {v:?}"), } } #[test] fn test_promise_finally_fulfilled() { match eval_global( "Promise.resolve(42)['finally'](function() { result = 'done'; });", "result", ) .unwrap() { Value::String(s) => assert_eq!(s, "done"), v => panic!("expected 'done', got {v:?}"), } } #[test] fn test_promise_finally_rejected() { match eval_global( "Promise.reject('err')['finally'](function() { result = 'done'; });", "result", ) .unwrap() { Value::String(s) => assert_eq!(s, "done"), v => panic!("expected 'done', got {v:?}"), } } #[test] fn test_promise_all_empty() { match eval_global( "Promise.all([]).then(function(v) { result = v.length; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 0.0), v => panic!("expected 0, got {v:?}"), } } #[test] fn test_promise_all_resolved() { match eval_global( "Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]).then(function(v) { result = v[0] + v[1] + v[2]; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 6.0), v => panic!("expected 6, got {v:?}"), } } #[test] fn test_promise_all_rejects_on_first() { match eval_global( "Promise.all([Promise.resolve(1), Promise.reject('fail')])['catch'](function(e) { result = e; });", "result", ) .unwrap() { Value::String(s) => assert_eq!(s, "fail"), v => panic!("expected 'fail', got {v:?}"), } } #[test] fn test_promise_race_first_wins() { match eval_global( "Promise.race([Promise.resolve('first'), Promise.resolve('second')]).then(function(v) { result = v; });", "result", ) .unwrap() { Value::String(s) => assert_eq!(s, "first"), v => panic!("expected 'first', got {v:?}"), } } #[test] fn test_promise_race_reject_wins() { match eval_global( "Promise.race([Promise.reject('err')])['catch'](function(e) { result = e; });", "result", ) .unwrap() { Value::String(s) => assert_eq!(s, "err"), v => panic!("expected 'err', got {v:?}"), } } #[test] fn test_promise_any_first_fulfilled() { match eval_global( "Promise.any([Promise.reject('a'), Promise.resolve('b')]).then(function(v) { result = v; });", "result", ) .unwrap() { Value::String(s) => assert_eq!(s, "b"), v => panic!("expected 'b', got {v:?}"), } } #[test] fn test_promise_any_all_rejected() { match eval_global( "Promise.any([Promise.reject('a'), Promise.reject('b')])['catch'](function(e) { result = e; });", "result", ) .unwrap() { Value::String(ref s) if s.contains("rejected") => {} v => panic!("expected AggregateError string, got {v:?}"), } } #[test] fn test_promise_all_with_non_promises() { match eval_global( "Promise.all([1, 2, 3]).then(function(v) { result = v[0] + v[1] + v[2]; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 6.0), v => panic!("expected 6, got {v:?}"), } } #[test] fn test_promise_race_with_non_promise() { match eval_global( "Promise.race([42]).then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_promise_then_identity_passthrough() { match eval_global( "Promise.resolve(7).then().then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 7.0), v => panic!("expected 7, got {v:?}"), } } #[test] fn test_promise_catch_passthrough() { match eval_global( "Promise.resolve(99)['catch'](function(e) { result = 'bad'; }).then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 99.0), v => panic!("expected 99, got {v:?}"), } } #[test] fn test_promise_all_settled() { match eval_global( "Promise.allSettled([Promise.resolve(1), Promise.reject('err')]).then(function(v) { result = v[0].status + ',' + v[1].status; });", "result", ) .unwrap() { Value::String(s) => assert_eq!(s, "fulfilled,rejected"), v => panic!("expected 'fulfilled,rejected', got {v:?}"), } } // ── Iterator and for...of tests ──────────────────────── #[test] fn test_for_of_array() { match eval( "var result = ''; var arr = [10, 20, 30]; for (var x of arr) { result = result + x + ','; } result", ) .unwrap() { Value::String(s) => assert_eq!(s, "10,20,30,"), v => panic!("expected '10,20,30,', got {v:?}"), } } #[test] fn test_for_of_string() { match eval("var result = ''; for (var ch of 'abc') { result = result + ch; } result") .unwrap() { Value::String(s) => assert_eq!(s, "abc"), v => panic!("expected 'abc', got {v:?}"), } } #[test] fn test_for_of_with_break() { match eval( "var result = 0; for (var x of [1, 2, 3, 4, 5]) { if (x === 3) break; result = result + x; } result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } } #[test] fn test_for_of_with_continue() { match eval( "var result = 0; for (var x of [1, 2, 3, 4, 5]) { if (x === 3) continue; result = result + x; } result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 12.0), v => panic!("expected 12, got {v:?}"), } } // ── Generator tests ──────────────────────────────────── #[test] fn test_generator_typeof() { // First test: does gen() return an object? match eval("function* gen() { yield 1; } typeof gen()").unwrap() { Value::String(s) => assert_eq!(s, "object"), v => panic!("expected 'object', got {v:?}"), } } #[test] fn test_generator_has_next() { // Test: does the generator have a next method? match eval("function* gen() { yield 1; } var g = gen(); typeof g.next").unwrap() { Value::String(s) => assert_eq!(s, "function"), v => panic!("expected 'function', got {v:?}"), } } #[test] fn test_basic_generator() { match eval( "function* gen() { yield 1; yield 2; yield 3; } var g = gen(); var a = g.next(); a.value", ) .unwrap() { Value::Number(n) => assert_eq!(n, 1.0), v => panic!("expected 1, got {v:?}"), } } #[test] fn test_generator_multiple_yields() { match eval( "function* gen() { yield 10; yield 20; yield 30; } var g = gen(); var r1 = g.next(); var r2 = g.next(); var r3 = g.next(); var r4 = g.next(); '' + r1.value + ',' + r2.value + ',' + r3.value + ',' + r4.done", ) .unwrap() { Value::String(s) => assert_eq!(s, "10,20,30,true"), v => panic!("expected '10,20,30,true', got {v:?}"), } } #[test] fn test_generator_send_value() { match eval( "function* gen() { var x = yield 'hello'; yield x + ' world'; } var g = gen(); g.next(); var r = g.next('beautiful'); r.value", ) .unwrap() { Value::String(s) => assert_eq!(s, "beautiful world"), v => panic!("expected 'beautiful world', got {v:?}"), } } #[test] fn test_generator_return() { match eval( "function* gen() { yield 1; yield 2; yield 3; } var g = gen(); g.next(); var r = g['return'](42); '' + r.value + ',' + r.done", ) .unwrap() { Value::String(s) => assert_eq!(s, "42,true"), v => panic!("expected '42,true', got {v:?}"), } } #[test] fn test_generator_in_for_of() { match eval( "function* range(start, end) { for (var i = start; i < end; i = i + 1) { yield i; } } var result = 0; for (var n of range(1, 5)) { result = result + n; } result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 10.0), v => panic!("expected 10, got {v:?}"), } } #[test] fn test_generator_with_return_value() { match eval( "function* gen() { yield 1; return 'done'; } var g = gen(); var a = g.next(); var b = g.next(); '' + a.value + ',' + a.done + ',' + b.value + ',' + b.done", ) .unwrap() { Value::String(s) => assert_eq!(s, "1,false,done,true"), v => panic!("expected '1,false,done,true', got {v:?}"), } } // ── Destructuring tests ──────────────────────────────── #[test] fn test_array_destructuring_basic() { match eval("var [a, b, c] = [1, 2, 3]; a + b + c").unwrap() { Value::Number(n) => assert_eq!(n, 6.0), v => panic!("expected 6, got {v:?}"), } } #[test] fn test_array_destructuring_rest() { match eval("var [first, ...rest] = [1, 2, 3, 4]; '' + first + ',' + rest.length").unwrap() { Value::String(s) => assert_eq!(s, "1,3"), v => panic!("expected '1,3', got {v:?}"), } } #[test] fn test_array_destructuring_default() { match eval("var [a = 10, b = 20] = [1]; '' + a + ',' + b").unwrap() { Value::String(s) => assert_eq!(s, "1,20"), v => panic!("expected '1,20', got {v:?}"), } } #[test] fn test_object_destructuring_basic() { match eval("var {x, y} = {x: 1, y: 2}; x + y").unwrap() { Value::Number(n) => assert_eq!(n, 3.0), v => panic!("expected 3, got {v:?}"), } } #[test] fn test_object_destructuring_alias() { match eval("var {x: a, y: b} = {x: 10, y: 20}; a + b").unwrap() { Value::Number(n) => assert_eq!(n, 30.0), v => panic!("expected 30, got {v:?}"), } } #[test] fn test_nested_destructuring() { match eval("var {a: {b}} = {a: {b: 42}}; b").unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_destructuring_in_for_of() { match eval( "var result = 0; var pairs = [[1, 2], [3, 4], [5, 6]]; for (var [a, b] of pairs) { result = result + a + b; } result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 21.0), v => panic!("expected 21, got {v:?}"), } } // ── Spread tests ─────────────────────────────────────── #[test] fn test_spread_in_array() { match eval("var a = [1, 2, 3]; var b = [0, ...a, 4]; b.length").unwrap() { Value::Number(n) => assert_eq!(n, 5.0), v => panic!("expected 5, got {v:?}"), } } #[test] fn test_spread_in_array_values() { match eval( "var a = [1, 2, 3]; var b = [0, ...a, 4]; '' + b[0] + ',' + b[1] + ',' + b[2] + ',' + b[3] + ',' + b[4]", ) .unwrap() { Value::String(s) => assert_eq!(s, "0,1,2,3,4"), v => panic!("expected '0,1,2,3,4', got {v:?}"), } } // ── Custom iterable tests ────────────────────────────── #[test] fn test_custom_iterable() { match eval( "var obj = {}; obj['@@iterator'] = function() { var i = 0; return { next: function() { i = i + 1; if (i <= 3) return {value: i, done: false}; return {value: undefined, done: true}; } }; }; var result = 0; for (var v of obj) { result = result + v; } result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 6.0), v => panic!("expected 6, got {v:?}"), } } // ── Array.from with iterables ────────────────────────── #[test] fn test_array_keys_values_entries() { match eval( "var arr = [10, 20, 30]; var r = ''; for (var v of arr.values()) { r = r + v + ','; } r", ) .unwrap() { Value::String(s) => assert_eq!(s, "10,20,30,"), v => panic!("expected '10,20,30,', got {v:?}"), } } // ── Async/await tests ──────────────────────────────────── #[test] fn test_async_function_returns_promise() { match eval("async function f() { return 42; } typeof f()").unwrap() { Value::String(s) => assert_eq!(s, "object"), v => panic!("expected 'object', got {v:?}"), } } #[test] fn test_async_function_resolves_value() { match eval_global( "async function f() { return 42; } f().then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_await_resolved_promise() { match eval_global( "async function f() { var x = await Promise.resolve(10); return x + 5; } f().then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 15.0), v => panic!("expected 15, got {v:?}"), } } #[test] fn test_await_non_promise_value() { match eval_global( "async function f() { var x = await 7; return x * 3; } f().then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 21.0), v => panic!("expected 21, got {v:?}"), } } #[test] fn test_multiple_awaits_in_sequence() { match eval_global( "async function f() { var a = await Promise.resolve(1); var b = await Promise.resolve(2); var c = await Promise.resolve(3); return a + b + c; } f().then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 6.0), v => panic!("expected 6, got {v:?}"), } } #[test] fn test_await_rejected_promise_throws() { match eval_global( "async function f() { try { await Promise.reject('oops'); } catch(e) { return 'caught: ' + e; } } f().then(function(v) { result = v; });", "result", ) .unwrap() { Value::String(s) => assert_eq!(s, "caught: oops"), v => panic!("expected 'caught: oops', got {v:?}"), } } #[test] fn test_async_function_throw_rejects() { match eval_global( "async function f() { throw 'error!'; } f()['catch'](function(e) { result = 'got: ' + e; });", "result", ) .unwrap() { Value::String(s) => assert_eq!(s, "got: error!"), v => panic!("expected 'got: error!', got {v:?}"), } } #[test] fn test_async_function_expression() { match eval_global( "var f = async function() { var x = await Promise.resolve(99); return x; }; f().then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 99.0), v => panic!("expected 99, got {v:?}"), } } #[test] fn test_async_function_expression_with_args() { match eval_global( "var f = async function(x) { return x * 2; }; f(21).then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_async_arrow_function() { match eval_global( "var f = async x => { var y = await Promise.resolve(x); return y + 1; }; f(10).then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 11.0), v => panic!("expected 11, got {v:?}"), } } #[test] fn test_async_arrow_concise_body() { match eval_global( "var f = async x => x * 3; f(7).then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 21.0), v => panic!("expected 21, got {v:?}"), } } #[test] fn test_await_chained_promises() { match eval_global( "async function f() { var p = Promise.resolve(5).then(function(v) { return v * 10; }); return await p; } f().then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 50.0), v => panic!("expected 50, got {v:?}"), } } #[test] fn test_async_function_with_closure() { match eval_global( "function make() { var x = 100; return async function() { var y = await Promise.resolve(23); return x + y; }; } make()().then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 123.0), v => panic!("expected 123, got {v:?}"), } } #[test] fn test_async_method_in_object() { match eval_global( "var obj = { async getValue() { return await Promise.resolve(42); } }; obj.getValue().then(function(v) { result = v; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 42.0), v => panic!("expected 42, got {v:?}"), } } #[test] fn test_async_generator_basic() { match eval_global( "async function* gen() { yield 1; yield 2; yield 3; } var it = gen(); it.next().then(function(r) { result = r.value; });", "result", ) .unwrap() { Value::Number(n) => assert_eq!(n, 1.0), v => panic!("expected 1, got {v:?}"), } } #[test] fn test_for_await_of_parsing() { let prog = crate::parser::Parser::parse("async function f() { for await (let x of iter) { } }"); assert!(prog.is_ok()); } // ── Console API tests ───────────────────────────────────── use std::cell::RefCell; use std::rc::Rc; /// A console output sink that captures messages for testing. struct CapturedConsole { log_messages: RefCell>, error_messages: RefCell>, warn_messages: RefCell>, } impl CapturedConsole { fn new() -> Self { Self { log_messages: RefCell::new(Vec::new()), error_messages: RefCell::new(Vec::new()), warn_messages: RefCell::new(Vec::new()), } } } impl ConsoleOutput for CapturedConsole { fn log(&self, message: &str) { self.log_messages.borrow_mut().push(message.to_string()); } fn error(&self, message: &str) { self.error_messages.borrow_mut().push(message.to_string()); } fn warn(&self, message: &str) { self.warn_messages.borrow_mut().push(message.to_string()); } } /// Helper: compile and execute JS, capturing console output. fn eval_with_console(source: &str) -> (Result, Rc) { let console = Rc::new(CapturedConsole::new()); let program = Parser::parse(source).expect("parse failed"); let func = compiler::compile(&program).expect("compile failed"); let mut vm = Vm::new(); vm.set_console_output(Box::new(RcConsole(console.clone()))); let result = vm.execute(&func); (result, console) } /// Wrapper to use Rc as Box. struct RcConsole(Rc); impl ConsoleOutput for RcConsole { fn log(&self, message: &str) { self.0.log(message); } fn error(&self, message: &str) { self.0.error(message); } fn warn(&self, message: &str) { self.0.warn(message); } } #[test] fn test_console_log_string() { let (result, console) = eval_with_console("console.log('hello')"); assert!(result.is_ok()); assert!(matches!(result.unwrap(), Value::Undefined)); let logs = console.log_messages.borrow(); assert_eq!(logs.len(), 1); assert_eq!(logs[0], "hello"); } #[test] fn test_console_log_multiple_args() { let (_, console) = eval_with_console("console.log('a', 1, true)"); let logs = console.log_messages.borrow(); assert_eq!(logs[0], "a 1 true"); } #[test] fn test_console_error_to_error_channel() { let (_, console) = eval_with_console("console.error('oops')"); assert!(console.log_messages.borrow().is_empty()); let errors = console.error_messages.borrow(); assert_eq!(errors.len(), 1); assert_eq!(errors[0], "oops"); } #[test] fn test_console_warn_to_warn_channel() { let (_, console) = eval_with_console("console.warn('warning')"); assert!(console.log_messages.borrow().is_empty()); let warns = console.warn_messages.borrow(); assert_eq!(warns.len(), 1); assert_eq!(warns[0], "warning"); } #[test] fn test_console_info_aliases_log() { let (_, console) = eval_with_console("console.info('info msg')"); let logs = console.log_messages.borrow(); assert_eq!(logs.len(), 1); assert_eq!(logs[0], "info msg"); } #[test] fn test_console_debug_aliases_log() { let (_, console) = eval_with_console("console.debug('debug msg')"); let logs = console.log_messages.borrow(); assert_eq!(logs.len(), 1); assert_eq!(logs[0], "debug msg"); } #[test] fn test_console_log_returns_undefined() { let result = eval("console.log('test')").unwrap(); assert!(matches!(result, Value::Undefined)); } #[test] fn test_console_log_no_args() { let (_, console) = eval_with_console("console.log()"); let logs = console.log_messages.borrow(); assert_eq!(logs.len(), 1); assert_eq!(logs[0], ""); } #[test] fn test_console_log_primitives() { let (_, console) = eval_with_console( "console.log(undefined); console.log(null); console.log(42); console.log(true);", ); let logs = console.log_messages.borrow(); assert_eq!(logs.len(), 4); assert_eq!(logs[0], "undefined"); assert_eq!(logs[1], "null"); assert_eq!(logs[2], "42"); assert_eq!(logs[3], "true"); } #[test] fn test_console_log_array() { let (_, console) = eval_with_console("console.log([1, 2, 3])"); let logs = console.log_messages.borrow(); assert_eq!(logs[0], "[ 1, 2, 3 ]"); } #[test] fn test_console_log_object() { let (_, console) = eval_with_console("console.log({x: 1})"); let logs = console.log_messages.borrow(); assert_eq!(logs[0], "{ x: 1 }"); } #[test] fn test_console_typeof() { match eval("typeof console.log").unwrap() { Value::String(s) => assert_eq!(s, "function"), v => panic!("expected 'function', got {v:?}"), } } #[test] fn test_console_is_object() { match eval("typeof console").unwrap() { Value::String(s) => assert_eq!(s, "object"), v => panic!("expected 'object', got {v:?}"), } } #[test] fn test_console_never_throws() { let result = eval("console.log({x: 1}); console.log([1,2]); console.log(undefined); 'ok'"); assert!(result.is_ok()); match result.unwrap() { Value::String(s) => assert_eq!(s, "ok"), v => panic!("expected 'ok', got {v:?}"), } } // ── Inline cache tests ───────────────────────────────────── #[test] fn test_ic_property_read_in_loop() { // Repeated property access on same-shaped objects should use IC fast path. let result = eval( "var sum = 0; for (var i = 0; i < 10; i++) { var obj = {x: i}; sum = sum + obj.x; } sum", ); match result.unwrap() { Value::Number(n) => assert_eq!(n, 45.0), v => panic!("expected 45, got {v:?}"), } } #[test] fn test_ic_property_write_in_loop() { // Repeated property writes on same-shaped objects. let result = eval( "var obj = {x: 0}; for (var i = 0; i < 100; i++) { obj.x = i; } obj.x", ); match result.unwrap() { Value::Number(n) => assert_eq!(n, 99.0), v => panic!("expected 99, got {v:?}"), } } #[test] fn test_ic_different_shapes() { // Property access on objects with different shapes still works correctly. let result = eval( "function getX(o) { return o.x; } var a = {x: 1}; var b = {y: 2, x: 3}; var c = {z: 4, x: 5}; getX(a) + getX(b) + getX(c)", ); match result.unwrap() { Value::Number(n) => assert_eq!(n, 9.0), v => panic!("expected 9, got {v:?}"), } } #[test] fn test_ic_object_with_prototype() { // Property access on objects with prototypes should still work // (prototype chain properties are not cached by IC, but should resolve). let result = eval( "var proto = {x: 10}; var obj = Object.create(proto); obj.y = 20; obj.x + obj.y", ); match result.unwrap() { Value::Number(n) => assert_eq!(n, 30.0), v => panic!("expected 30, got {v:?}"), } } #[test] fn test_ic_multiple_properties() { // Multiple property accesses in a loop on the same object. let result = eval( "var obj = {a: 1, b: 2, c: 3}; var sum = 0; for (var i = 0; i < 10; i++) { sum = sum + obj.a + obj.b + obj.c; } sum", ); match result.unwrap() { Value::Number(n) => assert_eq!(n, 60.0), v => panic!("expected 60, got {v:?}"), } } }