From 68e0ad7fa1c7e0a4231d3ba0cc425495c64e5417 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 21 Sep 2026 16:27:14 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=B1=BB=E6=9E=84=E9=80=A0=E5=99=A8?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E5=BA=8F=E4=BF=AE=E6=AD=A3=20+=20prototype?= =?UTF-8?q?=20=E5=93=A8=E5=85=B5=E9=94=AE=E6=9E=9A=E4=B8=BE=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/object.rs | 55 ++++-- crates/oxide_cli/src/main.rs | 4 +- crates/oxide_vm/src/vm_support.rs | 4 +- .../oxide_vm/tests/si0_prototype_key_tests.rs | 162 ++++++++++++++++++ 4 files changed, 209 insertions(+), 16 deletions(-) create mode 100644 crates/oxide_vm/tests/si0_prototype_key_tests.rs diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 3385f4a0..73da09a2 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -55,8 +55,7 @@ pub fn walk_own_keys(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> { } for id in shape_ids.iter().rev() { if let Some(shape) = vm.kernel_core().shape_forge().get_shape(*id) { - if shape.property_name != 0 - && !is_symbol_key(shape.property_name) + if !is_symbol_key(shape.property_name) && !is_private_name_key(shape.property_name) { // 绝对存储索引:数组命名属性位于元素区之后。 @@ -66,17 +65,47 @@ pub fn walk_own_keys(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> { } pos += 1; } - keys.sort_by(|(a_si, _), (b_si, _)| { - // 整数键(含 INT 区间键)以数值升序排在普通字符串键之前。 - let a_idx = int_or_string_index(vm, *a_si); - let b_idx = int_or_string_index(vm, *b_si); - match (a_idx, b_idx) { - (Some(ai), Some(bi)) => ai.cmp(&bi), - (Some(_), None) => std::cmp::Ordering::Less, - (None, Some(_)) => std::cmp::Ordering::Greater, - (None, None) => std::cmp::Ordering::Equal, + // 类构造器属性序修正:shape 链构建序为 prototype→length→name(叶→根), + // 规范要求的插入序为 length→name→prototype。按规范重排字符串键。 + if obj.is_class_constructor() { + let perm = vm.kernel_core().perm_interner(); + let canonical = ["length", "name", "prototype"]; + let mut canonical_map: std::collections::HashMap<&str, usize> = std::collections::HashMap::new(); + for (i, &name) in canonical.iter().enumerate() { + canonical_map.insert(name, i); } - }); + // 按规范序重新排列字符串键,整数键保持原位。 + keys.sort_by(|(a_si, _), (b_si, _)| { + let a_idx = int_or_string_index(vm, *a_si); + let b_idx = int_or_string_index(vm, *b_si); + match (a_idx, b_idx) { + (Some(ai), Some(bi)) => ai.cmp(&bi), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => { + // 两者均为字符串键:查规范序。 + let a_name = perm.lookup(*a_si).unwrap_or(""); + let b_name = perm.lookup(*b_si).unwrap_or(""); + let a_rank = canonical_map.get(a_name).copied().unwrap_or(usize::MAX); + let b_rank = canonical_map.get(b_name).copied().unwrap_or(usize::MAX); + // 规范键按序排,其余保持原序。 + a_rank.cmp(&b_rank) + } + } + }); + } else { + keys.sort_by(|(a_si, _), (b_si, _)| { + // 整数键(含 INT 区间键)以数值升序排在普通字符串键之前。 + let a_idx = int_or_string_index(vm, *a_si); + let b_idx = int_or_string_index(vm, *b_si); + match (a_idx, b_idx) { + (Some(ai), Some(bi)) => ai.cmp(&bi), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + }); + } keys } @@ -132,7 +161,7 @@ pub(crate) fn walk_own_symbol_keys(vm: &H, obj: &JsObject) -> Vec<(u3 if let Some(shape) = vm.kernel_core().shape_forge().get_shape(*id) { // Symbol 键节点按绝对槽位回传(数组命名属性位于元素区之后)。 let store = if obj.is_array() { obj.array_prop_count + pos } else { pos }; - if shape.property_name != 0 && is_symbol_key(shape.property_name) { + if is_symbol_key(shape.property_name) { keys.push((shape.property_name, store)); } } diff --git a/crates/oxide_cli/src/main.rs b/crates/oxide_cli/src/main.rs index 57553859..2cb5645c 100644 --- a/crates/oxide_cli/src/main.rs +++ b/crates/oxide_cli/src/main.rs @@ -16,6 +16,7 @@ use oxide_kernel::{kernel_error, kernel_info}; use oxide_log::{Level, SUBSYSTEM_COUNT}; use oxide_parser::Allocator; use oxide_types::object::JsObject; +use oxide_types::private_key::{is_private_name_key, is_symbol_key}; use oxide_vm::vm::Vm; use oxide_vm::vm_error; use oxide_vm::vm_pool::VmPool; @@ -249,7 +250,8 @@ fn format_object( let mut pos: u32 = 0; for id in shape_ids.iter().rev() { if let Some(shape) = shape_forge.get_shape(*id) { - if shape.property_name != 0 { + // 跳过 Symbol/私有名键,仅展示字符串属性名。 + if !is_symbol_key(shape.property_name) && !is_private_name_key(shape.property_name) { let prop_val = obj.get_prop_at(pos); if prop_val.is_undefined() { pos += 1; diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index f7f42bc9..63371aa6 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -41,7 +41,7 @@ impl Vm { let mut session = KernelSession::new(&core); bindings::init_kernel_builtins(&core, &mut session); // 在 builtin 绑定之后取 id:绑定过程已 intern "length",此处命中缓存得到 - // 非 0 的稳定 id(保持枚举层"id 0 哨兵"的既有约定,见 walk_own_keys)。 + // 稳定的 id,用于运行期常见属性(如函数 length 槽)的快路径。 let length_si = core.perm_interner().intern("length").0; let obj_proto = P::clone(&session.builtin_world().object_proto); // 提前缓存执行期字符串 GC 初始水位(构造后 config 不再变化)。 @@ -172,7 +172,7 @@ impl Vm { let mut session = KernelSession::new(&core); bindings::init_kernel_builtins(&core, &mut session); // 在 builtin 绑定之后取 id:绑定过程已 intern "length",此处命中缓存得到 - // 非 0 的稳定 id(保持枚举层"id 0 哨兵"的既有约定,见 walk_own_keys)。 + // 稳定的 id,用于运行期常见属性(如函数 length 槽)的快路径。 let length_si = core.perm_interner().intern("length").0; let obj_proto = P::clone(&session.builtin_world().object_proto); // 提前缓存执行期字符串 GC 初始水位(构造后 config 不再变化)。 diff --git a/crates/oxide_vm/tests/si0_prototype_key_tests.rs b/crates/oxide_vm/tests/si0_prototype_key_tests.rs new file mode 100644 index 00000000..be8ca420 --- /dev/null +++ b/crates/oxide_vm/tests/si0_prototype_key_tests.rs @@ -0,0 +1,162 @@ +//! 回归测试:si-0 哨兵键("prototype")枚举正确性。 +//! +//! 根因:`builtin_labels` 将 "prototype" intern 为 si 0,但三处 +//! `property_name != 0` 守卫将其排除在枚举之外,导致 delete 重建、 +//! getOwnPropertyNames 等路径丢失 "prototype" 键。 +//! +//! 修复:删除守卫,si 0 成为一等属性键。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> String { + let allocator = Allocator::default(); + let program = match oxide_parser::parse(&allocator, source) { + Ok(p) => p, + Err(e) => return format!("parse error: {}", e[0].message), + }; + let module = match Compiler::new().compile(&program) { + Ok(m) => m, + Err(e) => return format!("compile error: {e}"), + }; + let mut vm = Vm::new(); + match vm.run(&Arc::new(module)) { + Ok(result) => js_value_to_str(result), + Err(e) => format!("vm error: {e}"), + } +} + +fn eval_json(source: &str) -> String { + let allocator = Allocator::default(); + let program = match oxide_parser::parse(&allocator, source) { + Ok(p) => p, + Err(e) => return format!("parse error: {}", e[0].message), + }; + let module = match Compiler::new().compile(&program) { + Ok(m) => m, + Err(e) => return format!("compile error: {e}"), + }; + let mut vm = Vm::new(); + match vm.run(&Arc::new(module)) { + Ok(result) => js_value_to_str(result), + Err(e) => format!("vm error: {e}"), + } +} + +/// 将 VM 返回值转为 JS 可见字符串:字符串值直接取文本,整型/布尔/undefined +/// 走 Debug display,对象走 Debug display(测试中不用于对象比较)。 +fn js_value_to_str(v: JsValue) -> String { + if v.is_string() { + // SAFETY: 字符串值指向 VM arena(调用方保持 vm 存活)。 + unsafe { &*v.as_string_ptr() }.to_owned_string() + } else { + format!("{v}") + } +} + +// ── 机制 A:delete 重建后 "prototype" 不丢失 ── + +#[test] +fn delete_others_preserves_prototype() { + // {prototype:{m:1},x:2}; delete o.x; typeof o.prototype → "object" + assert_eq!(eval("var o={prototype:{m:1},x:2}; delete o.x; typeof o.prototype"), "object"); +} + +#[test] +fn delete_class_prototype_method_preserves_prototype_prop() { + // class C{prototype(){}x(){}} delete C.prototype.x; typeof C.prototype.prototype → "function" + assert_eq!( + eval("class C{prototype(){}x(){}} delete C.prototype.x; typeof C.prototype.prototype"), + "function" + ); +} + +// ── Object.keys / getOwnPropertyNames 包含 "prototype" ── + +#[test] +fn object_keys_contains_prototype_key() { + // Object.keys({prototype:1}) 应返回 ["prototype"] + let r = eval("var o={prototype:1}; Object.keys(o).length"); + assert_eq!(r, "1", "Object.keys should include prototype key"); +} + +#[test] +fn get_own_property_names_class_constructor_has_prototype() { + // Object.getOwnPropertyNames(class A{}) 应包含 "prototype" + let r = eval("Object.getOwnPropertyNames(class A{}).length"); + assert_eq!(r, "3", "class constructor should have length, name, prototype"); +} + +#[test] +fn get_own_property_names_object_literal_has_prototype() { + // Object.getOwnPropertyNames({prototype:1}) 应包含 "prototype" + let r = eval("Object.getOwnPropertyNames({prototype:1}).length"); + assert_eq!(r, "1", "object literal should have prototype key"); +} + +// ── 类构造器属性序:[length, name, prototype] ── + +#[test] +fn class_constructor_property_order() { + // 类构造器属性序应为 [length, name, prototype] + let r = eval("Object.getOwnPropertyNames(class A{}).join(',')"); + assert_eq!(r, "length,name,prototype", "class constructor property order"); +} + +// ── JSON.stringify 包含 "prototype" ── + +#[test] +fn json_stringify_includes_prototype() { + // JSON.stringify({prototype:1}) → "{\"prototype\":1}" + let r = eval_json("JSON.stringify({prototype:1})"); + assert_eq!(r, "{\"prototype\":1}", "JSON.stringify should include prototype key"); +} + +// ── Reflect.ownKeys 包含 "prototype" ── + +#[test] +fn reflect_own_keys_includes_prototype() { + let r = eval("Reflect.ownKeys({prototype:1}).length"); + assert_eq!(r, "1", "Reflect.ownKeys should include prototype key"); +} + +// ── for-in 不产出 "prototype"(不可枚举,修后仍正确)── + +#[test] +fn for_in_excludes_non_enumerable_prototype() { + // 构造器 "prototype" 不可枚举,for-in 不应产出 + let r = eval("var c=0; class A{} for(var k in A) c++; c"); + assert_eq!(r, "0", "for-in should not enumerate non-enumerable prototype on class constructor"); +} + +// ── 存在性检查不受影响 ── + +#[test] +fn has_own_property_prototype_true() { + let r = eval("Object.getOwnPropertyDescriptor(class A{}, 'prototype') !== undefined"); + assert_eq!(r, "true", "hasOwnProperty should return true for prototype"); +} + +#[test] +fn in_operator_prototype_true() { + let r = eval("'prototype' in function f(){}"); + assert_eq!(r, "true", "'prototype' in function should be true"); +} + +// ── 对象字面量 "prototype" 属性读写 ── + +#[test] +fn object_literal_prototype_read_write() { + let r = eval("var o={prototype:42}; o.prototype"); + assert_eq!(r, "42", "should read prototype property from object literal"); +} + +#[test] +fn object_literal_prototype_delete() { + let r = eval("var o={prototype:42,x:1}; delete o.x; o.prototype"); + assert_eq!(r, "42", "delete other prop should not affect prototype property"); +}