diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 065ce96..eead06e 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -2,8 +2,8 @@ use oxide_kernel::shape_forge::{ShapeForge, EMPTY_SHAPE_ID}; use oxide_kernel::string_forge::PermInterner; use oxide_types::object::{JsObject, PropAttributes, PropMetaEntry}; use oxide_types::private_key::{ - int_key_value, is_int_key, is_private_name_key, is_symbol_key, make_well_known_symbol_key, symbol_index_from_key, - well_known_symbol_id_from_key, + int_key_value, is_int_key, is_private_name_key, is_symbol_key, make_int_key, make_well_known_symbol_key, + symbol_index_from_key, well_known_symbol_id_from_key, }; use oxide_types::value::JsValue; @@ -16,10 +16,20 @@ fn is_integer_index(key: &str) -> bool { key.bytes().all(|b| b.is_ascii_digit()) && key.parse::().unwrap_or(u64::MAX) < (1u64 << 32) - 1 } -/// 收集对象全部自身属性(shape 链),按规范顺序排列:整数索引在前升序,其余保持插入序。 -/// 数组元素区(整数下标)不在此列,调用方需另行枚举。 +/// 收集对象全部自身属性(数组元素区 + shape 链),按规范顺序排列:整数索引在前 +/// 升序,其余保持插入序。返回 `(属性键 si, 绝对存储索引)`:数组对象元素区索引即 +/// 绝对下标,命名属性 = `array_prop_count + shape 槽位`;普通对象即 shape 槽位。 pub fn walk_own_keys(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> { let mut keys: Vec<(u32, u32)> = Vec::new(); + // 数组元素区:整数下标是可枚举自身属性(hole 视为不存在),排在命名属性之前。 + if obj.is_array() { + for i in 0..obj.array_prop_count { + if obj.prop_meta_at(i).is_some_and(|m| m.is_hole()) { + continue; + } + keys.push((make_int_key(i), i)); + } + } let shape_id = obj.shape_id(); let mut pos: u32 = 0; let mut shape_ids = Vec::new(); @@ -30,11 +40,9 @@ pub fn walk_own_keys(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> { } if let Some(shape) = vm.kernel_core().shape_forge().get_shape(id) { cursor = shape.parent; - // Symbol 键/私有名键非字符串属性名,排除在字符串枚举之外。 - if shape.property_name != u32::MAX - && !is_symbol_key(shape.property_name) - && !is_private_name_key(shape.property_name) - { + // Symbol 键/私有名键非字符串属性名,排除在字符串枚举之外(但仍占 shape + // 槽位,pos 计数须含它们才能与物理存储对齐)。 + if shape.property_name != u32::MAX { shape_ids.push(id); } } else { @@ -47,7 +55,9 @@ pub fn walk_own_keys(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> { && !is_symbol_key(shape.property_name) && !is_private_name_key(shape.property_name) { - keys.push((shape.property_name, pos)); + // 绝对存储索引:数组命名属性位于元素区之后。 + let store = if obj.is_array() { obj.array_prop_count + pos } else { pos }; + keys.push((shape.property_name, store)); } } pos += 1; @@ -236,10 +246,9 @@ pub fn delete_own_property(vm: &mut H, obj: &mut JsObject, key_si: u3 let Some(delete_pos) = all_keys.iter().find(|(si, _)| *si == key_si).map(|(_, pos)| *pos) else { return true; }; - // delete_pos 是 shape 槽位;数组对象存储索引 = 元素数 + 槽位。 - let delete_store = if obj.is_array() { obj.array_prop_count + delete_pos } else { delete_pos }; + // walk_own_keys 已返回绝对存储索引(数组含元素区偏移),直接使用。 if obj - .prop_meta_at(delete_store) + .prop_meta_at(delete_pos) .map(|meta| !meta.attributes.configurable()) .unwrap_or(false) { @@ -260,11 +269,9 @@ pub fn delete_own_property(vm: &mut H, obj: &mut JsObject, key_si: u3 let retained: Vec<(u32, JsValue, Option)> = all_keys .into_iter() - .filter(|(_, pos)| *pos != delete_pos) - .map(|(si, pos)| { - let store = if obj.is_array() { obj.array_prop_count + pos } else { pos }; - (si, obj.get_prop_at(store), obj.prop_meta_at(store)) - }) + // 数组元素区由下方独立保存/恢复,此处只重建命名属性(元素键绝对下标 < 元素数)。 + .filter(|(_, pos)| *pos != delete_pos && !(obj.is_array() && *pos < obj.array_prop_count)) + .map(|(si, pos)| (si, obj.get_prop_at(pos), obj.prop_meta_at(pos))) .collect(); // 重建 shape 链与属性表(数组先清空,含元素区,随后恢复)。 @@ -446,7 +453,7 @@ pub fn object_assign(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Ok(target_val); } - let mut all_assignments: Vec<(u32, JsValue, JsValue)> = Vec::new(); + let mut all_assignments: Vec<(u32, JsValue)> = Vec::new(); for &arg_reg in args.iter().skip(2) { let source_val = vm.reg(arg_reg); if !source_val.is_object() { @@ -459,18 +466,35 @@ pub fn object_assign(vm: &mut H, args: &[u8]) -> NativeResult { let source_keys: Vec<(u32, u32)> = { let source = unsafe { &*source_ptr }; walk_own_keys(vm, source) + .into_iter() + // CopyDataProperties:只拷贝可枚举自身属性(无显式 meta 视为可枚举)。 + .filter(|(_si, offset)| { + source + .prop_meta_at(*offset) + .map(|m| m.attributes.enumerable()) + .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()) + }) + .collect() }; - for (si, offset) in source_keys { + 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)); + // ordinary_get 取值(receiver = 源对象):数据属性等价直读,访问器属性 + // 触发 getter;getter 抛错按规范中断整个 assign。 + let val = match vm.ordinary_get(source, si, source_val) { + Ok(v) => v, + Err(err) => return NativeResult::Err(crate::error::create_type_error(vm, &err)), + }; + all_assignments.push((si, val)); } } let target = unsafe { &mut *target_ptr }; - for (si, source_val, val) in all_assignments { + for (si, 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); + // Set(to, key, value, true):receiver 为目标对象(目标同名 setter 的 this 指向 target)。 + if let Err(err) = vm.ordinary_set(target, si, promoted, target_val) { + return NativeResult::Err(crate::error::create_type_error(vm, &err)); + } } NativeResult::Ok(target_val) } @@ -803,11 +827,10 @@ pub fn object_freeze(vm: &mut H, args: &[u8]) -> NativeResult { let obj_ptr = val.as_js_object_ptr(); { let obj = unsafe { &mut *obj_ptr }; - // 命名属性:shape 链(数组对象存储索引 = 元素数 + 槽位)。 + // 命名属性:walk_own_keys 返回绝对存储索引(数组含元素区偏移)。 let keys = walk_own_keys(vm, obj); for (_si, pos) in keys { - let store = if obj.is_array() { obj.array_prop_count + pos } else { pos }; - freeze_own_prop_meta(obj, store); + freeze_own_prop_meta(obj, pos); } // 数组元素区独立于 shape 链(hole 非 own 属性,跳过)。 if obj.is_array() { @@ -842,8 +865,7 @@ pub fn object_seal(vm: &mut H, args: &[u8]) -> NativeResult { let obj = unsafe { &mut *obj_ptr }; let keys = walk_own_keys(vm, obj); for (_si, pos) in keys { - let store = if obj.is_array() { obj.array_prop_count + pos } else { pos }; - seal_own_prop_meta(obj, store); + seal_own_prop_meta(obj, pos); } if obj.is_array() { for i in 0..obj.array_prop_count { diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index 63fe0fc..ac74a23 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -766,36 +766,12 @@ impl Vm { } } - // 数组元素区:整数下标可枚举元素(hole 跳过),排除 pattern 已绑定的键。 - if src_obj.is_array() { - for i in 0..src_obj.array_prop_count { - if src_obj.prop_meta_at(i).is_some_and(|m| m.is_hole()) { - continue; - } - let enumerable = src_obj - .prop_meta_at(i) - .map(|m| m.attributes.enumerable()) - .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()); - if enumerable { - let si = make_int_key(i); - if excluded.contains(&si) { - continue; - } - let val = match self.ordinary_get(src_obj, si, src) { - Ok(v) => v, - Err(e) => return self.raise_call_error(&e).map(|_| ()), - }; - assignments.push((si, val)); - } - } - } - - // 命名属性:shape 链(walk_own_keys 规范顺序),仅可枚举,跳过 pattern 已绑定的键。 + // 自身属性:walk_own_keys 已合并数组元素区(hole 跳过)并返回绝对存储索引, + // 仅可枚举,跳过 pattern 已绑定的键。 let keys = oxide_builtins::object::walk_own_keys(self, src_obj); for (si, pos) in keys { - let store = if src_obj.is_array() { src_obj.array_prop_count + pos } else { pos }; let enumerable = src_obj - .prop_meta_at(store) + .prop_meta_at(pos) .map(|m| m.attributes.enumerable()) .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()); if !enumerable { @@ -882,30 +858,12 @@ impl Vm { // 再统一写目标,避免提交写与取值互相交错。 let mut assignments: Vec<(u32, JsValue)> = Vec::new(); - // 数组元素区:整数下标可枚举元素(hole 跳过)。 - if src_obj.is_array() { - for i in 0..src_obj.array_prop_count { - if src_obj.prop_meta_at(i).is_some_and(|m| m.is_hole()) { - continue; - } - let enumerable = src_obj - .prop_meta_at(i) - .map(|m| m.attributes.enumerable()) - .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()); - if enumerable { - let si = make_int_key(i); - let val = self.ordinary_get(src_obj, si, src)?; - assignments.push((si, val)); - } - } - } - - // 命名属性:shape 链(walk_own_keys 规范顺序),仅可枚举。 + // 自身属性:walk_own_keys 已合并数组元素区(hole 跳过)并返回绝对存储索引, + // 仅可枚举;取值经 ordinary_get 触发访问器 getter。 let keys = oxide_builtins::object::walk_own_keys(self, src_obj); for (si, pos) in keys { - let store = if src_obj.is_array() { src_obj.array_prop_count + pos } else { pos }; let enumerable = src_obj - .prop_meta_at(store) + .prop_meta_at(pos) .map(|m| m.attributes.enumerable()) .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()); if !enumerable { diff --git a/crates/oxide_vm/tests/builtins_object_tests.rs b/crates/oxide_vm/tests/builtins_object_tests.rs index 4be0606..9750106 100644 --- a/crates/oxide_vm/tests/builtins_object_tests.rs +++ b/crates/oxide_vm/tests/builtins_object_tests.rs @@ -215,3 +215,114 @@ fn from_entries_multiple_pairs_roundtrip() { let (_vm, result) = eval("var o = Object.fromEntries([[1,'a'],[2,'b']]); o[1] + o[2]").unwrap(); assert!(result.is_string() && unsafe { &*result.as_string_ptr() }.as_str() == "ab"); } + +// -- Object.keys/values/entries 数组元素区 -- + +#[test] +fn keys_array_includes_integer_elements() { + let (_vm, result) = eval("Object.keys([1,2]).join(',')").unwrap(); + let s = unsafe { &*result.as_string_ptr() }.to_owned_string(); + assert_eq!(s, "0,1"); +} + +#[test] +fn keys_array_skips_holes() { + let (_vm, result) = eval("Object.keys([1,,3]).join(',')").unwrap(); + let s = unsafe { &*result.as_string_ptr() }.to_owned_string(); + assert_eq!(s, "0,2"); +} + +#[test] +fn entries_array_reads_element_values() { + let (_vm, result) = eval("var e = Object.entries([1,2]); e[1][1]").unwrap(); + assert!( + result.is_int() && result.as_int() == 2, + "second entry value should be 2, got {:?}", + result + ); +} + +#[test] +fn keys_array_mixed_named_prop_order() { + // 整数下标升序在前,命名属性按插入序随后(无重复键)。 + let (_vm, result) = eval("var a = [10]; a.foo = 1; Object.keys(a).join(',')").unwrap(); + let s = unsafe { &*result.as_string_ptr() }.to_owned_string(); + assert_eq!(s, "0,foo"); +} + +#[test] +fn keys_array_non_enumerable_element_filtered() { + // defineProperty 把元素改为不可枚举后 keys 应省略(values/entries 同理)。 + let (_vm, result) = + eval("var a = [1, 2]; Object.defineProperty(a, '1', {enumerable: false}); Object.keys(a).join(',')").unwrap(); + let s = unsafe { &*result.as_string_ptr() }.to_owned_string(); + assert_eq!(s, "0"); +} + +// -- Object.assign -- + +#[test] +fn assign_filters_non_enumerable_source() { + let (_vm, result) = eval( + "var src = Object.defineProperty({}, 'x', {value: 1, enumerable: false}); JSON.stringify(Object.assign({}, src))", + ) + .unwrap(); + let s = unsafe { &*result.as_string_ptr() }.to_owned_string(); + assert_eq!(s, "{}"); +} + +#[test] +fn assign_triggers_source_getter() { + let (_vm, result) = eval("var n = 0; var t = Object.assign({}, {get b(){ n++; return 2 }}); [t.b, n]").unwrap(); + let obj = unsafe { &*result.as_js_object_ptr() }; + assert!( + obj.get_prop_at(0).is_int() && obj.get_prop_at(0).as_int() == 2, + "getter value should be copied" + ); + assert!( + obj.get_prop_at(1).is_int() && obj.get_prop_at(1).as_int() == 1, + "getter should run exactly once" + ); +} + +#[test] +fn assign_from_array_source() { + let (_vm, result) = eval("var t = Object.assign({}, [1, 2]); [t['0'], t['1']]").unwrap(); + let obj = unsafe { &*result.as_js_object_ptr() }; + assert!(obj.get_prop_at(0).is_int() && obj.get_prop_at(0).as_int() == 1); + assert!(obj.get_prop_at(1).is_int() && obj.get_prop_at(1).as_int() == 2); +} + +#[test] +fn assign_target_setter_receives_target() { + // 规范 Set(to, key, value, true):target 同名 setter 的 this 是 target。 + let (_vm, result) = eval( + "var recv = null; var t = {}; Object.defineProperty(t, 'x', {set: function(v){ recv = this; }, configurable: true}); Object.assign(t, {x: 1}); recv === t", + ) + .unwrap(); + assert!(result.is_bool() && result.as_bool(), "target setter should receive target as this"); +} + +#[test] +fn assign_to_frozen_target_throws() { + let err = match eval("var t = Object.freeze({a: 1}); Object.assign(t, {b: 2});") { + Ok(_) => panic!("assign to frozen target should fail"), + Err(err) => err, + }; + assert!(err.contains("extensible"), "unexpected error: {err}"); +} + +// -- Symbol 键错位回归(shape 槽位计数含 symbol 键) -- + +#[test] +fn entries_values_with_interleaved_symbol_key() { + let (_vm, result) = eval( + "var s = Symbol(); var o = {a: 1, [s]: 2, b: 3}; [JSON.stringify(Object.entries(o)), JSON.stringify(Object.values(o))]", + ) + .unwrap(); + let obj = unsafe { &*result.as_js_object_ptr() }; + let s0 = unsafe { &*obj.get_prop_at(0).as_string_ptr() }.to_owned_string(); + let s1 = unsafe { &*obj.get_prop_at(1).as_string_ptr() }.to_owned_string(); + assert_eq!(s0, "[[\"a\",1],[\"b\",3]]"); + assert_eq!(s1, "[1,3]"); +}