feat: 异常与 return 穿越释放资源,dispose_count 门控

This commit is contained in:
2026-10-09 19:14:36 +00:00
parent 48ffb54c4f
commit ae73fb8da5
20 changed files with 264 additions and 152 deletions
+4 -2
View File
@@ -723,15 +723,17 @@ impl Vm {
.iter()
.filter(|h| h.frame_depth == self.frames.len() && h.finally_pc.is_some())
.count();
// .return()/.throw() 注入时生成器挂起快照中打开的全部迭代器一并逃出
// (栈上迭代器属于本生成器),完成恢复处统一关闭。
// .return()/.throw() 注入时生成器挂起快照中打开的全部迭代器与资源一并逃出
// (栈上迭代器/资源属于本生成器),完成恢复处统一关闭/释放。
let for_of_count = self.iters.for_of_iters.len();
let for_in_count = self.iters.for_in_iters.len();
let dispose_count = self.dispose_stack.len();
let completion = crate::vm::Completion::Return {
value,
remaining_finally: crossed,
for_of_count,
for_in_count,
dispose_count,
};
if let Some(finally_pc) = self.record_completion(completion) {
self.pc = finally_pc;
+15 -9
View File
@@ -285,16 +285,19 @@ mod tests {
}
#[test]
fn break_ret_ext_word_count_is_one() {
// BREAK/CONTINUE/RETURN 恒带 1 个逃出计数 ext 字(pack_escape_counts)。
fn break_ret_ext_word_count_is_two() {
// BREAK/CONTINUE/RETURN 恒带 2 个逃出计数 ext 字:第一字
// pack_escape_counts(迭代器层数打包),第二字 dispose_count(资源条数)。
let bytecode = vec![
opcode::encode(OpCode::BREAK, 0, 0, 0),
0x0000_0040,
0x0000_0002,
opcode::encode(OpCode::RETURN, 1, 0, 0),
0x1234_5678,
0x0000_0003,
];
assert_eq!(ext_word_count(&bytecode, 0), 1, "BREAK ext 字数 = 1");
assert_eq!(ext_word_count(&bytecode, 2), 1, "RETURN ext 字数 = 1");
assert_eq!(ext_word_count(&bytecode, 0), 2, "BREAK ext 字数 = 2");
assert_eq!(ext_word_count(&bytecode, 3), 2, "RETURN ext 字数 = 2");
}
#[test]
@@ -331,12 +334,14 @@ mod tests {
#[test]
fn clear_ic_caches_skips_break_ext_word() {
// BREAK 的逃出计数 ext 字必须原样保留;其低 8 位 = for_of_count,取 0x40
// (CALL 指令号,带 ext)——未登记时扫描会把它当 CALL 指令解析,越过后续
// IC 指令导致扩展字漏清,本用例可抓住该错位。
// BREAK 的逃出计数 ext 字(2 个:迭代器打包字 + dispose_count 字)必须原样
// 保留;第一字低 8 位 = for_of_count,取 0x40(CALL 指令号,带 ext)——未
// 登记时扫描会把它当 CALL 指令解析,越过后续 IC 指令导致扩展字漏清,本用例
// 可抓住该错位。
let mut bytecode = vec![
opcode::encode(OpCode::BREAK, 0, 0, 0),
0x0000_0040,
0x0000_0002,
opcode::encode(OpCode::IC_GET_PROP, 1, 2, 3),
0xAAAA_AAAA,
0xBBBB_BBBB,
@@ -348,8 +353,9 @@ mod tests {
0x2222_2222,
];
clear_ic_caches(&mut bytecode);
assert_eq!(bytecode[1], 0x0000_0040, "BREAK ext 字保持原值");
assert_eq!(&bytecode[3..=10], &[0; 8], "后续 IC 扩展字被清零");
assert_eq!(bytecode[1], 0x0000_0040, "BREAK 第一 ext 字保持原值");
assert_eq!(bytecode[2], 0x0000_0002, "BREAK 第二 ext 字保持原值");
assert_eq!(&bytecode[4..=11], &[0; 8], "后续 IC 扩展字被清零");
}
#[test]
+1
View File
@@ -312,6 +312,7 @@ mod tests {
remaining_finally: 1,
for_of_count: 0,
for_in_count: 0,
dispose_count: 0,
});
let mut frame = SuspendedFrame::new_empty();
+24 -2
View File
@@ -109,14 +109,18 @@ pub struct TryHandler {
pub frame_depth: usize,
/// try 入口时 for_of_iters 的长度,界定异常展开时哪些迭代器需要 IteratorClose。
pub for_of_depth: usize,
/// try 入口时释放栈(dispose_stack)的长度,界定异常展开时哪些资源需要
/// DisposeResources(逆序释放并截断到本水位)。
pub dispose_depth: usize,
}
/// 控制流完成:break/continue/return 逃出 finally 域时暂存的完成目标。
///
/// 仿 `pending_exception` 的侧通道:finally 执行期间悬挂在此,由 TRY_FINALLY_END
/// 逐个恢复(`remaining_finally` 为仍需穿越的 finally 体数,进入一个递减一个)。
/// `for_of_count`/`for_in_count` 为逃出时需关闭的迭代器层数:在全部 finally 穿越
/// 之后、跳转/返回之前执行(规范 §13.7.5.4 的 IteratorClose 在完成值之后)。
/// `for_of_count`/`for_in_count` 为逃出时需关闭的迭代器层数,`dispose_count` 为
/// 逃出时需释放的资源条数:在全部 finally 穿越之后、跳转/返回之前执行(规范序
/// IteratorClose 先、DisposeResources 后)。
#[derive(Debug, Clone, Copy)]
pub enum Completion {
Break {
@@ -124,18 +128,21 @@ pub enum Completion {
remaining_finally: usize,
for_of_count: usize,
for_in_count: usize,
dispose_count: usize,
},
Continue {
target_pc: usize,
remaining_finally: usize,
for_of_count: usize,
for_in_count: usize,
dispose_count: usize,
},
Return {
value: JsValue,
remaining_finally: usize,
for_of_count: usize,
for_in_count: usize,
dispose_count: usize,
},
}
@@ -167,6 +174,15 @@ impl Completion {
}
}
/// 逃出时需释放的资源条数(释放栈顶部连续条数)。
pub fn dispose_count(&self) -> usize {
match *self {
Completion::Break { dispose_count, .. }
| Completion::Continue { dispose_count, .. }
| Completion::Return { dispose_count, .. } => dispose_count,
}
}
/// 复制并改写剩余 finally 计数(进入一个 finally 后递减)。
pub fn with_remaining(&self, remaining: usize) -> Completion {
match *self {
@@ -174,34 +190,40 @@ impl Completion {
target_pc,
for_of_count,
for_in_count,
dispose_count,
..
} => Completion::Break {
target_pc,
remaining_finally: remaining,
for_of_count,
for_in_count,
dispose_count,
},
Completion::Continue {
target_pc,
for_of_count,
for_in_count,
dispose_count,
..
} => Completion::Continue {
target_pc,
remaining_finally: remaining,
for_of_count,
for_in_count,
dispose_count,
},
Completion::Return {
value,
for_of_count,
for_in_count,
dispose_count,
..
} => Completion::Return {
value,
remaining_finally: remaining,
for_of_count,
for_in_count,
dispose_count,
},
}
}
+23 -10
View File
@@ -631,7 +631,7 @@ impl Vm {
/// ext 字携带逃出的迭代器层数:无 finally 穿越时立即关闭后跳转,有 finally
/// 时计数随 Completion 悬挂,由 TRY_FINALLY_END 在穿越完成后消费。
pub(crate) fn dispatch_break(&mut self, instr: u32) -> Result<(), String> {
let (for_of_count, for_in_count) = self.read_escape_counts();
let (for_of_count, for_in_count, dispose_count) = self.read_escape_counts();
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;
@@ -640,6 +640,7 @@ impl Vm {
remaining_finally: crossed,
for_of_count,
for_in_count,
dispose_count,
};
if let Some(finally_pc) = self.record_completion(completion) {
self.pc = finally_pc;
@@ -658,7 +659,7 @@ impl Vm {
/// continue 完成:同 break,目标为循环继续位置。
pub(crate) fn dispatch_continue(&mut self, instr: u32) -> Result<(), String> {
let (for_of_count, for_in_count) = self.read_escape_counts();
let (for_of_count, for_in_count, dispose_count) = self.read_escape_counts();
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;
@@ -667,6 +668,7 @@ impl Vm {
remaining_finally: crossed,
for_of_count,
for_in_count,
dispose_count,
};
if let Some(finally_pc) = self.record_completion(completion) {
self.pc = finally_pc;
@@ -683,19 +685,21 @@ impl Vm {
Ok(())
}
/// 读取 BREAK/CONTINUE/RETURN 的 ext 字逃出计数(低 16 位 for-of,高 16 位
/// for-in)。调用时机:主循环已把 pc 推进到 ext 字位置;两条跳转/返回路径
/// 都会覆盖 pc,此处无需再推进。
/// 读取 BREAK/CONTINUE/RETURN 的 ext 字逃出计数:第一字低 16 位 for-of、
/// 高 16 位 for-in,第二字为 dispose_count(逃出资源条数)。调用时机:主循环
/// 已把 pc 推进到 ext 字位置;两条跳转/返回路径都会覆盖 pc,此处无需再推进。
///
/// # 边界与前提
/// - 手工构造的 IR 可能让 RETURN/BREAK/CONTINUE 落在末位且无 ext 字(pc == len),
/// 此时按 (0, 0) 处理:无迭代器逃出,语义正确。
fn read_escape_counts(&self) -> (usize, usize) {
/// 此时按 (0, 0, 0) 处理:无迭代器/资源逃出,语义正确。第二字缺失(pc+1 == len)
/// 同样按 0 兜底。
fn read_escape_counts(&self) -> (usize, usize, usize) {
if self.pc >= self.bytecode.len() {
return (0, 0);
return (0, 0, 0);
}
let packed = self.bytecode[self.pc];
((packed & 0xFFFF) as usize, (packed >> 16) as usize)
let dispose = self.bytecode.get(self.pc + 1).copied().unwrap_or(0) as usize;
((packed & 0xFFFF) as usize, (packed >> 16) as usize, dispose)
}
/// 记录一次控制流完成:穿越 `remaining_finally` 个 finally 体后执行完成本身。
@@ -752,7 +756,7 @@ impl Vm {
pub(crate) fn dispatch_return(&mut self, instr: u32) -> Result<Option<JsValue>, String> {
let rd = opcode::rd(instr) as usize;
let result = self.regs[rd];
let (for_of_count, for_in_count) = self.read_escape_counts();
let (for_of_count, for_in_count, dispose_count) = self.read_escape_counts();
crate::vm_debug!(
"RETURN depth={} saved_pc={}",
self.frames.len(),
@@ -772,6 +776,7 @@ impl Vm {
remaining_finally: crossed,
for_of_count,
for_in_count,
dispose_count,
};
if let Some(finally_pc) = self.record_completion(completion) {
self.pc = finally_pc;
@@ -903,6 +908,7 @@ impl Vm {
finally_active: false,
frame_depth: self.frames.len(),
for_of_depth: self.iters.for_of_iters.len(),
dispose_depth: self.dispose_stack.len(),
});
}
@@ -921,6 +927,7 @@ impl Vm {
finally_active: false,
frame_depth: self.frames.len(),
for_of_depth: self.iters.for_of_iters.len(),
dispose_depth: self.dispose_stack.len(),
});
}
@@ -1029,6 +1036,12 @@ impl Vm {
return Ok(CloseEscapeOutcome::Handled);
}
}
// 逃出资源释放(规范序:IteratorClose 先、DisposeResources 后):逆序释放
// 顶部 dispose_count 条。释放方法抛错时新错误替代完成值,展开到外围处理器。
if self.dispose_top_n(completion.dispose_count()) {
self.unwind()?;
return Ok(CloseEscapeOutcome::Handled);
}
// 异步条目逐层关闭:首个有 return 方法的登记挂起,其余入 remaining;无
// return 方法时继续下一层。
let mut remaining = async_iters;
+51
View File
@@ -689,6 +689,57 @@ impl Vm {
Ok(())
}
/// 逆序释放 `depth` 以上资源并截断释放栈到 `depth`,同步弹出水位栈中
/// `>= depth` 的基线条目(保两栈一致,防异常路径基线泄漏)。
///
/// # 步骤
/// 1. 逐条弹出并释放(`dispose_value`)。
/// 2. 释放方法抛错时,新错误替代在途异常向外传播(`exception_value` 改写),
/// 剩余资源继续释放(其错误被抑制,多错合并归后续任务)。
/// 3. 截断释放栈后,弹出水位栈中 `>= depth` 的基线条目。
///
/// # 返回值
/// `true` = 有释放方法抛错(新错误已写入 `exception_value`);`false` = 全部正常。
///
/// # 副作用
/// - 释放栈截断到 `depth`、水位栈弹出 `>= depth` 的基线。
/// - 有释放方法抛错时改写 `exception_value`/`pending_error_kind`。
pub(crate) fn dispose_above(&mut self, depth: usize) -> bool {
let mut threw = false;
let truncated = self.dispose_stack.len() > depth;
while self.dispose_stack.len() > depth {
let (value, hint) = self.dispose_stack.pop().expect("水位以上必有条目");
if let Err(e) = self.dispose_value(value, hint) {
let exc = self
.last_uncaught_value
.take()
.unwrap_or_else(|| oxide_builtins::error::create_from_text(self, &e));
let kind = self.thrown_error_kind(exc);
self.exception_value = Some(exc);
self.pending_error_kind = Some(kind);
threw = true;
}
}
// 仅在实际截断释放栈时同步弹基线,防 no-op 调用(如 dispose_top_n(0))
// 误弹外层作用域的水位。
if truncated {
while self.dispose_marks.last().is_some_and(|&m| m >= depth) {
self.dispose_marks.pop();
}
}
threw
}
/// 释放栈顶部 `n` 条资源(逃出释放:return/break/continue 完成值逃出作用域时
/// 逆序释放被逃出的资源)。`n` 超过栈长时按全量释放。
///
/// # 返回值
/// 同 `dispose_above`:`true` = 有释放方法抛错。
pub(crate) fn dispose_top_n(&mut self, n: usize) -> bool {
let depth = self.dispose_stack.len().saturating_sub(n);
self.dispose_above(depth)
}
/// 单资源释放(规范 Dispose 语义):非对象跳过;按释放提示取方法(同步只取
/// Symbol.dispose;异步先取 Symbol.asyncDispose,未定义回退 Symbol.dispose),
/// 不可调用跳过;以资源自身为 this 零参调用。
+12 -8
View File
@@ -870,6 +870,9 @@ impl Vm {
}
// 对该处理器作用域内被中断的 for-of 循环执行 IteratorClose。
self.close_for_of_above(handler.for_of_depth);
// 异常穿越释放:逆序释放该作用域内登记的资源(规范序 IteratorClose 先、
// DisposeResources 后)。释放方法抛错时新错误替代在途异常继续向外展开。
self.dispose_above(handler.dispose_depth);
if let Some(finally_pc) = handler.finally_pc {
if handler.finally_active {
// 新异常在 finally 体内抛出:覆盖在途异常与完成,继续向外展开,
@@ -899,6 +902,8 @@ impl Vm {
}
}
self.close_for_of_above(0);
// 未捕获兜底:释放栈上全部资源(异常穿越全作用域)。
self.dispose_above(0);
while let Some(frame) = self.frames.pop() {
self.cell_stack.pop();
self.restore_frame(frame);
@@ -1003,6 +1008,7 @@ mod tests {
remaining_finally: 1,
for_of_count: 0,
for_in_count: 0,
dispose_count: 0,
});
vm.iters.for_of_iters.push(crate::vm_state::ForOfEntry {
iterator: JsValue::float(8.5),
@@ -1146,19 +1152,17 @@ mod tests {
#[test]
fn using_declaration_registers_dispose_stack() {
// using 声明经 DISPOSE_REGISTER 把资源值压入释放栈。早 return 跳过函数
// 体 POP(穿越面,归后续任务),资源残留栈上;run 边界(下次 run 入口)
// 清栈,残留条目不得跨 run 可见。
// using 声明经 DISPOSE_REGISTER 把资源值压入释放栈。早 return 经逃出释放
// (dispose_top_n)逆序释放本作用域资源,函数返回后栈空;run 边界清栈防
// 跨 run 残留。
let mut vm = Vm::new();
let module = Arc::new(compile("function f() { using x = { a: 1 }; return 42; } f();"));
vm.run(&module).expect("run");
assert_eq!(vm.dispose_stack.len(), 1, "早 return 跳过 POP,资源残留栈上");
assert!(vm.dispose_stack[0].0.is_object(), "登记值应为资源对象");
assert_eq!(vm.dispose_stack[0].1, 0, "using 声明释放提示应为同步");
assert!(vm.dispose_stack.is_empty(), "早 return 经逃出释放,函数返回后栈空");
// 二次 run 入口清栈后重新登记:残留条目不跨 run 存活。
// 二次 run 入口清栈:残留条目不跨 run 存活。
vm.run(&module).expect("run2");
assert_eq!(vm.dispose_stack.len(), 1, "二次 run 应恰好登记一条新条目");
assert!(vm.dispose_stack.is_empty(), "二次 run 后栈仍空");
// 执行状态清空同样清释放栈。
vm.clear_execution_state();
+1
View File
@@ -841,6 +841,7 @@ fn reset_clears_runtime_state_like_rerun() {
finally_active: false,
frame_depth: 0,
for_of_depth: 0,
dispose_depth: 0,
});
vm.exception_value = Some(JsValue::int(2));
vm.pending_exception = Some(JsValue::int(3));
+2 -2
View File
@@ -64,7 +64,7 @@ fn spill_unspill_roundtrip() {
#[test]
fn frame_boundary_parent_spill_survives_call() {
let child = int_module(
vec![Inst::load_const(Operand::Reg(3), 0), Inst::ret(Operand::Reg(3), 0, 0)],
vec![Inst::load_const(Operand::Reg(3), 0), Inst::ret(Operand::Reg(3), 0, 0, 0)],
99,
4,
Vec::new(),
@@ -89,7 +89,7 @@ fn nested_call_both_spill() {
vec![
Inst::load_const(Operand::Reg(3), 0),
Inst::inst_spill(Operand::Reg(3), 0),
Inst::ret(Operand::Reg(3), 0, 0),
Inst::ret(Operand::Reg(3), 0, 0, 0),
],
99,
4,