On 13-04-27 08:49 AM, Lee Braiden wrote:
> Hi all,
> 
> This is going to be long, but I've tried to organise my thoughts clearly
> and as succinctly as possible.

I've read your email a few times and I _think_ it mostly consists of a
request to add catchable exceptions to the language. Which we won't do
(or I won't do, and I will resist strongly as I think it will hurt
users, performance and correctness of code). I will reply -- somewhat
repetitively, I'm afraid -- to minor points but want to clarify a few
things in advance:

  - Rust is not gaining any sort of "new" transactional execution model
    in the current design iteration. Maybe rust-3.0 or rust-STM by a
    different team, but it's not part of this group's (the owners of
    the mozilla/rust repo) work-plan

  - Similarly, any sort of "catching" of an unwind will only ever be
    available as an unsafe implementation-of-the-task-model thing;
    the whole point of partitioning heaps into tasks is to provide
    a comprehensible isolation boundary. If you catch unwinding in a
    local heap and attempt resumption, you have no idea what state it's
    in. It breaks the mental model. If it makes you feel better,
    think of tasks _as_ transactions. They don't isolate I/O like
    the STM monad, but we don't isolate I/O in general in this language.
    That's not going to change.

  - Conditions already push and pop themselves, and can already call
    the outer condition handler in the chain. Or other conditions.

  - We have a macro system that can be used to adjust parts of the
    condition-trapping syntax. But we also avoided "try/catch" syntax
    specifically because it suggested a catchable-unwind behavior from
    exceptions that we're not providing. Intentionally. Criticizing it
    for not looking like try/catch is beside the point.

Specific replies follow:

> So, FWIW, since aspirational examples were mentioned, I would aim for
> something more like this:

... try / handle / catch / raise / finally example ...

This example has too many moving parts. We currently have _one_ moving
part in our condition-handling system: dynamically scoped TLS, that you
grab a closure out of and call. That's it. If it fails, your task is over.

> In other words, I'm imagining a language where:
> 
>   * You have Conditions, built by ConditionFactories.  The casual user
>     doesn't need to care, he just uses the API and handles its Conditions.

Currently conditions are static structs held in modules. They name a key
that can be used to bind a handler to TLS and look up / call that
handler. API users can ignore all this if they don't wish to use the
condition: raised conditions invoke default handlers, which usually
fail. Users who wish to handle them can look for the conditions.

>   * ConditionFactories interface with the call stack to allow building a
>     stack of Handlers for the Condition.

That is how our current condition system works. Each installed handler
chains to the previous one.

>   * Conditions are ALSO usable just like exceptions: they're /throwable/
>     & /catchable/, by naming the specific class you want to catch, or by
>     naming a base class/trait.

This requires all sorts of machinery I do not want to add, and would
break the mental model of tasks that execute linearly until failure or
completion.

>   * The only real difference between catching a thrown Condition and
>     handling one would be that:
> 
>       o In-place Condition handling is done with a closure or a function
>         call

The current system does this.

>       o Thrown Conditions first use a special in-place Handler that
>         unwinds the stack, passing the condition up the stack to a catch
>         block, or, if no more catch blocks exist, to terminate() (or
>         Rust's equivalent).

This I do not want to add.

>   * If you don't set a Handler, you get the default behaviour defined in
>     that Condition class, or, if that is not set, the language/library
>     default for /any/ Condition class, which is probably throwing, with
>     the *option* of catching.

The current system does this.

>   * While a Condition can be caught by catch() {}, handled by a
>     function, or handled by a closure, all three methods take a
>     compatible argument: a Condition trait, or a variation.  This allows
>     code sharing, aids familiarity, etc.

Two of these are in practice identical (function / closure handler) and
done by the current system; the third, catching, I do not want to add.

>   * Condition handling code ends by calling one of:
> 
>         cond.retry()
> 
>             To attempt to retry the code that raised the condition.  I
>             confess, I've little knowledge of how this works in Rust,
>             but I imagine it's something like a guarded try block, with
>             potential to become like STM.

If retrying makes sense in a condition-raising context, it is easy to
define the return from a condition as an enum including "Retry". The
signaling site can then retry the operation. That is as far as we're
going to go with the concept of retrying, not STM or rewindable
execution. That's for other research languages to experiment with.

>         raise cond
> 
>             To throw the condition to a catch () block further up the stack

The current system does this.

>   * The Condition trait would support retrieving the stack frame, the
>     file/line which the condition occurred at, etc., through methods.

We do not presently reify this at the raise site, but rather at the fail
site. We could do so at the raise site also.

>   * In the case of catch blocks, some of that information would have
>     been built up during the stack unwind, before the information was lost.

This I do not want to add.

>   * Conditions passed to in-place code where no stack unwinding is
>     needed, would LOOK the same to the user, but they'd be FASTER, because:
> 
>       o The handling code would (interchangeably) be closures or functions.

These are the same in any such implementation.

>       o That code would ideally be inlined whenever advantageous.

This is only possible if the handler assignment and condition-raising
happens in the same static code extent. Which is pointless: if you're
there, you might as well just call the handler directly. The point of
condition/handler separation is caller/callee decoupling: indirection.
It never inlines.

>       o The Condition object would not be built up ahead of time, since
>         it might not be needed, and all the information would be there
>         in the current and previous stack frames anyway.  There's no
>         need to package all that information unless the Condition is thrown.

That is how the current system works. It doesn't build condition objects
or unwind. It just fetches a closure from TLS and calls it.

>   * Some kind of tracking of previously set handlers is needed.  This
>     could be as simple as setting tempory variables.  However, it could
>     be useful to allow a full stack of handlers, on a per-call-stack
>     basis, so that raise cond in inline handlers calls the previous
>     handler, until it reaches the throw handler, and then catch blocks
>     take over.  Using raise cond in those rethrows cond up the stack.

That is how the current system works.

> To me, the extra keywords/work seem like a small price, for what we'd
> gain in clarity, elegance, and flexibility.  We'd have the best of two
> popular error handling models, to choose from at will, with a nice
> syntax, too.

It's not the keyword(s) that I don't want to add; it's breaking the
mental model and adding the extra semantic and runtime machinery.

We've been over this many times. I will repeat this argument as often as
necessary, because IMO it's important to writing sensible code.
Catchable-exception-safety is not something we want to require in normal
code. Failure safety (releasing resources best-effort) is hard enough.

-Graydon

_______________________________________________
Rust-dev mailing list
Rust-dev@mozilla.org
https://mail.mozilla.org/listinfo/rust-dev

Reply via email to