source: doc/proposals/exceptions-assert.md@ 2ec81ca

Last change on this file since 2ec81ca was 2ec81ca, checked in by Andrew Beach <ajbeach@…>, 23 hours ago

Six months to a year after it was due for a round of early feedback: The original exception rework proposal.

  • Property mode set to 100644
File size: 15.3 KB
Line 
1Assertion Like Exceptions
2=========================
3This is a proposal for a rework to the existing exception system.
4
5Motivation
6----------
7The current exception handling mechanism (EHM) is functional and has wide
8use cases. However, there is a certain awkwardness to using them,
9particularly the exceptions themselves.
10
11Some of this is just feature incompleteness. But some of it comes down to
12a paradigm mismatch. Traditional exceptions carry data and behaviour from
13throw to catch - that is, they are objects - to allow for the transfer and
14handling of the exception, but CFA does not have an OO design.
15Now there are ways to at least lessen this strain (see `Exception.hfa`), but
16those tend to limit functionality or are dependent on one or more upcoming
17works (such as closed traits or modules).
18
19Instead of trying to solve that fundamental mismatch, this proposal tries to
20create exception handling that does not depend on OO-style features.
21This restructures the EHM to try and use CFA's existing design patterns.
22
23Design
24------
25To make exceptions fit better into the rest of Cforall, what if we reused
26some of the design that is already in the language: assertions.
27The result is a system of checked exceptions that could serve as at least a
28partial replacement for the current exception handling mechanism and
29exception declarations.
30
31These exceptions work much like existing exceptions, they are raised from a
32throw, traverse up the stack to a handler and run the handler.
33Where control continues after the routine can vary to mimic both termination
34and resumption throws.
35
36An exception is formatted like a function call: `RetType Name(ArgTypes...)`.
37The `Name` is a label only used for matching, the ArgTypes give the types of
38the data passed from the throw to the handler and RetType can give the type of
39the data passed from the handler back to the throw. RetType can also be
40`void` to reply with no data, or a noreturn marker to say that control cannot
41return to the throw.
42
43This exception signature is usually only written out in full as part of
44a function declaration, which says that this function may throw that
45exception. Throws and catches will usually replace the ArgTypes with the
46arguments or parameters to hold those values.
47
48The actual termination vs. resumption divide comes from the handler. The
49handler may `resume`, if it is handling a void or value exception, which
50returns control to the throw creating a resumption. If controls runs out
51of the handler block, it continues at the end of the try statement after
52unwinding the stack.
53
54Throwing expressions (throws or a throwing function calls) must be inside
55a function that can throw the exception, or within a catch clause that can
56handle it.
57
58There is no case for unhandled exceptions because of the assertion-like part.
59Functions are annotated with the exceptions that they throw (thrown within
60the function or a called function and not handled). By ensuring that `main`
61throws no exceptions, that means all throws will be matched to a try.
62
63Syntax and Usage
64----------------
65The throws annotations is new a syntax. The design is not final, but for
66examples we will use: `throws (EXCEPTIONS)`, where EXCEPTIONS is a comma
67separated list of exception signatures. This can happen within the scope of a
68forall clause so it can be polymorphic with the rest of the function.
69
70There is no syntax to declare exceptions, although one could be added, they
71are effectively declared in the throws annotations.
72
73Throw statements remain, but now they are joined by throw expressions. The
74throw expressions return their response value. Throw statements can
75(Throw expressions with a response of `void` can be a void expression, in the
76few places where a void expression is allowed.)
77```
78throw Name(Exprs,...)
79```
80Along with the addition of the expression form, the main change is that the
81exception is not an expression, but a literal name and a series of
82expressions.
83
84Try statements are the same as the current design except for the catch clauses.
85```
86catch Name(Arguments...) { ... }
87```
88Exceptions thrown in the try block are checked for matching handlers. The
89match is simply preformed by label name. (Although with type annotations on
90the arguments or for the return type, could be extended to involve full
91resolution on the signature of the exception.) When an exception is thrown
92the matching catch is put onto the stack and run. Running off the end, or
93otherwise exiting the catch block, causes a termination and unwinds the
94stack.
95
96In addition there is a new statement that can only be used inside a catch
97statement (and only in one that can return to the throw).
98```
99resume [EXPR];
100```
101This is like a return statement, the expression is required if the exception
102expects a value in response. If the expression is present, it is evaluated
103before the resume occurs. When the resume occurs, we exit the handler and the
104throw finishes execution, resulting in the evaluated value as appropriate.
105
106### Exception Signature Polymorphism
107We have everything to make a functional exception handling mechanism.
108However, to get a flexible (especially in regards to higher order functions)
109system we need a basic level of polymorphism.
110
111```
112forall(T, [N], exception E) throws(E)
113void foreach(void op(T &) throws(E), array(T, N) & data) {
114 for (i; N) op(data[i]);
115}
116```
117
118A new type of polymorphic parameter is added `exception` which is polymorphic
119not on an exception, but a possibly empty of exceptions.
120A set of exceptions is can be used in an exception signature in the same
121way a single exception can be and are traced from callee to caller in the
122same way a single exception is.
123They do not interact with throws or catches as those work on concrete types.
124At the top and bottom of the polymorph call stack concrete functions will
125throw and catch the exception, passing through the polymorphic section.
126
127Examples
128--------
129
130### Noreturn Example (Enumeration succ)
131A simple example from the enumeration traits, there is a `succ_unsafe`
132function in the Serial trait that is wrapped by a `succ` function that adds
133some checks and aborts if they fail.
134
135Instead of aborting, it could have an assertion-like exception:
136```
137int succ(int value) throws(noreturn out_of_range()) {
138 if (value == MAX) {
139 throw out_of_range();
140 }
141 return value + 1;
142}
143```
144This serves the same role as the existing function, but combines the succ
145with a range check the caller can respond to.
146
147For an example of catching this, consider creating a wrapper to get the
148current succeed or abort implementation:
149```
150forall(T | { T succ(T) throws(noreturn out_of_range(); })
151T succ_abort(T val) {
152 try {
153 return succ(val);
154 } catch out_of_range() {
155 abort("Attempt to succ to non-existant successor.");
156 }
157}
158```
159
160### Value Resume Example (lookup)
161For an example of catching and resuming with a value, consider a map lookup
162that throws when you attempt to lookup a key not in the map and a wrapper
163that provides a default value.
164```
165forall(K, V | relational(K)) throws(V NotFound(K &));
166V lookup(map(K, V) const & mapping, K & key) {
167 // Internal code equivilent too:
168 node(K, V) * node = lookup_internal(mapping, key);
169
170 return ((node) ? node->value : throw NotFound(key));
171}
172
173forall(K, V | relational(K))
174V lookup(map(K, V) const & mapping, K & key, V & defaultValue) {
175 try {
176 return lookup(mapping, key);
177 } catch NoFound(_key) {
178 resume defaultValue;
179 }
180}
181```
182
183This may not be the most efficient way to implement a lookup-with-default
184function, but it would work. Practically, one would handle the NoFound
185directly, even running more complex code or using information not passed
186to the lookup, as showed here:
187
188```
189// local_var is declared up here.
190try {
191 function_that_calls_lookup(args);
192} catch NoFound(key) {
193 if (is_special_key(key)) {
194 resume build_default_value(key, local_var);
195 } else {
196 resume simple_default_value;
197 }
198}
199```
200
201### Void Resume/Terminate Example (Out of Memory)
202The third type of resumption is a void return, which you may resume but do
203not pass any data back to the throw.
204
205```
206try {
207 ...
208} catch NoMem(size_t target_size, Buffer * buff) {
209 if (void * new_buff = realloc(buff->data, target_size)) {
210 buff->data = new_buff;
211 resume;
212 }
213}
214```
215
216Here, the error cannot be fixed with information, but rather a correction to
217the container state. This also shows how resumption is optional (for both
218void and value throws) and the handler can instead decide to terminate and
219unwind the stack.
220
221### Polymorphism Example (Save/Restore)
222Higher order functions can handle types simply need to polymorphic on the
223same exception type as the functions they take as parameters.
224```
225forall(T, exception E)
226T isolate_state(T (*func)() throws (E), State state) {
227 // SavedState wraps a State and restores it on deconstruction.
228 SavedState _saved = save_state();
229 set_state(state);
230 return func();
231}
232```
233This isolates a function by saving and restoring state around the function,
234where that state is some generic global state. The wrapped function can raise
235any of those exceptions and they will pass through the isolateState. When
236one of those exceptions terminate or func returns, _saved's destructor runs.
237
238### Polymorphic Error Example (foreach)
239Here is a simple example of polymorphism over a higher order function.
240
241```
242void print_widget(Widget &) throws (noreturn FormatError());
243
244forall(T, [N], exception E) throws(E)
245void foreach(void op(T &) throws(E), array(T, N) & data) {
246 for (i; N) op(data[i]);
247}
248
249forall([N])
250void print_widgets(array(Widget, N) & widgets) {
251 // FormatError not handled.
252 foreach(print_widget, widgets);
253}
254```
255It is infact too simple as foreach will have the same exception signature
256as print_widget and that means print_widgets is not handling the function
257nor passing it to its caller.
258
259However, this example (or a version that catches FormatError) will work
260because the exception signatures all match.
261```
262forall([N])
263void print_widgets(array(Wiget, N) & widgets) throws (noreturn FormatError()) {
264 foreach(print_widget, widgets);
265}
266```
267
268Details And Additional Notes
269----------------------------
270
271### Rethrows and Conditional Catch
272As extension to the core system, we could add rethrowing or conditional
273catching to try blocks. There purpose is the same, to allow a handler to
274use non-type information to decide if it can handle an exception or not.
275
276If an exception is rethrown or the condition evaluates to false, exception
277propogation continues up the stack. As far as control flow goes this should
278be the same as not catching it at all.
279
280A rethrowing or conditional catch does not remove the exception it could
281catch and handle from the unhandled exception set. Because it may not, so
282the type system has to account for that.
283
284### Performance (Where to Pay Costs)
285Although the behaviour has been laid out, the performance has not. We would
286like "good" performance of course, but there is a choice between fast tries
287and fast throws.
288
289The current system uses fast tries, which means it is designed for very rare
290cases, almost always errors.
291
292Fast throws mean that entering and leaving a try block is now more expensive,
293but it means throws can be common. Exceptions can now be used for alternate
294returns and other relatively frequent cases.
295
296A single choice doesn't have to be made for the entire language. It could
297choose differently for different cases or even let the user decide for
298different exceptions.
299
300### Implementation
301The implementation has not been specified. See the performance section for
302fast tries vs fast throws.
303
304The current implementation could be adapted if we want to stay with fast
305tries. The exception could become an id and a pointer to stack allocated
306storage. If the id can even be contextual and if a function doesn't handle
307the exception, it gives a new id to pass onto its owner.
308
309(You might need to modify libunwind to allow returning successfully from an
310unwind call without unwinding, but last time I checked that change could be
311made in a platform independent part of libunwind.)
312
313### Polymorphic Tracing Details
314For simplicity, polymorphic exceptions should probably just be black boxed
315and not interact with any other handlers or exceptions except at the edges
316where it is boxed and unboxed.
317
318The programming language Flix has effect polymorphism (but not polymorphic
319effects, avoid confusing those), which has many of the same design
320constraints.
321
322### Polymorphism Ease of Use
323It would be nice if the language's defaults were set up in such a way that
324the common usage pattern is the simplest/most natural way to write a
325function.
326
327For a point of comparison, consider otype (object-type) parameter `(T)` as
328compared to the dtype (data-type) parameter `(T &)`. Although dtype is
329the more primitive/simplest form, the simplest syntax goes to otype, which is
330the most common form. Something similar could be done for higher-order
331polymorphic functions.
332
333Something like if a polymorphic function takes another function and none of
334them have explicit exception signatures, a exception polymorphic argument
335could be added and all the functions may throw that. Details may depend on
336how widening works. Also, you must be able to explicitly say that functions
337do not throw, doing this to one of the functions (by convention, the main
338function, not any function parameters) would disable this feature.
339
340The exact rules should come from usage patterns that emerge once the feature
341is implemented.
342
343### Unwind Timing
344Although the current implementation can mimic both termination and resumption
345exceptions. There is one major difference in the behaviour of the termination
346throw and catch, the unwinding of the stack now occurs after running the
347handler instead of before.
348
349When the handler is short and operates locally, this is unlikely to matter.
350In fact, this can fix some life-time issues (see implementation).
351However, if there is a lot of work in the handler we haven't cleared the
352stack up or freed up other resources (the destructors haven't run, things
353are still taking up stake space, etc.).
354
355If this turns out to be a problem, there should either be a way to force an
356unwind in the handler, or make sure local control flow can take us to an
357external block to handle the exception.
358
359### Exceptions in Traits
360In another way to make exceptions like assertions, they could be added into
361traits. They can probably just mixed into the bodies with assertions.
362
363This would generally just be a short-hand, but also offers a tool to
364organize exceptions like in effects, which can have multiple options.
365
366### Cancellation
367This system is narrower than the current exception system. Rather than trying
368to cover every use case directly/with one feature, I would propose another
369feature to handle some of the important cases not seen covered here.
370
371The biggest other cases are the non-recoverable cases. These may not be
372caught are all (in which case they are the same as an abort) or are caught
373and the operation is abandoned. Control then continues from a main loop
374(as in an engine or server dispatcher).
375
376For these cases, an unchecked exception that only supports catch-all or our
377cancellations might work better. They only information they need to carry is
378enough for a user facing error message.
379
380### Default Exception Handlers
381Handling every exception has often proven more work than people want to put
382in. Now, this may not be an issue with fewer more significant exceptions,
383but if it is default exception handling within a program might help.
384
385This could be a universal thing, such as triggering an abort or cancellation
386if an exception is not handled, or something narrower using assertions or
387configuration on the exception itself.
Note: See TracBrowser for help on using the repository browser.