From 39183f449c19dc173df5ec510c869890e995f749 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 18 Sep 2026 00:32:12 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=A8=A1=E5=9D=97=E5=86=99=E7=A9=BF?= =?UTF-8?q?=E6=8C=89=E7=BB=91=E5=AE=9A=E6=A7=BD=E8=BF=87=E6=BB=A4=E5=B9=B6?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E6=9B=B4=E6=96=B0=E8=B5=8B=E5=80=BC=E8=B7=AF?= =?UTF-8?q?=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_emit/src/compile_ctx.rs | 11 +++-- crates/oxide_emit/src/expr/assignment.rs | 2 + crates/oxide_emit/src/expr/operator/mod.rs | 5 ++ crates/oxide_emit/src/module.rs | 26 ++++++---- crates/oxide_emit/src/shared/binding.rs | 6 +++ .../oxide_vm/tests/module_namespace_tests.rs | 47 +++++++++++++++++++ 6 files changed, 82 insertions(+), 15 deletions(-) diff --git a/crates/oxide_emit/src/compile_ctx.rs b/crates/oxide_emit/src/compile_ctx.rs index 2e5065fd..bf1f511b 100644 --- a/crates/oxide_emit/src/compile_ctx.rs +++ b/crates/oxide_emit/src/compile_ctx.rs @@ -137,10 +137,11 @@ pub struct CompileCtx { pub(crate) for_head_store_registers: HashSet, /// 模块编译上下文:当前模块命名空间对象寄存器(`__moduleObject` 返回值)。 pub(crate) module_ns_reg: Option, - /// 写穿反演表:本模块源绑定名 → 引用它的导出名集合。仅自导入 ns 的 live 模块 - /// 填充;顶层对该源绑定的赋值据此同步命名空间条目。嵌套函数 ctx 为新建, - /// 不继承本字段,写穿天然只作用于模块顶层。 - pub(crate) module_local_exports: HashMap>, + /// 写穿反演表:本模块源绑定槽寄存器 → 引用它的导出名集合。仅自导入 ns 的 + /// live 模块填充;顶层赋值按解析后的绑定槽命中此表时同步命名空间条目。 + /// 以槽位而非名字为键,块级/catch 同名遮蔽解析到不同槽位,天然不误触发。 + /// 嵌套函数 ctx 为新建,不继承本字段,写穿只作用于模块顶层。 + pub(crate) module_local_export_regs: HashMap>, /// 已求值依赖模块的命名空间对象寄存器(按 import/export source 字符串索引)。 pub(crate) module_dep_ns_regs: HashMap, /// 依赖模块规范路径(按 import/export source 字符串索引):再导出来源身份用。 @@ -235,7 +236,7 @@ impl CompileCtx { register_update_names: Vec::new(), for_head_store_registers: HashSet::new(), module_ns_reg: None, - module_local_exports: HashMap::new(), + module_local_export_regs: HashMap::new(), module_dep_ns_regs: HashMap::new(), module_dep_paths: HashMap::new(), module_import_origins: HashMap::new(), diff --git a/crates/oxide_emit/src/expr/assignment.rs b/crates/oxide_emit/src/expr/assignment.rs index 289d1511..7b627a00 100644 --- a/crates/oxide_emit/src/expr/assignment.rs +++ b/crates/oxide_emit/src/expr/assignment.rs @@ -474,6 +474,8 @@ impl Emitter { } } } + // 逻辑赋值仅在短路通过后到达此处:写穿同步命名空间条目。 + self.emit_module_write_through(name, val_reg, ctx)?; ctx.inst(Inst::new( OpCode::LOAD_VAR, Operand::Reg(result_reg), diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index fe794243..c37e237b 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -674,6 +674,7 @@ impl Emitter { Operand::Reg(new_reg), Operand::Imm(uv as u16), )); + self.emit_module_write_through(name, new_reg, ctx)?; Ok(if update.prefix { new_reg } else { old_reg }) } else if let Some(cell_idx) = captured_cell { // 被捕获 cell:CELL_GET 旧值 + 常量 1 + ADD/SUB + CELL_SET。 @@ -710,6 +711,7 @@ impl Emitter { Operand::Reg(new_reg), Operand::Imm(cell_idx as u16), )); + self.emit_module_write_through(name, new_reg, ctx)?; Ok(if update.prefix { new_reg } else { old_reg }) } else { let var_reg = ctx.lookup_or_global(name); @@ -766,6 +768,9 @@ impl Emitter { // 可写内置名:RMW 新值同步落全局对象属性。 self.emit_global_put_write(name, var_reg, ctx); } + // 槽位寄存器 rd 承载更新后的新值:后缀形式 result_reg 保留旧值, + // 写穿必须取 var_reg,否则命名空间条目被写回旧值。 + self.emit_module_write_through(name, var_reg, ctx)?; Ok(result_reg) } } diff --git a/crates/oxide_emit/src/module.rs b/crates/oxide_emit/src/module.rs index af50eed0..56ead56f 100644 --- a/crates/oxide_emit/src/module.rs +++ b/crates/oxide_emit/src/module.rs @@ -496,18 +496,19 @@ impl Emitter { } } - // —— 写穿反演:源绑定名 → 导出名集合。仅 live 模块(自导入 ns)填充: - // 非 live 模块不激活写穿,编译产物逐字节零 IR 变化。按名排序保证多导出名 - // 映射到同一源绑定时的发射顺序确定。—— + // —— 写穿反演:源绑定槽寄存器 → 导出名集合。仅 live 模块(自导入 ns)填充: + // 非 live 模块不激活写穿,编译产物逐字节零 IR 变化。以槽位为键使块级/catch + // 同名遮蔽解析到内层槽时不命中;按名排序保证多导出名映射到同一源绑定的 + // 发射顺序确定。—— if has_self_ns_import { let mut pairs: Vec<(&String, &String)> = export_name_map.iter().map(|(exported, source)| (source, exported)).collect(); pairs.sort(); for (source, exported) in pairs { - ctx.module_local_exports - .entry(source.clone()) - .or_default() - .push(exported.clone()); + let Some(reg) = ctx.scopes.symbols.lookup_any(source) else { + continue; + }; + ctx.module_local_export_regs.entry(reg).or_default().push(exported.clone()); } } @@ -710,8 +711,10 @@ impl Emitter { /// 顶层对导出源绑定的写入同步到命名空间:按反演表把新值写入每个导出名。 /// /// # 边界与前提 - /// - 仅在模块顶层 live 模块激活:`module_local_exports` 只由自导入 ns 模块填充, - /// 嵌套函数 ctx 不继承该表,行为自然为零;`name` 非导出源时直接返回。 + /// - 仅在模块顶层 live 模块激活:`module_local_export_regs` 只由自导入 ns 模块 + /// 填充,嵌套函数 ctx 不继承该表,行为自然为零。 + /// - `name` 按当前作用域解析到绑定槽,槽不在表内(未声明名、块级/catch 同名 + /// 遮蔽等)时直接返回,不误写同名导出。 /// - 调用方保证 `value_reg` 是写入后的当前值;const 抛错分支不得调用本函数。 /// /// # 副作用 @@ -722,7 +725,10 @@ impl Emitter { let Some(ns_reg) = ctx.module_ns_reg else { return Ok(()); }; - let Some(export_names) = ctx.module_local_exports.get(name).cloned() else { + let Some((binding, _)) = ctx.scopes.symbols.lookup_any_binding(name) else { + return Ok(()); + }; + let Some(export_names) = ctx.module_local_export_regs.get(&binding.reg).cloned() else { return Ok(()); }; for export_name in export_names { diff --git a/crates/oxide_emit/src/shared/binding.rs b/crates/oxide_emit/src/shared/binding.rs index 0ce72e16..6555e93d 100644 --- a/crates/oxide_emit/src/shared/binding.rs +++ b/crates/oxide_emit/src/shared/binding.rs @@ -179,6 +179,8 @@ impl Emitter { // 可写内置名:值同步落全局对象属性。 self.emit_global_put_write(name, var_reg, ctx); } + // live 模块顶层解构赋值目标同样写穿命名空间条目。 + self.emit_module_write_through(name, src_reg, ctx)?; Ok(()) } AssignmentTarget::ArrayAssignmentTarget(ap) => self.emit_array_assignment(ap, src_reg, ctx), @@ -390,6 +392,8 @@ impl Emitter { // 可写内置名:值同步落全局对象属性。 self.emit_global_put_write(name, var_reg, ctx); } + // live 模块顶层解构赋值目标同样写穿命名空间条目。 + self.emit_module_write_through(name, src_reg, ctx)?; Ok(()) } _ => Err("assignment target not supported".into()), @@ -483,6 +487,8 @@ impl Emitter { // 可写内置名:值同步落全局对象属性(其余属性继续赋值)。 self.emit_global_put_write(name, var_reg, ctx); } + // live 模块顶层解构赋值目标同样写穿命名空间条目。 + self.emit_module_write_through(name, prop_reg, ctx)?; } AssignmentTargetProperty::AssignmentTargetPropertyProperty(prop) => { let (prop_reg, static_key, key_reg) = diff --git a/crates/oxide_vm/tests/module_namespace_tests.rs b/crates/oxide_vm/tests/module_namespace_tests.rs index cbc55ee8..6de4e43b 100644 --- a/crates/oxide_vm/tests/module_namespace_tests.rs +++ b/crates/oxide_vm/tests/module_namespace_tests.rs @@ -248,6 +248,53 @@ fn namespace_assignment_write_through_multiple_export_names() { assert_eq!(result, "2|2|3", "导出源绑定重赋未写穿命名空间活值"); } +/// 写穿以解析后的绑定身份为准:块级/catch 同名遮蔽的赋值不得污染模块导出。 +/// 遮蔽名解析到内层绑定槽,与导出源绑定槽不同,写穿须按槽位过滤而非仅按名字。 +#[test] +fn namespace_write_through_respects_block_shadowing() { + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join("__module_namespace_shadow__"); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write( + dir.join("main.mjs"), + "import * as ns from './main.mjs';\n \ + export let x = 1;\n \ + { let x = 2; x = 3; }\n \ + try { throw 0; } catch (x) { x = 4; }\n \ + globalThis.__ns = [ns.x, x].join('|');", + ) + .expect("write main"); + let _cleanup = Cleanup(dir.clone()); + + let result = run_namespace_module(&dir); + assert_eq!(result, "1|1", "块级/catch 同名遮蔽的赋值不得写穿命名空间条目"); +} + +/// 写穿覆盖 Update 表达式、逻辑赋值与解构赋值三条发射路径,与简单/二元复合 +/// 赋值一致地把新值同步到命名空间条目。 +#[test] +fn namespace_write_through_covers_update_logical_destructuring() { + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join("__module_namespace_write_paths__"); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write( + dir.join("main.mjs"), + "import * as ns from './main.mjs';\n \ + export let a = 1;\n \ + export let b = 0;\n \ + export let c = 1;\n \ + a++;\n \ + b ||= 7;\n \ + [c] = [9];\n \ + globalThis.__ns = [ns.a, ns.b, ns.c].join('|');", + ) + .expect("write main"); + let _cleanup = Cleanup(dir.clone()); + + let result = run_namespace_module(&dir); + assert_eq!(result, "2|7|9", "Update/逻辑/解构赋值的写穿缺失"); +} + /// G2 反向守卫:外部导入的命名空间无条目表(非 live),枚举与 for-in 行为不变。 #[test] fn external_namespace_enumeration_stays_unaffected() {