From efcb92b47edaa0793067b6ba17e213990a673c2d Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Tue, 6 Oct 2026 01:09:33 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BF=90=E8=A1=8C=E6=97=B6=20cell=20TDZ?= =?UTF-8?q?=20=E6=B6=88=E6=81=AF=E5=B8=A6=E4=B8=8A=E8=A2=AB=E8=AE=BF?= =?UTF-8?q?=E9=97=AE=E7=BB=91=E5=AE=9A=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 36 +++++----- Cargo.toml | 2 +- crates/oxide_bytecode/src/module.rs | 6 ++ crates/oxide_emit/src/compile_ctx.rs | 28 +++++--- crates/oxide_ir/src/lib.rs | 4 ++ crates/oxide_ir/src/lower.rs | 1 + crates/oxide_vm/src/dispatch/call.rs | 43 ++++++++++-- .../oxide_vm/tests/tdz_named_message_tests.rs | 70 +++++++++++++++++++ 8 files changed, 157 insertions(+), 33 deletions(-) create mode 100644 crates/oxide_vm/tests/tdz_named_message_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 0dea470c..5c33c175 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1095,7 +1095,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.8.16" +version = "0.8.17" dependencies = [ "chrono", "fast-float", @@ -1120,7 +1120,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.8.16" +version = "0.8.17" dependencies = [ "num-bigint", "oxide_log", @@ -1128,7 +1128,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.8.16" +version = "0.8.17" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1139,7 +1139,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.8.16" +version = "0.8.17" dependencies = [ "ansi_term", "bumpalo", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.8.16" +version = "0.8.17" dependencies = [ "lru", "oxide_bytecode", @@ -1168,7 +1168,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.8.16" +version = "0.8.17" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1187,7 +1187,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.8.16" +version = "0.8.17" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1201,7 +1201,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.8.16" +version = "0.8.17" dependencies = [ "num-bigint", "oxc_allocator", @@ -1216,7 +1216,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.8.16" +version = "0.8.17" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1225,7 +1225,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.8.16" +version = "0.8.17" dependencies = [ "bumpalo", "dashmap", @@ -1240,7 +1240,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.8.16" +version = "0.8.17" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1252,7 +1252,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.8.16" +version = "0.8.17" dependencies = [ "tracing", "tracing-appender", @@ -1261,7 +1261,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.8.16" +version = "0.8.17" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.8.16" +version = "0.8.17" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1292,7 +1292,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.8.16" +version = "0.8.17" dependencies = [ "num-bigint", "num-traits", @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.8.16" +version = "0.8.17" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.8.16" +version = "0.8.17" dependencies = [ "bumpalo", "hashbrown 0.14.5", @@ -1333,7 +1333,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.8.16" +version = "0.8.17" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index c2340087..52754759 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ ] [workspace.package] -version = "0.8.16" +version = "0.8.17" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_bytecode/src/module.rs b/crates/oxide_bytecode/src/module.rs index 5ad34415..f39c8e60 100644 --- a/crates/oxide_bytecode/src/module.rs +++ b/crates/oxide_bytecode/src/module.rs @@ -83,6 +83,10 @@ pub struct CompiledModule { pub is_async: bool, pub upvalue_captures: Vec, pub cells_needed: u8, + /// own cell 绑定名表:下标 = cell 下标,值 = 绑定名;未覆盖下标(合成 + /// cell、块级预声明追加)留空串。与 `cells_needed` 同容量口径,供运行时 + /// TDZ 消息取名。 + pub cell_names: Vec, /// 全局扁平模块 id:编译末端 flatten 阶段分配(顶层 0,子模块 DFS 递增)。 /// `CREATE_CLOSURE` 的 imm16 在 flatten 后即此 id,运行时以它为平表下标。 pub flat_id: u32, @@ -115,6 +119,7 @@ impl CompiledModule { is_async: false, upvalue_captures: Vec::new(), cells_needed: 0, + cell_names: Vec::new(), flat_id: 0, is_es_module: false, } @@ -150,6 +155,7 @@ impl Clone for CompiledModule { is_async: self.is_async, upvalue_captures: self.upvalue_captures.clone(), cells_needed: self.cells_needed, + cell_names: self.cell_names.clone(), flat_id: self.flat_id, is_es_module: self.is_es_module, } diff --git a/crates/oxide_emit/src/compile_ctx.rs b/crates/oxide_emit/src/compile_ctx.rs index fdff8abc..f27fdbf3 100644 --- a/crates/oxide_emit/src/compile_ctx.rs +++ b/crates/oxide_emit/src/compile_ctx.rs @@ -931,6 +931,22 @@ impl CompileCtx { } }) .collect(); + // 容量覆盖全部已分配 cell 索引:函数级名字排序索引(映射值 0..n-1) + // 与追加索引(计数器,块级预声明 / 合成 cell)取较大者加一;别名 + // 捕获合并让多个名字共享同一 cell,下标可重复。 + let cells_needed = self + .captured_bindings + .values() + .copied() + .max() + .map_or(0, |m| m.saturating_add(1)) + .max(self.next_cell_idx); + // own cell 绑定名表:下标 = cell 下标,值 = 绑定名;合成 cell 与块级 + // 预声明追加无源名,留空串。 + let mut cell_names = vec![String::new(); cells_needed as usize]; + for (name, idx) in &self.captured_bindings { + cell_names[*idx as usize] = name.clone(); + } IRFunction { insts: std::mem::take(&mut self.insts), label_pos: std::mem::take(&mut self.labels.label_pos), @@ -939,16 +955,8 @@ impl CompileCtx { param_layout, builtin_reg_map: std::mem::take(&mut self.scopes.builtin_reg_map), upvalue_captures, - // 容量覆盖全部已分配 cell 索引:函数级名字排序索引(映射值 0..n-1) - // 与追加索引(计数器,块级预声明 / 合成 cell)取较大者加一;别名 - // 捕获合并让多个名字共享同一 cell,下标可重复。 - cells_needed: self - .captured_bindings - .values() - .copied() - .max() - .map_or(0, |m| m.saturating_add(1)) - .max(self.next_cell_idx), + cells_needed, + cell_names, n_registers: self.max_regs, is_arrow: false, is_class_constructor: false, diff --git a/crates/oxide_ir/src/lib.rs b/crates/oxide_ir/src/lib.rs index dfbd78e6..17b44b1b 100644 --- a/crates/oxide_ir/src/lib.rs +++ b/crates/oxide_ir/src/lib.rs @@ -45,6 +45,9 @@ pub struct IRFunction { // closures 域 pub upvalue_captures: Vec, pub cells_needed: u8, + /// own cell 绑定名表:下标 = cell 下标,值 = 绑定名;未覆盖下标留空串。 + /// 与 `cells_needed` 同容量口径,供运行时 TDZ 消息取名。 + pub cell_names: Vec, // meta 域 pub n_registers: u32, pub is_arrow: bool, @@ -85,6 +88,7 @@ impl IRFunction { builtin_reg_map: Vec::new(), upvalue_captures: Vec::new(), cells_needed: 0, + cell_names: Vec::new(), n_registers: 0, is_arrow: false, is_class_constructor: false, diff --git a/crates/oxide_ir/src/lower.rs b/crates/oxide_ir/src/lower.rs index 6501855a..2213b447 100644 --- a/crates/oxide_ir/src/lower.rs +++ b/crates/oxide_ir/src/lower.rs @@ -98,6 +98,7 @@ pub fn lower(f: &IRFunction) -> Result { is_async: f.is_async, upvalue_captures: f.upvalue_captures.clone(), cells_needed: f.cells_needed, + cell_names: f.cell_names.clone(), flat_id: 0, is_es_module: false, }) diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index 51d3b04b..2a80db66 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -338,7 +338,7 @@ impl Vm { } let c = unsafe { &*current[cell_idx] }; if !c.is_initialized() { - return self.raise_error_kind("ReferenceError", "Cannot access variable before initialization"); + return self.raise_error_kind("ReferenceError", &Self::tdz_message(self.tdz_binding_name(cell_idx, false))); } self.regs[rd] = c.value; Ok(()) @@ -376,7 +376,8 @@ impl Vm { let cell = &mut *cell_ptr; // TDZ 写检查:cell 未初始化(捕获绑定声明点前)写抛 ReferenceError。 if !cell.is_initialized() { - return self.raise_error_kind("ReferenceError", "Cannot access variable before initialization"); + return self + .raise_error_kind("ReferenceError", &Self::tdz_message(self.tdz_binding_name(cell_idx, false))); } cell.value = src_val; cell.set_initialized(true); @@ -390,6 +391,36 @@ impl Vm { self.frames.last().map(|f| f.callee).or(self.inline_callee) } + /// TDZ 消息的绑定名:解析当前函数对象的编译模块,按站点类型取名 + /// (upvalue 站点查 upvalue_captures,own cell 站点查 cell_names)。 + /// + /// # 边界 + /// - 无当前函数对象(顶层脚本直读)、非对象 callee、模块解析缺失或下标 + /// 越界时返回 None,调用方回退通用文本。 + fn tdz_binding_name(&self, idx: usize, is_upvalue: bool) -> Option<&str> { + let callee = self.current_callee()?; + if !callee.is_object() { + return None; + } + // callee 是 GC 根,对象在取名期间稳定。 + let obj = unsafe { &*callee.as_js_object_ptr() }; + let module = self.callee_module(obj)?; + if is_upvalue { + module.upvalue_captures.get(idx).map(|c| c.name.as_str()) + } else { + module.cell_names.get(idx).filter(|n| !n.is_empty()).map(|n| n.as_str()) + } + } + + /// TDZ 消息:取到绑定名时用 `Cannot access 'x' before initialization` 形态, + /// 取不到时回退通用文本。 + fn tdz_message(name: Option<&str>) -> String { + match name { + Some(n) => format!("Cannot access '{n}' before initialization"), + None => String::from("Cannot access variable before initialization"), + } + } + /// 读取当前闭包的 upvalue:命中 cell 判初始化后取值,未命中委托惰性建 cell。 /// /// # 步骤 @@ -413,7 +444,8 @@ impl Vm { if !cell.is_null() { let c = unsafe { &*cell }; if !c.is_initialized() { - return self.raise_error_kind("ReferenceError", "Cannot access variable before initialization"); + return self + .raise_error_kind("ReferenceError", &Self::tdz_message(self.tdz_binding_name(uv_idx, true))); } self.regs[rd] = c.value; return Ok(()); @@ -483,7 +515,10 @@ impl Vm { let cell = &mut *upvals[uv_idx]; // TDZ 写检查:cell 未初始化(捕获绑定声明点前)写抛 ReferenceError。 if !cell.is_initialized() { - self.raise_error_kind("ReferenceError", "Cannot access variable before initialization")?; + self.raise_error_kind( + "ReferenceError", + &Self::tdz_message(self.tdz_binding_name(uv_idx, true)), + )?; return Ok(()); } // const guard:TDZ 检查后 cell 必已初始化(存在性判定,与值无关), diff --git a/crates/oxide_vm/tests/tdz_named_message_tests.rs b/crates/oxide_vm/tests/tdz_named_message_tests.rs new file mode 100644 index 00000000..ceee9f65 --- /dev/null +++ b/crates/oxide_vm/tests/tdz_named_message_tests.rs @@ -0,0 +1,70 @@ +//! TDZ 错误消息绑定名钉:运行时 cell 的四个 TDZ 抛点(own cell 读/写、upvalue +//! 读/写)消息须携带被访问绑定名(`Cannot access 'x' before initialization`, +//! 与 node 口径一致);取不到名时回退通用文本。每站点各钉一例:闭包读未初始化 +//! 捕获 let(LOAD_UPVALUE)、闭包写未初始化捕获 let(STORE_UPVALUE)、同函数块级 +//! let 声明前读(CELL_GET)、同函数块级 let 声明前写(CELL_SET)。 +//! +//! 块级 let 用例的被捕获绑定须被嵌套闭包引用(捕获名跳过静态 TDZ 守卫、留运行时 +//! 判定),故源例均含一个引用该绑定的嵌套闭包。顶层直读(无函数对象)取不到名, +//! 回退通用文本,属已知残面,单独钉住。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("parse error: {}", e[0].message))?; + let module = Compiler::new().compile(&program).map_err(|e| format!("compile error: {e}"))?; + let mut vm = Vm::new(); + vm.run(&Arc::new(module)) +} + +/// 断言程序抛错且未捕获错误消息含带名 TDZ 形态。 +fn assert_tdz_message(source: &str, binding: &str) { + let err = match eval(source) { + Ok(v) => panic!("expected a TDZ throw, got completion value {v:?}\nsource: {source}"), + Err(e) => e, + }; + let expected = format!("Cannot access '{binding}' before initialization"); + assert!(err.contains(&expected), "expected {expected} in: {err}\nsource: {source}"); +} + +#[test] +fn tdz_upvalue_read_message_carries_binding_name() { + // 闭包读未初始化捕获 let(LOAD_UPVALUE 站点):消息带绑定名。 + assert_tdz_message("function f() { return x; } f(); let x = 1", "x"); +} + +#[test] +fn tdz_upvalue_write_message_carries_binding_name() { + // 闭包写未初始化捕获 let(STORE_UPVALUE 站点):消息带绑定名。 + assert_tdz_message("function f() { x = 1; } f(); let x = 1", "x"); +} + +#[test] +fn tdz_cell_read_message_carries_binding_name() { + // 同函数块级 let 声明前读(CELL_GET 站点):绑定被块内闭包捕获,静态守卫 + // 跳过,声明前读经运行时 cell 抛带名 TDZ。 + assert_tdz_message("function f() { const g = () => x; return x; let x; } f()", "x"); +} + +#[test] +fn tdz_cell_write_message_carries_binding_name() { + // 同函数块级 let 声明前写(CELL_SET 站点):同读例的捕获形态。 + assert_tdz_message("function f() { const g = () => x; x = 1; let x; } f()", "x"); +} + +#[test] +fn tdz_top_level_read_falls_back_to_generic_message() { + // 顶层直读(无函数对象)取不到绑定名:回退通用文本(已知残面钉)。 + let err = match eval("function f() { return x; } x; let x = 1") { + Ok(v) => panic!("expected a TDZ throw, got completion value {v:?}"), + Err(e) => e, + }; + assert!( + err.contains("Cannot access variable before initialization"), + "expected generic TDZ message in: {err}" + ); +}