1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
|
# Exceptions
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 Raku 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
Handlers are associated with (static) frames. A handler consists of:
* The start of the protected region (an offset from the frame's bytecode start)
* The end of the protected region (an offset from the frame's bytecode start)
* An exception category filter:
* 1 = Catch Exception
* 2 = Control Exception
* 4 = Next
* 8 = Redo
* 16 = Last
* 32 = Return
* 64 = Unwind (triggers if we unwind out of it due to an exception being
thrown; normal block exits do not cause this)
* A handler action
* 0 = Unwind any required frames, then goto the specified address. It is
not possible to get any exception object or do any kind of rethrow.
* 1 = Unwind any required frames, then goto the specified address. An
exception object is available. This kind of handler leaves a handler
record active on the stack, which the handler should remove by doing
a rethrow or making the exception handled.
* 2 = Invoke the specified block, and unwind unless it chooses to resume.
Once the block returns, the handler is over.
* In the case of a goto address handler, the offset of the handler
* In the case of a block handler, the register in the frame that holds the
block to invoke. The block should take no parameters.
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.
## Handler representation in MAST
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.
## Handler representation in bytecode
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.
## Exception Objects
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.
### exception w(obj)
Gets the exception currently being handled. Only valid in the scope of handler.
### handled r(obj)
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.
### newexception w(obj)
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).
### bindexmessage r(obj), r(str)
Sets the exception object's string message.
### bindexpayload r(obj), r(obj)
Sets the exception object's payload (some other object).
### bindexcategory r(obj), r(int64)
Sets the exception object's category
### getexmessage w(str), r(obj)
Gets the exception object's string message.
### getexpayload W(obj), r(obj)
Gets the exception object's payload.
### getexcategory w(int64), r(obj)
Gets the exception object's category
## Throwing Exceptions
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:
* dyn means "search caller"
* lex means "search outer", with the caveat that the outer must also be on the caller
chain too
* lexotic combines the two; for each entry in the dynamic scope, we scan all outers
from that point; note that such an outer should also be in the call chain
## Rethrowing
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.
## Goto handlers that access exception objects and may rethrow
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.
## Overall mechanism
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:
* The category, CAT
* The exception object, OBJ
* How to search (dyn, lex, lexotic), MODE
* The current scope, SCOPE
* The curent thread's active handler stack, HSTACK
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()
|