source: doc/proposals/exceptions-assert.md@ b3609b2

Last change on this file since b3609b2 was 58560d2, checked in by Andrew Beach <ajbeach@…>, 13 days ago

Added to the Assertion Like Exceptions proposal, just a little section to address checked exception concerns.

  • Property mode set to 100644
File size: 25.4 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
63Features
64--------
65The detailed design of the proposal organized by feature.
66There are core features (exception signatures, the throw expression and
67try statement) followed by extra features. The core features form the minimal
68working version of this system and should be understood together.
69
70All syntax given is a mock-up and can be changed.
71
72### Exception Signatures
73Exceptions exist as assertion-like addition to a function signature.
74
75An exception signature describes an exception, and consists of a response
76type, a identifier as the name and the parenthesized list of comma separated
77parameter types.
78```
79RESPONSE_TYPE NAME(PARAMETER_TYPES)
80```
81Each exception signature notes an exception that can be thrown.
82The parameter types are the types of the values passed from the throw to
83the handler and the response type is the type of the value that might be
84passed from the handler back to the throw.
85
86These are put into the throws clause of a function,
87an optional part of the function signature.
88The function signature is just the keyword throws followed by a parenthesized
89and comma separated list of exception signatures.
90```
91throws(EXCEPTION_SIGNATURES)
92```
93By convention, this goes after the parameter list of the function,
94unless the function has a forall list in which case it goes after that.
95
96This doesn't actual do anything, it just annotes what exceptions can come
97from this function. Or rather propagate from call expressions that use it.
98Expressions and statements also - implicitly - have their own throws clause
99of the exceptions that can come from them.
100For most expressions and statements it is simply the combined set of
101exceptions of their components.
102
103The function's throws clause should match that of its body statement.
104Any missing exceptions is a compile time error.
105Any extra exceptions should be a warning,
106some notation to silence the warning would be provided.
107
108The throws clause does not participate in overload resolution. Once the
109overload is choosen, the throws clauses are checked and errors raised
110when there is a mismatch.
111
112Two exceptions are the same exception if their name, parameter types and
113response type are the same. It should follow the same pattern as function
114resolution.
115
116No Return Signature (iterator):
117Consider an iterator interface, that returns a value and updates the iterator
118in place to produce a value, and throws an exception to indicate the end of
119iteration.
120```
121forall(T, I)
122trait Iterator {
123 I next(T &) throws(noreturn end_of_iteration());
124}
125```
126
127Function Exception Separation (too-much-overloading):
128Exceptions don't exist in the same space as functions and variables.
129The places they can be used only accept exception names so there is no
130confusion.
131```
132int f(char) throws(int f(char));
133
134void g() throws(int f(char)) {
135 ...
136 int i = f('a');
137 ...
138}
139```
140This will always call the function as exceptions are in their own namespace
141and do not interact with function names, variable names or trait names.
142
143### Throw Expression
144Exceptions come from throw expressions.
145
146A throw expression is made of the keyword `throw`,
147the exception name and the parenthesized list of argument expressions.
148```
149throw NAME(ARGUMENTS)
150```
151When the throw expression is evaluated all the arguments are evaluated
152and then the exception is thrown.
153If the handler responds to it, that is the value the expression evaluates to.
154If an unwinding is triggered during the handling,
155the unwinding starts at this expression.
156
157The parameter types and response type are not explicitly given.
158Since the exception must be given in context, it can resolve the expression
159against the list of possible exceptions. Annotation casts can be used for
160disambiguation when needed, but it shouldn't be a common occurrence.
161
162Although throws are expressions they can, and in some cases should, be used
163at the top level as a statement.
164+ Value Returning: These should only be used as expressions, it is why they
165 are expressions, so the response can be captured and used.
166 There should be a warning if the value is not captured.
167+ Void Returning: This can be use either as an expression (in a place where
168 a void expression is allowed) or as a statement.
169+ Non-Returning: As control will never return, they could be put anywhere
170 but should only be used at the statement level for clarity of control
171 flow.
172
173Value Throw Example (lookup):
174Here is a simple example of a throw that both passes a value and uses the
175response. If the lookup finds a value, it is returns it, otherwise it uses
176a throw to try and find a default value.
177```
178forall(K, V | relational(K)) throws(V NotFound(K &));
179V lookup(map(K, V) const & mapping, K & key) {
180 // Internal code equivilent too:
181 node(K, V) * node = lookup_internal(mapping, key);
182
183 return ((node) ? node->value : throw NotFound(key));
184}
185```
186
187No Return Throw Example (iterator):
188An iterator that uses an exception to note that the iterator is empty.
189```
190int next(Iter & iter) throws (noreturn end_of_iteration()) {
191 if (iter.size <= iter.position) {
192 throw end_of_iteration();
193 }
194 ++iter.position;
195 return iter.data[iter.position - 1];
196}
197```
198
199### Try Statement
200The exceptions are handled by try statements.
201
202The try statement is made up of the try keyword, the try block
203and then one or more catch clauses.
204```
205try {
206 BODY
207} catch NAME(ARGS) {
208 HANDLER
209}
210```
211When the try statement is executed, the BODY is evaluated and then control
212continues after the try statement. The try statement is the same as wrapping
213the BODY in a compound statement unless an exception is thrown while BODY
214is being executed.
215
216When an exception is thrown in the BODY (either directly or indirectly)
217that matches one of the catch clauses then that exception is handled by this
218try statement. This means the exception does not continue outwards from here.
219That binds to the values passed to the throw to the names in ARGS and then
220runs the HANDLER code.
221
222Within the HANDLER, you can also respond to the exception with a resume
223statement, which is passing to it the response value.
224```
225resume EXPR;
226```
227(We also allow just `resume;` for void responses.)
228You cannot respond to a noreturn statement, so you cannot use a resume
229statement inside a handler for a noreturn expression.
230
231Value Try Example (lookup):
232Although you would probably want to implement a separate lookup with default
233function, you could implement one as a wrapper:
234```
235forall(K, V | relational(K))
236V lookup(map(K, V) const & mapping, K & key, V & defaultValue) {
237 try {
238 return lookup(mapping, key);
239 } catch NotFound(_key) {
240 resume defaultValue;
241 }
242}
243```
244
245Void Resume/Terminate Example (Out of Memory):
246Here is an example of void resume (not passing back a value) and also a
247termination, where the handler can trigger unwinding.
248```
249try {
250 ...
251} catch NoMem(size_t target_size, Buffer * buff) {
252 if (void * new_buff = realloc(buff->data, target_size)) {
253 buff->data = new_buff;
254 resume;
255 }
256 // If the realloc fails, we go to a recovery path.
257}
258```
259Here, the error cannot be fixed with information, but rather a correction to
260the container state. This also shows how resumption is optional (for both
261void and value throws) and the handler can instead decide to terminate and
262unwind the stack.
263
264No Return Try Example (succ):
265There are few functions that are very hard to make complete, any could
266require an awkward check before calling it, or the function can report that
267with an exception.
268
269For example, a checked successor function for enumerations:
270`SomeEnum succ(SomeEnum) throws(noreturn out_of_range())`
271
272This can be used as a check on a loop:
273```
274int sum = 0;
275try {
276 SomeEnum e = minimum;
277 for () {
278 e = succ(e);
279 sum += transform(e);
280 }
281} catch OutOfRange() {
282 return sum;
283}
284```
285
286This pattern could also be used with iterators and be wrapped up in a
287control-flow structure for ease of use, like with Python's StopIteration.
288
289### Rethrow (Conditional Catch)
290Sometimes the decision if an exception can be handled does not come down
291to the location in code and the exception in question; which is how they
292base try statements decide it.
293
294This could be implemented one of two ways: a rethrow or conditional catch.
295The conditional catch is a bit more straightforward so we will start there.
296
297A conditional catch is part of a conditional clause, and adds an if keyword
298followed by a parenthesized boolean expression, the condition.
299```
300catch NAME(ARGS) if(COND) {
301 HANDLER
302}
303```
304When the exception matches a catch clause, after the arguments are bound
305to their names but before it commits to handling the exception,
306the condition is evaluated.
307If the condition evaluates to true the handler runs,
308otherwise the exception continues to propagate out and up.
309
310This is a change from the existing syntax `catch NAME(...; ...)` because
311the declaration section of the clause (before the `;`) now is a comma
312separated list, which could make it harder to see the `;` separator.
313That might be fine but it was enough to change my mock-up syntax.
314
315Conditional Catch Example (special key):
316Although the predicate can be as complex as it needs to be, using catch
317arguments, local varaibles and function calls, it is ultimately just a
318conditional expression:
319```
320catch NotFound(key) if (is_special_key(key)) {
321 resume make_special_value(key);
322}
323```
324
325The rethrow is an equivalent feature, but is organized differently, in a way
326that is more flexible but has the same structure. When the rethrow statement
327(a `throw` with no exception) is executed the handler stops executing and
328the exception propagation continues.
329```
330catch NAME(...) {
331 ...
332 throw;
333 ...
334}
335```
336Unlike in some other languages, a rethrow just continues the original throw
337and doesn't create a new throw with the same or a new exception.
338
339Rethrow Example (special key):
340```
341catch NotFound(key) {
342 if (is_special_key(key)) {
343 resume make_special_value(key);
344 }
345 throw;
346}
347```
348
349Rethrow Variable Example (metadata):
350The rethrow can weave in other code into the check more easily.
351```
352catch NotFound(key) {
353 KeyData meta = get_metadata(key);
354 if (is_set("ignore-default", meta)) {
355 throw;
356 }
357 resume get_string("default", meta);
358}
359```
360
361### Polymorphism
362Functions polymorphic over exceptions in throws signature.
363
364We extend forall clauses to include a new type of polymorphic parameters,
365that declare exception sets: `exception VAR`. The declared variable may be
366used in any throws clause, as you would include an exception in the list.
367
368For example, a higher order function that passes all of its exceptions
369through to its caller works like this:
370```
371forall(T, [N], exception E) throws(E)
372void foreach(void op(T &) throws(E), array(T, N) & data) {
373 for (i; N) op(data[i]);
374}
375```
376
377It is important to note it is an exception set, not a single exception. If
378the inner function throws one or more exceptions, then foreach throws the
379same exceptions. If the inner function doesn't throw anything, then foreach
380doesn't throw anything and its caller doesn't have to handle anything.
381
382It should be noted that these exception sets to not interact with concrete
383exceptions within the polymorphic functions themselves. They are logically
384"packed" when they move from monomorphic to polymorphic code and "unpacked"
385when it moves back from polymorphic to monomorphic code.
386
387A new type of polymorphic parameter is added `exception` which is polymorphic
388not on an exception, but a possibly empty of exceptions.
389A set of exceptions is can be used in an exception signature in the same
390way a single exception can be and are traced from callee to caller in the
391same way a single exception is.
392They do not interact with throws or catches as those work on concrete types.
393At the top and bottom of the polymorph call stack concrete functions will
394throw and catch the exception, passing through the polymorphic section.
395
396Polymorphic Error Example (foreach):
397Here is a simple example of polymorphism over a higher order function.
398```
399void print_widget(Widget &) throws (void FormatError(Widget &));
400
401forall(T, [N], exception E) throws(E)
402void foreach(void op(T &) throws(E), array(T, N) & data) {
403 for (i; N) op(data[i]);
404}
405
406// Incorrect wrapping:
407forall([N])
408void print_widgets_ERROR(array(Widget, N) & widgets) {
409 // FormatError not handled.
410 foreach(print_widget, widgets);
411}
412
413forall([N])
414void print_widgets(array(Widget, N) & widgets) {
415 try {
416 foreach(print_widget, widgets);
417 } catch FormatError(Widget &) {
418 sout | "Opaque Widget";
419 resume;
420 }
421}
422```
423
424### Exception Tunnelling
425Exceptions can pass through functions they should not interact with.
426
427(Credit to Mike for the original idea.)
428
429Higher order functions that take functions as parameters can "erase"
430exceptions from the throws list (including all of them).
431If it does so those exceptions can no longer be handled by that function or
432any of its callees. If the passed function raises any exceptions, they
433skip forward and appear from the call expression where the "erasing" happens.
434
435Here is an overview of some interactions:
436```
437int high(int op(int, int) throws(A, C)) throws(D);
438int passed(int, int) throws(A, B);
439
440int res = high(passed);
441```
442+ The exception(s) A are not thrown from this expression.
443 Thrown by passed and handled by high. Either by being caught by high (or
444 some other function between it and the calling of passed) or by also
445 being part of D.
446+ The exception(s) B are thrown from this expression.
447 These are the tunnelled exceptions that pass from the call site of passed
448 to this call site of high.
449+ The exception(s) C are not thrown from this expression.
450 This is just to show that throwing extra exceptions is fine.
451+ The exception(s) D are thrown from this expression.
452 No interaction with exception tunnelling, they come from high.
453
454Tunnelling Error Example (foreach):
455Here is an example of an exception tunnelling though a higher order function.
456```
457void print_widget(Widget &) throws (void FormatError(Widget &));
458
459forall(T, [N])
460void foreach(void op(T &), array(T, N) & data) {
461 for (i; N) op(data[i]);
462}
463
464// Incorrect wrapping:
465forall([N])
466void print_widgets_ERROR(array(Widget, N) & widgets) {
467 // FormatError not handled.
468 foreach(print_widget, widgets);
469}
470
471forall([N])
472void print_widgets(array(Widget, N) & widgets) {
473 try {
474 foreach(print_widget, widgets);
475 } catch FormatError(Widget &) {
476 sout | "Opaque Widget";
477 resume;
478 }
479}
480```
481
482Tunnelling Thunk Expansion (sort):
483Although the implementation is not set,
484the behaviour of tunnelling can also be described by creating a thunk.
485
486Consider the following example were a sort is called with a less than
487operation that throws in some way the sort did not expect.
488```
489forall(T, [N] | { bool ?<?(T &, T &); })
490void sort(array(T, N) & data);
491
492struct SomeType { ... };
493
494bool ?<?(SomeType & lhs, SomeType & rhs) throw(RType except(AType)) {
495 ...
496}
497
498void example_function() {
499 ...
500 sort(data);
501 ...
502}
503```
504
505Internally this creates an thunk that contains some special code, probably
506in the form of some special annotations, but we can represent most of it
507with a try statement.
508```
509void example_function() {
510 ...
511 RaisePoint: bool ?<?'thunk(SomeType & lhs, SomeType & rhs) {
512 try {
513 return ?<?(lhs, rhs);
514 } catch except(AType data) {
515 // Captures the frame and code location.
516 RType value = magic_resume_from RaisePoint except(data);
517 resume value;
518 }
519 }
520 sort(data);
521 ...
522}
523```
524
525Thunks do lead to issues in life-time management and contribute to the general
526thunk problem. (Thunks may also be created when we pass a function to a
527function pointer that throws more exceptions, even though there should be
528no change in behaviour.)
529
530### Finally Clauses
531You could add `finally` clauses to try statements to run when they unwind.
532
533This is a classic tool used to ensure proper scoping of resources, a dual to
534destructors which are a OO-ish (not true OO) way of handling the problem.
535We can bring the syntax over to the new system:
536```
537try
538 TRY_BLOCK
539finally
540 FINALLY_BLOCK
541```
542
543However, although this does fire with the same timing, when the try statement
544is removed from the stack, this does interact with the change in unwind
545timing in a perhaps unexpected way. It will run after the exception passes
546it and the handler decides to unwind.
547
548A separate construct would be needed if you wanted to run code when the
549search leaves that section of the stack, because then you might need a paired
550construct to run code when you re-enter it.
551
552Details And Additional Notes
553----------------------------
554
555### Performance (Where to Pay Costs)
556Although the behaviour has been laid out, the performance has not. We would
557like "good" performance of course, but there is a choice between fast tries
558and fast throws.
559
560The current system uses fast tries, which means it is designed for very rare
561cases, almost always errors.
562
563Fast throws mean that entering and leaving a try block is now more expensive,
564but it means throws can be common. Exceptions can now be used for alternate
565returns and other relatively frequent cases.
566
567A single choice doesn't have to be made for the entire language. It could
568choose differently for different cases or even let the user decide for
569different exceptions.
570
571### Implementation
572The implementation has not been specified. See the performance section for
573fast tries vs fast throws.
574
575The current implementation could be adapted if we want to stay with fast
576tries. The exception could become an id and a pointer to stack allocated
577storage. If the id can even be contextual and if a function doesn't handle
578the exception, it gives a new id to pass onto its owner.
579
580(You might need to modify libunwind to allow returning successfully from an
581unwind call without unwinding, but last time I checked that change could be
582made in a platform independent part of libunwind.)
583
584### Polymorphic Tracing Details
585For simplicity, polymorphic exceptions should probably just be black boxed
586and not interact with any other handlers or exceptions except at the edges
587where it is boxed and unboxed.
588
589The programming language Flix has effect polymorphism (but not polymorphic
590effects, avoid confusing those), which has many of the same design
591constraints.
592
593### Polymorphism Ease of Use
594It would be nice if the language's defaults were set up in such a way that
595the common usage pattern is the simplest/most natural way to write a
596function.
597
598For a point of comparison, consider otype (object-type) parameter `(T)` as
599compared to the dtype (data-type) parameter `(T &)`. Although dtype is
600the more primitive/simplest form, the simplest syntax goes to otype, which is
601the most common form. Something similar could be done for higher-order
602polymorphic functions.
603
604Something like if a polymorphic function takes another function and none of
605them have explicit exception signatures, a exception polymorphic argument
606could be added and all the functions may throw that. Details may depend on
607how widening works. Also, you must be able to explicitly say that functions
608do not throw, doing this to one of the functions (by convention, the main
609function, not any function parameters) would disable this feature.
610
611The exact rules should come from usage patterns that emerge once the feature
612is implemented.
613
614### Unwind Timing
615Although the current implementation can mimic both termination and resumption
616exceptions. There is one major difference in the behaviour of the termination
617throw and catch, the unwinding of the stack now occurs after running the
618handler instead of before.
619
620When the handler is short and operates locally, this is unlikely to matter.
621In fact, this can fix some life-time issues (see implementation).
622However, if there is a lot of work in the handler we haven't cleared the
623stack up or freed up other resources (the destructors haven't run, things
624are still taking up stake space, etc.).
625
626If this turns out to be a problem, there should either be a way to force an
627unwind in the handler, or make sure local control flow can take us to an
628external block to handle the exception.
629
630### Exceptions in Traits
631In another way to make exceptions like assertions, they could be added into
632traits. They can probably just mixed into the bodies with assertions.
633
634This would generally just be a short-hand, but also offers a tool to
635organize exceptions like in effects, which can have multiple options.
636
637### Cancellation
638This system is narrower than the current exception system. Rather than trying
639to cover every use case directly/with one feature, I would propose another
640feature to handle some of the important cases not seen covered here.
641
642The biggest other cases are the non-recoverable cases. These may not be
643caught are all (in which case they are the same as an abort) or are caught
644and the operation is abandoned. Control then continues from a main loop
645(as in an engine or server dispatcher).
646
647For these cases, an unchecked exception that only supports catch-all or our
648cancellations might work better. They only information they need to carry is
649enough for a user facing error message.
650
651### THE CHECKED EXCEPTION PROBLEM
652Yes, this gets an all caps title (it almost got its own section) because the
653single biggest concern about this system is the additional workload of
654maintaining checked exception lists on all functions. This is a real concern
655but I believe this proposal avoids the worst of it.
656
657One part of "the worst of it" is the annotating of many functions that call
658some distant helper function that throws an unhandled exception.
659At this point, it can be impossible to preform any sort of detailed recovery.
660In these cases the exceptions should be converted to cancellations.
661
662In my experiance the majority of exception throws (writen, not fired) are
663effectively assertion failures that are only catchable exceptions out of
664princible (or are in a long running program were individual operations can
665just be written off). Cancellation replaces all of these cases instead of
666the new exception system.
667
668Another part is the interaction with higher order functions.
669If a higher order function only works with particular exceptions thrown from
670the function passed to it, it loses a lot of its flexibility.
671Both polymorphism and exception tunnelling are ways to address this problem.
672These stratagies both mean you don't need many different variations of those
673functions, and hopefully have little (or no) additional code.
674
675### Default Exception Handlers
676Handling every exception has often proven more work than people want to put
677in. Now, this may not be an issue with fewer more significant exceptions,
678but if it is default exception handling within a program might help.
679
680This could be a universal thing, such as triggering an abort or cancellation
681if an exception is not handled, or something narrower using assertions or
682configuration on the exception itself.
Note: See TracBrowser for help on using the repository browser.