From 5768febadb7ec97de491ef751800342e050758ff Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 17 Sep 2026 23:10:48 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=87=AA=E5=AF=BC=E5=85=A5=E5=88=AB?= =?UTF-8?q?=E5=90=8D=E4=B8=8E=E6=BA=90=E5=90=8C=E6=97=B6=E6=8D=95=E8=8E=B7?= =?UTF-8?q?=E6=97=B6=E5=90=88=E5=B9=B6=E5=8D=95=E5=85=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_emit/src/compile_ctx.rs | 2 + crates/oxide_emit/src/module.rs | 48 +++++++------ .../tests/module_top_completion_tests.rs | 16 +++++ .../oxide_vm/tests/self_import_path_tests.rs | 68 +++++++++++++++++++ 4 files changed, 114 insertions(+), 20 deletions(-) diff --git a/crates/oxide_emit/src/compile_ctx.rs b/crates/oxide_emit/src/compile_ctx.rs index 83e185ea..53a6f932 100644 --- a/crates/oxide_emit/src/compile_ctx.rs +++ b/crates/oxide_emit/src/compile_ctx.rs @@ -683,6 +683,8 @@ impl CompileCtx { param_layout, builtin_reg_map: std::mem::take(&mut self.scopes.builtin_reg_map), upvalue_captures, + // 容量按名字集大小取:别名捕获合并会让多个名字共享同一 cell, + // 下标可重复但不超过名字数,故 len 恒覆盖实际使用的最大下标。 cells_needed: self.captured_bindings.len() as u8, n_registers: self.max_regs, is_arrow: false, diff --git a/crates/oxide_emit/src/module.rs b/crates/oxide_emit/src/module.rs index c7521054..d7178207 100644 --- a/crates/oxide_emit/src/module.rs +++ b/crates/oxide_emit/src/module.rs @@ -649,33 +649,41 @@ impl Emitter { /// /// # 边界与前提 /// - `captured_bindings` 是名字级索引;源与别名共用寄存器槽,但捕获分析按 - /// 名字给出各自(或仅一方)的 entry。两侧都有时以源 cell 为准并把别名指向 - /// 同一 cell;仅别名有 cell 时把源也指向该 cell(别名会被 MAKE_CELL 建 cell)。 + /// 名字给出各自(或仅一方)的 entry。同一源的多个别名须并入同一 cell: + /// 声明点的 MAKE_CELL 只按源名发射,未被覆盖的别名 cell 会停留未初始化。 + /// - 权威 cell 优先取源名自身的 entry(源被捕获时声明点的 MAKE_CELL 写它); + /// 源未被捕获而别名被捕获时取组内别名已分配的 entry,再把源名补进映射。 /// - 两者都未被捕获时读路径直接读源寄存器,天然活值,无需处理。 /// /// # 副作用 - /// - 修改 `captured_bindings` 映射;使 `cells_needed` 与实际使用的 cell - /// 上限一致(多余 entry 无害,缺失 entry 会导致子函数 upvalue 索引错位)。 + /// - 修改 `captured_bindings` 映射;每个名字仍占一个 entry,容量口径不变 + /// (`cells_needed` 按名字集大小取,cell 下标可重复,见 `assemble_ir`)。 fn reconcile_alias_captures(&self, ctx: &mut CompileCtx) { let pairs = std::mem::take(&mut ctx.module_alias_pairs); + // 以最终基源名为组键统一组内全部名字。逐个 pair 直接覆盖会让同源的多个 + // 别名中后处理者把源改指向新 cell,先处理的别名与源分裂成两个 cell。 + let mut authority: HashMap = HashMap::new(); for (local, source) in pairs { - let local_cell = ctx.captured_bindings.get(&local).copied(); - let source_cell = ctx.captured_bindings.get(&source).copied(); - match (local_cell, source_cell) { - (Some(idx), _) => { - // 别名被捕获:源须共用同一 cell(源尚未捕获时补映射)。 - if source_cell.is_none() { - ctx.captured_bindings.insert(source.clone(), idx); - } - } - (None, Some(idx)) => { - // 仅源被捕获:别名读经源 cell,补别名映射使子函数引用别名时 - // upvalue 解析到同一 cell。 - ctx.captured_bindings.insert(local.clone(), idx); - } - (None, None) => {} - } + let source = ctx.scopes.symbols.resolve_alias_base(&source).to_string(); + let cell = authority.get(&source).copied().or_else(|| { + ctx.captured_bindings + .get(source.as_str()) + .or_else(|| ctx.captured_bindings.get(local.as_str())) + .copied() + }); + let Some(cell) = cell else { continue }; + authority.insert(source.clone(), cell); + ctx.captured_bindings.insert(source, cell); + ctx.captured_bindings.insert(local, cell); } + // cell 下标由名字排序分配,可重复使用;容量按名字集大小取,故每个下标 + // 必须落在名字集范围内,否则子函数 upvalue 会索引到未分配的 cell。 + debug_assert!( + ctx.captured_bindings + .values() + .all(|&i| (i as usize) < ctx.captured_bindings.len()), + "cell 下标须落在 captured_bindings 名字集范围内" + ); } fn load_var_reg(&self, name: &str, ctx: &mut CompileCtx) -> Result { diff --git a/crates/oxide_vm/tests/module_top_completion_tests.rs b/crates/oxide_vm/tests/module_top_completion_tests.rs index ff9c14f5..7dbfd381 100644 --- a/crates/oxide_vm/tests/module_top_completion_tests.rs +++ b/crates/oxide_vm/tests/module_top_completion_tests.rs @@ -112,3 +112,19 @@ fn exported_class_declaration_binding_is_mutable() { let r = probe(&mut vm, "globalThis.__mutable === true"); assert!(r.is_bool() && r.as_bool(), "模块 class 外层绑定应可重赋,实际 {r:?}"); } + +#[test] +fn exported_default_function_binding_is_mutable() { + // 具名 `export default function` 的声明名是可变函数绑定(Var 提升), + // 允许声明后重赋;若误回退为 lexical Const,重赋会抛 TypeError。 + let allocator = oxide_parser::Allocator::default(); + let source = "export default function f() { return 1; }\nf = null;\nglobalThis.__fn_mutable = f === null;"; + let program = oxide_parser::parse_module(&allocator, source).expect("parse module"); + let module = Compiler::new() + .compile_module(&program, "test.mjs", &mut FileLoader) + .expect("compile module"); + let mut vm = Vm::new(); + vm.run(&Arc::new(module)).expect("module run"); + let r = probe(&mut vm, "globalThis.__fn_mutable === true"); + assert!(r.is_bool() && r.as_bool(), "具名 default 函数声明绑定应可重赋,实际 {r:?}"); +} diff --git a/crates/oxide_vm/tests/self_import_path_tests.rs b/crates/oxide_vm/tests/self_import_path_tests.rs index f688e896..afd1631d 100644 --- a/crates/oxide_vm/tests/self_import_path_tests.rs +++ b/crates/oxide_vm/tests/self_import_path_tests.rs @@ -111,3 +111,71 @@ fn self_import_alias_reads_live_source_value() { let text = vm.lookup_str(r).unwrap_or_default(); assert_eq!(text, "true|2", "自导入别名应共享源绑定活值"); } + +/// 同一闭包同时引用源与别名时,两者必须落到同一 cell:重赋后两侧读到同一新值。 +/// 捕获分析按名字分配 cell,源与别名是两个名字;若捕获后处理不把二者合并到同一 +/// 单元,别名的 cell 永不初始化,闭包读别名会误抛 TDZ ReferenceError。 +#[test] +fn self_import_alias_and_source_share_cell() { + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join("__selfimport_both__"); + let file = dir.join("both.mjs"); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write( + &file, + "import { x as y } from './both.mjs';\n \ + function readBoth() { return [x, y]; }\n \ + export let x = 1;\n \ + x = 2;\n \ + globalThis.__both = readBoth().join('|');", + ) + .expect("write module"); + let _cleanup = Cleanup(dir.clone()); + + let allocator = oxide_parser::Allocator::default(); + let source = std::fs::read_to_string(&file).expect("read module"); + let program = oxide_parser::parse_module(&allocator, &source).expect("parse module"); + let module = Compiler::new() + .compile_module(&program, dir.join("both.mjs").to_string_lossy().as_ref(), &mut AbsLoader) + .expect("compile module"); + + let mut vm = Vm::new(); + vm.run(&Arc::new(module)).expect("module run"); + let r = probe(&mut vm, "globalThis.__both"); + let text = vm.lookup_str(r).unwrap_or_default(); + assert_eq!(text, "2|2", "源与别名须共享同一 cell,重赋后两侧同值"); +} + +/// 同一源被多个别名与源一并捕获时,所有名字必须归并到同一 cell:逐个别名对 +/// 处理会让后处理的 pair 把源改指向新 cell,先处理的别名 cell 掉队未初始化。 +#[test] +fn self_import_multiple_aliases_share_source_cell() { + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join("__selfimport_multi__"); + let file = dir.join("multi.mjs"); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write( + &file, + "import { x as y } from './multi.mjs';\n \ + import { x as z } from './multi.mjs';\n \ + function readAll() { return [x, y, z]; }\n \ + export let x = 1;\n \ + x = 3;\n \ + globalThis.__multi = readAll().join('|');", + ) + .expect("write module"); + let _cleanup = Cleanup(dir.clone()); + + let allocator = oxide_parser::Allocator::default(); + let source = std::fs::read_to_string(&file).expect("read module"); + let program = oxide_parser::parse_module(&allocator, &source).expect("parse module"); + let module = Compiler::new() + .compile_module(&program, dir.join("multi.mjs").to_string_lossy().as_ref(), &mut AbsLoader) + .expect("compile module"); + + let mut vm = Vm::new(); + vm.run(&Arc::new(module)).expect("module run"); + let r = probe(&mut vm, "globalThis.__multi"); + let text = vm.lookup_str(r).unwrap_or_default(); + assert_eq!(text, "3|3|3", "同源多别名与源须归并到同一 cell"); +}