From dc98074d7998b86a12794ab1aadbed110c32f430 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 14 Aug 2026 22:37:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AF=B9=E8=B1=A1=E5=BB=BA=E9=94=AE?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E7=BB=9F=E4=B8=80=E8=B5=B0=20property=5Fkey?= =?UTF-8?q?=5Fsi=20=E8=A7=84=E8=8C=83=E5=8C=96=EF=BC=8C=E6=95=B0=E5=AD=97?= =?UTF-8?q?=E9=94=AE=E4=B8=8D=E5=86=8D=E5=88=86=E8=A3=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Object.fromEntries/JSON.parse/new Map 对象 entry/rest 解构静态 excluded 四处直接 intern 动态字符串键绕过整数键区间映射,规范数字键分裂为 字符串键与整数键两个键。统一经 string_key_si 规范化:规范数字串映射 整数键,其余 intern 字符串键,与 property_key_si 字符串分支口径一致。 make_int_key 增加 debug 断言兜底索引越界。 --- crates/oxide_builtins/src/json.rs | 3 +- crates/oxide_builtins/src/map.rs | 6 ++-- crates/oxide_builtins/src/object.rs | 7 +++-- crates/oxide_runtime_api/src/lib.rs | 3 ++ crates/oxide_types/src/private_key.rs | 3 +- crates/oxide_vm/src/vm.rs | 7 +++++ crates/oxide_vm/src/vm_dispatch_misc.rs | 10 +++++-- crates/oxide_vm/tests/builtins_map_tests.rs | 16 ++++++++++ .../oxide_vm/tests/builtins_object_tests.rs | 30 +++++++++++++++++++ crates/oxide_vm/tests/destructuring_tests.rs | 30 +++++++++++++++++++ crates/oxide_vm/tests/json_tests.rs | 27 +++++++++++++++++ .../tests/property_keys_order_tests.rs | 12 ++++++++ 12 files changed, 145 insertions(+), 9 deletions(-) diff --git a/crates/oxide_builtins/src/json.rs b/crates/oxide_builtins/src/json.rs index 3daeb42b..6cf0b3da 100644 --- a/crates/oxide_builtins/src/json.rs +++ b/crates/oxide_builtins/src/json.rs @@ -139,7 +139,8 @@ fn value_to_jsvalue(vm: &mut H, val: &serde_json::Value) -> JsValue { let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject; let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)); for (key, val) in map { - let si = vm.kernel_core().perm_interner().intern(key).0; + // 键经字符串规范化:规范数字串("0"/"5")映射整数键,与属性访问统一。 + let si = vm.string_key_si(key); let jsv = value_to_jsvalue(vm, val); let new_shape = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), si); obj.set_shape_id(new_shape); diff --git a/crates/oxide_builtins/src/map.rs b/crates/oxide_builtins/src/map.rs index b7a3bddb..e54f49d3 100644 --- a/crates/oxide_builtins/src/map.rs +++ b/crates/oxide_builtins/src/map.rs @@ -210,8 +210,10 @@ pub fn map_constructor(vm: &mut H, args: &[u8]) -> NativeResult { if !crate::iterator::is_callable(adder) { return NativeResult::Err(crate::error::create_type_error(vm, "Map.set is not callable")); } - let key_si = vm.kernel_core().perm_interner().intern("0").0; - let value_si = vm.kernel_core().perm_interner().intern("1").0; + // entry 读取键按 ToPropertyKey 规范化:数组 entry 的元素区与对象 entry + // 的 shape 链都走整数键,保证 `[k,v]` 与 `{0:k,1:v}` 两种形态都命中。 + let key_si = vm.property_key_si(JsValue::int(0)); + let value_si = vm.property_key_si(JsValue::int(1)); if let Err(err) = crate::iterator::iterate_elements(vm, iterable, |vm, item| { if !item.is_object() { return Err(crate::error::create_type_error(vm, "iterator value is not an entry object")); diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 43ca8252..04127e82 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -962,7 +962,8 @@ pub fn object_from_entries(vm: &mut H, args: &[u8]) -> NativeResult { JsValue::from_js_object(vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject), )); let target_val = JsValue::from_js_object(obj); - let n: usize = unsafe { (*entries_ptr).prop_vec_len() }; + // entry 数组的元素存元素区(元素区之外才是命名属性区),按元素区长度迭代。 + let n: usize = unsafe { (*entries_ptr).array_prop_count } as usize; for i in 0..n { let pair_val = unsafe { (*entries_ptr).get_prop_at(i) }; if !pair_val.is_object() { @@ -975,8 +976,8 @@ pub fn object_from_entries(vm: &mut H, args: &[u8]) -> NativeResult { let pair = unsafe { &*pair_ptr }; let key_val = pair.get_prop_at(0); let value_val = pair.get_prop_at(1); - let key_str = oxide_runtime_api::to_string(key_val); - let si = vm.kernel_core().perm_interner().intern(&key_str).0; + // ToPropertyKey 语义建键:int/规范数字串/symbol 统一映射,避免数字键分裂。 + let si = vm.property_key_si(key_val); let promoted = vm.promote_if_needed_for_write_ptr(obj, value_val); let _ = vm.ordinary_set(unsafe { &mut *obj }, si, promoted, target_val); } diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index 1124ebbe..37f0ebe2 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -109,6 +109,9 @@ pub trait VmHost { // 属性解析 fn property_key_si(&mut self, val: JsValue) -> u32; + /// 字符串→键规范化:规范数字串(`"5"`)映射整数键,其余 intern 字符串键。 + /// 供建键入口(fromEntries/json/rest excluded)与 `property_key_si` 的字符串分支统一口径。 + fn string_key_si(&mut self, s: &str) -> u32; fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option; fn get_own_property_slot(&self, obj: &JsObject, prop_name_si: u32) -> Option; diff --git a/crates/oxide_types/src/private_key.rs b/crates/oxide_types/src/private_key.rs index f5e2eb0b..719ec312 100644 --- a/crates/oxide_types/src/private_key.rs +++ b/crates/oxide_types/src/private_key.rs @@ -28,9 +28,10 @@ pub const INT_KEY_COUNT: u32 = 0x4000_0000; /// 把非负小整数索引编码为整数属性键(与 [`int_key_value`] 互逆)。 /// -/// 调用方须保证 `i < INT_KEY_COUNT`,否则溢出到 private 区间。 +/// 调用方须保证 `i < INT_KEY_COUNT`,否则溢出到 private 区间(debug 构建断言兜底)。 #[inline] pub const fn make_int_key(i: u32) -> u32 { + debug_assert!(i < INT_KEY_COUNT, "整数键索引超出 INT_KEY_COUNT"); INT_KEY_BASE + i } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 2715e7c3..38bcfb9c 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -1911,6 +1911,13 @@ impl oxide_runtime_api::VmHost for Vm { self.property_key_si(val) .unwrap_or_else(|_| self.kernel_core.perm_interner().intern("").0) } + fn string_key_si(&mut self, s: &str) -> u32 { + if let Some(i) = canonical_index_of(s) { + make_int_key(i) + } else { + self.kernel_core.perm_interner().intern(s).0 + } + } fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option { self.resolve_property(obj, prop_name_si) } diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index d4dc9fbf..8b3c8a5a 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -768,7 +768,7 @@ impl Vm { let mut excluded: std::collections::HashSet = excluded_const .split('\0') .filter(|s| !s.is_empty()) - .map(|s| self.kernel_core.perm_interner().intern(s).0) + .map(|s| self.string_key_si(s)) .collect(); // 运行时 excluded:b 槽数组(computed key 求值结果)的元素 ToPropertyKey 后排除。 if b != 0 { @@ -798,6 +798,9 @@ impl Vm { let code_units: Vec = s.encode_utf16().collect(); for (i, unit) in code_units.iter().enumerate() { let si = make_int_key(i as u32); + if excluded.contains(&si) { + continue; + } let ch_val = match char::from_u32(*unit as u32) { Some(c) => match self.single_char(c) { Some(v) => v, @@ -809,7 +812,7 @@ impl Vm { } } - // 数组元素区:整数下标可枚举元素(hole 跳过)。 + // 数组元素区:整数下标可枚举元素(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()) { @@ -821,6 +824,9 @@ impl Vm { .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(|_| ()), diff --git a/crates/oxide_vm/tests/builtins_map_tests.rs b/crates/oxide_vm/tests/builtins_map_tests.rs index bffb3830..9961c197 100644 --- a/crates/oxide_vm/tests/builtins_map_tests.rs +++ b/crates/oxide_vm/tests/builtins_map_tests.rs @@ -113,3 +113,19 @@ fn map_new_delete() { let r = eval(&mut vm, "var m = new Map(); m.set('x', 10); m.delete('x'); m.has('x')").unwrap(); assert!(!r.as_bool()); } + +#[test] +fn map_new_object_entry_pair_reads_key_and_value() { + // 对象 entry({0:'k',1:'v'})的数字键是整数键:读取须经 ToPropertyKey 规范化。 + let mut vm = Vm::new(); + let r = eval(&mut vm, "var m = new Map([{0:'k',1:'v'}]); m.get('k')").unwrap(); + assert_eq!(str_val(&vm, r), "v"); +} + +#[test] +fn map_new_mixed_array_and_object_entries() { + // 数组 entry 与对象 entry 两种形态混合可共存。 + let mut vm = Vm::new(); + let r = eval(&mut vm, "var m = new Map([['a',1],{0:'b',1:2}]); m.get('a') + '/' + m.get('b')").unwrap(); + assert_eq!(str_val(&vm, r), "1/2"); +} diff --git a/crates/oxide_vm/tests/builtins_object_tests.rs b/crates/oxide_vm/tests/builtins_object_tests.rs index a4edeabc..4be0606c 100644 --- a/crates/oxide_vm/tests/builtins_object_tests.rs +++ b/crates/oxide_vm/tests/builtins_object_tests.rs @@ -185,3 +185,33 @@ fn object_values_returns_array() { let obj = unsafe { &*result.as_js_object_ptr() }; assert_eq!(obj.prop_count(), 2); } + +// -- Object.fromEntries -- + +#[test] +fn from_entries_numeric_key_normalized_to_int_key() { + // 数字键 entry 走 ToPropertyKey 规范化:fromEntries 产出的 "5" 键与 o[5] 同一键。 + let (_vm, result) = eval("Object.fromEntries([[5,'a']])[5]").unwrap(); + assert!(result.is_string() && unsafe { &*result.as_string_ptr() }.as_str() == "a"); +} + +#[test] +fn from_entries_string_index_key_reads_via_numeric_access() { + // 字符串 "5" 键是规范数组下标,读 o[5] 命中同一整数键。 + let (_vm, result) = eval("Object.fromEntries([['5','a']])[5]").unwrap(); + assert!(result.is_string() && unsafe { &*result.as_string_ptr() }.as_str() == "a"); +} + +#[test] +fn from_entries_numeric_key_write_keeps_single_key() { + // 再写 o[5] 不产生第二个 "5" 键(键唯一)。 + let (_vm, result) = + eval("var o = Object.fromEntries([[5,'a']]); o[5]='x'; Object.getOwnPropertyNames(o).join(',')").unwrap(); + assert!(result.is_string() && unsafe { &*result.as_string_ptr() }.as_str() == "5"); +} + +#[test] +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"); +} diff --git a/crates/oxide_vm/tests/destructuring_tests.rs b/crates/oxide_vm/tests/destructuring_tests.rs index b8614ddd..62fc9583 100644 --- a/crates/oxide_vm/tests/destructuring_tests.rs +++ b/crates/oxide_vm/tests/destructuring_tests.rs @@ -130,3 +130,33 @@ fn standalone_nested_rest_assignment() { let result = eval(&mut vm, "var x; [...[x]]=[1,2,3]; x").unwrap(); assert_num(result, 1.0); } + +#[test] +fn object_rest_excludes_numeric_bound_key_from_array_source() { + // 数组元素区整数键:pattern 绑定 "0" 后 rest 不得再包含 "0"。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "const {0:x, ...r} = [1,2,3]; Object.keys(r).length").unwrap(); + assert_eq!(result.as_int(), 2); + let result = eval(&mut vm, "const {0:x, ...r} = [1,2,3]; x === 1").unwrap(); + assert!(result.as_bool()); + // rest 从 1 开始:r[1] 是源元素 2,r[0] 已被排除为 undefined。 + let result = eval(&mut vm, "const {0:x, ...r} = [1,2,3]; r[1] + r[2]").unwrap(); + assert_eq!(result.as_int(), 5); +} + +#[test] +fn object_rest_excludes_multiple_numeric_keys() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "const {0:x, 1:y, ...r} = [1,2,3,4]; Object.keys(r).join(',')").unwrap(); + let s = unsafe { &*result.as_string_ptr() }.as_str().to_string(); + assert_eq!(s, "2,3"); +} + +#[test] +fn object_rest_keeps_numeric_key_from_shape_source() { + // 对象源数字键(shape 链整数键)同样排除。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "const {0:x, ...r} = {0:'a',1:'b',2:'c'}; Object.keys(r).join(',')").unwrap(); + let s = unsafe { &*result.as_string_ptr() }.as_str().to_string(); + assert_eq!(s, "1,2"); +} diff --git a/crates/oxide_vm/tests/json_tests.rs b/crates/oxide_vm/tests/json_tests.rs index ddbf6444..f7a495e8 100644 --- a/crates/oxide_vm/tests/json_tests.rs +++ b/crates/oxide_vm/tests/json_tests.rs @@ -274,3 +274,30 @@ fn json_stringify_cycle_throws_type_error() { let err = eval(&mut vm, "var a = {}; a.self = a; JSON.stringify(a)"); assert!(err.is_err()); } + +// -- 数字键对象(P1-2 回归防线) -- + +#[test] +fn json_parse_numeric_object_key_reads_via_index() { + // JSON 键 "5" 经字符串规范化映射整数键:o[5] 与 o["5"] 命中同一键。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.parse('{\"5\":1}')[5]").unwrap(); + assert!((result.as_double() - 1.0).abs() < 0.0001); +} + +#[test] +fn json_parse_numeric_keys_do_not_clash_between_keys() { + // 多个数字键各自独立:{"0":7,"2024":8} 的 0 与 2024 不互相覆盖。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "var o = JSON.parse('{\"0\":7,\"2024\":8}'); o[0] + o[2024]").unwrap(); + assert!((result.as_double() - 15.0).abs() < 0.0001); +} + +#[test] +fn json_parse_numeric_keys_stringify_roundtrip() { + // 数字键对象经 stringify 后键名保持 "5"(键字符串化语义)。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify(JSON.parse('{\"5\":1}'))").unwrap(); + let s = string_value(&vm, result); + assert_eq!(s, "{\"5\":1}"); +} diff --git a/crates/oxide_vm/tests/property_keys_order_tests.rs b/crates/oxide_vm/tests/property_keys_order_tests.rs index b74f2132..cd342329 100644 --- a/crates/oxide_vm/tests/property_keys_order_tests.rs +++ b/crates/oxide_vm/tests/property_keys_order_tests.rs @@ -110,3 +110,15 @@ fn keys_with_leading_zero_not_integer_index() { let idx_1 = keys.iter().position(|k| k == "1").unwrap(); assert!(idx_1 < idx_01, "integer '1' should come before string '01'"); } + +// ── 对象字面量数字键字符串化 ── +#[test] +fn numeric_literal_key_materializes_as_string() { + // `{5:1}` 的键是字符串 "5"(键经 ToPropertyKey 字符串化),读 o[5]/o["5"] 同一键。 + let r = eval_str("var o={5:1}; Object.getOwnPropertyNames(o)[0]").unwrap(); + assert!(r.is_string()); + let s = unsafe { &*r.as_string_ptr() }.data.clone(); + assert_eq!(s, "5"); + let r = eval_str("var o={5:1}; o[5] + o['5']").unwrap(); + assert!(r.is_int() && r.as_int() == 2); +}