| # Drop elaboration | |
| ## Dynamic drops | |
| According to [Rust Reference][reference-drop]: | |
| > When an initialized variable or temporary goes out of scope, its destructor | |
| > is run, or it is dropped. Assignment also runs the destructor of its | |
| > left-hand operand, if it's initialized. If a variable has been partially | |
| > initialized, only its initialized fields are dropped. | |
| When building the MIR, the `Drop` and `DropAndReplace` terminators represent | |
| places where drops may occur. | |
| However, in this phase, the presence of these | |
| terminators does not guarantee that a destructor will run. | |
| That's because the target of a drop may be uninitialized (usually because it has been moved from) | |
| before the terminator is reached. | |
| In general, we cannot know at compile-time whether a variable is initialized. | |
| ```rust | |
| let mut y = vec![]; | |
| { | |
| let x = vec![1, 2, 3]; | |
| if std::process::id() % 2 == 0 { | |
| y = x; // conditionally move `x` into `y` | |
| } | |
| } // `x` goes out of scope here. Should it be dropped? | |
| ``` | |
| In these cases, we need to keep track of whether a variable is initialized _dynamically_. | |
| The rules are laid out in detail in [RFC 320: Non-zeroing dynamic drops][RFC 320]. | |
| ## Drop obligations | |
| From the RFC: | |
| > When a local variable becomes initialized, it establishes a set of "drop | |
| > obligations": a set of structural paths (e.g. a local `a`, or a path to a | |
| > field `b.f.y`) that need to be dropped. | |
| > | |
| > The drop obligations for a local variable x of struct-type `T` are computed | |
| > from analyzing the structure of `T`. If `T` itself implements `Drop`, then `x` is a | |
| > drop obligation. If `T` does not implement `Drop`, then the set of drop | |
| > obligations is the union of the drop obligations of the fields of `T`. | |
| When a structural path is moved from (and thus becomes uninitialized), any drop | |
| obligations for that path or its descendants (`path.f`, `path.f.g.h`, etc.) are | |
| released. | |
| Types with `Drop` implementations do not permit moves from individual | |
| fields, so there is no need to track initializedness through them. | |
| When a local variable goes out of scope (`Drop`), or when a structural path is | |
| overwritten via assignment (`DropAndReplace`), we check for any drop | |
| obligations for that variable or path. | |
| Unless that obligation has been | |
| released by this point, its associated `Drop` implementation will be called. | |
| For `enum` types, only fields corresponding to the "active" variant need to be dropped. | |
| When processing drop obligations for such types, we first check the | |
| discriminant to determine the active variant. | |
| All drop obligations for variants besides the active one are ignored. | |
| Here are a few interesting types to help illustrate these rules: | |
| ```rust | |
| struct NoDrop(u8); // No `Drop` impl. No fields with `Drop` impls. | |
| struct NeedsDrop(Vec<u8>); // No `Drop` impl but has fields with `Drop` impls. | |
| struct ThinVec(*const u8); // Custom `Drop` impl. Individual fields cannot be moved from. | |
| impl Drop for ThinVec { | |
| fn drop(&mut self) { /* ... */ } | |
| } | |
| enum MaybeDrop { | |
| Yes(NeedsDrop), | |
| No(NoDrop), | |
| } | |
| ``` | |
| ## Drop elaboration | |
| One valid model for these rules is to keep a boolean flag (a "drop flag") for | |
| every structural path that is used at any point in the function. | |
| This flag is set when its path is initialized and is cleared when the path is moved from. | |
| When a `Drop` occurs, we check the flags for every obligation associated with | |
| the target of the `Drop` and call the associated `Drop` impl for those that are still applicable. | |
| This process—transforming the newly built MIR with its imprecise `Drop` and | |
| `DropAndReplace` terminators into one with drop flags—is known as drop elaboration. | |
| When a MIR statement causes a variable to become initialized (or | |
| uninitialized), drop elaboration inserts code that sets (or clears) the drop flag for that variable. | |
| It wraps `Drop` terminators in conditionals that check the newly inserted drop flags. | |
| Drop elaboration also splits `DropAndReplace` terminators into a `Drop` of the | |
| target and a write of the newly dropped place. | |
| This is somewhat unrelated to what we've discussed above. | |
| Once this is complete, `Drop` terminators in the MIR correspond to a call to | |
| the "drop glue" or "drop shim" for the type of the dropped place. | |
| The drop glue for a type calls the `Drop` impl for that type (if one exists), and then | |
| recursively calls the drop glue for all fields of that type. | |
| ## Drop elaboration in `rustc` | |
| The approach described above is more expensive than necessary. | |
| One can imagine a few optimizations: | |
| - Only paths that are the target of a `Drop` (or have the target as a prefix) need drop flags. | |
| - Some variables are known to be initialized (or uninitialized) when they are dropped. | |
| These do not need drop flags. | |
| - If a set of paths are only dropped or moved from via a shared prefix, those | |
| paths can share a single drop flag. | |
| A subset of these are implemented in `rustc`. | |
| In the compiler, drop elaboration is split across several modules. | |
| The pass itself is defined [here][drops-transform], but the [main logic][drops] is | |
| defined elsewhere since it is also used to build [drop shims][drops-shim]. | |
| Drop elaboration designates each `Drop` in the newly built MIR as one of four kinds: | |
| - `Static`, the target is always initialized. | |
| - `Dead`, the target is always **un**initialized. | |
| - `Conditional`, the target is either wholly initialized or wholly uninitialized. | |
| It is not partly initialized. | |
| - `Open`, the target may be partly initialized. | |
| For this, it uses a pair of dataflow analyses, `MaybeInitializedPlaces` and | |
| `MaybeUninitializedPlaces`. | |
| If a place is in one but not the other, then the | |
| initializedness of the target is known at compile-time (`Dead` or `Static`). | |
| In this case, drop elaboration does not add a flag for the target. | |
| It simply removes (`Dead`) or preserves (`Static`) the `Drop` terminator. | |
| For `Conditional` drops, we know that the initializedness of the variable as a | |
| whole is the same as the initializedness of its fields. | |
| Therefore, once we generate a drop flag for the target of that drop, it's safe to call the drop | |
| glue for that target. | |
| ### `Open` drops | |
| `Open` drops are the most complex, since we need to break down a single `Drop` | |
| terminator into several different ones, one for each field of the target whose | |
| type has drop glue (`Ty::needs_drop`). | |
| We cannot call the drop glue for the | |
| target itself because that requires all fields of the target to be initialized. | |
| Remember, variables whose type has a custom `Drop` impl do not allow `Open` | |
| drops because their fields cannot be moved from. | |
| This is accomplished by recursively categorizing each field as `Dead`, | |
| `Static`, `Conditional`, or `Open`. | |
| Fields whose type does not have drop glue | |
| are automatically `Dead` and need not be considered during the recursion. | |
| When we reach a field whose kind is not `Open`, we handle it as we did above. | |
| If the field is also `Open`, the recursion continues. | |
| It's worth noting how we handle `Open` drops of enums. | |
| Inside drop elaboration, | |
| each variant of the enum is treated like a field, with the invariant that only | |
| one of those "variant fields" can be initialized at any given time. | |
| In the general case, we do not know which variant is the active one, so we will have | |
| to call the drop glue for the enum (which checks the discriminant) or check the | |
| discriminant ourselves as part of an elaborated `Open` drop. | |
| However, in certain cases (within a `match` arm, for example) we do know which variant of | |
| an enum is active. | |
| This information is encoded in the `MaybeInitializedPlaces` | |
| and `MaybeUninitializedPlaces` dataflow analyses by marking all places | |
| corresponding to inactive variants as uninitialized. | |
| ### Cleanup paths | |
| TODO: Discuss drop elaboration and unwinding. | |
| ## Aside: drop elaboration and const-eval | |
| In Rust, functions that are eligible for evaluation at compile-time must be | |
| marked explicitly using the `const` keyword. | |
| This includes implementations of the `Drop` trait, which may or may not be `const`. | |
| Code that is eligible for compile-time evaluation may only call `const` functions, so any calls to | |
| non-const `Drop` implementations in such code must be forbidden. | |
| A call to a `Drop` impl is encoded as a `Drop` terminator in the MIR. | |
| However, | |
| as we discussed above, a `Drop` terminator in newly built MIR does not | |
| necessarily result in a call to `Drop::drop`. | |
| The drop target may be uninitialized at that point. | |
| This means that checking for non-const `Drop`s on the newly built MIR can result in spurious errors. | |
| Instead, we wait until after | |
| drop elaboration runs, which eliminates `Dead` drops (ones where the target is | |
| known to be uninitialized) to run these checks. | |
| [RFC 320]: https://rust-lang.github.io/rfcs/0320-nonzeroing-dynamic-drop.html | |
| [reference-drop]: https://doc.rust-lang.org/reference/destructors.html | |
| [drops]: https://github.com/rust-lang/rust/blob/HEAD/compiler/rustc_mir_transform/src/elaborate_drops.rs | |
| [drops-shim]: https://github.com/rust-lang/rust/blob/HEAD/compiler/rustc_mir_transform/src/shim.rs | |
| [drops-transform]: https://github.com/rust-lang/rust/blob/HEAD/compiler/rustc_mir_transform/src/elaborate_drops.rs |