craftinginterpreters icon indicating copy to clipboard operation
craftinginterpreters copied to clipboard

Design discussion: Exceptions?

Open nocturn9x opened this issue 4 years ago • 14 comments

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!

nocturn9x avatar Aug 24 '20 07:08 nocturn9x

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.

kandersen avatar Aug 24 '20 08:08 kandersen

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.

BigZaphod avatar Aug 24 '20 16:08 BigZaphod

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

nocturn9x avatar Aug 25 '20 22:08 nocturn9x

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.

BigZaphod avatar Aug 26 '20 03:08 BigZaphod

Yeah, I guess I'll have to try that out myself and see what comes out then!

nocturn9x avatar Aug 26 '20 22:08 nocturn9x

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.

munificent avatar Sep 02 '20 04:09 munificent

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.

BigZaphod avatar Sep 02 '20 14:09 BigZaphod

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!

nocturn9x avatar Sep 04 '20 09:09 nocturn9x

@BigZaphod any updates on your progress?

nocturn9x avatar Oct 01 '20 07:10 nocturn9x

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.

michaelmalonenz avatar Apr 02 '21 08:04 michaelmalonenz

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.

mcfriend99 avatar May 15 '21 14:05 mcfriend99

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/

michaelmalonenz avatar May 16 '21 07:05 michaelmalonenz

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.

mcfriend99 avatar May 18 '21 08:05 mcfriend99

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.

chrisjbreisch avatar Jul 19 '22 02:07 chrisjbreisch