mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
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:
@@ -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 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);
|
||||
|
||||
@@ -210,8 +210,10 @@ pub fn map_constructor<H: VmHost>(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"));
|
||||
|
||||
@@ -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),
|
||||
));
|
||||
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<H: VmHost>(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);
|
||||
}
|
||||
|
||||
@@ -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<JsValue>;
|
||||
fn get_own_property_slot(&self, obj: &JsObject, prop_name_si: u32) -> Option<u32>;
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<JsValue> {
|
||||
self.resolve_property(obj, prop_name_si)
|
||||
}
|
||||
|
||||
@@ -768,7 +768,7 @@ impl Vm {
|
||||
let mut excluded: std::collections::HashSet<u32> = 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<u16> = 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(|_| ()),
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user