Changeset 63917ab
- Timestamp:
- Jun 25, 2026, 11:40:15 AM (17 hours ago)
- Branches:
- master
- Parents:
- 9d7a19f
- File:
-
- 1 edited
-
doc/proposals/exceptions-assert.md (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
doc/proposals/exceptions-assert.md
r9d7a19f r63917ab 61 61 throws no exceptions, that means all throws will be matched to a try. 62 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 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: 111 370 ``` 112 371 forall(T, [N], exception E) throws(E) 113 372 void foreach(void op(T &) throws(E), array(T, N) & data) { 114 for (i; N) op(data[i]); 115 } 116 ``` 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. 117 386 118 387 A new type of polymorphic parameter is added `exception` which is polymorphic … … 125 394 throw and catch the exception, passing through the polymorphic section. 126 395 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) 396 Polymorphic Error Example (foreach): 239 397 Here is a simple example of polymorphism over a higher order function. 240 241 ``` 242 void print_widget(Widget &) throws (noreturn FormatError()); 398 ``` 399 void print_widget(Widget &) throws (void FormatError(Widget &)); 243 400 244 401 forall(T, [N], exception E) throws(E) … … 247 404 } 248 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 249 413 forall([N]) 250 414 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 ``` 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: 262 465 forall([N]) 263 void print_widgets(array(Wiget, N) & widgets) throws (noreturn FormatError()) { 264 foreach(print_widget, widgets); 265 } 266 ``` 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. 267 551 268 552 Details And Additional Notes 269 553 ---------------------------- 270 271 ### Rethrows and Conditional Catch272 As extension to the core system, we could add rethrowing or conditional273 catching to try blocks. There purpose is the same, to allow a handler to274 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, exception277 propogation continues up the stack. As far as control flow goes this should278 be the same as not catching it at all.279 280 A rethrowing or conditional catch does not remove the exception it could281 catch and handle from the unhandled exception set. Because it may not, so282 the type system has to account for that.283 554 284 555 ### Performance (Where to Pay Costs)
Note:
See TracChangeset
for help on using the changeset viewer.