Changeset 63917ab


Ignore:
Timestamp:
Jun 25, 2026, 11:40:15 AM (17 hours ago)
Author:
Andrew Beach <ajbeach@…>
Branches:
master
Parents:
9d7a19f
Message:

Rewrote part of the assertion like exceptions proposal, combining Syntax and Usage section with the Examples into Features.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • doc/proposals/exceptions-assert.md

    r9d7a19f r63917ab  
    6161throws no exceptions, that means all throws will be matched to a try.
    6262
    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 
     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:
    111370```
    112371forall(T, [N], exception E) throws(E)
    113372void 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
     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.
    117386
    118387A new type of polymorphic parameter is added `exception` which is polymorphic
     
    125394throw and catch the exception, passing through the polymorphic section.
    126395
    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)
     396Polymorphic Error Example (foreach):
    239397Here is a simple example of polymorphism over a higher order function.
    240 
    241 ```
    242 void print_widget(Widget &) throws (noreturn FormatError());
     398```
     399void print_widget(Widget &) throws (void FormatError(Widget &));
    243400
    244401forall(T, [N], exception E) throws(E)
     
    247404}
    248405
     406// Incorrect wrapping:
     407forall([N])
     408void print_widgets_ERROR(array(Widget, N) & widgets) {
     409    // FormatError not handled.
     410    foreach(print_widget, widgets);
     411}
     412
    249413forall([N])
    250414void 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
     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:
    262465forall([N])
    263 void print_widgets(array(Wiget, N) & widgets) throws (noreturn FormatError()) {
    264   foreach(print_widget, widgets);
    265 }
    266 ```
     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.
    267551
    268552Details And Additional Notes
    269553----------------------------
    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.
    283554
    284555### Performance (Where to Pay Costs)
Note: See TracChangeset for help on using the changeset viewer.