Skip to main content

rustc_mir_transform/
dest_prop.rs

1//! Propagates assignment destinations backwards in the CFG to eliminate redundant assignments.
2//!
3//! # Motivation
4//!
5//! MIR building can insert a lot of redundant copies, and Rust code in general often tends to move
6//! values around a lot. The result is a lot of assignments of the form `dest = {move} src;` in MIR.
7//! MIR building for constants in particular tends to create additional locals that are only used
8//! inside a single block to shuffle a value around unnecessarily.
9//!
10//! LLVM by itself is not good enough at eliminating these redundant copies (eg. see
11//! <https://github.com/rust-lang/rust/issues/32966>), so this leaves some performance on the table
12//! that we can regain by implementing an optimization for removing these assign statements in rustc
13//! itself. When this optimization runs fast enough, it can also speed up the constant evaluation
14//! and code generation phases of rustc due to the reduced number of statements and locals.
15//!
16//! # The Optimization
17//!
18//! Conceptually, this optimization is "destination propagation". It is similar to the Named Return
19//! Value Optimization, or NRVO, known from the C++ world, except that it isn't limited to return
20//! values or the return place `_0`. On a very high level, independent of the actual implementation
21//! details, it does the following:
22//!
23//! 1) Identify `dest = src;` statements with values for `dest` and `src` whose storage can soundly
24//!    be merged.
25//! 2) Replace all mentions of `src` with `dest` ("unifying" them and propagating the destination
26//!    backwards).
27//! 3) Delete the `dest = src;` statement (by making it a `nop`).
28//!
29//! Step 1) is by far the hardest, so it is explained in more detail below.
30//!
31//! ## Soundness
32//!
33//! We have a pair of places `p` and `q`, whose memory we would like to merge. In order for this to
34//! be sound, we need to check a number of conditions:
35//!
36//! * `p` and `q` must both be *constant* - it does not make much sense to talk about merging them
37//!   if they do not consistently refer to the same place in memory. This is satisfied if they do
38//!   not contain any indirection through a pointer or any indexing projections.
39//!
40//! * `p` and `q` must have the **same type**. If we replace a local with a subtype or supertype,
41//!   we may end up with a different vtable for that local. See the `subtyping-impacts-selection`
42//!   tests for an example where that causes issues.
43//!
44//! * We need to make sure that the goal of "merging the memory" is actually structurally possible
45//!   in MIR. For example, even if all the other conditions are satisfied, there is no way to
46//!   "merge" `_5.foo` and `_6.bar`. For now, we ensure this by requiring that both `p` and `q` are
47//!   locals with no further projections. Future iterations of this pass should improve on this.
48//!
49//! * Finally, we want `p` and `q` to use the same memory - however, we still need to make sure that
50//!   each of them has enough "ownership" of that memory to continue "doing its job." More
51//!   precisely, what we will check is that whenever the program performs a write to `p`, then it
52//!   does not currently care about what the value in `q` is (and vice versa). We formalize the
53//!   notion of "does not care what the value in `q` is" by checking the *liveness* of `q`.
54//!
55//!   Because of the difficulty of computing liveness of places that have their address taken, we do
56//!   not even attempt to do it. Any places that are in a local that has its address taken is
57//!   excluded from the optimization.
58//!
59//! The first two conditions are simple structural requirements on the `Assign` statements that can
60//! be trivially checked. The third requirement however is more difficult and costly to check.
61//!
62//! ## Current implementation
63//!
64//! The current implementation relies on live range computation to check for conflicts. We only
65//! allow to merge locals that have disjoint live ranges. The live range are defined with
66//! half-statement granularity, so as to make all writes be live for at least a half statement.
67//!
68//! ## Future Improvements
69//!
70//! There are a number of ways in which this pass could be improved in the future:
71//!
72//! * Merging storage liveness ranges instead of removing storage statements completely. This may
73//!   improve stack usage.
74//!
75//! * Allow merging locals into places with projections, eg `_5` into `_6.foo`.
76//!
77//! * Liveness analysis with more precision than whole locals at a time. The smaller benefit of this
78//!   is that it would allow us to dest prop at "sub-local" levels in some cases. The bigger benefit
79//!   of this is that such liveness analysis can report more accurate results about whole locals at
80//!   a time. For example, consider:
81//!
82//!   ```ignore (syntax-highlighting-only)
83//!   _1 = u;
84//!   // unrelated code
85//!   _1.f1 = v;
86//!   _2 = _1.f1;
87//!   ```
88//!
89//!   Because the current analysis only thinks in terms of locals, it does not have enough
90//!   information to report that `_1` is dead in the "unrelated code" section.
91//!
92//! * Liveness analysis enabled by alias analysis. This would allow us to not just bail on locals
93//!   that ever have their address taken. Of course that requires actually having alias analysis
94//!   (and a model to build it on), so this might be a bit of a ways off.
95//!
96//! * Various perf improvements. There are a bunch of comments in here marked `PERF` with ideas for
97//!   how to do things more efficiently. However, the complexity of the pass as a whole should be
98//!   kept in mind.
99//!
100//! ## Previous Work
101//!
102//! A [previous attempt][attempt 1] at implementing an optimization like this turned out to be a
103//! significant regression in compiler performance. Fixing the regressions introduced a lot of
104//! undesirable complexity to the implementation.
105//!
106//! A [subsequent approach][attempt 2] tried to avoid the costly computation by limiting itself to
107//! acyclic CFGs, but still turned out to be far too costly to run due to suboptimal performance
108//! within individual basic blocks, requiring a walk across the entire block for every assignment
109//! found within the block. For the `tuple-stress` benchmark, which has 458745 statements in a
110//! single block, this proved to be far too costly.
111//!
112//! [Another approach after that][attempt 3] was much closer to correct, but had some soundness
113//! issues - it was failing to consider stores outside live ranges, and failed to uphold some of the
114//! requirements that MIR has for non-overlapping places within statements. However, it also had
115//! performance issues caused by `O(l² * s)` runtime, where `l` is the number of locals and `s` is
116//! the number of statements and terminators.
117//!
118//! Since the first attempt at this, the compiler has improved dramatically, and new analysis
119//! frameworks have been added that should make this approach viable without requiring a limited
120//! approach that only works for some classes of CFGs:
121//! - rustc now has a powerful dataflow analysis framework that can handle forwards and backwards
122//!   analyses efficiently.
123//! - Layout optimizations for coroutines have been added to improve code generation for
124//!   async/await, which are very similar in spirit to what this optimization does.
125//!
126//! [The next approach][attempt 4] computes a conflict matrix between locals by forbidding merging
127//! locals with competing writes or with one write while the other is live.
128//!
129//! ## Pre/Post Optimization
130//!
131//! It is recommended to run `SimplifyCfg` and then `SimplifyLocals` some time after this pass, as
132//! it replaces the eliminated assign statements with `nop`s and leaves unused locals behind.
133//!
134//! [liveness]: https://en.wikipedia.org/wiki/Live_variable_analysis
135//! [attempt 1]: https://github.com/rust-lang/rust/pull/47954
136//! [attempt 2]: https://github.com/rust-lang/rust/pull/71003
137//! [attempt 3]: https://github.com/rust-lang/rust/pull/72632
138//! [attempt 4]: https://github.com/rust-lang/rust/pull/96451
139
140use rustc_data_structures::union_find::UnionFind;
141use rustc_index::bit_set::DenseBitSet;
142use rustc_index::interval::SparseIntervalMatrix;
143use rustc_index::{IndexVec, newtype_index};
144use rustc_middle::mir::visit::{MutVisitor, PlaceContext, VisitPlacesWith, Visitor};
145use rustc_middle::mir::*;
146use rustc_middle::ty::TyCtxt;
147use rustc_mir_dataflow::impls::{DefUse, LivenessTransferFunction, MaybeLiveLocals};
148use rustc_mir_dataflow::points::DenseLocationMap;
149use rustc_mir_dataflow::{Analysis, EntryStates, GenKill};
150use tracing::{debug, trace};
151
152use crate::PassPolicy;
153
154pub(super) struct DestinationPropagation;
155
156impl<'tcx> crate::MirPass<'tcx> for DestinationPropagation {
157    fn policy(&self, sess: &rustc_session::Session) -> PassPolicy {
158        PassPolicy::optimization(sess.mir_opt_level() >= 2)
159    }
160
161    #[tracing::instrument(level = "trace", skip(self, tcx, body))]
162    fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
163        let def_id = body.source.def_id();
164        trace!(?def_id);
165
166        let borrowed = rustc_mir_dataflow::impls::borrowed_locals(body);
167
168        let candidates = Candidates::find(body, &borrowed);
169        trace!(?candidates);
170        if candidates.c.is_empty() {
171            return;
172        }
173
174        let live = MaybeLiveLocals.iterate_to_fixpoint(tcx, body, Some("MaybeLiveLocals-DestProp"));
175
176        let points = DenseLocationMap::new(body);
177        let mut relevant = RelevantLocals::compute(&candidates, body.local_decls.len());
178        let mut live = save_as_intervals(&points, body, &relevant, live.entry_states);
179
180        dest_prop_mir_dump(tcx, body, &points, &live, &relevant);
181
182        let mut merged_locals = DenseBitSet::new_empty(body.local_decls.len());
183
184        for (src, dst) in candidates.c.into_iter() {
185            trace!(?src, ?dst);
186
187            let Some(mut src) = relevant.find(src) else { continue };
188            let Some(mut dst) = relevant.find(dst) else { continue };
189            if src == dst {
190                continue;
191            }
192
193            let Some(src_live_ranges) = live.row(src) else { continue };
194            let Some(dst_live_ranges) = live.row(dst) else { continue };
195            trace!(?src, ?src_live_ranges);
196            trace!(?dst, ?dst_live_ranges);
197
198            if src_live_ranges.disjoint(dst_live_ranges) {
199                // We want to replace `src` by `dst`.
200                let mut orig_src = relevant.original[src];
201                let mut orig_dst = relevant.original[dst];
202
203                // The return place and function arguments are required and cannot be renamed.
204                // This check cannot be made during candidate collection, as we may want to
205                // unify the same non-required local with several required locals.
206                match (is_local_required(orig_src, body), is_local_required(orig_dst, body)) {
207                    // Renaming `src` is ok.
208                    (false, _) => {}
209                    // Renaming `src` is wrong, but renaming `dst` is ok.
210                    (true, false) => {
211                        std::mem::swap(&mut src, &mut dst);
212                        std::mem::swap(&mut orig_src, &mut orig_dst);
213                    }
214                    // Neither local can be renamed, so skip this case.
215                    (true, true) => continue,
216                }
217
218                trace!(?src, ?dst, "merge");
219                merged_locals.insert(orig_src);
220                merged_locals.insert(orig_dst);
221
222                // Replace `src` by `dst`.
223                let head = relevant.union(src, dst);
224                live.union_rows(/* read */ src, /* write */ head);
225                live.union_rows(/* read */ dst, /* write */ head);
226            }
227        }
228        trace!(?merged_locals);
229        trace!(?relevant.renames);
230
231        if merged_locals.is_empty() {
232            return;
233        }
234
235        apply_merges(body, tcx, relevant, merged_locals);
236    }
237}
238
239//////////////////////////////////////////////////////////
240// Merging
241//
242// Applies the actual optimization
243
244fn apply_merges<'tcx>(
245    body: &mut Body<'tcx>,
246    tcx: TyCtxt<'tcx>,
247    relevant: RelevantLocals,
248    merged_locals: DenseBitSet<Local>,
249) {
250    let mut merger = Merger { tcx, relevant, merged_locals };
251    merger.visit_body_preserves_cfg(body);
252}
253
254struct Merger<'tcx> {
255    tcx: TyCtxt<'tcx>,
256    relevant: RelevantLocals,
257    merged_locals: DenseBitSet<Local>,
258}
259
260impl<'tcx> MutVisitor<'tcx> for Merger<'tcx> {
261    fn tcx(&self) -> TyCtxt<'tcx> {
262        self.tcx
263    }
264
265    fn visit_local(&mut self, local: &mut Local, _: PlaceContext, _location: Location) {
266        if let Some(relevant) = self.relevant.find(*local) {
267            *local = self.relevant.original[relevant];
268        }
269    }
270
271    fn visit_statement(&mut self, statement: &mut Statement<'tcx>, location: Location) {
272        match &statement.kind {
273            // FIXME: Don't delete storage statements, but "merge" the storage ranges instead.
274            StatementKind::StorageDead(local) | StatementKind::StorageLive(local)
275                if self.merged_locals.contains(*local) =>
276            {
277                statement.make_nop(true);
278            }
279            _ => (),
280        };
281        self.super_statement(statement, location);
282        match &statement.kind {
283            StatementKind::Assign((dest, rvalue)) => {
284                match rvalue {
285                    Rvalue::Use(Operand::Copy(place) | Operand::Move(place), _) => {
286                        // These might've been turned into self-assignments by the replacement
287                        // (this includes the original statement we wanted to eliminate).
288                        if dest == place {
289                            debug!("{:?} turned into self-assignment, deleting", location);
290                            statement.make_nop(true);
291                        }
292                    }
293                    _ => {}
294                }
295            }
296
297            _ => {}
298        }
299    }
300}
301
302//////////////////////////////////////////////////////////
303// Relevant locals
304//
305// Small utility to reduce size of the conflict matrix by only considering locals that appear in
306// the candidates
307
308newtype_index! {
309    /// Represent a subset of locals which appear in candidates.
310    struct RelevantLocal {}
311}
312
313#[derive(Debug)]
314struct RelevantLocals {
315    original: IndexVec<RelevantLocal, Local>,
316    shrink: IndexVec<Local, Option<RelevantLocal>>,
317    renames: UnionFind<RelevantLocal>,
318}
319
320impl RelevantLocals {
321    #[tracing::instrument(level = "trace", skip(candidates, num_locals), ret)]
322    fn compute(candidates: &Candidates, num_locals: usize) -> RelevantLocals {
323        let mut original = IndexVec::with_capacity(candidates.c.len());
324        let mut shrink = IndexVec::from_elem_n(None, num_locals);
325
326        // Mark a local as relevant and record it into the maps.
327        let mut declare = |local| {
328            shrink.get_or_insert_with(local, || original.push(local));
329        };
330
331        for &(src, dest) in candidates.c.iter() {
332            declare(src);
333            declare(dest)
334        }
335
336        let renames = UnionFind::new(original.len());
337        RelevantLocals { original, shrink, renames }
338    }
339
340    fn find(&mut self, src: Local) -> Option<RelevantLocal> {
341        let src = self.shrink[src]?;
342        let src = self.renames.find(src);
343        Some(src)
344    }
345
346    fn union(&mut self, lhs: RelevantLocal, rhs: RelevantLocal) -> RelevantLocal {
347        let head = self.renames.unify(lhs, rhs);
348        // We need to ensure we keep the original local of the RHS, as it may be a required local.
349        self.original[head] = self.original[rhs];
350        head
351    }
352}
353
354/////////////////////////////////////////////////////
355// Candidate accumulation
356
357#[derive(Debug, Default)]
358struct Candidates {
359    /// The set of candidates we are considering in this optimization.
360    ///
361    /// Whether a place ends up in the key or the value does not correspond to whether it appears as
362    /// the lhs or rhs of any assignment. As a matter of fact, the places in here might never appear
363    /// in an assignment at all. This happens because if we see an assignment like this:
364    ///
365    /// ```ignore (syntax-highlighting-only)
366    /// _1.0 = _2.0
367    /// ```
368    ///
369    /// We will still report that we would like to merge `_1` and `_2` in an attempt to allow us to
370    /// remove that assignment.
371    c: Vec<(Local, Local)>,
372}
373
374// We first implement some utility functions which we will expose removing candidates according to
375// different needs. Throughout the liveness filtering, the `candidates` are only ever accessed
376// through these methods, and not directly.
377impl Candidates {
378    /// Collects the candidates for merging.
379    ///
380    /// This is responsible for enforcing the first and third bullet point.
381    fn find(body: &Body<'_>, borrowed: &DenseBitSet<Local>) -> Candidates {
382        let mut visitor = FindAssignments { body, candidates: Default::default(), borrowed };
383        visitor.visit_body(body);
384
385        Candidates { c: visitor.candidates }
386    }
387}
388
389struct FindAssignments<'a, 'tcx> {
390    body: &'a Body<'tcx>,
391    candidates: Vec<(Local, Local)>,
392    borrowed: &'a DenseBitSet<Local>,
393}
394
395impl<'tcx> Visitor<'tcx> for FindAssignments<'_, 'tcx> {
396    fn visit_statement(&mut self, statement: &Statement<'tcx>, _: Location) {
397        if let StatementKind::Assign((lhs, Rvalue::Use(Operand::Copy(rhs) | Operand::Move(rhs), _))) =
398            &statement.kind
399            && let Some(src) = lhs.as_local()
400            && let Some(dest) = rhs.as_local()
401        {
402            // As described at the top of the file, we do not go near things that have
403            // their address taken.
404            if self.borrowed.contains(src) || self.borrowed.contains(dest) {
405                return;
406            }
407
408            // As described at the top of this file, we do not touch locals which have
409            // different types.
410            let src_ty = self.body.local_decls()[src].ty;
411            let dest_ty = self.body.local_decls()[dest].ty;
412            if src_ty != dest_ty {
413                // FIXME(#112651): This can be removed afterwards. Also update the module description.
414                trace!("skipped `{src:?} = {dest:?}` due to subtyping: {src_ty} != {dest_ty}");
415                return;
416            }
417
418            // We may insert duplicates here, but that's fine
419            self.candidates.push((src, dest));
420        }
421    }
422}
423
424/// Some locals are part of the function's interface and can not be removed.
425///
426/// Note that these locals *can* still be merged with non-required locals by removing that other
427/// local.
428fn is_local_required(local: Local, body: &Body<'_>) -> bool {
429    match body.local_kind(local) {
430        LocalKind::Arg | LocalKind::ReturnPointer => true,
431        LocalKind::Temp => false,
432    }
433}
434
435/////////////////////////////////////////////////////////
436// MIR Dump
437
438fn dest_prop_mir_dump<'tcx>(
439    tcx: TyCtxt<'tcx>,
440    body: &Body<'tcx>,
441    points: &DenseLocationMap,
442    live: &SparseIntervalMatrix<RelevantLocal, TwoStepIndex>,
443    relevant: &RelevantLocals,
444) {
445    let locals_live_at = |location| {
446        live.rows()
447            .filter(|&r| live.contains(r, location))
448            .map(|rl| relevant.original[rl])
449            .collect::<Vec<_>>()
450    };
451
452    if let Some(dumper) = MirDumper::new(tcx, "DestinationPropagation-dataflow", body) {
453        let extra_data = &|pass_where, w: &mut dyn std::io::Write| {
454            if let PassWhere::BeforeLocation(loc) = pass_where {
455                let location = TwoStepIndex::new(points, loc, Effect::Before);
456                let live = locals_live_at(location);
457                writeln!(w, "        // before: {:?} => {:?}", location, live)?;
458            }
459            if let PassWhere::AfterLocation(loc) = pass_where {
460                let location = TwoStepIndex::new(points, loc, Effect::After);
461                let live = locals_live_at(location);
462                writeln!(w, "        // after: {:?} => {:?}", location, live)?;
463            }
464            Ok(())
465        };
466
467        dumper.set_extra_data(extra_data).dump_mir(body)
468    }
469}
470
471#[derive(Copy, Clone, Debug, PartialEq, Eq)]
472enum Effect {
473    Before,
474    After,
475}
476
477rustc_index::newtype_index! {
478    /// A reversed `PointIndex` but with the lower bit encoding early/late inside the statement.
479    /// The reversed order allows to use the more efficient `IntervalSet::append` method while we
480    /// iterate on the statements in reverse order.
481    #[orderable]
482    #[debug_format = "TwoStepIndex({})"]
483    struct TwoStepIndex {}
484}
485
486impl TwoStepIndex {
487    fn new(elements: &DenseLocationMap, location: Location, effect: Effect) -> TwoStepIndex {
488        let point = elements.point_from_location(location);
489        let effect = match effect {
490            Effect::Before => 0,
491            Effect::After => 1,
492        };
493        let max_index = 2 * elements.num_points() as u32 - 1;
494        let index = 2 * point.as_u32() + (effect as u32);
495        // Reverse the indexing to use more efficient `IntervalSet::append`.
496        TwoStepIndex::from_u32(max_index - index)
497    }
498
499    fn effect(self) -> Effect {
500        if self.as_u32() & 1 == 0 { Effect::After } else { Effect::Before }
501    }
502}
503
504/// Add points depending on the result of the given dataflow analysis.
505#[tracing::instrument(level = "trace", skip(elements, body))]
506fn save_as_intervals<'tcx>(
507    elements: &DenseLocationMap,
508    body: &Body<'tcx>,
509    relevant: &RelevantLocals,
510    entry_states: EntryStates<DenseBitSet<Local>>,
511) -> SparseIntervalMatrix<RelevantLocal, TwoStepIndex> {
512    /// Generalized dataflow state for use inside a given block.
513    struct GenKillIntervalMatrix<'a> {
514        values: SparseIntervalMatrix<RelevantLocal, TwoStepIndex>,
515        relevant: &'a RelevantLocals,
516        /// If a local is live, this stores the start of the live range.
517        /// If a local is dead, this stores `None`.
518        pending: IndexVec<RelevantLocal, Option<TwoStepIndex>>,
519        /// The current position of the cursor inside the MIR body.
520        current: TwoStepIndex,
521    }
522
523    impl GenKill<Local> for GenKillIntervalMatrix<'_> {
524        fn gen_(&mut self, elem: Local) {
525            let Some(elem) = self.relevant.shrink[elem] else { return };
526            // If the local was already live, do not overwrite the start position.
527            let _ = self.pending[elem].get_or_insert(self.current);
528        }
529
530        fn kill(&mut self, elem: Local) {
531            // Ensure we only kill for `Effect::Before`, so `insert_single` is well-behaved.
532            debug_assert_eq!(self.current.effect(), Effect::Before);
533            let Some(elem) = self.relevant.shrink[elem] else { return };
534            if let Some(start) = self.pending[elem].take() {
535                debug_assert!(start <= self.current);
536                // The local is live since `start`.
537                // We are killing it, so it won't be after `current`, hence an exclusive range.
538                self.values.append_range(elem, start..self.current);
539            }
540        }
541    }
542
543    impl GenKillIntervalMatrix<'_> {
544        /// Insert a singleton range. This can be used for dead locals to mark conflicts, for
545        /// instance `move` operands in function calls or partial writes.
546        fn insert_single(&mut self, elem: RelevantLocal) {
547            // If we have a set pending, we will insert it when killing it, so nothing more to do.
548            // Kills only happen for `Effect::Before`, so we don't risk `kill` to insert
549            // a range excluding `self.current`.
550            debug_assert_eq!(self.current.effect(), Effect::After);
551            if self.pending[elem].is_none() {
552                self.values.append(elem, self.current);
553            }
554        }
555
556        fn start_block(&mut self, entry_state: &DenseBitSet<Local>) {
557            debug_assert!(self.pending.iter().all(Option::is_none));
558            for local in entry_state.iter() {
559                if let Some(elem) = self.relevant.shrink[local] {
560                    self.pending[elem] = Some(self.current);
561                }
562            }
563        }
564
565        fn end_block(&mut self) {
566            for (elem, start) in self.pending.iter_enumerated_mut() {
567                if let Some(start) = start.take() {
568                    debug_assert!(start <= self.current);
569                    // We are ending a block, mark all live locals as live up to `current`,
570                    // including that position (which is still inside the block).
571                    self.values.append_range(elem, start..=self.current);
572                }
573            }
574        }
575    }
576
577    let reachable_blocks = traversal::reachable_as_bitset(body);
578    let two_step_loc = |location, effect| TwoStepIndex::new(elements, location, effect);
579
580    let mut state = GenKillIntervalMatrix {
581        values: SparseIntervalMatrix::new(2 * elements.num_points()),
582        relevant,
583        pending: IndexVec::from_elem(None, &relevant.original),
584        // Dummy value.
585        current: TwoStepIndex::from_u32(0),
586    };
587
588    // Iterate blocks in decreasing order, to visit locations in decreasing order. This
589    // allows to use the more efficient `append` method to interval sets.
590    for block in body.basic_blocks.indices().rev() {
591        if !reachable_blocks.contains(block) {
592            continue;
593        }
594
595        let block_data = &body.basic_blocks[block];
596        let loc = Location { block, statement_index: block_data.statements.len() };
597        state.current = two_step_loc(loc, Effect::After);
598
599        // Setup the new block.
600        state.start_block(&entry_states[block]);
601
602        let term = block_data.terminator();
603
604        // Ensure we have a non-zero live range even for dead stores. This is done by marking all
605        // the written-to locals as live in the second half of the statement.
606        // We also ensure that operands read by terminators conflict with writes by that terminator.
607        // For instance a function call may read args after having written to the destination.
608        VisitPlacesWith(|place: Place<'tcx>, ctxt| {
609            if let Some(relevant) = relevant.shrink[place.local] {
610                match DefUse::for_place(place, ctxt) {
611                    DefUse::Def | DefUse::Use | DefUse::PartialWrite => {
612                        state.insert_single(relevant);
613                    }
614                    DefUse::NonUse => {}
615                }
616            }
617        })
618        .visit_terminator(term, loc);
619
620        state.current = state.current + 1;
621        debug_assert_eq!(state.current, two_step_loc(loc, Effect::Before));
622        LivenessTransferFunction(&mut state).visit_terminator(term, loc);
623
624        for (statement_index, stmt) in block_data.statements.iter().enumerate().rev() {
625            let loc = Location { block, statement_index };
626            state.current = state.current + 1;
627            debug_assert_eq!(state.current, two_step_loc(loc, Effect::After));
628
629            // Like terminators, ensure we have a non-zero live range even for dead stores.
630            // Some rvalues interleave reads and writes, for instance `Rvalue::Aggregate`, see
631            // https://github.com/rust-lang/rust/issues/146383. By precaution, treat statements
632            // as behaving so by default.
633            // We make an exception for simple assignments `_a.stuff = {copy|move} _b.stuff`,
634            // as marking `_b` live here would prevent unification.
635            let is_simple_assignment = match stmt.kind {
636                StatementKind::Assign((
637                    lhs,
638                    Rvalue::CopyForDeref(rhs)
639                    | Rvalue::Use(Operand::Copy(rhs) | Operand::Move(rhs), _),
640                )) => lhs.projection == rhs.projection,
641                _ => false,
642            };
643            VisitPlacesWith(|place: Place<'tcx>, ctxt| {
644                if let Some(relevant) = relevant.shrink[place.local] {
645                    match DefUse::for_place(place, ctxt) {
646                        DefUse::Def | DefUse::PartialWrite => {
647                            state.insert_single(relevant);
648                        }
649                        DefUse::Use if !is_simple_assignment => {
650                            state.insert_single(relevant);
651                        }
652                        DefUse::Use | DefUse::NonUse => {}
653                    }
654                }
655            })
656            .visit_statement(stmt, loc);
657
658            // ... but reads from operands are marked as live here so they do not conflict with
659            // the all the writes we manually marked as live in the second half of the statement.
660            state.current = TwoStepIndex::from_u32(state.current.as_u32() + 1);
661            debug_assert_eq!(state.current, two_step_loc(loc, Effect::Before));
662            LivenessTransferFunction(&mut state).visit_statement(stmt, loc);
663        }
664
665        // Cleanup the current block for the next one.
666        state.end_block();
667    }
668
669    state.values
670}