From cd214069cd0d671845b89e1d7ca9432d4892bab3 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 8 Aug 2026 12:38:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20try/finally=20=E6=94=AF=E6=8C=81=20break?= =?UTF-8?q?/continue/return=20=E7=A9=BF=E8=B6=8A=20finally=EF=BC=88complet?= =?UTF-8?q?ion=20=E6=9C=BA=E5=88=B6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_bytecode/src/opcode.rs | 4 +- crates/oxide_cfg/src/lib.rs | 2 + crates/oxide_cfg/src/partition.rs | 2 +- crates/oxide_cfg/src/split.rs | 2 +- crates/oxide_emit/src/emit.rs | 26 ++- crates/oxide_emit/src/emit_ctx.rs | 8 +- .../src/stmt/basic/break_continue.rs | 45 +++-- .../src/stmt/exception/try_catch.rs | 4 + crates/oxide_ir/src/contract.rs | 4 + crates/oxide_ir/src/inst.rs | 12 ++ crates/oxide_ir/src/lower.rs | 3 + crates/oxide_regalloc/src/rewrite.rs | 2 + crates/oxide_vm/src/dispatch/control_flow.rs | 10 +- crates/oxide_vm/src/vm.rs | 66 +++++- crates/oxide_vm/src/vm_dispatch_ctrl.rs | 167 ++++++++++++++- crates/oxide_vm/src/vm_runtime.rs | 17 +- crates/oxide_vm/src/vm_support.rs | 3 + .../tests/try_finally_control_flow_tests.rs | 191 ++++++++++++++++++ 18 files changed, 529 insertions(+), 39 deletions(-) create mode 100644 crates/oxide_vm/tests/try_finally_control_flow_tests.rs diff --git a/crates/oxide_bytecode/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs index b9fcf3a2..c067bb41 100644 --- a/crates/oxide_bytecode/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -82,7 +82,9 @@ define_opcodes! { STRICT_NEQ = 0x1C => "STRICT_NEQ", UNARY_PLUS = 0x1D => "UNARY_PLUS", - // ── 控制流 (0x20-0x2F) ── + // ── 控制流 (0x1E-0x2F) ── + BREAK = 0x1E => "BREAK", + CONTINUE = 0x1F => "CONTINUE", JMP = 0x20 => "JMP", JMP_IF_FALSE = 0x21 => "JMP_IF_FALSE", JMP_IF_TRUE = 0x22 => "JMP_IF_TRUE", diff --git a/crates/oxide_cfg/src/lib.rs b/crates/oxide_cfg/src/lib.rs index 7dfb5ebc..12d130ba 100644 --- a/crates/oxide_cfg/src/lib.rs +++ b/crates/oxide_cfg/src/lib.rs @@ -71,6 +71,8 @@ fn is_terminator(op: OpCode) -> bool { matches!( op, OpCode::JMP + | OpCode::BREAK + | OpCode::CONTINUE | OpCode::JMP_IF_TRUE | OpCode::JMP_IF_FALSE | OpCode::JMP_IF_NULLISH diff --git a/crates/oxide_cfg/src/partition.rs b/crates/oxide_cfg/src/partition.rs index 1fef11c1..8840f01a 100644 --- a/crates/oxide_cfg/src/partition.rs +++ b/crates/oxide_cfg/src/partition.rs @@ -25,7 +25,7 @@ pub(super) fn partition_blocks(f: &IRFunction) -> Vec { // 跳转的 fallthrough 后继是块头:条件跳转否则 cond+then 融块、条件边丢失; // 无条件 JMP 同样必须切块头——JMP 不 fallthrough,但其后继位置需成块边界, // 否则 JMP 与后续指令融块、块尾判定错、Jump 边丢失(DCE 可达性误删目标块)。 - if matches!(inst.op, OpCode::JMP | OpCode::JMP_IF_TRUE | OpCode::JMP_IF_FALSE | OpCode::JMP_IF_NULLISH) { + if matches!(inst.op, OpCode::JMP | OpCode::BREAK | OpCode::CONTINUE | OpCode::JMP_IF_TRUE | OpCode::JMP_IF_FALSE | OpCode::JMP_IF_NULLISH) { heads[i + 1] = true; } } diff --git a/crates/oxide_cfg/src/split.rs b/crates/oxide_cfg/src/split.rs index 0026ea68..db76d936 100644 --- a/crates/oxide_cfg/src/split.rs +++ b/crates/oxide_cfg/src/split.rs @@ -37,7 +37,7 @@ pub(super) fn split_and_edges(f: &IRFunction, heads: &[bool]) -> (Vec { + OpCode::JMP | OpCode::BREAK | OpCode::CONTINUE => { if let Operand::Label(l) = last.b { match f.label_pos.get(l as usize).and_then(|p| *p) { Some(p) => { diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index 3fe16379..1a5fec97 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -143,6 +143,8 @@ pub struct LabelScope { pub(crate) name: String, pub(crate) break_label: LabelId, pub(crate) continue_label: Option, + /// 标签打开时嵌套的 finally 域数:break/continue 跨越 finally 的计数依据。 + pub(crate) finally_depth_at_open: usize, } /// 单函数编译上下文:执行流 + 作用域 + 闭包捕获的聚合状态。 @@ -233,6 +235,7 @@ impl CompileCtx { switch_stack: Vec::new(), label_scopes: Vec::new(), pending_loop_labels: Vec::new(), + finally_depth: 0, label_counter: 0, }, scopes: ScopeCtx { @@ -375,39 +378,52 @@ impl CompileCtx { } pub(crate) fn push_loop(&mut self, break_label: LabelId, continue_label: LabelId) { - self.labels.loop_stack.push((break_label, continue_label)); + let fd = self.labels.finally_depth; + self.labels.loop_stack.push((break_label, continue_label, fd)); } pub(crate) fn pop_loop(&mut self) { self.labels.loop_stack.pop(); } - pub(crate) fn current_loop(&self) -> Option<&(LabelId, LabelId)> { + pub(crate) fn current_loop(&self) -> Option<&(LabelId, LabelId, usize)> { self.labels.loop_stack.last() } pub(crate) fn push_switch(&mut self, break_label: LabelId) { - self.labels.switch_stack.push(break_label); + let fd = self.labels.finally_depth; + self.labels.switch_stack.push((break_label, fd)); } pub(crate) fn pop_switch(&mut self) { self.labels.switch_stack.pop(); } - pub(crate) fn current_switch(&self) -> Option<&LabelId> { + pub(crate) fn current_switch(&self) -> Option<&(LabelId, usize)> { self.labels.switch_stack.last() } + /// 进入/离开一个 try/finally 域:break/continue 跨越 finally 计数用。 + pub(crate) fn push_finally_domain(&mut self) { + self.labels.finally_depth += 1; + } + + pub(crate) fn pop_finally_domain(&mut self) { + self.labels.finally_depth -= 1; + } + pub(crate) fn push_label_scope( &mut self, name: &str, break_label: LabelId, continue_label: Option, ) -> Result<(), String> { if self.labels.label_scopes.iter().any(|s| s.name == name) { return Err(format!("SyntaxError: Label '{name}' has already been declared")); } + let fd = self.labels.finally_depth; self.labels.label_scopes.push(LabelScope { name: name.to_string(), break_label, continue_label, + finally_depth_at_open: fd, }); Ok(()) } @@ -437,11 +453,13 @@ impl CompileCtx { pub(crate) fn take_pending_loop_labels(&mut self, break_label: LabelId, continue_label: LabelId) -> usize { let names = std::mem::take(&mut self.labels.pending_loop_labels); let count = names.len(); + let fd = self.labels.finally_depth; for name in names { self.labels.label_scopes.push(LabelScope { name, break_label, continue_label: Some(continue_label), + finally_depth_at_open: fd, }); } count diff --git a/crates/oxide_emit/src/emit_ctx.rs b/crates/oxide_emit/src/emit_ctx.rs index 44196f24..ec0d7ad8 100644 --- a/crates/oxide_emit/src/emit_ctx.rs +++ b/crates/oxide_emit/src/emit_ctx.rs @@ -13,12 +13,16 @@ use oxide_ir::operand::LabelId; pub(crate) struct LabelCtx { /// label id → 指令下标。id 连续递增,Vec 索引即 id;写入前须扩容。 pub(crate) label_pos: Vec>, - pub(crate) loop_stack: Vec<(LabelId, LabelId)>, - pub(crate) switch_stack: Vec, + /// 每个条目记录循环打开时嵌套的 finally 域数(break/continue 跨越 finally 计数用)。 + pub(crate) loop_stack: Vec<(LabelId, LabelId, usize)>, + /// 每个条目记录 switch 打开时嵌套的 finally 域数。 + pub(crate) switch_stack: Vec<(LabelId, usize)>, /// 活动标签语句作用域(解析 `break label` / `continue label`)。 pub(crate) label_scopes: Vec, /// 等待绑定到下一个循环 continue 目标的标签名。 pub(crate) pending_loop_labels: Vec, + /// 当前打开(正在 emit)的 try/finally 域数。 + pub(crate) finally_depth: usize, pub(crate) label_counter: u32, } diff --git a/crates/oxide_emit/src/stmt/basic/break_continue.rs b/crates/oxide_emit/src/stmt/basic/break_continue.rs index 1109304d..2b690ac0 100644 --- a/crates/oxide_emit/src/stmt/basic/break_continue.rs +++ b/crates/oxide_emit/src/stmt/basic/break_continue.rs @@ -1,4 +1,8 @@ //! break/continue 语句 emit:解析标签与最近循环的跳转目标。 +//! +//! 计算 break/continue 逃出的 finally 域数(目标打开时的 finally 深度与当前深度之差): +//! 逃出 ≥1 个 finally → 发 BREAK/CONTINUE(运行时穿越 finally 后再跳转); +//! 不逃出(目标在 try 域内,如 switch/循环内部的 break)→ 发普通 JMP。 use crate::{CompileCtx, Emitter}; use oxide_ir::inst::Inst; @@ -7,38 +11,51 @@ impl Emitter { pub(crate) fn emit_break_statement( &self, stmt: &oxide_parser::BreakStatement, ctx: &mut CompileCtx, ) -> Result, String> { - let break_label = if let Some(label) = &stmt.label { + let (break_label, fd_at_open) = if let Some(label) = &stmt.label { let name = label.name.as_str(); let scope = ctx .find_label(name) .ok_or_else(|| format!("SyntaxError: Undefined label '{name}'"))?; - scope.break_label - } else if let Some(sw_label) = ctx.current_switch() { - *sw_label + (scope.break_label, scope.finally_depth_at_open) + } else if let Some((sw_label, fd)) = ctx.current_switch() { + (*sw_label, *fd) } else { - let (bl, _) = ctx.current_loop().ok_or("break outside switch or loop".to_string())?; - *bl + let (bl, _, fd) = ctx.current_loop().ok_or("break outside switch or loop".to_string())?; + (*bl, *fd) }; - ctx.inst(Inst::jmp(break_label)); + let crossed = ctx.labels.finally_depth.saturating_sub(fd_at_open) as u16; + if crossed > 0 { + ctx.inst(Inst::brk(break_label, crossed)); + } else { + ctx.inst(Inst::jmp(break_label)); + } Ok(None) } pub(crate) fn emit_continue_statement( &self, stmt: &oxide_parser::ContinueStatement, ctx: &mut CompileCtx, ) -> Result, String> { - let continue_label = if let Some(label) = &stmt.label { + let (continue_label, fd_at_open) = if let Some(label) = &stmt.label { let name = label.name.as_str(); let scope = ctx .find_label(name) .ok_or_else(|| format!("SyntaxError: Undefined label '{name}'"))?; - scope.continue_label.ok_or_else(|| { - format!("SyntaxError: Illegal continue statement: '{name}' does not denote an iteration statement") - })? + ( + scope.continue_label.ok_or_else(|| { + format!("SyntaxError: Illegal continue statement: '{name}' does not denote an iteration statement") + })?, + scope.finally_depth_at_open, + ) } else { - let (_, cl) = ctx.current_loop().ok_or("continue outside loop".to_string())?; - *cl + let (_, cl, fd) = ctx.current_loop().ok_or("continue outside loop".to_string())?; + (*cl, *fd) }; - ctx.inst(Inst::jmp(continue_label)); + let crossed = ctx.labels.finally_depth.saturating_sub(fd_at_open) as u16; + if crossed > 0 { + ctx.inst(Inst::cont(continue_label, crossed)); + } else { + ctx.inst(Inst::jmp(continue_label)); + } Ok(None) } } diff --git a/crates/oxide_emit/src/stmt/exception/try_catch.rs b/crates/oxide_emit/src/stmt/exception/try_catch.rs index dffcff91..88854649 100644 --- a/crates/oxide_emit/src/stmt/exception/try_catch.rs +++ b/crates/oxide_emit/src/stmt/exception/try_catch.rs @@ -18,6 +18,9 @@ impl Emitter { let has_finally = ts.finalizer.is_some(); let result_reg = ctx.alloc_reg(); if has_finally { + // finally 域跨整个语句(try 体 + catch 体 + finally 体):期间 break/continue + // 逃出语句时须穿越本 finally,词法上记录域深度供跨越计数。 + ctx.push_finally_domain(); ctx.inst(Inst::try_finally_begin(finally_label)); } if has_catch { @@ -81,6 +84,7 @@ impl Emitter { Operand::None, )); ctx.inst(Inst::new(OpCode::TRY_FINALLY_END, Operand::None, Operand::None, Operand::None)); + ctx.pop_finally_domain(); } ctx.labels.set_label_pos(try_end_label, ctx.insts.len()); Ok(Some(result_reg)) diff --git a/crates/oxide_ir/src/contract.rs b/crates/oxide_ir/src/contract.rs index e0c3b392..d1dd07d0 100644 --- a/crates/oxide_ir/src/contract.rs +++ b/crates/oxide_ir/src/contract.rs @@ -36,6 +36,8 @@ impl Inst { | OpCode::RETURN | OpCode::THROW | OpCode::JMP + | OpCode::BREAK + | OpCode::CONTINUE | OpCode::JMP_IF_FALSE | OpCode::JMP_IF_TRUE | OpCode::JMP_IF_NULLISH @@ -197,6 +199,8 @@ impl Inst { } // 无条件跳转 / try 标记:b 是 Label,无寄存器 OpCode::JMP + | OpCode::BREAK + | OpCode::CONTINUE | OpCode::TRY_BEGIN | OpCode::TRY_FINALLY_BEGIN | OpCode::TRY_END diff --git a/crates/oxide_ir/src/inst.rs b/crates/oxide_ir/src/inst.rs index 7d47d148..be1b0e92 100644 --- a/crates/oxide_ir/src/inst.rs +++ b/crates/oxide_ir/src/inst.rs @@ -218,6 +218,18 @@ impl Inst { Self::new(OpCode::JMP, Operand::None, Operand::None, Operand::Label(label)) } + /// break 完成:label 指向循环/switch 出口,`crossed` 为逃出的 finally 域数 + /// (emit 词法计算,运行时据此逐个穿越 finally;0 表示未逃出,不产生完成语义)。 + /// 编码:rd 槽放 crossed(≤255),label 放 b 槽。 + pub fn brk(label: LabelId, crossed: u16) -> Self { + Self::new(OpCode::BREAK, Operand::Imm(crossed), Operand::None, Operand::Label(label)) + } + + /// continue 完成:同 break,label 指向循环继续目标。 + pub fn cont(label: LabelId, crossed: u16) -> Self { + Self::new(OpCode::CONTINUE, Operand::Imm(crossed), Operand::None, Operand::Label(label)) + } + /// 条件寄存器为 false 时跳转。 pub fn jmp_if_false(cond_reg: u32, label: LabelId) -> Self { Self::new(OpCode::JMP_IF_FALSE, Operand::Reg(cond_reg), Operand::None, Operand::Label(label)) diff --git a/crates/oxide_ir/src/lower.rs b/crates/oxide_ir/src/lower.rs index c37d6231..1e9c2fea 100644 --- a/crates/oxide_ir/src/lower.rs +++ b/crates/oxide_ir/src/lower.rs @@ -126,6 +126,7 @@ fn encode_inst(inst: &Inst, instrs: &mut Vec, jumps: &mut Vec<(usize, u32)> let start = instrs.len(); let instr = match inst.op { OpCode::JMP => opcode::encode_jmp(0), + OpCode::BREAK | OpCode::CONTINUE => opcode::encode(inst.op, cond, 0, 0), OpCode::JMP_IF_FALSE => opcode::encode_jmp_if_false(cond, 0), OpCode::JMP_IF_TRUE => opcode::encode_jmp_if_true(cond, 0), OpCode::JMP_IF_NULLISH => opcode::encode_jmp_if_nullish(cond, 0), @@ -149,6 +150,8 @@ fn is_jump_op(op: OpCode) -> bool { matches!( op, OpCode::JMP + | OpCode::BREAK + | OpCode::CONTINUE | OpCode::JMP_IF_FALSE | OpCode::JMP_IF_TRUE | OpCode::JMP_IF_NULLISH diff --git a/crates/oxide_regalloc/src/rewrite.rs b/crates/oxide_regalloc/src/rewrite.rs index 3b48a0bc..713921ba 100644 --- a/crates/oxide_regalloc/src/rewrite.rs +++ b/crates/oxide_regalloc/src/rewrite.rs @@ -284,6 +284,8 @@ fn is_terminator(op: OpCode) -> bool { matches!( op, OpCode::JMP + | OpCode::BREAK + | OpCode::CONTINUE | OpCode::JMP_IF_TRUE | OpCode::JMP_IF_FALSE | OpCode::JMP_IF_NULLISH diff --git a/crates/oxide_vm/src/dispatch/control_flow.rs b/crates/oxide_vm/src/dispatch/control_flow.rs index c671f93e..cfbfd534 100644 --- a/crates/oxide_vm/src/dispatch/control_flow.rs +++ b/crates/oxide_vm/src/dispatch/control_flow.rs @@ -8,8 +8,16 @@ impl Vm { #[inline(always)] pub(crate) fn dispatch_jmp(&mut self, instr: Instr) { let offset = opcode::offset16(instr) as isize; + let target = ((self.pc as isize) + offset - 1) as usize; + // try 体正常完成跳入 finally(`JMP finally_pc`):标记 handler 进入 finally 体, + // 供 break/continue/return/异常判定覆盖语义。 + if let Some(h) = self.try_stack.last_mut() { + if h.frame_depth == self.frames.len() && h.finally_pc == Some(target) { + h.finally_active = true; + } + } vm_trace!("JMP offset={}", offset); - self.pc = ((self.pc as isize) + offset - 1) as usize; + self.pc = target; } #[inline(always)] diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 5a46dadd..89559e26 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -139,11 +139,55 @@ pub struct ForInIter<'bump> { pub struct TryHandler { pub catch_pc: Option, pub finally_pc: Option, + /// finally 体是否已进入(normal JMP / 异常展开 / 完成穿越都会置位)。 + /// 用标志而非 pc 范围判定"当前是否执行 finally 体":try 体末指令可能因 DCE + /// 紧贴 finally 入口,pc 边界会误判。 + pub finally_active: bool, pub frame_depth: usize, /// try 入口时 for_of_iters 的长度,界定异常展开时哪些迭代器需要 IteratorClose。 pub for_of_depth: usize, } +/// 控制流完成:break/continue/return 逃出 finally 域时暂存的完成目标。 +/// +/// 仿 `pending_exception` 的侧通道:finally 执行期间悬挂在此,由 TRY_FINALLY_END +/// 逐个恢复(`remaining_finally` 为仍需穿越的 finally 体数,进入一个递减一个)。 +#[derive(Debug, Clone, Copy)] +pub enum Completion { + Break { + target_pc: usize, + remaining_finally: usize, + }, + Continue { + target_pc: usize, + remaining_finally: usize, + }, + Return { + value: JsValue, + remaining_finally: usize, + }, +} + +impl Completion { + /// 仍需穿越的 finally 体数。 + pub fn remaining_finally(&self) -> usize { + match *self { + Completion::Break { remaining_finally, .. } + | Completion::Continue { remaining_finally, .. } + | Completion::Return { remaining_finally, .. } => remaining_finally, + } + } + + /// 复制并改写剩余 finally 计数(进入一个 finally 后递减)。 + pub fn with_remaining(&self, remaining: usize) -> Completion { + match *self { + Completion::Break { target_pc, .. } => Completion::Break { target_pc, remaining_finally: remaining }, + Completion::Continue { target_pc, .. } => Completion::Continue { target_pc, remaining_finally: remaining }, + Completion::Return { value, .. } => Completion::Return { value, remaining_finally: remaining }, + } + } +} + /// `call_bytecode_function_inline` 使用的堆分配快照。 /// 放在堆上避免 JS 代码链式同步字节码调用(如 sort 比较器、accessor)时 /// 耗尽 Rust 栈。 @@ -159,6 +203,7 @@ pub(crate) struct InlineSyncState { pub(crate) exception_value: Option, pub(crate) pending_exception: Option, pub(crate) pending_error_kind: Option<&'static str>, + pub(crate) pending_completion: Option, pub(crate) for_in_iters: Vec<*mut ForInIter<'static>>, pub(crate) for_of_iters: Vec, pub(crate) last_for_of_result: JsValue, @@ -214,6 +259,8 @@ pub struct Vm { pub(crate) last_uncaught_value: Option, pub(crate) pending_exception: Option, pub(crate) pending_error_kind: Option<&'static str>, + /// 控制流完成(break/continue/return)暂存,finally 执行后由 TRY_FINALLY_END 恢复。 + pub(crate) pending_completion: Option, pub(crate) root_reg_limit: u8, pub(crate) active_reg_limit: u8, pub(crate) native_call_depth: usize, @@ -424,6 +471,12 @@ impl Vm { f(JsValue::from_js_object(self.session.global_object().as_ptr() as *mut JsObject)); f(self.exception_value.unwrap_or(JsValue::undefined())); f(self.pending_exception.unwrap_or(JsValue::undefined())); + // 悬挂的 return 完成持有返回值,是 GC 根。 + if let Some(Completion::Return { value, .. }) = self.pending_completion { + if value.is_object() || value.is_string() { + f(value); + } + } for &v in &self.iters.for_of_iters { f(v); } @@ -1063,6 +1116,14 @@ impl Vm { self.dispatch_jmp(instr); } + OpCode::BREAK => { + self.dispatch_break(instr); + } + + OpCode::CONTINUE => { + self.dispatch_continue(instr); + } + OpCode::JMP_IF_FALSE => { self.dispatch_jmp_if_false(rd, instr); } @@ -1374,8 +1435,8 @@ impl Vm { } OpCode::TRY_FINALLY_END => match self.dispatch_try_finally_end() { - Ok(true) => continue, - Ok(false) => {} + Ok(Some(result)) => return Ok(result), + Ok(None) => {} Err(e) => return Err(e), }, @@ -1706,6 +1767,7 @@ mod tests { vm.try_stack.push(TryHandler { catch_pc: Some(1), finally_pc: None, + finally_active: false, frame_depth: 0, for_of_depth: 0, }); diff --git a/crates/oxide_vm/src/vm_dispatch_ctrl.rs b/crates/oxide_vm/src/vm_dispatch_ctrl.rs index 4fd70683..8a03af33 100644 --- a/crates/oxide_vm/src/vm_dispatch_ctrl.rs +++ b/crates/oxide_vm/src/vm_dispatch_ctrl.rs @@ -1,4 +1,4 @@ -use crate::vm::{FrameContinuation, TryHandler, Vm}; +use crate::vm::{Completion, FrameContinuation, TryHandler, Vm}; use crate::vm_trace; use oxide_bytecode::opcode; use oxide_types::object::PropAttributes; @@ -124,6 +124,83 @@ impl Vm { } } + /// break 完成:`crossed`(rd 槽)为 emit 词法算出的逃出 finally 域数。 + /// 逐个穿越 finally 后跳转到目标;crossed 为 0 时直接跳转。 + pub(crate) fn dispatch_break(&mut self, instr: u32) { + let offset = opcode::offset16(instr) as isize; + let target_pc = ((self.pc as isize) + offset - 1) as usize; + let crossed = opcode::rd(instr) as usize; + if let Some(finally_pc) = self.record_completion(Completion::Break { target_pc, remaining_finally: crossed }) { + self.pc = finally_pc; + } else { + self.pc = target_pc; + } + } + + /// continue 完成:同 break,目标为循环继续位置。 + pub(crate) fn dispatch_continue(&mut self, instr: u32) { + let offset = opcode::offset16(instr) as isize; + let target_pc = ((self.pc as isize) + offset - 1) as usize; + let crossed = opcode::rd(instr) as usize; + if let Some(finally_pc) = self.record_completion(Completion::Continue { target_pc, remaining_finally: crossed }) { + self.pc = finally_pc; + } else { + self.pc = target_pc; + } + } + + /// 记录一次控制流完成:穿越 `remaining_finally` 个 finally 体后执行完成本身。 + /// + /// # 步骤 + /// 1. 新完成覆盖在途异常/完成(break/continue/return 是新的突然完成)。 + /// 2. 从栈顶向下扫描:逃出的 catch-only handler 弹出;正在执行的 finally 体 + /// (`finally_active`)被覆盖弹出并计数;未进入的包裹 finally 计数后进入。 + /// 3. 计数耗尽(全部跨越的 finally 已进入或已覆盖)→ 返回 None(快速路径)。 + /// + /// # 边界与前提 + /// - 只扫描当前帧深度(`frames.len()`)的 handler;调用者的 handler 不参与—— + /// break/continue 词法不跨函数,return 也只逃出本函数 finally。 + /// + /// # 副作用 + /// - 清空 `pending_exception`/`pending_completion`,弹出被逃出的 handler。 + fn record_completion(&mut self, c: Completion) -> Option { + self.pending_exception = None; + self.pending_error_kind = None; + self.pending_completion = None; + let depth = self.frames.len(); + let mut remaining = c.remaining_finally(); + loop { + if remaining == 0 { + return None; + } + let (frame_depth, finally_pc, finally_active) = { + let Some(h) = self.try_stack.last() else { + return None; + }; + (h.frame_depth, h.finally_pc, h.finally_active) + }; + if frame_depth != depth { + return None; + } + let Some(fp) = finally_pc else { + // 逃出的 catch-only handler:丢弃防泄漏。 + self.try_stack.pop(); + continue; + }; + remaining -= 1; + if finally_active { + // 正在执行本 finally 体:本完成覆盖它,弹出。 + self.try_stack.pop(); + continue; + } + self.try_stack.last_mut().unwrap().finally_active = true; + self.pending_completion = Some(c.with_remaining(remaining)); + return Some(fp); + } + } + + /// return 完成:`crossed` = 当前帧深度内全部 finally handler 数(return 逃出整个 + /// 函数,途中被覆盖的 finally 体也在内),穿越后实际返回。 pub(crate) fn dispatch_return(&mut self, rd: usize) -> Result, String> { let result = self.regs[rd]; crate::vm_debug!( @@ -131,6 +208,23 @@ impl Vm { self.frames.len(), self.frames.last().map(|f| f.return_addr).unwrap_or(0) ); + let crossed = self + .try_stack + .iter() + .filter(|h| h.frame_depth == self.frames.len() && h.finally_pc.is_some()) + .count(); + if let Some(finally_pc) = + self.record_completion(Completion::Return { value: result, remaining_finally: crossed }) + { + self.pc = finally_pc; + return Ok(None); + } + self.do_return(result) + } + + /// 实际执行返回:弹出当前帧并交付返回值(供 dispatch_return 与 finally 完成 + /// 恢复共用)。 + fn do_return(&mut self, result: JsValue) -> Result, String> { if let Some(frame) = self.frames.pop() { self.cell_stack.pop(); let construct_result_reg = frame.construct_result_reg; @@ -139,10 +233,9 @@ impl Vm { let continuation = frame.continuation; let callee_this = self.regs[254]; vm_trace!( - "RETURN frame: continuation={:?}, derived={}, result_reg={}", + "RETURN frame: continuation={:?}, derived={}", continuation, - is_derived_constructor, - rd + is_derived_constructor ); self.restore_frame(frame); if let (Some(target_reg), Some(constructed_this)) = (construct_result_reg, constructed_this) { @@ -192,6 +285,7 @@ impl Vm { self.try_stack.push(TryHandler { catch_pc, finally_pc: None, + finally_active: false, frame_depth: self.frames.len(), for_of_depth: self.iters.for_of_iters.len(), }); @@ -209,20 +303,75 @@ impl Vm { self.try_stack.push(TryHandler { catch_pc: None, finally_pc: Some(finally_pc), + finally_active: false, frame_depth: self.frames.len(), for_of_depth: self.iters.for_of_iters.len(), }); } - pub(crate) fn dispatch_try_finally_end(&mut self) -> Result { + /// finally 完成分发点:弹出当前 handler 后统一恢复在途异常或控制流完成。 + /// + /// # 步骤 + /// 1. 弹出刚执行完的 finally handler。 + /// 2. 优先恢复在途异常(unwind 继续向外展开)。 + /// 3. 否则查 `pending_completion`:仍有剩余 finally 体则进入下一个(递减计数); + /// 耗尽则执行完成本身(break/continue 跳转目标,return 交付返回值)。 + /// + /// # 返回值 + /// `Ok(Some(value))` = 函数返回;`Ok(None)` = 正常继续 dispatch。 + pub(crate) fn dispatch_try_finally_end(&mut self) -> Result, String> { vm_trace!("TRY_FINALLY_END has_pending_exc={}", self.pending_exception.is_some()); self.try_stack.pop(); if self.pending_exception.is_some() && self.exception_value.is_none() { self.exception_value = self.pending_exception.take(); - self.unwind().map(|_| true) - } else { - self.pending_exception = None; - Ok(false) + self.unwind()?; + return Ok(None); + } + self.pending_exception = None; + let Some(c) = self.pending_completion.take() else { + return Ok(None); + }; + if c.remaining_finally() > 0 { + let next = c.with_remaining(c.remaining_finally() - 1); + if let Some(finally_pc) = self.find_next_finally() { + self.pending_completion = Some(next); + self.pc = finally_pc; + return Ok(None); + } + } + match c { + Completion::Break { target_pc, .. } | Completion::Continue { target_pc, .. } => { + self.pc = target_pc; + Ok(None) + } + Completion::Return { value, .. } => self.do_return(value), + } + } + + /// 找下一个未被覆盖的包裹 finally 并进入(resume 路径用):弹出途中被逃出的 + /// catch-only 与防御性的 active handler。无更多返回 None。 + fn find_next_finally(&mut self) -> Option { + let depth = self.frames.len(); + loop { + let (frame_depth, finally_pc, finally_active) = { + let Some(h) = self.try_stack.last() else { + return None; + }; + (h.frame_depth, h.finally_pc, h.finally_active) + }; + if frame_depth != depth { + return None; + } + let Some(fp) = finally_pc else { + self.try_stack.pop(); + continue; + }; + if finally_active { + self.try_stack.pop(); + continue; + } + self.try_stack.last_mut().unwrap().finally_active = true; + return Some(fp); } } } diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index e0776622..512d4455 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -72,6 +72,7 @@ impl Vm { exception_value: self.exception_value.take(), pending_exception: self.pending_exception.take(), pending_error_kind: self.pending_error_kind.take(), + pending_completion: self.pending_completion, for_in_iters: std::mem::take(&mut self.iters.for_in_iters), for_of_iters: std::mem::take(&mut self.iters.for_of_iters), last_for_of_result: self.iters.last_for_of_result, @@ -133,6 +134,7 @@ impl Vm { self.exception_value = saved.exception_value; self.pending_exception = saved.pending_exception; self.pending_error_kind = saved.pending_error_kind; + self.pending_completion = saved.pending_completion; self.iters.for_in_iters = saved.for_in_iters; self.iters.for_of_iters = saved.for_of_iters; self.iters.last_for_of_result = saved.last_for_of_result; @@ -222,7 +224,7 @@ impl Vm { pub(crate) fn unwind(&mut self) -> Result<(), String> { vm_debug!("unwind: {} try handlers on stack", self.try_stack.len()); - while let Some(handler) = self.try_stack.pop() { + while let Some(mut handler) = self.try_stack.pop() { while self.frames.len() > handler.frame_depth { if let Some(frame) = self.frames.pop() { self.cell_stack.pop(); @@ -232,10 +234,17 @@ impl Vm { // 对该处理器作用域内被中断的 for-of 循环执行 IteratorClose。 self.close_for_of_above(handler.for_of_depth); if let Some(finally_pc) = handler.finally_pc { - vm_trace!("unwind: entering finally at pc={}", finally_pc); - if self.pending_exception.is_none() { - self.pending_exception = self.exception_value.take(); + if handler.finally_active { + // 新异常在 finally 体内抛出:覆盖在途异常与完成,继续向外展开, + // 不再重入本 finally(否则已执行的 finally 会重复运行)。 + self.pending_exception = None; + self.pending_error_kind = None; + self.pending_completion = None; + continue; } + vm_trace!("unwind: entering finally at pc={}", finally_pc); + self.pending_exception = Some(self.exception_value.take().unwrap_or(JsValue::undefined())); + handler.finally_active = true; self.try_stack.push(handler); self.pc = finally_pc; return Ok(()); diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 56642785..c50fa3bc 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -43,6 +43,7 @@ impl Vm { last_uncaught_value: None, pending_exception: None, pending_error_kind: None, + pending_completion: None, root_reg_limit: 0, active_reg_limit: 0, native_call_depth: 0, @@ -108,6 +109,7 @@ impl Vm { last_uncaught_value: None, pending_exception: None, pending_error_kind: None, + pending_completion: None, root_reg_limit: 0, active_reg_limit: 0, native_call_depth: 0, @@ -223,6 +225,7 @@ impl Vm { self.exception_value = None; self.pending_exception = None; self.pending_error_kind = None; + self.pending_completion = None; self.native_call_depth = 0; } diff --git a/crates/oxide_vm/tests/try_finally_control_flow_tests.rs b/crates/oxide_vm/tests/try_finally_control_flow_tests.rs new file mode 100644 index 00000000..50547097 --- /dev/null +++ b/crates/oxide_vm/tests/try_finally_control_flow_tests.rs @@ -0,0 +1,191 @@ +//! try/finally + break/continue/return 控制流冒烟:finally 必须按语义执行。 +//! +//! 覆盖:break/continue/return 穿越 finally、finally 内控制流覆盖、 +//! 异常路径不回归、嵌套 try/finally、for-of 迭代器关闭、普通循环零开销路径。 +//! 字符串值在此引擎 Display 为 `{string}`,断言一律用数值编码。 + +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(&module) { + Ok(result) => format!("{result}"), + Err(e) => format!("vm error: {e}"), + } +} + +#[test] +fn break_runs_finally_and_exits_loop() { + assert_eq!(eval("var fin = 0; var i = 0; for (; i < 5; ) { try { break; } finally { fin = 1; } i = i + 1; } fin * 10 + i"), "10"); +} + +#[test] +fn continue_runs_finally_every_iteration() { + assert_eq!(eval("var fin = 0; for (var i = 0; i < 5; i++) { try { continue; } finally { fin = fin + 1; } } fin"), "5"); +} + +#[test] +fn return_runs_finally() { + assert_eq!(eval("var fin = 0; function f() { try { return 1; } finally { fin = 1; } } var r = f(); r * 10 + fin"), "11"); +} + +#[test] +fn break_inside_finally_breaks_loop() { + assert_eq!(eval("var fin = 0; var n = 0; for (var i = 0; i < 5; i++) { try { } finally { fin = fin + 1; break; } n = n + 1; } fin * 10 + n"), "10"); +} + +#[test] +fn throw_still_runs_finally() { + assert_eq!(eval("var fin = 0; function g() { try { throw 1; } finally { fin = 1; } } try { g(); } catch (e) {} fin"), "1"); +} + +#[test] +fn nested_try_finally_both_run_on_break() { + assert_eq!(eval("var log = 0; for (var i = 0; i < 3; i++) { try { try { break; } finally { log = log * 10 + 1; } } finally { log = log * 10 + 2; } } log"), "12"); +} + +#[test] +fn for_of_break_runs_finally() { + assert_eq!(eval("var fin = 0; var it = 0; for (var x of [1,2,3]) { try { break; } finally { fin = 1; } it = it + 1; } fin * 10 + it"), "10"); +} + +#[test] +fn plain_loop_without_try_unchanged() { + assert_eq!(eval("var s = 0; for (var i = 0; i < 5; i++) { s = s + i; } s"), "10"); +} + +#[test] +fn try_catch_break_does_not_leak_handler() { + assert_eq!( + eval("var caught = 0; for (var i = 0; i < 1; i++) { try { break; } catch (e) { caught = 1; } } try { throw 1; } catch (e) { caught = 2; } caught"), + "2" + ); +} + +#[test] +fn try_finally_break_does_not_leak_handler() { + assert_eq!( + eval("var caught = 0; for (var i = 0; i < 1; i++) { try { break; } finally { } } try { throw 1; } catch (e) { caught = 2; } caught"), + "2" + ); +} + +#[test] +fn return_in_finally_overrides() { + assert_eq!(eval("function f() { try { return 1; } finally { return 2; } } f()"), "2"); +} + +#[test] +fn return_in_finally_overrides_exception() { + assert_eq!(eval("function f() { try { throw 1; } finally { return 2; } } f()"), "2"); +} + +#[test] +fn break_in_finally_overrides_exception() { + assert_eq!( + eval("var n = 0; for (var i = 0; i < 5; i++) { try { try { throw 1; } finally { break; } } catch (e) { n = n + 1; } } n"), + "0" + ); +} + +#[test] +fn continue_in_finally_overrides_break() { + assert_eq!(eval("var log = 0; for (var i = 0; i < 3; i++) { try { break; } finally { log = log * 10 + i; continue; } } log"), "12"); +} + +#[test] +fn throw_in_finally_overrides_previous_exception() { + assert_eq!(eval("var fin = 0; try { try { throw 1; } finally { throw 2; } } catch (e) { fin = e; } fin"), "2"); +} + +#[test] +fn throw_then_finally_then_catch_chain_kept() { + assert_eq!( + eval("var fin = 0; var cc = 0; for (var i = 0; i < 3; i++) { try { try { throw i; } finally { fin = fin + 1; } } catch (e) { cc = cc + 1; } } fin * 10 + cc"), + "33" + ); +} + +#[test] +fn labeled_break_runs_finally() { + assert_eq!( + eval("var fin = 0; outer: for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { try { break outer; } finally { fin = fin + 1; } } } fin"), + "1" + ); +} + +#[test] +fn nested_function_break_ignores_caller_finally() { + assert_eq!( + eval("var log = 0; function inner() { for (var i = 0; i < 2; i++) { break; } return 1; } try { inner(); log = log * 10 + 1; } finally { log = log * 10 + 2; } log"), + "12" + ); +} + +#[test] +fn continue_with_finally_conditionally() { + assert_eq!( + eval("var fin = 0; var count = 0; for (var i = 0; i < 4; i++) { try { if (i % 2 === 0) continue; count = count + 1; } finally { fin = fin + 1; } } fin * 10 + count"), + "42" + ); +} + +#[test] +fn normal_finally_path_still_works() { + assert_eq!(eval("var s = 0; for (var i = 0; i < 3; i++) { try { s = s + i; } finally { s = s + 10; } } s"), "33"); +} + +#[test] +fn internal_loop_break_inside_try_does_not_trip_finally() { + // break 目标是 try 域内的循环出口:不穿越 finally,finally 只走正常路径一次。 + assert_eq!( + eval("var fin = 0; var i = 0; for (var k = 0; k < 3; k++) { try { while (i < 1) { break; } } finally { fin = fin + 1; } i = i + 1; } fin"), + "3" + ); +} + +#[test] +fn triple_nested_finally_all_run_on_break() { + // break 穿越三层 finally,每个都执行一次。 + assert_eq!( + eval("var log = 0; for (var i = 0; i < 3; i++) { try { try { try { break; } finally { log = log * 10 + 1; } } finally { log = log * 10 + 2; } } finally { log = log * 10 + 3; } } log"), + "123" + ); +} + +#[test] +fn return_crosses_multiple_finally() { + assert_eq!( + eval("var fin = 0; function f() { try { try { return 5; } finally { fin = fin + 1; } } finally { fin = fin + 10; } } var r = f(); r * 100 + fin"), + "511" + ); +} + +#[test] +fn continue_across_two_finally_runs_both_each_iteration() { + // 内层 try/finally 包裹外层 try/finally,continue 逃出两者,两者每轮都执行。 + assert_eq!( + eval("var a = 0; var b = 0; for (var i = 0; i < 3; i++) { try { try { continue; } finally { a = a + 1; } } finally { b = b + 1; } } a * 10 + b"), + "33" + ); +} + +#[test] +fn throw_in_finally_discards_pending_break() { + // finally 内 throw 覆盖悬挂的 break 完成:break 不执行,异常向外传播。 + assert_eq!( + eval("var fin = 0; var n = 0; for (var i = 0; i < 5; i++) { try { try { break; } finally { throw 9; } } catch (e) { n = e; } } n"), + "9" + ); +}