diff --git a/Cargo.lock b/Cargo.lock index b42dfcd..917b0d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1059,10 +1059,13 @@ dependencies = [ "fast-float", "indexmap", "memchr", + "oxide_compiler", "oxide_kernel", "oxide_log", + "oxide_parser", "oxide_runtime_api", "oxide_types", + "oxide_vm", "regex", "ryu", "serde_json", diff --git a/crates/oxide_builtins/Cargo.toml b/crates/oxide_builtins/Cargo.toml index dea8897..8171484 100644 --- a/crates/oxide_builtins/Cargo.toml +++ b/crates/oxide_builtins/Cargo.toml @@ -19,3 +19,8 @@ serde_json.workspace = true chrono.workspace = true unicode-normalization.workspace = true indexmap.workspace = true + +[dev-dependencies] +oxide_vm = { path = "../oxide_vm" } +oxide_compiler = { path = "../oxide_compiler" } +oxide_parser = { path = "../oxide_parser" } diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 654d78f..005f07c 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -42,17 +42,10 @@ pub fn object_constructor(vm: &mut H, _args: &[u8]) -> NativeResult { } pub fn object_keys(vm: &mut H, args: &[u8]) -> NativeResult { - if args.len() < 2 { - return NativeResult::Err(JsValue::undefined()); - } - let val = vm.reg(args[1]); - if !val.is_object() { - return NativeResult::Err(JsValue::undefined()); - } - let obj_ptr = val.as_js_object_ptr(); - if obj_ptr.is_null() { - return NativeResult::Err(JsValue::undefined()); - } + let obj_ptr = match require_obj_arg(vm, args, "keys") { + Ok(ptr) => ptr, + Err(err) => return NativeResult::Err(err), + }; let key_names: Vec; { @@ -86,11 +79,18 @@ pub fn object_keys(vm: &mut H, args: &[u8]) -> NativeResult { pub fn object_create(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { - return NativeResult::Ok(JsValue::undefined()); + return NativeResult::Err(crate::error::create_type_error(vm, "Object.create: at least 1 argument required")); } let proto_val = vm.reg(args[1]); - if !proto_val.is_null() && !proto_val.is_object() { - return NativeResult::Ok(JsValue::undefined()); + if proto_val.is_null() { + let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); + return NativeResult::Ok(JsValue::from_js_object(obj)); + } + if !proto_val.is_object() { + return NativeResult::Err(crate::error::create_type_error( + vm, + "Object.create: prototype must be an object or null", + )); } let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, proto_val)); NativeResult::Ok(JsValue::from_js_object(obj)) @@ -109,24 +109,32 @@ pub fn object_assign(vm: &mut H, args: &[u8]) -> NativeResult { if target_ptr.is_null() { return NativeResult::Ok(target_val); } - { - let target = unsafe { &mut *target_ptr }; - for &arg_reg in args.iter().skip(2) { - let source_val = vm.reg(arg_reg); - if !source_val.is_object() { - continue; - } - let source_ptr = source_val.as_js_object_ptr(); - if source_ptr.is_null() { - continue; - } - let source = unsafe { &*source_ptr }; - let source_keys = walk_own_keys(vm, source); - for (_si, offset) in source_keys { - let prop_val = source.get_prop_at(offset); - target.push_prop(prop_val); - } + + let mut all_assignments: Vec<(u32, JsValue, JsValue)> = Vec::new(); + for &arg_reg in args.iter().skip(2) { + let source_val = vm.reg(arg_reg); + if !source_val.is_object() { + continue; } + let source_ptr = source_val.as_js_object_ptr(); + if source_ptr.is_null() { + continue; + } + let source_keys: Vec<(u32, u32)> = { + let source = unsafe { &*source_ptr }; + walk_own_keys(vm, source) + }; + for (si, offset) in source_keys { + let source = unsafe { &*source_ptr }; + let val = source.get_prop_at(offset); + all_assignments.push((si, source_val, val)); + } + } + + let target = unsafe { &mut *target_ptr }; + for (si, source_val, val) in all_assignments { + let promoted = vm.promote_if_needed_for_write_ptr(target_ptr, val); + let _ = vm.ordinary_set(target, si, promoted, source_val); } NativeResult::Ok(target_val) } @@ -139,23 +147,26 @@ pub fn object_is(vm: &mut H, args: &[u8]) -> NativeResult { pub fn object_define_property(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 4 { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(crate::error::create_type_error( + vm, + "Object.defineProperty: expected at least 3 arguments", + )); } let obj_val = vm.reg(args[1]); if !obj_val.is_object() { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(crate::error::create_type_error(vm, "Object.defineProperty called on non-object")); } let obj_ptr = obj_val.as_js_object_ptr(); if obj_ptr.is_null() { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(crate::error::create_type_error(vm, "Object.defineProperty called on non-object")); } let desc_val = vm.reg(args[3]); if !desc_val.is_object() { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(crate::error::create_type_error(vm, "Property description must be an object")); } let desc_ptr = desc_val.as_js_object_ptr(); if desc_ptr.is_null() { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(crate::error::create_type_error(vm, "Property description must be an object")); } let prop_name_str = oxide_runtime_api::to_string(vm.reg(args[2])); let si = vm.kernel_core().perm_interner().intern(&prop_name_str).0; diff --git a/crates/oxide_builtins/tests/object_boundary_tests.rs b/crates/oxide_builtins/tests/object_boundary_tests.rs new file mode 100644 index 0000000..4bd14df --- /dev/null +++ b/crates/oxide_builtins/tests/object_boundary_tests.rs @@ -0,0 +1,101 @@ +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> Result<(Vm, JsValue), String> { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?; + let mut vm = Vm::new(); + let result = vm.run(&module)?; + Ok((vm, result)) +} + +#[test] +fn object_keys_no_args_type_error() { + assert!(eval("Object.keys()").is_err(), "Object.keys() should throw TypeError"); +} + +#[test] +fn object_keys_non_object_type_error() { + assert!(eval("Object.keys(42)").is_err(), "Object.keys(42) should throw TypeError"); +} + +#[test] +fn object_create_null_proto() { + let (_vm, result) = eval("var o = Object.create(null); o").unwrap(); + assert!(result.is_object()); + let obj_ptr = result.as_js_object_ptr(); + assert!(!obj_ptr.is_null()); + let obj = unsafe { &*obj_ptr }; + let proto = obj.proto(); + assert!(proto.is_null(), "Object.create(null) should set prototype to null"); +} + +#[test] +fn object_create_number_proto_type_error() { + assert!(eval("Object.create(42)").is_err(), "Object.create(42) should throw TypeError"); +} + +#[test] +fn object_create_no_args_type_error() { + assert!(eval("Object.create()").is_err(), "Object.create() should throw TypeError"); +} + +#[test] +fn object_get_own_property_names_no_args_type_error() { + assert!( + eval("Object.getOwnPropertyNames()").is_err(), + "Object.getOwnPropertyNames() should throw TypeError" + ); +} + +#[test] +fn object_get_own_property_names_non_object_type_error() { + assert!( + eval("Object.getOwnPropertyNames(42)").is_err(), + "Object.getOwnPropertyNames(42) should throw TypeError" + ); +} + +#[test] +fn object_keys_valid_object() { + let (_vm, result) = eval("var o = {a:1, b:2}; Object.keys(o).join(',')").unwrap(); + assert!(result.is_string(), "Object.keys should return an array"); +} + +#[test] +fn test_define_property_data_and_accessor_conflict() { + assert!( + eval("Object.defineProperty({}, 'x', {value: 1, get: function() {}})").is_err(), + "data+accessor conflict should throw TypeError" + ); +} + +#[test] +fn test_define_property_non_object_target() { + assert!( + eval("Object.defineProperty(null, 'x', {value: 1})").is_err(), + "non-object target should throw TypeError" + ); +} + +#[test] +fn test_define_property_missing_descriptor() { + assert!( + eval("Object.defineProperty({}, 'x')").is_err(), + "missing descriptor should throw TypeError" + ); +} + +#[test] +fn test_assign_skips_null_source() { + let (_vm, result) = eval("var t = {a:1}; Object.assign(t, null); t.a").unwrap(); + assert_eq!(result.as_int(), 1, "Object.assign should skip null sources"); +} + +#[test] +fn test_assign_merges_two_sources() { + let (_vm, result) = eval("var t = {a:1}; Object.assign(t, {b:2}); t.b").unwrap(); + assert!(result.is_int() || result.is_double(), "Object.assign should copy from source"); +} diff --git a/crates/oxide_builtins/tests/runtime_api_tests.rs b/crates/oxide_builtins/tests/runtime_api_tests.rs new file mode 100644 index 0000000..a4a9e4c --- /dev/null +++ b/crates/oxide_builtins/tests/runtime_api_tests.rs @@ -0,0 +1,82 @@ +use oxide_runtime_api::{same_value_zero, to_integer_or_infinity, to_length}; +use oxide_types::value::JsValue; + +#[test] +fn test_to_integer_or_infinity_nan() { + assert_eq!(to_integer_or_infinity(JsValue::float(f64::NAN)), 0.0); +} + +#[test] +fn test_to_integer_or_infinity_zero() { + assert_eq!(to_integer_or_infinity(JsValue::float(0.0)), 0.0); + assert_eq!(to_integer_or_infinity(JsValue::float(-0.0)), 0.0); +} + +#[test] +fn test_to_integer_or_infinity_infinity() { + assert_eq!(to_integer_or_infinity(JsValue::float(f64::INFINITY)), f64::INFINITY); + assert_eq!(to_integer_or_infinity(JsValue::float(f64::NEG_INFINITY)), f64::NEG_INFINITY); +} + +#[test] +fn test_to_integer_or_infinity_truncation() { + assert_eq!(to_integer_or_infinity(JsValue::float(3.7)), 3.0); + assert_eq!(to_integer_or_infinity(JsValue::float(-3.7)), -3.0); +} + +#[test] +fn test_to_integer_or_infinity_int() { + assert_eq!(to_integer_or_infinity(JsValue::int(42)), 42.0); +} + +#[test] +fn test_to_length_nan() { + assert_eq!(to_length(JsValue::float(f64::NAN)), 0); +} + +#[test] +fn test_to_length_negative() { + assert_eq!(to_length(JsValue::float(-1.0)), 0); + assert_eq!(to_length(JsValue::int(-42)), 0); +} + +#[test] +fn test_to_length_positive() { + assert_eq!(to_length(JsValue::float(100.0)), 100); + assert_eq!(to_length(JsValue::int(999)), 999); +} + +#[test] +fn test_to_length_overflow_clamp() { + assert_eq!(to_length(JsValue::float(1e100)), 9_007_199_254_740_991); +} + +#[test] +fn test_same_value_zero_nan() { + assert!(same_value_zero(JsValue::float(f64::NAN), JsValue::float(f64::NAN))); +} + +#[test] +fn test_same_value_zero_positive_and_negative_zero() { + assert!(same_value_zero(JsValue::float(0.0), JsValue::float(-0.0))); + assert!(same_value_zero(JsValue::float(-0.0), JsValue::float(0.0))); +} + +#[test] +fn test_same_value_zero_equal_numbers() { + assert!(same_value_zero(JsValue::float(42.0), JsValue::float(42.0))); + assert!(same_value_zero(JsValue::int(42), JsValue::int(42))); + assert!(same_value_zero(JsValue::int(42), JsValue::float(42.0))); +} + +#[test] +fn test_same_value_zero_different_numbers() { + assert!(!same_value_zero(JsValue::float(1.0), JsValue::float(2.0))); +} + +#[test] +fn test_same_value_zero_null_undefined() { + assert!(same_value_zero(JsValue::null(), JsValue::null())); + assert!(same_value_zero(JsValue::undefined(), JsValue::undefined())); + assert!(!same_value_zero(JsValue::null(), JsValue::undefined())); +} diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index d360379..6912727 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -442,6 +442,38 @@ pub fn same_value(lhs: JsValue, rhs: JsValue) -> bool { false } +/// ToIntegerOrInfinity(argument) — ECMA-262 §7.1.4. +pub fn to_integer_or_infinity(val: JsValue) -> f64 { + let n = to_number(val); + if n.is_nan() || n == 0.0 { + 0.0 + } else if n.is_infinite() { + n + } else { + n.trunc() + } +} + +/// ToLength(argument) — ECMA-262 §7.1.20. +pub fn to_length(val: JsValue) -> u64 { + let n = to_number(val); + let len = if n.is_nan() || n <= 0.0 { 0.0 } else { n.min(9_007_199_254_740_991.0) }; + len.trunc() as u64 +} + +/// SameValueZero(x, y) — ECMA-262 §7.2.11. +pub fn same_value_zero(lhs: JsValue, rhs: JsValue) -> bool { + if (lhs.is_double() || lhs.is_int()) && (rhs.is_double() || rhs.is_int()) { + let a = to_f64(lhs); + let b = to_f64(rhs); + if a.is_nan() && b.is_nan() { + return true; + } + return a == b; + } + same_value(lhs, rhs) +} + pub fn strict_equality(lhs: JsValue, rhs: JsValue) -> bool { if lhs.is_double() && rhs.is_double() { return strict_double_eq(lhs.as_double(), rhs.as_double());