fix: 运行时 cell TDZ 消息带上被访问绑定名

This commit is contained in:
2026-10-06 01:09:33 +00:00
parent cb844ce64d
commit efcb92b47e
8 changed files with 157 additions and 33 deletions
+39 -4
View File
@@ -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 必已初始化(存在性判定,与值无关),
@@ -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<oxide_types::value::JsValue, String> {
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}"
);
}