From a0c271ef3c8d06cde544ed8d8fdbf3b0602a6be3 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 9 Aug 2026 07:41:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20try=20=E5=86=85=20return=20=E6=B3=84?= =?UTF-8?q?=E6=BC=8F=20TryHandler=EF=BC=8C=E5=BC=82=E5=B8=B8=E8=B7=B3?= =?UTF-8?q?=E5=9B=9E=E6=AD=BB=E5=87=BD=E6=95=B0=20catch=20=E6=AD=BB?= =?UTF-8?q?=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_emit/src/emit.rs | 28 +++++++++ .../oxide_emit/src/stmt/basic/return_stmt.rs | 19 +++++++ .../src/stmt/exception/try_catch.rs | 4 ++ crates/oxide_vm/src/vm_dispatch_ctrl.rs | 28 +++++++++ crates/oxide_vm/src/vm_runtime.rs | 7 +++ .../tests/try_finally_control_flow_tests.rs | 57 +++++++++++++++++++ 6 files changed, 143 insertions(+) diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index 688e4a15..d704395d 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -178,6 +178,10 @@ pub struct CompileCtx { /// with 语句作用域栈:元素为 (with 对象寄存器, 打开时的作用域深度)。 /// 非空时 with 体内的自由标识符需动态解析(先查对象属性,回退外层)。 pub(crate) with_stack: Vec<(u32, usize)>, + /// 打开中(尚未 emit 对应 END)的 try handler 栈,自底向上镜像运行时 + /// try_stack 组成。每项标记是否为纯 catch handler(TRY_BEGIN,由 TRY_END + /// 弹出):return 逃出 try 域时据此弹出栈顶连续纯 catch,防 handler 泄漏。 + pub(crate) open_try_handlers: Vec, } /// 函数体编译上下文:决定 `this`/`super` 绑定与参数前导(prologue)形态。 @@ -259,6 +263,7 @@ impl CompileCtx { captured_bindings: BTreeMap::new(), const_overflow: false, with_stack: Vec::new(), + open_try_handlers: Vec::new(), } } @@ -416,6 +421,29 @@ impl CompileCtx { self.labels.finally_depth -= 1; } + /// 记录一个纯 catch handler 打开(对应 emit try_begin 后的运行时 TRY_BEGIN)。 + pub(crate) fn push_open_catch_handler(&mut self) { + self.open_try_handlers.push(true); + } + + /// 记录一个 finally handler 打开(对应 emit try_finally_begin 后的运行时 + /// TRY_FINALLY_BEGIN)。 + pub(crate) fn push_open_finally_handler(&mut self) { + self.open_try_handlers.push(false); + } + + /// 弹出最近打开的 handler(对应 emit 的 TRY_END / TRY_FINALLY_END)。 + pub(crate) fn pop_open_try_handler(&mut self) { + self.open_try_handlers.pop(); + } + + /// 栈顶连续打开的纯 catch handler 数(遇 finally handler 即停)。 + /// return 逃出本函数时,这些 handler 可由 TRY_END 直接从栈顶弹出; + /// finally 之下的 catch 无法经 TRY_END 弹出,交给运行时统一清理。 + pub(crate) fn top_open_catch_handlers(&self) -> usize { + self.open_try_handlers.iter().rev().take_while(|&&is_catch| is_catch).count() + } + pub(crate) fn push_label_scope( &mut self, name: &str, break_label: LabelId, continue_label: Option, ) -> Result<(), String> { diff --git a/crates/oxide_emit/src/stmt/basic/return_stmt.rs b/crates/oxide_emit/src/stmt/basic/return_stmt.rs index 151d123e..a548c0b8 100644 --- a/crates/oxide_emit/src/stmt/basic/return_stmt.rs +++ b/crates/oxide_emit/src/stmt/basic/return_stmt.rs @@ -14,15 +14,34 @@ impl Emitter { match &ret.argument { Some(expr) => { let r = self.emit_expression(expr, ctx)?; + // return 逃出本函数全部 try 域:值求值后、发 RETURN 前,先从栈顶 + // 弹出连续纯 catch handler(finally handler 走运行时完成穿越逐个 + // 执行)。否则 return 跳过 TRY_END 会让 handler 残留在 try_stack, + // 后续异常 unwind 会跳回已返回函数的 catch 形成死循环。 + self.emit_return_try_cleanup(ctx); ctx.inst(Inst::new(OpCode::RETURN, Operand::Reg(r), Operand::None, Operand::None)); } None => { + self.emit_return_try_cleanup(ctx); ctx.inst(Inst::new(OpCode::RETURN, Operand::None, Operand::None, Operand::None)); } } Ok(None) } + /// 在 RETURN 前弹出栈顶连续打开的纯 catch handler。 + /// + /// # 边界与前提 + /// - 只处理位于栈顶的连续纯 catch handler;finally handler 及其下方的 catch + /// handler 不由 TRY_END 弹出(LIFO 无法跨 finally 弹出),由运行时 + /// dispatch_return 扫描兜底清理。 + fn emit_return_try_cleanup(&self, ctx: &mut CompileCtx) { + let open_catches = ctx.top_open_catch_handlers(); + for _ in 0..open_catches { + ctx.inst(Inst::new(OpCode::TRY_END, Operand::None, Operand::None, Operand::None)); + } + } + pub(crate) fn emit_basic_return(&self, stmt: &Statement, ctx: &mut CompileCtx) -> Result, String> { self.emit_return_statement(stmt, ctx) } diff --git a/crates/oxide_emit/src/stmt/exception/try_catch.rs b/crates/oxide_emit/src/stmt/exception/try_catch.rs index 88854649..215aa7a9 100644 --- a/crates/oxide_emit/src/stmt/exception/try_catch.rs +++ b/crates/oxide_emit/src/stmt/exception/try_catch.rs @@ -22,9 +22,11 @@ impl Emitter { // 逃出语句时须穿越本 finally,词法上记录域深度供跨越计数。 ctx.push_finally_domain(); ctx.inst(Inst::try_finally_begin(finally_label)); + ctx.push_open_finally_handler(); } if has_catch { ctx.inst(Inst::try_begin(catch_label)); + ctx.push_open_catch_handler(); } let mut last_try_result: Option = None; for s in &ts.block.body { @@ -40,6 +42,7 @@ impl Emitter { )); if has_catch { ctx.inst(Inst::new(OpCode::TRY_END, Operand::None, Operand::None, Operand::None)); + ctx.pop_open_try_handler(); } let jmp_needed = has_catch || has_finally; if jmp_needed { @@ -84,6 +87,7 @@ impl Emitter { Operand::None, )); ctx.inst(Inst::new(OpCode::TRY_FINALLY_END, Operand::None, Operand::None, Operand::None)); + ctx.pop_open_try_handler(); ctx.pop_finally_domain(); } ctx.labels.set_label_pos(try_end_label, ctx.insts.len()); diff --git a/crates/oxide_vm/src/vm_dispatch_ctrl.rs b/crates/oxide_vm/src/vm_dispatch_ctrl.rs index 2cd6c1c4..c1f3cc7a 100644 --- a/crates/oxide_vm/src/vm_dispatch_ctrl.rs +++ b/crates/oxide_vm/src/vm_dispatch_ctrl.rs @@ -214,6 +214,12 @@ impl Vm { self.frames.len(), self.frames.last().map(|f| f.return_addr).unwrap_or(0) ); + // return 逃出整个函数:当前帧残留的纯 catch handler 一律弹出(return 不被 + // catch 捕获),finally handler 保留给下方完成穿越逐个执行。即使 emit 侧已 + // 从栈顶弹出连续 catch,这里仍扫描兜底——finally 之下的 catch 无法由 + // TRY_END 直接弹出。防 handler 泄漏到已返回函数,导致后续异常 unwind + // 跳回死函数的 catch 形成死循环。 + self.pop_frame_catch_handlers(); let crossed = self .try_stack .iter() @@ -228,9 +234,31 @@ impl Vm { self.do_return(result) } + /// 弹出当前帧内所有残留的纯 catch handler(无 finally 域),供 return 逃出 + /// 本函数时清理。保留 finally handler(供完成穿越)与其它帧的 handler。 + /// + /// # 边界与前提 + /// - 只处理 `frame_depth == frames.len()` 的 handler:return 只逃出本帧, + /// 调用者的 handler 必须保留。 + /// - 纯 catch 判定为 `finally_pc.is_none()`(不携带 finally 的 TRY_BEGIN)。 + fn pop_frame_catch_handlers(&mut self) { + let depth = self.frames.len(); + let mut kept = Vec::with_capacity(self.try_stack.len()); + for h in self.try_stack.drain(..) { + if h.frame_depth == depth && h.finally_pc.is_none() { + continue; + } + kept.push(h); + } + self.try_stack = kept; + } + /// 实际执行返回:弹出当前帧并交付返回值(供 dispatch_return 与 finally 完成 /// 恢复共用)。 fn do_return(&mut self, result: JsValue) -> Result, String> { + // 兜底:return 完成恢复路径(dispatch_try_finally_end 直达)也可能携带 + // 未清理的纯 catch handler,先弹出再弹帧。 + self.pop_frame_catch_handlers(); if let Some(frame) = self.frames.pop() { self.cell_stack.pop(); let construct_result_reg = frame.construct_result_reg; diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index a1d6ea6d..e0327a4a 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -227,6 +227,13 @@ impl Vm { pub(crate) fn unwind(&mut self) -> Result<(), String> { vm_debug!("unwind: {} try handlers on stack", self.try_stack.len()); while let Some(mut handler) = self.try_stack.pop() { + if handler.frame_depth > self.frames.len() { + // 残留 handler 所属帧已返回(return 逃出泄漏的兜底):丢弃防跳回 + // 死函数的 catch/finally——否则 catch 再抛会反复弹出同一 handler + // 形成死循环。其 for_of_depth 快照属于已死帧,也不可据此关闭 + // 当前帧的迭代器,故整体跳过。 + continue; + } while self.frames.len() > handler.frame_depth { if let Some(frame) = self.frames.pop() { self.cell_stack.pop(); diff --git a/crates/oxide_vm/tests/try_finally_control_flow_tests.rs b/crates/oxide_vm/tests/try_finally_control_flow_tests.rs index 50547097..0f72a69e 100644 --- a/crates/oxide_vm/tests/try_finally_control_flow_tests.rs +++ b/crates/oxide_vm/tests/try_finally_control_flow_tests.rs @@ -189,3 +189,60 @@ fn throw_in_finally_discards_pending_break() { "9" ); } + +#[test] +fn return_in_try_catch_cleans_handler() { + assert_eq!(eval("function f(){ try{ return 1 } catch(e){ return 2 } }; f()"), "1"); +} + +#[test] +fn return_in_try_catch_value_path() { + assert_eq!(eval("function f(){ try{ throw 1 } catch(e){ return e } }; f()"), "1"); +} + +#[test] +fn return_in_try_finally_runs_finally() { + assert_eq!(eval("var fin = 0; function f(){ try{ return 1 } finally{ fin = 1 } } f() * 10 + fin"), "11"); +} + +#[test] +fn finally_throw_overrides_return() { + assert_eq!(eval("function f(){ try{ return 1 } finally { throw 2 } } ; try{ f() }catch(e){ e }"), "2"); +} + +#[test] +fn rethrow_in_leaked_catch_no_longer_hangs() { + // return 穿过 try/catch 后,泄漏的 catch handler 不得在后续异常时被跳入: + // 否则 catch 再 throw → 同一 handler 再弹 → 无限循环。修复后该 handler + // 在 return 时被清理 / unwind 时被跳过,异常交给外层 catch。 + assert_eq!( + eval("var n = 0; function f(){ try { return 1 } catch(e) { throw e; } } f(); try { undefined.x } catch(e){ n = 1; } n"), + "1" + ); +} + +#[test] +fn return_crosses_finally_below_outer_catch() { + // return 穿过 finally(其下还有外层 catch):finally 必须执行,外层 catch + // 不得残留泄漏(该场景 TRY_END 无法从栈顶弹出,靠运行时扫描兜底)。 + assert_eq!( + eval("var fin = 0; function f(){ try { try { return 1 } finally { fin = 1 } } catch(e){ return 2 } } f() * 10 + fin"), + "11" + ); +} + +#[test] +fn return_crosses_outer_finally_above_inner_catch() { + assert_eq!( + eval("var fin = 0; function f(){ try { try { return 1 } catch(e){ return 2 } } finally { fin = 1 } } f() * 10 + fin"), + "11" + ); +} + +#[test] +fn leaked_nested_handler_then_exception_is_caught() { + assert_eq!( + eval("function f(){ try { try { return 1 } finally { } } catch(e){ } } f(); var n = 0; try { undefined.x } catch(e){ n = (e.name === 'TypeError') ? 1 : 2 } n"), + "1" + ); +}