diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs new file mode 100644 index 0000000..8fd7348 --- /dev/null +++ b/crates/js/src/builtins.rs @@ -0,0 +1,1669 @@ +//! Built-in JavaScript objects and functions: Object, Array, Function, Error. +//! +//! 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::collections::HashMap; + +// ── Helpers ────────────────────────────────────────────────── + +/// Create a native function GcRef. +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. +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, + }, + ); + } + } +} + +/// 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); + + // Store prototypes in VM for use by CreateArray/CreateObject. + vm.object_prototype = Some(obj_proto); + vm.array_prototype = Some(arr_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, obj_proto, err_proto); + + // 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)); +} + +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); + if arg_len > 0 { + 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(), + }, + ) + .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, obj_proto: GcRef, 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, obj_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, obj_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, obj_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, obj_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, obj_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, obj_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, + _obj_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}"))) + } +} + +// ── 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); +} + +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(10); + 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); +} diff --git a/crates/js/src/compiler.rs b/crates/js/src/compiler.rs index c3651c8..5a25e86 100644 --- a/crates/js/src/compiler.rs +++ b/crates/js/src/compiler.rs @@ -2101,6 +2101,21 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), fc.free_reg(val_reg); } } + // Set length property to the number of elements. + if !elements.is_empty() { + let len_name = fc.builder.add_name("length"); + let len_reg = fc.alloc_reg(); + if elements.len() <= 127 { + fc.builder.emit_load_int8(len_reg, elements.len() as i8); + } else { + let ci = fc + .builder + .add_constant(Constant::Number(elements.len() as f64)); + fc.builder.emit_reg_u16(Op::LoadConst, len_reg, ci); + } + fc.builder.emit_set_prop_name(dst, len_name, len_reg); + fc.free_reg(len_reg); + } } ExprKind::Object(properties) => { diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index 3bbace5..a30e64f 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -1,6 +1,7 @@ //! JavaScript engine — lexer, parser, bytecode, register VM, GC, JIT (AArch64). pub mod ast; +pub mod builtins; pub mod bytecode; pub mod compiler; pub mod gc; diff --git a/crates/js/src/vm.rs b/crates/js/src/vm.rs index 6b57873..41f6168 100644 --- a/crates/js/src/vm.rs +++ b/crates/js/src/vm.rs @@ -146,7 +146,13 @@ pub struct BytecodeFunc { /// A native function callable from JS. #[derive(Clone)] pub struct NativeFunc { - pub callback: fn(&[Value]) -> Result, + pub callback: fn(&[Value], &mut NativeContext) -> Result, +} + +/// Context passed to native functions, providing GC access and `this` binding. +pub struct NativeContext<'a> { + pub gc: &'a mut Gc, + pub this: Value, } // ── JS Value ────────────────────────────────────────────────── @@ -672,6 +678,10 @@ pub struct Vm { instruction_limit: Option, /// Number of instructions executed so far. instructions_executed: u64, + /// Built-in Object.prototype (root of the prototype chain). + pub object_prototype: Option, + /// Built-in Array.prototype (set on newly created arrays). + pub array_prototype: Option, } /// Maximum register file size. @@ -681,14 +691,18 @@ const MAX_CALL_DEPTH: usize = 512; impl Vm { pub fn new() -> Self { - Self { + let mut vm = Self { registers: vec![Value::Undefined; 256], frames: Vec::new(), globals: HashMap::new(), gc: Gc::new(), instruction_limit: None, instructions_executed: 0, - } + object_prototype: None, + array_prototype: None, + }; + crate::builtins::init_builtins(&mut vm); + vm } /// Set an instruction limit. The VM will return a RuntimeError after @@ -772,6 +786,13 @@ impl Vm { roots.push(uv); } } + // Built-in prototype roots. + if let Some(r) = self.object_prototype { + roots.push(r); + } + if let Some(r) = self.array_prototype { + roots.push(r); + } roots } @@ -1176,17 +1197,28 @@ impl Vm { }; match call_info { - CallInfo::Native(callback) => match callback(&args) { - Ok(val) => { - self.registers[base + dst as usize] = val; - } - Err(err) => { - let err_val = err.to_value(&mut self.gc); - if !self.handle_exception(err_val) { - return Err(err); + CallInfo::Native(callback) => { + let this = self + .globals + .get("this") + .cloned() + .unwrap_or(Value::Undefined); + let mut ctx = NativeContext { + gc: &mut self.gc, + this, + }; + match callback(&args, &mut ctx) { + Ok(val) => { + self.registers[base + dst as usize] = val; + } + Err(err) => { + let err_val = err.to_value(&mut self.gc); + if !self.handle_exception(err_val) { + return Err(err); + } } } - }, + } CallInfo::Bytecode(callee_func, callee_upvalues) => { if self.frames.len() >= MAX_CALL_DEPTH { let err = @@ -1360,7 +1392,9 @@ impl Vm { Op::CreateObject => { let dst = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; - let gc_ref = self.gc.alloc(HeapObject::Object(ObjectData::new())); + let mut obj = ObjectData::new(); + obj.prototype = self.object_prototype; + let gc_ref = self.gc.alloc(HeapObject::Object(obj)); self.registers[base + dst as usize] = Value::Object(gc_ref); if self.gc.should_collect() { @@ -1372,6 +1406,7 @@ impl Vm { let dst = Self::read_u8(&mut self.frames[fi]); let base = self.frames[fi].base; let mut obj = ObjectData::new(); + obj.prototype = self.array_prototype; obj.properties.insert( "length".to_string(), Property { @@ -1666,7 +1701,7 @@ impl Vm { pub fn define_native( &mut self, name: &str, - callback: fn(&[Value]) -> Result, + callback: fn(&[Value], &mut NativeContext) -> Result, ) { let gc_ref = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: name.to_string(), @@ -1698,7 +1733,7 @@ impl Default for Vm { /// Internal enum to avoid holding a GC borrow across the call setup. enum CallInfo { - Native(fn(&[Value]) -> Result), + Native(fn(&[Value], &mut NativeContext) -> Result), Bytecode(Function, Vec), } @@ -1894,7 +1929,7 @@ mod tests { let func = b.finish(); let mut vm = Vm::new(); - vm.define_native("double", |args| { + vm.define_native("double", |args, _ctx| { let n = args.first().unwrap_or(&Value::Undefined).to_number(); Ok(Value::Number(n * 2.0)) }); @@ -2523,7 +2558,7 @@ mod tests { let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "Foo".to_string(), kind: FunctionKind::Native(NativeFunc { - callback: |_| Ok(Value::Undefined), + callback: |_, _ctx| Ok(Value::Undefined), }), prototype_obj: Some(proto), properties: HashMap::new(), @@ -2750,4 +2785,474 @@ mod tests { v => panic!("expected 10, got {v:?}"), } } + + // ── Object built-in tests ──────────────────────────────── + + #[test] + fn test_object_keys() { + let src = r#" + var obj = {}; + obj.a = 1; + obj.b = 2; + obj.c = 3; + var k = Object.keys(obj); + k.length + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 3.0), + v => panic!("expected 3, got {v:?}"), + } + } + + #[test] + fn test_object_values() { + let src = r#" + var obj = {}; + obj.x = 10; + var v = Object.values(obj); + v[0] + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 10.0), + v => panic!("expected 10, got {v:?}"), + } + } + + #[test] + fn test_object_entries() { + let src = r#" + var obj = {}; + obj.x = 42; + var e = Object.entries(obj); + e[0][1] + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 42.0), + v => panic!("expected 42, got {v:?}"), + } + } + + #[test] + fn test_object_assign() { + let src = r#" + var a = {}; + a.x = 1; + var b = {}; + b.y = 2; + var c = Object.assign(a, b); + c.y + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + } + + #[test] + fn test_object_create() { + let src = r#" + var proto = {}; + proto.greet = "hello"; + var child = Object.create(proto); + child.greet + "#; + match eval(src).unwrap() { + Value::String(s) => assert_eq!(s, "hello"), + v => panic!("expected 'hello', got {v:?}"), + } + } + + #[test] + fn test_object_is() { + let src = "Object.is(NaN, NaN)"; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_object_freeze() { + let src = r#" + var obj = {}; + obj.x = 1; + Object.freeze(obj); + Object.isFrozen(obj) + "#; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_object_has_own_property() { + let src = r#" + var obj = {}; + obj.x = 1; + obj.hasOwnProperty("x") + "#; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + // ── Array built-in tests ───────────────────────────────── + + #[test] + fn test_array_push_pop() { + let src = r#" + var arr = [1, 2, 3]; + arr.push(4); + arr.pop() + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 4.0), + v => panic!("expected 4, got {v:?}"), + } + } + + #[test] + fn test_array_push_length() { + let src = r#" + var arr = []; + arr.push(10); + arr.push(20); + arr.length + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + } + + #[test] + fn test_array_shift_unshift() { + let src = r#" + var arr = [1, 2, 3]; + arr.unshift(0); + arr.shift() + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 0.0), + v => panic!("expected 0, got {v:?}"), + } + } + + #[test] + fn test_array_index_of() { + let src = r#" + var arr = [10, 20, 30]; + arr.indexOf(20) + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 1.0), + v => panic!("expected 1, got {v:?}"), + } + } + + #[test] + fn test_array_includes() { + let src = r#" + var arr = [1, 2, 3]; + arr.includes(2) + "#; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_array_join() { + let src = r#" + var arr = [1, 2, 3]; + arr.join("-") + "#; + match eval(src).unwrap() { + Value::String(s) => assert_eq!(s, "1-2-3"), + v => panic!("expected '1-2-3', got {v:?}"), + } + } + + #[test] + fn test_array_slice() { + let src = r#" + var arr = [1, 2, 3, 4, 5]; + var s = arr.slice(1, 3); + s.length + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + } + + #[test] + fn test_array_concat() { + let src = r#" + var a = [1, 2]; + var b = [3, 4]; + var c = a.concat(b); + c.length + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 4.0), + v => panic!("expected 4, got {v:?}"), + } + } + + #[test] + fn test_array_reverse() { + let src = r#" + var arr = [1, 2, 3]; + arr.reverse(); + arr[0] + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 3.0), + v => panic!("expected 3, got {v:?}"), + } + } + + #[test] + fn test_array_splice() { + let src = r#" + var arr = [1, 2, 3, 4, 5]; + var removed = arr.splice(1, 2); + removed.length + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + } + + #[test] + fn test_array_is_array() { + let src = "Array.isArray([1, 2, 3])"; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_array_map() { + let src = r#" + var arr = [1, 2, 3]; + var doubled = arr.map(function(x) { return x * 2; }); + doubled[1] + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 4.0), + v => panic!("expected 4, got {v:?}"), + } + } + + #[test] + fn test_array_filter() { + let src = r#" + var arr = [1, 2, 3, 4, 5]; + var evens = arr.filter(function(x) { return x % 2 === 0; }); + evens.length + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + } + + #[test] + fn test_array_reduce() { + let src = r#" + var arr = [1, 2, 3, 4]; + arr.reduce(function(acc, x) { return acc + x; }, 0) + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 10.0), + v => panic!("expected 10, got {v:?}"), + } + } + + #[test] + fn test_array_foreach() { + let src = r#" + var arr = [1, 2, 3]; + var sum = 0; + arr.forEach(function(x) { sum = sum + x; }); + sum + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 6.0), + v => panic!("expected 6, got {v:?}"), + } + } + + #[test] + fn test_array_find() { + let src = r#" + var arr = [1, 2, 3, 4]; + arr.find(function(x) { return x > 2; }) + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 3.0), + v => panic!("expected 3, got {v:?}"), + } + } + + #[test] + fn test_array_find_index() { + let src = r#" + var arr = [1, 2, 3, 4]; + arr.findIndex(function(x) { return x > 2; }) + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + } + + #[test] + fn test_array_some_every() { + let src = r#" + var arr = [2, 4, 6]; + var all_even = arr.every(function(x) { return x % 2 === 0; }); + var has_big = arr.some(function(x) { return x > 10; }); + all_even && !has_big + "#; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_array_sort() { + let src = r#" + var arr = [3, 1, 2]; + arr.sort(); + arr[0] + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 1.0), + v => panic!("expected 1, got {v:?}"), + } + } + + #[test] + fn test_array_sort_custom() { + let src = r#" + var arr = [3, 1, 2]; + arr.sort(function(a, b) { return a - b; }); + arr[2] + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 3.0), + v => panic!("expected 3, got {v:?}"), + } + } + + #[test] + fn test_array_from() { + let src = r#" + var arr = Array.from("abc"); + arr.length + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 3.0), + v => panic!("expected 3, got {v:?}"), + } + } + + #[test] + fn test_array_from_array() { + let src = r#" + var orig = [10, 20, 30]; + var copy = Array.from(orig); + copy[2] + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 30.0), + v => panic!("expected 30, got {v:?}"), + } + } + + #[test] + fn test_array_flat() { + let src = r#" + var arr = [[1, 2], [3, 4]]; + var flat = arr.flat(); + flat.length + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 4.0), + v => panic!("expected 4, got {v:?}"), + } + } + + // ── Error built-in tests ───────────────────────────────── + + #[test] + fn test_error_constructor() { + let src = r#" + var e = new Error("oops"); + e.message + "#; + match eval(src).unwrap() { + Value::String(s) => assert_eq!(s, "oops"), + v => panic!("expected 'oops', got {v:?}"), + } + } + + #[test] + fn test_type_error_constructor() { + let src = r#" + var e = new TypeError("bad type"); + e.message + "#; + match eval(src).unwrap() { + Value::String(s) => assert_eq!(s, "bad type"), + v => panic!("expected 'bad type', got {v:?}"), + } + } + + // ── Global function tests ──────────────────────────────── + + #[test] + fn test_parse_int() { + let src = "parseInt('42')"; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 42.0), + v => panic!("expected 42, got {v:?}"), + } + } + + #[test] + fn test_parse_int_hex() { + let src = "parseInt('0xFF', 16)"; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 255.0), + v => panic!("expected 255, got {v:?}"), + } + } + + #[test] + fn test_is_nan() { + let src = "isNaN(NaN)"; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_is_finite() { + let src = "isFinite(42)"; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } }