From 6a5ed5c78e9dfdece56be87f15f5c3fe1eb786e8 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 15 Aug 2026 11:41:58 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=90=AB=20try=20=E6=8C=87=E4=BB=A4?= =?UTF-8?q?=E7=9A=84=E5=87=BD=E6=95=B0=E8=B7=B3=E8=BF=87=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E7=BC=96=E7=A0=81=E9=98=B2=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=20handler=20=E4=B8=A2=E5=AD=98=E6=B4=BB=E5=AF=84=E5=AD=98?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 异常边只从 TRY 标记所在 BB 发出,分支/循环 BB 内调用点的 liveness 不含仅 catch/finally 存活的寄存器;按存活上界截断窗口会使 unwind 恢复后 handler 读到 callee 残留垃圾值(可为悬垂对象指针)。含 TRY_BEGIN/TRY_FINALLY_BEGIN 的函数 整体跳过窗口编码(回退全量窗口),恢复基线保守行为,普通函数不受影响。 补分支内调用 + catch 读 try 前变量的 Red/Green 测试与 finally 回归锚点。 --- crates/oxide_regalloc/src/call_window.rs | 36 ++++++++++- .../oxide_vm/tests/call_frame_window_tests.rs | 64 +++++++++++++++++++ 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/crates/oxide_regalloc/src/call_window.rs b/crates/oxide_regalloc/src/call_window.rs index b09240db..fbb66b20 100644 --- a/crates/oxide_regalloc/src/call_window.rs +++ b/crates/oxide_regalloc/src/call_window.rs @@ -4,7 +4,9 @@ //! 物理寄存器最大槽号 + 1(即压帧窗口 `regs[0..上界]`);上界 0 表示未编码 //! (运行时回退到调用方 `active_reg_limit` 全量窗口)。`regs[254]/[255]` //! (this/new.target)由帧单独保存不占窗口,计算时排除。生成器 / 异步函数体 -//! 内部调用点不编码——挂起恢复按全量寄存器快照搬移,保持保守语义。 +//! 内部调用点不编码——挂起恢复按全量寄存器快照搬移,保持保守语义。含 TRY +//! 指令的函数同样不编码——异常 handler 的存活集不经分支/循环内调用点传播, +//! 截断窗口会丢仅 handler 存活的槽(见 `encode_call_window` 内 has_try 说明)。 use oxide_bytecode::opcode::OpCode; use oxide_ir::IRFunction; @@ -21,6 +23,13 @@ pub fn encode_call_window(f: &mut IRFunction, live: &LiveInfo) { if f.insts.is_empty() && f.nested.is_empty() { return; } + // 含 try/catch/finally 的函数跳过编码:异常边只从 TRY 标记所在 BB 发出, + // 分支/循环 BB 内调用点的 liveness 不含仅 catch/finally 存活的寄存器, + // 截断窗口会丢槽(unwind 恢复后 handler 读到 callee 残留)。整体回退全量窗口。 + let has_try = f + .insts + .iter() + .any(|i| matches!(i.op, OpCode::TRY_BEGIN | OpCode::TRY_FINALLY_BEGIN)); // LiveInfo 维度守卫:与 alloc 相同纪律,过期则重算。 let live = if live.inst_live_before.len() == f.insts.len() && !f.insts.is_empty() { live.clone() @@ -28,8 +37,9 @@ pub fn encode_call_window(f: &mut IRFunction, live: &LiveInfo) { let cfg = oxide_cfg::build_cfg(f); oxide_liveness::liveness(f, &cfg) }; - // 生成器 / 异步函数体:调用点保持全量窗口(挂起恢复按全量寄存器快照搬移)。 - if !f.is_generator && !f.is_async { + // 生成器 / 异步函数体 + 含 try 的函数:调用点保持全量窗口(挂起恢复按全量 + // 寄存器快照搬移;异常 handler 存活集经截断窗口会丢值)。 + if !has_try && !f.is_generator && !f.is_async { for (i, inst) in f.insts.iter_mut().enumerate() { if matches!(inst.op, OpCode::CALL | OpCode::NEW_EXPRESSION | OpCode::SUPER_CALL) { let after = &live.inst_live_after[i]; @@ -140,4 +150,24 @@ mod tests { encode_call_window(&mut f, &LiveInfo::new()); assert_eq!(f.insts[0].ext[0], 1 | (5 << 8)); } + + #[test] + fn call_window_skips_function_with_try() { + // 含 TRY_BEGIN 的函数整体跳过编码(全量窗口):异常边只从 TRY 所在 BB 出发, + // 分支/循环内调用点的 liveness 不含仅 handler 存活的寄存器,截断窗口会丢槽。 + let mut f = IRFunction::new(); + f.insts.push(Inst::try_begin(0)); // 0: TRY_BEGIN → L0 + f.insts.push(Inst::call(Operand::Reg(1), Operand::Reg(2), Operand::Reg(3), 1)); + f.insts + .push(Inst::new(OpCode::ADD, Operand::Reg(5), Operand::Reg(4), Operand::Reg(4))); + f.insts + .push(Inst::new(OpCode::RETURN, Operand::Reg(5), Operand::None, Operand::None)); + f.label_pos = vec![Some(2)]; + f.label_count = 1; + let cfg = oxide_cfg::build_cfg(&f); + let live = oxide_liveness::liveness(&f, &cfg); + encode_call_window(&mut f, &live); + // 存活集 {4} 本可编码上界 5,但含 try 保持全量(ext 高 8 位 = 0) + assert_eq!(f.insts[1].ext[0], 1); + } } diff --git a/crates/oxide_vm/tests/call_frame_window_tests.rs b/crates/oxide_vm/tests/call_frame_window_tests.rs index e269f463..2da601fe 100644 --- a/crates/oxide_vm/tests/call_frame_window_tests.rs +++ b/crates/oxide_vm/tests/call_frame_window_tests.rs @@ -218,3 +218,67 @@ fn call_window_upper_bound_encoded_into_bytecode() { module.n_registers ); } + +#[test] +fn call_window_try_branch_call_preserves_catch_live_vars() { + // try 体内分支中的调用点(CALL 与 TRY_BEGIN 不同 BB,异常边不反向传播): + // catch 读 try 前的存活变量。截断窗口若不含这些槽,callee 抛错展开时 + // restore_frame 只回拷窗口,catch 读到 callee 残留垃圾值。 + // boom 的局部变量值(百位档)与 keep 期望值(1..10)刻意错开,覆盖必现错值。 + let src = "function boom(){ \ + var a1=100,a2=200,a3=300,a4=400,a5=500,a6=600,a7=700,a8=800,a9=900,a10=1000, \ + a11=1100,a12=1200,a13=1300,a14=1400,a15=1500,a16=1600,a17=1700,a18=1800,a19=1900,a20=2000, \ + a21=2100,a22=2200,a23=2300,a24=2400,a25=2500; \ + if (a1+a2+a3+a4+a5+a6+a7+a8+a9+a10+a11+a12+a13+a14+a15+a16+a17+a18+a19+a20+a21+a22+a23+a24+a25) throw 'err'; } \ + function caller(x){ \ + var keep1=1,keep2=2,keep3=3,keep4=4,keep5=5,keep6=6,keep7=7,keep8=8,keep9=9,keep10=10; \ + try { if (x) { boom(); } } \ + catch(e) { return keep1+keep2+keep3+keep4+keep5+keep6+keep7+keep8+keep9+keep10; } \ + return -1; } \ + caller(true)"; + assert_eq!(eval(src), "55"); +} + +#[test] +fn call_window_try_branch_call_finally_preserves_live_vars() { + // 回归锚点:finally 沿正常边进入,其活集经正常 CFG 传播到分支内调用点, + // 窗口机制须保持该场景正确(finally 读 try 前变量不丢值)。 + let src = "function boom(){ \ + var a1=100,a2=200,a3=300,a4=400,a5=500,a6=600,a7=700,a8=800,a9=900,a10=1000, \ + a11=1100,a12=1200,a13=1300,a14=1400,a15=1500,a16=1600,a17=1700,a18=1800,a19=1900,a20=2000, \ + a21=2100,a22=2200,a23=2300,a24=2400,a25=2500; \ + return a1+a2+a3+a4+a5+a6+a7+a8+a9+a10+a11+a12+a13+a14+a15+a16+a17+a18+a19+a20+a21+a22+a23+a24+a25; } \ + function caller(x){ \ + var keep = 99; \ + try { if (x) { boom(); } } \ + finally { keep = keep + 1; } \ + return keep; } \ + caller(true)"; + assert_eq!(eval(src), "100"); +} + +#[test] +fn call_window_nested_callee_param_overlap_arguments() { + // 回归锚点:嵌套 callee 调用路径(含 arguments 实参区)在压帧拷贝顺序调整后 + // 值保持正确。真 bug(实参区与形参写入区重叠串值)由 vm 单元测试 + // push_bytecode_frame_param_overlap_reads_spill_first 直接构造验证。 + let src = "function outer(){ \ + var x = 5; \ + function inner(a, b){ return a*10 + b; } \ + var r = inner(x, 7); \ + return r; } \ + outer()"; + assert_eq!(eval(src), "57"); +} + +#[test] +fn call_window_nested_callee_param_overlap_with_arguments_object() { + // 同回归锚点,加 arguments 对象:spill 实参区构建的 arguments 对象值正确。 + let src = "function outer(){ \ + var x = 5; \ + function inner(a, b){ return a*10 + b + arguments[1]; } \ + var r = inner(x, 7); \ + return r; } \ + outer()"; + assert_eq!(eval(src), "64"); +}