File: exceptions.markdown

package info (click to toggle)
moarvm 2020.12%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 18,652 kB
  • sloc: ansic: 268,178; perl: 8,186; python: 1,316; makefile: 768; sh: 287
file content (263 lines) | stat: -rw-r--r-- 10,013 bytes parent folder | download | duplicates (3)
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()