mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-27 12:11:12 +08:00
fix: 类构造器属性序修正 + prototype 哨兵键枚举修复
This commit is contained in:
@@ -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() {
|
for id in shape_ids.iter().rev() {
|
||||||
if let Some(shape) = vm.kernel_core().shape_forge().get_shape(*id) {
|
if let Some(shape) = vm.kernel_core().shape_forge().get_shape(*id) {
|
||||||
if shape.property_name != 0
|
if !is_symbol_key(shape.property_name)
|
||||||
&& !is_symbol_key(shape.property_name)
|
|
||||||
&& !is_private_name_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;
|
pos += 1;
|
||||||
}
|
}
|
||||||
keys.sort_by(|(a_si, _), (b_si, _)| {
|
// 类构造器属性序修正:shape 链构建序为 prototype→length→name(叶→根),
|
||||||
// 整数键(含 INT 区间键)以数值升序排在普通字符串键之前。
|
// 规范要求的插入序为 length→name→prototype。按规范重排字符串键。
|
||||||
let a_idx = int_or_string_index(vm, *a_si);
|
if obj.is_class_constructor() {
|
||||||
let b_idx = int_or_string_index(vm, *b_si);
|
let perm = vm.kernel_core().perm_interner();
|
||||||
match (a_idx, b_idx) {
|
let canonical = ["length", "name", "prototype"];
|
||||||
(Some(ai), Some(bi)) => ai.cmp(&bi),
|
let mut canonical_map: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
|
||||||
(Some(_), None) => std::cmp::Ordering::Less,
|
for (i, &name) in canonical.iter().enumerate() {
|
||||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
canonical_map.insert(name, i);
|
||||||
(None, None) => std::cmp::Ordering::Equal,
|
|
||||||
}
|
}
|
||||||
});
|
// 按规范序重新排列字符串键,整数键保持原位。
|
||||||
|
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
|
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) {
|
if let Some(shape) = vm.kernel_core().shape_forge().get_shape(*id) {
|
||||||
// Symbol 键节点按绝对槽位回传(数组命名属性位于元素区之后)。
|
// Symbol 键节点按绝对槽位回传(数组命名属性位于元素区之后)。
|
||||||
let store = if obj.is_array() { obj.array_prop_count + pos } else { pos };
|
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));
|
keys.push((shape.property_name, store));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use oxide_kernel::{kernel_error, kernel_info};
|
|||||||
use oxide_log::{Level, SUBSYSTEM_COUNT};
|
use oxide_log::{Level, SUBSYSTEM_COUNT};
|
||||||
use oxide_parser::Allocator;
|
use oxide_parser::Allocator;
|
||||||
use oxide_types::object::JsObject;
|
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::Vm;
|
||||||
use oxide_vm::vm_error;
|
use oxide_vm::vm_error;
|
||||||
use oxide_vm::vm_pool::VmPool;
|
use oxide_vm::vm_pool::VmPool;
|
||||||
@@ -249,7 +250,8 @@ fn format_object(
|
|||||||
let mut pos: u32 = 0;
|
let mut pos: u32 = 0;
|
||||||
for id in shape_ids.iter().rev() {
|
for id in shape_ids.iter().rev() {
|
||||||
if let Some(shape) = shape_forge.get_shape(*id) {
|
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);
|
let prop_val = obj.get_prop_at(pos);
|
||||||
if prop_val.is_undefined() {
|
if prop_val.is_undefined() {
|
||||||
pos += 1;
|
pos += 1;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ impl Vm {
|
|||||||
let mut session = KernelSession::new(&core);
|
let mut session = KernelSession::new(&core);
|
||||||
bindings::init_kernel_builtins(&core, &mut session);
|
bindings::init_kernel_builtins(&core, &mut session);
|
||||||
// 在 builtin 绑定之后取 id:绑定过程已 intern "length",此处命中缓存得到
|
// 在 builtin 绑定之后取 id:绑定过程已 intern "length",此处命中缓存得到
|
||||||
// 非 0 的稳定 id(保持枚举层"id 0 哨兵"的既有约定,见 walk_own_keys)。
|
// 稳定的 id,用于运行期常见属性(如函数 length 槽)的快路径。
|
||||||
let length_si = core.perm_interner().intern("length").0;
|
let length_si = core.perm_interner().intern("length").0;
|
||||||
let obj_proto = P::clone(&session.builtin_world().object_proto);
|
let obj_proto = P::clone(&session.builtin_world().object_proto);
|
||||||
// 提前缓存执行期字符串 GC 初始水位(构造后 config 不再变化)。
|
// 提前缓存执行期字符串 GC 初始水位(构造后 config 不再变化)。
|
||||||
@@ -172,7 +172,7 @@ impl Vm {
|
|||||||
let mut session = KernelSession::new(&core);
|
let mut session = KernelSession::new(&core);
|
||||||
bindings::init_kernel_builtins(&core, &mut session);
|
bindings::init_kernel_builtins(&core, &mut session);
|
||||||
// 在 builtin 绑定之后取 id:绑定过程已 intern "length",此处命中缓存得到
|
// 在 builtin 绑定之后取 id:绑定过程已 intern "length",此处命中缓存得到
|
||||||
// 非 0 的稳定 id(保持枚举层"id 0 哨兵"的既有约定,见 walk_own_keys)。
|
// 稳定的 id,用于运行期常见属性(如函数 length 槽)的快路径。
|
||||||
let length_si = core.perm_interner().intern("length").0;
|
let length_si = core.perm_interner().intern("length").0;
|
||||||
let obj_proto = P::clone(&session.builtin_world().object_proto);
|
let obj_proto = P::clone(&session.builtin_world().object_proto);
|
||||||
// 提前缓存执行期字符串 GC 初始水位(构造后 config 不再变化)。
|
// 提前缓存执行期字符串 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");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user