Skip to main content

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}