feat: 支持 try-catch 语法字节码执行

This commit is contained in:
2026-06-07 22:00:51 +08:00
parent 8e0a9864f1
commit d44447da6f
5 changed files with 223 additions and 16 deletions
+101 -1
View File
@@ -49,6 +49,12 @@ pub struct ForInIter<'bump> {
pub index: usize,
}
pub struct TryHandler {
pub catch_pc: Option<usize>,
pub finally_pc: Option<usize>,
pub frame_depth: usize,
}
pub struct Vm {
regs: [JsValue; 256],
pc: usize,
@@ -67,6 +73,10 @@ pub struct Vm {
saved_bytecode_stack: Vec<Vec<opcode::Instr>>,
/// Stack of saved constants for nested bytecode calls
saved_constants_stack: Vec<Vec<JsValue>>,
try_stack: Vec<TryHandler>,
exception_value: Option<JsValue>,
pending_exception: Option<JsValue>,
pending_error_kind: Option<&'static str>,
}
fn bind_object(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
@@ -660,6 +670,10 @@ impl Vm {
sub_module_constants: Vec::new(),
saved_bytecode_stack: Vec::new(),
saved_constants_stack: Vec::new(),
try_stack: Vec::new(),
exception_value: None,
pending_exception: None,
pending_error_kind: None,
}
}
@@ -680,6 +694,10 @@ impl Vm {
sub_module_constants: Vec::new(),
saved_bytecode_stack: Vec::new(),
saved_constants_stack: Vec::new(),
try_stack: Vec::new(),
exception_value: None,
pending_exception: None,
pending_error_kind: None,
}
}
@@ -864,6 +882,10 @@ impl Vm {
self.regs = [JsValue::undefined(); 256];
self.frames.clear();
self.for_in_iters.clear();
self.try_stack.clear();
self.exception_value = None;
self.pending_exception = None;
self.pending_error_kind = None;
self.clear_ic_caches();
self.dispatch()
}
@@ -895,6 +917,10 @@ impl Vm {
self.frames.clear();
self.saved_bytecode_stack.clear();
self.saved_constants_stack.clear();
self.try_stack.clear();
self.exception_value = None;
self.pending_exception = None;
self.pending_error_kind = None;
for (name, reg) in &module.builtin_reg_map {
let si = self.kernel.string_forge().intern(name.as_str()).0;
@@ -922,6 +948,31 @@ impl Vm {
Err("bytecode function calls not yet supported".into())
}
fn unwind(&mut self) -> Result<(), String> {
while let Some(handler) = self.try_stack.pop() {
if let Some(finally_pc) = handler.finally_pc {
if self.pending_exception.is_none() {
self.pending_exception = self.exception_value.take();
}
self.try_stack.push(handler);
self.pc = finally_pc;
return Ok(());
}
if let Some(catch_pc) = handler.catch_pc {
let exc = self.exception_value.take().unwrap_or(JsValue::undefined());
self.regs[0] = exc;
self.pc = catch_pc;
return Ok(());
}
}
let msg = self
.exception_value
.take()
.map(|v| format!("uncaught {v}"))
.unwrap_or_else(|| "uncaught exception".to_string());
Err(msg)
}
fn dispatch(&mut self) -> Result<JsValue, String> {
let mut steps: u64 = 0;
const MAX_STEPS: u64 = 100_000_000;
@@ -1264,7 +1315,7 @@ impl Vm {
}
OpCode::NEW_EXPRESSION => {
let constructor_reg = a as usize;
let constructor_reg = a;
let first_arg_reg = b as u8;
let constructor = self.regs[constructor_reg];
@@ -2412,6 +2463,55 @@ impl Vm {
self.for_in_iters.pop();
}
OpCode::THROW => {
let exc_value = self.regs[rd];
self.exception_value = Some(exc_value);
match self.unwind() {
Ok(()) => continue,
Err(e) => return Err(e),
}
}
OpCode::TRY_BEGIN => {
let offset = opcode::offset16(instr) as isize;
let catch_pc = if offset == 0 {
None
} else {
Some(((self.pc as isize) + offset - 1) as usize)
};
self.try_stack.push(TryHandler {
catch_pc,
finally_pc: None,
frame_depth: self.frames.len(),
});
}
OpCode::TRY_END => {
self.try_stack.pop();
}
OpCode::TRY_FINALLY_BEGIN => {
let offset = opcode::offset16(instr) as isize;
let finally_pc = ((self.pc as isize) + offset - 1) as usize;
self.try_stack.push(TryHandler {
catch_pc: None,
finally_pc: Some(finally_pc),
frame_depth: self.frames.len(),
});
}
OpCode::TRY_FINALLY_END => {
self.try_stack.pop();
if self.pending_exception.is_some() && self.exception_value.is_none() {
self.exception_value = self.pending_exception.take();
match self.unwind() {
Ok(()) => continue,
Err(e) => return Err(e),
}
}
self.pending_exception = None;
}
_ => {
if !op.is_implemented() {
return Ok(JsValue::undefined());
+77
View File
@@ -0,0 +1,77 @@
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 test_throw_caught_by_catch() {
assert_eq!(eval("try { throw 42; } catch(e) { e; }"), "42");
}
#[test]
fn test_throw_without_catch() {
let result = eval("throw 'err';");
assert!(
result.contains("uncaught"),
"expected uncaught, got: {result}"
);
}
#[test]
fn test_try_finally_normal_path() {
assert_eq!(eval("var x = 1; try { x = 2; } finally { x = 3; } x;"), "3");
}
#[test]
fn test_try_finally_exception_path() {
let result = eval("var x = 0; try { throw 1; } finally { x = 2; }");
assert!(
result.contains("uncaught"),
"expected uncaught, got: {result}"
);
}
#[test]
fn test_nested_try_inner_catches() {
assert_eq!(
eval("try { try { throw 1; } catch(e) { e; } } catch(e) { 2; }"),
"1"
);
}
#[test]
fn test_nested_try_outer_catches() {
assert_eq!(
eval("try { try { throw 1; } catch(e) { throw 2; } } catch(e) { e; }"),
"2"
);
}
#[test]
fn test_try_catch_finally() {
assert_eq!(
eval("var x = 0; try { x = 1; } catch(e) { x = 2; } finally { x = 3; } x;"),
"3"
);
}
#[test]
fn test_catch_param_binding() {
assert_eq!(eval("try { throw 99; } catch(myErr) { myErr; }"), "99");
}