Disclaimer: shower thought, of a sort, I have no idea how to encode the type...
I have been wondering if maybe defer should be reduced in functionality.
One of the key motivation I mentioned in the starting post is that scope guard libraries today struggle to offer the functionality due to the closures borrowing early, not late.
What if defer was just a closure modifier, like move, to specify an early bind, late (deferred) borrow? (instead of the regular early bind, early borrow)
The functionality to execute during drop glue would still be covered by combining the resulting closure with a scope-guard:
fn foo_it() {
let mut file = ...;
let _guard = scope_guard(defer || file.flush().expect("Flush"));
file.write(...);
}
This would not cover the interception of the value, and it may be more difficult to implement than a special-case defer statement, but the functionality would be more orthogonal -- not overlapping with drop glue.
Should the borrow be restricted to the scope of execution of the closure -- it starts when the closure starts executing, stops when the closure stops executing, repeatedly -- then it may also open the door to patterns such as:
let mut baz = ...;
let on_foo = defer |foo| baz.push_foo(foo);
let on_bar = defer |bar| baz.push_bar(bar);
do_the_thing(on_foo, on_bar);
Today, such patterns require either unsafe code or cells.
This is an interesting idea, but how would the compiler tell where the borrowing needs to happen? It doesn't know exactly what scope_guard is, and borrowing happens statically at compile time. For all the compiler knows, scope_guard will immediately call the closure (the borrow checker can't see through a function call). It would possibly work with a macro scope_guard! though.
The simpler possibility, I guess, would be for the compiler to be optimistic and assume that any interaction with the closure requires -- and recursively, with anything which contains the closure -- may require the borrowing to occur.
Indeed, and that is perfectly fine. The borrowing is ephemeral -- limited to the execution of the closure itself -- so as long as borrowing is allowed during the construction and destruction of the scope-guard, it's fine.
It gets more complicated if a the scope-guard comes with a dismiss function, since unless a specific marker is introduced, calling any function on the scope-guard could potentially invoke the closure, and thus require borrowing. Though even without special treatment, there are likely many situations where dismiss could be called without issues.
The compiler can't know that scope_guard doesn't start a background thread and keep calling the closure over and over while the rest of the code runs. Or maybe scope guard stores the closure in a thread local that some other function you call then uses. Etc.
I don't see any way for early binding/late borrow to work without either special language support for scope guards or moving everything to a macro (so the compiler can see inside and see exactly what happens to the closure).
To expand a bit, I think this train of thought of 'only' annotating closures or async blocks goes in the direction to something similar to Kotlin's inline functions and how it treats lambdas passed to them, where a lambda can be auto-promoted to an async lambda (and also can do non-local return). This was also obsevered by @rpjohnst earlier in the thread.
I think something like that would be required to support scope_guard(async defer { ...}) plus additionally it would need to extend to drop as well, ie. an inline drop.
Edit: By inline drop I mean a special type of drop that can be (has to be) inlined into the containing scope. A type with an inline drop could not be moved out of the containing scope to be dropped elsewhere. Arguably, such a narrower / more restricted variant of drop would be easier to be made async.
I don't think so. Someone smarter than me feel free to correct this, but afaik drops aren't desugared like this, they're really just a method. They might be inlined as part of optimizations but that happens way past borrowck.