Try... catch support
coder-mike opened this issue · comments
On Reddit, two people pointed out that exceptions support is quite important for basic control flow in JS. This is something I've been thinking of for a while but it's been lower down on the priority list. But seeing 2 people interested in this feature, and it's not too difficult to implement, I thought maybe I should have a go at it now.
I'm using this ticket to track and document progress.
I'm going to measure the size before and after to see how much ROM size it adds. Without the feature, it's currently 8414 bytes which is 8.22kB.
Exceptions Design
I originally had in this plan to have support for finally
as well, but it's actually a lot more complicated than just catch
, and arguably it's not used as often so maybe it's not worth it (right now). The reason it's complicated is because a statement like return
needs to execute all the intermediate finally blocks, while still remembering that its "returning". I think similarly for break
-- you need to execute the finally block and remember that you're breaking and where you're breaking to. So there is a lot of implicit state related to a finally block: are we throwing? what are we throwing? are we returning? are we breaking? where are we breaking to?
Note: I think it's still possible to implement finally
without adding any more registers or opcodes. All the extra state that needs to be managed can be managed as local variables, essentially treating the finally
feature as a desugaring to code that uses try...catch
. It's just not very simple and maybe doesn't add a lot of value at this time compared to other pressing features.
New Machine Instructions
StartTry
- given a literal label argument that points to the catch block:
- Push the the contents of the
catchTarget
register, which already contains the LSb1
so the GC ignores it (see below) - Push the label address relative to the beginning of bytecode, with an added
1
so GC ignores it - Set the
catchTarget
register to the stack address of#1
, relative to the stack base, with an extra1
LSb.
EndTry
:
- Assert that the stack is 4 bytes ahead of the current
catchTarget
, since the end of atry
block should have the stack in the same place as the beginning. - Set the stack pointer to the current
catchTarget
- Set the
catchTarget
to the value at the current stack pointer
Throw
- existing instruction but semantics will change:
- Pop value (exception) off the stack and keep it in a temporary register as the "exception"
- If the current
catchTarget
is 0 then return from the machine with "uncaught exception". Else, continue: - Set the stack pointer to the current
catchTarget
- Set the program counter to the target referenced by the word that is 2 bytes ahead of the current stack pointer
Compiling
try ... catch
A try
block compiles into a normal block but with a startTry
and an endTry
at the beginning and end. The end will also be followed by a jump to the continue label (the program address after the end of the catch
block). The startTry
references the catch
block.
try {...} catch {...}
If the catch
has no binding for the exception value, then the first instruction will just be a pop
to pop the exception off the stack.
The end of the catch
will jump to the continue label.
try {} catch (e) {}
If the catch
has a binding for the exception, the first instruction will "save" the exception to the binding location, which may be a no-op if slot is on the stack and happens to be the same location, but it could also be a closure slot.
After the try and catch blocks will be a continue label (dummy block) representing the next instruction to execute.
Need to be careful to pad the catch block to an even boundary so that the catchTarget
can use the LSb
to hide from the GC.
Break and Return
A return
instruction in JS needs to unwind the try stack. We already have the scopeStack
with a leaveScope
which break
uses to unwind the lexical stack when breaking out of loops. I think maybe it's easiest to to add another parameter to leaveScope
for isReturning
. And then enterScope
could specify whether the scope is a try scope or not, so that the epilogue can be customized.
Closures and scope analysis
I need to make sure that the scope analysis generates a lexical binding for the exception in the catch block. Even if there is no lexical binding, I need to make sure that lexical bindings within the catch
block do not overlap with the slot chosen for the exception. Possibly need an explicit catchSlot
for a catch block.
Need to make sure that the binding is available to closures, like any other binding.
try {
// ...
} catch (e) {
return () => e
}
Microvium now has try...catch support.
I don't actually know exactly how much size it added to the ROM footprint because I landed up adding some other features in parallel. I think it's possibly 156 bytes.