Exceptions in MoarVM need to handle a range of cases. There exist both control exceptions (last/next/redo) where we want to reach the handler in the most expedient way possible, unwinding the stack as we go, and probably just do a goto instruction. In these cases, we don’t expect to need any kind of exception object. At the other end of the scale, there are Perl 6 exceptions. These want to run the handler in the dynamic scope of the exception, and potentially resume rather than unwinding. These differences are properties of the handler rather than the exception; a CONTROL is interested in being run on the stack top when a “next” reaches it, whereas a while loop’s handler for that just wants control delivered to the appropriate place.
Handlers are associated with (static) frames. A handler consists of:
A bitwise and
between the category filter and the category of the exception
being thrown is used to check if the handler is applicable.
Note that an Unwind handler is never actually set as the category of an exception; these are just for triggering actions during unwinds due to other exceptions. In the case of an unwind handler, the current exception is thus the one to blame for the unwinding. It is expected that an unwind handler will always rethrow once it’s done what is needed.
The MAST::HandlerScope node indicates the instructions covered by handler and details of the kind of handler it is. See the MAST node definitions for more.
Handlers are stored per frame and listed in a table. It is important that more deeply nested handlers appear in the table earlier than those lexically outer to them. This is really a job for the MAST to Bytecode compiler, since the MAST encodes the structure as nested nodes. Really, though, it’s just a case of writing an entry into the frame’s table after the node has been processed. See the bytecode specification for details.
Some opcodes exist for creating exception objects and working with them. An exception object is anything with the VMException representation. Note that most HLLs will wish to attach their own objects as the payload.
Gets the exception currently being handled. Only valid in the scope of handler.
Marks the specified exception as handled. Only valid in the scope of a handler for the specified exception. Also, only required for goto handlers that also include an exception object.
Creates a new exception object, based on the current HLL’s configured exception type object or using BOOTException otherwise. By default it has an empty message and a category of 1 (a catch exception).
Sets the exception object’s string message.
Sets the exception object’s payload (some other object).
Sets the exception object’s category
Gets the exception object’s string message.
Gets the exception object’s payload.
Gets the exception object’s category
There are various instructions for throwing a new exception object.
throwdyn w(obj) r(obj)
throwlex w(obj) r(obj)
throwlexotic w(obj) r(obj)
There are also instructions for throwing a particular category of exception without first creating an exception object.
throwcatdyn w(obj) int64
throwcatlex w(obj) int64
throwcatlexotic w(obj) int64
These will only produce an exception object for handlers that need it. The object that is produced will have a null message and payload, so only its category will be of interest. These are mostly intended for control exceptions.
Finally, for convenience, there is also:
die w(obj) r(str)
Which creates a catch exception with a string message and throws it.
One may wonder why all of these throw instructions take a register to write into. This is because a handler that invokes in the dynamic scope of the throw has the option to prevent stack unwinding by instead indicating that execution be resumed. When it does so, it specifies an argument for the resumption; this argument is then written into the register should resumption take place.
As for the dyn/lex/lexotic difference:
Sometimes, a handler may want to look at an exception, see if it’s what it expects to handle, and if not pass it along as if the handler never saw it. This is the job of rethrow. A rethrow may only be used on the exception currently being handled. It is a simple instruction:
rethrow
Since it’s always about the exception for the current handler, there’s no need to say what should be rethrown.
A goto handler that is allowed to get the exception object and/or rethrow it must mark the point they consider the handler over in the case they do not rethrow. The op for this is simply:
handled r(obj)
Note that if, while the handler is active, another exception is thrown and unwinds the stack past this handler, that’s fine.
A stack of current handlers is maintained. Note that this is handlers we’ve actually invoked as the result of an exception being thrown (there may be many handler scopes that we are in, but only those that are presently handling exceptions get an entry on the stack).
When we search for handlers to invoke, any active handler is automatically skipped, so that a handler can never catch an exception thrown within it. Otherwise, you can easily imagine a mass of hangs.
When an exception is thrown, some pieces of information are initially needed:
Here is the overall algorithm in pseudo-code.
XXX TODO: Finish this up. :-)
search_frame_handlers(f, cat):
for h in f.handlers
if h.category_mask & cat
if f.pc >= h.from && f.pc < h.to
if !in_handler_stack(HSTACK, h)
return h
return NULL
search_for_handler_from(f, mode, cat)
if mode == LEXOTIC
while f != NULL
h = search_for_handler_from(f, LEX, cat)
if h != NULL
return h
f = f.caller
else
while f != NULL
h = search_frame_handlers(f, cat)
if h != NULL
return h
if mode == DYN
f = f.caller
else if f == LEX
f_maybe = f.outer
while f_maybe != NULL && !is_in_caller_chain(f, f_maybe)
f_maybe = f_maybe.outer
f = f_maybe
return NULL
run_handler(h, target_scope)
if h.mode == 0
unwind_to(target_scope)
pc = h.goto
return_to_runloop
if h.mode == 1
unwind_to(target_scope)
pc = h.goto
push_handler(h, target_scope)
return_to_runloop
if h.mode == 2
unwind_to(target_scope)
push_handler(h, target_scope)
SCOPE.return_special = ...
SCOPE.return_special_data = ...
invoke(get_reg(target_scope, h.local_idx))
return_to_runloop
if h.mode == 3
push_handler(h, target_scope)
SCOPE.return_special = ...
SCOPE.return_special_data = ...
invoke(get_reg(target_scope, h.local_idx))
return_to_runloop
panic_unhandled(scope, obj):
note "Unahndled exception: " + obj.message
note backtrace(scope)
exit 1
panic_unhandled_cat(scope, cat):
note "Unahndled exception of category " + category_name(cat)
note backtrace(scope)
exit 1
throw(mode):
(h, target_scope) = search_for_handler_from(SCOPE, mode, CAT)
if h == NULL
panic_unhandled_cat(SCOPE, CAT)
run_handler_(h, target_scope, obj)
throwcat(mode):
(h, target_scope) = search_for_handler_from(SCOPE, mode, CAT)
if h == NULL
panic_unhandled_cat(SCOPE, CAT)
run_handler_(h, target_scope, NULL)
handled():
HSTACK.pop()