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