I think there are two key questions to answer. The first is whether these new TCP-preserving closures should use the same syntax on the caller side, and the second is whether they should use the same traits and types on the callee side.
As for the first question: one could imagine that we might use the same closure syntax (|| xyz), and try to infer (somehow) whether the closure ought to be TCP-preserving or not. Even if we could make this work, it seems like this is a recipe for confusion, since if you have something like foo(|| return 22) you wonât know, without examining the signature of foo, where the return will return to. So letâs say instead that the syntax for a TCP-preserving closure is different, as you suggested â perhaps do || ....
Now we have to wonder about the callee side. If you have a TCP-preserving closure that returns type T in the normal (non-break, non-continue, etc) case, does it implement the trait FnOnce() -> T? (or FnMut, whatever). On the one hand, if it were to do so, that would mean that TCP-preserving closures could be used with all existing APIs, which is pretty cool.
On the other hand, it would imply an implementation based on unwinding, which implies some amount of implementation cost (and starts to chip away at the idea that unwinding paths can be expensive, since they wonât be used in the common case). It also makes me worried because it introduces ânon-localâ reasoning: when you call a closure, itâs possible that the closure will never return (in the normal sense) and instead unwinding will take you to some outer scope (but without a panic). This implies that functions like this:
fn wrap<F,R>(f: F) -> R where F: FnOnce() -> R {
do_something();
let r = f();
undo_something();
r
}
are potentially broken, since undo_something may never execute. Put another way, it means that all Rust code will have to be exception-safe, even in the non-unwinding case. Experience with C++ suggests this will not work out well in practice. (Moreover, this will chip away at the benefits of -C panic=abort, since we may need landing pads anyhow.)
An alternative is that a TCP-preserving closure either implements new closure traits or has some special return value (e.g., FnOnce() -> Control<T> instead of just T). A return value is appealing both because three closure traits are enough for any language and because it means that wrapper functions like the one above âjust workâ in the way intended, though I guess there might be other functions that maybe do not work as you might expect (e.g., the motivating example, I think, of foo.iter().map(do || x?).collect().
Anyway, I know we talked about this a while back â just sketching out the possibilities as I see them currently.