fix: 对象建键入口统一走 property_key_si 规范化,数字键不再分裂

Object.fromEntries/JSON.parse/new Map 对象 entry/rest 解构静态 excluded
四处直接 intern 动态字符串键绕过整数键区间映射,规范数字键分裂为
字符串键与整数键两个键。统一经 string_key_si 规范化:规范数字串映射
整数键,其余 intern 字符串键,与 property_key_si 字符串分支口径一致。
make_int_key 增加 debug 断言兜底索引越界。
This commit is contained in:
2026-08-14 22:37:46 +08:00
parent 540153030b
commit b82fe9af32
12 changed files with 145 additions and 9 deletions
+2 -1
View File
@@ -139,7 +139,8 @@ fn value_to_jsvalue<H: VmHost>(vm: &mut H, val: &serde_json::Value) -> JsValue {
let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject; 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)); let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto));
for (key, val) in map { 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 jsv = value_to_jsvalue(vm, val);
let new_shape = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), si); let new_shape = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), si);
obj.set_shape_id(new_shape); obj.set_shape_id(new_shape);
+4 -2
View File
@@ -210,8 +210,10 @@ pub fn map_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if !crate::iterator::is_callable(adder) { if !crate::iterator::is_callable(adder) {
return NativeResult::Err(crate::error::create_type_error(vm, "Map.set is not callable")); 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; // entry 读取键按 ToPropertyKey 规范化:数组 entry 的元素区与对象 entry
let value_si = vm.kernel_core().perm_interner().intern("1").0; // 的 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 let Err(err) = crate::iterator::iterate_elements(vm, iterable, |vm, item| {
if !item.is_object() { if !item.is_object() {
return Err(crate::error::create_type_error(vm, "iterator value is not an entry object")); return Err(crate::error::create_type_error(vm, "iterator value is not an entry object"));
+4 -3
View File
@@ -962,7 +962,8 @@ pub fn object_from_entries<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
JsValue::from_js_object(vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject), JsValue::from_js_object(vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject),
)); ));
let target_val = JsValue::from_js_object(obj); 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 { for i in 0..n {
let pair_val = unsafe { (*entries_ptr).get_prop_at(i) }; let pair_val = unsafe { (*entries_ptr).get_prop_at(i) };
if !pair_val.is_object() { if !pair_val.is_object() {
@@ -975,8 +976,8 @@ pub fn object_from_entries<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let pair = unsafe { &*pair_ptr }; let pair = unsafe { &*pair_ptr };
let key_val = pair.get_prop_at(0); let key_val = pair.get_prop_at(0);
let value_val = pair.get_prop_at(1); let value_val = pair.get_prop_at(1);
let key_str = oxide_runtime_api::to_string(key_val); // ToPropertyKey 语义建键:int/规范数字串/symbol 统一映射,避免数字键分裂。
let si = vm.kernel_core().perm_interner().intern(&key_str).0; let si = vm.property_key_si(key_val);
let promoted = vm.promote_if_needed_for_write_ptr(obj, value_val); let promoted = vm.promote_if_needed_for_write_ptr(obj, value_val);
let _ = vm.ordinary_set(unsafe { &mut *obj }, si, promoted, target_val); let _ = vm.ordinary_set(unsafe { &mut *obj }, si, promoted, target_val);
} }
+3
View File
@@ -109,6 +109,9 @@ pub trait VmHost {
// 属性解析 // 属性解析
fn property_key_si(&mut self, val: JsValue) -> u32; 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<JsValue>; fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option<JsValue>;
fn get_own_property_slot(&self, obj: &JsObject, prop_name_si: u32) -> Option<u32>; fn get_own_property_slot(&self, obj: &JsObject, prop_name_si: u32) -> Option<u32>;
+2 -1
View File
@@ -28,9 +28,10 @@ pub const INT_KEY_COUNT: u32 = 0x4000_0000;
/// 把非负小整数索引编码为整数属性键(与 [`int_key_value`] 互逆)。 /// 把非负小整数索引编码为整数属性键(与 [`int_key_value`] 互逆)。
/// ///
/// 调用方须保证 `i < INT_KEY_COUNT`,否则溢出到 private 区间。 /// 调用方须保证 `i < INT_KEY_COUNT`,否则溢出到 private 区间(debug 构建断言兜底)。
#[inline] #[inline]
pub const fn make_int_key(i: u32) -> u32 { pub const fn make_int_key(i: u32) -> u32 {
debug_assert!(i < INT_KEY_COUNT, "整数键索引超出 INT_KEY_COUNT");
INT_KEY_BASE + i INT_KEY_BASE + i
} }
+7
View File
@@ -1911,6 +1911,13 @@ impl oxide_runtime_api::VmHost for Vm {
self.property_key_si(val) self.property_key_si(val)
.unwrap_or_else(|_| self.kernel_core.perm_interner().intern("").0) .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<JsValue> { fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option<JsValue> {
self.resolve_property(obj, prop_name_si) self.resolve_property(obj, prop_name_si)
} }
+8 -2
View File
@@ -768,7 +768,7 @@ impl Vm {
let mut excluded: std::collections::HashSet<u32> = excluded_const let mut excluded: std::collections::HashSet<u32> = excluded_const
.split('\0') .split('\0')
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.map(|s| self.kernel_core.perm_interner().intern(s).0) .map(|s| self.string_key_si(s))
.collect(); .collect();
// 运行时 excluded:b 槽数组(computed key 求值结果)的元素 ToPropertyKey 后排除。 // 运行时 excluded:b 槽数组(computed key 求值结果)的元素 ToPropertyKey 后排除。
if b != 0 { if b != 0 {
@@ -798,6 +798,9 @@ impl Vm {
let code_units: Vec<u16> = s.encode_utf16().collect(); let code_units: Vec<u16> = s.encode_utf16().collect();
for (i, unit) in code_units.iter().enumerate() { for (i, unit) in code_units.iter().enumerate() {
let si = make_int_key(i as u32); let si = make_int_key(i as u32);
if excluded.contains(&si) {
continue;
}
let ch_val = match char::from_u32(*unit as u32) { let ch_val = match char::from_u32(*unit as u32) {
Some(c) => match self.single_char(c) { Some(c) => match self.single_char(c) {
Some(v) => v, Some(v) => v,
@@ -809,7 +812,7 @@ impl Vm {
} }
} }
// 数组元素区:整数下标可枚举元素(hole 跳过)。 // 数组元素区:整数下标可枚举元素(hole 跳过),排除 pattern 已绑定的键。
if src_obj.is_array() { if src_obj.is_array() {
for i in 0..src_obj.array_prop_count { for i in 0..src_obj.array_prop_count {
if src_obj.prop_meta_at(i).is_some_and(|m| m.is_hole()) { 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()); .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable());
if enumerable { if enumerable {
let si = make_int_key(i); let si = make_int_key(i);
if excluded.contains(&si) {
continue;
}
let val = match self.ordinary_get(src_obj, si, src) { let val = match self.ordinary_get(src_obj, si, src) {
Ok(v) => v, Ok(v) => v,
Err(e) => return self.raise_call_error(&e).map(|_| ()), Err(e) => return self.raise_call_error(&e).map(|_| ()),
@@ -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(); let r = eval(&mut vm, "var m = new Map(); m.set('x', 10); m.delete('x'); m.has('x')").unwrap();
assert!(!r.as_bool()); 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");
}
@@ -185,3 +185,33 @@ fn object_values_returns_array() {
let obj = unsafe { &*result.as_js_object_ptr() }; let obj = unsafe { &*result.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 2); 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");
}
@@ -130,3 +130,33 @@ fn standalone_nested_rest_assignment() {
let result = eval(&mut vm, "var x; [...[x]]=[1,2,3]; x").unwrap(); let result = eval(&mut vm, "var x; [...[x]]=[1,2,3]; x").unwrap();
assert_num(result, 1.0); 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");
}
+27
View File
@@ -274,3 +274,30 @@ fn json_stringify_cycle_throws_type_error() {
let err = eval(&mut vm, "var a = {}; a.self = a; JSON.stringify(a)"); let err = eval(&mut vm, "var a = {}; a.self = a; JSON.stringify(a)");
assert!(err.is_err()); 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}");
}
@@ -110,3 +110,15 @@ fn keys_with_leading_zero_not_integer_index() {
let idx_1 = keys.iter().position(|k| k == "1").unwrap(); let idx_1 = keys.iter().position(|k| k == "1").unwrap();
assert!(idx_1 < idx_01, "integer '1' should come before string '01'"); 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);
}