From 2c77cb5e9f03526c2b0d93c63c7720d006a1f8aa Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 17 Sep 2026 17:10:02 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9C=AA=E5=88=9D=E5=A7=8B=E5=8C=96=20v?= =?UTF-8?q?ar=20=E5=9C=A8=E8=B5=8B=E5=80=BC=E5=89=8D=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E5=8F=96=E5=AF=84=E5=AD=98=E5=99=A8=E6=AE=8B?= =?UTF-8?q?=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_emit/src/function_body.rs | 35 ++++++ crates/oxide_emit/tests/emit_ir_tests.rs | 36 +++++- .../tests/var_entry_undefined_tests.rs | 117 ++++++++++++++++++ 3 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 crates/oxide_vm/tests/var_entry_undefined_tests.rs diff --git a/crates/oxide_emit/src/function_body.rs b/crates/oxide_emit/src/function_body.rs index e6d5c17e..45f43c05 100644 --- a/crates/oxide_emit/src/function_body.rs +++ b/crates/oxide_emit/src/function_body.rs @@ -427,6 +427,12 @@ impl Emitter { } } + // `var` 绑定入口实例化(非捕获名):预声明只登记槽位、不发射定义指令, + // 声明点对已绑定名又跳过 undefined 写,缺失入口写会让首次写入前的读取 + // 取到调用方遗留的寄存器值。捕获名已由参数 prologue 的 + // MAKE_CELL(undefined) 实例化,此处只补未捕获名。 + self.instantiate_var_bindings(body_stmts, &mut ctx); + // 生成器:body 起点标记——调用时参数初始化(emit_params_prologue)结束后挂起于此, // 参数副作用/异常在 `g()` 调用时刻生效,首次 next() 从这继续执行 body。 if is_generator { @@ -685,6 +691,35 @@ impl Emitter { Ok(param_base) } + /// 非捕获 `var` 名的函数入口实例化:把每个未捕获 var 槽在入口写为 undefined。 + /// + /// # 边界与前提 + /// - 须在 `var` 预声明之后、body 语句发射之前调用:预声明只登记槽位不发定义 + /// 指令,声明语句对已绑定名又跳过 undefined 写,缺失入口写会让首次写入前的 + /// 读取取到调用方遗留的寄存器值。 + /// - 形参与 `arguments` 由参数 prologue 写入实参,跳过以免覆盖。 + /// - 捕获名由参数 prologue 的 MAKE_CELL(undefined) 实例化;STORE_VAR 不更新 + /// cell,跳过以免冗余。 + /// - 符号表查不到的名字(如解构 pattern 叶名未被预声明)跳过,不报错。 + fn instantiate_var_bindings(&self, body_stmts: &[Statement], ctx: &mut CompileCtx) { + let mut names: Vec = collect_var_binding_names(body_stmts) + .into_iter() + .filter(|n| !ctx.param_names.contains(n) && n != "arguments") + .filter(|n| !ctx.captured_bindings.contains_key(n)) + .collect(); + if names.is_empty() { + return; + } + // 按名排序发射:哈希集迭代序随进程变化,排序使字节码确定。 + names.sort(); + let undef_reg = self.emit_undefined(ctx); + for name in names { + if let Some(reg) = ctx.scopes.symbols.lookup_any(&name) { + ctx.inst(Inst::new(OpCode::STORE_VAR, Operand::Reg(reg), Operand::Reg(undef_reg), Operand::Imm(0))); + } + } + } + /// 双 sub-pass emit body:先函数声明(hoisting),再其余语句。返回最后结果寄存器。 fn emit_body_stmts(&self, body_stmts: &[Statement], ctx: &mut CompileCtx) -> Result, String> { let mut last_result_reg = None; diff --git a/crates/oxide_emit/tests/emit_ir_tests.rs b/crates/oxide_emit/tests/emit_ir_tests.rs index 96f69051..8d4b8de1 100644 --- a/crates/oxide_emit/tests/emit_ir_tests.rs +++ b/crates/oxide_emit/tests/emit_ir_tests.rs @@ -238,7 +238,41 @@ fn strict_flag_inherited() { assert!(top.is_strict, "脚本顶层 use strict 应置位"); } -/// 面 9:source_encoded 口径下正则源池键为 marker 形态(与静态口径的明文键不同形)。 +/// 面 9:未捕获 `var` 名在函数入口补发 undefined 定义(预声明只登记槽位, +/// 声明点对已绑定名跳过 undefined 写,入口缺定义会使首次写入前读取取到残留)。 +#[test] +fn var_entry_init_emits_undefined_store() { + let ir = emit_ir("function f() { var x; return typeof x; }"); + let f = &ir.nested[0]; + assert_eq!(count_op(&f.insts, OpCode::STORE_VAR), 1, "未捕获 var 应在函数入口恰写一次 undefined"); +} + +/// 面 10:入口补写的过滤面——形参、`arguments`、捕获名不触发补写, +/// 多个未捕获 var 名各自补一条。 +#[test] +fn var_entry_init_filters_params_arguments_captured() { + let param = emit_ir("function f(a) { var x; return typeof a + typeof x; }"); + assert_eq!(count_op(¶m.nested[0].insts, OpCode::STORE_VAR), 1, "形参名不补写,仅未捕获 var 补写"); + + let arguments = emit_ir("function f() { var x; return arguments.length + typeof x; }"); + assert_eq!( + count_op(&arguments.nested[0].insts, OpCode::STORE_VAR), + 1, + "arguments 名不补写,未捕获 var 补写" + ); + + let captured = emit_ir("function f() { var x; return function g() { return typeof x; }; }"); + assert_eq!( + count_op(&captured.nested[0].insts, OpCode::STORE_VAR), + 0, + "捕获名由入口 MAKE_CELL(undefined) 实例化,不再补 STORE_VAR" + ); + + let multi = emit_ir("function f() { var a, b; return typeof a + typeof b; }"); + assert_eq!(count_op(&multi.nested[0].insts, OpCode::STORE_VAR), 2, "多 var 名各自补一条入口定义"); +} + +/// 面 11:source_encoded 口径下正则源池键为 marker 形态(与静态口径的明文键不同形)。 #[test] fn source_encoded_regex_pool_key() { let src = "var r = /\\ud800/g;"; diff --git a/crates/oxide_vm/tests/var_entry_undefined_tests.rs b/crates/oxide_vm/tests/var_entry_undefined_tests.rs new file mode 100644 index 00000000..e4d9432f --- /dev/null +++ b/crates/oxide_vm/tests/var_entry_undefined_tests.rs @@ -0,0 +1,117 @@ +//! 函数入口 `var` 绑定实例化:未捕获 `var` 槽在首次写入前读取须见 undefined, +//! 不得取到调用方遗留的寄存器值。 +//! +//! 覆盖:赋值前 typeof 读主形、纯声明无后续写、死分支/for 头 var、直接读、 +//! 多 var、赋值后读与带初始化器守卫、嵌套函数自身 var、捕获名守卫、形参与 +//! `arguments` 守卫、`x=5; var x` 声明 no-op 形、多形参移位染色形。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +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: {:?}", e))?; + let module = Compiler::new().compile(&program).map_err(|e| format!("Compile: {}", e))?; + let mut vm = Vm::new(); + vm.run(&Arc::new(module)) +} + +fn truthy(source: &str) { + let result = eval(source).unwrap_or_else(|e| panic!("{source} -> {e}")); + assert!(result.as_bool(), "{source} -> 期望 true,得 {:?}", result); +} + +#[test] +fn var_read_before_assignment_is_undefined() { + // 主形:赋值前 `typeof x` 不得取到调用方残留(残留曾使结果为 'string')。 + truthy("function f(){ var x; var r=typeof x; x=1; return r } f() === 'undefined'"); +} + +#[test] +fn var_typeof_before_any_write_is_undefined() { + // 纯声明形:函数内 var 只声明不写,读者见 undefined。 + truthy("function f(){ var x; return typeof x } f() === 'undefined'"); +} + +#[test] +fn var_direct_read_before_assignment_is_undefined() { + // 直接读(非 typeof)同样见 undefined。 + truthy("function f(){ var x; var r=x; x=1; return r } f() === undefined"); +} + +#[test] +fn var_in_dead_branch_read_is_undefined() { + // 死分支内声明:var 仍提升到函数入口实例化。 + truthy("function f(){ if(false) var x; return typeof x } f() === 'undefined'"); +} + +#[test] +fn var_in_for_head_zero_iterations_read_is_undefined() { + // C-for 头 var 零迭代:入口实例化兜底,无运行期写入也见 undefined。 + truthy("function f(){ for (var x; false;) {} return typeof x } f() === 'undefined'"); +} + +#[test] +fn multi_var_all_undefined() { + // 多 var 名每个槽都实例化(曾一名残留、一名 undefined)。 + truthy("function f(){ var a,b; return typeof a + typeof b } f() === 'undefinedundefined'"); +} + +#[test] +fn var_assignment_before_read_keeps_value() { + // 守卫:赋值后读仍是赋值结果,入口实例化不覆写运行期写入。 + truthy("function f(){ var x; x=1; return typeof x } f() === 'number'"); +} + +#[test] +fn var_initializer_before_read_keeps_value() { + // 守卫:带初始化器的声明在入口实例化之后执行,保留初始化值。 + truthy("function f(){ var x=1; return typeof x } f() === 'number'"); +} + +#[test] +fn nested_function_own_var_is_undefined() { + // 嵌套函数自身的 var 同样在入口实例化。 + truthy("function f(){ function g(){ var y; return typeof y } return g() } f() === 'undefined'"); +} + +#[test] +fn captured_var_stays_undefined() { + // 守卫:被闭包捕获的 var 由 cell 入口实例化,读取仍为 undefined。 + truthy("function f(){ var x; var g=function(){ return typeof x }; return g() } f() === 'undefined'"); +} + +#[test] +fn param_value_not_overwritten_by_entry_init() { + // 守卫:形参名过滤,入口实例化不冲毁实参写入。 + truthy("function f(a){ var x; return a + typeof x } f(5) === '5undefined'"); +} + +#[test] +fn arguments_not_overwritten_by_entry_init() { + // 守卫:arguments 名过滤,入口实例化不冲毁 arguments 对象。 + truthy("function f(){ var x; return arguments.length + typeof x } f(1,2) === '2undefined'"); +} + +#[test] +fn declaration_after_assignment_is_noop() { + // 规范:`var x;` 在既有运行期写入后是空操作,保留值 5。 + truthy("function f(){ x=5; var x; return x } f() === 5"); +} + +#[test] +fn many_params_do_not_change_result() { + // 多形参移位染色:结果与寄存器布局无关,恒为 undefined。 + truthy("function f(a,b,c,d,e,g,h,i){ var x; var r=typeof x; x=1; return r } f() === 'undefined'"); +} + +#[test] +fn captured_and_plain_var_both_undefined() { + // 捕获名与非捕获名并存:两条实例化路径互不干扰。 + truthy( + "function f(){ var x,y; var g=function(){ return x }; g(); return typeof x + typeof y } f() === 'undefinedundefined'", + ); +}