|
| 1 | +# Drop elaboration |
| 2 | + |
| 3 | +<!-- toc --> |
| 4 | + |
| 5 | +## Dynamic drops |
| 6 | + |
| 7 | +According to the [reference][reference-drop]: |
| 8 | + |
| 9 | +> When an initialized variable or temporary goes out of scope, its destructor |
| 10 | +> is run, or it is dropped. Assignment also runs the destructor of its |
| 11 | +> left-hand operand, if it's initialized. If a variable has been partially |
| 12 | +> initialized, only its initialized fields are dropped. |
| 13 | +
|
| 14 | +When building the MIR, the `Drop` and `DropAndReplace` terminators represent |
| 15 | +places where drops may occur. However, in this phase, the presence of these |
| 16 | +terminators does not guarantee that a destructor will run. That's because the |
| 17 | +target of a drop may be uninitialized (usually because it has been moved from) |
| 18 | +before the terminator is reached. In general, we cannot know at compile-time whether a |
| 19 | +variable is initialized. |
| 20 | + |
| 21 | +```rust |
| 22 | +let mut y = vec![]; |
| 23 | + |
| 24 | +{ |
| 25 | + let x = vec![1, 2, 3]; |
| 26 | + if std::process::id() % 2 == 0 { |
| 27 | + y = x; // conditionally move `x` into `y` |
| 28 | + } |
| 29 | +} // `x` goes out of scope here. Should it be dropped? |
| 30 | +``` |
| 31 | + |
| 32 | +In these cases, we need to keep track of whether a variable is initialized |
| 33 | +*dynamically*. The rules are laid out in detail in [RFC 320: Non-zeroing |
| 34 | +dynamic drops][RFC 320]. |
| 35 | + |
| 36 | +## Drop obligations |
| 37 | + |
| 38 | +From the RFC: |
| 39 | + |
| 40 | +> When a local variable becomes initialized, it establishes a set of "drop |
| 41 | +> obligations": a set of structural paths (e.g. a local `a`, or a path to a |
| 42 | +> field `b.f.y`) that need to be dropped. |
| 43 | +> |
| 44 | +> The drop obligations for a local variable x of struct-type `T` are computed |
| 45 | +> from analyzing the structure of `T`. If `T` itself implements `Drop`, then `x` is a |
| 46 | +> drop obligation. If `T` does not implement `Drop`, then the set of drop |
| 47 | +> obligations is the union of the drop obligations of the fields of `T`. |
| 48 | +
|
| 49 | +When a structural path is moved from (and thus becomes uninitialized), any drop |
| 50 | +obligations for that path or its descendants (`path.f`, `path.f.g.h`, etc.) are |
| 51 | +released. Types with `Drop` implementations do not permit moves from individual |
| 52 | +fields, so there is no need to track initializedness through them. |
| 53 | + |
| 54 | +When a local variable goes out of scope (`Drop`), or when a structural path is |
| 55 | +overwritten via assignment (`DropAndReplace`), we check for any drop |
| 56 | +obligations for that variable or path. Unless that obligation has been |
| 57 | +released by this point, its associated `Drop` implementation will be called. |
| 58 | +For `enum` types, only fields corresponding to the "active" variant need to be |
| 59 | +dropped. When processing drop obligations for such types, we first check the |
| 60 | +discriminant to determine the active variant. All drop obligations for variants |
| 61 | +besides the active one are ignored. |
| 62 | + |
| 63 | +Here are a few interesting types to help illustrate these rules: |
| 64 | + |
| 65 | +```rust |
| 66 | +struct NoDrop(u8); // No `Drop` impl. No fields with `Drop` impls. |
| 67 | + |
| 68 | +struct NeedsDrop(Vec<u8>); // No `Drop` impl but has fields with `Drop` impls. |
| 69 | + |
| 70 | +struct ThinVec(*const u8); // Custom `Drop` impl. Individual fields cannot be moved from. |
| 71 | + |
| 72 | +impl Drop for ThinVec { |
| 73 | + fn drop(&mut self) { /* ... */ } |
| 74 | +} |
| 75 | + |
| 76 | +enum MaybeDrop { |
| 77 | + Yes(NeedsDrop), |
| 78 | + No(NoDrop), |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +## Drop elaboration |
| 83 | + |
| 84 | +One valid model for these rules is to keep a boolean flag (a "drop flag") for |
| 85 | +every structural path that is used at any point in the function. This flag is |
| 86 | +set when its path is initialized and is cleared when the path is moved from. |
| 87 | +When a `Drop` occurs, we check the flags for every obligation associated with |
| 88 | +the target of the `Drop` and call the associated `Drop` impl for those that are |
| 89 | +still applicable. |
| 90 | + |
| 91 | +This process—transforming the newly built MIR with its imprecise `Drop` and |
| 92 | +`DropAndReplace` terminators into one with drop flags—is known as drop |
| 93 | +elaboration. When a MIR statement causes a variable to become initialized (or |
| 94 | +uninitialized), drop elaboration inserts code that sets (or clears) the drop |
| 95 | +flag for that variable. It wraps `Drop` terminators in conditionals that check |
| 96 | +the newly inserted drop flags. |
| 97 | + |
| 98 | +Drop elaboration also splits `DropAndReplace` terminators into a `Drop` of the |
| 99 | +target and a write of the newly dropped place. This is somewhat unrelated to what |
| 100 | +we've discussed above. |
| 101 | + |
| 102 | +Once this is complete, `Drop` terminators in the MIR correspond to a call to |
| 103 | +the "drop glue" or "drop shim" for the type of the dropped place. The drop |
| 104 | +glue for a type calls the `Drop` impl for that type (if one exists), and then |
| 105 | +recursively calls the drop glue for all fields of that type. |
| 106 | + |
| 107 | +## Drop elaboration in `rustc` |
| 108 | + |
| 109 | +The approach described above is more expensive than necessary. One can imagine |
| 110 | +a few optimizations: |
| 111 | + |
| 112 | +- Only paths that are the target of a `Drop` (or have the target as a prefix) |
| 113 | + need drop flags. |
| 114 | +- Some variables are known to initialized (or uninitialized) when they are |
| 115 | + dropped. These do not need drop flags. |
| 116 | +- If a set of paths are only dropped or moved from via a shared prefix, those |
| 117 | + paths can share a single drop flag. |
| 118 | + |
| 119 | +A subset of these are implemented in `rustc`. |
| 120 | + |
| 121 | +In the compiler, drop elaboration is split across several modules. The pass |
| 122 | +itself is defined [here][drops-transform], but the [main logic][drops] is |
| 123 | +defined elsewhere since it is also used to build [drop shims][drops-shim]. |
| 124 | + |
| 125 | +Drop elaboration designates each `Drop` in the newly built MIR as one of four |
| 126 | +kinds: |
| 127 | + |
| 128 | +- `Static`, the target is always initialized. |
| 129 | +- `Dead`, the target is always **un**initialized. |
| 130 | +- `Conditional`, the target is either wholly initialized or wholly |
| 131 | + uninitialized. It is not partly initialized. |
| 132 | +- `Open`, the target may be partly initialized. |
| 133 | + |
| 134 | +For this, it uses a pair of dataflow analyses, `MaybeInitializedPlaces` and |
| 135 | +`MaybeUninitializedPlaces`. If a place is in one but not the other, then the |
| 136 | +initializedness of the target is known at compile-time (`Dead` or `Static`). |
| 137 | +In this case, drop elaboration does not add a flag for the target. It simply |
| 138 | +removes (`Dead`) or preserves (`Static`) the `Drop` terminator. |
| 139 | + |
| 140 | +For `Conditional` drops, we know that the initializedness of the variable as a |
| 141 | +whole is the same as the initializedness of its fields. Therefore, once we |
| 142 | +generate a drop flag for the target of that drop, it's safe to call the drop |
| 143 | +glue for that target. |
| 144 | + |
| 145 | +### `Open` drops |
| 146 | + |
| 147 | +`Open` drops are the most complex, since we need to break down a single `Drop` |
| 148 | +terminator into several different ones, one for each field of the target whose |
| 149 | +type has drop glue (`Ty::needs_drop`). We cannot call the drop glue for the |
| 150 | +target itself because that requires all fields of the target to be initialized. |
| 151 | +Remember, variables whose type has a custom `Drop` impl do not allow `Open` |
| 152 | +drops because their fields cannot be moved from. |
| 153 | + |
| 154 | +This is accomplished by recursively categorizing each field as `Dead`, |
| 155 | +`Static`, `Conditional` or `Open`. Fields whose type does not have drop glue |
| 156 | +are automatically `Dead` and need not be considered during the recursion. When |
| 157 | +we reach a field whose kind is not `Open`, we handle it as we did above. If the |
| 158 | +field is also `Open`, the recursion continues. |
| 159 | + |
| 160 | +It's worth noting how we handle `Open` drops of enums. Inside drop elaboration, |
| 161 | +each variant of the enum is treated like a field, with the invariant that only |
| 162 | +one of those "variant fields" can be initialized at any given time. In the |
| 163 | +general case, we do not know which variant is the active one, so we will have |
| 164 | +to call the drop glue for the enum (which checks the discriminant) or check the |
| 165 | +discriminant ourselves as part of an elaborated `Open` drop. However, in |
| 166 | +certain cases (within a `match` arm, for example) we do know which variant of |
| 167 | +an enum is active. This information is encoded in the `MaybeInitializedPlaces` |
| 168 | +and `MaybeUninitializedPlaces` dataflow analyses by marking all places |
| 169 | +corresponding to inactive variants as uninitialized. |
| 170 | + |
| 171 | +### Cleanup paths |
| 172 | + |
| 173 | +TODO: Discuss drop elaboration and unwinding. |
| 174 | + |
| 175 | +## Aside: drop elaboration and const-eval |
| 176 | + |
| 177 | +In Rust, functions that are eligible for evaluation at compile-time must be |
| 178 | +marked explicitly using the `const` keyword. This includes implementations of |
| 179 | +the `Drop` trait, which may or may not be `const`. Code that is eligible for |
| 180 | +compile-time evaluation may only call `const` functions, so any calls to |
| 181 | +non-const `Drop` implementations in such code must be forbidden. |
| 182 | + |
| 183 | +A call to a `Drop` impl is encoded as a `Drop` terminator in the MIR. However, |
| 184 | +as we discussed above, a `Drop` terminator in newly built MIR does not |
| 185 | +necessarily result in a call to `Drop::drop`. The drop target may be |
| 186 | +uninitialized at that point. This means that checking for non-const `Drop`s on |
| 187 | +the newly built MIR can result in spurious errors. Instead, we wait until after |
| 188 | +drop elaboration runs, which eliminates `Dead` drops (ones where the target is |
| 189 | +known to be uninitialized) to run these checks. |
| 190 | + |
| 191 | +[RFC 320]: https://rust-lang.github.io/rfcs/0320-nonzeroing-dynamic-drop.html |
| 192 | +[reference-drop]: https://doc.rust-lang.org/reference/destructors.html |
| 193 | +[drops]: https://github.com/rust-lang/rust/blob/master/compiler/rustc_mir_dataflow/src/elaborate_drops.rs |
| 194 | +[drops-shim]: https://github.com/rust-lang/rust/blob/master/compiler/rustc_mir_transform/src/shim.rs |
| 195 | +[drops-transform]: https://github.com/rust-lang/rust/blob/master/compiler/rustc_mir_dataflow/src/elaborate_drops.rs |
0 commit comments