//! Built-in JavaScript objects and functions: Object, Array, Function, Error, //! String, Number, Boolean, Symbol, Math, Date, JSON, Promise. //! //! Registers constructors, static methods, and prototype methods as globals //! in the VM. Callback-based array methods (map, filter, etc.) are defined //! via a JS preamble executed at init time. use crate::gc::{Gc, GcRef}; use crate::vm::*; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::time::{SystemTime, UNIX_EPOCH}; /// Native callback type alias to satisfy clippy::type_complexity. type NativeMethod = ( &'static str, fn(&[Value], &mut NativeContext) -> Result, ); // ── Helpers ────────────────────────────────────────────────── /// Create a native function GcRef. pub fn make_native( gc: &mut Gc, name: &str, callback: fn(&[Value], &mut NativeContext) -> Result, ) -> GcRef { 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(), }))) } /// Set a non-enumerable property on an object. pub fn set_builtin_prop(gc: &mut Gc, obj: GcRef, key: &str, val: Value) { if let Some(HeapObject::Object(data)) = gc.get_mut(obj) { data.properties .insert(key.to_string(), Property::builtin(val)); } } /// Set a non-enumerable property on a function. fn set_func_prop(gc: &mut Gc, func: GcRef, key: &str, val: Value) { if let Some(HeapObject::Function(fdata)) = gc.get_mut(func) { fdata .properties .insert(key.to_string(), Property::builtin(val)); } } /// Get own enumerable string keys of an object (for Object.keys, etc.). fn own_enumerable_keys(gc: &Gc, obj_ref: GcRef) -> Vec { match gc.get(obj_ref) { Some(HeapObject::Object(data)) => { let mut int_keys: Vec<(u32, String)> = Vec::new(); let mut str_keys: Vec = Vec::new(); for (k, prop) in &data.properties { if prop.enumerable { if let Ok(idx) = k.parse::() { int_keys.push((idx, k.clone())); } else { str_keys.push(k.clone()); } } } int_keys.sort_by_key(|(idx, _)| *idx); let mut result: Vec = int_keys.into_iter().map(|(_, k)| k).collect(); result.extend(str_keys); result } _ => Vec::new(), } } /// Get all own property names (enumerable or not) of an object. fn own_property_names(gc: &Gc, obj_ref: GcRef) -> Vec { match gc.get(obj_ref) { Some(HeapObject::Object(data)) => { let mut int_keys: Vec<(u32, String)> = Vec::new(); let mut str_keys: Vec = Vec::new(); for k in data.properties.keys() { if let Ok(idx) = k.parse::() { int_keys.push((idx, k.clone())); } else { str_keys.push(k.clone()); } } int_keys.sort_by_key(|(idx, _)| *idx); let mut result: Vec = int_keys.into_iter().map(|(_, k)| k).collect(); result.extend(str_keys); result } _ => Vec::new(), } } /// Read the "length" property of an array-like object as usize. fn array_length(gc: &Gc, obj: GcRef) -> usize { match gc.get(obj) { Some(HeapObject::Object(data)) => match data.properties.get("length") { Some(prop) => prop.value.to_number() as usize, None => 0, }, _ => 0, } } /// Set the "length" property on an array-like object. fn set_array_length(gc: &mut Gc, obj: GcRef, len: usize) { if let Some(HeapObject::Object(data)) = gc.get_mut(obj) { if let Some(prop) = data.properties.get_mut("length") { prop.value = Value::Number(len as f64); } else { data.properties.insert( "length".to_string(), Property { value: Value::Number(len as f64), writable: true, enumerable: false, configurable: false, }, ); } } } /// Check whether an object has a "length" property (i.e. is array-like). fn array_length_exists(gc: &Gc, obj: GcRef) -> bool { match gc.get(obj) { Some(HeapObject::Object(data)) => data.properties.contains_key("length"), _ => false, } } /// Get an element by index from an array-like object. fn array_get(gc: &Gc, obj: GcRef, idx: usize) -> Value { match gc.get(obj) { Some(HeapObject::Object(data)) => { let key = idx.to_string(); data.properties .get(&key) .map(|p| p.value.clone()) .unwrap_or(Value::Undefined) } _ => Value::Undefined, } } /// Set an element by index on an array-like object. fn array_set(gc: &mut Gc, obj: GcRef, idx: usize, val: Value) { if let Some(HeapObject::Object(data)) = gc.get_mut(obj) { data.properties.insert(idx.to_string(), Property::data(val)); } } // ── Initialization ─────────────────────────────────────────── /// Initialize all built-in objects and register them in the VM. pub fn init_builtins(vm: &mut Vm) { // Create Object.prototype first (root of the prototype chain). let obj_proto = vm.gc.alloc(HeapObject::Object(ObjectData::new())); init_object_prototype(&mut vm.gc, obj_proto); // Create Array.prototype (inherits from Object.prototype). let mut arr_proto_data = ObjectData::new(); arr_proto_data.prototype = Some(obj_proto); let arr_proto = vm.gc.alloc(HeapObject::Object(arr_proto_data)); init_array_prototype(&mut vm.gc, arr_proto); // Create Error.prototype (inherits from Object.prototype). let mut err_proto_data = ObjectData::new(); err_proto_data.prototype = Some(obj_proto); err_proto_data.properties.insert( "name".to_string(), Property::builtin(Value::String("Error".to_string())), ); err_proto_data.properties.insert( "message".to_string(), Property::builtin(Value::String(String::new())), ); let err_proto = vm.gc.alloc(HeapObject::Object(err_proto_data)); init_error_prototype(&mut vm.gc, err_proto); // Create String.prototype (inherits from Object.prototype). let mut str_proto_data = ObjectData::new(); str_proto_data.prototype = Some(obj_proto); let str_proto = vm.gc.alloc(HeapObject::Object(str_proto_data)); init_string_prototype(&mut vm.gc, str_proto); // Create Number.prototype (inherits from Object.prototype). let mut num_proto_data = ObjectData::new(); num_proto_data.prototype = Some(obj_proto); let num_proto = vm.gc.alloc(HeapObject::Object(num_proto_data)); init_number_prototype(&mut vm.gc, num_proto); // Create Boolean.prototype (inherits from Object.prototype). let mut bool_proto_data = ObjectData::new(); bool_proto_data.prototype = Some(obj_proto); let bool_proto = vm.gc.alloc(HeapObject::Object(bool_proto_data)); init_boolean_prototype(&mut vm.gc, bool_proto); // Store prototypes in VM for use by CreateArray/CreateObject and auto-boxing. vm.object_prototype = Some(obj_proto); vm.array_prototype = Some(arr_proto); vm.string_prototype = Some(str_proto); vm.number_prototype = Some(num_proto); vm.boolean_prototype = Some(bool_proto); // Create and register Object constructor. let obj_ctor = init_object_constructor(&mut vm.gc, obj_proto); vm.set_global("Object", Value::Function(obj_ctor)); // Create and register Array constructor. let arr_ctor = init_array_constructor(&mut vm.gc, arr_proto); vm.set_global("Array", Value::Function(arr_ctor)); // Create and register Error constructors. init_error_constructors(vm, err_proto); // Create and register String constructor. let str_ctor = init_string_constructor(&mut vm.gc, str_proto); vm.set_global("String", Value::Function(str_ctor)); // Create and register Number constructor. let num_ctor = init_number_constructor(&mut vm.gc, num_proto); vm.set_global("Number", Value::Function(num_ctor)); // Create and register Boolean constructor. let bool_ctor = init_boolean_constructor(&mut vm.gc, bool_proto); vm.set_global("Boolean", Value::Function(bool_ctor)); // Create and register Symbol factory. init_symbol_builtins(vm); // Create and register Math object (static methods only, not a constructor). init_math_object(vm); // Create and register Date constructor. init_date_builtins(vm); // Create and register RegExp constructor. init_regexp_builtins(vm); // Create and register Map, Set, WeakMap, WeakSet constructors. init_map_set_builtins(vm); // Create and register Promise (prototype methods + native helpers). init_promise_builtins(vm); // Create and register JSON object (static methods only). init_json_object(vm); // Create and register console object. init_console_object(vm); // Register global utility functions. init_global_functions(vm); // Execute JS preamble for callback-based methods. init_js_preamble(vm); } // ── Object.prototype ───────────────────────────────────────── fn init_object_prototype(gc: &mut Gc, proto: GcRef) { let has_own = make_native(gc, "hasOwnProperty", object_proto_has_own_property); set_builtin_prop(gc, proto, "hasOwnProperty", Value::Function(has_own)); let to_string = make_native(gc, "toString", object_proto_to_string); set_builtin_prop(gc, proto, "toString", Value::Function(to_string)); let value_of = make_native(gc, "valueOf", object_proto_value_of); set_builtin_prop(gc, proto, "valueOf", Value::Function(value_of)); } fn object_proto_has_own_property( args: &[Value], ctx: &mut NativeContext, ) -> Result { let key = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); match ctx.this.gc_ref() { Some(obj_ref) => match ctx.gc.get(obj_ref) { Some(HeapObject::Object(data)) => { Ok(Value::Boolean(data.properties.contains_key(&key))) } Some(HeapObject::Function(fdata)) => { Ok(Value::Boolean(fdata.properties.contains_key(&key))) } _ => Ok(Value::Boolean(false)), }, None => Ok(Value::Boolean(false)), } } fn object_proto_to_string( _args: &[Value], _ctx: &mut NativeContext, ) -> Result { Ok(Value::String("[object Object]".to_string())) } fn object_proto_value_of(_args: &[Value], ctx: &mut NativeContext) -> Result { Ok(ctx.this.clone()) } // ── Object constructor + static methods ────────────────────── fn init_object_constructor(gc: &mut Gc, obj_proto: GcRef) -> GcRef { let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "Object".to_string(), kind: FunctionKind::Native(NativeFunc { callback: object_constructor, }), prototype_obj: Some(obj_proto), properties: HashMap::new(), upvalues: Vec::new(), }))); // Static methods. let keys = make_native(gc, "keys", object_keys); set_func_prop(gc, ctor, "keys", Value::Function(keys)); let values = make_native(gc, "values", object_values); set_func_prop(gc, ctor, "values", Value::Function(values)); let entries = make_native(gc, "entries", object_entries); set_func_prop(gc, ctor, "entries", Value::Function(entries)); let assign = make_native(gc, "assign", object_assign); set_func_prop(gc, ctor, "assign", Value::Function(assign)); let create = make_native(gc, "create", object_create); set_func_prop(gc, ctor, "create", Value::Function(create)); let is = make_native(gc, "is", object_is); set_func_prop(gc, ctor, "is", Value::Function(is)); let get_proto = make_native(gc, "getPrototypeOf", object_get_prototype_of); set_func_prop(gc, ctor, "getPrototypeOf", Value::Function(get_proto)); let get_own_names = make_native(gc, "getOwnPropertyNames", object_get_own_property_names); set_func_prop( gc, ctor, "getOwnPropertyNames", Value::Function(get_own_names), ); let get_own_desc = make_native(gc, "getOwnPropertyDescriptor", object_get_own_prop_desc); set_func_prop( gc, ctor, "getOwnPropertyDescriptor", Value::Function(get_own_desc), ); let define_prop = make_native(gc, "defineProperty", object_define_property); set_func_prop(gc, ctor, "defineProperty", Value::Function(define_prop)); let freeze = make_native(gc, "freeze", object_freeze); set_func_prop(gc, ctor, "freeze", Value::Function(freeze)); let seal = make_native(gc, "seal", object_seal); set_func_prop(gc, ctor, "seal", Value::Function(seal)); let is_frozen = make_native(gc, "isFrozen", object_is_frozen); set_func_prop(gc, ctor, "isFrozen", Value::Function(is_frozen)); let is_sealed = make_native(gc, "isSealed", object_is_sealed); set_func_prop(gc, ctor, "isSealed", Value::Function(is_sealed)); let from_entries = make_native(gc, "fromEntries", object_from_entries); set_func_prop(gc, ctor, "fromEntries", Value::Function(from_entries)); let has_own = make_native(gc, "hasOwn", object_has_own); set_func_prop(gc, ctor, "hasOwn", Value::Function(has_own)); ctor } fn object_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { match args.first() { Some(Value::Object(_)) | Some(Value::Function(_)) => Ok(args[0].clone()), Some(Value::Null) | Some(Value::Undefined) | None => { let obj = ctx.gc.alloc(HeapObject::Object(ObjectData::new())); Ok(Value::Object(obj)) } _ => { // Primitive wrapping (simplified): return a new object. let obj = ctx.gc.alloc(HeapObject::Object(ObjectData::new())); Ok(Value::Object(obj)) } } } fn object_keys(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match args.first() { Some(Value::Object(r)) | Some(Value::Function(r)) => *r, _ => return Err(RuntimeError::type_error("Object.keys requires an object")), }; let keys = own_enumerable_keys(ctx.gc, obj_ref); Ok(make_string_array(ctx.gc, &keys)) } fn object_values(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match args.first() { Some(Value::Object(r)) => *r, _ => return Err(RuntimeError::type_error("Object.values requires an object")), }; let keys = own_enumerable_keys(ctx.gc, obj_ref); let values: Vec = keys .iter() .map(|k| { ctx.gc .get(obj_ref) .and_then(|ho| match ho { HeapObject::Object(data) => data.properties.get(k).map(|p| p.value.clone()), _ => None, }) .unwrap_or(Value::Undefined) }) .collect(); Ok(make_value_array(ctx.gc, &values)) } fn object_entries(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match args.first() { Some(Value::Object(r)) => *r, _ => { return Err(RuntimeError::type_error( "Object.entries requires an object", )) } }; let keys = own_enumerable_keys(ctx.gc, obj_ref); let mut entries = Vec::new(); for k in &keys { let val = ctx .gc .get(obj_ref) .and_then(|ho| match ho { HeapObject::Object(data) => data.properties.get(k).map(|p| p.value.clone()), _ => None, }) .unwrap_or(Value::Undefined); // Create a [key, value] pair array. let pair = make_value_array(ctx.gc, &[Value::String(k.clone()), val]); entries.push(pair); } Ok(make_value_array(ctx.gc, &entries)) } fn object_assign(args: &[Value], ctx: &mut NativeContext) -> Result { let target_ref = match args.first() { Some(Value::Object(r)) => *r, _ => { return Err(RuntimeError::type_error( "Object.assign requires an object target", )) } }; for source in args.iter().skip(1) { let src_ref = match source { Value::Object(r) => *r, Value::Null | Value::Undefined => continue, _ => continue, }; let keys = own_enumerable_keys(ctx.gc, src_ref); for k in &keys { let val = ctx .gc .get(src_ref) .and_then(|ho| match ho { HeapObject::Object(data) => data.properties.get(k).map(|p| p.value.clone()), _ => None, }) .unwrap_or(Value::Undefined); if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(target_ref) { data.properties.insert(k.clone(), Property::data(val)); } } } Ok(Value::Object(target_ref)) } fn object_create(args: &[Value], ctx: &mut NativeContext) -> Result { let proto = match args.first() { Some(Value::Object(r)) => Some(*r), Some(Value::Null) => None, _ => { return Err(RuntimeError::type_error( "Object.create prototype must be an object or null", )) } }; let mut obj = ObjectData::new(); obj.prototype = proto; let gc_ref = ctx.gc.alloc(HeapObject::Object(obj)); Ok(Value::Object(gc_ref)) } fn object_is(args: &[Value], _ctx: &mut NativeContext) -> Result { let x = args.first().cloned().unwrap_or(Value::Undefined); let y = args.get(1).cloned().unwrap_or(Value::Undefined); Ok(Value::Boolean(same_value(&x, &y))) } /// SameValue algorithm (ECMA-262 §7.2.10). fn same_value(x: &Value, y: &Value) -> bool { match (x, y) { (Value::Number(a), Value::Number(b)) => { if a.is_nan() && b.is_nan() { return true; } if *a == 0.0 && *b == 0.0 { return a.is_sign_positive() == b.is_sign_positive(); } a == b } (Value::Undefined, Value::Undefined) => true, (Value::Null, Value::Null) => true, (Value::Boolean(a), Value::Boolean(b)) => a == b, (Value::String(a), Value::String(b)) => a == b, (Value::Object(a), Value::Object(b)) => a == b, (Value::Function(a), Value::Function(b)) => a == b, _ => false, } } fn object_get_prototype_of(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match args.first() { Some(Value::Object(r)) => *r, _ => { return Err(RuntimeError::type_error( "Object.getPrototypeOf requires an object", )) } }; match ctx.gc.get(obj_ref) { Some(HeapObject::Object(data)) => match data.prototype { Some(proto) => Ok(Value::Object(proto)), None => Ok(Value::Null), }, _ => Ok(Value::Null), } } fn object_get_own_property_names( args: &[Value], ctx: &mut NativeContext, ) -> Result { let obj_ref = match args.first() { Some(Value::Object(r)) | Some(Value::Function(r)) => *r, _ => { return Err(RuntimeError::type_error( "Object.getOwnPropertyNames requires an object", )) } }; let names = own_property_names(ctx.gc, obj_ref); Ok(make_string_array(ctx.gc, &names)) } fn object_get_own_prop_desc( args: &[Value], ctx: &mut NativeContext, ) -> Result { let obj_ref = match args.first() { Some(Value::Object(r)) => *r, _ => return Ok(Value::Undefined), }; let key = args .get(1) .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let prop = match ctx.gc.get(obj_ref) { Some(HeapObject::Object(data)) => data.properties.get(&key).cloned(), _ => None, }; match prop { Some(p) => { let mut desc = ObjectData::new(); desc.properties .insert("value".to_string(), Property::data(p.value)); desc.properties.insert( "writable".to_string(), Property::data(Value::Boolean(p.writable)), ); desc.properties.insert( "enumerable".to_string(), Property::data(Value::Boolean(p.enumerable)), ); desc.properties.insert( "configurable".to_string(), Property::data(Value::Boolean(p.configurable)), ); Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(desc)))) } None => Ok(Value::Undefined), } } fn object_define_property(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match args.first() { Some(Value::Object(r)) => *r, _ => { return Err(RuntimeError::type_error( "Object.defineProperty requires an object", )) } }; let key = args .get(1) .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let desc_ref = match args.get(2) { Some(Value::Object(r)) => *r, _ => { return Err(RuntimeError::type_error( "Property descriptor must be an object", )) } }; // Read descriptor properties. let (value, writable, enumerable, configurable) = { match ctx.gc.get(desc_ref) { Some(HeapObject::Object(desc_data)) => { let value = desc_data .properties .get("value") .map(|p| p.value.clone()) .unwrap_or(Value::Undefined); let writable = desc_data .properties .get("writable") .map(|p| p.value.to_boolean()) .unwrap_or(false); let enumerable = desc_data .properties .get("enumerable") .map(|p| p.value.to_boolean()) .unwrap_or(false); let configurable = desc_data .properties .get("configurable") .map(|p| p.value.to_boolean()) .unwrap_or(false); (value, writable, enumerable, configurable) } _ => (Value::Undefined, false, false, false), } }; if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) { data.properties.insert( key, Property { value, writable, enumerable, configurable, }, ); } Ok(Value::Object(obj_ref)) } fn object_freeze(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match args.first() { Some(Value::Object(r)) => *r, Some(other) => return Ok(other.clone()), None => return Ok(Value::Undefined), }; if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) { data.extensible = false; for prop in data.properties.values_mut() { prop.writable = false; prop.configurable = false; } } Ok(Value::Object(obj_ref)) } fn object_seal(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match args.first() { Some(Value::Object(r)) => *r, Some(other) => return Ok(other.clone()), None => return Ok(Value::Undefined), }; if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) { data.extensible = false; for prop in data.properties.values_mut() { prop.configurable = false; } } Ok(Value::Object(obj_ref)) } fn object_is_frozen(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match args.first() { Some(Value::Object(r)) => *r, _ => return Ok(Value::Boolean(true)), }; match ctx.gc.get(obj_ref) { Some(HeapObject::Object(data)) => { if data.extensible { return Ok(Value::Boolean(false)); } let frozen = data .properties .values() .all(|p| !p.writable && !p.configurable); Ok(Value::Boolean(frozen)) } _ => Ok(Value::Boolean(true)), } } fn object_is_sealed(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match args.first() { Some(Value::Object(r)) => *r, _ => return Ok(Value::Boolean(true)), }; match ctx.gc.get(obj_ref) { Some(HeapObject::Object(data)) => { if data.extensible { return Ok(Value::Boolean(false)); } let sealed = data.properties.values().all(|p| !p.configurable); Ok(Value::Boolean(sealed)) } _ => Ok(Value::Boolean(true)), } } fn object_from_entries(args: &[Value], ctx: &mut NativeContext) -> Result { let arr_ref = match args.first() { Some(Value::Object(r)) => *r, _ => { return Err(RuntimeError::type_error( "Object.fromEntries requires an iterable", )) } }; let len = array_length(ctx.gc, arr_ref); let mut obj = ObjectData::new(); for i in 0..len { let pair_val = array_get(ctx.gc, arr_ref, i); if let Value::Object(pair_ref) = pair_val { let key = array_get(ctx.gc, pair_ref, 0); let val = array_get(ctx.gc, pair_ref, 1); let key_str = key.to_js_string(ctx.gc); obj.properties.insert(key_str, Property::data(val)); } } Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(obj)))) } fn object_has_own(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match args.first() { Some(Value::Object(r)) => *r, _ => return Ok(Value::Boolean(false)), }; let key = args .get(1) .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); match ctx.gc.get(obj_ref) { Some(HeapObject::Object(data)) => Ok(Value::Boolean(data.properties.contains_key(&key))), _ => Ok(Value::Boolean(false)), } } // ── Array helpers ──────────────────────────────────────────── /// Create a JS array from a slice of string values. fn make_string_array(gc: &mut Gc, items: &[String]) -> Value { let mut obj = ObjectData::new(); for (i, s) in items.iter().enumerate() { obj.properties .insert(i.to_string(), Property::data(Value::String(s.clone()))); } obj.properties.insert( "length".to_string(), Property { value: Value::Number(items.len() as f64), writable: true, enumerable: false, configurable: false, }, ); Value::Object(gc.alloc(HeapObject::Object(obj))) } /// Create a JS array from a slice of Values. fn make_value_array(gc: &mut Gc, items: &[Value]) -> Value { let mut obj = ObjectData::new(); for (i, v) in items.iter().enumerate() { obj.properties .insert(i.to_string(), Property::data(v.clone())); } obj.properties.insert( "length".to_string(), Property { value: Value::Number(items.len() as f64), writable: true, enumerable: false, configurable: false, }, ); Value::Object(gc.alloc(HeapObject::Object(obj))) } // ── Array.prototype ────────────────────────────────────────── fn init_array_prototype(gc: &mut Gc, proto: GcRef) { let push = make_native(gc, "push", array_push); set_builtin_prop(gc, proto, "push", Value::Function(push)); let pop = make_native(gc, "pop", array_pop); set_builtin_prop(gc, proto, "pop", Value::Function(pop)); let shift = make_native(gc, "shift", array_shift); set_builtin_prop(gc, proto, "shift", Value::Function(shift)); let unshift = make_native(gc, "unshift", array_unshift); set_builtin_prop(gc, proto, "unshift", Value::Function(unshift)); let index_of = make_native(gc, "indexOf", array_index_of); set_builtin_prop(gc, proto, "indexOf", Value::Function(index_of)); let last_index_of = make_native(gc, "lastIndexOf", array_last_index_of); set_builtin_prop(gc, proto, "lastIndexOf", Value::Function(last_index_of)); let includes = make_native(gc, "includes", array_includes); set_builtin_prop(gc, proto, "includes", Value::Function(includes)); let join = make_native(gc, "join", array_join); set_builtin_prop(gc, proto, "join", Value::Function(join)); let slice = make_native(gc, "slice", array_slice); set_builtin_prop(gc, proto, "slice", Value::Function(slice)); let concat = make_native(gc, "concat", array_concat); set_builtin_prop(gc, proto, "concat", Value::Function(concat)); let reverse = make_native(gc, "reverse", array_reverse); set_builtin_prop(gc, proto, "reverse", Value::Function(reverse)); let splice = make_native(gc, "splice", array_splice); set_builtin_prop(gc, proto, "splice", Value::Function(splice)); let fill = make_native(gc, "fill", array_fill); set_builtin_prop(gc, proto, "fill", Value::Function(fill)); let to_string = make_native(gc, "toString", array_to_string); set_builtin_prop(gc, proto, "toString", Value::Function(to_string)); let at = make_native(gc, "at", array_at); set_builtin_prop(gc, proto, "at", Value::Function(at)); // @@iterator: returns an array iterator (values). let iter = make_native(gc, "[Symbol.iterator]", array_iterator); set_builtin_prop(gc, proto, "@@iterator", Value::Function(iter)); // Array.prototype.keys/values/entries let keys_fn = make_native(gc, "keys", array_keys_iter); set_builtin_prop(gc, proto, "keys", Value::Function(keys_fn)); let values_fn = make_native(gc, "values", array_values_iter); set_builtin_prop(gc, proto, "values", Value::Function(values_fn)); let entries_fn = make_native(gc, "entries", array_entries_iter); set_builtin_prop(gc, proto, "entries", Value::Function(entries_fn)); } fn array_push(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Err(RuntimeError::type_error("push called on non-object")), }; let mut len = array_length(ctx.gc, obj_ref); for val in args { array_set(ctx.gc, obj_ref, len, val.clone()); len += 1; } set_array_length(ctx.gc, obj_ref, len); Ok(Value::Number(len as f64)) } fn array_pop(args: &[Value], ctx: &mut NativeContext) -> Result { let _ = args; let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Err(RuntimeError::type_error("pop called on non-object")), }; let len = array_length(ctx.gc, obj_ref); if len == 0 { return Ok(Value::Undefined); } let val = array_get(ctx.gc, obj_ref, len - 1); // Remove the last element. if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) { data.properties.remove(&(len - 1).to_string()); } set_array_length(ctx.gc, obj_ref, len - 1); Ok(val) } fn array_shift(args: &[Value], ctx: &mut NativeContext) -> Result { let _ = args; let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Err(RuntimeError::type_error("shift called on non-object")), }; let len = array_length(ctx.gc, obj_ref); if len == 0 { return Ok(Value::Undefined); } let first = array_get(ctx.gc, obj_ref, 0); // Shift all elements down. let mut vals = Vec::with_capacity(len - 1); for i in 1..len { vals.push(array_get(ctx.gc, obj_ref, i)); } if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) { // Remove all numeric keys. for i in 0..len { data.properties.remove(&i.to_string()); } // Re-insert shifted values. for (i, v) in vals.into_iter().enumerate() { data.properties.insert(i.to_string(), Property::data(v)); } } set_array_length(ctx.gc, obj_ref, len - 1); Ok(first) } fn array_unshift(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Err(RuntimeError::type_error("unshift called on non-object")), }; let len = array_length(ctx.gc, obj_ref); let insert_count = args.len(); // Read existing values. let mut existing = Vec::with_capacity(len); for i in 0..len { existing.push(array_get(ctx.gc, obj_ref, i)); } // Write new values at the start, then existing values after. if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) { for i in 0..len { data.properties.remove(&i.to_string()); } for (i, v) in args.iter().enumerate() { data.properties .insert(i.to_string(), Property::data(v.clone())); } for (i, v) in existing.into_iter().enumerate() { data.properties .insert((i + insert_count).to_string(), Property::data(v)); } } let new_len = len + insert_count; set_array_length(ctx.gc, obj_ref, new_len); Ok(Value::Number(new_len as f64)) } fn array_index_of(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Ok(Value::Number(-1.0)), }; let search = args.first().cloned().unwrap_or(Value::Undefined); let from = args .get(1) .map(|v| { let n = v.to_number() as i64; let len = array_length(ctx.gc, obj_ref) as i64; if n < 0 { (len + n).max(0) as usize } else { n as usize } }) .unwrap_or(0); let len = array_length(ctx.gc, obj_ref); for i in from..len { let elem = array_get(ctx.gc, obj_ref, i); if strict_eq_values(&elem, &search) { return Ok(Value::Number(i as f64)); } } Ok(Value::Number(-1.0)) } fn array_last_index_of(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Ok(Value::Number(-1.0)), }; let search = args.first().cloned().unwrap_or(Value::Undefined); let len = array_length(ctx.gc, obj_ref); if len == 0 { return Ok(Value::Number(-1.0)); } let from = args .get(1) .map(|v| { let n = v.to_number() as i64; if n < 0 { (len as i64 + n) as usize } else { (n as usize).min(len - 1) } }) .unwrap_or(len - 1); for i in (0..=from).rev() { let elem = array_get(ctx.gc, obj_ref, i); if strict_eq_values(&elem, &search) { return Ok(Value::Number(i as f64)); } } Ok(Value::Number(-1.0)) } fn array_includes(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Ok(Value::Boolean(false)), }; let search = args.first().cloned().unwrap_or(Value::Undefined); let len = array_length(ctx.gc, obj_ref); let from = args .get(1) .map(|v| { let n = v.to_number() as i64; if n < 0 { (len as i64 + n).max(0) as usize } else { n as usize } }) .unwrap_or(0); for i in from..len { let elem = array_get(ctx.gc, obj_ref, i); // includes uses SameValueZero (like === but NaN === NaN). if same_value_zero(&elem, &search) { return Ok(Value::Boolean(true)); } } Ok(Value::Boolean(false)) } fn array_join(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Ok(Value::String(String::new())), }; let sep = args .first() .map(|v| { if matches!(v, Value::Undefined) { ",".to_string() } else { v.to_js_string(ctx.gc) } }) .unwrap_or_else(|| ",".to_string()); let len = array_length(ctx.gc, obj_ref); let mut parts = Vec::with_capacity(len); for i in 0..len { let elem = array_get(ctx.gc, obj_ref, i); if matches!(elem, Value::Undefined | Value::Null) { parts.push(String::new()); } else { parts.push(elem.to_js_string(ctx.gc)); } } Ok(Value::String(parts.join(&sep))) } fn array_slice(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Ok(Value::Undefined), }; let len = array_length(ctx.gc, obj_ref) as i64; let start = args .first() .map(|v| { let n = v.to_number() as i64; if n < 0 { (len + n).max(0) } else { n.min(len) } }) .unwrap_or(0) as usize; let end = args .get(1) .map(|v| { if matches!(v, Value::Undefined) { len } else { let n = v.to_number() as i64; if n < 0 { (len + n).max(0) } else { n.min(len) } } }) .unwrap_or(len) as usize; let mut items = Vec::new(); for i in start..end { items.push(array_get(ctx.gc, obj_ref, i)); } Ok(make_value_array(ctx.gc, &items)) } fn array_concat(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Ok(Value::Undefined), }; let mut items = Vec::new(); // First, add elements from `this`. let len = array_length(ctx.gc, obj_ref); for i in 0..len { items.push(array_get(ctx.gc, obj_ref, i)); } // Then add from each argument. for arg in args { match arg { Value::Object(r) => { let arg_len = array_length(ctx.gc, *r); // Only spread array-like objects (those with a length property). if array_length_exists(ctx.gc, *r) { for i in 0..arg_len { items.push(array_get(ctx.gc, *r, i)); } } else { items.push(arg.clone()); } } _ => items.push(arg.clone()), } } Ok(make_value_array(ctx.gc, &items)) } fn array_reverse(args: &[Value], ctx: &mut NativeContext) -> Result { let _ = args; let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Ok(Value::Undefined), }; let len = array_length(ctx.gc, obj_ref); // Read all values. let mut vals: Vec = (0..len).map(|i| array_get(ctx.gc, obj_ref, i)).collect(); vals.reverse(); // Write back. if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) { for (i, v) in vals.into_iter().enumerate() { data.properties.insert(i.to_string(), Property::data(v)); } } Ok(ctx.this.clone()) } fn array_splice(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Ok(Value::Undefined), }; let len = array_length(ctx.gc, obj_ref) as i64; let start = args .first() .map(|v| { let n = v.to_number() as i64; if n < 0 { (len + n).max(0) } else { n.min(len) } }) .unwrap_or(0) as usize; let delete_count = args .get(1) .map(|v| { let n = v.to_number() as i64; n.max(0).min(len - start as i64) as usize }) .unwrap_or((len - start as i64).max(0) as usize); let insert_items: Vec = args.iter().skip(2).cloned().collect(); // Collect current values. let all_vals: Vec = (0..len as usize) .map(|i| array_get(ctx.gc, obj_ref, i)) .collect(); // Build removed slice. let removed: Vec = all_vals[start..start + delete_count].to_vec(); // Build new array content. let mut new_vals = Vec::new(); new_vals.extend_from_slice(&all_vals[..start]); new_vals.extend(insert_items); new_vals.extend_from_slice(&all_vals[start + delete_count..]); // Write back. if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) { // Remove all numeric keys. let old_keys: Vec = data .properties .keys() .filter(|k| k.parse::().is_ok()) .cloned() .collect(); for k in old_keys { data.properties.remove(&k); } // Write new values. for (i, v) in new_vals.iter().enumerate() { data.properties .insert(i.to_string(), Property::data(v.clone())); } } set_array_length(ctx.gc, obj_ref, new_vals.len()); Ok(make_value_array(ctx.gc, &removed)) } fn array_fill(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Ok(Value::Undefined), }; let val = args.first().cloned().unwrap_or(Value::Undefined); let len = array_length(ctx.gc, obj_ref) as i64; let start = args .get(1) .map(|v| { let n = v.to_number() as i64; if n < 0 { (len + n).max(0) } else { n.min(len) } }) .unwrap_or(0) as usize; let end = args .get(2) .map(|v| { if matches!(v, Value::Undefined) { len } else { let n = v.to_number() as i64; if n < 0 { (len + n).max(0) } else { n.min(len) } } }) .unwrap_or(len) as usize; for i in start..end { array_set(ctx.gc, obj_ref, i, val.clone()); } Ok(ctx.this.clone()) } fn array_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result { // Array.prototype.toString is the same as join(","). array_join( &[Value::String(",".to_string())], &mut NativeContext { gc: ctx.gc, this: ctx.this.clone(), console_output: ctx.console_output, dom_bridge: ctx.dom_bridge, }, ) .or_else(|_| Ok(Value::String(String::new()))) } fn array_at(args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Ok(Value::Undefined), }; let len = array_length(ctx.gc, obj_ref) as i64; let index = args.first().map(|v| v.to_number() as i64).unwrap_or(0); let actual = if index < 0 { len + index } else { index }; if actual < 0 || actual >= len { Ok(Value::Undefined) } else { Ok(array_get(ctx.gc, obj_ref, actual as usize)) } } // ── Array constructor + static methods ─────────────────────── fn init_array_constructor(gc: &mut Gc, arr_proto: GcRef) -> GcRef { let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "Array".to_string(), kind: FunctionKind::Native(NativeFunc { callback: array_constructor, }), prototype_obj: Some(arr_proto), properties: HashMap::new(), upvalues: Vec::new(), }))); let is_array = make_native(gc, "isArray", array_is_array); set_func_prop(gc, ctor, "isArray", Value::Function(is_array)); let from = make_native(gc, "from", array_from); set_func_prop(gc, ctor, "from", Value::Function(from)); let of = make_native(gc, "of", array_of); set_func_prop(gc, ctor, "of", Value::Function(of)); ctor } fn array_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { if args.len() == 1 { if let Value::Number(n) = &args[0] { let len = *n as usize; let mut obj = ObjectData::new(); obj.properties.insert( "length".to_string(), Property { value: Value::Number(len as f64), writable: true, enumerable: false, configurable: false, }, ); return Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(obj)))); } } Ok(make_value_array(ctx.gc, args)) } fn array_is_array(args: &[Value], ctx: &mut NativeContext) -> Result { // An "array" is an object that has a numeric length property. // This is a simplified check — real JS uses an internal [[Class]] slot. match args.first() { Some(Value::Object(r)) => match ctx.gc.get(*r) { Some(HeapObject::Object(data)) => { Ok(Value::Boolean(data.properties.contains_key("length"))) } _ => Ok(Value::Boolean(false)), }, _ => Ok(Value::Boolean(false)), } } fn array_from(args: &[Value], ctx: &mut NativeContext) -> Result { let iterable = args.first().cloned().unwrap_or(Value::Undefined); match iterable { Value::Object(r) => { let len = array_length(ctx.gc, r); let mut items = Vec::with_capacity(len); for i in 0..len { items.push(array_get(ctx.gc, r, i)); } Ok(make_value_array(ctx.gc, &items)) } Value::String(s) => { let chars: Vec = s.chars().map(|c| Value::String(c.to_string())).collect(); Ok(make_value_array(ctx.gc, &chars)) } _ => Ok(make_value_array(ctx.gc, &[])), } } fn array_of(args: &[Value], ctx: &mut NativeContext) -> Result { Ok(make_value_array(ctx.gc, args)) } // ── Error constructors ─────────────────────────────────────── fn init_error_prototype(gc: &mut Gc, proto: GcRef) { let to_string = make_native(gc, "toString", error_proto_to_string); set_builtin_prop(gc, proto, "toString", Value::Function(to_string)); } fn init_error_constructors(vm: &mut Vm, err_proto: GcRef) { // Base Error. let error_ctor = make_error_constructor(&mut vm.gc, "Error", err_proto); vm.set_global("Error", Value::Function(error_ctor)); // TypeError. let te_proto = make_error_subclass_proto(&mut vm.gc, "TypeError", err_proto); let te_ctor = make_error_constructor(&mut vm.gc, "TypeError", te_proto); vm.set_global("TypeError", Value::Function(te_ctor)); // ReferenceError. let re_proto = make_error_subclass_proto(&mut vm.gc, "ReferenceError", err_proto); let re_ctor = make_error_constructor(&mut vm.gc, "ReferenceError", re_proto); vm.set_global("ReferenceError", Value::Function(re_ctor)); // SyntaxError. let se_proto = make_error_subclass_proto(&mut vm.gc, "SyntaxError", err_proto); let se_ctor = make_error_constructor(&mut vm.gc, "SyntaxError", se_proto); vm.set_global("SyntaxError", Value::Function(se_ctor)); // RangeError. let rae_proto = make_error_subclass_proto(&mut vm.gc, "RangeError", err_proto); let rae_ctor = make_error_constructor(&mut vm.gc, "RangeError", rae_proto); vm.set_global("RangeError", Value::Function(rae_ctor)); // URIError. let ue_proto = make_error_subclass_proto(&mut vm.gc, "URIError", err_proto); let ue_ctor = make_error_constructor(&mut vm.gc, "URIError", ue_proto); vm.set_global("URIError", Value::Function(ue_ctor)); // EvalError. let ee_proto = make_error_subclass_proto(&mut vm.gc, "EvalError", err_proto); let ee_ctor = make_error_constructor(&mut vm.gc, "EvalError", ee_proto); vm.set_global("EvalError", Value::Function(ee_ctor)); } fn make_error_subclass_proto(gc: &mut Gc, name: &str, parent_proto: GcRef) -> GcRef { let mut data = ObjectData::new(); data.prototype = Some(parent_proto); data.properties.insert( "name".to_string(), Property::builtin(Value::String(name.to_string())), ); data.properties.insert( "message".to_string(), Property::builtin(Value::String(String::new())), ); gc.alloc(HeapObject::Object(data)) } fn make_error_constructor(gc: &mut Gc, name: &str, proto: GcRef) -> GcRef { gc.alloc(HeapObject::Function(Box::new(FunctionData { name: name.to_string(), kind: FunctionKind::Native(NativeFunc { callback: error_constructor, }), prototype_obj: Some(proto), properties: HashMap::new(), upvalues: Vec::new(), }))) } fn error_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { let message = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let mut obj = ObjectData::new(); obj.properties.insert( "message".to_string(), Property::data(Value::String(message)), ); // The "name" property comes from the prototype chain. Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(obj)))) } fn error_proto_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Ok(Value::String("Error".to_string())), }; let name = match ctx.gc.get(obj_ref) { Some(HeapObject::Object(data)) => data .properties .get("name") .map(|p| p.value.to_js_string(ctx.gc)) .unwrap_or_else(|| "Error".to_string()), _ => "Error".to_string(), }; let message = match ctx.gc.get(obj_ref) { Some(HeapObject::Object(data)) => data .properties .get("message") .map(|p| p.value.to_js_string(ctx.gc)) .unwrap_or_default(), _ => String::new(), }; if message.is_empty() { Ok(Value::String(name)) } else { Ok(Value::String(format!("{name}: {message}"))) } } // ── Array iterators ────────────────────────────────────────── /// Helper: create an iterator object that yields values from a closure. /// `state` is a GcRef to an object with `__items__` (array) and `__idx__` (number). fn make_simple_iterator( gc: &mut Gc, items: GcRef, next_fn: fn(&[Value], &mut NativeContext) -> Result, ) -> Value { let mut obj = ObjectData::new(); obj.properties.insert( "__items__".to_string(), Property::builtin(Value::Object(items)), ); obj.properties .insert("__idx__".to_string(), Property::builtin(Value::Number(0.0))); let next = gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "next".to_string(), kind: FunctionKind::Native(NativeFunc { callback: next_fn }), prototype_obj: None, properties: HashMap::new(), upvalues: Vec::new(), }))); obj.properties .insert("next".to_string(), Property::builtin(Value::Function(next))); // @@iterator returns self. let self_iter = gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "[Symbol.iterator]".to_string(), kind: FunctionKind::Native(NativeFunc { callback: iter_self, }), prototype_obj: None, properties: HashMap::new(), upvalues: Vec::new(), }))); obj.properties.insert( "@@iterator".to_string(), Property::builtin(Value::Function(self_iter)), ); let r = gc.alloc(HeapObject::Object(obj)); Value::Object(r) } fn iter_self(_args: &[Value], ctx: &mut NativeContext) -> Result { Ok(ctx.this.clone()) } fn make_iterator_result_native(gc: &mut Gc, value: Value, done: bool) -> Value { let mut obj = ObjectData::new(); obj.properties .insert("value".to_string(), Property::data(value)); obj.properties .insert("done".to_string(), Property::data(Value::Boolean(done))); let r = gc.alloc(HeapObject::Object(obj)); Value::Object(r) } /// Array.prototype[@@iterator]() — same as values(). fn array_iterator(_args: &[Value], ctx: &mut NativeContext) -> Result { array_values_iter(_args, ctx) } /// Array.prototype.values() — returns iterator over values. fn array_values_iter(_args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = ctx .this .gc_ref() .ok_or_else(|| RuntimeError::type_error("values called on non-object"))?; Ok(make_simple_iterator(ctx.gc, obj_ref, array_values_next)) } fn array_values_next(_args: &[Value], ctx: &mut NativeContext) -> Result { let iter_ref = ctx .this .gc_ref() .ok_or_else(|| RuntimeError::type_error("next called on non-iterator"))?; let (items_ref, idx) = get_iter_state(ctx.gc, iter_ref); let items_ref = match items_ref { Some(r) => r, None => return Ok(make_iterator_result_native(ctx.gc, Value::Undefined, true)), }; let len = array_length(ctx.gc, items_ref); if idx >= len { return Ok(make_iterator_result_native(ctx.gc, Value::Undefined, true)); } let val = array_get(ctx.gc, items_ref, idx); set_iter_idx(ctx.gc, iter_ref, idx + 1); Ok(make_iterator_result_native(ctx.gc, val, false)) } /// Array.prototype.keys() — returns iterator over indices. fn array_keys_iter(_args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = ctx .this .gc_ref() .ok_or_else(|| RuntimeError::type_error("keys called on non-object"))?; Ok(make_simple_iterator(ctx.gc, obj_ref, array_keys_next)) } fn array_keys_next(_args: &[Value], ctx: &mut NativeContext) -> Result { let iter_ref = ctx .this .gc_ref() .ok_or_else(|| RuntimeError::type_error("next called on non-iterator"))?; let (items_ref, idx) = get_iter_state(ctx.gc, iter_ref); let items_ref = match items_ref { Some(r) => r, None => return Ok(make_iterator_result_native(ctx.gc, Value::Undefined, true)), }; let len = array_length(ctx.gc, items_ref); if idx >= len { return Ok(make_iterator_result_native(ctx.gc, Value::Undefined, true)); } set_iter_idx(ctx.gc, iter_ref, idx + 1); Ok(make_iterator_result_native( ctx.gc, Value::Number(idx as f64), false, )) } /// Array.prototype.entries() — returns iterator over [index, value] pairs. fn array_entries_iter(_args: &[Value], ctx: &mut NativeContext) -> Result { let obj_ref = ctx .this .gc_ref() .ok_or_else(|| RuntimeError::type_error("entries called on non-object"))?; Ok(make_simple_iterator(ctx.gc, obj_ref, array_entries_next)) } fn array_entries_next(_args: &[Value], ctx: &mut NativeContext) -> Result { let iter_ref = ctx .this .gc_ref() .ok_or_else(|| RuntimeError::type_error("next called on non-iterator"))?; let (items_ref, idx) = get_iter_state(ctx.gc, iter_ref); let items_ref = match items_ref { Some(r) => r, None => return Ok(make_iterator_result_native(ctx.gc, Value::Undefined, true)), }; let len = array_length(ctx.gc, items_ref); if idx >= len { return Ok(make_iterator_result_native(ctx.gc, Value::Undefined, true)); } let val = array_get(ctx.gc, items_ref, idx); set_iter_idx(ctx.gc, iter_ref, idx + 1); // Create [index, value] pair array. let mut pair = ObjectData::new(); pair.properties .insert("0".to_string(), Property::data(Value::Number(idx as f64))); pair.properties.insert("1".to_string(), Property::data(val)); pair.properties.insert( "length".to_string(), Property { value: Value::Number(2.0), writable: true, enumerable: false, configurable: false, }, ); let pair_ref = ctx.gc.alloc(HeapObject::Object(pair)); Ok(make_iterator_result_native( ctx.gc, Value::Object(pair_ref), false, )) } /// Helper to read __items__ and __idx__ from an iterator state object. fn get_iter_state(gc: &Gc, iter_ref: GcRef) -> (Option, usize) { match gc.get(iter_ref) { Some(HeapObject::Object(data)) => { let items = data .properties .get("__items__") .and_then(|p| p.value.gc_ref()); let idx = data .properties .get("__idx__") .map(|p| p.value.to_number() as usize) .unwrap_or(0); (items, idx) } _ => (None, 0), } } /// Helper to update __idx__ on an iterator state object. fn set_iter_idx(gc: &mut Gc, iter_ref: GcRef, idx: usize) { if let Some(HeapObject::Object(data)) = gc.get_mut(iter_ref) { data.properties.insert( "__idx__".to_string(), Property::builtin(Value::Number(idx as f64)), ); } } // ── String built-in ────────────────────────────────────────── fn init_string_prototype(gc: &mut Gc, proto: GcRef) { let methods: &[NativeMethod] = &[ ("charAt", string_proto_char_at), ("charCodeAt", string_proto_char_code_at), ("codePointAt", string_proto_code_point_at), ("concat", string_proto_concat), ("slice", string_proto_slice), ("substring", string_proto_substring), ("substr", string_proto_substr), ("indexOf", string_proto_index_of), ("lastIndexOf", string_proto_last_index_of), ("includes", string_proto_includes), ("startsWith", string_proto_starts_with), ("endsWith", string_proto_ends_with), ("trim", string_proto_trim), ("trimStart", string_proto_trim_start), ("trimEnd", string_proto_trim_end), ("padStart", string_proto_pad_start), ("padEnd", string_proto_pad_end), ("repeat", string_proto_repeat), ("split", string_proto_split), ("replace", string_proto_replace), ("replaceAll", string_proto_replace_all), ("match", string_proto_match), ("matchAll", string_proto_match_all), ("search", string_proto_search), ("toLowerCase", string_proto_to_lower_case), ("toUpperCase", string_proto_to_upper_case), ("at", string_proto_at), ("toString", string_proto_to_string), ("valueOf", string_proto_value_of), ]; for &(name, callback) in methods { let f = make_native(gc, name, callback); set_builtin_prop(gc, proto, name, Value::Function(f)); } // @@iterator: iterates over characters. let iter_fn = make_native(gc, "[Symbol.iterator]", string_iterator); set_builtin_prop(gc, proto, "@@iterator", Value::Function(iter_fn)); } /// String.prototype[@@iterator]() — returns an iterator over characters. fn string_iterator(_args: &[Value], ctx: &mut NativeContext) -> Result { let s = ctx.this.to_js_string(ctx.gc); // Store the string's characters in an array-like object. let mut items = ObjectData::new(); for (i, ch) in s.chars().enumerate() { items .properties .insert(i.to_string(), Property::data(Value::String(ch.to_string()))); } items.properties.insert( "length".to_string(), Property { value: Value::Number(s.chars().count() as f64), writable: true, enumerable: false, configurable: false, }, ); let items_ref = ctx.gc.alloc(HeapObject::Object(items)); Ok(make_simple_iterator(ctx.gc, items_ref, array_values_next)) } fn init_string_constructor(gc: &mut Gc, str_proto: GcRef) -> GcRef { let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "String".to_string(), kind: FunctionKind::Native(NativeFunc { callback: string_constructor, }), prototype_obj: Some(str_proto), properties: HashMap::new(), upvalues: Vec::new(), }))); let from_char_code = make_native(gc, "fromCharCode", string_from_char_code); set_func_prop(gc, ctor, "fromCharCode", Value::Function(from_char_code)); let from_code_point = make_native(gc, "fromCodePoint", string_from_code_point); set_func_prop(gc, ctor, "fromCodePoint", Value::Function(from_code_point)); ctor } fn string_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { let s = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); Ok(Value::String(s)) } fn string_from_char_code(args: &[Value], _ctx: &mut NativeContext) -> Result { let s: String = args .iter() .filter_map(|v| { let code = v.to_number() as u32; char::from_u32(code) }) .collect(); Ok(Value::String(s)) } fn string_from_code_point(args: &[Value], _ctx: &mut NativeContext) -> Result { let mut s = String::new(); for v in args { let code = v.to_number() as u32; match char::from_u32(code) { Some(c) => s.push(c), None => { return Err(RuntimeError::range_error(format!( "Invalid code point {code}" ))) } } } Ok(Value::String(s)) } /// Helper: extract the string from `this` for String.prototype methods. fn this_string(ctx: &NativeContext) -> String { ctx.this.to_js_string(ctx.gc) } /// Helper: get chars as a Vec for index-based operations. fn str_chars(s: &str) -> Vec { s.chars().collect() } fn string_proto_char_at(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); let chars = str_chars(&s); let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0); if idx < 0 || idx as usize >= chars.len() { Ok(Value::String(String::new())) } else { Ok(Value::String(chars[idx as usize].to_string())) } } fn string_proto_char_code_at( args: &[Value], ctx: &mut NativeContext, ) -> Result { let s = this_string(ctx); let chars = str_chars(&s); let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0); if idx < 0 || idx as usize >= chars.len() { Ok(Value::Number(f64::NAN)) } else { Ok(Value::Number(chars[idx as usize] as u32 as f64)) } } fn string_proto_code_point_at( args: &[Value], ctx: &mut NativeContext, ) -> Result { let s = this_string(ctx); let chars = str_chars(&s); let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0); if idx < 0 || idx as usize >= chars.len() { Ok(Value::Undefined) } else { Ok(Value::Number(chars[idx as usize] as u32 as f64)) } } fn string_proto_concat(args: &[Value], ctx: &mut NativeContext) -> Result { let mut s = this_string(ctx); for arg in args { s.push_str(&arg.to_js_string(ctx.gc)); } Ok(Value::String(s)) } fn string_proto_slice(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); let chars = str_chars(&s); let len = chars.len() as i64; let start = args.first().map(|v| v.to_number() as i64).unwrap_or(0); let end = args.get(1).map(|v| v.to_number() as i64).unwrap_or(len); let start = if start < 0 { (len + start).max(0) as usize } else { start.min(len) as usize }; let end = if end < 0 { (len + end).max(0) as usize } else { end.min(len) as usize }; if start >= end { Ok(Value::String(String::new())) } else { Ok(Value::String(chars[start..end].iter().collect())) } } fn string_proto_substring(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); let chars = str_chars(&s); let len = chars.len() as i64; let a = args .first() .map(|v| v.to_number() as i64) .unwrap_or(0) .clamp(0, len) as usize; let b = args .get(1) .map(|v| v.to_number() as i64) .unwrap_or(len) .clamp(0, len) as usize; let (start, end) = if a <= b { (a, b) } else { (b, a) }; Ok(Value::String(chars[start..end].iter().collect())) } fn string_proto_substr(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); let chars = str_chars(&s); let len = chars.len() as i64; let start = args.first().map(|v| v.to_number() as i64).unwrap_or(0); let start = if start < 0 { (len + start).max(0) as usize } else { start.min(len) as usize }; let count = args.get(1).map(|v| v.to_number() as i64).unwrap_or(len) as usize; let end = (start + count).min(chars.len()); Ok(Value::String(chars[start..end].iter().collect())) } fn string_proto_index_of(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); let search = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let from = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0); let chars = str_chars(&s); let search_chars = str_chars(&search); if search_chars.is_empty() { return Ok(Value::Number(from.min(chars.len()) as f64)); } for i in from..chars.len() { if i + search_chars.len() <= chars.len() && chars[i..i + search_chars.len()] == search_chars[..] { return Ok(Value::Number(i as f64)); } } Ok(Value::Number(-1.0)) } fn string_proto_last_index_of( args: &[Value], ctx: &mut NativeContext, ) -> Result { let s = this_string(ctx); let search = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let chars = str_chars(&s); let search_chars = str_chars(&search); let from = args .get(1) .map(|v| { let n = v.to_number(); if n.is_nan() { chars.len() } else { n as usize } }) .unwrap_or(chars.len()); if search_chars.is_empty() { return Ok(Value::Number(from.min(chars.len()) as f64)); } let max_start = from.min(chars.len().saturating_sub(search_chars.len())); for i in (0..=max_start).rev() { if i + search_chars.len() <= chars.len() && chars[i..i + search_chars.len()] == search_chars[..] { return Ok(Value::Number(i as f64)); } } Ok(Value::Number(-1.0)) } fn string_proto_includes(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); let search = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let from = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0); let chars = str_chars(&s); let search_chars = str_chars(&search); if search_chars.is_empty() { return Ok(Value::Boolean(true)); } for i in from..chars.len() { if i + search_chars.len() <= chars.len() && chars[i..i + search_chars.len()] == search_chars[..] { return Ok(Value::Boolean(true)); } } Ok(Value::Boolean(false)) } fn string_proto_starts_with( args: &[Value], ctx: &mut NativeContext, ) -> Result { let s = this_string(ctx); let search = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let pos = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0); let chars = str_chars(&s); let search_chars = str_chars(&search); if pos + search_chars.len() > chars.len() { return Ok(Value::Boolean(false)); } Ok(Value::Boolean( chars[pos..pos + search_chars.len()] == search_chars[..], )) } fn string_proto_ends_with(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); let search = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let chars = str_chars(&s); let search_chars = str_chars(&search); let end_pos = args .get(1) .map(|v| (v.to_number() as usize).min(chars.len())) .unwrap_or(chars.len()); if search_chars.len() > end_pos { return Ok(Value::Boolean(false)); } let start = end_pos - search_chars.len(); Ok(Value::Boolean(chars[start..end_pos] == search_chars[..])) } fn string_proto_trim(args: &[Value], ctx: &mut NativeContext) -> Result { let _ = args; Ok(Value::String(this_string(ctx).trim().to_string())) } fn string_proto_trim_start(args: &[Value], ctx: &mut NativeContext) -> Result { let _ = args; Ok(Value::String(this_string(ctx).trim_start().to_string())) } fn string_proto_trim_end(args: &[Value], ctx: &mut NativeContext) -> Result { let _ = args; Ok(Value::String(this_string(ctx).trim_end().to_string())) } fn string_proto_pad_start(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); let target_len = args.first().map(|v| v.to_number() as usize).unwrap_or(0); let fill = args .get(1) .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_else(|| " ".to_string()); let chars = str_chars(&s); if chars.len() >= target_len || fill.is_empty() { return Ok(Value::String(s)); } let fill_chars = str_chars(&fill); let needed = target_len - chars.len(); let mut pad = String::new(); for i in 0..needed { pad.push(fill_chars[i % fill_chars.len()]); } pad.push_str(&s); Ok(Value::String(pad)) } fn string_proto_pad_end(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); let target_len = args.first().map(|v| v.to_number() as usize).unwrap_or(0); let fill = args .get(1) .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_else(|| " ".to_string()); let chars = str_chars(&s); if chars.len() >= target_len || fill.is_empty() { return Ok(Value::String(s)); } let fill_chars = str_chars(&fill); let needed = target_len - chars.len(); let mut result = s; for i in 0..needed { result.push(fill_chars[i % fill_chars.len()]); } Ok(Value::String(result)) } fn string_proto_repeat(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); let count = args.first().map(|v| v.to_number()).unwrap_or(0.0); if count < 0.0 || count.is_infinite() { return Err(RuntimeError::range_error("Invalid count value")); } Ok(Value::String(s.repeat(count as usize))) } fn string_proto_split(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); if args.is_empty() || matches!(args.first(), Some(Value::Undefined)) { return Ok(make_value_array(ctx.gc, &[Value::String(s)])); } let limit = args .get(1) .map(|v| v.to_number() as usize) .unwrap_or(usize::MAX); // Check if separator is a RegExp. if let Some(arg0) = args.first() { if is_regexp(ctx.gc, arg0) { return string_split_regexp(ctx.gc, &s, arg0, limit); } } let sep = args[0].to_js_string(ctx.gc); if sep.is_empty() { let items: Vec = str_chars(&s) .into_iter() .take(limit) .map(|c| Value::String(c.to_string())) .collect(); return Ok(make_value_array(ctx.gc, &items)); } let mut items = Vec::new(); let mut start = 0; let sep_len = sep.len(); while let Some(pos) = s[start..].find(&sep) { if items.len() >= limit { break; } items.push(Value::String(s[start..start + pos].to_string())); start += pos + sep_len; } if items.len() < limit { items.push(Value::String(s[start..].to_string())); } Ok(make_value_array(ctx.gc, &items)) } fn string_split_regexp( gc: &mut Gc, s: &str, regexp: &Value, limit: usize, ) -> Result { use crate::regex::{exec, CompiledRegex}; let pattern = regexp_get_pattern(gc, regexp).unwrap_or_default(); let flags_str = regexp_get_flags(gc, regexp).unwrap_or_default(); let compiled = CompiledRegex::new(&pattern, &flags_str).map_err(RuntimeError::syntax_error)?; let chars: Vec = s.chars().collect(); let mut items = Vec::new(); let mut last_end = 0usize; loop { if items.len() >= limit { break; } match exec(&compiled, s, last_end) { Some(m) => { // Avoid infinite loop on zero-length matches. if m.start == m.end && m.start == last_end { if last_end >= chars.len() { break; } items.push(Value::String(chars[last_end].to_string())); last_end += 1; continue; } let piece: String = chars[last_end..m.start].iter().collect(); items.push(Value::String(piece)); // Add capturing groups. for i in 1..m.captures.len() { if items.len() >= limit { break; } match m.captures[i] { Some((cs, ce)) => { let cap: String = chars[cs..ce].iter().collect(); items.push(Value::String(cap)); } None => items.push(Value::Undefined), } } last_end = m.end; } None => break, } } if items.len() < limit { let rest: String = chars[last_end..].iter().collect(); items.push(Value::String(rest)); } Ok(make_value_array(gc, &items)) } fn string_proto_replace(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); // Check if search argument is a RegExp. if let Some(arg0) = args.first() { if is_regexp(ctx.gc, arg0) { let replacement = args .get(1) .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); return string_replace_regexp(ctx.gc, &s, arg0, &replacement); } } let search = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let replacement = args .get(1) .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); // Replace only the first occurrence. if let Some(pos) = s.find(&search) { let mut result = String::with_capacity(s.len()); result.push_str(&s[..pos]); result.push_str(&replacement); result.push_str(&s[pos + search.len()..]); Ok(Value::String(result)) } else { Ok(Value::String(s)) } } fn string_replace_regexp( gc: &mut Gc, s: &str, regexp: &Value, replacement: &str, ) -> Result { use crate::regex::{exec, CompiledRegex}; let pattern = regexp_get_pattern(gc, regexp).unwrap_or_default(); let flags_str = regexp_get_flags(gc, regexp).unwrap_or_default(); let compiled = CompiledRegex::new(&pattern, &flags_str).map_err(RuntimeError::syntax_error)?; let is_global = compiled.flags.global; let chars: Vec = s.chars().collect(); let mut result = String::new(); let mut last_end = 0usize; while let Some(m) = exec(&compiled, s, last_end) { // Append text before match. let before: String = chars[last_end..m.start].iter().collect(); result.push_str(&before); // Process replacement with $-substitutions. let matched: String = chars[m.start..m.end].iter().collect(); result.push_str(&apply_replacement( replacement, &matched, &m.captures, &chars, )); last_end = m.end; if !is_global { break; } // Avoid infinite loop on zero-length match. if m.start == m.end { if last_end < chars.len() { result.push(chars[last_end]); last_end += 1; } else { break; } } } let rest: String = chars[last_end..].iter().collect(); result.push_str(&rest); Ok(Value::String(result)) } /// Apply replacement string with $-substitutions ($&, $1, etc.). fn apply_replacement( replacement: &str, matched: &str, captures: &[Option<(usize, usize)>], chars: &[char], ) -> String { let rep_chars: Vec = replacement.chars().collect(); let mut result = String::new(); let mut i = 0; while i < rep_chars.len() { if rep_chars[i] == '$' && i + 1 < rep_chars.len() { match rep_chars[i + 1] { '$' => { result.push('$'); i += 2; } '&' => { result.push_str(matched); i += 2; } '`' => { // $` — text before match. if let Some(Some((start, _))) = captures.first() { let before: String = chars[..*start].iter().collect(); result.push_str(&before); } i += 2; } '\'' => { // $' — text after match. if let Some(Some((_, end))) = captures.first() { let after: String = chars[*end..].iter().collect(); result.push_str(&after); } i += 2; } d if d.is_ascii_digit() => { // $1, $12 etc. let mut num_str = String::new(); let mut j = i + 1; while j < rep_chars.len() && rep_chars[j].is_ascii_digit() { num_str.push(rep_chars[j]); j += 1; } if let Ok(idx) = num_str.parse::() { if idx > 0 && idx < captures.len() { if let Some((s, e)) = captures[idx] { let cap: String = chars[s..e].iter().collect(); result.push_str(&cap); } } } i = j; } _ => { result.push('$'); i += 1; } } } else { result.push(rep_chars[i]); i += 1; } } result } fn string_proto_replace_all( args: &[Value], ctx: &mut NativeContext, ) -> Result { let s = this_string(ctx); // If search is a RegExp, it must have the global flag. if let Some(arg0) = args.first() { if is_regexp(ctx.gc, arg0) { let flags = regexp_get_flags(ctx.gc, arg0).unwrap_or_default(); if !flags.contains('g') { return Err(RuntimeError::type_error( "String.prototype.replaceAll called with a non-global RegExp argument", )); } let replacement = args .get(1) .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); return string_replace_regexp(ctx.gc, &s, arg0, &replacement); } } let search = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let replacement = args .get(1) .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); Ok(Value::String(s.replace(&search, &replacement))) } fn string_proto_match(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); if args.is_empty() { return Ok(Value::Null); } let arg0 = &args[0]; // If arg is not a RegExp, create one. let regexp_val = if is_regexp(ctx.gc, arg0) { arg0.clone() } else { let pattern = arg0.to_js_string(ctx.gc); let proto = REGEXP_PROTO.with(|cell| cell.get()); make_regexp_obj(ctx.gc, &pattern, "", proto).map_err(RuntimeError::syntax_error)? }; let is_global = regexp_get_flags(ctx.gc, ®exp_val) .map(|f| f.contains('g')) .unwrap_or(false); if !is_global { // Non-global: return exec result. return regexp_exec_internal(ctx.gc, ®exp_val, &s); } // Global: collect all matches. regexp_set_last_index(ctx.gc, ®exp_val, 0.0); let mut matches = Vec::new(); loop { let result = regexp_exec_internal(ctx.gc, ®exp_val, &s)?; if matches!(result, Value::Null) { break; } // Get the matched string (index 0 of the result array). if let Value::Object(r) = &result { if let Some(HeapObject::Object(data)) = ctx.gc.get(*r) { if let Some(prop) = data.properties.get("0") { matches.push(prop.value.clone()); // Advance past zero-length matches. let match_str = prop.value.to_js_string(ctx.gc); if match_str.is_empty() { let li = regexp_get_last_index(ctx.gc, ®exp_val); regexp_set_last_index(ctx.gc, ®exp_val, li + 1.0); } } } } } if matches.is_empty() { Ok(Value::Null) } else { Ok(make_value_array(ctx.gc, &matches)) } } fn string_proto_match_all(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); if args.is_empty() { return Ok(make_value_array(ctx.gc, &[])); } let arg0 = &args[0]; // If arg is a RegExp, it must have global flag. let regexp_val = if is_regexp(ctx.gc, arg0) { let flags = regexp_get_flags(ctx.gc, arg0).unwrap_or_default(); if !flags.contains('g') { return Err(RuntimeError::type_error( "String.prototype.matchAll called with a non-global RegExp argument", )); } arg0.clone() } else { let pattern = arg0.to_js_string(ctx.gc); let proto = REGEXP_PROTO.with(|cell| cell.get()); make_regexp_obj(ctx.gc, &pattern, "g", proto).map_err(RuntimeError::syntax_error)? }; // Collect all match results. regexp_set_last_index(ctx.gc, ®exp_val, 0.0); let mut results = Vec::new(); loop { let result = regexp_exec_internal(ctx.gc, ®exp_val, &s)?; if matches!(result, Value::Null) { break; } // Advance past zero-length matches. if let Value::Object(r) = &result { if let Some(HeapObject::Object(data)) = ctx.gc.get(*r) { if let Some(prop) = data.properties.get("0") { let match_str = prop.value.to_js_string(ctx.gc); if match_str.is_empty() { let li = regexp_get_last_index(ctx.gc, ®exp_val); regexp_set_last_index(ctx.gc, ®exp_val, li + 1.0); } } } } results.push(result); } Ok(make_value_array(ctx.gc, &results)) } fn string_proto_search(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); if args.is_empty() { return Ok(Value::Number(0.0)); // /(?:)/ matches at 0. } let arg0 = &args[0]; let regexp_val = if is_regexp(ctx.gc, arg0) { arg0.clone() } else { let pattern = arg0.to_js_string(ctx.gc); let proto = REGEXP_PROTO.with(|cell| cell.get()); make_regexp_obj(ctx.gc, &pattern, "", proto).map_err(RuntimeError::syntax_error)? }; // search always starts from 0 and ignores global/lastIndex. // Save and restore lastIndex so exec_internal's global/sticky handling doesn't interfere. let saved_last_index = regexp_get_last_index(ctx.gc, ®exp_val); regexp_set_last_index(ctx.gc, ®exp_val, 0.0); let result = regexp_exec_internal(ctx.gc, ®exp_val, &s)?; regexp_set_last_index(ctx.gc, ®exp_val, saved_last_index); match result { Value::Null => Ok(Value::Number(-1.0)), Value::Object(r) => { let idx = match ctx.gc.get(r) { Some(HeapObject::Object(data)) => data .properties .get("index") .map(|p| p.value.to_number()) .unwrap_or(-1.0), _ => -1.0, }; Ok(Value::Number(idx)) } _ => Ok(Value::Number(-1.0)), } } fn string_proto_to_lower_case( args: &[Value], ctx: &mut NativeContext, ) -> Result { let _ = args; Ok(Value::String(this_string(ctx).to_lowercase())) } fn string_proto_to_upper_case( args: &[Value], ctx: &mut NativeContext, ) -> Result { let _ = args; Ok(Value::String(this_string(ctx).to_uppercase())) } fn string_proto_at(args: &[Value], ctx: &mut NativeContext) -> Result { let s = this_string(ctx); let chars = str_chars(&s); let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0); let actual = if idx < 0 { chars.len() as i64 + idx } else { idx }; if actual < 0 || actual as usize >= chars.len() { Ok(Value::Undefined) } else { Ok(Value::String(chars[actual as usize].to_string())) } } fn string_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result { let _ = args; Ok(Value::String(this_string(ctx))) } fn string_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result { let _ = args; Ok(Value::String(this_string(ctx))) } // ── Number built-in ────────────────────────────────────────── fn init_number_prototype(gc: &mut Gc, proto: GcRef) { let methods: &[NativeMethod] = &[ ("toString", number_proto_to_string), ("valueOf", number_proto_value_of), ("toFixed", number_proto_to_fixed), ("toPrecision", number_proto_to_precision), ("toExponential", number_proto_to_exponential), ]; for &(name, callback) in methods { let f = make_native(gc, name, callback); set_builtin_prop(gc, proto, name, Value::Function(f)); } } fn init_number_constructor(gc: &mut Gc, num_proto: GcRef) -> GcRef { let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "Number".to_string(), kind: FunctionKind::Native(NativeFunc { callback: number_constructor, }), prototype_obj: Some(num_proto), properties: HashMap::new(), upvalues: Vec::new(), }))); // Static methods. let is_nan = make_native(gc, "isNaN", number_is_nan); set_func_prop(gc, ctor, "isNaN", Value::Function(is_nan)); let is_finite = make_native(gc, "isFinite", number_is_finite); set_func_prop(gc, ctor, "isFinite", Value::Function(is_finite)); let is_integer = make_native(gc, "isInteger", number_is_integer); set_func_prop(gc, ctor, "isInteger", Value::Function(is_integer)); let is_safe_integer = make_native(gc, "isSafeInteger", number_is_safe_integer); set_func_prop(gc, ctor, "isSafeInteger", Value::Function(is_safe_integer)); let parse_int_fn = make_native(gc, "parseInt", crate::builtins::parse_int); set_func_prop(gc, ctor, "parseInt", Value::Function(parse_int_fn)); let parse_float_fn = make_native(gc, "parseFloat", crate::builtins::parse_float); set_func_prop(gc, ctor, "parseFloat", Value::Function(parse_float_fn)); // Constants. set_func_prop(gc, ctor, "EPSILON", Value::Number(f64::EPSILON)); set_func_prop( gc, ctor, "MAX_SAFE_INTEGER", Value::Number(9007199254740991.0), ); set_func_prop( gc, ctor, "MIN_SAFE_INTEGER", Value::Number(-9007199254740991.0), ); set_func_prop(gc, ctor, "MAX_VALUE", Value::Number(f64::MAX)); set_func_prop(gc, ctor, "MIN_VALUE", Value::Number(f64::MIN_POSITIVE)); set_func_prop(gc, ctor, "NaN", Value::Number(f64::NAN)); set_func_prop(gc, ctor, "POSITIVE_INFINITY", Value::Number(f64::INFINITY)); set_func_prop( gc, ctor, "NEGATIVE_INFINITY", Value::Number(f64::NEG_INFINITY), ); ctor } fn number_constructor(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(0.0); Ok(Value::Number(n)) } fn number_is_nan(args: &[Value], _ctx: &mut NativeContext) -> Result { match args.first() { Some(Value::Number(n)) => Ok(Value::Boolean(n.is_nan())), _ => Ok(Value::Boolean(false)), } } fn number_is_finite(args: &[Value], _ctx: &mut NativeContext) -> Result { match args.first() { Some(Value::Number(n)) => Ok(Value::Boolean(n.is_finite())), _ => Ok(Value::Boolean(false)), } } fn number_is_integer(args: &[Value], _ctx: &mut NativeContext) -> Result { match args.first() { Some(Value::Number(n)) => Ok(Value::Boolean(n.is_finite() && n.trunc() == *n)), _ => Ok(Value::Boolean(false)), } } fn number_is_safe_integer(args: &[Value], _ctx: &mut NativeContext) -> Result { match args.first() { Some(Value::Number(n)) => { let safe = n.is_finite() && n.trunc() == *n && n.abs() <= 9007199254740991.0; Ok(Value::Boolean(safe)) } _ => Ok(Value::Boolean(false)), } } /// Helper: extract the number from `this` for Number.prototype methods. fn this_number(ctx: &NativeContext) -> f64 { ctx.this.to_number() } fn number_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result { let n = this_number(ctx); let radix = args.first().map(|v| v.to_number() as u32).unwrap_or(10); if !(2..=36).contains(&radix) { return Err(RuntimeError::range_error( "toString() radix must be between 2 and 36", )); } if radix == 10 { return Ok(Value::String(Value::Number(n).to_js_string(ctx.gc))); } if n.is_nan() { return Ok(Value::String("NaN".to_string())); } if n.is_infinite() { return Ok(Value::String(if n > 0.0 { "Infinity".to_string() } else { "-Infinity".to_string() })); } // Integer path for non-decimal radix. let neg = n < 0.0; let abs = n.abs() as u64; let mut digits = Vec::new(); let mut val = abs; if val == 0 { digits.push('0'); } else { while val > 0 { let d = (val % radix as u64) as u32; digits.push(char::from_digit(d, radix).unwrap_or('?')); val /= radix as u64; } } digits.reverse(); let mut result = String::new(); if neg { result.push('-'); } result.extend(digits); Ok(Value::String(result)) } fn number_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result { let _ = args; Ok(Value::Number(this_number(ctx))) } fn number_proto_to_fixed(args: &[Value], ctx: &mut NativeContext) -> Result { let n = this_number(ctx); let digits = args.first().map(|v| v.to_number() as usize).unwrap_or(0); if digits > 100 { return Err(RuntimeError::range_error( "toFixed() digits argument must be between 0 and 100", )); } Ok(Value::String(format!("{n:.digits$}"))) } fn number_proto_to_precision( args: &[Value], ctx: &mut NativeContext, ) -> Result { let n = this_number(ctx); if args.is_empty() || matches!(args.first(), Some(Value::Undefined)) { return Ok(Value::String(Value::Number(n).to_js_string(ctx.gc))); } let prec = args[0].to_number() as usize; if !(1..=100).contains(&prec) { return Err(RuntimeError::range_error( "toPrecision() argument must be between 1 and 100", )); } Ok(Value::String(format!("{n:.prec$e}"))) } fn number_proto_to_exponential( args: &[Value], ctx: &mut NativeContext, ) -> Result { let n = this_number(ctx); let digits = args.first().map(|v| v.to_number() as usize).unwrap_or(6); if digits > 100 { return Err(RuntimeError::range_error( "toExponential() argument must be between 0 and 100", )); } Ok(Value::String(format!("{n:.digits$e}"))) } // ── Boolean built-in ───────────────────────────────────────── fn init_boolean_prototype(gc: &mut Gc, proto: GcRef) { let to_string = make_native(gc, "toString", boolean_proto_to_string); set_builtin_prop(gc, proto, "toString", Value::Function(to_string)); let value_of = make_native(gc, "valueOf", boolean_proto_value_of); set_builtin_prop(gc, proto, "valueOf", Value::Function(value_of)); } fn init_boolean_constructor(gc: &mut Gc, bool_proto: GcRef) -> GcRef { gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "Boolean".to_string(), kind: FunctionKind::Native(NativeFunc { callback: boolean_constructor, }), prototype_obj: Some(bool_proto), properties: HashMap::new(), upvalues: Vec::new(), }))) } fn boolean_constructor(args: &[Value], _ctx: &mut NativeContext) -> Result { let b = args.first().map(|v| v.to_boolean()).unwrap_or(false); Ok(Value::Boolean(b)) } fn boolean_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result { let _ = args; Ok(Value::String( if ctx.this.to_boolean() { "true" } else { "false" } .to_string(), )) } fn boolean_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result { let _ = args; Ok(Value::Boolean(ctx.this.to_boolean())) } // ── Symbol built-in ────────────────────────────────────────── /// Global symbol ID counter. Each Symbol() call increments this. static SYMBOL_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); // Thread-local Date prototype GcRef for use in date_constructor. // The Date constructor callback doesn't have VM access, so we store the // prototype here during init and read it back during construction. thread_local! { static DATE_PROTO: std::cell::Cell> = const { std::cell::Cell::new(None) }; } fn init_symbol_builtins(vm: &mut Vm) { // Create a function object so we can hang static props on it. let gc_ref = make_native(&mut vm.gc, "Symbol", symbol_factory); // Well-known symbols as string constants. let well_known = [ ("iterator", "@@iterator"), ("toPrimitive", "@@toPrimitive"), ("toStringTag", "@@toStringTag"), ("hasInstance", "@@hasInstance"), ]; for (name, value) in well_known { set_func_prop(&mut vm.gc, gc_ref, name, Value::String(value.to_string())); } // Symbol.for() and Symbol.keyFor(). let sym_for = make_native(&mut vm.gc, "for", symbol_for); set_func_prop(&mut vm.gc, gc_ref, "for", Value::Function(sym_for)); let sym_key_for = make_native(&mut vm.gc, "keyFor", symbol_key_for); set_func_prop(&mut vm.gc, gc_ref, "keyFor", Value::Function(sym_key_for)); vm.set_global("Symbol", Value::Function(gc_ref)); // Register global NaN and Infinity constants. vm.set_global("NaN", Value::Number(f64::NAN)); vm.set_global("Infinity", Value::Number(f64::INFINITY)); vm.set_global("undefined", Value::Undefined); } fn symbol_factory(args: &[Value], ctx: &mut NativeContext) -> Result { let desc = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let id = SYMBOL_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); // Return a unique string representation. Real Symbol is a distinct type, // but this pragmatic approach works for property keys and identity checks. Ok(Value::String(format!("@@sym_{id}_{desc}"))) } fn symbol_for(args: &[Value], ctx: &mut NativeContext) -> Result { let key = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); // Deterministic: same key always produces same symbol string. Ok(Value::String(format!("@@global_{key}"))) } fn symbol_key_for(args: &[Value], ctx: &mut NativeContext) -> Result { let sym = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); if let Some(key) = sym.strip_prefix("@@global_") { Ok(Value::String(key.to_string())) } else { Ok(Value::Undefined) } } // ── Math object ────────────────────────────────────────────── /// Simple xorshift64 PRNG state (no crypto requirement). static PRNG_STATE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); fn prng_next() -> f64 { let mut s = PRNG_STATE.load(std::sync::atomic::Ordering::Relaxed); if s == 0 { // Seed from system time. s = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_nanos() as u64) .unwrap_or(123456789); } s ^= s << 13; s ^= s >> 7; s ^= s << 17; PRNG_STATE.store(s, std::sync::atomic::Ordering::Relaxed); // Map to [0, 1). (s >> 11) as f64 / ((1u64 << 53) as f64) } fn init_math_object(vm: &mut Vm) { let mut data = ObjectData::new(); if let Some(proto) = vm.object_prototype { data.prototype = Some(proto); } let math_ref = vm.gc.alloc(HeapObject::Object(data)); // Constants. let constants: &[(&str, f64)] = &[ ("E", std::f64::consts::E), ("LN2", std::f64::consts::LN_2), ("LN10", std::f64::consts::LN_10), ("LOG2E", std::f64::consts::LOG2_E), ("LOG10E", std::f64::consts::LOG10_E), ("PI", std::f64::consts::PI), ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2), ("SQRT2", std::f64::consts::SQRT_2), ]; for &(name, val) in constants { set_builtin_prop(&mut vm.gc, math_ref, name, Value::Number(val)); } // Methods. let methods: &[NativeMethod] = &[ ("abs", math_abs), ("ceil", math_ceil), ("floor", math_floor), ("round", math_round), ("trunc", math_trunc), ("max", math_max), ("min", math_min), ("pow", math_pow), ("sqrt", math_sqrt), ("cbrt", math_cbrt), ("hypot", math_hypot), ("sin", math_sin), ("cos", math_cos), ("tan", math_tan), ("asin", math_asin), ("acos", math_acos), ("atan", math_atan), ("atan2", math_atan2), ("exp", math_exp), ("log", math_log), ("log2", math_log2), ("log10", math_log10), ("expm1", math_expm1), ("log1p", math_log1p), ("sign", math_sign), ("clz32", math_clz32), ("fround", math_fround), ("random", math_random), ("imul", math_imul), ]; for &(name, cb) in methods { let f = make_native(&mut vm.gc, name, cb); set_builtin_prop(&mut vm.gc, math_ref, name, Value::Function(f)); } vm.set_global("Math", Value::Object(math_ref)); } fn math_abs(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.abs())) } fn math_ceil(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.ceil())) } fn math_floor(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.floor())) } fn math_round(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); // JS Math.round rounds .5 up (toward +Infinity). Ok(Value::Number(n.round())) } fn math_trunc(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.trunc())) } fn math_max(args: &[Value], _ctx: &mut NativeContext) -> Result { if args.is_empty() { return Ok(Value::Number(f64::NEG_INFINITY)); } let mut result = f64::NEG_INFINITY; for arg in args { let n = arg.to_number(); if n.is_nan() { return Ok(Value::Number(f64::NAN)); } if n > result { result = n; } } Ok(Value::Number(result)) } fn math_min(args: &[Value], _ctx: &mut NativeContext) -> Result { if args.is_empty() { return Ok(Value::Number(f64::INFINITY)); } let mut result = f64::INFINITY; for arg in args { let n = arg.to_number(); if n.is_nan() { return Ok(Value::Number(f64::NAN)); } if n < result { result = n; } } Ok(Value::Number(result)) } fn math_pow(args: &[Value], _ctx: &mut NativeContext) -> Result { let base = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); let exp = args.get(1).map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(base.powf(exp))) } fn math_sqrt(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.sqrt())) } fn math_cbrt(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.cbrt())) } fn math_hypot(args: &[Value], _ctx: &mut NativeContext) -> Result { if args.is_empty() { return Ok(Value::Number(0.0)); } let mut sum = 0.0f64; for arg in args { let n = arg.to_number(); if n.is_infinite() { return Ok(Value::Number(f64::INFINITY)); } if n.is_nan() { return Ok(Value::Number(f64::NAN)); } sum += n * n; } Ok(Value::Number(sum.sqrt())) } fn math_sin(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.sin())) } fn math_cos(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.cos())) } fn math_tan(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.tan())) } fn math_asin(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.asin())) } fn math_acos(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.acos())) } fn math_atan(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.atan())) } fn math_atan2(args: &[Value], _ctx: &mut NativeContext) -> Result { let y = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); let x = args.get(1).map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(y.atan2(x))) } fn math_exp(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.exp())) } fn math_log(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.ln())) } fn math_log2(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.log2())) } fn math_log10(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.log10())) } fn math_expm1(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.exp_m1())) } fn math_log1p(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number(n.ln_1p())) } fn math_sign(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); if n.is_nan() { Ok(Value::Number(f64::NAN)) } else if n == 0.0 { // Preserve -0 and +0. Ok(Value::Number(n)) } else if n > 0.0 { Ok(Value::Number(1.0)) } else { Ok(Value::Number(-1.0)) } } fn math_clz32(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(0.0); let i = n as u32; Ok(Value::Number(i.leading_zeros() as f64)) } fn math_fround(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Number((n as f32) as f64)) } fn math_random(_args: &[Value], _ctx: &mut NativeContext) -> Result { Ok(Value::Number(prng_next())) } fn math_imul(args: &[Value], _ctx: &mut NativeContext) -> Result { // ToUint32 then reinterpret as i32 per spec. let a = args.first().map(|v| v.to_number()).unwrap_or(0.0) as u32 as i32; let b = args.get(1).map(|v| v.to_number()).unwrap_or(0.0) as u32 as i32; Ok(Value::Number(a.wrapping_mul(b) as f64)) } // ── Date built-in ──────────────────────────────────────────── /// Milliseconds since Unix epoch from SystemTime. fn now_ms() -> f64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_millis() as f64) .unwrap_or(0.0) } /// Calendar components from a timestamp (milliseconds since epoch). /// Returns (year, month0, day, hours, minutes, seconds, ms, weekday) in UTC. fn ms_to_utc_components(ms: f64) -> (i64, i64, i64, i64, i64, i64, i64, i64) { let total_ms = ms as i64; let ms_part = ((total_ms % 1000) + 1000) % 1000; let mut days = total_ms.div_euclid(86_400_000); // Weekday: Jan 1 1970 was Thursday (4). let weekday = ((days % 7) + 4 + 7) % 7; let secs_in_day = (total_ms.rem_euclid(86_400_000)) / 1000; let hours = secs_in_day / 3600; let minutes = (secs_in_day % 3600) / 60; let seconds = secs_in_day % 60; // Convert days since epoch to year/month/day using a civil calendar algorithm. // Shift epoch to March 1, 2000. days += 719_468; let era = if days >= 0 { days / 146_097 } else { (days - 146_096) / 146_097 }; let doe = days - era * 146_097; // day of era [0, 146096] let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let y = yoe + era * 400; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; let y = if m <= 2 { y + 1 } else { y }; (y, m - 1, d, hours, minutes, seconds, ms_part, weekday) } /// Convert UTC components to milliseconds since epoch. fn utc_components_to_ms(year: i64, month: i64, day: i64, h: i64, min: i64, s: i64, ms: i64) -> f64 { // Normalize month overflow. let y = year + month.div_euclid(12); let m = month.rem_euclid(12) + 1; // 1-based // Civil calendar to days (inverse of above). let (y2, m2) = if m <= 2 { (y - 1, m + 9) } else { (y, m - 3) }; let era = if y2 >= 0 { y2 / 400 } else { (y2 - 399) / 400 }; let yoe = y2 - era * 400; let doy = (153 * m2 + 2) / 5 + day - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; let days = era * 146_097 + doe - 719_468; (days * 86_400_000 + h * 3_600_000 + min * 60_000 + s * 1000 + ms) as f64 } /// Parse a subset of ISO 8601 date strings (YYYY-MM-DDTHH:MM:SS.sssZ). fn parse_date_string(s: &str) -> Option { let s = s.trim(); // Try YYYY-MM-DDTHH:MM:SS.sssZ or YYYY-MM-DD or YYYY let parts: Vec<&str> = s.splitn(2, 'T').collect(); let date_part = parts[0]; let time_part = parts.get(1).copied().unwrap_or(""); let date_fields: Vec<&str> = date_part.split('-').collect(); if date_fields.is_empty() { return None; } let year: i64 = date_fields[0].parse().ok()?; let month: i64 = date_fields.get(1).and_then(|s| s.parse().ok()).unwrap_or(1); let day: i64 = date_fields.get(2).and_then(|s| s.parse().ok()).unwrap_or(1); if !(1..=12).contains(&month) || !(1..=31).contains(&day) { return None; } let (h, min, sec, ms) = if time_part.is_empty() { (0, 0, 0, 0) } else { // Strip trailing Z. let tp = time_part.strip_suffix('Z').unwrap_or(time_part); // Split seconds from milliseconds. let (time_str, ms_val) = if let Some((t, m)) = tp.split_once('.') { let ms_str = &m[..m.len().min(3)]; let mut ms_val: i64 = ms_str.parse().unwrap_or(0); // Pad to 3 digits if needed. for _ in ms_str.len()..3 { ms_val *= 10; } (t, ms_val) } else { (tp, 0i64) }; let time_fields: Vec<&str> = time_str.split(':').collect(); let h: i64 = time_fields .first() .and_then(|s| s.parse().ok()) .unwrap_or(0); let min: i64 = time_fields.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); let sec: i64 = time_fields.get(2).and_then(|s| s.parse().ok()).unwrap_or(0); (h, min, sec, ms_val) }; Some(utc_components_to_ms(year, month - 1, day, h, min, sec, ms)) } static DAY_NAMES: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; static MONTH_NAMES: [&str; 12] = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; fn init_date_builtins(vm: &mut Vm) { // Date.prototype. let mut date_proto_data = ObjectData::new(); if let Some(proto) = vm.object_prototype { date_proto_data.prototype = Some(proto); } let date_proto = vm.gc.alloc(HeapObject::Object(date_proto_data)); init_date_prototype(&mut vm.gc, date_proto); // Store prototype for constructor access. vm.date_prototype = Some(date_proto); DATE_PROTO.with(|cell| cell.set(Some(date_proto))); // Date constructor function. let ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "Date".to_string(), kind: FunctionKind::Native(NativeFunc { callback: date_constructor, }), prototype_obj: Some(date_proto), properties: HashMap::new(), upvalues: Vec::new(), }))); // Static methods. let now_fn = make_native(&mut vm.gc, "now", date_now); set_func_prop(&mut vm.gc, ctor, "now", Value::Function(now_fn)); let parse_fn = make_native(&mut vm.gc, "parse", date_parse); set_func_prop(&mut vm.gc, ctor, "parse", Value::Function(parse_fn)); let utc_fn = make_native(&mut vm.gc, "UTC", date_utc); set_func_prop(&mut vm.gc, ctor, "UTC", Value::Function(utc_fn)); vm.set_global("Date", Value::Function(ctor)); } /// Internal: create a Date object (plain object with __date_ms__ property). fn make_date_obj(gc: &mut Gc, ms: f64, proto: Option) -> Value { let mut data = ObjectData::new(); if let Some(p) = proto { data.prototype = Some(p); } data.properties.insert( "__date_ms__".to_string(), Property::builtin(Value::Number(ms)), ); Value::Object(gc.alloc(HeapObject::Object(data))) } /// Extract timestamp from a Date object. fn date_get_ms(gc: &Gc, this: &Value) -> f64 { match this { Value::Object(r) => match gc.get(*r) { Some(HeapObject::Object(data)) => data .properties .get("__date_ms__") .map(|p| p.value.to_number()) .unwrap_or(f64::NAN), _ => f64::NAN, }, _ => f64::NAN, } } /// Set timestamp on a Date object and return the new value. fn date_set_ms(gc: &mut Gc, this: &Value, ms: f64) -> f64 { if let Value::Object(r) = this { if let Some(HeapObject::Object(data)) = gc.get_mut(*r) { if let Some(prop) = data.properties.get_mut("__date_ms__") { prop.value = Value::Number(ms); } } } ms } fn date_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { let proto = DATE_PROTO.with(|cell| cell.get()); let ms = match args.len() { 0 => now_ms(), 1 => match &args[0] { Value::String(s) => parse_date_string(s).unwrap_or(f64::NAN), Value::Number(n) => *n, v => v.to_number(), }, _ => { // new Date(year, month, day?, hours?, min?, sec?, ms?) let year = args[0].to_number() as i64; let month = args[1].to_number() as i64; let day = args.get(2).map(|v| v.to_number() as i64).unwrap_or(1); let h = args.get(3).map(|v| v.to_number() as i64).unwrap_or(0); let min = args.get(4).map(|v| v.to_number() as i64).unwrap_or(0); let sec = args.get(5).map(|v| v.to_number() as i64).unwrap_or(0); let ms = args.get(6).map(|v| v.to_number() as i64).unwrap_or(0); // Years 0-99 map to 1900-1999 per spec. let year = if (0..100).contains(&year) { year + 1900 } else { year }; utc_components_to_ms(year, month, day, h, min, sec, ms) } }; Ok(make_date_obj(ctx.gc, ms, proto)) } fn date_now(_args: &[Value], _ctx: &mut NativeContext) -> Result { Ok(Value::Number(now_ms())) } fn date_parse(args: &[Value], ctx: &mut NativeContext) -> Result { let s = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); Ok(Value::Number(parse_date_string(&s).unwrap_or(f64::NAN))) } fn date_utc(args: &[Value], _ctx: &mut NativeContext) -> Result { let year = args.first().map(|v| v.to_number() as i64).unwrap_or(1970); let month = args.get(1).map(|v| v.to_number() as i64).unwrap_or(0); let day = args.get(2).map(|v| v.to_number() as i64).unwrap_or(1); let h = args.get(3).map(|v| v.to_number() as i64).unwrap_or(0); let min = args.get(4).map(|v| v.to_number() as i64).unwrap_or(0); let sec = args.get(5).map(|v| v.to_number() as i64).unwrap_or(0); let ms = args.get(6).map(|v| v.to_number() as i64).unwrap_or(0); let year = if (0..100).contains(&year) { year + 1900 } else { year }; Ok(Value::Number(utc_components_to_ms( year, month, day, h, min, sec, ms, ))) } fn init_date_prototype(gc: &mut Gc, proto: GcRef) { let methods: &[NativeMethod] = &[ ("getTime", date_get_time), ("valueOf", date_get_time), // valueOf === getTime ("getFullYear", date_get_full_year), ("getMonth", date_get_month), ("getDate", date_get_date), ("getDay", date_get_day), ("getHours", date_get_hours), ("getMinutes", date_get_minutes), ("getSeconds", date_get_seconds), ("getMilliseconds", date_get_milliseconds), ("getTimezoneOffset", date_get_timezone_offset), ("getUTCFullYear", date_get_full_year), ("getUTCMonth", date_get_month), ("getUTCDate", date_get_date), ("getUTCDay", date_get_day), ("getUTCHours", date_get_hours), ("getUTCMinutes", date_get_minutes), ("getUTCSeconds", date_get_seconds), ("getUTCMilliseconds", date_get_milliseconds), ("setTime", date_set_time), ("setFullYear", date_set_full_year), ("setMonth", date_set_month), ("setDate", date_set_date), ("setHours", date_set_hours), ("setMinutes", date_set_minutes), ("setSeconds", date_set_seconds), ("setMilliseconds", date_set_milliseconds), ("toString", date_to_string), ("toISOString", date_to_iso_string), ("toUTCString", date_to_utc_string), ("toLocaleDateString", date_to_locale_date_string), ("toJSON", date_to_json), ]; for &(name, cb) in methods { let f = make_native(gc, name, cb); set_builtin_prop(gc, proto, name, Value::Function(f)); } } fn date_get_time(args: &[Value], ctx: &mut NativeContext) -> Result { let _ = args; Ok(Value::Number(date_get_ms(ctx.gc, &ctx.this))) } fn date_get_full_year(_args: &[Value], ctx: &mut NativeContext) -> Result { let ms = date_get_ms(ctx.gc, &ctx.this); if ms.is_nan() { return Ok(Value::Number(f64::NAN)); } let (y, ..) = ms_to_utc_components(ms); Ok(Value::Number(y as f64)) } fn date_get_month(_args: &[Value], ctx: &mut NativeContext) -> Result { let ms = date_get_ms(ctx.gc, &ctx.this); if ms.is_nan() { return Ok(Value::Number(f64::NAN)); } let (_, m, ..) = ms_to_utc_components(ms); Ok(Value::Number(m as f64)) } fn date_get_date(_args: &[Value], ctx: &mut NativeContext) -> Result { let ms = date_get_ms(ctx.gc, &ctx.this); if ms.is_nan() { return Ok(Value::Number(f64::NAN)); } let (_, _, d, ..) = ms_to_utc_components(ms); Ok(Value::Number(d as f64)) } fn date_get_day(_args: &[Value], ctx: &mut NativeContext) -> Result { let ms = date_get_ms(ctx.gc, &ctx.this); if ms.is_nan() { return Ok(Value::Number(f64::NAN)); } let (.., wd) = ms_to_utc_components(ms); Ok(Value::Number(wd as f64)) } fn date_get_hours(_args: &[Value], ctx: &mut NativeContext) -> Result { let ms = date_get_ms(ctx.gc, &ctx.this); if ms.is_nan() { return Ok(Value::Number(f64::NAN)); } let (_, _, _, h, ..) = ms_to_utc_components(ms); Ok(Value::Number(h as f64)) } fn date_get_minutes(_args: &[Value], ctx: &mut NativeContext) -> Result { let ms = date_get_ms(ctx.gc, &ctx.this); if ms.is_nan() { return Ok(Value::Number(f64::NAN)); } let (_, _, _, _, min, ..) = ms_to_utc_components(ms); Ok(Value::Number(min as f64)) } fn date_get_seconds(_args: &[Value], ctx: &mut NativeContext) -> Result { let ms = date_get_ms(ctx.gc, &ctx.this); if ms.is_nan() { return Ok(Value::Number(f64::NAN)); } let (_, _, _, _, _, s, ..) = ms_to_utc_components(ms); Ok(Value::Number(s as f64)) } fn date_get_milliseconds(_args: &[Value], ctx: &mut NativeContext) -> Result { let ms = date_get_ms(ctx.gc, &ctx.this); if ms.is_nan() { return Ok(Value::Number(f64::NAN)); } let (_, _, _, _, _, _, ms_part, _) = ms_to_utc_components(ms); Ok(Value::Number(ms_part as f64)) } fn date_get_timezone_offset( _args: &[Value], _ctx: &mut NativeContext, ) -> Result { // We operate in UTC; return 0. Ok(Value::Number(0.0)) } fn date_set_time(args: &[Value], ctx: &mut NativeContext) -> Result { let ms = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); date_set_ms(ctx.gc, &ctx.this, ms); Ok(Value::Number(ms)) } fn date_set_full_year(args: &[Value], ctx: &mut NativeContext) -> Result { let old = date_get_ms(ctx.gc, &ctx.this); let (_, m, d, h, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); let year = args.first().map(|v| v.to_number() as i64).unwrap_or(0); let month = args.get(1).map(|v| v.to_number() as i64).unwrap_or(m); let day = args.get(2).map(|v| v.to_number() as i64).unwrap_or(d); let new_ms = utc_components_to_ms(year, month, day, h, min, s, ms); date_set_ms(ctx.gc, &ctx.this, new_ms); Ok(Value::Number(new_ms)) } fn date_set_month(args: &[Value], ctx: &mut NativeContext) -> Result { let old = date_get_ms(ctx.gc, &ctx.this); let (y, _, d, h, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); let month = args.first().map(|v| v.to_number() as i64).unwrap_or(0); let day = args.get(1).map(|v| v.to_number() as i64).unwrap_or(d); let new_ms = utc_components_to_ms(y, month, day, h, min, s, ms); date_set_ms(ctx.gc, &ctx.this, new_ms); Ok(Value::Number(new_ms)) } fn date_set_date(args: &[Value], ctx: &mut NativeContext) -> Result { let old = date_get_ms(ctx.gc, &ctx.this); let (y, m, _, h, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); let day = args.first().map(|v| v.to_number() as i64).unwrap_or(1); let new_ms = utc_components_to_ms(y, m, day, h, min, s, ms); date_set_ms(ctx.gc, &ctx.this, new_ms); Ok(Value::Number(new_ms)) } fn date_set_hours(args: &[Value], ctx: &mut NativeContext) -> Result { let old = date_get_ms(ctx.gc, &ctx.this); let (y, m, d, _, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); let h = args.first().map(|v| v.to_number() as i64).unwrap_or(0); let min_v = args.get(1).map(|v| v.to_number() as i64).unwrap_or(min); let sec = args.get(2).map(|v| v.to_number() as i64).unwrap_or(s); let ms_v = args.get(3).map(|v| v.to_number() as i64).unwrap_or(ms); let new_ms = utc_components_to_ms(y, m, d, h, min_v, sec, ms_v); date_set_ms(ctx.gc, &ctx.this, new_ms); Ok(Value::Number(new_ms)) } fn date_set_minutes(args: &[Value], ctx: &mut NativeContext) -> Result { let old = date_get_ms(ctx.gc, &ctx.this); let (y, m, d, h, _, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); let min = args.first().map(|v| v.to_number() as i64).unwrap_or(0); let sec = args.get(1).map(|v| v.to_number() as i64).unwrap_or(s); let ms_v = args.get(2).map(|v| v.to_number() as i64).unwrap_or(ms); let new_ms = utc_components_to_ms(y, m, d, h, min, sec, ms_v); date_set_ms(ctx.gc, &ctx.this, new_ms); Ok(Value::Number(new_ms)) } fn date_set_seconds(args: &[Value], ctx: &mut NativeContext) -> Result { let old = date_get_ms(ctx.gc, &ctx.this); let (y, m, d, h, min, _, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); let sec = args.first().map(|v| v.to_number() as i64).unwrap_or(0); let ms_v = args.get(1).map(|v| v.to_number() as i64).unwrap_or(ms); let new_ms = utc_components_to_ms(y, m, d, h, min, sec, ms_v); date_set_ms(ctx.gc, &ctx.this, new_ms); Ok(Value::Number(new_ms)) } fn date_set_milliseconds(args: &[Value], ctx: &mut NativeContext) -> Result { let old = date_get_ms(ctx.gc, &ctx.this); let (y, m, d, h, min, s, _, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); let ms_v = args.first().map(|v| v.to_number() as i64).unwrap_or(0); let new_ms = utc_components_to_ms(y, m, d, h, min, s, ms_v); date_set_ms(ctx.gc, &ctx.this, new_ms); Ok(Value::Number(new_ms)) } fn date_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result { let ms = date_get_ms(ctx.gc, &ctx.this); if ms.is_nan() { return Ok(Value::String("Invalid Date".to_string())); } let (y, m, d, h, min, s, _, wd) = ms_to_utc_components(ms); Ok(Value::String(format!( "{} {} {:02} {} {:02}:{:02}:{:02} GMT", DAY_NAMES[wd as usize], MONTH_NAMES[m as usize], d, y, h, min, s ))) } fn date_to_iso_string(_args: &[Value], ctx: &mut NativeContext) -> Result { let ms = date_get_ms(ctx.gc, &ctx.this); if ms.is_nan() { return Err(RuntimeError::range_error("Invalid time value")); } let (y, m, d, h, min, s, ms_part, _) = ms_to_utc_components(ms); Ok(Value::String(format!( "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z", y, m + 1, d, h, min, s, ms_part ))) } fn date_to_utc_string(_args: &[Value], ctx: &mut NativeContext) -> Result { let ms = date_get_ms(ctx.gc, &ctx.this); if ms.is_nan() { return Ok(Value::String("Invalid Date".to_string())); } let (y, m, d, h, min, s, _, wd) = ms_to_utc_components(ms); Ok(Value::String(format!( "{}, {:02} {} {} {:02}:{:02}:{:02} GMT", DAY_NAMES[wd as usize], d, MONTH_NAMES[m as usize], y, h, min, s ))) } fn date_to_locale_date_string( _args: &[Value], ctx: &mut NativeContext, ) -> Result { // Simplified: same as toISOString date portion. let ms = date_get_ms(ctx.gc, &ctx.this); if ms.is_nan() { return Ok(Value::String("Invalid Date".to_string())); } let (y, m, d, ..) = ms_to_utc_components(ms); Ok(Value::String(format!("{:04}-{:02}-{:02}", y, m + 1, d))) } fn date_to_json(_args: &[Value], ctx: &mut NativeContext) -> Result { let ms = date_get_ms(ctx.gc, &ctx.this); if ms.is_nan() { return Ok(Value::Null); } date_to_iso_string(_args, ctx) } // ── RegExp built-in ────────────────────────────────────────── thread_local! { static REGEXP_PROTO: std::cell::Cell> = const { std::cell::Cell::new(None) }; } fn init_regexp_builtins(vm: &mut Vm) { // RegExp.prototype. let mut regexp_proto_data = ObjectData::new(); if let Some(proto) = vm.object_prototype { regexp_proto_data.prototype = Some(proto); } let regexp_proto = vm.gc.alloc(HeapObject::Object(regexp_proto_data)); init_regexp_prototype(&mut vm.gc, regexp_proto); vm.regexp_prototype = Some(regexp_proto); REGEXP_PROTO.with(|cell| cell.set(Some(regexp_proto))); // RegExp constructor function. let ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "RegExp".to_string(), kind: FunctionKind::Native(NativeFunc { callback: regexp_constructor, }), prototype_obj: Some(regexp_proto), properties: HashMap::new(), upvalues: Vec::new(), }))); vm.set_global("RegExp", Value::Function(ctor)); } fn init_regexp_prototype(gc: &mut Gc, proto: GcRef) { let methods: &[NativeMethod] = &[ ("test", regexp_proto_test), ("exec", regexp_proto_exec), ("toString", regexp_proto_to_string), ]; for &(name, callback) in methods { let f = make_native(gc, name, callback); set_builtin_prop(gc, proto, name, Value::Function(f)); } } /// Create a RegExp object storing compiled regex state in hidden properties. pub fn make_regexp_obj( gc: &mut Gc, pattern: &str, flags_str: &str, proto: Option, ) -> Result { use crate::regex::CompiledRegex; let compiled = CompiledRegex::new(pattern, flags_str)?; let flags = compiled.flags; let mut data = ObjectData::new(); if let Some(p) = proto { data.prototype = Some(p); } // Store pattern and flags as properties. data.properties.insert( "source".to_string(), Property::builtin(Value::String(pattern.to_string())), ); let flags_string = flags.as_flag_string(); data.properties.insert( "flags".to_string(), Property::builtin(Value::String(flags_string)), ); data.properties.insert( "global".to_string(), Property::builtin(Value::Boolean(flags.global)), ); data.properties.insert( "ignoreCase".to_string(), Property::builtin(Value::Boolean(flags.ignore_case)), ); data.properties.insert( "multiline".to_string(), Property::builtin(Value::Boolean(flags.multiline)), ); data.properties.insert( "dotAll".to_string(), Property::builtin(Value::Boolean(flags.dot_all)), ); data.properties.insert( "unicode".to_string(), Property::builtin(Value::Boolean(flags.unicode)), ); data.properties.insert( "sticky".to_string(), Property::builtin(Value::Boolean(flags.sticky)), ); data.properties .insert("lastIndex".to_string(), Property::data(Value::Number(0.0))); // Hidden: serialized pattern for re-compilation. data.properties.insert( "__regexp_pattern__".to_string(), Property::builtin(Value::String(pattern.to_string())), ); data.properties.insert( "__regexp_flags__".to_string(), Property::builtin(Value::String(flags.as_flag_string())), ); Ok(Value::Object(gc.alloc(HeapObject::Object(data)))) } /// Check if a Value is a RegExp object. pub fn is_regexp(gc: &Gc, val: &Value) -> bool { match val { Value::Object(r) => match gc.get(*r) { Some(HeapObject::Object(data)) => data.properties.contains_key("__regexp_pattern__"), _ => false, }, _ => false, } } /// Extract the pattern from a RegExp object. fn regexp_get_pattern(gc: &Gc, val: &Value) -> Option { match val { Value::Object(r) => match gc.get(*r) { Some(HeapObject::Object(data)) => { data.properties .get("__regexp_pattern__") .and_then(|p| match &p.value { Value::String(s) => Some(s.clone()), _ => None, }) } _ => None, }, _ => None, } } /// Extract the flags string from a RegExp object. fn regexp_get_flags(gc: &Gc, val: &Value) -> Option { match val { Value::Object(r) => match gc.get(*r) { Some(HeapObject::Object(data)) => { data.properties .get("__regexp_flags__") .and_then(|p| match &p.value { Value::String(s) => Some(s.clone()), _ => None, }) } _ => None, }, _ => None, } } /// Get lastIndex from a RegExp object. fn regexp_get_last_index(gc: &Gc, val: &Value) -> f64 { match val { Value::Object(r) => match gc.get(*r) { Some(HeapObject::Object(data)) => data .properties .get("lastIndex") .map(|p| p.value.to_number()) .unwrap_or(0.0), _ => 0.0, }, _ => 0.0, } } /// Set lastIndex on a RegExp object. fn regexp_set_last_index(gc: &mut Gc, val: &Value, idx: f64) { if let Value::Object(r) = val { if let Some(HeapObject::Object(data)) = gc.get_mut(*r) { if let Some(prop) = data.properties.get_mut("lastIndex") { prop.value = Value::Number(idx); } } } } /// Execute the regex on a string and return a match result array or null. fn regexp_exec_internal( gc: &mut Gc, this: &Value, input: &str, ) -> Result { use crate::regex::{exec, CompiledRegex}; let pattern = regexp_get_pattern(gc, this) .ok_or_else(|| RuntimeError::type_error("not a RegExp".to_string()))?; let flags_str = regexp_get_flags(gc, this).unwrap_or_default(); let compiled = CompiledRegex::new(&pattern, &flags_str).map_err(RuntimeError::syntax_error)?; let is_global = compiled.flags.global; let is_sticky = compiled.flags.sticky; let start_index = if is_global || is_sticky { let li = regexp_get_last_index(gc, this); if li < 0.0 { 0 } else { li as usize } } else { 0 }; let chars: Vec = input.chars().collect(); let result = exec(&compiled, input, start_index); match result { Some(m) => { if is_global || is_sticky { regexp_set_last_index(gc, this, m.end as f64); } // Build result array: [fullMatch, ...groups] let mut items: Vec = Vec::new(); // Full match (index 0). let full: String = chars[m.start..m.end].iter().collect(); items.push(Value::String(full)); // Capture groups (index 1..n). for i in 1..m.captures.len() { match m.captures[i] { Some((s, e)) => { let cap: String = chars[s..e].iter().collect(); items.push(Value::String(cap)); } None => items.push(Value::Undefined), } } // Build named groups object (if any) before creating the array. let groups_val = { let (node, _) = crate::regex::parse_pattern(&pattern).map_err(RuntimeError::syntax_error)?; let named = collect_named_groups(&node); if named.is_empty() { Value::Undefined } else { let mut groups_data = ObjectData::new(); for (name, idx) in &named { let cap_idx = *idx as usize; let val = if cap_idx < m.captures.len() { match m.captures[cap_idx] { Some((s, e)) => { let cap: String = chars[s..e].iter().collect(); Value::String(cap) } None => Value::Undefined, } } else { Value::Undefined }; groups_data .properties .insert(name.clone(), Property::data(val)); } Value::Object(gc.alloc(HeapObject::Object(groups_data))) } }; let arr = make_value_array(gc, &items); // Set index, input, and groups properties on the result array. if let Value::Object(r) = arr { if let Some(HeapObject::Object(data)) = gc.get_mut(r) { data.properties.insert( "index".to_string(), Property::data(Value::Number(m.start as f64)), ); data.properties.insert( "input".to_string(), Property::data(Value::String(input.to_string())), ); data.properties .insert("groups".to_string(), Property::data(groups_val)); } Ok(Value::Object(r)) } else { Ok(arr) } } None => { if is_global || is_sticky { regexp_set_last_index(gc, this, 0.0); } Ok(Value::Null) } } } /// Collect named groups from a regex AST node. fn collect_named_groups(node: &crate::regex::Node) -> Vec<(String, u32)> { use crate::regex::Node; let mut result = Vec::new(); match node { Node::Group { index, name: Some(name), node: inner, } => { result.push((name.clone(), *index)); result.extend(collect_named_groups(inner)); } Node::Group { node: inner, .. } | Node::NonCapturingGroup(inner) | Node::Lookahead(inner) | Node::NegativeLookahead(inner) => { result.extend(collect_named_groups(inner)); } Node::Quantifier { node: inner, .. } => { result.extend(collect_named_groups(inner)); } Node::Sequence(nodes) | Node::Alternation(nodes) => { for n in nodes { result.extend(collect_named_groups(n)); } } _ => {} } result } fn regexp_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { let proto = REGEXP_PROTO.with(|cell| cell.get()); let pattern = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let flags = args .get(1) .map(|v| { if matches!(v, Value::Undefined) { String::new() } else { v.to_js_string(ctx.gc) } }) .unwrap_or_default(); make_regexp_obj(ctx.gc, &pattern, &flags, proto).map_err(RuntimeError::syntax_error) } fn regexp_proto_test(args: &[Value], ctx: &mut NativeContext) -> Result { let input = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let result = regexp_exec_internal(ctx.gc, &ctx.this, &input)?; Ok(Value::Boolean(!matches!(result, Value::Null))) } fn regexp_proto_exec(args: &[Value], ctx: &mut NativeContext) -> Result { let input = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); regexp_exec_internal(ctx.gc, &ctx.this, &input) } fn regexp_proto_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result { let pattern = regexp_get_pattern(ctx.gc, &ctx.this).unwrap_or_default(); let flags = regexp_get_flags(ctx.gc, &ctx.this).unwrap_or_default(); Ok(Value::String(format!("/{}/{}", pattern, flags))) } // ── Map built-in ───────────────────────────────────────────── thread_local! { static MAP_PROTO: std::cell::Cell> = const { std::cell::Cell::new(None) }; static SET_PROTO: std::cell::Cell> = const { std::cell::Cell::new(None) }; static WEAKMAP_PROTO: std::cell::Cell> = const { std::cell::Cell::new(None) }; static WEAKSET_PROTO: std::cell::Cell> = const { std::cell::Cell::new(None) }; } fn init_map_set_builtins(vm: &mut Vm) { // ── Map ── let mut map_proto_data = ObjectData::new(); if let Some(proto) = vm.object_prototype { map_proto_data.prototype = Some(proto); } let map_proto = vm.gc.alloc(HeapObject::Object(map_proto_data)); init_map_prototype(&mut vm.gc, map_proto); MAP_PROTO.with(|cell| cell.set(Some(map_proto))); let map_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "Map".to_string(), kind: FunctionKind::Native(NativeFunc { callback: map_constructor, }), prototype_obj: Some(map_proto), properties: HashMap::new(), upvalues: Vec::new(), }))); vm.set_global("Map", Value::Function(map_ctor)); // ── Set ── let mut set_proto_data = ObjectData::new(); if let Some(proto) = vm.object_prototype { set_proto_data.prototype = Some(proto); } let set_proto = vm.gc.alloc(HeapObject::Object(set_proto_data)); init_set_prototype(&mut vm.gc, set_proto); SET_PROTO.with(|cell| cell.set(Some(set_proto))); let set_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "Set".to_string(), kind: FunctionKind::Native(NativeFunc { callback: set_constructor, }), prototype_obj: Some(set_proto), properties: HashMap::new(), upvalues: Vec::new(), }))); vm.set_global("Set", Value::Function(set_ctor)); // ── WeakMap ── let mut wm_proto_data = ObjectData::new(); if let Some(proto) = vm.object_prototype { wm_proto_data.prototype = Some(proto); } let wm_proto = vm.gc.alloc(HeapObject::Object(wm_proto_data)); init_weakmap_prototype(&mut vm.gc, wm_proto); WEAKMAP_PROTO.with(|cell| cell.set(Some(wm_proto))); let wm_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "WeakMap".to_string(), kind: FunctionKind::Native(NativeFunc { callback: weakmap_constructor, }), prototype_obj: Some(wm_proto), properties: HashMap::new(), upvalues: Vec::new(), }))); vm.set_global("WeakMap", Value::Function(wm_ctor)); // ── WeakSet ── let mut ws_proto_data = ObjectData::new(); if let Some(proto) = vm.object_prototype { ws_proto_data.prototype = Some(proto); } let ws_proto = vm.gc.alloc(HeapObject::Object(ws_proto_data)); init_weakset_prototype(&mut vm.gc, ws_proto); WEAKSET_PROTO.with(|cell| cell.set(Some(ws_proto))); let ws_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "WeakSet".to_string(), kind: FunctionKind::Native(NativeFunc { callback: weakset_constructor, }), prototype_obj: Some(ws_proto), properties: HashMap::new(), upvalues: Vec::new(), }))); vm.set_global("WeakSet", Value::Function(ws_ctor)); } // ── Map / Set internal helpers ─────────────────────────────── /// Internal storage key for Map/Set entries. /// We store entries as a hidden object with indexed key/value pairs: /// __entries__: GcRef to an object with "0_k", "0_v", "1_k", "1_v", ... /// __entry_count__: total slots allocated (some may be deleted) /// __live_count__: number of non-deleted entries /// Deleted entries have their key set to a special "__deleted__" marker. const ENTRIES_KEY: &str = "__entries__"; const ENTRY_COUNT_KEY: &str = "__entry_count__"; const LIVE_COUNT_KEY: &str = "__live_count__"; const DELETED_MARKER: &str = "__deleted__"; /// Create a new empty Map/Set internal storage object. fn make_collection_obj(gc: &mut Gc, proto: Option) -> GcRef { let entries_obj = gc.alloc(HeapObject::Object(ObjectData::new())); let mut data = ObjectData::new(); if let Some(p) = proto { data.prototype = Some(p); } data.properties.insert( ENTRIES_KEY.to_string(), Property::builtin(Value::Object(entries_obj)), ); data.properties.insert( ENTRY_COUNT_KEY.to_string(), Property::builtin(Value::Number(0.0)), ); data.properties.insert( LIVE_COUNT_KEY.to_string(), Property::builtin(Value::Number(0.0)), ); // size is a read-only, non-enumerable property. data.properties .insert("size".to_string(), Property::builtin(Value::Number(0.0))); gc.alloc(HeapObject::Object(data)) } /// Get the entries object GcRef from a Map/Set object. fn collection_entries(gc: &Gc, obj: &Value) -> Option { let gc_ref = obj.gc_ref()?; let heap = gc.get(gc_ref)?; if let HeapObject::Object(data) = heap { if let Some(prop) = data.properties.get(ENTRIES_KEY) { return prop.value.gc_ref(); } } None } /// Get the entry count from a Map/Set object. fn collection_entry_count(gc: &Gc, obj: &Value) -> usize { let gc_ref = match obj.gc_ref() { Some(r) => r, None => return 0, }; match gc.get(gc_ref) { Some(HeapObject::Object(data)) => data .properties .get(ENTRY_COUNT_KEY) .map(|p| p.value.to_number() as usize) .unwrap_or(0), _ => 0, } } /// Get the live count from a Map/Set object. fn collection_live_count(gc: &Gc, obj: &Value) -> usize { let gc_ref = match obj.gc_ref() { Some(r) => r, None => return 0, }; match gc.get(gc_ref) { Some(HeapObject::Object(data)) => data .properties .get(LIVE_COUNT_KEY) .map(|p| p.value.to_number() as usize) .unwrap_or(0), _ => 0, } } /// Set the entry count on a Map/Set object and update the `size` property. fn set_collection_count(gc: &mut Gc, obj: &Value, entry_count: usize, live: usize) { let gc_ref = match obj.gc_ref() { Some(r) => r, None => return, }; if let Some(HeapObject::Object(data)) = gc.get_mut(gc_ref) { data.properties.insert( ENTRY_COUNT_KEY.to_string(), Property::builtin(Value::Number(entry_count as f64)), ); data.properties.insert( LIVE_COUNT_KEY.to_string(), Property::builtin(Value::Number(live as f64)), ); data.properties.insert( "size".to_string(), Property::builtin(Value::Number(live as f64)), ); } } /// Get the key at index `i` in the entries object (returns None if deleted or missing). fn entry_key_at(gc: &Gc, entries: GcRef, i: usize) -> Option { match gc.get(entries) { Some(HeapObject::Object(data)) => { let k = format!("{i}_k"); let prop = data.properties.get(&k)?; if let Value::String(s) = &prop.value { if s == DELETED_MARKER { return None; } } Some(prop.value.clone()) } _ => None, } } /// Get the value at index `i` in the entries object. fn entry_value_at(gc: &Gc, entries: GcRef, i: usize) -> Value { match gc.get(entries) { Some(HeapObject::Object(data)) => { let v = format!("{i}_v"); data.properties .get(&v) .map(|p| p.value.clone()) .unwrap_or(Value::Undefined) } _ => Value::Undefined, } } /// Set an entry at index `i`. fn set_entry_at(gc: &mut Gc, entries: GcRef, i: usize, key: Value, value: Value) { if let Some(HeapObject::Object(data)) = gc.get_mut(entries) { data.properties .insert(format!("{i}_k"), Property::builtin(key)); data.properties .insert(format!("{i}_v"), Property::builtin(value)); } } /// Mark entry at index `i` as deleted. fn delete_entry_at(gc: &mut Gc, entries: GcRef, i: usize) { if let Some(HeapObject::Object(data)) = gc.get_mut(entries) { data.properties.insert( format!("{i}_k"), Property::builtin(Value::String(DELETED_MARKER.to_string())), ); data.properties.remove(&format!("{i}_v")); } } /// Find the index of a key in the entries, using SameValueZero. fn find_key_index(gc: &Gc, entries: GcRef, count: usize, key: &Value) -> Option { for i in 0..count { if let Some(existing) = entry_key_at(gc, entries, i) { if same_value_zero(&existing, key) { return Some(i); } } } None } // ── Map constructor & prototype ────────────────────────────── fn map_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { let proto = MAP_PROTO.with(|cell| cell.get()); let obj_ref = make_collection_obj(ctx.gc, proto); let obj = Value::Object(obj_ref); // If an iterable argument is provided, add entries from it. // We support arrays of [key, value] pairs. if let Some(arg) = args.first() { if !matches!(arg, Value::Undefined | Value::Null) { if let Some(arr_ref) = arg.gc_ref() { let len = array_length(ctx.gc, arr_ref); for i in 0..len { let pair = get_property(ctx.gc, &Value::Object(arr_ref), &i.to_string()); if let Some(pair_ref) = pair.gc_ref() { let k = get_property(ctx.gc, &Value::Object(pair_ref), "0"); let v = get_property(ctx.gc, &Value::Object(pair_ref), "1"); map_set_internal(ctx.gc, &obj, k, v); } } } } } Ok(obj) } fn map_set_internal(gc: &mut Gc, map: &Value, key: Value, value: Value) { let entries = match collection_entries(gc, map) { Some(e) => e, None => return, }; let count = collection_entry_count(gc, map); let live = collection_live_count(gc, map); // Check if key already exists. if let Some(idx) = find_key_index(gc, entries, count, &key) { set_entry_at(gc, entries, idx, key, value); return; } // Add new entry. set_entry_at(gc, entries, count, key, value); set_collection_count(gc, map, count + 1, live + 1); } fn init_map_prototype(gc: &mut Gc, proto: GcRef) { let methods: &[NativeMethod] = &[ ("set", map_proto_set), ("get", map_proto_get), ("has", map_proto_has), ("delete", map_proto_delete), ("clear", map_proto_clear), ("forEach", map_proto_for_each), ("keys", map_proto_keys), ("values", map_proto_values), ("entries", map_proto_entries), ]; for &(name, callback) in methods { let f = make_native(gc, name, callback); set_builtin_prop(gc, proto, name, Value::Function(f)); } // Map.prototype[@@iterator] — returns an iterator of [key, value] pairs. let iter_fn = make_native(gc, "[Symbol.iterator]", map_symbol_iterator); set_builtin_prop(gc, proto, "@@iterator", Value::Function(iter_fn)); } fn map_proto_set(args: &[Value], ctx: &mut NativeContext) -> Result { let key = args.first().cloned().unwrap_or(Value::Undefined); let value = args.get(1).cloned().unwrap_or(Value::Undefined); // Normalize -0 to +0 for key. let key = normalize_zero(key); map_set_internal(ctx.gc, &ctx.this, key, value); Ok(ctx.this.clone()) } fn map_proto_get(args: &[Value], ctx: &mut NativeContext) -> Result { let key = args.first().cloned().unwrap_or(Value::Undefined); let key = normalize_zero(key); let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(Value::Undefined), }; let count = collection_entry_count(ctx.gc, &ctx.this); if let Some(idx) = find_key_index(ctx.gc, entries, count, &key) { return Ok(entry_value_at(ctx.gc, entries, idx)); } Ok(Value::Undefined) } fn map_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result { let key = args.first().cloned().unwrap_or(Value::Undefined); let key = normalize_zero(key); let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(Value::Boolean(false)), }; let count = collection_entry_count(ctx.gc, &ctx.this); Ok(Value::Boolean( find_key_index(ctx.gc, entries, count, &key).is_some(), )) } fn map_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result { let key = args.first().cloned().unwrap_or(Value::Undefined); let key = normalize_zero(key); let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(Value::Boolean(false)), }; let count = collection_entry_count(ctx.gc, &ctx.this); let live = collection_live_count(ctx.gc, &ctx.this); if let Some(idx) = find_key_index(ctx.gc, entries, count, &key) { delete_entry_at(ctx.gc, entries, idx); set_collection_count(ctx.gc, &ctx.this, count, live.saturating_sub(1)); return Ok(Value::Boolean(true)); } Ok(Value::Boolean(false)) } fn map_proto_clear(_args: &[Value], ctx: &mut NativeContext) -> Result { clear_collection(ctx.gc, &ctx.this); Ok(Value::Undefined) } /// Clear all entries from a Map/Set collection. fn clear_collection(gc: &mut Gc, obj: &Value) { let new_entries = gc.alloc(HeapObject::Object(ObjectData::new())); if let Some(gc_ref) = obj.gc_ref() { if let Some(HeapObject::Object(data)) = gc.get_mut(gc_ref) { data.properties.insert( ENTRIES_KEY.to_string(), Property::builtin(Value::Object(new_entries)), ); data.properties.insert( ENTRY_COUNT_KEY.to_string(), Property::builtin(Value::Number(0.0)), ); data.properties.insert( LIVE_COUNT_KEY.to_string(), Property::builtin(Value::Number(0.0)), ); data.properties .insert("size".to_string(), Property::builtin(Value::Number(0.0))); } } } fn map_proto_for_each(args: &[Value], ctx: &mut NativeContext) -> Result { let callback = match args.first() { Some(Value::Function(r)) => *r, _ => { return Err(RuntimeError::type_error( "Map.prototype.forEach requires a function argument", )) } }; let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(Value::Undefined), }; let count = collection_entry_count(ctx.gc, &ctx.this); // Collect entries first to avoid borrow issues. let mut pairs = Vec::new(); for i in 0..count { if let Some(k) = entry_key_at(ctx.gc, entries, i) { let v = entry_value_at(ctx.gc, entries, i); pairs.push((k, v)); } } let this_val = ctx.this.clone(); for (k, v) in pairs { call_native_callback( ctx.gc, callback, &[v, k, this_val.clone()], ctx.console_output, ctx.dom_bridge, )?; } Ok(Value::Undefined) } fn map_proto_keys(args: &[Value], ctx: &mut NativeContext) -> Result { map_proto_iter(args, ctx, IterKind::Keys) } fn map_proto_values(args: &[Value], ctx: &mut NativeContext) -> Result { map_proto_iter(args, ctx, IterKind::Values) } fn map_proto_entries(args: &[Value], ctx: &mut NativeContext) -> Result { map_proto_iter(args, ctx, IterKind::Entries) } enum IterKind { Keys, Values, Entries, } fn map_proto_iter( _args: &[Value], ctx: &mut NativeContext, kind: IterKind, ) -> Result { let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(make_value_array(ctx.gc, &[])), }; let count = collection_entry_count(ctx.gc, &ctx.this); let mut items = Vec::new(); for i in 0..count { if let Some(k) = entry_key_at(ctx.gc, entries, i) { match kind { IterKind::Keys => items.push(k), IterKind::Values => { let v = entry_value_at(ctx.gc, entries, i); items.push(v); } IterKind::Entries => { let v = entry_value_at(ctx.gc, entries, i); let pair = make_value_array(ctx.gc, &[k, v]); items.push(pair); } } } } Ok(make_value_array(ctx.gc, &items)) } /// Map[@@iterator]() — wraps entries array into an iterator. fn map_symbol_iterator(_args: &[Value], ctx: &mut NativeContext) -> Result { let arr_val = map_proto_iter(_args, ctx, IterKind::Entries)?; let arr_ref = match arr_val.gc_ref() { Some(r) => r, None => return Ok(Value::Undefined), }; Ok(make_simple_iterator(ctx.gc, arr_ref, array_values_next)) } /// Set[@@iterator]() — wraps values array into an iterator. fn set_symbol_iterator(_args: &[Value], ctx: &mut NativeContext) -> Result { let arr_val = set_proto_values(_args, ctx)?; let arr_ref = match arr_val.gc_ref() { Some(r) => r, None => return Ok(Value::Undefined), }; Ok(make_simple_iterator(ctx.gc, arr_ref, array_values_next)) } /// Normalize -0 to +0 for Map/Set key equality. fn normalize_zero(val: Value) -> Value { if let Value::Number(n) = &val { if *n == 0.0 { return Value::Number(0.0); } } val } /// Helper to get a property from an object value by key (used for reading iterable pairs). fn get_property(gc: &Gc, obj: &Value, key: &str) -> Value { let gc_ref = match obj.gc_ref() { Some(r) => r, None => return Value::Undefined, }; match gc.get(gc_ref) { Some(HeapObject::Object(data)) => data .properties .get(key) .map(|p| p.value.clone()) .unwrap_or(Value::Undefined), _ => Value::Undefined, } } /// Call a native callback function (for forEach). fn call_native_callback( gc: &mut Gc, func_ref: GcRef, args: &[Value], console_output: &dyn ConsoleOutput, dom_bridge: Option<&DomBridge>, ) -> Result { match gc.get(func_ref) { Some(HeapObject::Function(fdata)) => match &fdata.kind { FunctionKind::Native(native) => { let cb = native.callback; let mut ctx = NativeContext { gc, this: Value::Undefined, console_output, dom_bridge, }; cb(args, &mut ctx) } FunctionKind::Bytecode(_) => Err(RuntimeError::type_error( "bytecode callbacks in forEach not yet supported", )), }, _ => Err(RuntimeError::type_error("not a function")), } } // ── Set constructor & prototype ────────────────────────────── fn set_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { let proto = SET_PROTO.with(|cell| cell.get()); let obj_ref = make_collection_obj(ctx.gc, proto); let obj = Value::Object(obj_ref); // If an iterable argument is provided, add values from it. if let Some(arg) = args.first() { if !matches!(arg, Value::Undefined | Value::Null) { if let Some(arr_ref) = arg.gc_ref() { let len = array_length(ctx.gc, arr_ref); for i in 0..len { let v = get_property(ctx.gc, &Value::Object(arr_ref), &i.to_string()); set_add_internal(ctx.gc, &obj, v); } } } } Ok(obj) } fn set_add_internal(gc: &mut Gc, set: &Value, value: Value) { let entries = match collection_entries(gc, set) { Some(e) => e, None => return, }; let count = collection_entry_count(gc, set); let live = collection_live_count(gc, set); // Check if value already exists. if find_key_index(gc, entries, count, &value).is_some() { return; } // Add new entry (value stored as key, value slot unused). set_entry_at(gc, entries, count, value, Value::Undefined); set_collection_count(gc, set, count + 1, live + 1); } fn init_set_prototype(gc: &mut Gc, proto: GcRef) { let methods: &[NativeMethod] = &[ ("add", set_proto_add), ("has", set_proto_has), ("delete", set_proto_delete), ("clear", set_proto_clear), ("forEach", set_proto_for_each), ("keys", set_proto_keys), ("values", set_proto_values), ("entries", set_proto_entries), ]; for &(name, callback) in methods { let f = make_native(gc, name, callback); set_builtin_prop(gc, proto, name, Value::Function(f)); } // Set.prototype[@@iterator] — returns an iterator of values. let iter_fn = make_native(gc, "[Symbol.iterator]", set_symbol_iterator); set_builtin_prop(gc, proto, "@@iterator", Value::Function(iter_fn)); } fn set_proto_add(args: &[Value], ctx: &mut NativeContext) -> Result { let value = args.first().cloned().unwrap_or(Value::Undefined); let value = normalize_zero(value); set_add_internal(ctx.gc, &ctx.this, value); Ok(ctx.this.clone()) } fn set_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result { let value = args.first().cloned().unwrap_or(Value::Undefined); let value = normalize_zero(value); let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(Value::Boolean(false)), }; let count = collection_entry_count(ctx.gc, &ctx.this); Ok(Value::Boolean( find_key_index(ctx.gc, entries, count, &value).is_some(), )) } fn set_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result { let value = args.first().cloned().unwrap_or(Value::Undefined); let value = normalize_zero(value); let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(Value::Boolean(false)), }; let count = collection_entry_count(ctx.gc, &ctx.this); let live = collection_live_count(ctx.gc, &ctx.this); if let Some(idx) = find_key_index(ctx.gc, entries, count, &value) { delete_entry_at(ctx.gc, entries, idx); set_collection_count(ctx.gc, &ctx.this, count, live.saturating_sub(1)); return Ok(Value::Boolean(true)); } Ok(Value::Boolean(false)) } fn set_proto_clear(_args: &[Value], ctx: &mut NativeContext) -> Result { clear_collection(ctx.gc, &ctx.this); Ok(Value::Undefined) } fn set_proto_for_each(args: &[Value], ctx: &mut NativeContext) -> Result { let callback = match args.first() { Some(Value::Function(r)) => *r, _ => { return Err(RuntimeError::type_error( "Set.prototype.forEach requires a function argument", )) } }; let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(Value::Undefined), }; let count = collection_entry_count(ctx.gc, &ctx.this); let mut values = Vec::new(); for i in 0..count { if let Some(k) = entry_key_at(ctx.gc, entries, i) { values.push(k); } } let this_val = ctx.this.clone(); for v in values { call_native_callback( ctx.gc, callback, &[v.clone(), v, this_val.clone()], ctx.console_output, ctx.dom_bridge, )?; } Ok(Value::Undefined) } fn set_proto_keys(_args: &[Value], ctx: &mut NativeContext) -> Result { set_proto_values(_args, ctx) } fn set_proto_values(_args: &[Value], ctx: &mut NativeContext) -> Result { let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(make_value_array(ctx.gc, &[])), }; let count = collection_entry_count(ctx.gc, &ctx.this); let mut items = Vec::new(); for i in 0..count { if let Some(k) = entry_key_at(ctx.gc, entries, i) { items.push(k); } } Ok(make_value_array(ctx.gc, &items)) } fn set_proto_entries(_args: &[Value], ctx: &mut NativeContext) -> Result { let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(make_value_array(ctx.gc, &[])), }; let count = collection_entry_count(ctx.gc, &ctx.this); let mut items = Vec::new(); for i in 0..count { if let Some(k) = entry_key_at(ctx.gc, entries, i) { let pair = make_value_array(ctx.gc, &[k.clone(), k]); items.push(pair); } } Ok(make_value_array(ctx.gc, &items)) } // ── WeakMap constructor & prototype ────────────────────────── fn weakmap_constructor(_args: &[Value], ctx: &mut NativeContext) -> Result { let proto = WEAKMAP_PROTO.with(|cell| cell.get()); let obj_ref = make_collection_obj(ctx.gc, proto); Ok(Value::Object(obj_ref)) } fn is_object_value(val: &Value) -> bool { matches!(val, Value::Object(_) | Value::Function(_)) } fn init_weakmap_prototype(gc: &mut Gc, proto: GcRef) { let methods: &[NativeMethod] = &[ ("set", weakmap_proto_set), ("get", weakmap_proto_get), ("has", weakmap_proto_has), ("delete", weakmap_proto_delete), ]; for &(name, callback) in methods { let f = make_native(gc, name, callback); set_builtin_prop(gc, proto, name, Value::Function(f)); } } fn weakmap_proto_set(args: &[Value], ctx: &mut NativeContext) -> Result { let key = args.first().cloned().unwrap_or(Value::Undefined); if !is_object_value(&key) { return Err(RuntimeError::type_error("WeakMap key must be an object")); } let value = args.get(1).cloned().unwrap_or(Value::Undefined); map_set_internal(ctx.gc, &ctx.this, key, value); Ok(ctx.this.clone()) } fn weakmap_proto_get(args: &[Value], ctx: &mut NativeContext) -> Result { let key = args.first().cloned().unwrap_or(Value::Undefined); if !is_object_value(&key) { return Ok(Value::Undefined); } let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(Value::Undefined), }; let count = collection_entry_count(ctx.gc, &ctx.this); if let Some(idx) = find_key_index(ctx.gc, entries, count, &key) { return Ok(entry_value_at(ctx.gc, entries, idx)); } Ok(Value::Undefined) } fn weakmap_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result { let key = args.first().cloned().unwrap_or(Value::Undefined); if !is_object_value(&key) { return Ok(Value::Boolean(false)); } let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(Value::Boolean(false)), }; let count = collection_entry_count(ctx.gc, &ctx.this); Ok(Value::Boolean( find_key_index(ctx.gc, entries, count, &key).is_some(), )) } fn weakmap_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result { let key = args.first().cloned().unwrap_or(Value::Undefined); if !is_object_value(&key) { return Ok(Value::Boolean(false)); } let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(Value::Boolean(false)), }; let count = collection_entry_count(ctx.gc, &ctx.this); let live = collection_live_count(ctx.gc, &ctx.this); if let Some(idx) = find_key_index(ctx.gc, entries, count, &key) { delete_entry_at(ctx.gc, entries, idx); set_collection_count(ctx.gc, &ctx.this, count, live.saturating_sub(1)); return Ok(Value::Boolean(true)); } Ok(Value::Boolean(false)) } // ── WeakSet constructor & prototype ────────────────────────── fn weakset_constructor(_args: &[Value], ctx: &mut NativeContext) -> Result { let proto = WEAKSET_PROTO.with(|cell| cell.get()); let obj_ref = make_collection_obj(ctx.gc, proto); Ok(Value::Object(obj_ref)) } fn init_weakset_prototype(gc: &mut Gc, proto: GcRef) { let methods: &[NativeMethod] = &[ ("add", weakset_proto_add), ("has", weakset_proto_has), ("delete", weakset_proto_delete), ]; for &(name, callback) in methods { let f = make_native(gc, name, callback); set_builtin_prop(gc, proto, name, Value::Function(f)); } } fn weakset_proto_add(args: &[Value], ctx: &mut NativeContext) -> Result { let value = args.first().cloned().unwrap_or(Value::Undefined); if !is_object_value(&value) { return Err(RuntimeError::type_error("WeakSet value must be an object")); } set_add_internal(ctx.gc, &ctx.this, value); Ok(ctx.this.clone()) } fn weakset_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result { let value = args.first().cloned().unwrap_or(Value::Undefined); if !is_object_value(&value) { return Ok(Value::Boolean(false)); } let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(Value::Boolean(false)), }; let count = collection_entry_count(ctx.gc, &ctx.this); Ok(Value::Boolean( find_key_index(ctx.gc, entries, count, &value).is_some(), )) } fn weakset_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result { let value = args.first().cloned().unwrap_or(Value::Undefined); if !is_object_value(&value) { return Ok(Value::Boolean(false)); } let entries = match collection_entries(ctx.gc, &ctx.this) { Some(e) => e, None => return Ok(Value::Boolean(false)), }; let count = collection_entry_count(ctx.gc, &ctx.this); let live = collection_live_count(ctx.gc, &ctx.this); if let Some(idx) = find_key_index(ctx.gc, entries, count, &value) { delete_entry_at(ctx.gc, entries, idx); set_collection_count(ctx.gc, &ctx.this, count, live.saturating_sub(1)); return Ok(Value::Boolean(true)); } Ok(Value::Boolean(false)) } // ── Promise built-in ───────────────────────────────────────── /// Promise states. const PROMISE_PENDING: f64 = 0.0; pub const PROMISE_FULFILLED: f64 = 1.0; pub const PROMISE_REJECTED: f64 = 2.0; /// Hidden property keys for Promise objects. const PROMISE_STATE_KEY: &str = "__promise_state__"; pub const PROMISE_RESULT_KEY: &str = "__promise_result__"; const PROMISE_REACTIONS_KEY: &str = "__promise_reactions__"; const PROMISE_REACTION_COUNT_KEY: &str = "__promise_reaction_count__"; thread_local! { static PROMISE_PROTO: std::cell::Cell> = const { std::cell::Cell::new(None) }; static MICROTASK_QUEUE: RefCell> = const { RefCell::new(Vec::new()) }; } /// A microtask queued by a promise settlement. pub struct Microtask { /// The handler to call (onFulfilled or onRejected). None means identity/thrower. pub handler: Option, /// The value to pass to the handler. pub value: Value, /// The chained promise to resolve/reject with the handler's result. pub chained_promise: Option, /// Whether this is a fulfillment reaction (true) or rejection (false). pub is_fulfillment: bool, } /// Take all pending microtasks from the queue (called by the VM). pub fn take_microtasks() -> Vec { MICROTASK_QUEUE.with(|q| std::mem::take(&mut *q.borrow_mut())) } fn enqueue_microtask(task: Microtask) { MICROTASK_QUEUE.with(|q| q.borrow_mut().push(task)); } /// Public wrappers for functions used by the VM's microtask drain. pub fn promise_get_prop_pub(gc: &Gc, promise: GcRef, key: &str) -> Value { promise_get_prop(gc, promise, key) } pub fn promise_state_pub(gc: &Gc, promise: GcRef) -> f64 { promise_state(gc, promise) } pub fn is_promise_pub(gc: &Gc, value: &Value) -> bool { is_promise(gc, value) } pub fn chain_promise_pub(gc: &mut Gc, source: GcRef, target: GcRef) { chain_promise(gc, source, target) } pub fn create_promise_object_pub(gc: &mut Gc) -> GcRef { create_promise_object(gc) } pub fn enqueue_microtask_pub(task: Microtask) { enqueue_microtask(task); } pub fn add_reaction_pub( gc: &mut Gc, promise: GcRef, on_fulfilled: Value, on_rejected: Value, ) -> GcRef { add_reaction(gc, promise, on_fulfilled, on_rejected) } /// Initialize Promise.prototype in a standalone GC (for unit tests that /// create promise objects without a full VM). pub fn init_promise_proto_for_test(gc: &mut Gc) { let proto_data = ObjectData::new(); let promise_proto = gc.alloc(HeapObject::Object(proto_data)); init_promise_prototype(gc, promise_proto); PROMISE_PROTO.with(|cell| cell.set(Some(promise_proto))); } fn init_promise_builtins(vm: &mut Vm) { // Create Promise.prototype (inherits from Object.prototype). let mut proto_data = ObjectData::new(); if let Some(proto) = vm.object_prototype { proto_data.prototype = Some(proto); } let promise_proto = vm.gc.alloc(HeapObject::Object(proto_data)); init_promise_prototype(&mut vm.gc, promise_proto); PROMISE_PROTO.with(|cell| cell.set(Some(promise_proto))); vm.promise_prototype = Some(promise_proto); // Register native helper functions used by the JS preamble. let create_fn = make_native(&mut vm.gc, "__Promise_create", promise_native_create); vm.set_global("__Promise_create", Value::Function(create_fn)); let resolve_fn = make_native(&mut vm.gc, "__Promise_resolve", promise_native_resolve); vm.set_global("__Promise_resolve", Value::Function(resolve_fn)); let reject_fn = make_native(&mut vm.gc, "__Promise_reject", promise_native_reject); vm.set_global("__Promise_reject", Value::Function(reject_fn)); // Register Promise.resolve and Promise.reject as standalone globals // that the JS preamble will attach to the Promise constructor. let static_resolve = make_native(&mut vm.gc, "resolve", promise_static_resolve); vm.set_global("__Promise_static_resolve", Value::Function(static_resolve)); let static_reject = make_native(&mut vm.gc, "reject", promise_static_reject); vm.set_global("__Promise_static_reject", Value::Function(static_reject)); let static_all = make_native(&mut vm.gc, "all", promise_static_all); vm.set_global("__Promise_static_all", Value::Function(static_all)); let static_race = make_native(&mut vm.gc, "race", promise_static_race); vm.set_global("__Promise_static_race", Value::Function(static_race)); let static_all_settled = make_native(&mut vm.gc, "allSettled", promise_static_all_settled); vm.set_global( "__Promise_static_allSettled", Value::Function(static_all_settled), ); let static_any = make_native(&mut vm.gc, "any", promise_static_any); vm.set_global("__Promise_static_any", Value::Function(static_any)); } fn init_promise_prototype(gc: &mut Gc, proto: GcRef) { let methods: &[NativeMethod] = &[ ("then", promise_proto_then), ("catch", promise_proto_catch), ("finally", promise_proto_finally), ]; for &(name, callback) in methods { let f = make_native(gc, name, callback); set_builtin_prop(gc, proto, name, Value::Function(f)); } } /// Create a new pending promise object. fn create_promise_object(gc: &mut Gc) -> GcRef { let reactions = gc.alloc(HeapObject::Object(ObjectData::new())); let proto = PROMISE_PROTO.with(|cell| cell.get()); let mut data = ObjectData::new(); if let Some(p) = proto { data.prototype = Some(p); } data.properties.insert( PROMISE_STATE_KEY.to_string(), Property::builtin(Value::Number(PROMISE_PENDING)), ); data.properties.insert( PROMISE_RESULT_KEY.to_string(), Property::builtin(Value::Undefined), ); data.properties.insert( PROMISE_REACTIONS_KEY.to_string(), Property::builtin(Value::Object(reactions)), ); data.properties.insert( PROMISE_REACTION_COUNT_KEY.to_string(), Property::builtin(Value::Number(0.0)), ); gc.alloc(HeapObject::Object(data)) } /// Get a hidden property from a promise object. fn promise_get_prop(gc: &Gc, promise: GcRef, key: &str) -> Value { match gc.get(promise) { Some(HeapObject::Object(data)) => data .properties .get(key) .map(|p| p.value.clone()) .unwrap_or(Value::Undefined), _ => Value::Undefined, } } /// Set a hidden property on a promise object. fn promise_set_prop(gc: &mut Gc, promise: GcRef, key: &str, value: Value) { if let Some(HeapObject::Object(data)) = gc.get_mut(promise) { data.properties .insert(key.to_string(), Property::builtin(value)); } } /// Get the state of a promise (PROMISE_PENDING/FULFILLED/REJECTED). fn promise_state(gc: &Gc, promise: GcRef) -> f64 { match promise_get_prop(gc, promise, PROMISE_STATE_KEY) { Value::Number(n) => n, _ => PROMISE_PENDING, } } /// Resolve a pending promise with a value. pub fn resolve_promise_internal(gc: &mut Gc, promise: GcRef, value: Value) { if promise_state(gc, promise) != PROMISE_PENDING { return; // Already settled. } promise_set_prop( gc, promise, PROMISE_STATE_KEY, Value::Number(PROMISE_FULFILLED), ); promise_set_prop(gc, promise, PROMISE_RESULT_KEY, value.clone()); trigger_reactions(gc, promise, value, true); } /// Reject a pending promise with a reason. pub fn reject_promise_internal(gc: &mut Gc, promise: GcRef, reason: Value) { if promise_state(gc, promise) != PROMISE_PENDING { return; // Already settled. } promise_set_prop( gc, promise, PROMISE_STATE_KEY, Value::Number(PROMISE_REJECTED), ); promise_set_prop(gc, promise, PROMISE_RESULT_KEY, reason.clone()); trigger_reactions(gc, promise, reason, false); } /// Enqueue microtasks for all registered reactions on a promise. fn trigger_reactions(gc: &mut Gc, promise: GcRef, value: Value, fulfilled: bool) { let reactions_ref = match promise_get_prop(gc, promise, PROMISE_REACTIONS_KEY) { Value::Object(r) => r, _ => return, }; let count = match promise_get_prop(gc, promise, PROMISE_REACTION_COUNT_KEY) { Value::Number(n) => n as usize, _ => 0, }; // Collect reactions before mutating. let mut reactions = Vec::new(); for i in 0..count { let fulfill_key = format!("{i}_fulfill"); let reject_key = format!("{i}_reject"); let promise_key = format!("{i}_promise"); let on_fulfilled = match gc.get(reactions_ref) { Some(HeapObject::Object(data)) => data .properties .get(&fulfill_key) .map(|p| p.value.clone()) .unwrap_or(Value::Undefined), _ => Value::Undefined, }; let on_rejected = match gc.get(reactions_ref) { Some(HeapObject::Object(data)) => data .properties .get(&reject_key) .map(|p| p.value.clone()) .unwrap_or(Value::Undefined), _ => Value::Undefined, }; let chained = match gc.get(reactions_ref) { Some(HeapObject::Object(data)) => data .properties .get(&promise_key) .and_then(|p| p.value.gc_ref()), _ => None, }; let handler = if fulfilled { on_fulfilled } else { on_rejected }; let handler_ref = match &handler { Value::Function(r) => Some(*r), _ => None, }; reactions.push(Microtask { handler: handler_ref, value: value.clone(), chained_promise: chained, is_fulfillment: fulfilled, }); } for task in reactions { enqueue_microtask(task); } } /// Add a reaction to a promise. Returns the chained promise GcRef. fn add_reaction( gc: &mut Gc, promise: GcRef, on_fulfilled: Value, on_rejected: Value, ) -> GcRef { let chained = create_promise_object(gc); let reactions_ref = match promise_get_prop(gc, promise, PROMISE_REACTIONS_KEY) { Value::Object(r) => r, _ => return chained, }; let count = match promise_get_prop(gc, promise, PROMISE_REACTION_COUNT_KEY) { Value::Number(n) => n as usize, _ => 0, }; if let Some(HeapObject::Object(data)) = gc.get_mut(reactions_ref) { data.properties .insert(format!("{count}_fulfill"), Property::builtin(on_fulfilled)); data.properties .insert(format!("{count}_reject"), Property::builtin(on_rejected)); data.properties.insert( format!("{count}_promise"), Property::builtin(Value::Object(chained)), ); } promise_set_prop( gc, promise, PROMISE_REACTION_COUNT_KEY, Value::Number((count + 1) as f64), ); chained } // ── Promise native helpers (called from JS preamble) ───────── /// `__Promise_create()` — create a new pending promise object. fn promise_native_create(_args: &[Value], ctx: &mut NativeContext) -> Result { let promise = create_promise_object(ctx.gc); Ok(Value::Object(promise)) } /// `__Promise_resolve(promise, value)` — resolve a pending promise. fn promise_native_resolve(args: &[Value], ctx: &mut NativeContext) -> Result { let promise_ref = args.first().and_then(|v| v.gc_ref()).ok_or_else(|| { RuntimeError::type_error("__Promise_resolve: first arg must be a promise") })?; let value = args.get(1).cloned().unwrap_or(Value::Undefined); // If value is a thenable (promise), chain it. if is_promise(ctx.gc, &value) { let value_ref = value.gc_ref().unwrap(); let state = promise_state(ctx.gc, value_ref); if state == PROMISE_FULFILLED { let result = promise_get_prop(ctx.gc, value_ref, PROMISE_RESULT_KEY); resolve_promise_internal(ctx.gc, promise_ref, result); } else if state == PROMISE_REJECTED { let reason = promise_get_prop(ctx.gc, value_ref, PROMISE_RESULT_KEY); reject_promise_internal(ctx.gc, promise_ref, reason); } else { // Pending thenable: chain so that when value_ref settles, // promise_ref settles the same way. chain_promise(ctx.gc, value_ref, promise_ref); } } else { resolve_promise_internal(ctx.gc, promise_ref, value); } Ok(Value::Undefined) } /// Chain a source promise to a target: when source settles, settle target the same way. fn chain_promise(gc: &mut Gc, source: GcRef, target: GcRef) { let reactions_ref = match promise_get_prop(gc, source, PROMISE_REACTIONS_KEY) { Value::Object(r) => r, _ => return, }; let count = match promise_get_prop(gc, source, PROMISE_REACTION_COUNT_KEY) { Value::Number(n) => n as usize, _ => 0, }; // Store the target promise directly — the microtask drain handles identity propagation. if let Some(HeapObject::Object(data)) = gc.get_mut(reactions_ref) { data.properties.insert( format!("{count}_fulfill"), Property::builtin(Value::Undefined), ); data.properties.insert( format!("{count}_reject"), Property::builtin(Value::Undefined), ); data.properties.insert( format!("{count}_promise"), Property::builtin(Value::Object(target)), ); } promise_set_prop( gc, source, PROMISE_REACTION_COUNT_KEY, Value::Number((count + 1) as f64), ); } /// Check if a value is a promise (has __promise_state__ property). fn is_promise(gc: &Gc, value: &Value) -> bool { let gc_ref = match value.gc_ref() { Some(r) => r, None => return false, }; match gc.get(gc_ref) { Some(HeapObject::Object(data)) => data.properties.contains_key(PROMISE_STATE_KEY), _ => false, } } /// `__Promise_reject(promise, reason)` — reject a pending promise. fn promise_native_reject(args: &[Value], ctx: &mut NativeContext) -> Result { let promise_ref = args .first() .and_then(|v| v.gc_ref()) .ok_or_else(|| RuntimeError::type_error("__Promise_reject: first arg must be a promise"))?; let reason = args.get(1).cloned().unwrap_or(Value::Undefined); reject_promise_internal(ctx.gc, promise_ref, reason); Ok(Value::Undefined) } // ── Promise.prototype.then / catch / finally ───────────────── fn promise_proto_then(args: &[Value], ctx: &mut NativeContext) -> Result { let promise_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Err(RuntimeError::type_error("then called on non-object")), }; let on_fulfilled = args.first().cloned().unwrap_or(Value::Undefined); let on_rejected = args.get(1).cloned().unwrap_or(Value::Undefined); let state = promise_state(ctx.gc, promise_ref); if state == PROMISE_PENDING { // Register reaction for later. let chained = add_reaction(ctx.gc, promise_ref, on_fulfilled, on_rejected); return Ok(Value::Object(chained)); } // Already settled — enqueue microtask immediately. let result = promise_get_prop(ctx.gc, promise_ref, PROMISE_RESULT_KEY); let chained = create_promise_object(ctx.gc); let fulfilled = state == PROMISE_FULFILLED; let handler = if fulfilled { &on_fulfilled } else { &on_rejected }; let handler_ref = match handler { Value::Function(r) => Some(*r), _ => None, }; enqueue_microtask(Microtask { handler: handler_ref, value: result, chained_promise: Some(chained), is_fulfillment: fulfilled, }); Ok(Value::Object(chained)) } fn promise_proto_catch(args: &[Value], ctx: &mut NativeContext) -> Result { let on_rejected = args.first().cloned().unwrap_or(Value::Undefined); promise_proto_then(&[Value::Undefined, on_rejected], ctx) } fn promise_proto_finally(args: &[Value], ctx: &mut NativeContext) -> Result { let promise_ref = match ctx.this.gc_ref() { Some(r) => r, None => return Err(RuntimeError::type_error("finally called on non-object")), }; let on_finally = args.first().cloned().unwrap_or(Value::Undefined); let state = promise_state(ctx.gc, promise_ref); if state == PROMISE_PENDING { // Register reaction: finally handler doesn't receive value, just runs. let chained = add_reaction(ctx.gc, promise_ref, on_finally.clone(), on_finally); // Mark the chained promise reactions as "finally" so drain can handle them. promise_set_prop(ctx.gc, chained, "__finally__", Value::Boolean(true)); // Store parent result for propagation. promise_set_prop( ctx.gc, chained, "__finally_parent__", Value::Object(promise_ref), ); return Ok(Value::Object(chained)); } // Already settled. let _result = promise_get_prop(ctx.gc, promise_ref, PROMISE_RESULT_KEY); let chained = create_promise_object(ctx.gc); let handler_ref = match &on_finally { Value::Function(r) => Some(*r), _ => None, }; // For finally, we enqueue the callback but propagate the original result. promise_set_prop(ctx.gc, chained, "__finally__", Value::Boolean(true)); promise_set_prop( ctx.gc, chained, "__finally_parent__", Value::Object(promise_ref), ); enqueue_microtask(Microtask { handler: handler_ref, value: Value::Undefined, // finally callback gets no arguments chained_promise: Some(chained), is_fulfillment: state == PROMISE_FULFILLED, }); Ok(Value::Object(chained)) } // ── Promise static methods ─────────────────────────────────── fn promise_static_resolve(args: &[Value], ctx: &mut NativeContext) -> Result { let value = args.first().cloned().unwrap_or(Value::Undefined); // If already a promise, return it. if is_promise(ctx.gc, &value) { return Ok(value); } let promise = create_promise_object(ctx.gc); resolve_promise_internal(ctx.gc, promise, value); Ok(Value::Object(promise)) } fn promise_static_reject(args: &[Value], ctx: &mut NativeContext) -> Result { let reason = args.first().cloned().unwrap_or(Value::Undefined); let promise = create_promise_object(ctx.gc); reject_promise_internal(ctx.gc, promise, reason); Ok(Value::Object(promise)) } fn promise_static_all(args: &[Value], ctx: &mut NativeContext) -> Result { let iterable = args.first().cloned().unwrap_or(Value::Undefined); let arr_ref = match iterable.gc_ref() { Some(r) => r, None => { let p = create_promise_object(ctx.gc); reject_promise_internal( ctx.gc, p, Value::String("Promise.all requires an iterable".to_string()), ); return Ok(Value::Object(p)); } }; let len = array_length(ctx.gc, arr_ref); let result_promise = create_promise_object(ctx.gc); if len == 0 { let empty = make_value_array(ctx.gc, &[]); resolve_promise_internal(ctx.gc, result_promise, empty); return Ok(Value::Object(result_promise)); } // Create a results array and a counter object. let results = make_value_array(ctx.gc, &vec![Value::Undefined; len]); let results_ref = results.gc_ref().unwrap(); // We track remaining count and results in hidden props on result_promise. promise_set_prop( ctx.gc, result_promise, "__all_remaining__", Value::Number(len as f64), ); promise_set_prop(ctx.gc, result_promise, "__all_results__", results.clone()); for i in 0..len { let item = array_get(ctx.gc, arr_ref, i); if is_promise(ctx.gc, &item) { let item_ref = item.gc_ref().unwrap(); let state = promise_state(ctx.gc, item_ref); if state == PROMISE_FULFILLED { let val = promise_get_prop(ctx.gc, item_ref, PROMISE_RESULT_KEY); array_set(ctx.gc, results_ref, i, val); promise_all_decrement(ctx.gc, result_promise); } else if state == PROMISE_REJECTED { let reason = promise_get_prop(ctx.gc, item_ref, PROMISE_RESULT_KEY); reject_promise_internal(ctx.gc, result_promise, reason); return Ok(Value::Object(result_promise)); } else { // Pending: we need to register a reaction. Store index info. promise_set_prop( ctx.gc, item_ref, &format!("__all_target_{i}__"), Value::Object(result_promise), ); // Add a reaction — the microtask drain will handle all-tracking. let chained = add_reaction(ctx.gc, item_ref, Value::Undefined, Value::Undefined); promise_set_prop(ctx.gc, chained, "__all_index__", Value::Number(i as f64)); promise_set_prop( ctx.gc, chained, "__all_target__", Value::Object(result_promise), ); } } else { // Non-promise value: treat as immediately resolved. array_set(ctx.gc, results_ref, i, item); promise_all_decrement(ctx.gc, result_promise); } } Ok(Value::Object(result_promise)) } fn promise_all_decrement(gc: &mut Gc, result_promise: GcRef) { let remaining = match promise_get_prop(gc, result_promise, "__all_remaining__") { Value::Number(n) => n as usize, _ => return, }; let new_remaining = remaining.saturating_sub(1); promise_set_prop( gc, result_promise, "__all_remaining__", Value::Number(new_remaining as f64), ); if new_remaining == 0 { let results = promise_get_prop(gc, result_promise, "__all_results__"); resolve_promise_internal(gc, result_promise, results); } } fn promise_static_race(args: &[Value], ctx: &mut NativeContext) -> Result { let iterable = args.first().cloned().unwrap_or(Value::Undefined); let arr_ref = match iterable.gc_ref() { Some(r) => r, None => { let p = create_promise_object(ctx.gc); reject_promise_internal( ctx.gc, p, Value::String("Promise.race requires an iterable".to_string()), ); return Ok(Value::Object(p)); } }; let len = array_length(ctx.gc, arr_ref); let result_promise = create_promise_object(ctx.gc); for i in 0..len { let item = array_get(ctx.gc, arr_ref, i); if is_promise(ctx.gc, &item) { let item_ref = item.gc_ref().unwrap(); let state = promise_state(ctx.gc, item_ref); if state == PROMISE_FULFILLED { let val = promise_get_prop(ctx.gc, item_ref, PROMISE_RESULT_KEY); resolve_promise_internal(ctx.gc, result_promise, val); return Ok(Value::Object(result_promise)); } else if state == PROMISE_REJECTED { let reason = promise_get_prop(ctx.gc, item_ref, PROMISE_RESULT_KEY); reject_promise_internal(ctx.gc, result_promise, reason); return Ok(Value::Object(result_promise)); } else { chain_promise(ctx.gc, item_ref, result_promise); } } else { resolve_promise_internal(ctx.gc, result_promise, item); return Ok(Value::Object(result_promise)); } } Ok(Value::Object(result_promise)) } fn promise_static_all_settled( args: &[Value], ctx: &mut NativeContext, ) -> Result { let iterable = args.first().cloned().unwrap_or(Value::Undefined); let arr_ref = match iterable.gc_ref() { Some(r) => r, None => { let p = create_promise_object(ctx.gc); reject_promise_internal( ctx.gc, p, Value::String("Promise.allSettled requires an iterable".to_string()), ); return Ok(Value::Object(p)); } }; let len = array_length(ctx.gc, arr_ref); let result_promise = create_promise_object(ctx.gc); if len == 0 { let empty = make_value_array(ctx.gc, &[]); resolve_promise_internal(ctx.gc, result_promise, empty); return Ok(Value::Object(result_promise)); } let results = make_value_array(ctx.gc, &vec![Value::Undefined; len]); let results_ref = results.gc_ref().unwrap(); promise_set_prop( ctx.gc, result_promise, "__all_remaining__", Value::Number(len as f64), ); promise_set_prop(ctx.gc, result_promise, "__all_results__", results); for i in 0..len { let item = array_get(ctx.gc, arr_ref, i); if is_promise(ctx.gc, &item) { let item_ref = item.gc_ref().unwrap(); let state = promise_state(ctx.gc, item_ref); if state != PROMISE_PENDING { let val = promise_get_prop(ctx.gc, item_ref, PROMISE_RESULT_KEY); let status_str = if state == PROMISE_FULFILLED { "fulfilled" } else { "rejected" }; let entry = make_settled_entry(ctx.gc, status_str, &val, state == PROMISE_FULFILLED); array_set(ctx.gc, results_ref, i, entry); promise_all_decrement(ctx.gc, result_promise); } else { chain_promise(ctx.gc, item_ref, result_promise); } } else { let entry = make_settled_entry(ctx.gc, "fulfilled", &item, true); array_set(ctx.gc, results_ref, i, entry); promise_all_decrement(ctx.gc, result_promise); } } Ok(Value::Object(result_promise)) } fn make_settled_entry( gc: &mut Gc, status: &str, value: &Value, is_fulfilled: bool, ) -> Value { let mut data = ObjectData::new(); data.properties.insert( "status".to_string(), Property::data(Value::String(status.to_string())), ); if is_fulfilled { data.properties .insert("value".to_string(), Property::data(value.clone())); } else { data.properties .insert("reason".to_string(), Property::data(value.clone())); } Value::Object(gc.alloc(HeapObject::Object(data))) } fn promise_static_any(args: &[Value], ctx: &mut NativeContext) -> Result { let iterable = args.first().cloned().unwrap_or(Value::Undefined); let arr_ref = match iterable.gc_ref() { Some(r) => r, None => { let p = create_promise_object(ctx.gc); reject_promise_internal( ctx.gc, p, Value::String("Promise.any requires an iterable".to_string()), ); return Ok(Value::Object(p)); } }; let len = array_length(ctx.gc, arr_ref); let result_promise = create_promise_object(ctx.gc); if len == 0 { // Reject with AggregateError. let err = Value::String("All promises were rejected".to_string()); reject_promise_internal(ctx.gc, result_promise, err); return Ok(Value::Object(result_promise)); } let errors = make_value_array(ctx.gc, &vec![Value::Undefined; len]); let errors_ref = errors.gc_ref().unwrap(); promise_set_prop( ctx.gc, result_promise, "__any_remaining__", Value::Number(len as f64), ); promise_set_prop(ctx.gc, result_promise, "__any_errors__", errors); for i in 0..len { let item = array_get(ctx.gc, arr_ref, i); if is_promise(ctx.gc, &item) { let item_ref = item.gc_ref().unwrap(); let state = promise_state(ctx.gc, item_ref); if state == PROMISE_FULFILLED { let val = promise_get_prop(ctx.gc, item_ref, PROMISE_RESULT_KEY); resolve_promise_internal(ctx.gc, result_promise, val); return Ok(Value::Object(result_promise)); } else if state == PROMISE_REJECTED { let reason = promise_get_prop(ctx.gc, item_ref, PROMISE_RESULT_KEY); array_set(ctx.gc, errors_ref, i, reason); promise_any_decrement(ctx.gc, result_promise); } else { chain_promise(ctx.gc, item_ref, result_promise); } } else { resolve_promise_internal(ctx.gc, result_promise, item); return Ok(Value::Object(result_promise)); } } Ok(Value::Object(result_promise)) } fn promise_any_decrement(gc: &mut Gc, result_promise: GcRef) { let remaining = match promise_get_prop(gc, result_promise, "__any_remaining__") { Value::Number(n) => n as usize, _ => return, }; let new_remaining = remaining.saturating_sub(1); promise_set_prop( gc, result_promise, "__any_remaining__", Value::Number(new_remaining as f64), ); if new_remaining == 0 { let err = Value::String("All promises were rejected".to_string()); reject_promise_internal(gc, result_promise, err); } } // ── JSON object ────────────────────────────────────────────── fn init_json_object(vm: &mut Vm) { let mut data = ObjectData::new(); if let Some(proto) = vm.object_prototype { data.prototype = Some(proto); } let json_ref = vm.gc.alloc(HeapObject::Object(data)); let parse_fn = make_native(&mut vm.gc, "parse", json_parse); set_builtin_prop(&mut vm.gc, json_ref, "parse", Value::Function(parse_fn)); let stringify_fn = make_native(&mut vm.gc, "stringify", json_stringify); set_builtin_prop( &mut vm.gc, json_ref, "stringify", Value::Function(stringify_fn), ); vm.set_global("JSON", Value::Object(json_ref)); } // ── JSON.parse ─────────────────────────────────────────────── /// Minimal JSON tokenizer. #[derive(Debug, Clone, PartialEq)] enum JsonToken { LBrace, RBrace, LBracket, RBracket, Colon, Comma, String(String), Number(f64), True, False, Null, } struct JsonLexer<'a> { chars: &'a [u8], pos: usize, } impl<'a> JsonLexer<'a> { fn new(input: &'a str) -> Self { Self { chars: input.as_bytes(), pos: 0, } } fn skip_ws(&mut self) { while self.pos < self.chars.len() { match self.chars[self.pos] { b' ' | b'\t' | b'\n' | b'\r' => self.pos += 1, _ => break, } } } fn next_token(&mut self) -> Result, RuntimeError> { self.skip_ws(); if self.pos >= self.chars.len() { return Ok(None); } let ch = self.chars[self.pos]; match ch { b'{' => { self.pos += 1; Ok(Some(JsonToken::LBrace)) } b'}' => { self.pos += 1; Ok(Some(JsonToken::RBrace)) } b'[' => { self.pos += 1; Ok(Some(JsonToken::LBracket)) } b']' => { self.pos += 1; Ok(Some(JsonToken::RBracket)) } b':' => { self.pos += 1; Ok(Some(JsonToken::Colon)) } b',' => { self.pos += 1; Ok(Some(JsonToken::Comma)) } b'"' => self.read_string(), b't' => self.read_keyword("true", JsonToken::True), b'f' => self.read_keyword("false", JsonToken::False), b'n' => self.read_keyword("null", JsonToken::Null), b'-' | b'0'..=b'9' => self.read_number(), _ => Err(RuntimeError::syntax_error(format!( "Unexpected character '{}' in JSON", ch as char ))), } } fn read_string(&mut self) -> Result, RuntimeError> { self.pos += 1; // skip opening quote let mut s = String::new(); while self.pos < self.chars.len() { let ch = self.chars[self.pos]; self.pos += 1; if ch == b'"' { return Ok(Some(JsonToken::String(s))); } if ch == b'\\' { if self.pos >= self.chars.len() { return Err(RuntimeError::syntax_error("Unterminated string in JSON")); } let esc = self.chars[self.pos]; self.pos += 1; match esc { b'"' => s.push('"'), b'\\' => s.push('\\'), b'/' => s.push('/'), b'b' => s.push('\u{0008}'), b'f' => s.push('\u{000C}'), b'n' => s.push('\n'), b'r' => s.push('\r'), b't' => s.push('\t'), b'u' => { if self.pos + 4 > self.chars.len() { return Err(RuntimeError::syntax_error("Invalid unicode escape")); } let hex = std::str::from_utf8(&self.chars[self.pos..self.pos + 4]) .map_err(|_| RuntimeError::syntax_error("Invalid unicode escape"))?; self.pos += 4; let code = u32::from_str_radix(hex, 16) .map_err(|_| RuntimeError::syntax_error("Invalid unicode escape"))?; if let Some(c) = char::from_u32(code) { s.push(c); } else { return Err(RuntimeError::syntax_error("Invalid unicode code point")); } } _ => { return Err(RuntimeError::syntax_error(format!( "Invalid escape character '\\{}'", esc as char ))); } } } else { s.push(ch as char); } } Err(RuntimeError::syntax_error("Unterminated string in JSON")) } fn read_number(&mut self) -> Result, RuntimeError> { let start = self.pos; if self.pos < self.chars.len() && self.chars[self.pos] == b'-' { self.pos += 1; } while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() { self.pos += 1; } if self.pos < self.chars.len() && self.chars[self.pos] == b'.' { self.pos += 1; while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() { self.pos += 1; } } if self.pos < self.chars.len() && (self.chars[self.pos] == b'e' || self.chars[self.pos] == b'E') { self.pos += 1; if self.pos < self.chars.len() && (self.chars[self.pos] == b'+' || self.chars[self.pos] == b'-') { self.pos += 1; } while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() { self.pos += 1; } } let num_str = std::str::from_utf8(&self.chars[start..self.pos]) .map_err(|_| RuntimeError::syntax_error("Invalid number in JSON"))?; let n: f64 = num_str .parse() .map_err(|_| RuntimeError::syntax_error("Invalid number in JSON"))?; Ok(Some(JsonToken::Number(n))) } fn read_keyword( &mut self, keyword: &str, token: JsonToken, ) -> Result, RuntimeError> { let end = self.pos + keyword.len(); if end <= self.chars.len() && &self.chars[self.pos..end] == keyword.as_bytes() { self.pos = end; Ok(Some(token)) } else { Err(RuntimeError::syntax_error(format!( "Unexpected token in JSON at position {}", self.pos ))) } } } struct JsonParser<'a> { lexer: JsonLexer<'a>, current: Option, } impl<'a> JsonParser<'a> { fn new(input: &'a str) -> Result { let mut lexer = JsonLexer::new(input); let current = lexer.next_token()?; Ok(Self { lexer, current }) } fn advance(&mut self) -> Result<(), RuntimeError> { self.current = self.lexer.next_token()?; Ok(()) } fn parse_value(&mut self, gc: &mut Gc) -> Result { match self.current.take() { Some(JsonToken::Null) => { self.advance()?; Ok(Value::Null) } Some(JsonToken::True) => { self.advance()?; Ok(Value::Boolean(true)) } Some(JsonToken::False) => { self.advance()?; Ok(Value::Boolean(false)) } Some(JsonToken::Number(n)) => { self.advance()?; Ok(Value::Number(n)) } Some(JsonToken::String(s)) => { self.advance()?; Ok(Value::String(s)) } Some(JsonToken::LBracket) => self.parse_array(gc), Some(JsonToken::LBrace) => self.parse_object(gc), Some(other) => { // Put it back for error context. self.current = Some(other); Err(RuntimeError::syntax_error("Unexpected token in JSON")) } None => Err(RuntimeError::syntax_error("Unexpected end of JSON input")), } } fn parse_array(&mut self, gc: &mut Gc) -> Result { // Current token was LBracket, already consumed via take(). self.advance()?; // move past '[' let mut items: Vec = Vec::new(); if self.current == Some(JsonToken::RBracket) { self.advance()?; let mut obj = ObjectData::new(); obj.properties.insert( "length".to_string(), Property { value: Value::Number(0.0), writable: true, enumerable: false, configurable: false, }, ); return Ok(Value::Object(gc.alloc(HeapObject::Object(obj)))); } loop { let val = self.parse_value(gc)?; items.push(val); match &self.current { Some(JsonToken::Comma) => { self.advance()?; } Some(JsonToken::RBracket) => { self.advance()?; break; } _ => { return Err(RuntimeError::syntax_error( "Expected ',' or ']' in JSON array", )); } } } let mut obj = ObjectData::new(); for (i, v) in items.iter().enumerate() { obj.properties .insert(i.to_string(), Property::data(v.clone())); } obj.properties.insert( "length".to_string(), Property { value: Value::Number(items.len() as f64), writable: true, enumerable: false, configurable: false, }, ); Ok(Value::Object(gc.alloc(HeapObject::Object(obj)))) } fn parse_object(&mut self, gc: &mut Gc) -> Result { self.advance()?; // move past '{' let mut obj = ObjectData::new(); if self.current == Some(JsonToken::RBrace) { self.advance()?; return Ok(Value::Object(gc.alloc(HeapObject::Object(obj)))); } loop { let key = match self.current.take() { Some(JsonToken::String(s)) => s, _ => { return Err(RuntimeError::syntax_error( "Expected string key in JSON object", )); } }; self.advance()?; if self.current != Some(JsonToken::Colon) { return Err(RuntimeError::syntax_error( "Expected ':' after key in JSON object", )); } self.advance()?; let val = self.parse_value(gc)?; obj.properties.insert(key, Property::data(val)); match &self.current { Some(JsonToken::Comma) => { self.advance()?; } Some(JsonToken::RBrace) => { self.advance()?; break; } _ => { return Err(RuntimeError::syntax_error( "Expected ',' or '}}' in JSON object", )); } } } Ok(Value::Object(gc.alloc(HeapObject::Object(obj)))) } } /// Public wrapper for `json_parse` used by the fetch module. pub fn json_parse_pub(args: &[Value], ctx: &mut NativeContext) -> Result { json_parse(args, ctx) } fn json_parse(args: &[Value], ctx: &mut NativeContext) -> Result { let text = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let mut parser = JsonParser::new(&text)?; let value = parser.parse_value(ctx.gc)?; // Ensure no trailing content. if parser.current.is_some() { return Err(RuntimeError::syntax_error( "Unexpected token after JSON value", )); } // TODO: reviver support — skip for now. Ok(value) } // ── JSON.stringify ─────────────────────────────────────────── fn json_stringify(args: &[Value], ctx: &mut NativeContext) -> Result { let value = args.first().cloned().unwrap_or(Value::Undefined); let indent = match args.get(2) { Some(Value::Number(n)) => { let n = *n as usize; if n > 0 { " ".repeat(n.min(10)) } else { String::new() } } Some(Value::String(s)) => s[..s.len().min(10)].to_string(), _ => String::new(), }; // Track visited objects for circular reference detection. let mut visited: Vec = Vec::new(); let result = json_stringify_value(&value, ctx.gc, &indent, "", &mut visited)?; match result { Some(s) => Ok(Value::String(s)), None => Ok(Value::Undefined), } } fn json_stringify_value( value: &Value, gc: &Gc, indent: &str, current_indent: &str, visited: &mut Vec, ) -> Result, RuntimeError> { match value { Value::Null => Ok(Some("null".to_string())), Value::Boolean(true) => Ok(Some("true".to_string())), Value::Boolean(false) => Ok(Some("false".to_string())), Value::Number(n) => { if n.is_nan() || n.is_infinite() { Ok(Some("null".to_string())) } else { Ok(Some(js_number_to_string(*n))) } } Value::String(s) => Ok(Some(json_quote_string(s))), Value::Undefined | Value::Function(_) => Ok(None), Value::Object(gc_ref) => { // Circular reference check. if visited.contains(gc_ref) { return Err(RuntimeError::type_error( "Converting circular structure to JSON", )); } visited.push(*gc_ref); let result = match gc.get(*gc_ref) { Some(HeapObject::Object(data)) => { // Check for toJSON method. if let Some(prop) = data.properties.get("toJSON") { if matches!(prop.value, Value::Function(_)) { // We can't call JS functions from here easily, // but for Date objects we recognize the __date_ms__ pattern. if let Some(date_prop) = data.properties.get("__date_ms__") { let ms = date_prop.value.to_number(); if ms.is_nan() { visited.pop(); return Ok(Some("null".to_string())); } let (y, m, d, h, min, s, ms_part, _) = ms_to_utc_components(ms); visited.pop(); return Ok(Some(format!( "\"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z\"", y, m + 1, d, h, min, s, ms_part ))); } } } if array_length_exists(gc, *gc_ref) { json_stringify_array(gc, *gc_ref, indent, current_indent, visited) } else { json_stringify_object(gc, data, indent, current_indent, visited) } } _ => Ok(Some("{}".to_string())), }; visited.pop(); result } } } fn json_stringify_array( gc: &Gc, arr: GcRef, indent: &str, current_indent: &str, visited: &mut Vec, ) -> Result, RuntimeError> { let len = array_length(gc, arr); if len == 0 { return Ok(Some("[]".to_string())); } let has_indent = !indent.is_empty(); let next_indent = if has_indent { format!("{}{}", current_indent, indent) } else { String::new() }; let mut parts: Vec = Vec::new(); for i in 0..len { let val = array_get(gc, arr, i); match json_stringify_value(&val, gc, indent, &next_indent, visited)? { Some(s) => parts.push(s), None => parts.push("null".to_string()), } } if has_indent { Ok(Some(format!( "[\n{}{}\n{}]", next_indent, parts.join(&format!(",\n{}", next_indent)), current_indent ))) } else { Ok(Some(format!("[{}]", parts.join(",")))) } } fn json_stringify_object( gc: &Gc, data: &ObjectData, indent: &str, current_indent: &str, visited: &mut Vec, ) -> Result, RuntimeError> { let has_indent = !indent.is_empty(); let next_indent = if has_indent { format!("{}{}", current_indent, indent) } else { String::new() }; let mut parts: Vec = Vec::new(); // Collect and sort keys for deterministic output. let mut keys: Vec<&String> = data.properties.keys().collect(); keys.sort(); for key in keys { let prop = &data.properties[key]; if !prop.enumerable { continue; } if let Some(val_str) = json_stringify_value(&prop.value, gc, indent, &next_indent, visited)? { if has_indent { parts.push(format!("{}: {}", json_quote_string(key), val_str)); } else { parts.push(format!("{}:{}", json_quote_string(key), val_str)); } } } if parts.is_empty() { return Ok(Some("{}".to_string())); } if has_indent { Ok(Some(format!( "{{\n{}{}\n{}}}", next_indent, parts.join(&format!(",\n{}", next_indent)), current_indent ))) } else { Ok(Some(format!("{{{}}}", parts.join(",")))) } } fn json_quote_string(s: &str) -> String { let mut out = String::with_capacity(s.len() + 2); out.push('"'); for ch in s.chars() { match ch { '"' => out.push_str("\\\""), '\\' => out.push_str("\\\\"), '\n' => out.push_str("\\n"), '\r' => out.push_str("\\r"), '\t' => out.push_str("\\t"), '\u{0008}' => out.push_str("\\b"), '\u{000C}' => out.push_str("\\f"), c if c < '\u{0020}' => { out.push_str(&format!("\\u{:04x}", c as u32)); } c => out.push(c), } } out.push('"'); out } // ── Console object ────────────────────────────────────────── fn init_console_object(vm: &mut Vm) { let mut data = ObjectData::new(); if let Some(proto) = vm.object_prototype { data.prototype = Some(proto); } let console_ref = vm.gc.alloc(HeapObject::Object(data)); let methods: &[NativeMethod] = &[ ("log", console_log), ("info", console_log), ("debug", console_log), ("error", console_error), ("warn", console_warn), ]; for &(name, callback) in methods { let func = make_native(&mut vm.gc, name, callback); set_builtin_prop(&mut vm.gc, console_ref, name, Value::Function(func)); } vm.set_global("console", Value::Object(console_ref)); } /// Format a JS value for console output with richer detail than `to_js_string`. /// Arrays show their contents and objects show their properties. fn console_format_value( value: &Value, gc: &Gc, depth: usize, seen: &mut HashSet, ) -> String { const MAX_DEPTH: usize = 4; match value { Value::Undefined => "undefined".to_string(), Value::Null => "null".to_string(), Value::Boolean(b) => b.to_string(), Value::Number(n) => js_number_to_string(*n), Value::String(s) => s.clone(), Value::Function(gc_ref) => gc .get(*gc_ref) .and_then(|obj| match obj { HeapObject::Function(f) => Some(format!("[Function: {}]", f.name)), _ => None, }) .unwrap_or_else(|| "[Function]".to_string()), Value::Object(gc_ref) => { if depth > MAX_DEPTH || seen.contains(gc_ref) { return "[Object]".to_string(); } seen.insert(*gc_ref); let result = match gc.get(*gc_ref) { Some(HeapObject::Object(obj_data)) => { // Check if it's an array (has a "length" property). if obj_data.properties.contains_key("length") { format_array(obj_data, gc, depth, seen) } else { format_object(obj_data, gc, depth, seen) } } _ => "[Object]".to_string(), }; seen.remove(gc_ref); result } } } fn format_array( data: &ObjectData, gc: &Gc, depth: usize, seen: &mut HashSet, ) -> String { let len = data .properties .get("length") .map(|p| p.value.to_number() as usize) .unwrap_or(0); let mut parts = Vec::with_capacity(len); for i in 0..len { let val = data .properties .get(&i.to_string()) .map(|p| &p.value) .unwrap_or(&Value::Undefined); parts.push(console_format_value(val, gc, depth + 1, seen)); } format!("[ {} ]", parts.join(", ")) } fn format_object( data: &ObjectData, gc: &Gc, depth: usize, seen: &mut HashSet, ) -> String { if data.properties.is_empty() { return "{}".to_string(); } let mut parts = Vec::new(); for (key, prop) in &data.properties { let val_str = console_format_value(&prop.value, gc, depth + 1, seen); parts.push(format!("{}: {}", key, val_str)); } parts.sort(); format!("{{ {} }}", parts.join(", ")) } /// Format all arguments for a console method, separated by spaces. fn console_format_args(args: &[Value], gc: &Gc) -> String { let mut seen = HashSet::new(); args.iter() .map(|v| console_format_value(v, gc, 0, &mut seen)) .collect::>() .join(" ") } fn console_log(args: &[Value], ctx: &mut NativeContext) -> Result { let msg = console_format_args(args, ctx.gc); ctx.console_output.log(&msg); Ok(Value::Undefined) } fn console_error(args: &[Value], ctx: &mut NativeContext) -> Result { let msg = console_format_args(args, ctx.gc); ctx.console_output.error(&msg); Ok(Value::Undefined) } fn console_warn(args: &[Value], ctx: &mut NativeContext) -> Result { let msg = console_format_args(args, ctx.gc); ctx.console_output.warn(&msg); Ok(Value::Undefined) } // ── Global utility functions ───────────────────────────────── fn init_global_functions(vm: &mut Vm) { vm.define_native("parseInt", parse_int); vm.define_native("parseFloat", parse_float); vm.define_native("isNaN", is_nan); vm.define_native("isFinite", is_finite); // Timer APIs. vm.define_native("setTimeout", crate::timers::set_timeout); vm.define_native("clearTimeout", crate::timers::clear_timeout); vm.define_native("setInterval", crate::timers::set_interval); vm.define_native("clearInterval", crate::timers::clear_interval); vm.define_native( "requestAnimationFrame", crate::timers::request_animation_frame, ); vm.define_native( "cancelAnimationFrame", crate::timers::cancel_animation_frame, ); } fn parse_int(args: &[Value], ctx: &mut NativeContext) -> Result { let s = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let radix = args.get(1).map(|v| v.to_number() as u32).unwrap_or(0); let s = s.trim(); if s.is_empty() { return Ok(Value::Number(f64::NAN)); } let (neg, s) = if let Some(rest) = s.strip_prefix('-') { (true, rest) } else if let Some(rest) = s.strip_prefix('+') { (false, rest) } else { (false, s) }; // Auto-detect hex. let (radix, s) = if radix == 0 || radix == 16 { if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { (16, rest) } else { (if radix == 0 { 10 } else { radix }, s) } } else { (radix, s) }; if !(2..=36).contains(&radix) { return Ok(Value::Number(f64::NAN)); } // Parse digits until invalid. let mut result: f64 = 0.0; let mut found = false; for c in s.chars() { let digit = match c.to_digit(radix) { Some(d) => d, None => break, }; result = result * radix as f64 + digit as f64; found = true; } if !found { return Ok(Value::Number(f64::NAN)); } if neg { result = -result; } Ok(Value::Number(result)) } fn parse_float(args: &[Value], ctx: &mut NativeContext) -> Result { let s = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let s = s.trim(); match s.parse::() { Ok(n) => Ok(Value::Number(n)), Err(_) => Ok(Value::Number(f64::NAN)), } } fn is_nan(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Boolean(n.is_nan())) } fn is_finite(args: &[Value], _ctx: &mut NativeContext) -> Result { let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); Ok(Value::Boolean(n.is_finite())) } // ── Strict equality (for Array.indexOf etc.) ───────────────── fn strict_eq_values(a: &Value, b: &Value) -> bool { match (a, b) { (Value::Undefined, Value::Undefined) => true, (Value::Null, Value::Null) => true, (Value::Number(x), Value::Number(y)) => x == y, (Value::String(x), Value::String(y)) => x == y, (Value::Boolean(x), Value::Boolean(y)) => x == y, (Value::Object(x), Value::Object(y)) => x == y, (Value::Function(x), Value::Function(y)) => x == y, _ => false, } } /// SameValueZero (like === but NaN === NaN is true). fn same_value_zero(a: &Value, b: &Value) -> bool { match (a, b) { (Value::Number(x), Value::Number(y)) => { if x.is_nan() && y.is_nan() { return true; } x == y } _ => strict_eq_values(a, b), } } // ── JS preamble for callback-based methods ─────────────────── fn init_js_preamble(vm: &mut Vm) { // NOTE: All preamble methods capture `this` into a local `_t` variable // because nested method calls (e.g. result.push()) clobber the global `this`. let preamble = r#" Array.prototype.forEach = function(cb) { var _t = this; for (var i = 0; i < _t.length; i = i + 1) { cb(_t[i], i, _t); } }; Array.prototype.map = function(cb) { var _t = this; var result = []; for (var i = 0; i < _t.length; i = i + 1) { result.push(cb(_t[i], i, _t)); } return result; }; Array.prototype.filter = function(cb) { var _t = this; var result = []; for (var i = 0; i < _t.length; i = i + 1) { if (cb(_t[i], i, _t)) { result.push(_t[i]); } } return result; }; Array.prototype.reduce = function(cb, init) { var _t = this; var acc = init; var start = 0; if (acc === undefined) { acc = _t[0]; start = 1; } for (var i = start; i < _t.length; i = i + 1) { acc = cb(acc, _t[i], i, _t); } return acc; }; Array.prototype.reduceRight = function(cb, init) { var _t = this; var acc = init; var start = _t.length - 1; if (acc === undefined) { acc = _t[start]; start = start - 1; } for (var i = start; i >= 0; i = i - 1) { acc = cb(acc, _t[i], i, _t); } return acc; }; Array.prototype.find = function(cb) { var _t = this; for (var i = 0; i < _t.length; i = i + 1) { if (cb(_t[i], i, _t)) { return _t[i]; } } return undefined; }; Array.prototype.findIndex = function(cb) { var _t = this; for (var i = 0; i < _t.length; i = i + 1) { if (cb(_t[i], i, _t)) { return i; } } return -1; }; Array.prototype.some = function(cb) { var _t = this; for (var i = 0; i < _t.length; i = i + 1) { if (cb(_t[i], i, _t)) { return true; } } return false; }; Array.prototype.every = function(cb) { var _t = this; for (var i = 0; i < _t.length; i = i + 1) { if (!cb(_t[i], i, _t)) { return false; } } return true; }; Array.prototype.sort = function(cmp) { var _t = this; var len = _t.length; for (var i = 0; i < len; i = i + 1) { for (var j = 0; j < len - i - 1; j = j + 1) { var a = _t[j]; var b = _t[j + 1]; var order; if (cmp) { order = cmp(a, b); } else { var sa = "" + a; var sb = "" + b; if (sa > sb) { order = 1; } else if (sa < sb) { order = -1; } else { order = 0; } } if (order > 0) { _t[j] = b; _t[j + 1] = a; } } } return _t; }; Array.prototype.flat = function(depth) { var _t = this; if (depth === undefined) { depth = 1; } var result = []; for (var i = 0; i < _t.length; i = i + 1) { var elem = _t[i]; if (depth > 0 && Array.isArray(elem)) { var sub = elem.flat(depth - 1); for (var j = 0; j < sub.length; j = j + 1) { result.push(sub[j]); } } else { result.push(elem); } } return result; }; Array.prototype.flatMap = function(cb) { var _t = this; return _t.map(cb).flat(); }; "#; // Compile and execute the preamble. let ast = match crate::parser::Parser::parse(preamble) { Ok(ast) => ast, Err(_) => return, // Silently skip if preamble fails to parse. }; let func = match crate::compiler::compile(&ast) { Ok(func) => func, Err(_) => return, }; let _ = vm.execute(&func); // Promise constructor and static methods (defined in JS so the executor // callback can be called as bytecode, not just native functions). let promise_preamble = r#" function Promise(executor) { var p = __Promise_create(); var alreadyResolved = false; function resolve(value) { if (!alreadyResolved) { alreadyResolved = true; __Promise_resolve(p, value); } } function reject(reason) { if (!alreadyResolved) { alreadyResolved = true; __Promise_reject(p, reason); } } try { executor(resolve, reject); } catch (e) { reject(e); } return p; } Promise.resolve = __Promise_static_resolve; Promise.reject = __Promise_static_reject; Promise.all = __Promise_static_all; Promise.race = __Promise_static_race; Promise.allSettled = __Promise_static_allSettled; Promise.any = __Promise_static_any; "#; let ast = match crate::parser::Parser::parse(promise_preamble) { Ok(ast) => ast, Err(_) => return, }; let func = match crate::compiler::compile(&ast) { Ok(func) => func, Err(_) => return, }; let _ = vm.execute(&func); }