From 26db7ae07c7774311158742488298c5f8af285c5 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 15 Aug 2026 21:55:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20TDZ=20=E5=86=99=E4=B8=8E=20const=20?= =?UTF-8?q?=E8=B5=8B=E5=80=BC=E6=A3=80=E6=9F=A5=E8=A1=A5=E5=85=A8=EF=BC=88?= =?UTF-8?q?=E5=90=AB=E9=97=AD=E5=8C=85=E6=8D=95=E8=8E=B7=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_emit/src/emit.rs | 67 ++++++++++++++++++++-- crates/oxide_emit/src/expr/assignment.rs | 27 +++++++-- crates/oxide_emit/src/expr/identifier.rs | 10 +++- crates/oxide_emit/src/expr/operator/mod.rs | 3 + crates/oxide_emit/src/shared/binding.rs | 9 +++ crates/oxide_vm/src/dispatch/call.rs | 16 +++++- crates/oxide_vm/tests/const_guard_tests.rs | 61 ++++++++++++++++++++ crates/oxide_vm/tests/scoping_tests.rs | 47 ++++++++++++++- 8 files changed, 224 insertions(+), 16 deletions(-) diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index eebe4701..792c54f9 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -183,6 +183,9 @@ pub struct CompileCtx { /// 捕获判断(MAKE_CELL / CELL_GET / CELL_SET)与子函数 upvalue cell_idx 统一查此映射, /// 消除符号表时序依赖与 cell 索引错位。 pub(crate) captured_bindings: BTreeMap, + /// 本函数从父函数捕获的 const 绑定名:子 ctx 不继承父函数作用域符号表, + /// 捕获 const 信息随 upvalue 收集一并快照,供 const 写检查(编译期拦截)使用。 + pub(crate) upvalue_const_flags: HashSet, /// 函数 `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 { - 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 { + 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 { + 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) { diff --git a/crates/oxide_emit/src/expr/assignment.rs b/crates/oxide_emit/src/expr/assignment.rs index bf9c2fb1..6ce6018a 100644 --- a/crates/oxide_emit/src/expr/assignment.rs +++ b/crates/oxide_emit/src/expr/assignment.rs @@ -18,6 +18,8 @@ impl Emitter { fn emit_compound_identifier_static( &self, name: &str, op: OpCode, rhs: u32, ctx: &mut CompileCtx, ) -> Result { + // 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); diff --git a/crates/oxide_emit/src/expr/identifier.rs b/crates/oxide_emit/src/expr/identifier.rs index 284df815..fafa6468 100644 --- a/crates/oxide_emit/src/expr/identifier.rs +++ b/crates/oxide_emit/src/expr/identifier.rs @@ -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) { diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index 0f3324e9..26321655 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -466,6 +466,9 @@ impl Emitter { fn emit_identifier_update_static( &self, name: &str, update: &oxide_parser::UpdateExpression, ctx: &mut CompileCtx, ) -> Result { + // 自增/自减先解析赋值引用: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, diff --git a/crates/oxide_emit/src/shared/binding.rs b/crates/oxide_emit/src/shared/binding.rs index 4f1188cd..746c5f87 100644 --- a/crates/oxide_emit/src/shared/binding.rs +++ b/crates/oxide_emit/src/shared/binding.rs @@ -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, diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index 92d0f418..db9f17fb 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -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(()); } diff --git a/crates/oxide_vm/tests/const_guard_tests.rs b/crates/oxide_vm/tests/const_guard_tests.rs index e496d1a7..c9b5d139 100644 --- a/crates/oxide_vm/tests/const_guard_tests.rs +++ b/crates/oxide_vm/tests/const_guard_tests.rs @@ -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}"); +} diff --git a/crates/oxide_vm/tests/scoping_tests.rs b/crates/oxide_vm/tests/scoping_tests.rs index 54218cd0..5be104d3 100644 --- a/crates/oxide_vm/tests/scoping_tests.rs +++ b/crates/oxide_vm/tests/scoping_tests.rs @@ -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); }