fix: TDZ 写与 const 赋值检查补全(含闭包捕获路径)

This commit is contained in:
2026-08-15 21:55:43 +08:00
parent 2669967967
commit 07ea7e6d4f
8 changed files with 224 additions and 16 deletions
+61 -6
View File
@@ -183,6 +183,9 @@ pub struct CompileCtx {
/// 捕获判断(MAKE_CELL / CELL_GET / CELL_SET)与子函数 upvalue cell_idx 统一查此映射,
/// 消除符号表时序依赖与 cell 索引错位。
pub(crate) captured_bindings: BTreeMap<String, u8>,
/// 本函数从父函数捕获的 const 绑定名:子 ctx 不继承父函数作用域符号表,
/// 捕获 const 信息随 upvalue 收集一并快照,供 const 写检查(编译期拦截)使用。
pub(crate) upvalue_const_flags: HashSet<String>,
/// 函数 `length` 属性值:首个带默认值形参之前的形参数(rest 不计)。
/// emit_params_prologue 前由编译入口从 param_specs 计算。
pub(crate) function_length: u32,
@@ -298,6 +301,7 @@ impl CompileCtx {
current_upvalue_captures: Vec::new(),
own_bindings: HashSet::new(),
captured_bindings: BTreeMap::new(),
upvalue_const_flags: HashSet::new(),
function_length: 0,
const_overflow: false,
with_stack: Vec::new(),
@@ -424,7 +428,8 @@ impl CompileCtx {
}
pub(crate) fn lookup_const_flag(&self, name: &str) -> bool {
self.scopes.symbols.lookup_is_const(name)
// upvalue 捕获的 const:子 ctx 符号表不含父函数作用域绑定,查快照标志。
self.scopes.symbols.lookup_is_const(name) || self.upvalue_const_flags.contains(name)
}
pub(crate) fn init_var(&mut self, name: &str) {
@@ -670,15 +675,18 @@ impl Emitter {
Self
}
/// 生成运行时抛 ReferenceError 的指令序列,返回一个未定义 dummy 寄存器
/// 生成运行时抛 `kind` 类型错误的指令序列,返回一个未定义 dummy 寄存器
/// 保证 THROW 后不可达控制流的寄存器良定义。
///
/// # 步骤
/// 1. 取全局 ReferenceError 构造器并 LOAD。
/// 2. 加载错误消息常量,`new ReferenceError(msg)` 构造错误对象。
/// 1. 取全局错误构造器并 LOAD。
/// 2. 加载错误消息常量,`new {kind}(msg)` 构造错误对象。
/// 3. THROW 抛出;尾接 dummy 值保持后续读引用有确定寄存器。
pub(crate) fn emit_tdz_throw(&self, msg: &str, ctx: &mut CompileCtx) -> Result<u32, String> {
let ctor_reg = ctx.lookup_or_builtin("ReferenceError")?;
///
/// # 边界与前提
/// - `kind` 必须是已注册的全局构造器名(如 "ReferenceError"/"TypeError")。
pub(crate) fn emit_throw_error(&self, kind: &str, msg: &str, ctx: &mut CompileCtx) -> Result<u32, String> {
let ctor_reg = ctx.lookup_or_builtin(kind)?;
let ctor = ctx.alloc_reg();
ctx.inst(Inst::new(OpCode::LOAD_VAR, Operand::Reg(ctor), Operand::Reg(ctor_reg), Operand::None));
let msg_reg = ctx.alloc_reg();
@@ -693,6 +701,37 @@ impl Emitter {
Ok(dummy)
}
/// 生成运行时抛 ReferenceError 的指令序列(TDZ 访问专用,语义见 [`emit_throw_error`])。
pub(crate) fn emit_tdz_throw(&self, msg: &str, ctx: &mut CompileCtx) -> Result<u32, String> {
self.emit_throw_error("ReferenceError", msg, ctx)
}
/// 赋值目标 TDZ 检查:未初始化绑定在赋值引用解析时抛 ReferenceError。
/// 须在 RHS 求值之前调用(规范:赋值 LHS 的 ResolveBinding 先于 RHS 副作用)。
///
/// # 副作用
/// - TDZ 命中时发射 THROW 指令序列,其后指令不可达但保持寄存器良定义。
pub(crate) fn emit_identifier_tdz_guard(&self, name: &str, ctx: &mut CompileCtx) -> Result<(), String> {
if let Some((binding, _)) = ctx.scopes.symbols.lookup_any_binding(name) {
if !binding.initialized {
let _ = self.emit_tdz_throw(&format!("Cannot access '{name}' before initialization"), ctx)?;
}
}
Ok(())
}
/// const 写检查:已初始化的 const 绑定再赋值编译期抛 TypeError(与槽值无关)。
/// 简单赋值在 RHS 求值之后、复合/更新在读旧值之前调用;解构赋值在写目标时调用。
///
/// # 副作用
/// - const 命中时发射 THROW 指令序列,其后写指令不可达但保持寄存器良定义。
pub(crate) fn emit_const_write_guard(&self, name: &str, ctx: &mut CompileCtx) -> Result<(), String> {
if ctx.lookup_const_flag(name) {
let _ = self.emit_throw_error("TypeError", "Assignment to constant variable", ctx)?;
}
Ok(())
}
/// 遍历解构 pattern 收集内嵌默认值表达式(AssignmentPattern.right)。
fn collect_pattern_default_exprs<'a>(
&self, pattern: &'a oxide_parser::BindingPattern<'a>, out: &mut Vec<&'a oxide_parser::Expression<'a>>,
@@ -1140,6 +1179,22 @@ impl Emitter {
&parent_ctx.current_upvalue_captures,
&ctx.own_bindings,
);
// 捕获 const 信息快照:父作用域符号表此时完整(预声明已完成),
// 直接查绑定 is_const(不依赖初始化状态,TDZ 中 const 也须拦截)。
ctx.upvalue_const_flags = ctx
.current_upvalue_captures
.iter()
.filter(|u| u.parent_uv_idx.is_none())
.filter(|u| {
parent_ctx
.scopes
.symbols
.lookup_any_binding(u.name.as_str())
.map(|(b, _)| b.is_const)
.unwrap_or(false)
})
.map(|u| u.name.clone())
.collect();
// 类字段 computed key 数组等合成捕获:直接追加 upvalue(cell_idx 由父分配)。
for (name, cell_idx) in extra_upvalue_names {
if !ctx.own_bindings.contains(*name) && !ctx.current_upvalue_captures.iter().any(|u| u.name == *name) {
+22 -5
View File
@@ -18,6 +18,8 @@ impl Emitter {
fn emit_compound_identifier_static(
&self, name: &str, op: OpCode, rhs: u32, ctx: &mut CompileCtx,
) -> Result<u32, String> {
// const 复合赋值恒抛 TypeError(编译期拦截,值无关);检查在读旧值之前。
self.emit_const_write_guard(name, ctx)?;
// 循环 update 段:被捕获绑定走寄存器 RMW(C 风格 for 每迭代 fresh,
// update 写寄存器供下一迭代 fresh 拷贝,不污染本迭代闭包捕获的 cell)。
if ctx.register_update_names.iter().any(|n| n == name) {
@@ -236,6 +238,8 @@ impl Emitter {
let store_label = ctx.next_label_id();
let end_label = ctx.next_label_id();
let name = id_ref.name.as_str();
// 逻辑赋值先解析赋值引用:TDZ 绑定在读旧值前抛 ReferenceError。
self.emit_identifier_tdz_guard(name, ctx)?;
// 目标判定:upvalue / 被捕获 cell / 普通槽,读与写须穿透共享单元。
let uv_idx = ctx.current_upvalue_captures.iter().position(|u| u.name == name);
let captured_cell = ctx.captured_bindings.get(name).copied();
@@ -270,6 +274,8 @@ impl Emitter {
self.emit_logical_assign_test(logical_op, result_reg, store_label, end_label, ctx)?;
ctx.labels.set_label_pos(store_label, ctx.insts.len());
let val_reg = self.emit_expression(&assign.right, ctx)?;
// 短路未通过才写:const 目标在此抛 TypeError(编译期拦截,值无关)。
self.emit_const_write_guard(name, ctx)?;
let const_flag = if ctx.lookup_const_flag(name) { 1 } else { 0 };
if let Some(uv) = uv_idx {
ctx.inst(Inst::new(
@@ -317,8 +323,14 @@ impl Emitter {
|| assign.operator == AssignmentOperator::ShiftRight
|| assign.operator == AssignmentOperator::ShiftRightZeroFill
{
let rhs = self.emit_expression(&assign.right, ctx)?;
let name = id_ref.name.as_str();
// with 体内自由标识符的复合赋值由运行时对象遮蔽判定,不静态检查;
// 其余路径 TDZ 检查在 RHS 求值之前(规范:赋值引用解析先于 RHS 副作用)。
let with_dynamic = !ctx.with_stack.is_empty() && !ctx.is_with_internal_binding(name);
if !with_dynamic {
self.emit_identifier_tdz_guard(name, ctx)?;
}
let rhs = self.emit_expression(&assign.right, ctx)?;
let op = match assign.operator {
AssignmentOperator::Addition => OpCode::COMPOUND_ADD,
AssignmentOperator::Subtraction => OpCode::COMPOUND_SUB,
@@ -336,7 +348,7 @@ impl Emitter {
};
// with 体内自由标识符的复合赋值走动态路径:对象有属性则读对象、
// 运算后写回对象,否则回退外层静态复合。
if !ctx.with_stack.is_empty() && !ctx.is_with_internal_binding(name) {
if with_dynamic {
let obj_reg = ctx.innermost_with_obj().expect("with stack non-empty");
let key_idx = ctx.add_constant(Constant::String(name.to_string()));
let key_reg = ctx.alloc_reg();
@@ -387,8 +399,14 @@ impl Emitter {
Err(format!("compound assignment operator {:?} not supported", assign.operator))
}
} else {
let val_reg = self.emit_expression(&assign.right, ctx)?;
let name = id_ref.name.as_str();
// with 体内自由标识符走动态写(对象属性运行时遮蔽判定,不静态检查 TDZ);
// 其余路径 TDZ 检查在 RHS 求值之前(规范:赋值引用解析先于 RHS 副作用)。
let with_dynamic = !ctx.with_stack.is_empty() && !ctx.is_with_internal_binding(name);
if !with_dynamic {
self.emit_identifier_tdz_guard(name, ctx)?;
}
let val_reg = self.emit_expression(&assign.right, ctx)?;
// 赋值函数/箭头/class 表达式 → 推断 name。
if crate::is_anonymous_function_definition(&assign.right) {
if let Some(sub_mod) = ctx.nested.last_mut() {
@@ -397,8 +415,7 @@ impl Emitter {
}
}
}
// with 体内的自由标识符走动态写:对象有属性则写对象,否则写外层。
if !ctx.with_stack.is_empty() && !ctx.is_with_internal_binding(name) {
if with_dynamic {
let is_const = ctx.lookup_const_flag(name);
let const_flag: u16 = if is_const { 1 } else { 0 };
self.emit_with_dynamic_write(name, val_reg, const_flag, ctx);
+8 -2
View File
@@ -116,9 +116,15 @@ impl Emitter {
/// 静态标识符写入:upvalue / 被捕获 cell / 普通槽(含 const guard)。
///
/// # 边界与前提
/// - `const_flag` 为 1 时走 STORE_VAR / STORE_UPVALUE 运行时 const guard(对 const 再赋值报错)。
/// - cell 写穿共享单元,不设 guard(类名/闭包捕获的初始化写入路径);upvalue 按 const_flag 设 guard。
/// - `const_flag` 为 1 时编译期直接抛 TypeError(值无关),覆盖普通/upvalue/cell 全写路径;
/// 不发射写指令(THROW 后不可达)。
/// - cell 写穿共享单元,无运行时 guard;const 拦截由本入口编译期完成。
pub(crate) fn emit_identifier_store(&self, name: &str, val_reg: u32, const_flag: u16, ctx: &mut CompileCtx) {
// const 再赋值编译期抛 TypeError:赋值路径(含闭包捕获 const 写 cell)统一拦截。
if const_flag != 0 {
let _ = self.emit_throw_error("TypeError", "Assignment to constant variable", ctx);
return;
}
// 循环 update 段:被捕获绑定走寄存器而非 cell(C 风格 for 每迭代 fresh,
// update 写寄存器供下一迭代 fresh 拷贝,不污染本迭代闭包捕获的 cell)。
if ctx.register_update_names.iter().any(|n| n == name) {
@@ -466,6 +466,9 @@ impl Emitter {
fn emit_identifier_update_static(
&self, name: &str, update: &oxide_parser::UpdateExpression, ctx: &mut CompileCtx,
) -> Result<u32, String> {
// 自增/自减先解析赋值引用:TDZ 绑定抛 ReferenceError;const 抛 TypeError(编译期拦截)。
self.emit_identifier_tdz_guard(name, ctx)?;
self.emit_const_write_guard(name, ctx)?;
let uv_idx = ctx.current_upvalue_captures.iter().position(|u| u.name == name);
let captured_cell = ctx.captured_bindings.get(name).copied();
// 循环 update 段:被捕获绑定走寄存器 INC/DEC(C 风格 for 每迭代 fresh,
+9
View File
@@ -112,6 +112,9 @@ impl Emitter {
match target {
AssignmentTarget::AssignmentTargetIdentifier(id) => {
let name = id.name.as_str();
// 解构赋值目标:TDZ 写抛 ReferenceError,const 写抛 TypeError(编译期拦截)。
self.emit_identifier_tdz_guard(name, ctx)?;
self.emit_const_write_guard(name, ctx)?;
let var_reg = ctx.lookup_or_global(name);
ctx.inst(Inst::new(
OpCode::STORE_VAR,
@@ -294,6 +297,9 @@ impl Emitter {
AssignmentTargetMaybeDefault::ObjectAssignmentTarget(op) => self.emit_object_assignment(op, src_reg, ctx),
AssignmentTargetMaybeDefault::AssignmentTargetIdentifier(id) => {
let name = id.name.as_str();
// 解构赋值目标:TDZ 写抛 ReferenceError,const 写抛 TypeError(编译期拦截)。
self.emit_identifier_tdz_guard(name, ctx)?;
self.emit_const_write_guard(name, ctx)?;
let var_reg = ctx.lookup_or_global(name);
ctx.inst(Inst::new(
OpCode::STORE_VAR,
@@ -358,6 +364,9 @@ impl Emitter {
prop_reg = self.emit_default_if_undefined(prop_reg, default_expr, Some(name), ctx)?;
}
let name = id.binding.name.as_str();
// 解构赋值目标:TDZ 写抛 ReferenceError,const 写抛 TypeError(编译期拦截)。
self.emit_identifier_tdz_guard(name, ctx)?;
self.emit_const_write_guard(name, ctx)?;
let var_reg = ctx.lookup_or_global(name);
ctx.inst(Inst::new(
OpCode::STORE_VAR,
+15 -1
View File
@@ -252,6 +252,10 @@ impl Vm {
}
unsafe {
let cell = &mut *cell_ptr;
// TDZ 写检查:cell 未初始化(捕获绑定声明点前)写抛 ReferenceError。
if !cell.is_initialized() {
return self.raise_error_kind("ReferenceError", "Cannot access variable before initialization");
}
cell.value = src_val;
cell.set_initialized(true);
}
@@ -330,7 +334,17 @@ impl Vm {
if !upvals[uv_idx].is_null() {
unsafe {
let cell = &mut *upvals[uv_idx];
if const_flag != 0 && !cell.value.is_undefined() {
// TDZ 写检查:cell 未初始化(捕获绑定声明点前)写抛 ReferenceError。
if !cell.is_initialized() {
self.raise_error_kind(
"ReferenceError",
"Cannot access variable before initialization",
)?;
return Ok(());
}
// const guard:TDZ 检查后 cell 必已初始化(存在性判定,与值无关),
// const 初始值为 undefined 时再赋值同样抛。
if const_flag != 0 {
self.raise_error_kind("TypeError", "Assignment to constant variable")?;
return Ok(());
}
@@ -50,3 +50,64 @@ fn block_scoped_const_shadowing_independent() {
fn const_destructuring_declaration_works() {
assert_eq!(eval("const { a } = { a: 7 }; a"), "7");
}
// const 初始化为 undefined 时再赋值也必须抛(guard 不能依赖槽值)。
#[test]
fn const_undefined_reassignment_throws() {
let out = eval("const x = undefined; x = 5");
assert!(out.contains("Assignment to constant variable"), "got: {out}");
}
// const 初始化为 undefined 时读取不受影响。
#[test]
fn const_undefined_read_ok() {
assert_eq!(eval("const x = undefined; x"), "undefined");
}
// 闭包捕获 const 赋值抛(cell 写路径编译期拦截)。
#[test]
fn const_captured_write_throws() {
let out = eval("const x = 1; (()=>x=2)()");
assert!(out.contains("Assignment to constant variable"), "got: {out}");
}
// 函数体内 const 被嵌套函数捕获(upvalue 路径)写同样抛。
#[test]
fn const_upvalue_write_throws() {
let out = eval("function outer(){ const x=1; function inner(){ x=2; } inner(); } outer()");
assert!(out.contains("Assignment to constant variable"), "got: {out}");
}
// 自增/自减路径 const 赋值抛。
#[test]
fn const_update_throws() {
let out = eval("const x = 1; x++");
assert!(out.contains("Assignment to constant variable"), "got: {out}");
}
// 复合赋值路径 const 抛。
#[test]
fn const_compound_throws() {
let out = eval("const x = 1; x += 1");
assert!(out.contains("Assignment to constant variable"), "got: {out}");
}
// 解构赋值目标 const 抛。
#[test]
fn const_destructuring_assignment_throws() {
let out = eval("const [a] = [1]; a = 5");
assert!(out.contains("Assignment to constant variable"), "got: {out}");
}
// 逻辑赋值短路场景:const truthy 时 ||= 不写不抛。
#[test]
fn const_logical_assign_short_circuit_ok() {
assert_eq!(eval("const x = 1; x ||= 5; x"), "1");
}
// 逻辑赋值非短路场景:const falsy 时 ||= 写抛。
#[test]
fn const_logical_assign_write_throws() {
let out = eval("const x = 0; x ||= 5");
assert!(out.contains("Assignment to constant variable"), "got: {out}");
}
+45 -2
View File
@@ -112,6 +112,49 @@ fn for_let_per_iteration_independent() {
#[test]
fn tdz_access_before_init_throws() {
let _ = eval("x; let x=1");
// 未支持:精确 TDZ 尚未实现。
// 块级预声明后声明点前读取:TDZ 读抛 ReferenceError。
let err = eval("x; let x=1").unwrap_err();
assert!(err.contains("ReferenceError"), "got: {}", err);
}
#[test]
fn tdz_write_throws() {
// TDZ 写:赋值引用解析先于 RHS,`x=1; let x;` 抛 ReferenceError。
let err = eval("x=1; let x;").unwrap_err();
assert!(err.contains("ReferenceError"), "got: {}", err);
}
#[test]
fn tdz_compound_write_throws() {
// 复合赋值路径 TDZ:`x += 1` 在读旧值前解析赋值引用。
let err = eval("{ x += 1; let x; }").unwrap_err();
assert!(err.contains("ReferenceError"), "got: {}", err);
}
#[test]
fn tdz_update_throws() {
// 自增/自减路径 TDZ:`x++` 同样抛 ReferenceError。
let err = eval("{ x++; let x; }").unwrap_err();
assert!(err.contains("ReferenceError"), "got: {}", err);
}
#[test]
fn tdz_destructuring_throws() {
// 解构赋值目标 TDZ:`[a]=[1]` 写未初始化 a 抛 ReferenceError。
let err = eval("[a]=[1]; let a;").unwrap_err();
assert!(err.contains("ReferenceError"), "got: {}", err);
}
#[test]
fn tdz_write_rhs_side_effect_not_evaluated() {
// 检查点顺序:TDZ 检查在 RHS 求值之前,RHS 副作用不执行(s 保持 0)。
let result = eval("var s=0; try{ x=(s=1); let x; }catch(e){} s").unwrap();
assert_eq!(result.as_int(), 0);
}
#[test]
fn typeof_tdz_throws() {
// typeof 对 TDZ 绑定抛 ReferenceError(非 "undefined")。
let err = eval("typeof x; let x;").unwrap_err();
assert!(err.contains("ReferenceError"), "got: {}", err);
}