fix: 类构造器属性序修正 + prototype 哨兵键枚举修复

This commit is contained in:
2026-09-21 16:27:14 +00:00
parent cc0f6975ca
commit 68e0ad7fa1
4 changed files with 209 additions and 16 deletions
+42 -13
View File
@@ -55,8 +55,7 @@ pub fn walk_own_keys<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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));
}
}
+3 -1
View File
@@ -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;
+2 -2
View File
@@ -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 不再变化)。
@@ -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");
}