diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 005f07ce..1dbd8914 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -339,62 +339,132 @@ macro_rules! native_try { pub fn object_freeze(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.freeze called on non-object")); } let val = vm.reg(args[1]); if !val.is_object() || val.as_js_object_ptr().is_null() { return NativeResult::Ok(val); } + let obj_ptr = val.as_js_object_ptr(); + unsafe { + (*obj_ptr).set_frozen(true); + (*obj_ptr).set_extensible(false); + } NativeResult::Ok(val) } pub fn object_seal(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.seal called on non-object")); } let val = vm.reg(args[1]); if !val.is_object() || val.as_js_object_ptr().is_null() { return NativeResult::Ok(val); } + let obj_ptr = val.as_js_object_ptr(); + unsafe { + (*obj_ptr).set_sealed(true); + (*obj_ptr).set_extensible(false); + } NativeResult::Ok(val) } pub fn object_prevent_extensions(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.preventExtensions called on non-object")); } let val = vm.reg(args[1]); if !val.is_object() || val.as_js_object_ptr().is_null() { return NativeResult::Ok(val); } + let obj_ptr = val.as_js_object_ptr(); + unsafe { + (*obj_ptr).set_extensible(false); + } NativeResult::Ok(val) } pub fn object_is_frozen(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { - return NativeResult::Ok(JsValue::bool(true)); + return NativeResult::Err(crate::error::create_type_error(vm, "Object.isFrozen called on non-object")); } let val = vm.reg(args[1]); - if !val.is_object() || val.as_js_object_ptr().is_null() { + if !val.is_object() { return NativeResult::Ok(JsValue::bool(true)); } - NativeResult::Ok(JsValue::bool(false)) + let obj_ptr = val.as_js_object_ptr(); + if obj_ptr.is_null() { + return NativeResult::Ok(JsValue::bool(true)); + } + let obj = unsafe { &*obj_ptr }; + if obj.is_frozen() { + return NativeResult::Ok(JsValue::bool(true)); + } + if obj.is_extensible() { + return NativeResult::Ok(JsValue::bool(false)); + } + let keys = walk_own_keys(vm, obj); + for (_si, offset) in keys { + if let Some(meta) = obj.prop_meta_at(offset) { + if meta.attributes.configurable() { + return NativeResult::Ok(JsValue::bool(false)); + } + if !meta.is_accessor && meta.attributes.writable() { + return NativeResult::Ok(JsValue::bool(false)); + } + } else { + return NativeResult::Ok(JsValue::bool(false)); + } + } + NativeResult::Ok(JsValue::bool(true)) } pub fn object_is_sealed(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { - return NativeResult::Ok(JsValue::bool(true)); + return NativeResult::Err(crate::error::create_type_error(vm, "Object.isSealed called on non-object")); } let val = vm.reg(args[1]); - if !val.is_object() || val.as_js_object_ptr().is_null() { + if !val.is_object() { return NativeResult::Ok(JsValue::bool(true)); } - NativeResult::Ok(JsValue::bool(false)) + let obj_ptr = val.as_js_object_ptr(); + if obj_ptr.is_null() { + return NativeResult::Ok(JsValue::bool(true)); + } + let obj = unsafe { &*obj_ptr }; + if obj.is_sealed() { + return NativeResult::Ok(JsValue::bool(true)); + } + if obj.is_extensible() { + return NativeResult::Ok(JsValue::bool(false)); + } + let keys = walk_own_keys(vm, obj); + for (_si, offset) in keys { + if let Some(meta) = obj.prop_meta_at(offset) { + if meta.attributes.configurable() { + return NativeResult::Ok(JsValue::bool(false)); + } + } else { + return NativeResult::Ok(JsValue::bool(false)); + } + } + NativeResult::Ok(JsValue::bool(true)) } pub fn object_is_extensible(vm: &mut H, args: &[u8]) -> NativeResult { - native_try!(require_obj_arg(vm, args, "isExtensible")); - NativeResult::Ok(JsValue::bool(true)) + if args.len() < 2 { + return NativeResult::Err(crate::error::create_type_error(vm, "Object.isExtensible called on non-object")); + } + let val = vm.reg(args[1]); + if !val.is_object() { + return NativeResult::Ok(JsValue::bool(false)); + } + let obj_ptr = val.as_js_object_ptr(); + if obj_ptr.is_null() { + return NativeResult::Ok(JsValue::bool(false)); + } + let obj = unsafe { &*obj_ptr }; + NativeResult::Ok(JsValue::bool(obj.is_extensible())) } pub fn object_get_own_property_names(vm: &mut H, args: &[u8]) -> NativeResult { diff --git a/crates/oxide_builtins/tests/object_boundary_tests.rs b/crates/oxide_builtins/tests/object_boundary_tests.rs index 4bd14dfc..06dc7182 100644 --- a/crates/oxide_builtins/tests/object_boundary_tests.rs +++ b/crates/oxide_builtins/tests/object_boundary_tests.rs @@ -99,3 +99,47 @@ 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"); } + +#[test] +fn test_integrity_freeze_returns_object() { + let (_vm, result) = eval("var o = {x:1}; Object.freeze(o) === o").unwrap(); + assert!(result.is_bool() && result.as_bool(), "freeze should return the object"); +} + +#[test] +fn test_integrity_freeze_sets_non_extensible() { + let (_vm, result) = eval("var o = {x:1}; Object.freeze(o); Object.isExtensible(o)").unwrap(); + assert!(result.is_bool() && !result.as_bool(), "freeze should set non-extensible"); +} + +#[test] +fn test_integrity_seal_non_extensible() { + let (_vm, result) = eval("var o = {x:1}; Object.seal(o); Object.isExtensible(o)").unwrap(); + assert!(result.is_bool() && !result.as_bool(), "seal should set non-extensible"); +} + +#[test] +fn test_integrity_prevent_extensions() { + let (_vm, result) = eval("var o = {x:1}; Object.preventExtensions(o); Object.isExtensible(o)").unwrap(); + assert!(result.is_bool() && !result.as_bool(), "preventExtensions should set non-extensible"); +} + +#[test] +fn test_is_frozen_non_object_returns_true() { + let (_vm, result) = eval("Object.isFrozen(42)").unwrap(); + assert!(result.is_bool() && result.as_bool(), "non-object should be frozen per ES spec"); +} + +#[test] +fn test_is_sealed_non_object_returns_true() { + let (_vm, result) = eval("Object.isSealed(42)").unwrap(); + assert!(result.is_bool() && result.as_bool(), "non-object should be sealed per ES spec"); +} + +#[test] +fn test_is_extensible_non_object_returns_false() { + let (_vm, result) = eval("Object.isExtensible(42)").unwrap(); + assert!(result.is_bool() && !result.as_bool(), "non-object should not be extensible per ES spec"); +} + + diff --git a/crates/oxide_builtins/tests/object_integrity_tests.rs b/crates/oxide_builtins/tests/object_integrity_tests.rs new file mode 100644 index 00000000..ce1b08b2 --- /dev/null +++ b/crates/oxide_builtins/tests/object_integrity_tests.rs @@ -0,0 +1,59 @@ +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +fn new_obj() -> JsObject { + JsObject::new_empty(0, JsValue::null()) +} + +#[test] +fn test_set_frozen() { + let mut obj = new_obj(); + assert!(!obj.is_frozen()); + obj.set_frozen(true); + assert!(obj.is_frozen()); +} + +#[test] +fn test_set_sealed() { + let mut obj = new_obj(); + assert!(!obj.is_sealed()); + obj.set_sealed(true); + assert!(obj.is_sealed()); +} + +#[test] +fn test_frozen_and_sealed_independent() { + let mut obj = new_obj(); + obj.set_frozen(true); + assert!(obj.is_frozen()); + assert!(!obj.is_sealed()); + obj.set_sealed(true); + assert!(obj.is_frozen()); + assert!(obj.is_sealed()); + obj.set_frozen(false); + assert!(!obj.is_frozen()); + assert!(obj.is_sealed()); +} + +#[test] +fn test_defaults_are_false() { + let obj = new_obj(); + assert!(!obj.is_frozen()); + assert!(!obj.is_sealed()); +} + +#[test] +fn test_clear_frozen() { + let mut obj = new_obj(); + obj.set_frozen(true); + obj.set_frozen(false); + assert!(!obj.is_frozen()); +} + +#[test] +fn test_clear_sealed() { + let mut obj = new_obj(); + obj.set_sealed(true); + obj.set_sealed(false); + assert!(!obj.is_sealed()); +} diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 72208698..e842ce8d 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -574,6 +574,30 @@ impl JsObject { } } + pub fn is_frozen(&self) -> bool { + (self._pad) & 1 != 0 + } + + pub fn set_frozen(&mut self, frozen: bool) { + if frozen { + self._pad |= 1 << 0; + } else { + self._pad &= !(1 << 0); + } + } + + pub fn is_sealed(&self) -> bool { + (self._pad >> 1) & 1 != 0 + } + + pub fn set_sealed(&mut self, sealed: bool) { + if sealed { + self._pad |= 1 << 1; + } else { + self._pad &= !(1 << 1); + } + } + pub fn is_set(&self) -> bool { (self.header >> 24) & 1 != 0 }