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