coder-mike / microvium

A compact, embeddable scripting engine for applications and microcontrollers for executing programs written in a subset of the JavaScript language.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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:

  1. Push the the contents of the catchTarget register, which already contains the LSb 1 so the GC ignores it (see below)
  2. Push the label address relative to the beginning of bytecode, with an added 1 so GC ignores it
  3. Set the catchTarget register to the stack address of #1, relative to the stack base, with an extra 1 LSb.

EndTry:

  1. Assert that the stack is 4 bytes ahead of the current catchTarget, since the end of a try block should have the stack in the same place as the beginning.
  2. Set the stack pointer to the current catchTarget
  3. Set the catchTarget to the value at the current stack pointer

Throw - existing instruction but semantics will change:

  1. Pop value (exception) off the stack and keep it in a temporary register as the "exception"
  2. If the current catchTarget is 0 then return from the machine with "uncaught exception". Else, continue:
  3. Set the stack pointer to the current catchTarget
  4. 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.