From 2b0238f310aeaf8a2753db5d9d5597751c84840d Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 20 Sep 2026 04:46:41 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20catch=20=E5=8F=82=E6=95=B0=E5=90=8D?= =?UTF-8?q?=E8=A1=A5=E5=85=A5=E6=8D=95=E8=8E=B7=E6=89=AB=E6=8F=8F=E4=B8=94?= =?UTF-8?q?=20catch=20=E5=85=A5=E5=8F=A3=E6=96=B0=20cell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_emit/src/capture/names.rs | 5 + .../src/stmt/exception/try_catch.rs | 5 +- .../tests/catch_param_capture_tests.rs | 251 ++++++++++++++++++ 3 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 crates/oxide_vm/tests/catch_param_capture_tests.rs diff --git a/crates/oxide_emit/src/capture/names.rs b/crates/oxide_emit/src/capture/names.rs index 4ffc2760..b05287cf 100644 --- a/crates/oxide_emit/src/capture/names.rs +++ b/crates/oxide_emit/src/capture/names.rs @@ -251,6 +251,11 @@ pub(crate) fn collect_decl_names_stmt(stmts: &[Statement], out: &mut HashSet { collect_decl_names_stmt(&ts.block.body, out); if let Some(h) = &ts.handler { + // catch 参数是本函数作用域的词法绑定:计入 own_bindings, + // 嵌套函数引用它时捕获分析才会建 cell 供 upvalue 读取。 + if let Some(param) = &h.param { + collect_binding_pattern_names(¶m.pattern, out); + } collect_decl_names_stmt(&h.body.body, out); } if let Some(f) = &ts.finalizer { diff --git a/crates/oxide_emit/src/stmt/exception/try_catch.rs b/crates/oxide_emit/src/stmt/exception/try_catch.rs index 99c76596..25bba364 100644 --- a/crates/oxide_emit/src/stmt/exception/try_catch.rs +++ b/crates/oxide_emit/src/stmt/exception/try_catch.rs @@ -72,7 +72,10 @@ impl Emitter { let src_reg = ctx.alloc_reg(); // 异常值在 VM 物理 regs[0](unwind 展开处写入),STORE_VAR a=None 读回。 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 执行都为参数新建词法环境:被捕获参数在入口发 + // MAKE_CELL_FRESH(替换而非覆写上一入口的 cell),上一入口 + // 闭包持有的旧 cell 指针仍可读到旧值。 + self.emit_binding_pattern(¶m.pattern, src_reg, VariableDeclarationKind::Let, false, true, ctx)?; } // catch 块函数预声明先于 lexical,块入口物化与 try/普通块同口径。 self.predeclare_block_function_declarations(&catch.body.body, ctx, false); diff --git a/crates/oxide_vm/tests/catch_param_capture_tests.rs b/crates/oxide_vm/tests/catch_param_capture_tests.rs new file mode 100644 index 00000000..698fe8f7 --- /dev/null +++ b/crates/oxide_vm/tests/catch_param_capture_tests.rs @@ -0,0 +1,251 @@ +//! catch 参数捕获与每入口新 cell 形态锁:catch 参数绑定名(含解构模式全名)计入 +//! 本函数 own 集,被嵌套闭包捕获时 catch 入口发 MAKE_CELL_FRESH——每次 catch 执行 +//! 为参数新建词法环境(替换上一入口的 cell 而非原地覆写),子模块经 upvalue 读; +//! try 体/finally 体闭包对参数的域外引用保持运行期 ReferenceError。 +//! +//! 完成值一律收敛为布尔(JsValue 的 Display 只暴露 number/bool,不暴露字符串内容), +//! 字符串比较在引擎内完成。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> String { + let allocator = Allocator::default(); + let program = match oxide_parser::parse(&allocator, source) { + Ok(p) => p, + Err(e) => return format!("parse error: {}", e[0].message), + }; + let module = match Compiler::new().compile(&program) { + Ok(m) => m, + Err(e) => return format!("compile error: {e}"), + }; + let mut vm = Vm::new(); + match vm.run(&Arc::new(module)) { + Ok(result) => format!("{result}"), + Err(e) => format!("vm error: {e}"), + } +} + +/// 断言运行期抛出未捕获 ReferenceError(域外引用守卫的期望形态)。 +fn assert_uncaught_reference_error(source: &str, msg: &str) { + let res = eval(source); + assert!( + res.starts_with("vm error") && res.contains("ReferenceError"), + "{msg}: expected an uncaught ReferenceError, got: {res}" + ); +} + +// ── 闭包捕获参数:直引 / 模式名 / 同名遮蔽 / 嵌套函数形 / 顶层 / 写穿 / 跨帧 ── + +#[test] +fn catch_param_closure_direct_ref_reads_thrown_value() { + // 主形态:catch 体闭包直引参数,读值为抛入的异常值。 + assert_eq!( + eval("(() => { let f; try { throw 1; } catch (e) { f = () => e; } return f(); })()"), + "1", + "closure in catch body must read the thrown value through the parameter cell" + ); +} + +#[test] +fn catch_param_destructured_name_captured_by_closure() { + // 解构参数模式内的绑定名与裸参数名同口径:闭包捕获读属性值。 + assert_eq!( + eval("(() => { let f; try { throw { message: 7 }; } catch ({ message: e }) { f = () => e; } return f(); })()"), + "7", + "destructuring catch parameter names are captured bindings too" + ); +} + +#[test] +fn catch_param_shadows_outer_var_closure_reads_param() { + // 外层 var 同名遮蔽:catch 体内的闭包捕获参数(内层绑定),读抛入值而非外层 var。 + assert_eq!( + eval("(() => { var e = 99; try { throw 5; } catch (e) { const f = () => e; return f(); } })()"), + "5", + "closure captures the catch parameter, not the same-named outer var" + ); +} + +#[test] +fn catch_param_shadows_function_param_closure_reads_param() { + // 外层函数形参同名遮蔽:函数作用域内 catch 参数遮蔽形参,闭包读参数。 + assert_eq!( + eval("(function(e) { try { throw 3; } catch (e) { const f = () => e; return f(); } })(88)"), + "3", + "closure captures the catch parameter, not the same-named outer function param" + ); +} + +#[test] +fn catch_param_function_decl_inside_catch_reads_param() { + // 函数声明形(非箭头):catch 体内声明的函数引用参数,读抛入值。 + assert_eq!( + eval("(() => { let f; try { throw 6; } catch (e) { function g() { return e; } f = g; } return f(); })()"), + "6", + "function declared in catch body reads the captured parameter" + ); +} + +#[test] +fn catch_param_class_method_inside_catch_reads_param() { + // 类方法形:catch 体内类表达式的方法引用参数,读抛入值。 + assert_eq!( + eval( + "(() => { let C; try { throw 8; } catch (e) { C = class { m() { return e; } }; } return new C().m(); })()" + ), + "8", + "class method defined in catch body reads the captured parameter" + ); +} + +#[test] +fn catch_param_top_level_closure_reads_param() { + // 脚本顶层 catch 参数被顶层闭包捕获:顶层 cell 层同样承载参数 cell。 + assert_eq!( + eval("let f; try { throw 11; } catch (e) { f = () => e; } f()"), + "11", + "top-level catch parameter is capturable by top-level closures" + ); +} + +#[test] +fn catch_param_reassign_then_closure_reads_new_value() { + // 写穿面:catch 体内重赋参数后闭包读新值(赋值须写穿共享单元)。 + assert_eq!( + eval("(() => { let f; try { throw 1; } catch (e) { e = 99; f = () => e; } return f(); })()"), + "99", + "assignment to the catch parameter writes through the captured cell" + ); +} + +#[test] +fn catch_param_recursive_frames_each_frame_isolated() { + // 跨帧隔离:同名参数各调用帧独立 cell,闭包读各自帧的抛入值。 + assert_eq!( + eval( + "(() => { function g(n) { try { throw n; } catch (e) { return () => e; } } \ + let fs = [g(1), g(2)]; return fs.map(f => f()).join('|') === '1|2'; })()" + ), + "true", + "each call frame builds its own parameter cell for the closure" + ); +} + +#[test] +fn catch_param_default_value_inner_fn_reads_param() { + // 参数默认值时序:模式默认值内嵌套函数引用参数,绑定先于默认值求值。 + assert_eq!( + eval("(() => { let f; try { throw 4; } catch (e) { const g = (x = () => e) => x; f = g(); } return f(); })()"), + "4", + "nested function in a parameter default reads the catch parameter" + ); +} + +// ── 每 catch 入口新 cell(替换而非覆写)── + +#[test] +fn catch_param_loop_reentry_each_iteration_new_cell() { + // 循环重入:每迭代 catch 入口新 cell,各迭代闭包读各自迭代值。 + assert_eq!( + eval( + "(() => { let r = []; for (let i = 0; i < 2; i++) { try { throw i; } catch (e) { r.push(() => e); } } \ + return r.map(f => f()).join('|') === '0|1'; })()" + ), + "true", + "each loop iteration's catch entry builds a fresh parameter cell" + ); +} + +#[test] +fn catch_param_consecutive_try_catch_each_entry_new_cell() { + // 无循环连续两个 try/catch 同名参数:第二个入口新 cell 不覆写第一个入口闭包 + // 已持的 cell 指针(门控若按循环在飞判定,本形态读错值)。 + assert_eq!( + eval( + "(() => { let r = []; try { throw 1; } catch (e) { r.push(() => e); } \ + try { throw 2; } catch (e) { r.push(() => e); } \ + return r.map(f => f()).join('|') === '1|2'; })()" + ), + "true", + "consecutive catch entries with the same parameter name build fresh cells" + ); +} + +#[test] +fn catch_param_nested_catch_outer_closure_keeps_old_cell() { + // 同名嵌套 catch:内层入口新 cell 替换槽位,外层闭包仍持旧 cell 指针读旧值。 + assert_eq!( + eval( + "(() => { let outer; try { throw 1; } catch (e) { outer = () => e; \ + try { throw 2; } catch (e) { } } return outer(); })()" + ), + "1", + "inner catch entry must not clobber the outer closure's cell" + ); +} + +// ── 守卫:既有直读/遮蔽/B022 模式面语义保真 ── + +#[test] +fn catch_param_direct_read_in_body() { + // 无闭包直读参数(最简形态):直读保真。 + assert_eq!( + eval("try { throw 42; } catch (e) { e }"), + "42", + "direct read of the catch parameter in its own body" + ); +} + +#[test] +fn catch_param_shadows_outer_var_direct_read() { + // var 同名遮蔽直读(无闭包):catch 体内直读参数值。 + assert_eq!( + eval("(() => { var e = 99; try { throw 5; } catch (e) { return e; } })()"), + "5", + "direct read in catch body sees the parameter, not the outer var" + ); +} + +#[test] +fn catch_param_destructured_direct_read() { + // 解构参数直读(无闭包):模式绑定求值后直读求和。 + assert_eq!( + eval("(() => { try { throw [1, 2]; } catch ([a, b]) { return a + b; } })()"), + "3", + "destructuring catch parameter direct read" + ); +} + +#[test] +fn catch_param_pattern_default_value_evaluated() { + // 模式默认值面:抛入值非 undefined 时默认值不求值,直读抛入元素。 + assert_eq!( + eval("(() => { const g = () => 55; try { throw [1]; } catch ([x = g()]) { return x; } })()"), + "1", + "pattern default value is not evaluated when the thrown element is present" + ); +} + +#[test] +fn catch_param_ref_from_try_body_stays_reference_error() { + // 域外引用:try 体闭包引用 catch 参数——参数在 try 体域内未声明, + // 修复后仍须运行期 ReferenceError(与 node 一致)。 + assert_uncaught_reference_error( + "(() => { let f; try { f = () => e; throw 1; } catch (e) {} return f(); })()", + "closure in try body referencing the catch parameter must stay unresolvable", + ); +} + +#[test] +fn catch_param_ref_from_finally_body_stays_reference_error() { + // 域外引用:finally 体闭包引用 catch 参数——参数作用域不含 finally 体, + // 修复后仍须运行期 ReferenceError(与 node 一致)。 + assert_uncaught_reference_error( + "(() => { let f; try { throw 1; } catch (e) {} finally { f = () => e; } return f(); })()", + "closure in finally body referencing the catch parameter must stay unresolvable", + ); +}