diff --git a/crates/oxide_emit/src/prepass/block_fn.rs b/crates/oxide_emit/src/prepass/block_fn.rs index d604f386..7ecc9077 100644 --- a/crates/oxide_emit/src/prepass/block_fn.rs +++ b/crates/oxide_emit/src/prepass/block_fn.rs @@ -1,5 +1,5 @@ //! emit 前置 pass,块级函数名收集:web-compat 外层 var 绑定实例化支撑—— -//! 语句树内块级函数声明名源序去重收集、词法声明名抑制集(块函数名与词法名 +//! 语句树内块级普通函数声明名源序去重收集、词法声明名抑制集(块函数名与词法名 //! 同名时退化为纯块作用域)、形参名 ∪ 词法名抑制集构建。 use std::collections::HashSet; @@ -9,10 +9,13 @@ use crate::Emitter; use oxide_parser::{Declaration, Statement, VariableDeclarationKind}; impl Emitter { - /// 递归收集语句树内块级函数声明名(源序去重),供 sloppy 模式浏览器兼容行为 - /// (块级函数声明建外层 var 绑定)下的外层 var 绑定实例化。 + /// 递归收集语句树内块级普通函数声明名(源序去重),供 sloppy 模式浏览器兼容 + /// 行为(Annex B.3.3 为块级普通函数声明建外层 var 绑定)下的外层 var 绑定实例化。 /// /// # 边界与前提 + /// - 只收普通 `FunctionDeclaration`:Annex B.3.3 的 web-compat 仅改写普通函数 + /// 声明,async 函数、生成器与 async 生成器是各自独立的块级词法绑定,块外 + /// 不可见,不入名集。 /// - 调用点直接子级(函数体/程序顶层直接子句)是提升 var 声明,非块级 /// 函数,不收集;进入嵌套语句(块/if/循环/switch case/try 各区)后的 /// 函数声明才是块级。 @@ -29,7 +32,7 @@ impl Emitter { for statement in statements { match statement { Statement::FunctionDeclaration(f) => { - if block_level { + if block_level && !f.generator && !f.r#async { if let Some(id) = &f.id { let name = id.name.to_string(); if !names.contains(&name) { diff --git a/crates/oxide_emit/src/stmt/declaration/function_decl.rs b/crates/oxide_emit/src/stmt/declaration/function_decl.rs index dc8863d4..006031db 100644 --- a/crates/oxide_emit/src/stmt/declaration/function_decl.rs +++ b/crates/oxide_emit/src/stmt/declaration/function_decl.rs @@ -49,17 +49,20 @@ impl Emitter { } else { self.materialize_function_declaration(fd, &name, ctx)? }; - // 块内函数声明求值后按三种情形处理: - // - // sloppy 模式下按浏览器/web 实现惯例为块级函数声明建外层 var 绑定并写回: + // sloppy 模式下按 Annex B.3.3 为块级普通函数声明建外层 var 绑定并写回: // 预声明期为该名实例化的函数作用域 var 绑定在此写入块槽位里的函数对象, // 求值一次写回一次(循环体内每迭代覆写头绑定)。 // + // async 函数、生成器与 async 生成器是块级词法绑定,不在 Annex B.3.3 改写 + // 范围,块外不可见,不写回外层 var。 + // // 形参或函数作用域树内同名词法声明(抑制集)不写回:该名被同名声明遮蔽。 // // 顶层不可写内置名不写回:sloppy 下对其赋值(put)永不成功,跳过与执行等价; // 顶层情形另补全局对象属性写(与 for-in 循环头的顶层写同形)。 if !ctx.is_strict + && !fd.generator + && !fd.r#async && ctx.scopes.symbols.scopes.len() > 1 && !ctx.block_fn_suppressed.contains(&name) && !(ctx.is_global_scope && CompileCtx::is_non_writable_global_builtin(&name)) diff --git a/crates/oxide_emit/src/stmt/exception/try_catch.rs b/crates/oxide_emit/src/stmt/exception/try_catch.rs index ce624efe..99c76596 100644 --- a/crates/oxide_emit/src/stmt/exception/try_catch.rs +++ b/crates/oxide_emit/src/stmt/exception/try_catch.rs @@ -1,5 +1,7 @@ //! try/catch/finally 语句 emit:`emit_try_statement` 生成 TRY_BEGIN 与处理入口。 +use std::collections::HashMap; + use crate::{CompileCtx, Emitter}; use oxide_bytecode::opcode::OpCode; use oxide_ir::inst::Inst; @@ -32,13 +34,21 @@ impl Emitter { // try block 是独立块作用域:lexical 声明(let/const/class)限定于 try 内, // 与外层同名绑定互不干扰;块级预声明使声明点前读取编译为 TDZ 抛错。 ctx.push_scope(); + // 函数预声明先于 lexical:同名 let 命中已有函数绑定报重复声明错。 + self.predeclare_block_function_declarations(&ts.block.body, ctx, false); // try 块 lexical 声明是局部绑定:不做受限全局名检查(重复声明错在 emit 期报)。 let _ = self.predeclare_lexical_declarations(&ts.block.body, ctx, false); + // 块级函数声明的块入口初始化:声明点前读命中函数对象,声明点复用入口槽。 + ctx.block_fn_entry_mats.push(HashMap::new()); + for s in &ts.block.body { + self.emit_block_fn_entry_init_stmt(s, ctx)?; + } for s in &ts.block.body { if let Some(r) = self.emit_statement(s, ctx)? { last_try_result = Some(r); } } + ctx.block_fn_entry_mats.pop(); ctx.pop_scope(); ctx.inst(Inst::new( OpCode::LOAD_VAR, @@ -64,14 +74,21 @@ impl Emitter { ctx.inst(Inst::new(OpCode::STORE_VAR, Operand::Reg(src_reg), Operand::None, Operand::None)); self.emit_binding_pattern(¶m.pattern, src_reg, VariableDeclarationKind::Let, false, false, ctx)?; } + // catch 块函数预声明先于 lexical,块入口物化与 try/普通块同口径。 + self.predeclare_block_function_declarations(&catch.body.body, ctx, false); // catch 块 lexical 声明同为局部绑定:不做受限全局名检查。 let _ = self.predeclare_lexical_declarations(&catch.body.body, ctx, false); + ctx.block_fn_entry_mats.push(HashMap::new()); + for s in &catch.body.body { + self.emit_block_fn_entry_init_stmt(s, ctx)?; + } let mut last_catch_result: Option = None; for s in &catch.body.body { if let Some(r) = self.emit_statement(s, ctx)? { last_catch_result = Some(r); } } + ctx.block_fn_entry_mats.pop(); ctx.inst(Inst::new( OpCode::LOAD_VAR, Operand::Reg(result_reg), @@ -91,11 +108,18 @@ impl Emitter { // try/catch 收敛值保持 try 体或 catch 体值,finally 值不渗入。 // finally block 同为独立块作用域,lexical 声明互不泄漏。 ctx.push_scope(); + // finally 块函数预声明先于 lexical,块入口物化与 try/普通块同口径。 + self.predeclare_block_function_declarations(&ts.finalizer.as_ref().unwrap().body, ctx, false); // finally 块 lexical 声明同为局部绑定:不做受限全局名检查。 let _ = self.predeclare_lexical_declarations(&ts.finalizer.as_ref().unwrap().body, ctx, false); + ctx.block_fn_entry_mats.push(HashMap::new()); + for s in &ts.finalizer.as_ref().unwrap().body { + self.emit_block_fn_entry_init_stmt(s, ctx)?; + } for s in &ts.finalizer.as_ref().unwrap().body { self.emit_statement(s, ctx)?; } + ctx.block_fn_entry_mats.pop(); ctx.pop_scope(); ctx.inst(Inst::new(OpCode::TRY_FINALLY_END, Operand::None, Operand::None, Operand::None)); ctx.pop_open_try_handler(); diff --git a/crates/oxide_vm/tests/block_fn_async_gen_tests.rs b/crates/oxide_vm/tests/block_fn_async_gen_tests.rs new file mode 100644 index 00000000..203bb03f --- /dev/null +++ b/crates/oxide_vm/tests/block_fn_async_gen_tests.rs @@ -0,0 +1,145 @@ +//! 块级 async/生成器函数声明不泄漏:Annex B.3.3 的 web-compat 外层 var 绑定 +//! 仅覆盖普通函数声明。async 函数、生成器与 async 生成器是块级词法绑定, +//! 块外不可见,也不覆写同名外层 var;块内读取仍命中块绑定。 +//! +//! 覆盖脚本顶层块、函数体内块、直接/间接 eval、嵌套块、switch case、try 块, +//! 以及同名 var 碰撞与普通函数泄漏正例守卫。 + +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 assert_true(source: &str) { + let result = eval(source).unwrap(); + assert!(result.as_bool(), "期望 true,源: {source},得 {result:?}"); +} + +#[test] +fn block_async_fn_decl_not_leaked_in_script() { + // 脚本顶层块:块级 async 函数声明不建外层绑定,块外 typeof 为 undefined。 + assert_true("{async function a(){}} typeof a === 'undefined'"); +} + +#[test] +fn block_generator_decl_not_leaked_in_script() { + // 生成器声明同为块级词法绑定,不泄漏。 + assert_true("{function* g(){}} typeof g === 'undefined'"); +} + +#[test] +fn block_async_generator_decl_not_leaked_in_script() { + // async 生成器声明不泄漏。 + assert_true("{async function* ag(){}} typeof ag === 'undefined'"); +} + +#[test] +fn block_async_fn_decl_not_reflected_on_global() { + // 不泄漏即不建全局对象属性:globalThis 反射不到块级 async 名。 + assert_true("{async function a(){}} !('a' in globalThis)"); +} + +#[test] +fn block_async_fn_decl_not_leaked_in_function_body() { + // 函数体内块与脚本面同口径:块外读为 undefined。 + assert_true("function q(){ {async function a(){}} return typeof a === 'undefined' } q()"); +} + +#[test] +fn block_generator_decl_not_leaked_in_function_body() { + // 函数体内块生成器同口径。 + assert_true("function q(){ {function* g(){}} return typeof g === 'undefined' } q()"); +} + +#[test] +fn block_async_fn_decl_not_leaked_in_direct_eval() { + // 直接 eval 顶层块:不泄漏到 eval 的 var 环境。 + assert_true(r#"function q(){ eval('{async function a(){}}'); return typeof a === 'undefined' } q()"#); +} + +#[test] +fn block_generator_decl_not_leaked_in_indirect_eval() { + // 间接 eval 顶层块:生成器名不落全局。 + assert_true(r#"(0,eval)('{function* g(){}}'); typeof g === 'undefined'"#); +} + +#[test] +fn nested_block_async_fn_decl_not_leaked() { + // 嵌套块:任一深度都不泄漏。 + assert_true("{{async function a(){}}} typeof a === 'undefined'"); +} + +#[test] +fn switch_case_async_fn_decl_not_leaked() { + // switch CaseBlock 内 async 声明不泄漏到 switch 外。 + assert_true("switch(0){case 0:async function a(){}} typeof a === 'undefined'"); +} + +#[test] +fn try_block_async_fn_decl_not_leaked() { + // try 块内 async 声明不泄漏到语句外,且块内可读。 + assert_true("try{async function a(){}}catch(e){} typeof a === 'undefined'"); +} + +#[test] +fn try_block_function_decl_entry_visible() { + // try 块作为独立块作用域:直接子函数声明的块入口物化,声明点前读可用。 + assert_true("var t; try{ t = typeof f; function f(){} }catch(e){} t === 'function'"); +} + +#[test] +fn block_async_fn_var_collision_keeps_var() { + // 同名外层 var 不被块级 async 声明覆写。 + assert_true("var a=1; {async function a(){}} typeof a === 'number'"); +} + +#[test] +fn block_async_fn_var_collision_in_function_body() { + // 函数体内同名 var 碰撞:块级 async 声明不写回外层 var,仍为 undefined。 + assert_true("(function q(){ {async function a(){}} var a; return typeof a === 'undefined' })()"); +} + +#[test] +fn block_generator_var_collision_keeps_var() { + // 生成器同名 var 碰撞同口径。 + assert_true("{function* g(){}} var g; typeof g === 'undefined'"); +} + +#[test] +fn block_async_fn_readable_inside_block() { + // 块内读取仍命中块级绑定(声明点后为 function)。 + assert_true("(function(){ {async function a(){}; return typeof a === 'function'} })()"); +} + +#[test] +fn plain_function_block_still_leaks() { + // 正例守卫:普通块级函数声明仍按 Annex B.3.3 泄漏外层。 + assert_true("{function f(){}} typeof f === 'function'"); +} + +#[test] +fn plain_function_var_collision_still_overwritten() { + // 正例守卫:普通块级函数声明求值写回同名外层 var。 + assert_true("var f=1; {function f(){}} typeof f === 'function'"); +} + +#[test] +fn strict_block_async_fn_not_leaked() { + // strict 面不建外层绑定,块级 async 声明不泄漏。 + assert_true("'use strict'; {async function a(){}} typeof a === 'undefined'"); +} + +#[test] +fn top_level_async_fn_still_global() { + // 守卫:脚本顶层 async 函数声明(非块级)仍建全局绑定并可反射。 + assert_true("async function top(){} typeof top === 'function' && ('top' in globalThis)"); +}