diff --git a/laythe_core/src/object/fiber/exception_handler.rs b/laythe_core/src/object/fiber/exception_handler.rs index f28798d2..fbbf89df 100644 --- a/laythe_core/src/object/fiber/exception_handler.rs +++ b/laythe_core/src/object/fiber/exception_handler.rs @@ -4,7 +4,7 @@ pub struct ExceptionHandler { /// The ip offset from the start call frame offset: usize, - /// The calldepth of this handler + /// The call depth of this handler call_frame_depth: usize, /// The slot depth at this handler diff --git a/laythe_core/src/object/fiber/mod.rs b/laythe_core/src/object/fiber/mod.rs index 0770a601..b35a2e47 100644 --- a/laythe_core/src/object/fiber/mod.rs +++ b/laythe_core/src/object/fiber/mod.rs @@ -2,7 +2,7 @@ mod call_frame; mod exception_handler; use self::{call_frame::CallFrame, exception_handler::ExceptionHandler}; -use super::{Channel, Fun, ObjectKind}; +use super::{Channel, Fun, List, ObjectKind}; use crate::{ captures::Captures, constants::SCRIPT, @@ -16,7 +16,7 @@ use std::{fmt, io::Write, mem, ptr}; const INITIAL_FRAME_SIZE: usize = 4; #[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum FiberState { +enum FiberState { Running, Unwinding, Pending, @@ -38,6 +38,24 @@ pub enum UnwindResult<'a> { UnwindStopped, } +fn frame_line(fun: GcObj, offset: usize) -> String { + match &*fun.name() { + SCRIPT => format!( + "{}:{} in script", + fun.module().path(), + fun.chunk().get_line(offset) + ) + .to_string(), + _ => format!( + "{}:{} in {}()", + fun.module().path(), + fun.chunk().get_line(offset), + fun.name() + ) + .to_string(), + } +} + pub struct Fiber { /// A stack holding all local variable currently in use stack: Vec, @@ -63,8 +81,11 @@ pub struct Fiber { /// The current state of this fiber state: FiberState, - /// The current error if one is active + /// The current error context if one is active error: Option, + + /// Backtrace's instruction pointers used during an unwind + backtrace_ips: Vec<*const u8>, } impl Fiber { @@ -113,6 +134,7 @@ impl Fiber { state: FiberState::Pending, error: None, frame: current_frame, + backtrace_ips: vec![], stack_top, }) } @@ -168,11 +190,38 @@ impl Fiber { self.state = FiberState::Running; } + /// When an error occurs while handling an exception as in + /// there is an error with the handler itself we need to + /// readjust the backtrace to point back to the current + /// instruction pointers + pub fn error_while_handling(&mut self) { + self.pop_exception_handler(); + self.backtrace_ips.clear(); + } + /// pause unwind to search for handler - #[inline] - pub fn pause_unwind(&mut self) { - assert_eq!(self.state, FiberState::Running); + fn pause_unwind(&mut self, new_frame_top: usize) { + assert!(matches!( + self.state, + FiberState::Running | FiberState::Unwinding + )); + self.state = FiberState::Unwinding; + + let current_len = self.backtrace_ips.len(); + let additional_len = ((self.frames().len() - new_frame_top) + 1) - current_len; + + // extend backtraces instruction pointers + self.backtrace_ips.extend( + self + .frames() + .iter() + .rev() + .skip(current_len) + .take(additional_len) + .map(|frame| frame.ip()) + .collect::>(), + ); } /// Put this fiber to sleep @@ -236,7 +285,7 @@ impl Fiber { .unwrap_or(None) } - /// A a channel to the list of used channels by + /// Add a channel to the list of used channels by /// this fiber pub fn add_used_channel(&mut self, channel: GcObj) { if !self.channels.iter().any(|c| *c == channel) { @@ -315,19 +364,6 @@ impl Fiber { self.set_val(distance + 1, val) } - /// Put the fiber back into an activated state after an unwind begins - /// This truncates off the frames that may still be dangling - pub fn finish_unwind(&mut self) { - // Now that we've found a handler we truncate the frames - let handler = self - .exception_handler() - .expect("Expected handler to still exist"); - self.frames.truncate(handler.call_frame_depth()); - - // Put the fiber back into an activated state - self.activate(); - } - /// Push a new exception handler onto this fiber pub fn push_exception_handler(&mut self, offset: usize, slot_depth: usize) { debug_assert!( @@ -346,7 +382,7 @@ impl Fiber { .push(ExceptionHandler::new(offset, call_frame_depth, slot_depth)); } - /// Push a new exception handler onto this fiber + /// Pop an exception handler off this fiber pub fn pop_exception_handler(&mut self) { assert!( self.exception_handlers.pop().is_some(), @@ -355,7 +391,7 @@ impl Fiber { } /// Do we currently have an active exception handler - pub fn exception_handler(&self) -> Option { + fn exception_handler(&self) -> Option { self.exception_handlers.last().copied() } @@ -415,7 +451,7 @@ impl Fiber { self.error } - /// Set the current error on this fiber + /// Set the current error on this fiber and ip it occurred at pub fn set_error(&mut self, error: Instance) { self.error = Some(error); } @@ -553,40 +589,84 @@ impl Fiber { /// Assumes the function try block slot offset points to a valid offset into the /// associated call frames slots pub unsafe fn stack_unwind(&mut self, bottom_frame: Option) -> UnwindResult { - match self.exception_handler() { + // grab the appropriate exception handler + let exception_handler = match self.exception_handler() { Some(exception_handler) => { let bottom_frame = bottom_frame.unwrap_or(0); if exception_handler.call_frame_depth() >= bottom_frame { - // put the fiber in a state to search for handle - self.pause_unwind(); + exception_handler + } else { + return UnwindResult::UnwindStopped; + } + }, + None => { + return match bottom_frame { + Some(_) => UnwindResult::UnwindStopped, + None => UnwindResult::Unhandled, + } + }, + }; - debug_assert!( - self.frames.len() >= exception_handler.call_frame_depth(), - "Exception handler points to non existing stack frame" - ); + debug_assert!( + self.frames.len() >= exception_handler.call_frame_depth(), + "Exception handler points to non existing stack frame" + ); - // set the current frame - let frame = &mut self.frames[exception_handler.call_frame_depth() - 1]; - let fun = frame.fun(); - let instructions = fun.chunk().instructions(); + // put the fiber in a state to search for handle + self.pause_unwind(exception_handler.call_frame_depth()); - let stack_top = frame.stack_start().add(exception_handler.slot_depth()); + // set the current frame based on the exception handler offset + let frame = &mut self.frames[exception_handler.call_frame_depth() - 1]; + let fun = frame.fun(); + let instructions = fun.chunk().instructions(); - // set the current ip frame and stack pointer - frame.store_ip(&instructions[exception_handler.offset()] as *const u8); - self.frame = frame as *mut CallFrame; - self.stack_top = stack_top; + let stack_top = frame.stack_start().add(exception_handler.slot_depth()); - UnwindResult::PotentiallyHandled(frame) - } else { - UnwindResult::UnwindStopped - } - }, - None => match bottom_frame { - Some(_) => UnwindResult::UnwindStopped, - None => UnwindResult::Unhandled, - }, - } + // set the current ip frame and stack pointer + frame.store_ip(&instructions[exception_handler.offset()] as *const u8); + self.frame = frame as *mut CallFrame; + self.stack_top = stack_top; + + UnwindResult::PotentiallyHandled(frame) + } + + /// Signals to the fiber to finish unwinding. The fiber is + /// moved back into the activated state. The unwound back trace + /// is returned and then truncated off the fiber + pub fn finish_unwind(&mut self) -> List { + // Now that we've found a handler we truncate the frames + let handler = self + .exception_handler() + .expect("Expected handler to still exist"); + + // Gather backtrace and set it on the error + let backtrace = self.error_backtrace(&handler); + self.frames.truncate(handler.call_frame_depth()); + self.backtrace_ips.clear(); + + // Put the fiber back into an activated state + self.activate(); + + backtrace + } + + // Create the string representation of the backtrace + fn error_backtrace(&self, handler: &ExceptionHandler) -> List { + let backtrace_len = (self.frames.len() - handler.call_frame_depth()) + 1; + + self + .frames + .iter() + .rev() + .take(backtrace_len) + .zip(self.backtrace_ips.iter()) + .map(|(frame, ip)| { + let fun = frame.fun(); + let offset = unsafe { ip.offset_from(fun.chunk().instructions().as_ptr()) } as usize; + + frame_line(fun, offset.saturating_sub(1)) + }) + .collect::>() } /// Print a error message with the associated stack track if found @@ -605,7 +685,7 @@ impl Fiber { log, " {}:{} in {}", fun.module().path(), - fun.chunk().get_line(offset), + fun.chunk().get_line(offset.saturating_sub(1)), location ) .expect("Unable to write to stderr"); @@ -726,7 +806,7 @@ impl Trace for Fiber { self.frames.iter().for_each(|call_frame| call_frame.trace()); - if let Some(error) = self.error { + if let Some(error) = &self.error { error.trace(); } } @@ -751,7 +831,7 @@ impl Trace for Fiber { .iter() .for_each(|call_frame| call_frame.trace_debug(log)); - if let Some(error) = self.error { + if let Some(error) = &self.error { error.trace_debug(log); } } @@ -1123,6 +1203,93 @@ mod test { fiber.pop_exception_handler(); } + #[test] + fn error_while_handling() { + let context = NoContext::default(); + let hooks = GcHooks::new(&context); + + let module1 = test_module(&hooks, "first module"); + let module2 = test_module(&hooks, "second module"); + + let mut builder1 = FunBuilder::new(hooks.manage_str("first"), module1, Arity::default()); + let mut builder2 = FunBuilder::new(hooks.manage_str("second"), module2, Arity::default()); + builder1.update_max_slots(4); + builder2.update_max_slots(3); + + let chunk1 = Chunk::new( + hooks.manage::<_, &[u8]>(&[20, 21, 22]), + hooks.manage::<_, &[Value]>(&[]), + hooks.manage::<_, &[u16]>(&[5, 6, 7]), + ); + let chunk2 = Chunk::new( + hooks.manage::<_, &[u8]>(&[40, 41, 42]), + hooks.manage::<_, &[Value]>(&[]), + hooks.manage::<_, &[u16]>(&[2, 3, 4]), + ); + + let fun1 = hooks.manage_obj(builder1.build(chunk1)); + let fun2 = hooks.manage_obj(builder2.build(chunk2)); + + let captures = Captures::new(&hooks, &[]); + + let mut fiber = FiberBuilder::default() + .max_slots(6) + .instructions(vec![0, 0, 0]) + .build(&hooks) + .expect("Expected to build"); + + let base_fun = fiber.frames()[0].fun(); + + unsafe { + fiber.push(VALUE_NIL); + fiber.push(val!(fun1)); + fiber.push(val!(10.0)); + fiber.push(val!(true)); + fiber.push(val!(fun2)); + fiber.push(val!(5.0)); + } + + fiber.push_exception_handler(2, 2); + fiber.push_frame(fun1, captures, 2); + fiber.store_ip(&fun1.chunk().instructions()[0]); + + fiber.push_exception_handler(1, 1); + fiber.push_frame(fun2, captures, 1); + + fiber.activate(); + fiber.store_ip(&fun2.chunk().instructions()[2]); + + unsafe { + match fiber.stack_unwind(None) { + UnwindResult::PotentiallyHandled(frame) => { + assert_eq!(frame.fun(), fun1); + assert_eq!(frame.captures(), captures); + }, + _ => assert!(false), + } + } + + assert_eq!(fiber.frames().len(), 3); + + // simulate an error handler instruction going bad + fiber.store_ip(&fun1.chunk().instructions()[2]); + fiber.error_while_handling(); + + unsafe { + match fiber.stack_unwind(None) { + UnwindResult::PotentiallyHandled(frame) => { + assert_eq!(frame.fun(), base_fun); + }, + _ => assert!(false), + } + } + let backtrace = fiber.finish_unwind(); + assert_eq!(backtrace.len(), 3); + assert_eq!(backtrace[0], "example:3 in second()"); + assert_eq!(backtrace[1], "example:6 in first()"); + assert_eq!(backtrace[2], "example:0 in script"); + } + #[test] fn is_complete() { let context = NoContext::default(); @@ -1478,14 +1645,26 @@ mod test { let context = NoContext::default(); let hooks = GcHooks::new(&context); - let module = test_module(&hooks, "first module"); - let mut builder = FunBuilder::new(hooks.manage_str("first"), module, Arity::default()); - builder.update_max_slots(4); + let module1 = test_module(&hooks, "first module"); + let module2 = test_module(&hooks, "second module"); - let chunk = Chunk::stub_with_instructions(&hooks, &[0, 0, 0]); + let mut builder1 = FunBuilder::new(hooks.manage_str("first"), module1, Arity::default()); + let builder2 = FunBuilder::new(hooks.manage_str("second"), module2, Arity::default()); + builder1.update_max_slots(4); - let fun1 = hooks.manage_obj(builder.build(chunk)); - let fun2 = test_fun(&hooks, "second", "second module"); + let chunk1 = Chunk::new( + hooks.manage::<_, &[u8]>(&[0, 0, 0]), + hooks.manage::<_, &[Value]>(&[]), + hooks.manage::<_, &[u16]>(&[0, 0, 1]), + ); + let chunk2 = Chunk::new( + hooks.manage::<_, &[u8]>(&[0, 0, 0]), + hooks.manage::<_, &[Value]>(&[]), + hooks.manage::<_, &[u16]>(&[0, 1, 1]), + ); + + let fun1 = hooks.manage_obj(builder1.build(chunk1)); + let fun2 = hooks.manage_obj(builder2.build(chunk2)); let captures = Captures::new(&hooks, &[]); @@ -1505,10 +1684,12 @@ mod test { fiber.push_frame(fun1, captures, 2); fiber.push_exception_handler(2, 2); + fiber.store_ip(&fun1.chunk().instructions()[1]); fiber.push_frame(fun2, captures, 1); fiber.activate(); + fiber.store_ip(&fun2.chunk().instructions()[2]); unsafe { match fiber.stack_unwind(None) { @@ -1524,7 +1705,11 @@ mod test { assert_eq!(fiber.frame().fun(), fun1); assert_eq!(fiber.frame().captures(), captures); - fiber.finish_unwind(); + let backtrace = fiber.finish_unwind(); + assert_eq!(backtrace.len(), 2); + assert_eq!(backtrace[0], "example:1 in second()"); + assert_eq!(backtrace[1], "example:0 in first()"); + assert_eq!(fiber.frames().len(), 2); assert_eq!(fiber.frame().fun(), fun1); assert_eq!(fiber.frame().captures(), captures); diff --git a/laythe_core/src/object/mod.rs b/laythe_core/src/object/mod.rs index e6e52576..346e7491 100644 --- a/laythe_core/src/object/mod.rs +++ b/laythe_core/src/object/mod.rs @@ -15,7 +15,7 @@ pub use channel::{Channel, CloseResult, ReceiveResult, SendResult}; pub use class::Class; pub use closure::Closure; pub use enumerator::{Enumerate, Enumerator}; -pub use fiber::{Fiber, FiberResult, FiberState, UnwindResult}; +pub use fiber::{Fiber, FiberResult, UnwindResult}; pub use fun::{Fun, FunBuilder, FunKind}; // pub use instance::Instance; pub use list::List; diff --git a/laythe_core/src/support/mod.rs b/laythe_core/src/support/mod.rs index 9af374af..ef31771d 100644 --- a/laythe_core/src/support/mod.rs +++ b/laythe_core/src/support/mod.rs @@ -2,14 +2,7 @@ use fnv::FnvBuildHasher; use hashbrown::HashMap; use crate::{ - captures::Captures, - chunk::{Chunk}, - hooks::GcHooks, - managed::{Gc, GcObj, GcStr}, - module::{module_class, Module}, - object::{Class, Fiber, FiberResult, Fun, FunBuilder}, - signature::Arity, - value::Value, + captures::Captures, chunk::Chunk, constants::SCRIPT, hooks::GcHooks, managed::{Gc, GcObj, GcStr}, module::{module_class, Module}, object::{Class, Fiber, FiberResult, Fun, FunBuilder}, signature::Arity, value::Value }; pub struct FiberBuilder { @@ -23,7 +16,7 @@ pub struct FiberBuilder { impl Default for FiberBuilder { fn default() -> Self { Self { - name: "Fiber".to_string(), + name: SCRIPT.to_string(), parent: None, module_name: "Fiber Module".to_string(), instructions: vec![0], diff --git a/laythe_core/src/value.rs b/laythe_core/src/value.rs index 6d0af13e..ca29a542 100644 --- a/laythe_core/src/value.rs +++ b/laythe_core/src/value.rs @@ -785,7 +785,7 @@ mod boxed { assert_eq!(mem::size_of::>(), 32); assert_eq!(mem::size_of::(), 16); assert_eq!(mem::size_of::(), 56); - assert_eq!(mem::size_of::(), 136); + assert_eq!(mem::size_of::(), 160); assert_eq!(mem::size_of::(), 104); assert_eq!(mem::size_of::(), 16); assert_eq!(mem::size_of::(), 24); diff --git a/laythe_lib/src/global/assert/mod.rs b/laythe_lib/src/global/assert/mod.rs index 79569332..cb69b12e 100644 --- a/laythe_lib/src/global/assert/mod.rs +++ b/laythe_lib/src/global/assert/mod.rs @@ -108,7 +108,7 @@ impl LyNative for Assert { create_error!( self.error, hooks, - "Assertion failed expected true received false" + "Expected assertion to return true." ) } } @@ -152,7 +152,7 @@ impl LyNative for AssertEq { create_error!( self.error, hooks, - format!("Assertion failed {arg0} and {arg1} are not equal.") + format!("Expected {arg0:?} to equal {arg1:?}.") ) } } @@ -197,7 +197,7 @@ impl LyNative for AssertNe { create_error!( self.error, hooks, - format!("Assertion failed {arg0} and {arg1} are equal.") + format!("Expected {arg0} not to equal {arg1}.") ) } } diff --git a/laythe_vm/fixture/language/exception/catch_not_class.lay b/laythe_vm/fixture/language/exception/catch_not_class.lay new file mode 100644 index 00000000..0d75ee2a --- /dev/null +++ b/laythe_vm/fixture/language/exception/catch_not_class.lay @@ -0,0 +1,15 @@ +let FakeError = "Something Else"; + +try { + try { + raise Error("test"); + } catch e: FakeError { + } + assert(false); +} catch e: TypeError { + assertEq(e.message, "Catch block must be blank or a subclass of Error."); + assertEq(e.backTrace.len(), 1); + assert(e.backTrace[0].has("catch_not_class.lay:6 in script")); +} catch e { + assert(false); +} diff --git a/laythe_vm/fixture/language/exception/catch_not_error_class.lay b/laythe_vm/fixture/language/exception/catch_not_error_class.lay new file mode 100644 index 00000000..796e97ec --- /dev/null +++ b/laythe_vm/fixture/language/exception/catch_not_error_class.lay @@ -0,0 +1,15 @@ +class FakeError {} + +try { + try { + raise Error("test"); + } catch e: FakeError { + } + assert(false); +} catch e: TypeError { + assertEq(e.message, "Catch block must be blank or a subclass of Error."); + assertEq(e.backTrace.len(), 1); + assert(e.backTrace[0].has("catch_not_error_class.lay:6 in script")); +} catch e { + assert(false); +} diff --git a/laythe_vm/fixture/language/exception/catch_not_identifier.lay b/laythe_vm/fixture/language/exception/catch_not_identifier.lay new file mode 100644 index 00000000..f1cee813 --- /dev/null +++ b/laythe_vm/fixture/language/exception/catch_not_identifier.lay @@ -0,0 +1,4 @@ + try { + raise Error("test"); + } catch e: 10 { + } \ No newline at end of file diff --git a/laythe_vm/fixture/language/exception/nested_break.lay b/laythe_vm/fixture/language/exception/nested_break.lay index 79ae388b..f38dc47d 100644 --- a/laythe_vm/fixture/language/exception/nested_break.lay +++ b/laythe_vm/fixture/language/exception/nested_break.lay @@ -19,8 +19,13 @@ let caught = false; try { notThrower(); raise Error("boom"); + assert(false); } catch e: Error { caught = true; + assertEq(e.message, "boom"); + assertEq(e.backTrace.len(), 1); + assert(e.backTrace[0].has("nested_break.lay:21 in script")); } assert(caught); + diff --git a/laythe_vm/fixture/language/exception/nested_continue.lay b/laythe_vm/fixture/language/exception/nested_continue.lay index 5629089c..dc933e03 100644 --- a/laythe_vm/fixture/language/exception/nested_continue.lay +++ b/laythe_vm/fixture/language/exception/nested_continue.lay @@ -19,8 +19,13 @@ let caught = false; try { notThrower(); raise Error("boom"); + assert(false); } catch e { caught = true; + assertEq(e.message, "boom"); + assertEq(e.backTrace.len(), 1); + assert(e.backTrace[0].has("nested_continue.lay:21 in script")); } assert(caught); + diff --git a/laythe_vm/fixture/language/exception/nested_return.lay b/laythe_vm/fixture/language/exception/nested_return.lay index 40943e72..60414d01 100644 --- a/laythe_vm/fixture/language/exception/nested_return.lay +++ b/laythe_vm/fixture/language/exception/nested_return.lay @@ -17,8 +17,12 @@ let caught = false; try { notThrower(); raise Error("boom"); + assert(false); } catch e: Error { caught = true; + assertEq(e.message, "boom"); + assertEq(e.backTrace.len(), 1); + assert(e.backTrace[0].has("nested_return.lay:19 in script")); } -assert(caught); +assert(true); diff --git a/laythe_vm/fixture/language/exception/not_error_catch_not_tested.lay b/laythe_vm/fixture/language/exception/not_error_catch_not_tested.lay new file mode 100644 index 00000000..5ec27019 --- /dev/null +++ b/laythe_vm/fixture/language/exception/not_error_catch_not_tested.lay @@ -0,0 +1,8 @@ +let FakeError = "Something Else"; + +try { + // Highlights that this is currently a + // runtime check +} catch e: FakeError { + assert(false); +} \ No newline at end of file diff --git a/laythe_vm/fixture/language/exception/one_deep_catch.lay b/laythe_vm/fixture/language/exception/one_deep_catch.lay index 11f48d7e..84736781 100644 --- a/laythe_vm/fixture/language/exception/one_deep_catch.lay +++ b/laythe_vm/fixture/language/exception/one_deep_catch.lay @@ -6,5 +6,8 @@ try { raiser(); assert(false); } catch e { - assert(true); + assertEq(e.message, "raise"); + assertEq(e.backTrace.len(), 2); + assert(e.backTrace[0].has("one_deep_catch.lay:2 in raiser()")); + assert(e.backTrace[1].has("one_deep_catch.lay:6 in script")); } \ No newline at end of file diff --git a/laythe_vm/fixture/language/exception/top_level_catch.lay b/laythe_vm/fixture/language/exception/top_level_catch.lay index 76afa1fe..0647d22f 100644 --- a/laythe_vm/fixture/language/exception/top_level_catch.lay +++ b/laythe_vm/fixture/language/exception/top_level_catch.lay @@ -2,5 +2,7 @@ try { raise Error("raise"); assert(false); } catch e { - + assertEq(e.message, "raise"); + assertEq(e.backTrace.len(), 1); + assert(e.backTrace[0].has("top_level_catch.lay:2 in script")); } \ No newline at end of file diff --git a/laythe_vm/fixture/language/exception/top_level_catch_raise.lay b/laythe_vm/fixture/language/exception/top_level_catch_raise.lay index aca659a1..abdb0579 100644 --- a/laythe_vm/fixture/language/exception/top_level_catch_raise.lay +++ b/laythe_vm/fixture/language/exception/top_level_catch_raise.lay @@ -2,10 +2,16 @@ try { raise Error("raise"); assert(false); } catch e: Error { + assertEq(e.message, "raise"); + assertEq(e.backTrace.len(), 1); + assert(e.backTrace[0].has("top_level_catch_raise.lay:2 in script")); + try { raise Error("raise"); assert(false); } catch e: Error { - assert(true); + assertEq(e.message, "raise"); + assertEq(e.backTrace.len(), 1); + assert(e.backTrace[0].has("top_level_catch_raise.lay:10 in script")); } } \ No newline at end of file diff --git a/laythe_vm/fixture/language/exception/try_error_in_catch.lay b/laythe_vm/fixture/language/exception/try_error_in_catch.lay new file mode 100644 index 00000000..f98fb702 --- /dev/null +++ b/laythe_vm/fixture/language/exception/try_error_in_catch.lay @@ -0,0 +1,18 @@ +try { + try { + raise Error("test"); + } catch e: Error { + raise Error("inner test", e); + } + + assert(false); +} catch e: Error { + assertEq(e.message, "inner test"); + assertEq(e.backTrace.len(), 1); + assert(e.backTrace[0].has("try_error_in_catch.lay:5 in script")); + + let inner = e.inner; + assertEq(inner.message, "test"); + assertEq(inner.backTrace.len(), 1); + assert(inner.backTrace[0].has("try_error_in_catch.lay:3 in script")); +} \ No newline at end of file diff --git a/laythe_vm/fixture/language/exception/try_fall_through.lay b/laythe_vm/fixture/language/exception/try_fall_through.lay new file mode 100644 index 00000000..f0d59b3e --- /dev/null +++ b/laythe_vm/fixture/language/exception/try_fall_through.lay @@ -0,0 +1,14 @@ +try { + try { + raise Error("test"); + } catch e: RuntimeError { + assert(false); + } catch e: SyntaxError { + assert(false); + } +} catch e: Error { + print(e.backTrace); + assertEq(e.message, "test"); + assertEq(e.backTrace.len(), 1); + assert(e.backTrace[0].has("try_fall_through.lay:3 in script")); +} \ No newline at end of file diff --git a/laythe_vm/fixture/language/exception/two_deep_catch.lay b/laythe_vm/fixture/language/exception/two_deep_catch.lay index b7911e21..e07e04be 100644 --- a/laythe_vm/fixture/language/exception/two_deep_catch.lay +++ b/laythe_vm/fixture/language/exception/two_deep_catch.lay @@ -10,5 +10,9 @@ try { outer(); assert(false); } catch e: Error { - assert(true); + assertEq(e.message, "raise"); + assertEq(e.backTrace.len(), 3); + assert(e.backTrace[0].has("two_deep_catch.lay:6 in raiser()")); + assert(e.backTrace[1].has("two_deep_catch.lay:2 in outer()")); + assert(e.backTrace[2].has("two_deep_catch.lay:10 in script")); } \ No newline at end of file diff --git a/laythe_vm/fixture/language/raise/raise_error.lay b/laythe_vm/fixture/language/raise/raise_error.lay index aee1d7d2..9ce833c3 100644 --- a/laythe_vm/fixture/language/raise/raise_error.lay +++ b/laythe_vm/fixture/language/raise/raise_error.lay @@ -1,6 +1,6 @@ try { raise RuntimeError("example"); assert(false); -} catch _ { - assert(true); +} catch e: RuntimeError { + assertEq(e.message, "example"); } \ No newline at end of file diff --git a/laythe_vm/src/byte_code.rs b/laythe_vm/src/byte_code.rs index 25dae34b..3ad331cb 100644 --- a/laythe_vm/src/byte_code.rs +++ b/laythe_vm/src/byte_code.rs @@ -187,9 +187,18 @@ pub enum SymbolicByteCode { /// Push an exception handler onto the fiber PushHandler((u16, Label)), + /// Check if this handler is appropriate + CheckHandler(Label), + + /// Load the current error into the stack + GetError, + /// Indicate we're done unwinding FinishUnwind, + /// We did not find an appropriate handler so continue unwinding + ContinueUnwind, + /// Pop an exception handler off the fiber PopHandler, @@ -313,8 +322,11 @@ impl SymbolicByteCode { Self::Jump(_) => 3, Self::Loop(_) => 3, Self::PushHandler(_) => 5, + Self::CheckHandler(_) => 3, Self::PopHandler => 1, Self::FinishUnwind => 1, + Self::ContinueUnwind => 1, + Self::GetError => 1, Self::Raise => 1, Self::Call(_) => 2, Self::Invoke((_, _)) => 4, @@ -392,8 +404,11 @@ impl SymbolicByteCode { Self::Jump(_) => 0, Self::Loop(_) => 0, Self::PushHandler(_) => 0, + Self::CheckHandler(_) => -1, Self::PopHandler => 0, Self::FinishUnwind => 0, + Self::ContinueUnwind => 0, + Self::GetError => 1, Self::Raise => -1, Self::Call(args) => -(*args as i32), Self::Invoke((_, args)) => -(*args as i32), @@ -528,8 +543,14 @@ impl<'a> ByteCodeEncoder<'a> { self.push_op_u16_tuple(ByteCode::PushHandler, *line, *slots, (jump) as u16); self.jump_error(jump) }, + SymbolicByteCode::CheckHandler(target) => { + let jump = label_offsets[target.0 as usize] - offset - 3; + self.op_jump(ByteCode::CheckHandler, *line, jump) + }, SymbolicByteCode::PopHandler => self.op(ByteCode::PopHandler, *line), SymbolicByteCode::FinishUnwind => self.op(ByteCode::FinishUnwind, *line), + SymbolicByteCode::ContinueUnwind => self.op(ByteCode::ContinueUnwind, *line), + SymbolicByteCode::GetError => self.op(ByteCode::GetError, *line), SymbolicByteCode::Raise => self.op(ByteCode::Raise, *line), SymbolicByteCode::Call(slot) => self.op_byte(ByteCode::Call, *line, *slot), SymbolicByteCode::Invoke((slot1, slot2)) => { @@ -546,12 +567,6 @@ impl<'a> ByteCodeEncoder<'a> { SymbolicByteCode::Inherit => self.op(ByteCode::Inherit, *line), SymbolicByteCode::GetSuper(slot) => self.op_short(ByteCode::GetSuper, *line, *slot), SymbolicByteCode::CaptureIndex(index) => self.op_capture(*index, *line), - - // { - // let encoded: u16 = unsafe { mem::transmute(*index) }; - // let bytes = encoded.to_ne_bytes(); - // self.encoded_code.extend_from_slice(&bytes); - // }, SymbolicByteCode::PropertySlot => self.op_property_slot(*line), SymbolicByteCode::InvokeSlot => self.op_invoke_slot(*line), SymbolicByteCode::Label(_) => (), @@ -830,9 +845,18 @@ pub enum ByteCode { /// Push an exception handler onto the fiber PushHandler, + /// Check if this handler is appropriate + CheckHandler, + + /// Load the current error into the stack + GetError, + /// Indicate we're done unwinding FinishUnwind, + /// We did not find an appropriate handler so continue unwinding + ContinueUnwind, + /// Raise an value Raise, @@ -1063,15 +1087,24 @@ pub enum AlignedByteCode { /// Push an exception handler onto the fiber PushHandler((u16, u16)), + /// Check if this handler is appropriate + CheckHandler(u16), + /// Pop an exception handler off the fiber PopHandler, + /// Load the current error into the stack + GetError, + /// Raise an value Raise, /// Indicate we're done unwinding FinishUnwind, + /// We did not find an appropriate handler so continue unwinding + ContinueUnwind, + /// Call a function Call(u8), @@ -1255,7 +1288,13 @@ impl AlignedByteCode { offset + 5, ), ByteCode::PopHandler => (Self::PopHandler, offset + 1), + ByteCode::CheckHandler => ( + Self::CheckHandler(decode_u16(&store[offset + 1..offset + 3])), + offset + 3, + ), ByteCode::FinishUnwind => (Self::FinishUnwind, offset + 1), + ByteCode::ContinueUnwind => (Self::ContinueUnwind, offset + 1), + ByteCode::GetError => (Self::GetError, offset + 1), ByteCode::Raise => (Self::Raise, offset + 1), ByteCode::Call => (Self::Call(store[offset + 1]), offset + 2), ByteCode::Invoke => ( @@ -1368,6 +1407,12 @@ mod test { (2, SymbolicByteCode::Box(66)), (1, SymbolicByteCode::EmptyBox), (1, SymbolicByteCode::FillBox), + (5, SymbolicByteCode::PushHandler((8888, Label::new(1)))), + (1, SymbolicByteCode::PopHandler), + (1, SymbolicByteCode::ContinueUnwind), + (1, SymbolicByteCode::FinishUnwind), + (1, SymbolicByteCode::GetError), + (3, SymbolicByteCode::CheckHandler(Label::new(1))), (3, SymbolicByteCode::Interpolate(3389)), (3, SymbolicByteCode::IterNext(81)), (3, SymbolicByteCode::IterCurrent(49882)), diff --git a/laythe_vm/src/compiler/ir/ast.rs b/laythe_vm/src/compiler/ir/ast.rs index 1679bbc9..c15b3c9f 100644 --- a/laythe_vm/src/compiler/ir/ast.rs +++ b/laythe_vm/src/compiler/ir/ast.rs @@ -478,7 +478,7 @@ impl<'a> Spanned for If<'a> { fn end(&self) -> u32 { self.else_.as_ref().map_or_else( - || self.cond.end(), + || self.body.end(), |else_| match else_ { Else::If(if_) => if_.end(), Else::Block(block) => block.end(), diff --git a/laythe_vm/src/compiler/mod.rs b/laythe_vm/src/compiler/mod.rs index a6ac24da..f49b8cc8 100644 --- a/laythe_vm/src/compiler/mod.rs +++ b/laythe_vm/src/compiler/mod.rs @@ -6,6 +6,7 @@ mod resolver; mod scanner; use bumpalo::{collections, Bump}; +use laythe_lib::global::ERROR_CLASS_NAME; pub use parser::Parser; pub use resolver::Resolver; @@ -196,9 +197,6 @@ pub struct Compiler<'a, 'src> { /// Are we currently compile for a repl repl: bool, - /// The current number of slots in use, - slots: i32, - /// The local variables currently in scope locals: collections::Vec<'a, Local<'a>>, @@ -276,7 +274,6 @@ impl<'a, 'src: 'a> Compiler<'a, 'src> { errors: vec![], fun_kind: FunKind::Script, scope_depth: 0, - slots: 1, repl, class_attributes: None, loop_attributes: None, @@ -347,7 +344,6 @@ impl<'a, 'src: 'a> Compiler<'a, 'src> { root_trace: enclosing.root_trace, fun_kind, scope_depth: enclosing.scope_depth, - slots: 1, repl: enclosing.repl, class_attributes: enclosing.class_attributes, loop_attributes: None, @@ -481,6 +477,7 @@ impl<'a, 'src: 'a> Compiler<'a, 'src> { self.local_tables.push(table); } + /// Set the global scope fn begin_global_scope(&mut self, table: &'a SymbolTable<'src>) { assert!(self.global_table.is_none()); assert!(self.local_tables.is_empty()); @@ -1469,22 +1466,19 @@ impl<'a, 'src: 'a> Compiler<'a, 'src> { /// Compile a try catch block fn try_(&mut self, try_: &'a ast::Try<'src>) { - if self.slots as usize > u16::MAX as usize { - self.error("Stack too deep for exception catch.", None); - } - // set this try block as the current let try_attributes = TryAttributes { scope_depth: self.scope_depth, }; let enclosing_try = mem::replace(&mut self.try_attributes, Some(try_attributes)); - let slots = self.slots as u16; let catch_label = self.label_emitter.emit(); + // We currently use zero as a placeholder as the peephole compiler + // determines the actual value self.emit_byte( - SymbolicByteCode::PushHandler((slots, catch_label)), - try_.block.end(), + SymbolicByteCode::PushHandler((0, catch_label)), + try_.block.start(), ); self.scope(try_.block.end(), &try_.block.symbols, |self_| { @@ -1493,27 +1487,60 @@ impl<'a, 'src: 'a> Compiler<'a, 'src> { self.emit_byte(SymbolicByteCode::PopHandler, try_.block.end()); - let end_label = self.label_emitter.emit(); - self.emit_byte(SymbolicByteCode::Jump(end_label), try_.block.end()); + let try_end_label = self.label_emitter.emit(); + self.emit_byte(SymbolicByteCode::Jump(try_end_label), try_.block.end()); // Temp shim in first catch block only // For now we basically ignore the rest of the catch blocks and don't bind anything to the // provided variable. This was be implemented later let catch = try_.catches.first().expect("Expected catch block"); - self.emit_byte(SymbolicByteCode::Label(catch_label), catch.start()); - - // When we pop a handler in a catch block we signal to the fiber - // we're done unwinding - self.emit_byte(SymbolicByteCode::FinishUnwind, catch.start()); - self.emit_byte(SymbolicByteCode::PopHandler, catch.start()); self.try_attributes = enclosing_try; + for catch in &try_.catches { + self.catch(catch, try_end_label); + } + + self.emit_byte(SymbolicByteCode::ContinueUnwind, catch.end()); + self.emit_byte(SymbolicByteCode::Label(try_end_label), catch.end()); + } + + /// Compile a catch block + fn catch(&mut self, catch: &'a ast::Catch<'src>, try_end_label: Label) { + let default_error = &Token::new( + TokenKind::Identifier, + Lexeme::Slice(ERROR_CLASS_NAME), + catch.name.end(), + catch.name.end(), + ); + + let catch_end_label: Label = self.label_emitter.emit(); self.scope(catch.end(), &catch.symbols, |self_| { - self_.block(&catch.block); + // Load error class onto stack + self_.variable_get(catch.class.as_ref().unwrap_or(default_error)); + self_.emit_byte( + SymbolicByteCode::CheckHandler(catch_end_label), + catch.start(), + ); + + // If we end the catch signal that we are done unwinding + self_.emit_byte(SymbolicByteCode::FinishUnwind, catch.start()); + self_.emit_byte(SymbolicByteCode::PopHandler, catch.start()); + + let var_state = self_.declare_variable(&catch.name, catch.name.end()); + let variable: u16 = self_.identifier_constant(catch.name.str()); + + self_.emit_byte(SymbolicByteCode::GetError, catch.name.start()); + + self_.define_variable(variable, var_state, catch.end()); + + self_.scope(catch.end(), &catch.block.symbols, |self__| { + self__.block(&catch.block); + }); }); - self.emit_byte(SymbolicByteCode::Label(end_label), catch.end()); + self.emit_byte(SymbolicByteCode::Jump(try_end_label), catch.end()); + self.emit_byte(SymbolicByteCode::Label(catch_end_label), catch.end()); } /// Compile a raise statement @@ -2566,9 +2593,15 @@ mod test { &vec![ AlignedByteCode::PushHandler((1, 4)), AlignedByteCode::PopHandler, - AlignedByteCode::Jump(2), + AlignedByteCode::Jump(14), + AlignedByteCode::GetGlobal(0), + AlignedByteCode::CheckHandler(7), AlignedByteCode::FinishUnwind, AlignedByteCode::PopHandler, + AlignedByteCode::GetError, + AlignedByteCode::Drop, + AlignedByteCode::Jump(1), + AlignedByteCode::ContinueUnwind, AlignedByteCode::Nil, AlignedByteCode::Return, ], @@ -2601,13 +2634,18 @@ mod test { AlignedByteCode::Slot(0), AlignedByteCode::DropN(2), AlignedByteCode::PopHandler, - AlignedByteCode::Jump(10), + AlignedByteCode::Jump(22), + AlignedByteCode::GetGlobal(3), + AlignedByteCode::CheckHandler(15), AlignedByteCode::FinishUnwind, AlignedByteCode::PopHandler, - AlignedByteCode::GetGlobal(3), - AlignedByteCode::Constant(4), + AlignedByteCode::GetError, + AlignedByteCode::GetGlobal(5), + AlignedByteCode::Constant(6), AlignedByteCode::Call(1), - AlignedByteCode::Drop, + AlignedByteCode::DropN(2), + AlignedByteCode::Jump(1), + AlignedByteCode::ContinueUnwind, AlignedByteCode::Nil, AlignedByteCode::Return, ], @@ -2634,9 +2672,9 @@ mod test { assert_simple_bytecode( &fun, - 3, + 4, &vec![ - AlignedByteCode::PushHandler((1, 51)), + AlignedByteCode::PushHandler((1, 63)), AlignedByteCode::List(0), AlignedByteCode::Constant(0), AlignedByteCode::Invoke((1, 1)), @@ -2649,21 +2687,31 @@ mod test { AlignedByteCode::Slot(1), AlignedByteCode::Drop, AlignedByteCode::PopHandler, - AlignedByteCode::Jump(10), + AlignedByteCode::Jump(22), + AlignedByteCode::GetGlobal(3), + AlignedByteCode::CheckHandler(15), AlignedByteCode::FinishUnwind, AlignedByteCode::PopHandler, - AlignedByteCode::GetGlobal(3), - AlignedByteCode::Constant(4), + AlignedByteCode::GetError, + AlignedByteCode::GetGlobal(5), + AlignedByteCode::Constant(6), AlignedByteCode::Call(1), - AlignedByteCode::Drop, + AlignedByteCode::DropN(2), + AlignedByteCode::Jump(1), + AlignedByteCode::ContinueUnwind, AlignedByteCode::PopHandler, - AlignedByteCode::Jump(10), + AlignedByteCode::Jump(22), + AlignedByteCode::GetGlobal(3), + AlignedByteCode::CheckHandler(15), AlignedByteCode::FinishUnwind, AlignedByteCode::PopHandler, - AlignedByteCode::GetGlobal(3), - AlignedByteCode::Constant(5), + AlignedByteCode::GetError, + AlignedByteCode::GetGlobal(5), + AlignedByteCode::Constant(7), AlignedByteCode::Call(1), - AlignedByteCode::Drop, + AlignedByteCode::DropN(2), + AlignedByteCode::Jump(1), + AlignedByteCode::ContinueUnwind, AlignedByteCode::Nil, AlignedByteCode::Return, ], @@ -2690,14 +2738,19 @@ mod test { 2, &vec![ AlignedByteCode::True, - AlignedByteCode::JumpIfFalse(17), + AlignedByteCode::JumpIfFalse(26), AlignedByteCode::PushHandler((1, 4)), AlignedByteCode::PopHandler, - AlignedByteCode::Jump(8), + AlignedByteCode::Jump(17), + AlignedByteCode::GetGlobal(0), + AlignedByteCode::CheckHandler(7), AlignedByteCode::FinishUnwind, AlignedByteCode::PopHandler, - AlignedByteCode::Jump(3), - AlignedByteCode::Loop(21), + AlignedByteCode::GetError, + AlignedByteCode::Drop, + AlignedByteCode::Jump(4), + AlignedByteCode::ContinueUnwind, + AlignedByteCode::Loop(30), AlignedByteCode::Nil, AlignedByteCode::Return, ], @@ -2724,14 +2777,19 @@ mod test { 2, &vec![ AlignedByteCode::True, - AlignedByteCode::JumpIfFalse(17), + AlignedByteCode::JumpIfFalse(26), AlignedByteCode::PushHandler((1, 4)), AlignedByteCode::PopHandler, AlignedByteCode::Loop(13), + AlignedByteCode::GetGlobal(0), + AlignedByteCode::CheckHandler(7), AlignedByteCode::FinishUnwind, AlignedByteCode::PopHandler, - AlignedByteCode::Loop(18), - AlignedByteCode::Loop(21), + AlignedByteCode::GetError, + AlignedByteCode::Drop, + AlignedByteCode::Loop(26), + AlignedByteCode::ContinueUnwind, + AlignedByteCode::Loop(30), AlignedByteCode::Nil, AlignedByteCode::Return, ], @@ -2759,16 +2817,20 @@ mod test { ByteCodeTest::Fun(( // example 1, - 2, + 3, vec![ ByteCodeTest::Code(AlignedByteCode::PushHandler((1, 3))), ByteCodeTest::Code(AlignedByteCode::Nil), ByteCodeTest::Code(AlignedByteCode::PopHandler), ByteCodeTest::Code(AlignedByteCode::Return), + ByteCodeTest::Code(AlignedByteCode::GetGlobal(0)), + ByteCodeTest::Code(AlignedByteCode::CheckHandler(5)), ByteCodeTest::Code(AlignedByteCode::FinishUnwind), ByteCodeTest::Code(AlignedByteCode::PopHandler), + ByteCodeTest::Code(AlignedByteCode::GetError), ByteCodeTest::Code(AlignedByteCode::Nil), ByteCodeTest::Code(AlignedByteCode::Return), + ByteCodeTest::Code(AlignedByteCode::ContinueUnwind), ByteCodeTest::Code(AlignedByteCode::Nil), ByteCodeTest::Code(AlignedByteCode::Return), ], diff --git a/laythe_vm/src/compiler/peephole.rs b/laythe_vm/src/compiler/peephole.rs index 29d18303..15814c56 100644 --- a/laythe_vm/src/compiler/peephole.rs +++ b/laythe_vm/src/compiler/peephole.rs @@ -93,10 +93,10 @@ pub fn peephole_compile<'a>( ) -> Result>> { let (instructions, constants, lines) = chunk_builder.take(); - let (instructions, lines) = peephole_optimize(instructions, lines); + let (mut instructions, lines) = peephole_optimize(instructions, lines); let label_count = label_count(&instructions); - apply_stack_effects(&mut fun_builder, &instructions); + apply_stack_effects(&mut fun_builder, &mut instructions); let mut label_offsets: collections::Vec = bumpalo::vec![in alloc; 0; label_count]; @@ -106,7 +106,7 @@ pub fn peephole_compile<'a>( compute_label_offsets(&instructions, &mut label_offsets[..label_count]); - let code_buffer = collections::Vec::with_capacity_in(instructions.len(), alloc); + let code_buffer = collections::Vec::with_capacity_in(instructions.len() * 2, alloc); let line_buffer = collections::Vec::with_capacity_in(instructions.len() * 2, alloc); let errors = collections::Vec::new_in(alloc); @@ -247,10 +247,15 @@ fn label_count(instructions: &[SymbolicByteCode]) -> usize { count } -fn apply_stack_effects(fun_builder: &mut FunBuilder, instructions: &[SymbolicByteCode]) { - let mut slots = 1; +fn apply_stack_effects(fun_builder: &mut FunBuilder, instructions: &mut [SymbolicByteCode]) { + let mut slots: i32 = 1; for instruction in instructions { + if let SymbolicByteCode::PushHandler((_, label)) = instruction { + // TODO handle to many slots + *instruction = SymbolicByteCode::PushHandler((slots as u16, *label)) + } + slots += instruction.stack_effect(); debug_assert!(slots >= 0); fun_builder.update_max_slots(slots); @@ -546,6 +551,70 @@ mod test { } } + mod stack_effects { + use laythe_core::{hooks::NoContext, signature::Arity, support::test_module}; + + use crate::byte_code::Label; + + use super::*; + + #[test] + fn calculate_func_max_stack() { + let context = NoContext::default(); + let hooks = GcHooks::new(&context); + + let module = test_module(&hooks, "test module"); + let mut builder = FunBuilder::new(hooks.manage_str("test"), module, Arity::default()); + + let mut instructions = vec![ + SymbolicByteCode::Constant(1), + SymbolicByteCode::Constant(1), + SymbolicByteCode::Constant(1), + SymbolicByteCode::DropN(2), + SymbolicByteCode::Constant(1), + SymbolicByteCode::Constant(1), + ]; + + apply_stack_effects(&mut builder, &mut instructions); + + assert_eq!(instructions.len(), 6); + assert_eq!(instructions[0], SymbolicByteCode::Constant(1)); + assert_eq!(instructions[1], SymbolicByteCode::Constant(1)); + assert_eq!(instructions[2], SymbolicByteCode::Constant(1)); + assert_eq!(instructions[3], SymbolicByteCode::DropN(2)); + assert_eq!(instructions[4], SymbolicByteCode::Constant(1)); + assert_eq!(instructions[4], SymbolicByteCode::Constant(1)); + + let chunk = Chunk::stub(&hooks); + let fun = builder.build(chunk); + assert_eq!(fun.max_slots(), 4); + } + + #[test] + fn calculate_push_handler() { + let context = NoContext::default(); + let hooks = GcHooks::new(&context); + + let module = test_module(&hooks, "test module"); + let mut builder = FunBuilder::new(hooks.manage_str("test"), module, Arity::default()); + + let mut instructions = vec![ + SymbolicByteCode::Constant(1), + SymbolicByteCode::Constant(1), + SymbolicByteCode::Constant(1), + SymbolicByteCode::PushHandler((0, Label::new(0))) + ]; + + apply_stack_effects(&mut builder, &mut instructions); + + assert_eq!(instructions.len(), 4); + assert_eq!(instructions[0], SymbolicByteCode::Constant(1)); + assert_eq!(instructions[1], SymbolicByteCode::Constant(1)); + assert_eq!(instructions[2], SymbolicByteCode::Constant(1)); + assert_eq!(instructions[3], SymbolicByteCode::PushHandler((4, Label::new(0)))); + } + } + mod optimize { use super::*; diff --git a/laythe_vm/src/compiler/resolver.rs b/laythe_vm/src/compiler/resolver.rs index f61990e7..b9a6719d 100644 --- a/laythe_vm/src/compiler/resolver.rs +++ b/laythe_vm/src/compiler/resolver.rs @@ -16,6 +16,7 @@ use laythe_core::{ module::{self, ImportError}, object::FunKind, }; +use laythe_lib::global::ERROR_CLASS_NAME; use std::vec; /// A placeholder token to fill the first slot for non method functions @@ -689,12 +690,19 @@ impl<'a, 'src> Resolver<'a, 'src> { } } - fn catch(&mut self, catch : &mut ast::Catch<'src>) { + fn catch(&mut self, catch: &mut ast::Catch<'src>) { self.declare_variable(&catch.name); self.define_variable(&catch.name); if let Some(class) = &catch.class { self.resolve_variable(class) + } else { + self.resolve_variable(&Token::new( + TokenKind::Identifier, + Lexeme::Slice(ERROR_CLASS_NAME), + catch.name.end(), + catch.name.end(), + )); } catch.block.symbols = self.scope(|self_| self_.block(&mut catch.block)); @@ -1267,7 +1275,6 @@ mod test { match &ast.decls[0] { Decl::Stmt(stmt) => match &**stmt { Stmt::Try(try_) => { - let captured = try_.catches.first().unwrap().symbols.get("e").unwrap(); assert_eq!(captured.state(), SymbolState::Initialized); diff --git a/laythe_vm/src/debug.rs b/laythe_vm/src/debug.rs index 55106b1e..e2bf6bd3 100644 --- a/laythe_vm/src/debug.rs +++ b/laythe_vm/src/debug.rs @@ -15,18 +15,21 @@ pub fn print_symbolic_code( chunk_builder: &ChunkBuilder, name: &str, ) -> io::Result<()> { + use std::u16; + let stdout = stdio.stdout(); writeln!(stdout)?; writeln!(stdout, "{0}", name)?; let mut offset: usize = 0; - let mut last_index: usize = 0; + let mut last_line: u16 = u16::MAX; while offset < chunk_builder.instructions().len() { - let show_line = chunk_builder.get_line(offset) == chunk_builder.get_line(last_index); - let temp = print_byte_code(stdio, chunk_builder, offset, show_line); - last_index = offset; - offset = temp?; + let line = chunk_builder.get_line(offset); + let show_line = line == last_line; + + offset = print_byte_code(stdio, chunk_builder, offset, line, show_line)?; + last_line = line; } Ok(()) @@ -38,6 +41,7 @@ pub fn print_byte_code( stdio: &mut Stdio, chunk: &ChunkBuilder, offset: usize, + line: u16, show_line: bool, ) -> io::Result { let stdout = stdio.stdout(); @@ -54,10 +58,10 @@ pub fn print_byte_code( write!(stdout, " {:0>4} ", offset)?; - if offset != 0 && show_line { + if show_line { write!(stdout, " | ")?; } else { - write!(stdout, "{:>4} ", chunk.get_line(offset))?; + write!(stdout, "{:>4} ", line)?; } match instruction { @@ -196,7 +200,11 @@ pub fn print_byte_code( symbolic_push_handler_instruction(stdio.stdout(), "PushHandler", slots, jump, offset) }, SymbolicByteCode::PopHandler => simple_instruction(stdio.stdout(), "PopHandler", offset), + SymbolicByteCode::CheckHandler(jump) => symbolic_jump_instruction(stdio.stdout(), "CheckHandler", jump, offset), + SymbolicByteCode::FinishUnwind => simple_instruction(stdio.stdout(), "FinishUnwind", offset), + SymbolicByteCode::ContinueUnwind => simple_instruction(stdio.stdout(), "ContinueUnwind", offset), SymbolicByteCode::Raise => simple_instruction(stdio.stdout(), "Raise", offset), + SymbolicByteCode::GetError => simple_instruction(stdio.stdout(), "GetError", offset), SymbolicByteCode::Equal => simple_instruction(stdio.stdout(), "Equal", offset), SymbolicByteCode::NotEqual => simple_instruction(stdio.stdout(), "NotEqual", offset), SymbolicByteCode::Greater => simple_instruction(stdio.stdout(), "Greater", offset), @@ -526,9 +534,12 @@ pub fn disassemble_instruction( AlignedByteCode::PushHandler((slots, jump)) => { push_handler_instruction(stdio.stdout(), "PushHandler", slots, jump, offset) }, + AlignedByteCode::CheckHandler(jump) => jump_instruction(stdio.stdout(), "CheckHandler", 1, jump, offset), AlignedByteCode::PopHandler => simple_instruction(stdio.stdout(), "PopHandler", offset), AlignedByteCode::FinishUnwind => simple_instruction(stdio.stdout(), "FinishUnwind", offset), + AlignedByteCode::ContinueUnwind => simple_instruction(stdio.stdout(), "ContinueUnwind", offset), AlignedByteCode::Raise => simple_instruction(stdio.stdout(), "Raise", offset), + AlignedByteCode::GetError => simple_instruction(stdio.stdout(), "GetError", offset), AlignedByteCode::Equal => simple_instruction(stdio.stdout(), "Equal", offset), AlignedByteCode::NotEqual => simple_instruction(stdio.stdout(), "NotEqual", offset), AlignedByteCode::Greater => simple_instruction(stdio.stdout(), "Greater", offset), diff --git a/laythe_vm/src/vm/mod.rs b/laythe_vm/src/vm/mod.rs index e6bd48a2..0a329181 100644 --- a/laythe_vm/src/vm/mod.rs +++ b/laythe_vm/src/vm/mod.rs @@ -366,7 +366,10 @@ impl Vm { ByteCode::Loop => self.op_loop(), ByteCode::PushHandler => self.op_push_handler(), ByteCode::PopHandler => self.op_pop_handler(), + ByteCode::CheckHandler => self.op_check_handler(), ByteCode::FinishUnwind => self.op_finish_unwind(), + ByteCode::ContinueUnwind => self.op_continue_unwind(), + ByteCode::GetError => self.op_get_error(), ByteCode::Raise => self.op_raise(), ByteCode::DefineGlobal => self.op_define_global(), ByteCode::Box => self.op_box(), diff --git a/laythe_vm/src/vm/ops.rs b/laythe_vm/src/vm/ops.rs index 0a5d2e90..57162cb4 100644 --- a/laythe_vm/src/vm/ops.rs +++ b/laythe_vm/src/vm/ops.rs @@ -499,6 +499,7 @@ impl Vm { ExecutionSignal::Ok } + /// Push an exception handler onto the handler stack pub(super) unsafe fn op_push_handler(&mut self) -> ExecutionSignal { let slot_depth = self.read_short() as usize; let jump = self.read_short() as usize; @@ -508,16 +509,77 @@ impl Vm { ExecutionSignal::Ok } + /// Pop an exception handler off the handler stack pub(super) unsafe fn op_pop_handler(&mut self) -> ExecutionSignal { self.fiber.pop_exception_handler(); ExecutionSignal::Ok } + /// Pop an exception handler off the handler stack + pub(super) unsafe fn op_check_handler(&mut self) -> ExecutionSignal { + let jump = self.read_short(); + let error_class = self.fiber.peek(0); + + let error = match self.fiber.error() { + Some(error) => error, + None => self.internal_error("Fiber error not present"), + }; + + if_let_obj!(ObjectKind::Class(error_class) = (error_class) { + if !error_class.is_subclass(self.builtin.errors.error) { + self.fiber.error_while_handling(); + self.runtime_error(self.builtin.errors.type_, "Catch block must be blank or a subclass of Error.") + } else { + + if !error.class().is_subclass(error_class) { + self.update_ip(jump as isize); + } + self.fiber.drop(); + + ExecutionSignal::Ok + } + + } else { + self.fiber.error_while_handling(); + self.runtime_error(self.builtin.errors.type_, "Catch block must be blank or a subclass of Error.") + }) + } + + /// Signal to the fiber we have finished unwinding pub(super) unsafe fn op_finish_unwind(&mut self) -> ExecutionSignal { - self.fiber.finish_unwind(); + let backtrace = self.fiber.finish_unwind(); + + match self.fiber.error() { + Some(mut error) => { + error[1] = val!(self.manage_tuple( + &backtrace + .iter() + .map(|line| val!(self.manage_str(line))) + .collect::>() + )) + }, + None => self.internal_error("Expected fiber error"), + } ExecutionSignal::Ok } + /// Pop the current exception handler and continue the unwind + pub(super) unsafe fn op_continue_unwind(&mut self) -> ExecutionSignal { + self.fiber.pop_exception_handler(); + ExecutionSignal::RuntimeError + } + + /// Push the current error onto the stack + pub(super) unsafe fn op_get_error(&mut self) -> ExecutionSignal { + match self.fiber.error() { + Some(error) => { + self.fiber.push(val!(error)); + ExecutionSignal::Ok + }, + None => self.internal_error("Fiber error not present"), + } + } + pub(super) unsafe fn op_raise(&mut self) -> ExecutionSignal { let exception = self.fiber.pop(); diff --git a/laythe_vm/tests/global.rs b/laythe_vm/tests/global.rs index 273ed99c..d412e81e 100644 --- a/laythe_vm/tests/global.rs +++ b/laythe_vm/tests/global.rs @@ -1,5 +1,8 @@ use laythe_vm::vm::VmExit; -use support::{assert_file_exit_and_stdio, assert_files_exit}; +use support::assert_files_exit; + +#[cfg(not(feature = "debug"))] +use support::assert_file_exit_and_stdio; mod support; @@ -7,6 +10,7 @@ fn test_files(paths: &[&str], result: VmExit) -> Result<(), std::io::Error> { assert_files_exit(paths, FILE_PATH, result) } +#[cfg(not(feature = "debug"))] fn test_file_with_stdio( path: &str, stdout: Option>, diff --git a/laythe_vm/tests/language.rs b/laythe_vm/tests/language.rs index 35916685..53ba1730 100644 --- a/laythe_vm/tests/language.rs +++ b/laythe_vm/tests/language.rs @@ -313,6 +313,11 @@ fn exception() -> Result<(), std::io::Error> { "language/exception/nested_continue.lay", "language/exception/nested_return.lay", "language/exception/one_deep_catch.lay", + "language/exception/not_error_catch_not_tested.lay", + "language/exception/try_fall_through.lay", + "language/exception/try_error_in_catch.lay", + "language/exception/catch_not_error_class.lay", + "language/exception/catch_not_class.lay", "language/exception/two_deep_catch.lay", "language/exception/top_level_catch_raise.lay", "language/exception/multiple_catches.lay", @@ -324,6 +329,7 @@ fn exception() -> Result<(), std::io::Error> { &vec![ "language/exception/catch_no_block.lay", "language/exception/try_no_block.lay", + "language/exception/catch_not_identifier.lay", "language/exception/try_no_catch.lay", ], VmExit::CompileError,