diff --git a/crates/oxide_emit/src/function_body.rs b/crates/oxide_emit/src/function_body.rs index 06deb6cd..87ede52c 100644 --- a/crates/oxide_emit/src/function_body.rs +++ b/crates/oxide_emit/src/function_body.rs @@ -431,9 +431,9 @@ impl Emitter { } } - // `var` 绑定入口实例化(非捕获名):预声明只登记槽位、不发射定义指令, - // 声明点对已绑定名又跳过 undefined 写,缺失入口写会让首次写入前的读取 - // 取到调用方遗留的寄存器值。捕获名已由参数 prologue 的 + // `var` 与块级函数外层 var 绑定入口实例化(非捕获名):预声明只登记槽位、 + // 不发射定义指令,声明点对已绑定名又跳过 undefined 写,缺失入口写会让首次 + // 写入前的读取取到调用方遗留的寄存器值。捕获名已由参数 prologue 的 // MAKE_CELL(undefined) 实例化,此处只补未捕获名。 self.instantiate_var_bindings(body_stmts, &mut ctx); @@ -772,18 +772,40 @@ impl Emitter { Ok(param_base) } - /// 非捕获 `var` 名的函数入口实例化:把每个未捕获 var 槽在入口写为 undefined。 + /// 非捕获 `var` 名与块级函数外层 var 名的函数入口实例化:把每个未捕获槽在入口 + /// 写为 undefined。 /// /// # 边界与前提 - /// - 须在 `var` 预声明之后、body 语句发射之前调用:预声明只登记槽位不发定义 - /// 指令,声明语句对已绑定名又跳过 undefined 写,缺失入口写会让首次写入前的 - /// 读取取到调用方遗留的寄存器值。 + /// - 须在 `var` 预声明与块级函数外层 var 预声明之后、body 语句发射之前调用: + /// 预声明只登记槽位不发定义指令,声明语句对已绑定名又跳过 undefined 写,缺失 + /// 入口写会让首次写入前的读取取到调用方遗留的寄存器值。 /// - 形参与 `arguments` 由参数 prologue 写入实参,跳过以免覆盖。 /// - 捕获名由参数 prologue 的 MAKE_CELL(undefined) 实例化;STORE_VAR 不更新 /// cell,跳过以免冗余。 + /// - 块级函数名仅在 sloppy 模式并入:strict 下块函数是块级词法绑定,不建外层 + /// var;被形参/词法同名抑制的名字无外层绑定,不并入。直接子标签函数声明由 + /// 函数入口单独物化闭包,不并入。 /// - 符号表查不到的名字(如解构 pattern 叶名未被预声明)跳过,不报错。 fn instantiate_var_bindings(&self, body_stmts: &[Statement], ctx: &mut CompileCtx) { - let mut names: Vec = collect_var_binding_names(body_stmts) + let mut name_set = collect_var_binding_names(body_stmts); + + // 块级函数外层 var 绑定同样只预声明不发射定义指令,缺失入口写会让块前读取 + // 取到调用方残留;入口写须并入(strict 与抑制名除外)。 + if !ctx.is_strict { + let labeled_hoisted: HashSet<&str> = body_stmts + .iter() + .filter_map(|stmt| Self::labeled_function_decl(stmt)) + .filter_map(|fd| fd.id.as_ref()) + .map(|id| id.name.as_str()) + .collect(); + for name in self.collect_block_function_names(body_stmts) { + if !ctx.block_fn_suppressed.contains(&name) && !labeled_hoisted.contains(name.as_str()) { + name_set.insert(name); + } + } + } + + let mut names: Vec = name_set .into_iter() .filter(|n| !ctx.param_names.contains(n) && n != "arguments") .filter(|n| !ctx.captured_bindings.contains_key(n)) diff --git a/crates/oxide_vm/tests/block_fn_outer_var_entry_tests.rs b/crates/oxide_vm/tests/block_fn_outer_var_entry_tests.rs new file mode 100644 index 00000000..e3d61c7b --- /dev/null +++ b/crates/oxide_vm/tests/block_fn_outer_var_entry_tests.rs @@ -0,0 +1,75 @@ +//! 块级函数外层 `var` 绑定的函数入口实例化:sloppy 下块内函数声明名在求值前 +//! 读取须见 undefined,不得取到调用方遗留的寄存器值。 +//! +//! 覆盖:显式块主形(块前读/块后读/调用)、if 支臂形、显式块内 if 支臂形、 +//! 嵌套块形、strict 不泄漏守卫、裸读形、形参同名抑制守卫、直接子标签函数 +//! 声明守卫。 + +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 block_fn_before_read_is_undefined_and_after_is_function() { + // 主形:块前读外层 var 绑定见 undefined,块后读与调用见函数对象。 + truthy("function t(){ var r = typeof g; { function g(){return 7} } return r === 'undefined' && typeof g === 'function' && g() === 7 } t()"); +} + +#[test] +fn block_fn_if_arm_before_read_is_undefined() { + // if 支臂形(无显式块):支臂声明名同样在入口实例化为 undefined。 + truthy("function t(){ var r = typeof g; if(true) function g(){return 1}; return r === 'undefined' && typeof g === 'function' } t()"); +} + +#[test] +fn block_fn_explicit_block_if_arm_before_read_is_undefined() { + // 显式块内 if 支臂形:块前读见 undefined,不依赖块 Let 巧合。 + truthy("function t(){ var r = typeof g; { if(true) function g(){return 1}; return r === 'undefined' } } t()"); +} + +#[test] +fn block_fn_nested_block_before_read_is_undefined() { + // 嵌套块形:内层块函数名仍提升到函数作用域并在入口实例化。 + truthy("function t(){ var r = typeof g; { { function g(){} } } return r === 'undefined' && typeof g === 'function' } t()"); +} + +#[test] +fn block_fn_bare_read_before_block_is_undefined() { + // 裸读形(非 typeof):入口实例化对裸读同样生效。 + truthy("function t(){ var a = g; { function g(){} } return a === undefined } t()"); +} + +#[test] +fn block_fn_strict_stays_block_scoped() { + // strict 守卫:块函数是块级词法绑定,不建外层 var,块外读恒 undefined。 + truthy("function t(){ 'use strict'; var r = typeof g; { function g(){} } return r === 'undefined' && typeof g === 'undefined' } t()"); +} + +#[test] +fn block_fn_param_suppression_keeps_param_value() { + // 形参同名抑制守卫:块函数不建外层 var,形参实参值不被入口写冲毁。 + truthy( + "function t(g){ var r = typeof g; { function g(){} } return r === 'number' && typeof g === 'number' } t(42)", + ); +} + +#[test] +fn block_fn_direct_labeled_decl_before_read_stays_function() { + // 直接子标签函数声明守卫:入口物化闭包,声明点前读仍是函数对象。 + truthy("function t(){ var r = typeof g; l: function g(){return 1}; return r === 'function' && typeof g === 'function' } t()"); +} diff --git a/crates/oxide_vm/tests/var_entry_undefined_tests.rs b/crates/oxide_vm/tests/var_entry_undefined_tests.rs index e4d9432f..9d04759e 100644 --- a/crates/oxide_vm/tests/var_entry_undefined_tests.rs +++ b/crates/oxide_vm/tests/var_entry_undefined_tests.rs @@ -54,6 +54,18 @@ fn var_in_for_head_zero_iterations_read_is_undefined() { truthy("function f(){ for (var x; false;) {} return typeof x } f() === 'undefined'"); } +#[test] +fn var_in_for_in_head_zero_iterations_read_is_undefined() { + // for-in 头 var 零迭代(空可迭代):入口实例化是唯一到达定义,读见 undefined。 + truthy("function f(){ for (var x in {}) {} return typeof x } f() === 'undefined'"); +} + +#[test] +fn var_in_for_of_head_zero_iterations_read_is_undefined() { + // for-of 头 var 零迭代(空可迭代):入口实例化是唯一到达定义,读见 undefined。 + truthy("function f(){ for (var x of []) {} return typeof x } f() === 'undefined'"); +} + #[test] fn multi_var_all_undefined() { // 多 var 名每个槽都实例化(曾一名残留、一名 undefined)。