rustc_mir_transform/tail_copy_to_move.rs
1//! Rewrite final-use copies before return into moves.
2//!
3//! # The problem
4//!
5//! MIR building represents reads of values whose type is `Copy` using
6//! `Operand::Copy`, including when such a local is returned. If that local's
7//! address has ever been observed, `MoveElimination` conservatively considers
8//! it live until its `StorageDead` or function exit. This keeps the local live
9//! across `_0 = copy local`, so its live range overlaps with the return place
10//! and `MoveElimination` cannot unify the source local with `_0`.
11//!
12//! # The solution
13//!
14//! At function return, all local allocations are about to become invalid
15//! anyway. After borrowck, this pass can therefore turn a final-use `Copy` into
16//! a `Move`, as long as shortening the source local's live range has no
17//! observable effect before the return happens. Concretely, between the
18//! transformed copy (now a move) and the return, there may only be writes to
19//! unborrowed locals, storage markers, nops, and gotos.
20//!
21//! # The algorithm
22//!
23//! Start from every `Return` terminator, with `_0` treated as used by the
24//! return. Then scan predecessor blocks backward through `Goto` edges, forming
25//! a return-tail tree. The scan maintains `used_after`, the set of locals
26//! accessed later on that path.
27//!
28//! A `Copy` operand is rewritten to a `Move` when its base local is not in
29//! `used_after`. Then any locals touched by that operand, including
30//! index-projection locals, are added to `used_after` before the backward scan
31//! continues.
32//!
33//! The scan stops when accessing an indirect place because that may access any
34//! borrowed local, which would make the pass unable to prove any useful final
35//! uses. It also stops at writes to borrowed locals, because those can create a
36//! new address-observed allocation range whose overlap with an earlier borrowed
37//! local must be preserved.
38
39use std::ops::ControlFlow;
40
41use rustc_index::bit_set::DenseBitSet;
42use rustc_middle::mir::*;
43use rustc_middle::ty::TyCtxt;
44use rustc_mir_dataflow::impls::borrowed_locals;
45
46use crate::PassPolicy;
47
48pub(super) struct TailCopyToMove;
49
50impl<'tcx> crate::MirPass<'tcx> for TailCopyToMove {
51 fn policy(&self, ctx: &crate::PassCtx<'_>) -> PassPolicy {
52 PassPolicy::optional(
53 ctx.mir_opt_level() >= 2 && ctx.opts.unstable_opts.mir_move_elimination,
54 )
55 }
56
57 #[tracing::instrument(level = "trace", skip(self, _tcx, body))]
58 fn run_pass(&self, _tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
59 let borrowed = borrowed_locals(body);
60 let predecessors = body.basic_blocks.predecessors().clone();
61 let mut stack = Vec::new();
62
63 // A return terminator implicitly uses the return place. Walking
64 // backward through assignments records the locals accessed later on
65 // this path.
66 for (bb, data) in body.basic_blocks.iter_enumerated() {
67 if matches!(data.terminator().kind, TerminatorKind::Return) {
68 let mut used_after = DenseBitSet::new_empty(body.local_decls.len());
69 used_after.insert(RETURN_PLACE);
70 stack.push(TailState { block: bb, used_after });
71 }
72 }
73
74 while let Some(mut state) = stack.pop() {
75 // `scan_block` rewrites final-use copies in this block and updates
76 // `used_after` to the locals whose allocation is accessed after the
77 // block starts. If the block is not pure tail code, this path is
78 // done.
79 if scan_block(body, state.block, &mut state.used_after, &borrowed).is_break() {
80 continue;
81 }
82
83 // Continue through predecessor blocks only when the predecessor's
84 // terminator is a plain `Goto` to this block. Other terminators are
85 // control-flow or effect boundaries.
86 let mut first = None;
87 for pred in predecessors[state.block].iter().copied() {
88 let terminator = body.basic_blocks[pred].terminator();
89 if let TerminatorKind::Goto { target, .. } = terminator.kind {
90 debug_assert_eq!(target, state.block);
91 if first.is_none() {
92 first = Some(pred);
93 } else {
94 stack.push(TailState { block: pred, used_after: state.used_after.clone() });
95 }
96 }
97 }
98
99 // Avoid cloning the bitset for the first predecessor.
100 if let Some(pred) = first {
101 stack.push(TailState { block: pred, used_after: state.used_after });
102 }
103 }
104 }
105}
106
107struct TailState {
108 block: BasicBlock,
109 used_after: DenseBitSet<Local>,
110}
111
112/// Scan a block backward while the return-tail invariant still holds.
113///
114/// The invariant is that a whole-local `Copy` can be changed to a `Move` only
115/// if this path has no later access to that local's allocation before
116/// returning, and no later operation whose observable behavior could depend on
117/// ending an address-observed local's allocation early. `used_after` tracks
118/// those later local-allocation accesses.
119fn scan_block<'tcx>(
120 body: &mut Body<'tcx>,
121 block: BasicBlock,
122 used_after: &mut DenseBitSet<Local>,
123 borrowed: &DenseBitSet<Local>,
124) -> ControlFlow<()> {
125 for statement in body.basic_blocks.as_mut_preserves_cfg()[block].statements.iter_mut().rev() {
126 match &mut statement.kind {
127 // Under the local lifetime semantics from RFC 3943, `StorageLive`
128 // does not allocate, and `StorageDead` has no effect if the local
129 // was already freed by a move. These markers therefore do not
130 // affect whether a copy can be treated as a final use.
131 StatementKind::StorageLive(_) | StatementKind::StorageDead(_) | StatementKind::Nop => {}
132 StatementKind::Assign((place, rhs)) => {
133 // Accessing an indirect place may touch any borrowed local, so
134 // continuing would require treating all borrowed locals as used
135 // after this point.
136 if place.is_indirect_first_projection() {
137 return ControlFlow::Break(());
138 }
139
140 // Writing to a borrowed local can start a new allocation range.
141 // Shortening an earlier borrowed local could remove an overlap
142 // with that new range.
143 if borrowed.contains(place.local) {
144 return ControlFlow::Break(());
145 }
146
147 // A destination write accesses the base local, and evaluating
148 // the destination may also access projection locals, such as an
149 // index.
150 record_place_locals(*place, used_after);
151
152 // This pass only models `Use` and `Aggregate` rvalues whose
153 // operands are direct. Other rvalues are outside the
154 // conservative return-tail shape handled here.
155 process_rvalue(rhs, used_after)?;
156 }
157 StatementKind::SetDiscriminant { place, .. } => {
158 // Accessing an indirect place may touch any borrowed local, so
159 // continuing would require treating all borrowed locals as used
160 // after this point.
161 if place.is_indirect_first_projection() {
162 return ControlFlow::Break(());
163 }
164
165 // Writing to a borrowed local can start a new allocation range.
166 // Shortening an earlier borrowed local could remove an overlap
167 // with that new range.
168 if borrowed.contains(place.local) {
169 return ControlFlow::Break(());
170 }
171
172 // `SetDiscriminant` has a validity invariant on the rest of the
173 // place, so treat the base local as accessed along with any
174 // projection locals.
175 record_place_locals(**place, used_after);
176 }
177 _ => {
178 // Anything else may perform effects or evaluate places in ways
179 // this pass does not model, so it is not part of the pure
180 // return tail.
181 return ControlFlow::Break(());
182 }
183 }
184 }
185
186 ControlFlow::Continue(())
187}
188
189/// Records all locals used in a place, including `Index` projections in
190/// `used_after`.
191fn record_place_locals<'tcx>(place: Place<'tcx>, used_after: &mut DenseBitSet<Local>) {
192 for local in place.as_ref().accessed_locals() {
193 used_after.insert(local);
194 }
195}
196
197/// Process the RHS of an assignment in a pure return tail.
198fn process_rvalue<'tcx>(
199 rvalue: &mut Rvalue<'tcx>,
200 used_after: &mut DenseBitSet<Local>,
201) -> ControlFlow<()> {
202 match rvalue {
203 Rvalue::Use(operand, _) => process_operand(operand, used_after),
204 Rvalue::Aggregate(_, operands) => {
205 // Operands are evaluated left-to-right. We scan them right-to-left
206 // so `used_after` includes uses later in the same statement. If an
207 // operand accesses an indirect place, only earlier operands and
208 // earlier statements are outside the pure tail.
209 for operand in operands.iter_mut().rev() {
210 process_operand(operand, used_after)?;
211 }
212
213 ControlFlow::Continue(())
214 }
215 _ => {
216 // This pass doesn't model other rvalues, so they are not part of
217 // the pure return tail.
218 ControlFlow::Break(())
219 }
220 }
221}
222
223/// Process one operand in an rvalue.
224fn process_operand<'tcx>(
225 operand: &mut Operand<'tcx>,
226 used_after: &mut DenseBitSet<Local>,
227) -> ControlFlow<()> {
228 let place = match operand {
229 Operand::Copy(place) | Operand::Move(place) if place.is_indirect_first_projection() => {
230 // Accessing an indirect place may touch any borrowed local.
231 // Continuing would require treating all borrowed locals as used
232 // after this point, which would prevent the useful copy-to-move
233 // rewrites this pass is looking for.
234 return ControlFlow::Break(());
235 }
236 Operand::Copy(place) => {
237 let place = *place;
238 // No later operation in the scanned tail accesses this local's
239 // allocation, so this copy is a final use on the current return
240 // path and can be represented as a move.
241 if !used_after.contains(place.local) {
242 *operand = Operand::Move(place);
243 }
244 Some(place)
245 }
246 Operand::Move(place) => Some(*place),
247 Operand::Constant(_) | Operand::RuntimeChecks(_) => None,
248 };
249
250 if let Some(place) = place {
251 // Reading an operand place accesses its base local, and evaluating its
252 // projections may access additional locals, such as the index local in
253 // `place[index]`.
254 record_place_locals(place, used_after);
255 }
256
257 ControlFlow::Continue(())
258}