# Thoughts on async drop, cancellation safety, and defer ## no_drop lint We can likely get pretty far with a lint that prevents implicit drop of a type. It can look for cases like * Going out of scope * Passing by value to a function that doesn't know the type The type can provide a method like `destroy(self)` that allows the lint and performs the necessary cleanup. This method can be async. > We can even couple this with an `[Async]Destroy` trait or similar so that generic code can accept types with trait bounds that tell you it is going to perform the necessary cleanup, but I'm not sure this is going to be useful. ### How does this interact with cancellation? Like today, cancellation will run default destructors, not the "fancy" ones. If you want to run these, put them in a `defer` block. We can lint on await points that might cause these destructors to be run, so people know to use `defer`. ### Relation to `Async` trait proposal This might be able to subsume the `Async` trait. The idea is that we put `#[no_drop]` on most new async code but also provide a cancellation request mechanism with a means to poll to completion. This would make it an (allow-able) error to pass one of these futures into most of the combinators we use today, but they could accept an `AsyncDestroy` or similar and promise to handle cancellation for those types correctly. Similar to the `Async` trait, we could blanket impl this for all types that already implement `Future`. So I guess this is essentially the `Async` trait proposal but with the option to use `#[allow]` for combinators etc. that don't support the new trait yet. ## Cancellation * Add a cancellation-request mechanism for future/async fn * Make cancellation "bubble up" at await points * When handling cancellation, we run all `defer` blocks, which can be async ## Key insights * We need non-linear control flow for handling cancellation and early return. We also want it to be async * Drop can serve this purpose in sync Rust, but isn't a good fit for async * Panics can be handled separately from cancellation * Cancellation logic can go in awaits and directly invoke defer logic --- 2022-09-22 Key things about using a lint for `#[no_implicit_drop]`: * You don't have to be perfect: This "doubles down" on the fact that destructors are not guaranteed to run in Rust * You can `#[allow]` * You can do analysis that would be hard to encode in the trait system For the last point, consider the example: ```rust #[no_drop] trait Database {} impl<T> Option<T> { fn new(inner: T) -> Self {} fn unwrap_or<T>(self, default: T) -> T {} } impl<DB: Database> Thing { fn foo<DB: Database>(&mut self, db: DB) { self.opt_db = Option::new(db); // no error! .. let db = self.opt_db.take().unwrap_or(DB::new()); // error let db = self.opt_db.take().unwrap_or_else(|| DB::new()); // also error? } } ``` `Option::new` takes `T` by value, but does not mark it `#[no_drop]`. However, we can see that it only has `T` in the environment, there are no ways to actually create a value of type `T`, and it returns a value containing a type `T`. Therefore we can reason that this function must return the `T` that was supplied and cannot drop it. Note that if `Option` required `T: Default` we could not do this. The call to `unwrap_or` is different. It has access to another value of type `T`, so we cannot say that it returns the value that was passed to it (and indeed it doesn't always). `unwrap_or_else` doesn't accept an actual value of type `T`, but we nevertheless have to assume the closure we provide will be called. Our options are: * We can get around this by adding an `#[allow]` on `unwrap_or_else` itself, but that's the kind of thing we're *trying* to avoid. * We can perform interprocedural analysis. In this case we would want to know something like `uses_result_of(Option::unwrap_or_else, arg: 1)`. In complicated cases we can fall back to returning false. We can also choose whether or not to ignore panics. * Or we can give up and not report anything in cases like this if we can't find examples of the kind of bug we're trying to avoid occurring like this. What if `Option::replace_with_none()` existed? We have to assume that anything that has mutable access to the option (and doesn't return a T or reference to T, without any other way to produce a T) could drop the inner value. But that's roughly what we want, I think. The Option itself must now be marked `#[no_drop]` internally due to its use of `T`. This means anywhere we pass the Option must be aware of the no_drop-ness, or we'll need an `#[allow]`. Ergonomically, it's also nice that we can take any bound of a trait with `#[no_drop]` as confirmation that the function is "aware" of the restriction instead of requiring you to write `?ImplicitDrop` everywhere. (Would we get that thanks to implied supertrait bounds? I don't think we would, I don't think that's how it works for `?Sized`.)