diff --git a/crates/oxide_compiler/src/counter.rs b/crates/oxide_compiler/src/counter.rs index 96b94e4..6a91dbb 100644 --- a/crates/oxide_compiler/src/counter.rs +++ b/crates/oxide_compiler/src/counter.rs @@ -206,6 +206,8 @@ impl Compiler { let has_catch = ts.handler.is_some(); let has_finally = ts.finalizer.is_some(); + ctx.alloc_reg(); // result_reg + if has_finally { ctx.projected_pc += 1; // TRY_FINALLY_BEGIN (before try body) } @@ -217,6 +219,7 @@ impl Compiler { for s in &ts.block.body { self.count_statement(s, ctx); } + ctx.projected_pc += 1; // LOAD_VAR result_reg (if try body has result) if has_catch { ctx.projected_pc += 1; // TRY_END @@ -237,6 +240,7 @@ impl Compiler { for cs in &catch.body.body { self.count_statement(cs, ctx); } + ctx.projected_pc += 1; // LOAD_VAR result_reg (if catch body has result) ctx.pop_scope(); } @@ -246,6 +250,7 @@ impl Compiler { for fs in &finally.body { self.count_statement(fs, ctx); } + ctx.projected_pc += 1; // LOAD_VAR result_reg (if finally has result) ctx.projected_pc += 1; // TRY_FINALLY_END } diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index cf1c4e1..3d9422c 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -383,6 +383,7 @@ impl Compiler { let has_catch = ts.handler.is_some(); let has_finally = ts.finalizer.is_some(); + let result_reg = ctx.alloc_reg(); let mut try_finally_begin_pos: Option = None; let mut try_begin_pos: Option = None; @@ -396,9 +397,18 @@ impl Compiler { ctx.emit(opcode::encode_try_begin(0)); // placeholder } + let mut last_try_result: Option = None; for s in &ts.block.body { - self.emit_statement(s, ctx)?; + if let Some(r) = self.emit_statement(s, ctx)? { + last_try_result = Some(r); + } } + ctx.emit(opcode::encode( + OpCode::LOAD_VAR, + result_reg, + last_try_result.unwrap_or(result_reg), + 0, + )); if has_catch { ctx.emit(opcode::encode(OpCode::TRY_END, 0, 0, 0)); @@ -417,7 +427,7 @@ impl Compiler { ctx.label_map.insert(catch_label, catch_label_pc); if let Some(try_begin_pc) = try_begin_pos { - let offset = catch_label_pc as isize - (try_begin_pc as isize + 1); + let offset = catch_label_pc as isize - (try_begin_pc as isize); ctx.bytecode[try_begin_pc] = opcode::encode_try_begin(offset as i16); } @@ -425,16 +435,24 @@ impl Compiler { ctx.push_scope(); if let Some(param) = &catch.param { let catch_reg = ctx.alloc_reg(); - if let oxide_parser::BindingPattern::BindingIdentifier(bi) = - ¶m.pattern + if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern { ctx.declare_initialized(bi.name.as_str(), catch_reg)?; ctx.emit(opcode::encode(OpCode::STORE_VAR, catch_reg, 0, 0)); } } + let mut last_catch_result: Option = None; for s in &catch.body.body { - self.emit_statement(s, ctx)?; + if let Some(r) = self.emit_statement(s, ctx)? { + last_catch_result = Some(r); + } } + ctx.emit(opcode::encode( + OpCode::LOAD_VAR, + result_reg, + last_catch_result.unwrap_or(result_reg), + 0, + )); ctx.pop_scope(); } @@ -442,30 +460,39 @@ impl Compiler { let finally_label = Label::FinallyBody(id); let finally_label_pc = ctx.bytecode.len(); ctx.label_map.insert(finally_label, finally_label_pc); - if let Some(fb_pos) = try_finally_begin_pos { - let offset = finally_label_pc as isize - (fb_pos as isize + 1); - ctx.bytecode[fb_pos] = opcode::encode_try_finally_begin(offset as i16); + let offset = finally_label_pc as isize - (fb_pos as isize); + ctx.bytecode[fb_pos] = + opcode::encode_try_finally_begin(offset as i16); } if let Some(jmp_pos) = jmp_skip_pos { - let offset = finally_label_pc as isize - (jmp_pos as isize + 1); + let offset = finally_label_pc as isize - (jmp_pos as isize); ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset as i16); } + let mut last_finally_result: Option = None; for s in &ts.finalizer.as_ref().unwrap().body { - self.emit_statement(s, ctx)?; + if let Some(r) = self.emit_statement(s, ctx)? { + last_finally_result = Some(r); + } } + ctx.emit(opcode::encode( + OpCode::LOAD_VAR, + result_reg, + last_finally_result.unwrap_or(result_reg), + 0, + )); ctx.emit(opcode::encode(OpCode::TRY_FINALLY_END, 0, 0, 0)); } else if let Some(jmp_pos) = jmp_skip_pos { - let offset = ctx.bytecode.len() as isize - (jmp_pos as isize + 1); + let offset = ctx.bytecode.len() as isize - (jmp_pos as isize); ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset as i16); } let try_end_pc = ctx.bytecode.len(); ctx.label_map.insert(try_end_label, try_end_pc); - Ok(None) + Ok(Some(result_reg)) } _ => Ok(None), } diff --git a/crates/oxide_compiler/src/hash.rs b/crates/oxide_compiler/src/hash.rs index 4c0e561..a088464 100644 --- a/crates/oxide_compiler/src/hash.rs +++ b/crates/oxide_compiler/src/hash.rs @@ -127,9 +127,7 @@ fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher) { } if let Some(catch) = &ts.handler { if let Some(param) = &catch.param { - if let oxide_parser::BindingPattern::BindingIdentifier(bi) = - ¶m.pattern - { + if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern { bi.name.as_str().hash(h); } } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index d3fde23..c394e63 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -49,6 +49,12 @@ pub struct ForInIter<'bump> { pub index: usize, } +pub struct TryHandler { + pub catch_pc: Option, + pub finally_pc: Option, + pub frame_depth: usize, +} + pub struct Vm { regs: [JsValue; 256], pc: usize, @@ -67,6 +73,10 @@ pub struct Vm { saved_bytecode_stack: Vec>, /// Stack of saved constants for nested bytecode calls saved_constants_stack: Vec>, + try_stack: Vec, + exception_value: Option, + pending_exception: Option, + pending_error_kind: Option<&'static str>, } fn bind_object(kernel: &Arc, 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 { 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()); diff --git a/crates/oxide_vm/tests/vm_tests.rs b/crates/oxide_vm/tests/vm_tests.rs new file mode 100644 index 0000000..8c183fc --- /dev/null +++ b/crates/oxide_vm/tests/vm_tests.rs @@ -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"); +}