craftinginterpreters
craftinginterpreters copied to clipboard
Design discussion: Exceptions?
Hi all, craftinginterpreters readers! I opened this issue because I'd like to start a discussion on a pretty important design choice that I'd love to be able to implement for my very own Lox (to which I already added other stuff as well, like actual integers/floats rather than just doubles, string slicing, OP_CONSTANT_LONG
and more), and that's exceptions. I would like to eventually use this little language for scripting and automation so exceptions are a must-have for a modern language. Unfortunately, exceptions are extremely hard to implement as they imply unrolling the call stack and doing many things I'm not experienced enough to implement or research about on my own (Google doesn't seem to have a tutorial on how to implement exceptions, damn!), so I started this discussion to see if anyone has some resources or has the slightest idea on how such a mechanism could be implemented (also, of course, a try/except thing after that)
Thanks in advance!
Similar to how returns (and break statements, if you did that exercise) can be implemented using the host language exception mechanism, you can handle exceptions in the same way! All 3 mechanisms are stack-unrolling control flow operators, and can implemented similarly.
All this assuming you are in the interpreter. I cannot speak to the VM part.
At a basic level, I suspect the CallFrame would have some kind of a flag that indicates that it was inside of a try block when it was created. When an exception is thrown, then you'd pop the call frame stack backward until you find a marked CallFrame on top (basically doing what the OP_RETURN instruction does in a loop except tossing out any return values). Somehow there'd have to be an offset or a jump instruction baked in there too so that once you rewind back to the right call frame you know where to then jump to in order to get into the catch block. Not sure what the complete answer is, but I'd start with something like that, I guess.
First, thank you all for your replies!
@kandersen yeah, unfortunately I'm already implementing the bytecode VM so I can't really take advantage of the host language because of the unstructured nature of bytecode.
@BigZaphod could you elaborate on that? Maybe provide a C snippet that I can translate to my host language? (Nim) That would be great to get me started, but thanks anyway!
I hope others join this discussion as well so that we can exchange opinions and find out what's the best way to implement such a thing
I don't have any C snippets for this since I've not tried to implement it myself - I was just thinking out loud. I'd suggest just trying some things and seeing what happens! :)
The basic idea of an exception is that it returns to a previous place on the call stack so I'd start with that. Fundamentally it's pretty much just an overpowered return statement, so it seems logical to start there, I think.
Yeah, I guess I'll have to try that out myself and see what comes out then!
It's in a dead and somewhat broken state, but one of my first bytecode VMs supports exceptions. The basic idea is that the VM maintains a stack of catch handlers. Each handler has a pointer to the beginning of the bytecode for its associated catch
block. When compiling a try
statement, the compiler emits BEGIN_TRY
instruction at the beginning, then compiles the try body, then emits END_TRY
. In the VM, BEGIN_TRY
pushes a new catch handler with a pointer to the bytecode for the catch clause and a reference to the current call frame. When a END_TRY
instruction is reached, it pops and discards that handler.
A throw
statement is compiled to some kind of THROW
instruction. That simply looks at the topmost registered catch handler discards any callframes above its callframe, and then jumps to the catch bytecode.
That's very simplified, but that's the rough idea. CPython does something similar, I believe.
Ah ha - I forgot that of course you could have multiple try/catch within a single function body so just marking the call frame wouldn't be enough. I might have to try adding exceptions now - they sound kinda fun.
Amazing! Right now I'm having issues fixing function calls for my bytecode VM so I'll have to get around to that first, but I'll have a look at it for sure!
@BigZaphod any updates on your progress?
I've done an implementation of Exceptions using the clox VM as my base. I use the method described above - having a stack of handlers that I push and pop. I'll attempt to finish writing the third post in the series soon (the code is written and works, I just need to write the post).
I've described it in detail here, using Bob's every-line-of-code must be in the post idea: https://amillioncodemonkeys.com/2021/02/03/interpreter-exception-handling-implementation/
The third post will cover finally blocks which took a bit of wrangling, but turned out quite well.
I've done an implementation of Exceptions using the clox VM as my base. I use the method described above - having a stack of handlers that I push and pop. I'll attempt to finish writing the third post in the series soon (the code is written and works, I just need to write the post).
I've described it in detail here, using Bob's every-line-of-code must be in the post idea: https://amillioncodemonkeys.com/2021/02/03/interpreter-exception-handling-implementation/
The third post will cover finally blocks which took a bit of wrangling, but turned out quite well.
Its's a nice read. I'm surely anticipating your part 3. What you implemented is almost what I had already. What I never catered for was the finally part. Will love to see how you handle it.
Thanks for reading - it's weirdly motivating. I have now published part 3, though it's probably a little thin on prose, so if you want anything further clarified, don't hesitate to drop a comment and I'll spend a bit of time fleshing that out.
The post can be found here: https://amillioncodemonkeys.com/2021/05/16/interpreter-exception-handling-part-3/
Nice article. But your implementation does not allow a finally block to run when a catch block throws an exception. Had to do a little modification to even make the finally block run without a catch block.
Forgive me for resurrecting such an old thread. I think there are two challenges here. The first is the hardest and has already been discussed, so I won't bother. And that's dealing with stacked try/catch blocks.
But if you've also added destructors to classes, (something I'm working on at the moment), then you'll probably want to call any destructors for created classes as you unwind the stack. You could leave it to the GC, but that seems unkind to the user.