From 9b65feb2abf08cba7bb61173f3f98691e2ac2a5f Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 17 Sep 2026 20:26:44 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=A8=A1=E5=9D=97=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E6=9A=B4=E9=9C=B2=E7=AC=A6=E5=8F=B7=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E8=87=AA=E8=BA=AB=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/module.rs | 14 ++- crates/oxide_builtins/src/object.rs | 79 +++++++++++++- crates/oxide_builtins/src/reflect.rs | 24 ++++- .../oxide_vm/tests/module_namespace_tests.rs | 100 ++++++++++++++++++ 4 files changed, 207 insertions(+), 10 deletions(-) create mode 100644 crates/oxide_vm/tests/module_namespace_tests.rs diff --git a/crates/oxide_builtins/src/module.rs b/crates/oxide_builtins/src/module.rs index ec6b9802..a0d1fcf5 100644 --- a/crates/oxide_builtins/src/module.rs +++ b/crates/oxide_builtins/src/module.rs @@ -8,8 +8,12 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::{NativeResult, VmHost}; use oxide_types::object::{JsObject, PropAttributes}; +use oxide_types::private_key::make_well_known_symbol_key; use oxide_types::value::JsValue; +/// `Symbol.toStringTag` 在 well-known symbol 表中的序号,命名空间标签键按其编码。 +const TO_STRING_TAG_SYMBOL_ID: u32 = 9; + /// 模块命名空间导出属性描述符:不可写、可枚举、不可配置(module namespace exotic)。 fn ns_attrs() -> PropAttributes { PropAttributes::new(false, true, false) @@ -19,10 +23,10 @@ fn type_error(vm: &mut H, msg: &str) -> NativeResult { NativeResult::Err(crate::error::create_type_error(vm, msg)) } -/// `__moduleObject()`:创建模块命名空间对象(null 原型,带 @@toStringTag)。 +/// `__moduleObject()`:创建模块命名空间对象(null 原型,带 @@toStringTag Symbol 键)。 pub fn module_object(vm: &mut H, _args: &[u8]) -> NativeResult { let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); - let tag_si = vm.kernel_core().perm_interner().intern("@@toStringTag").0; + let tag_si = make_well_known_symbol_key(TO_STRING_TAG_SYMBOL_ID); let tag_val = vm.new_string("Module"); let obj_ref = unsafe { &mut *obj }; if let Err(e) = vm.define_data_property(obj_ref, tag_si, tag_val, PropAttributes::new(false, false, false)) { @@ -49,6 +53,8 @@ pub fn module_set(vm: &mut H, args: &[u8]) -> NativeResult { if let Err(e) = vm.define_data_property(obj, name_si, value, ns_attrs()) { return NativeResult::Err(crate::error::create_error(vm, &e)); } + // 每次导出写入后重排:自导入在 body 内即可见有序的 [[OwnPropertyKeys]]。 + crate::object::sort_namespace_exports(vm, obj); NativeResult::Ok(JsValue::undefined()) } @@ -136,10 +142,12 @@ pub fn module_star(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Err(crate::error::create_error(vm, &e)); } } + // star 复制后重排:直接导出与 star 导出的合并键序保持规范顺序。 + crate::object::sort_namespace_exports(vm, dst); NativeResult::Ok(JsValue::undefined()) } -/// `__moduleSeal(ns)`:封冻命名空间(不可扩展;toStringTag 留待后续)。 +/// `__moduleSeal(ns)`:封冻命名空间(不可扩展)。 pub fn module_seal(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { return type_error(vm, "__moduleSeal: 1 argument required"); diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 6133921a..29f10b61 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -165,6 +165,80 @@ fn decode_symbol_key(vm: &H, key: u32) -> JsValue { JsValue::symbol(symbol_index_from_key(key)) } +/// 收集对象自身全部 Symbol 键并按插入序物化为 Symbol 值。 +/// +/// 供 `Object.getOwnPropertySymbols` 与 `Reflect.ownKeys` 的 Symbol 段共用; +/// 与 [`walk_own_keys`] 互补,后者只返回字符串键与整数键。 +/// +/// # 边界与前提 +/// - 无自身 Symbol 键时返回空向量;数组元素区不含 Symbol 键。 +/// +/// # 注意事项 +/// - 只读 shape 链,不分配对象;well-known symbol 值指向 session 内建 symbol 对象。 +pub fn own_symbol_key_values(vm: &H, obj: &JsObject) -> Vec { + walk_own_symbol_keys(vm, obj) + .iter() + .map(|(key, _)| decode_symbol_key(vm, *key)) + .collect() +} + +/// 按规范 [[OwnPropertyKeys]] 重排命名空间导出:字符串键按 UTF-16 单元序升序, +/// Symbol 键(`@@toStringTag`)保持在其后。 +/// +/// 命名空间导出在模块 body 执行期按语句序写入,而规范要求导出键有序。重建 shape +/// 链使插入序即枚举序,字符串枚举消费端(getOwnPropertyNames / Reflect.ownKeys / +/// for-in / Object.keys / JSON)无需各自特判。 +/// +/// # 边界与前提 +/// - 仅对模块命名空间对象调用;无自身字符串键时只保留 Symbol 键。 +/// +/// # 副作用 +/// - 重写 obj 的 shape 链与属性表并 bump generation;对象 identity、属性值与 +/// 描述符均不变。 +pub fn sort_namespace_exports(vm: &mut H, obj: &mut JsObject) { + // 收集字符串导出与其值/描述符;Symbol 键不参与字符串排序,收集后原序追加。 + let mut entries: Vec<(u32, JsValue, Option)> = walk_own_keys(vm, obj) + .into_iter() + .map(|(si, pos)| (si, obj.get_prop_at(pos), obj.prop_meta_at(pos))) + .collect(); + let symbols: Vec<(u32, JsValue, Option)> = walk_own_symbol_keys(vm, obj) + .into_iter() + .map(|(si, pos)| (si, obj.get_prop_at(pos), obj.prop_meta_at(pos))) + .collect(); + + // 排序口径 = UTF-16 单元序(BMP 外字符的单元序与码点序不同)。 + entries.sort_by_cached_key(|(si, _, _)| namespace_key_units(vm, *si)); + + // 重建 shape 链与属性表:插入序 = 枚举序。 + obj.set_shape_id(EMPTY_SHAPE_ID); + obj.clear_props(); + for (si, value, meta) in entries.into_iter().chain(symbols) { + let shape = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), si); + obj.set_shape_id(shape); + let pos = obj.push_prop(value); + if let Some(meta) = meta { + if meta.is_accessor { + obj.set_accessor_meta(pos, meta.get, meta.set, meta.attributes); + } else { + obj.set_data_meta(pos, meta.attributes); + } + } + } + obj.bump_generation(); +} + +/// 命名空间导出键的排序单元序列:字符串键取 UTF-16 单元,整数键取十进制文本。 +fn namespace_key_units(vm: &H, si: u32) -> Vec { + if is_int_key(si) { + return int_key_value(si).to_string().encode_utf16().collect(); + } + vm.kernel_core() + .perm_interner() + .lookup(si) + .map(|key| key.encode_utf16().collect()) + .unwrap_or_default() +} + /// `Object.getOwnPropertySymbols(obj)`:返回全部自身 Symbol 键数组。 pub fn object_get_own_property_symbols(vm: &mut H, args: &[u8]) -> NativeResult { let obj_ptr = match require_obj_arg(vm, args, "getOwnPropertySymbols") { @@ -174,10 +248,7 @@ pub fn object_get_own_property_symbols(vm: &mut H, args: &[u8]) -> Na let symbols: Vec = { let obj = unsafe { &*obj_ptr }; - walk_own_symbol_keys(vm, obj) - .iter() - .map(|(key, _)| decode_symbol_key(vm, *key)) - .collect() + own_symbol_key_values(vm, obj) }; let n = symbols.len(); diff --git a/crates/oxide_builtins/src/reflect.rs b/crates/oxide_builtins/src/reflect.rs index a86e005b..be44a6a9 100644 --- a/crates/oxide_builtins/src/reflect.rs +++ b/crates/oxide_builtins/src/reflect.rs @@ -3,7 +3,7 @@ use oxide_types::mem::P; use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::value::JsValue; -use crate::object::{delete_own_property, key_si_to_js_value, walk_own_keys}; +use crate::object::{delete_own_property, key_si_to_js_value, own_symbol_key_values, walk_own_keys}; use oxide_runtime_api::{NativeResult, VmHost}; @@ -184,15 +184,27 @@ pub fn reflect_is_extensible(vm: &mut H, args: &[u8]) -> NativeResult NativeResult::Ok(JsValue::bool(unsafe { &*target_ptr }.is_extensible())) } -/// `Reflect.ownKeys(target)`:返回对象全部自身属性名(字符串数组)。 +/// `Reflect.ownKeys(target)`:返回对象全部自身属性键,字符串键在前、Symbol 键在后。 +/// +/// # 步骤 +/// 1. 收集字符串键与整数键(整数升序,其余插入序)。 +/// 2. 追加自身 Symbol 键(保持插入序),符合 OrdinaryOwnPropertyKeys 的排序。 +/// +/// # 副作用 +/// - 结果数组分配在当前 epoch。 pub fn reflect_own_keys(vm: &mut H, args: &[u8]) -> NativeResult { let target_val = arg(vm, args, 1); let Some(target_ptr) = object_ptr(target_val) else { return type_error(vm, "Reflect.ownKeys target is not an object"); }; let target = unsafe { &*target_ptr }; + + // Symbol 键分流在消费端追加:底层字符串枚举源(Object.keys / JSON / for-in) + // 语义不变,不泄漏 Symbol 键。 let keys = walk_own_keys(vm, target); - let n = keys.len(); + let symbols = own_symbol_key_values(vm, target); + let n = keys.len() + symbols.len(); + let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; let arr = vm.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, @@ -206,6 +218,12 @@ pub fn reflect_own_keys(vm: &mut H, args: &[u8]) -> NativeResult { (*arr).set_prop_at(i, key_val); } } + let base = keys.len(); + for (j, sym) in symbols.iter().enumerate() { + unsafe { + (*arr).set_prop_at(base + j, *sym); + } + } unsafe { (*arr).set_prop_count(n); } diff --git a/crates/oxide_vm/tests/module_namespace_tests.rs b/crates/oxide_vm/tests/module_namespace_tests.rs new file mode 100644 index 00000000..21adca8e --- /dev/null +++ b/crates/oxide_vm/tests/module_namespace_tests.rs @@ -0,0 +1,100 @@ +//! 模块命名空间 @@toStringTag Symbol 键与自身键枚举口径: +//! 命名空间标签以 well-known symbol 键写入,`Reflect.ownKeys` 的 Symbol 段跟在 +//! 字符串段之后,字符串枚举面(getOwnPropertyNames / Object.keys)不受污染。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_emit::module::{ModuleKind, ModuleSourceLoader, ResolvedModule}; +use oxide_vm::vm::Vm; + +/// 文件加载器:base_dir/specifier join 后读源(依赖模块经真实文件解析)。 +struct FileLoader; + +impl ModuleSourceLoader for FileLoader { + fn resolve( + &mut self, base_dir: &str, specifier: &str, _attributes: &[(&str, &str)], + ) -> Result { + let full = std::path::Path::new(base_dir).join(specifier); + let source = std::fs::read_to_string(&full).map_err(|e| format!("cannot read {specifier}: {e}"))?; + Ok(ResolvedModule { + source, + path: full.to_string_lossy().into_owned(), + kind: ModuleKind::Js, + }) + } +} + +/// 测试结束删除临时目录,避免在 crate 树残留文件。 +struct Cleanup(std::path::PathBuf); +impl Drop for Cleanup { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +/// 同一 VM 续跑脚本并返回字符串完成值(读 global 探针用)。 +fn probe_str(vm: &mut Vm, source: &str) -> String { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse"); + let module = Compiler::new().compile(&program).expect("compile"); + let value = vm.run(&Arc::new(module)).expect("probe run"); + vm.lookup_str(value).unwrap_or_default() +} + +/// 编译并运行带依赖的模块,返回其探针字符串。 +fn run_namespace_module(dir: &std::path::Path) -> String { + let allocator = oxide_parser::Allocator::default(); + let source = std::fs::read_to_string(dir.join("main.mjs")).expect("read main"); + let program = oxide_parser::parse_module(&allocator, &source).expect("parse module"); + let module = Compiler::new() + .compile_module(&program, dir.join("main.mjs").to_string_lossy().as_ref(), &mut FileLoader) + .expect("compile module"); + let mut vm = Vm::new(); + vm.run(&Arc::new(module)).expect("module run"); + probe_str(&mut vm, "globalThis.__ns") +} + +#[test] +fn namespace_to_string_tag_symbol_key_and_own_keys() { + // 命名空间标签按 Symbol.toStringTag Symbol 键暴露:描述符值/属性正确, + // 字符串导出按 UTF-16 单元序排序,Symbol 段排在字符串段之后, + // 字符串枚举面不含标签。 + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join("__module_namespace__"); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write(dir.join("dep.mjs"), "export const b = 1;\nexport const a = 2;\nexport default 3;") + .expect("write dep"); + std::fs::write( + dir.join("main.mjs"), + "import * as ns from './dep.mjs';\n \ + var desc = Object.getOwnPropertyDescriptor(ns, Symbol.toStringTag);\n \ + globalThis.__ns = [\n \ + ns[Symbol.toStringTag],\n \ + typeof ns[Symbol.toStringTag],\n \ + Object.prototype.toString.call(ns),\n \ + Object.getOwnPropertySymbols(ns).length,\n \ + Object.getOwnPropertySymbols(ns)[0] === Symbol.toStringTag,\n \ + Object.getOwnPropertyNames(ns).length,\n \ + Object.getOwnPropertyNames(ns).join(','),\n \ + Reflect.ownKeys(ns).length,\n \ + Reflect.ownKeys(ns).indexOf(Symbol.toStringTag),\n \ + Symbol.toStringTag in ns,\n \ + Reflect.has(ns, Symbol.toStringTag),\n \ + Object.prototype.hasOwnProperty.call(ns, Symbol.toStringTag),\n \ + desc.writable,\n \ + desc.enumerable,\n \ + desc.configurable,\n \ + Object.getOwnPropertyNames(ns).indexOf('@@toStringTag'),\n \ + Object.keys(ns).length,\n \ + ].join('|');", + ) + .expect("write main"); + let _cleanup = Cleanup(dir.clone()); + + let result = run_namespace_module(&dir); + assert_eq!( + result, "Module|string|[object Module]|1|true|3|a,b,default|4|3|true|true|true|false|false|false|-1|3", + "命名空间标签 Symbol 键与自身键枚举面不符" + ); +}