From 6aa87cd40e136eb46ddb4ab240e878387632862b Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 17 Sep 2026 22:29:48 +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=E5=AF=B9=E8=B1=A1=E8=AE=BE=E4=B8=BA=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E6=89=A9=E5=B1=95=E5=B9=B6=E6=8B=A6=E6=88=AA=E5=86=99?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/module.rs | 30 +++++++++-- crates/oxide_builtins/src/object.rs | 29 +++++++++- crates/oxide_builtins/src/reflect.rs | 54 +++---------------- crates/oxide_types/src/object.rs | 19 ++++++- crates/oxide_vm/src/vm_props.rs | 8 +++ .../oxide_vm/tests/module_namespace_tests.rs | 47 +++++++++++++++- 6 files changed, 132 insertions(+), 55 deletions(-) diff --git a/crates/oxide_builtins/src/module.rs b/crates/oxide_builtins/src/module.rs index a0d1fcf5..f18e540a 100644 --- a/crates/oxide_builtins/src/module.rs +++ b/crates/oxide_builtins/src/module.rs @@ -14,9 +14,12 @@ use oxide_types::value::JsValue; /// `Symbol.toStringTag` 在 well-known symbol 表中的序号,命名空间标签键按其编码。 const TO_STRING_TAG_SYMBOL_ID: u32 = 9; -/// 模块命名空间导出属性描述符:不可写、可枚举、不可配置(module namespace exotic)。 +/// 模块命名空间导出属性描述符:可写、可枚举、不可配置(module namespace exotic)。 +/// +/// 规范 10.4.6.5 返回 `{writable: true, enumerable: true, configurable: false}`; +/// 写保护由命名空间的 `[[Set]]`/`[[DefineOwnProperty]]` 专属语义承担,不靠描述符位。 fn ns_attrs() -> PropAttributes { - PropAttributes::new(false, true, false) + PropAttributes::new(true, true, false) } fn type_error(vm: &mut H, msg: &str) -> NativeResult { @@ -24,6 +27,10 @@ fn type_error(vm: &mut H, msg: &str) -> NativeResult { } /// `__moduleObject()`:创建模块命名空间对象(null 原型,带 @@toStringTag Symbol 键)。 +/// +/// # 副作用 +/// - 对象创建起即标记 module namespace exotic 并置 non-extensible:导出定义由 +/// `module_set`/`module_star` 的瞬态可扩展窗口旁路,其余写/定义一律拒绝。 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 = make_well_known_symbol_key(TO_STRING_TAG_SYMBOL_ID); @@ -32,6 +39,8 @@ pub fn module_object(vm: &mut H, _args: &[u8]) -> NativeResult { if let Err(e) = vm.define_data_property(obj_ref, tag_si, tag_val, PropAttributes::new(false, false, false)) { return NativeResult::Err(crate::error::create_error(vm, &e)); } + obj_ref.set_module_namespace(true); + obj_ref.set_extensible(false); NativeResult::Ok(JsValue::from_js_object(obj)) } @@ -50,7 +59,12 @@ pub fn module_set(vm: &mut H, args: &[u8]) -> NativeResult { let value = vm.reg(args[3]); let name_si = vm.property_key_si(name_val); let obj = unsafe { &mut *ns_ptr }; - if let Err(e) = vm.define_data_property(obj, name_si, value, ns_attrs()) { + // 导出定义窗口:命名空间创建起不可扩展,定义新导出须瞬态放开扩展性。窗口内 + // 不运行 JS,无可观察副作用;错误路径先复位以维持 non-extensible 不变式。 + obj.set_extensible(true); + let result = vm.define_data_property(obj, name_si, value, ns_attrs()); + obj.set_extensible(false); + if let Err(e) = result { return NativeResult::Err(crate::error::create_error(vm, &e)); } // 每次导出写入后重排:自导入在 body 内即可见有序的 [[OwnPropertyKeys]]。 @@ -138,7 +152,11 @@ pub fn module_star(vm: &mut H, args: &[u8]) -> NativeResult { continue; } let value = src.get_prop_at(pos); - if let Err(e) = vm.define_data_property(dst, name_si, value, ns_attrs()) { + // star 导出同 module_set:定义新导出走瞬态可扩展窗口,错误路径先复位。 + dst.set_extensible(true); + let result = vm.define_data_property(dst, name_si, value, ns_attrs()); + dst.set_extensible(false); + if let Err(e) = result { return NativeResult::Err(crate::error::create_error(vm, &e)); } } @@ -183,6 +201,9 @@ pub fn module_eval(vm: &mut H, args: &[u8]) -> NativeResult { } /// `__moduleData(kind, content)`:构造数据模块命名空间(json/text;bytes 未支持)。 +/// +/// # 副作用 +/// - 同 `__moduleObject`:标记 module namespace exotic 并置 non-extensible。 pub fn module_data(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 3 { return type_error(vm, "__moduleData: 2 arguments required"); @@ -216,6 +237,7 @@ pub fn module_data(vm: &mut H, args: &[u8]) -> NativeResult { if let Err(e) = vm.define_data_property(obj_ref, default_si, default_val, ns_attrs()) { return NativeResult::Err(crate::error::create_error(vm, &e)); } + obj_ref.set_module_namespace(true); obj_ref.set_extensible(false); NativeResult::Ok(JsValue::from_js_object(obj)) } diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 29f10b61..d1d2152d 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -640,7 +640,7 @@ pub fn object_is(vm: &mut H, args: &[u8]) -> NativeResult { /// /// # 副作用 /// - 修改 obj 的 shape 链、属性表与 generation -fn define_from_descriptor( +pub(crate) fn define_from_descriptor( vm: &mut H, obj_ptr: *mut JsObject, key_si: u32, desc_val: JsValue, ) -> Result<(), String> { if !desc_val.is_object() { @@ -672,6 +672,25 @@ fn define_from_descriptor( .unwrap_or_else(|| oxide_types::object::PropMetaEntry::data(PropAttributes::DEFAULT_DATA)) }); + // 模块命名空间 exotic [[DefineOwnProperty]](规范 10.4.6.5):Symbol 键委托 + // 普通语义;导出键仅接受无变更、值不变或 writable:true,值变与 writable:false + // 拒绝;非导出键由对象不可扩展在后续 define 处拒绝。enumerable/configurable + // 与 accessor 的收窄由 define_* 的不可配置守卫承担。 + if unsafe { &*obj_ptr }.is_module_namespace() && !is_symbol_key(key_si) { + let Some(pos) = existing_pos else { + return Err("Cannot define property on module namespace: not an export".to_string()); + }; + if writable_field.is_some_and(|w| !oxide_runtime_api::to_boolean(w)) { + return Err("Cannot redefine module namespace export".to_string()); + } + if let Some(v) = value_field { + let obj = unsafe { &*obj_ptr }; + if !oxide_runtime_api::same_value(v, obj.get_prop_at(pos)) { + return Err("Cannot redefine module namespace export".to_string()); + } + } + } + // 修改已有属性时缺省字段回填现有值,仅定义新属性时缺省才为 false。 let enumerable = own_field(vm, desc_val, enumerable_si) .map(oxide_runtime_api::to_boolean) @@ -959,6 +978,14 @@ pub fn object_freeze(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Ok(val); } let obj_ptr = val.as_js_object_ptr(); + { + let obj = unsafe { &*obj_ptr }; + // 模块命名空间导出保持 writable:true,冻结须失败(规范 SetIntegrityLevel)。 + // 无字符串导出的空命名空间没有可写属性,允许冻结成功。 + if obj.is_module_namespace() && !walk_own_keys(vm, obj).is_empty() { + return NativeResult::Err(crate::error::create_type_error(vm, "Cannot freeze module namespace")); + } + } { let obj = unsafe { &mut *obj_ptr }; // 命名属性:walk_own_keys 返回绝对存储索引(数组含元素区偏移)。 diff --git a/crates/oxide_builtins/src/reflect.rs b/crates/oxide_builtins/src/reflect.rs index be44a6a9..608b27f6 100644 --- a/crates/oxide_builtins/src/reflect.rs +++ b/crates/oxide_builtins/src/reflect.rs @@ -1,6 +1,6 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::mem::P; -use oxide_types::object::{JsObject, PropAttributes}; +use oxide_types::object::JsObject; use oxide_types::value::JsValue; use crate::object::{delete_own_property, key_si_to_js_value, own_symbol_key_values, walk_own_keys}; @@ -70,6 +70,10 @@ pub fn reflect_construct(vm: &mut H, args: &[u8]) -> NativeResult { /// `Reflect.defineProperty(target, key, descriptor)`:按 descriptor 定义属性, /// 数据/访问器属性混用或非法 getter/setter 返回 false。 +/// +/// # 注意事项 +/// - 与 `Object.defineProperty` 共用 `define_from_descriptor`:缺失字段回填现有 +/// 属性、模块命名空间 exotic 收窄语义一处生效;错误投影为布尔 false。 pub fn reflect_define_property(vm: &mut H, args: &[u8]) -> NativeResult { let target_val = arg(vm, args, 1); let Some(target_ptr) = object_ptr(target_val) else { @@ -81,43 +85,7 @@ pub fn reflect_define_property(vm: &mut H, args: &[u8]) -> NativeResu }; let key_si = vm.property_key_si(arg(vm, args, 2)); - let value_si = vm.kernel_core().perm_interner().intern("value").0; - let get_si = vm.kernel_core().perm_interner().intern("get").0; - let set_si = vm.kernel_core().perm_interner().intern("set").0; - let writable_si = vm.kernel_core().perm_interner().intern("writable").0; - let enumerable_si = vm.kernel_core().perm_interner().intern("enumerable").0; - let configurable_si = vm.kernel_core().perm_interner().intern("configurable").0; - - let value_field = own_field(vm, desc_val, value_si); - let get_field = own_field(vm, desc_val, get_si); - let set_field = own_field(vm, desc_val, set_si); - let writable_field = own_field(vm, desc_val, writable_si); - let enumerable = own_field(vm, desc_val, enumerable_si) - .map(oxide_runtime_api::to_boolean) - .unwrap_or(false); - let configurable = own_field(vm, desc_val, configurable_si) - .map(oxide_runtime_api::to_boolean) - .unwrap_or(false); - - let has_data = value_field.is_some() || writable_field.is_some(); - let has_accessor = get_field.is_some() || set_field.is_some(); - if has_data && has_accessor { - return NativeResult::Ok(JsValue::bool(false)); - } - - let target = unsafe { &mut *target_ptr }; - let result = if has_accessor { - let get = get_field.unwrap_or(JsValue::undefined()); - let set = set_field.unwrap_or(JsValue::undefined()); - if (!get.is_undefined() && !is_callable(get)) || (!set.is_undefined() && !is_callable(set)) { - return NativeResult::Ok(JsValue::bool(false)); - } - vm.define_accessor_property(target, key_si, get, set, PropAttributes::new(false, enumerable, configurable)) - } else { - let value = value_field.unwrap_or(JsValue::undefined()); - let writable = writable_field.map(oxide_runtime_api::to_boolean).unwrap_or(false); - vm.define_data_property(target, key_si, value, PropAttributes::new(writable, enumerable, configurable)) - }; + let result = crate::object::define_from_descriptor(vm, target_ptr, key_si, desc_val); NativeResult::Ok(JsValue::bool(result.is_ok())) } @@ -295,13 +263,3 @@ fn array_like_elements(value: JsValue) -> Option> { let obj = unsafe { &*ptr }; Some((0..obj.prop_count() as usize).map(|idx| obj.get_prop_at(idx)).collect()) } - -/// 按 ToPropertyDescriptor 语义取描述符字段:沿原型链判存在性,存在时经 -/// ordinary_get 取值(触发 accessor getter,receiver 为描述符对象本身)。 -fn own_field(vm: &mut H, desc: JsValue, prop_si: u32) -> Option { - // ordinary_get 对缺失属性返回 undefined,无法区分"不存在"与"值为 - // undefined",故先用 resolve_property 沿原型链判 HasProperty。 - let obj = unsafe { &*desc.as_js_object_ptr() }; - vm.resolve_property(obj, prop_si)?; - vm.ordinary_get(obj, prop_si, desc).ok() -} diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index cfba8cde..8c9416d7 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -61,7 +61,10 @@ pub const MAX_DENSE_PROPS: usize = 1_000_000; /// native_arg_count: u8 (1 字节) /// type_tag: u8 — 标识包装/外来对象种类的 OBJ_TYPE_* 常量 (1 字节) /// is_session_epoch: u8 (1 字节) -/// _pad: u8 +/// _pad: u8 位 +/// \[0\] is_frozen +/// \[1\] is_sealed +/// \[2\] is_module_namespace /// array_elements: *mut u8 (8 字节,数组对象元素区 Box\\>) /// array_elements_meta: *mut u8 (8 字节,数组元素元数据 Box\\>\>) /// hash_props: *mut u8 (8 字节,命名属性 Box\\>) @@ -705,6 +708,20 @@ impl JsObject { } } + /// 是否模块命名空间 exotic 对象(`[[Set]]`/`[[DefineOwnProperty]]` 有专属语义)。 + pub fn is_module_namespace(&self) -> bool { + (self._pad >> 2) & 1 != 0 + } + + /// 设置模块命名空间 exotic 标志。 + pub fn set_module_namespace(&mut self, value: bool) { + if value { + self._pad |= 1 << 2; + } else { + self._pad &= !(1 << 2); + } + } + /// 是否 Set 实例(header bit 24)。 pub fn is_set(&self) -> bool { (self.header >> 24) & 1 != 0 diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 85cf418b..fd4858c3 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -310,6 +310,14 @@ impl Vm { prop_name_si, use_frame_push ); + // 模块命名空间 exotic [[Set]] 恒 false(规范 10.4.6.8):严格抛 TypeError, + // sloppy 静默 no-op;Reflect.set 以 strict=true 调用并投影为 false。 + if obj.is_module_namespace() { + if strict { + return self.raise_type_error("Cannot assign to a module namespace export"); + } + return Ok(()); + } // TypedArray 整数索引:receiver 为 TA 本体时写底层 buffer(越界静默忽略); // receiver 非 TA 时按规范把写入落到 receiver 对象,不碰 TA buffer。 if obj.is_typed_array_obj() { diff --git a/crates/oxide_vm/tests/module_namespace_tests.rs b/crates/oxide_vm/tests/module_namespace_tests.rs index 21adca8e..ec1bd894 100644 --- a/crates/oxide_vm/tests/module_namespace_tests.rs +++ b/crates/oxide_vm/tests/module_namespace_tests.rs @@ -15,7 +15,10 @@ 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 joined = std::path::Path::new(base_dir).join(specifier); + // 解析为规范绝对路径:自导入身份比较要求 loader 的 path 与入口模块的 + // 规范路径逐字对齐(emit 侧入口路径已 canonicalize)。 + let full = std::fs::canonicalize(&joined).map_err(|e| format!("cannot read {specifier}: {e}"))?; let source = std::fs::read_to_string(&full).map_err(|e| format!("cannot read {specifier}: {e}"))?; Ok(ResolvedModule { source, @@ -98,3 +101,45 @@ fn namespace_to_string_tag_symbol_key_and_own_keys() { "命名空间标签 Symbol 键与自身键枚举面不符" ); } + +#[test] +fn namespace_exotic_semantics() { + // 模块命名空间 exotic 四语义:创建起不可扩展、导出 writable:true、 + // [[Set]] 恒 false、[[DefineOwnProperty]] 收窄、Object.freeze 恒抛。 + // 用自导入在 body 内观测扩展性(此时 body 尚未执行到 seal)。 + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join("__module_namespace_exotic__"); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write( + dir.join("main.mjs"), + "import * as ns from './main.mjs';\n \ + export const x = 1;\n \ + export default 2;\n \ + const d = Object.getOwnPropertyDescriptor(ns, 'x');\n \ + function threw(fn) { try { fn(); return false; } catch (e) { return e instanceof TypeError; } }\n \ + globalThis.__ns = [\n \ + Object.isExtensible(ns),\n \ + d.writable === true && d.enumerable === true && d.configurable === false,\n \ + Reflect.set(ns, 'x', 9),\n \ + threw(function () { ns.x = 9; }),\n \ + Reflect.set(ns, Symbol('s'), 1),\n \ + Reflect.defineProperty(ns, 'x', {}),\n \ + Reflect.defineProperty(ns, 'x', { value: 9 }),\n \ + threw(function () { Object.defineProperty(ns, 'x', { value: 9 }); }),\n \ + Reflect.defineProperty(ns, 'x', { writable: false }),\n \ + Reflect.defineProperty(ns, 'newKey', {}),\n \ + threw(function () { Object.freeze(ns); }),\n \ + Object.isFrozen(ns),\n \ + Object.seal(ns) === ns,\n \ + ns.default,\n \ + ].join('|');", + ) + .expect("write main"); + let _cleanup = Cleanup(dir.clone()); + + let result = run_namespace_module(&dir); + assert_eq!( + result, "false|true|false|true|false|true|false|true|false|false|true|false|true|2", + "命名空间 exotic 语义不符" + ); +}