rust-lang/rust · #163337

MIR move elimination [3/6]: PreciseLiveness

Amanieu · merged Oct 9, 202621 files · 1322 + / 1 −
compiler/rustc_attr_ir/src/data_structures.rs1 + / 0 −
@@ -592,6 +592,7 @@ pub enum RustcDumpLayoutKind { #[derive(Clone, Debug, StableHash, Encodable, Decodable, PrintAttribute, PartialEq, Eq)] pub enum RustcMirKind {     PrettyLiveLocals,+    PrettyPreciseLiveness,     PrettyTransitiveLiveLocals,     PeekMaybeInit,     PeekMaybeUninit,
compiler/rustc_attr_parsing/src/attributes/rustc_internal.rs4 + / 0 −
@@ -630,6 +630,10 @@ impl CombineAttributeParser for RustcMirParser {                             cx.expect_no_args(mi.args())?;                             Some(RustcMirKind::PrettyLiveLocals)                         }+                        sym::rustc_pretty_precise_liveness => {+                            cx.expect_no_args(mi.args())?;+                            Some(RustcMirKind::PrettyPreciseLiveness)+                        }                         sym::rustc_pretty_transitive_live_locals => {                             cx.expect_no_args(mi.args())?;                             Some(RustcMirKind::PrettyTransitiveLiveLocals)
compiler/rustc_mir_dataflow/src/framework/direction.rs4 + / 0 −
@@ -127,6 +127,8 @@ impl Direction for Backward {             analysis.apply_primary_statement_effect(state, stmt, loc);             vis.visit_after_primary_statement_effect(state, stmt, loc);         }++        vis.visit_block_exit(state, block);     } } @@ -242,5 +244,7 @@ impl Direction for Forward {         vis.visit_after_early_terminator_effect(state, term, loc);         analysis.apply_primary_terminator_effect(state, term, loc);         vis.visit_after_primary_terminator_effect(state, term, loc);++        vis.visit_block_exit(state, block);     } }
compiler/rustc_mir_dataflow/src/framework/visitor.rs7 + / 0 −
@@ -34,6 +34,13 @@ pub trait ResultsVisitor<'tcx, A> where     A: Analysis<'tcx>, {+    /// Called after all effects in a block have been applied in the direction+    /// of the analysis.+    ///+    /// In a forwards analysis, `state` is from the block's end. In a backwards+    /// analysis, `state` is from the block's start.+    fn visit_block_exit(&mut self, _state: &A::Domain, _block: BasicBlock) {}+     /// Called after the "early" effect of the given statement is applied to `state`.     fn visit_after_early_statement_effect(         &mut self,
compiler/rustc_mir_dataflow/src/impls/mod.rs5 + / 0 −
@@ -1,6 +1,7 @@ mod borrowed_locals; mod initialized; mod liveness;+mod precise_liveness; mod storage_liveness;  pub use self::borrowed_locals::{MaybeBorrowedLocals, borrowed_locals};@@ -11,6 +12,10 @@ pub use self::initialized::{ pub use self::liveness::{     DefUse, LivenessTransferFunction, MaybeLiveLocals, MaybeTransitiveLiveLocals, };+pub use self::precise_liveness::{+    SplitPointEffect, SplitPointIndex, VisitAllocatedLocalsWith, VisitDeallocatedLocalsWith,+    dump_liveness_matrix, liveness_matrix,+}; pub use self::storage_liveness::{     MaybeRequiresStorage, MaybeStorageDead, MaybeStorageLive, always_storage_live_locals, };
compiler/rustc_mir_dataflow/src/impls/precise_liveness.rsadded596 + / 0 −
@@ -0,0 +1,596 @@+//! Computes the points where each local must have a distinct allocation.+//!+//! The result is a [`SparseIntervalMatrix`] with one row per local. Two locals+//! may share the same address only if their rows are disjoint. To model MIR+//! statements where a source operand and destination place may share an+//! address, each statement and terminator is split into an early point, where+//! operands are read, and a late point, where destinations are written.+//!+//! A local live range starts at the late point of any statement or terminator+//! that writes to it without a `Deref` projection. It ends at `StorageLive` or+//! `StorageDead`, or at the early point of a whole-local move operand or the+//! last use of that local on a control-flow path (only for locals whose address+//! is never observed). This happens at `StorageLive` because it resets a local+//! to an uninitialized state.+//!+//! `Call` terminators are handled specially: move operands are kept live+//! through the late point of the terminator so they conflict with each other+//! and with the destination place. This matches the runtime behavior where the+//! place is donated to the callee for the duration of the call.+//!+//! The analysis consists of three passes:+//!+//! 1. A backward value-liveness analysis which identifies the last point at+//!    which each local is directly used in each control flow path. This is used+//!    to shorten the lifetime of unborrowed locals to their last use.+//! 2. A forward allocation-liveness analysis which uses that information+//!    together with writes, moves, and storage markers to compute which+//!    allocations may be live on entry to each block.+//! 3. A final walk using those entry states which constructs the matrix and+//!    distinguishes whether each effect happens at the early or late point of a+//!    statement or terminator.++use rustc_index::IndexVec;+use rustc_index::bit_set::DenseBitSet;+use rustc_index::interval::SparseIntervalMatrix;+use rustc_middle::mir::visit::{+    MutatingUseContext, NonMutatingUseContext, NonUseContext, PlaceContext, VisitPlacesWith,+    Visitor,+};+use rustc_middle::mir::{self, BasicBlock, Local, Location, MirDumper, PassWhere, Place};+use rustc_middle::ty::TyCtxt;+use tracing::trace;++use crate::impls::{DefUse, MaybeLiveLocals, borrowed_locals};+use crate::points::{DenseLocationMap, PointIndex};+use crate::{Analysis, GenKill, ResultsVisitor, visit_results};++/// Visits locals that are implicitly allocated by a statement or terminator as+/// part of destination place evaluation.+pub struct VisitAllocatedLocalsWith<F>(pub F);++impl<'tcx, F> Visitor<'tcx> for VisitAllocatedLocalsWith<F>+where+    F: FnMut(Local, PlaceContext),+{+    fn visit_place(&mut self, place: &Place<'tcx>, context: PlaceContext, _: Location) {+        match DefUse::for_place(*place, context) {+            DefUse::Def | DefUse::PartialWrite => (self.0)(place.local, context),+            DefUse::Use | DefUse::NonUse => {}+        }+    }+}++/// Visits locals that are implicitly freed by being evaluated as a bare-local+/// move operand or through a storage statement.+pub struct VisitDeallocatedLocalsWith<F>(pub F);++impl<'tcx, F> Visitor<'tcx> for VisitDeallocatedLocalsWith<F>+where+    F: FnMut(Local, PlaceContext),+{+    fn visit_statement(&mut self, statement: &mir::Statement<'tcx>, location: Location) {+        match statement.kind {+            // StorageLive resets a local to the live-but-unallocated state,+            // which also has the effect of freeing its allocation.+            mir::StatementKind::StorageLive(local) => {+                (self.0)(local, PlaceContext::NonUse(NonUseContext::StorageLive));+            }+            mir::StatementKind::StorageDead(local) => {+                (self.0)(local, PlaceContext::NonUse(NonUseContext::StorageDead));+            }+            _ => self.super_statement(statement, location),+        }+    }++    fn visit_operand(&mut self, operand: &mir::Operand<'tcx>, _: Location) {+        if let mir::Operand::Move(place) = operand+            && let Some(local) = place.as_local()+        {+            (self.0)(local, PlaceContext::NonMutatingUse(NonMutatingUseContext::Move));+        }+    }+}++////////////////////////////////////////////////////////////////////////////////+// Backward dataflow pass+//+// This pass computes "kill points" for each local, indicating the location of+// their last use in a particular control flow branch. These are later used in+// the forward pass to end the live range of locals that are never borrowed at+// their last direct use.+//+// This pass ignores borrowed locals: we can't determine their last use from+// syntactic uses of the base local, so we conservatively treat them as always+// live in the backwards pass and don't emit kill points for them. The main+// forward pass still kills at `StorageDead` and whole-local moves, which is+// when their allocation is freed.+//+// This pass has 2 outputs: a set of kill points that mark the last use+// locations of locals and a per-block bitset indicating which locals are live+// on entry to that block. The latter acts as a set of kill points at block+// entry for all locals not in the bit set.++struct KillPoints<'a> {+    live_on_entry: IndexVec<BasicBlock, DenseBitSet<Local>>,+    kill_points_map: IndexVec<PointIndex, &'a [(Local, Location)]>,+}++impl<'a> KillPoints<'a> {+    fn compute<'tcx>(+        tcx: TyCtxt<'tcx>,+        body: &mir::Body<'tcx>,+        pass_name: Option<&'static str>,+        points: &DenseLocationMap,+        kill_points: &'a mut Vec<(Local, Location)>,+    ) -> Self {+        let maybe_live_locals = MaybeLiveLocals.iterate_to_fixpoint(tcx, body, pass_name);+        let borrowed_locals = borrowed_locals(body);++        // Initialize all borrowed locals as live on entry. We never try to kill+        // those.+        let mut live_on_entry =+            IndexVec::from_elem_n(borrowed_locals.clone(), body.basic_blocks.len());++        // Collect kill points and live-on-entry states from the results of+        // MaybeLiveLocals.+        kill_points.clear();+        let mut visitor = KillPointsVisitor {+            kill_points,+            live_on_entry: &mut live_on_entry,+            borrowed_locals: &borrowed_locals,+        };+        visit_results(+            body,+            mir::traversal::reachable(body).map(|(block, _)| block),+            &maybe_live_locals,+            &mut visitor,+        );+        trace!(?kill_points);+        trace!(?live_on_entry);++        // Create a mapping of `PointIndex` to the set of killed locals at that+        // location.+        let mut kill_points_map = IndexVec::from_elem_n(&[][..], points.num_points());+        for chunk in kill_points.chunk_by(|a, b| a.1 == b.1) {+            let point = points.point_from_location(chunk[0].1);+            trace!("Kill points at {:?}: {:?}", chunk[0].1, chunk);+            debug_assert!(kill_points_map[point].is_empty());+            kill_points_map[point] = chunk;+        }++        Self { live_on_entry, kill_points_map }+    }+}++struct KillPointsVisitor<'a> {+    kill_points: &'a mut Vec<(Local, Location)>,+    live_on_entry: &'a mut IndexVec<BasicBlock, DenseBitSet<Local>>,+    borrowed_locals: &'a DenseBitSet<Local>,+}++impl<'tcx> ResultsVisitor<'tcx, MaybeLiveLocals> for KillPointsVisitor<'_> {+    fn visit_block_exit(&mut self, state: &DenseBitSet<Local>, block: BasicBlock) {+        // Borrowed locals are already marked as live when live_on_entry was+        // initialized. This adds the non-borrowed locals that we have+        // determined are live on entry to this block.+        self.live_on_entry[block].union(state);+    }++    fn visit_after_early_statement_effect(+        &mut self,+        state: &DenseBitSet<Local>,+        statement: &mir::Statement<'tcx>,+        location: Location,+    ) {+        VisitPlacesWith(|place: Place<'tcx>, ctxt| {+            // Ignore non-uses.+            match ctxt {+                PlaceContext::NonMutatingUse(_) | PlaceContext::MutatingUse(_) => {}+                PlaceContext::NonUse(_) => return,+            }++            // If a local is used in a statement but is dead after it then this+            // location is a kill point. Don't emit a kill point for borrowed+            // locals.+            if !state.contains(place.local) && !self.borrowed_locals.contains(place.local) {+                self.kill_points.push((place.local, location));+            }+        })+        .visit_statement(statement, location);+    }++    fn visit_after_early_terminator_effect(+        &mut self,+        state: &DenseBitSet<Local>,+        terminator: &mir::Terminator<'tcx>,+        location: Location,+    ) {+        VisitPlacesWith(|place: Place<'tcx>, ctxt| {+            // Ignore non-uses (they don't do anything) and edge uses+            // (implicitly killed though live_on_entry at the start of the+            // corresponding successor).+            match ctxt {+                PlaceContext::MutatingUse(+                    MutatingUseContext::AsmOutput+                    | MutatingUseContext::Call+                    | MutatingUseContext::Yield,+                )+                | PlaceContext::NonUse(_) => return,+                PlaceContext::NonMutatingUse(_) | PlaceContext::MutatingUse(_) => {}+            }++            // If a local is used in a terminator but is dead after it then this+            // location is a kill point. Don't emit a kill point for borrowed+            // locals.+            if !state.contains(place.local) && !self.borrowed_locals.contains(place.local) {+                self.kill_points.push((place.local, location));+            }+        })+        .visit_terminator(terminator, location);+    }+}++////////////////////////////////////////////////////////////////////////////////+// Forward dataflow pass+//+// This is a simpler version of the main pass in `liveness_matrix`. It only+// needs to determine local liveness at block edges and doesn't distinguish+// between "early" and "late" effects. However it must still preserve the+// ordering of effects: moved operands are killed before destinations are+// generated.++struct PreciseLiveness<'a> {+    kill_points: &'a KillPoints<'a>,+    points: &'a DenseLocationMap,+}++impl PreciseLiveness<'_> {+    fn apply_block_start_effect(&self, state: &mut DenseBitSet<Local>, block: BasicBlock) {+        // Kill any unborrowed locals that are dead at the start of this block.+        // Notably this kills any dead results produced by a predecessor's+        // terminator. Borrowed locals are always included in live_on_entry.+        state.intersect(&self.kill_points.live_on_entry[block]);+    }+}++impl<'tcx> Analysis<'tcx> for PreciseLiveness<'_> {+    type Domain = DenseBitSet<Local>;++    const NAME: &'static str = "precise_liveness";++    fn bottom_value(&self, body: &mir::Body<'tcx>) -> DenseBitSet<Local> {+        DenseBitSet::new_empty(body.local_decls.len())+    }++    fn initialize_start_block(&self, body: &mir::Body<'tcx>, state: &mut DenseBitSet<Local>) {+        // Function arguments start out as live.+        for arg in body.args_iter() {+            state.gen_(arg);+        }+    }++    fn apply_primary_statement_effect(+        &self,+        state: &mut DenseBitSet<Local>,+        statement: &mir::Statement<'tcx>,+        location: Location,+    ) {+        if location.statement_index == 0 {+            self.apply_block_start_effect(state, location.block);+        }++        VisitDeallocatedLocalsWith(|local, _| state.kill(local))+            .visit_statement(statement, location);+        VisitAllocatedLocalsWith(|local, _| state.gen_(local)).visit_statement(statement, location);++        // Apply kill points at this statement: if a variable is dead then it+        // doesn't need storage.+        let point = self.points.point_from_location(location);+        for &(local, _) in self.kill_points.kill_points_map[point] {+            state.kill(local);+        }+    }++    fn apply_primary_terminator_effect(+        &self,+        state: &mut DenseBitSet<Local>,+        terminator: &mir::Terminator<'tcx>,+        location: Location,+    ) {+        if location.statement_index == 0 {+            self.apply_block_start_effect(state, location.block);+        }++        VisitDeallocatedLocalsWith(|local, _| state.kill(local))+            .visit_terminator(terminator, location);++        // Gen destination places.+        VisitAllocatedLocalsWith(|local, ctxt| {+            // The resume place of a Yield is only evaluated after the coroutine+            // is resumed. This is in contrast to call destinations and inline+            // asm outputs which are evaluated before the call and therefore+            // remain allocated on both normal and unwind edges.+            if ctxt != PlaceContext::MutatingUse(MutatingUseContext::Yield) {+                state.gen_(local);+            }+        })+        .visit_terminator(terminator, location);+    }++    fn apply_call_return_effect(+        &self,+        state: &mut DenseBitSet<Local>,+        _block: BasicBlock,+        return_places: mir::CallReturnPlaces<'_, 'tcx>,+    ) {+        if let mir::CallReturnPlaces::Yield(place) = return_places+            && !place.is_indirect()+        {+            state.gen_(place.local);+        }+    }+}++////////////////////////////////////////////////////////////////////////////////+// Matrix construction++/// Different "phases" of a single MIR statement, used to describe how+/// overlapping operands are handled.+///+/// As a general rule, source operands are read in the `Early` phase and+/// destination places are written in the `Late` phase.+#[derive(Copy, Clone, Debug, PartialEq, Eq)]+pub enum SplitPointEffect {+    Early = 0,+    Late = 1,+}++rustc_index::newtype_index! {+    /// A `PointIndex` with the lower bit encoding early/late inside a+    /// statement.+    ///+    /// This is used to model overlap constraints within a MIR statement: if a+    /// source/destination are allowed to overlap then the source is read in+    /// `SplitPointEffect::Early` and the write is done in+    /// `SplitPointEffect::Late`.+    #[orderable]+    #[debug_format = "SplitPointIndex({})"]+    pub struct SplitPointIndex {}+}++impl SplitPointIndex {+    pub fn new(point: PointIndex, effect: SplitPointEffect) -> SplitPointIndex {+        let index = (point.as_u32() << 1) | (effect as u32);+        SplitPointIndex::from_u32(index)+    }++    pub fn point(self) -> PointIndex {+        PointIndex::from_u32(self.as_u32() >> 1)+    }++    pub fn effect(self) -> SplitPointEffect {+        match self.as_u32() & 1 {+            0 => SplitPointEffect::Early,+            1 => SplitPointEffect::Late,+            _ => unreachable!(),+        }+    }+}++/// Helper type to construct a `SparseIntervalMatrix`.+struct MatrixBuilder {+    matrix: SparseIntervalMatrix<Local, SplitPointIndex>,+    range_start: IndexVec<Local, Option<SplitPointIndex>>,++    // Track locals that have been live at any point in a block so that at the+    // end of a block we don't need to iterate over all locals. This+    // significantly speeds up matrix building.+    maybe_live_locals: Vec<Local>,+}++impl MatrixBuilder {+    fn gen_(&mut self, local: Local, point: PointIndex, effect: SplitPointEffect) {+        let split_point = SplitPointIndex::new(point, effect);++        // No-op if the local is already live.+        if self.range_start[local].is_none() {+            self.range_start[local] = Some(split_point);+            self.maybe_live_locals.push(local);+        }+    }++    fn kill(&mut self, local: Local, point: PointIndex, effect: SplitPointEffect) {+        let end = SplitPointIndex::new(point, effect);++        // No-op if the local is already dead.+        if let Some(start) = self.range_start[local].take() {+            debug_assert!(end >= start);+            self.matrix.append_range(local, start..=end);+        }+    }++    fn kill_all(&mut self, point: PointIndex, effect: SplitPointEffect) {+        while let Some(local) = self.maybe_live_locals.pop() {+            self.kill(local, point, effect);+        }+    }++    fn kill_all_except(&mut self, except: Local, point: PointIndex, effect: SplitPointEffect) {+        while let Some(local) = self.maybe_live_locals.pop() {+            if local != except {+                self.kill(local, point, effect);+            }+        }+        self.maybe_live_locals.push(except);+    }+}++pub fn liveness_matrix<'tcx>(+    tcx: TyCtxt<'tcx>,+    body: &mir::Body<'tcx>,+    points: &DenseLocationMap,+    pass_name: Option<&'static str>,+) -> SparseIntervalMatrix<Local, SplitPointIndex> {+    let mut kill_points_vec = vec![];+    let kill_points = KillPoints::compute(tcx, body, pass_name, points, &mut kill_points_vec);+    let mut results = PreciseLiveness { kill_points: &kill_points, points }+        .iterate_to_fixpoint(tcx, body, pass_name);++    let mut builder = MatrixBuilder {+        matrix: SparseIntervalMatrix::new(points.num_points() * 2),+        range_start: IndexVec::from_elem_n(None, body.local_decls.len()),+        maybe_live_locals: Vec::new(),+    };+    for (block, block_data) in body.basic_blocks.iter_enumerated() {+        // We can mutate the state in-place since we're not using it any more+        // after this point.+        let state = &mut results.entry_states[block];++        // Notably this kills any dead results produced by a predecessor's+        // terminator.+        state.intersect(&kill_points.live_on_entry[block]);++        // Gen any locals that are live at the start of the block. If this block+        // only consists of a return terminator then only gen the return place+        // instead. This ensures that StorageDead for all other locals are+        // inserted before the return terminator.+        let terminator = block_data.terminator();+        if let mir::TerminatorKind::Return = terminator.kind+            && block_data.statements.is_empty()+        {+            if state.contains(mir::RETURN_PLACE) {+                builder.gen_(mir::RETURN_PLACE, points.entry_point(block), SplitPointEffect::Early);+            }+        } else {+            for local in state.iter() {+                builder.gen_(local, points.entry_point(block), SplitPointEffect::Early);+            }+        }++        for (statement_index, statement) in block_data.statements.iter().enumerate() {+            let location = Location { block, statement_index };+            let point = points.point_from_location(location);++            // Kill moved locals at the early point.+            VisitDeallocatedLocalsWith(|local, _| {+                builder.kill(local, point, SplitPointEffect::Early);+            })+            .visit_statement(statement, location);++            // Kill any locals which are no longer used after this statement.+            for &(local, _) in kill_points.kill_points_map[point] {+                builder.kill(local, point, SplitPointEffect::Early);+            }++            // Gen destination places at the late point.+            VisitAllocatedLocalsWith(|local, _| {+                builder.gen_(local, point, SplitPointEffect::Late);+            })+            .visit_statement(statement, location);++            // Kill any dead destination places: they will only appear at the+            // late point of the statement they are generated in, which is+            // sufficient for determining overlap.+            for &(local, _) in kill_points.kill_points_map[point] {+                builder.kill(local, point, SplitPointEffect::Late);+            }+        }++        // If this block ends in a return terminator, end all live ranges before+        // the terminator so that StorageDead statements are inserted before it.+        //+        // This is useful after inlining so that the lifetime of locals in the+        // inlined callee don't extend past the call in the caller.+        if let mir::TerminatorKind::Return = terminator.kind+            && !block_data.statements.is_empty()+        {+            // Blocks with only a return terminator are handled above.+            let location = Location { block, statement_index: block_data.statements.len() - 1 };+            let point = points.point_from_location(location);+            builder.kill_all_except(mir::RETURN_PLACE, point, SplitPointEffect::Late);+        }++        let location = Location { block, statement_index: block_data.statements.len() };+        let point = points.point_from_location(location);++        // Kill moved locals at the early point.+        VisitDeallocatedLocalsWith(|local, _| {+            builder.kill(local, point, SplitPointEffect::Early);+        })+        .visit_terminator(terminator, location);++        // Kill any locals which are no longer used after this terminator.+        for &(local, _) in kill_points.kill_points_map[point] {+            builder.kill(local, point, SplitPointEffect::Early);+        }++        // Gen destination places at the late point.+        VisitAllocatedLocalsWith(|local, _| {+            builder.gen_(local, point, SplitPointEffect::Late);+        })+        .visit_terminator(terminator, location);++        // Move arguments to a call are treated specially: the place that they+        // represent is passed directly to the callee, which means that they are+        // not allowed to alias any other move operand or the destination place.+        // This is represented here by extending their live range to the late+        // part, making it overlap with that of the destination place.+        //+        // Notably, this *doesn't* apply to TailCall.+        if let mir::TerminatorKind::Call {+            func: _,+            args,+            destination: _,+            target: _,+            unwind: _,+            call_source: _,+            fn_span: _,+        } = &terminator.kind+        {+            for arg in args {+                if let mir::Operand::Move(place) = arg.node+                    && !place.is_indirect()+                {+                    builder.gen_(place.local, point, SplitPointEffect::Late);+                }+            }+        }++        // End the lifetimes of all locals at the end of the block. Successor+        // blocks (which may not be contiguous in the index space!) will+        // initialize the lifetimes again from their entry state.+        builder.kill_all(point, SplitPointEffect::Late);+    }++    builder.matrix+}++pub fn dump_liveness_matrix<'tcx>(+    tcx: TyCtxt<'tcx>,+    body: &mir::Body<'tcx>,+    pass_name: &'static str,+    points: &DenseLocationMap,+    matrix: &SparseIntervalMatrix<Local, SplitPointIndex>,+) {+    let locals_live_at = |split_point| {+        matrix.rows().filter(|&r| matrix.contains(r, split_point)).collect::<Vec<_>>()+    };++    if let Some(dumper) = MirDumper::new(tcx, pass_name, body) {+        let extra_data = &|pass_where, w: &mut dyn std::io::Write| {+            if let PassWhere::BeforeLocation(loc) = pass_where {+                let point = points.point_from_location(loc);+                let split_point = SplitPointIndex::new(point, SplitPointEffect::Early);+                let live = locals_live_at(split_point);+                writeln!(w, "        // {loc:?}-early => {live:?}")?;+                let split_point = SplitPointIndex::new(point, SplitPointEffect::Late);+                let live = locals_live_at(split_point);+                writeln!(w, "        // {loc:?}-late => {live:?}")?;+            }+            Ok(())+        };++        dumper.set_extra_data(extra_data).dump_mir(body)+    }+}
compiler/rustc_mir_dataflow/src/pretty.rs28 + / 1 −
@@ -7,7 +7,11 @@ use rustc_middle::ty::TyCtxt;  use crate::debuginfo::debuginfo_locals; use crate::framework::Analysis;-use crate::impls::{MaybeLiveLocals, MaybeTransitiveLiveLocals, borrowed_locals};+use crate::impls::{+    MaybeLiveLocals, MaybeTransitiveLiveLocals, SplitPointEffect, SplitPointIndex, borrowed_locals,+    liveness_matrix,+};+use crate::points::DenseLocationMap; use crate::{ResultsVisitor, visit_results};  type ExtraDataFn = dyn Fn(PassWhere, &mut dyn io::Write) -> io::Result<()>;@@ -33,6 +37,29 @@ pub(crate) fn mir_pretty_extra_data<'tcx>(                     &mut Annotator { annotations: &mut annotations },                 );             }+            RustcMirKind::PrettyPreciseLiveness => {+                let points = DenseLocationMap::new(body);+                let matrix = liveness_matrix(tcx, body, &points, None);+                for (block, data) in body.basic_blocks.iter_enumerated() {+                    for statement_index in 0..=data.statements.len() {+                        let location = mir::Location { block, statement_index };+                        let point = points.point_from_location(location);+                        let locals_live_at = |effect| {+                            let split_point = SplitPointIndex::new(point, effect);+                            matrix+                                .rows()+                                .filter(|&r| matrix.contains(r, split_point))+                                .collect::<Vec<_>>()+                        };+                        let early = locals_live_at(SplitPointEffect::Early);+                        annotations+                            .add(PassWhere::BeforeLocation(location), format!("early: {early:?}"));+                        let late = locals_live_at(SplitPointEffect::Late);+                        annotations+                            .add(PassWhere::BeforeLocation(location), format!("late: {late:?}"));+                    }+                }+            }             RustcMirKind::PrettyTransitiveLiveLocals => {                 let borrowed_locals = borrowed_locals(body);                 let debuginfo_locals = debuginfo_locals(body);
compiler/rustc_span/src/symbol.rs1 + / 0 −
@@ -1899,6 +1899,7 @@ symbols! {         rustc_peek_maybe_uninit,         rustc_preserve_ub_checks,         rustc_pretty_live_locals,+        rustc_pretty_precise_liveness,         rustc_pretty_transitive_live_locals,         rustc_private,         rustc_proc_macro_decls,
tests/mir-opt/precise_liveness.borrowed.dataflow.0.miradded30 + / 0 −
@@ -0,0 +1,30 @@+// MIR for `borrowed` 0 dataflow++fn borrowed() -> u32 {+    let mut _0: u32;+    let mut _1: u32;+    let mut _2: u32;+    let mut _3: *const u32;+    let mut _4: *const u32;++    bb0: {+        // early: []+        // late: [_1]+        _1 = const 1_u32;+        // early: [_1]+        // late: [_1, _2]+        _2 = const 2_u32;+        // early: [_1, _2]+        // late: [_1, _2, _3]+        _3 = &raw const _1;+        // early: [_1, _2]+        // late: [_1, _2, _4]+        _4 = &raw const _2;+        // early: [_1, _2]+        // late: [_0, _1]+        _0 = Add(copy _1, move _2);+        // early: [_0]+        // late: []+        return;+    }+}
tests/mir-opt/precise_liveness.branch.dataflow.0.miradded33 + / 0 −
@@ -0,0 +1,33 @@+// MIR for `branch` 0 dataflow++fn branch(_1: bool) -> u32 {+    let mut _0: u32;+    let mut _2: u32;++    bb0: {+        // early: [_1]+        // late: [_1, _2]+        _2 = const 1_u32;+        // early: [_1, _2]+        // late: [_2]+        switchInt(copy _1) -> [1: bb1, otherwise: bb2];+    }++    bb1: {+        // early: [_2]+        // late: [_0]+        _0 = move _2;+        // early: [_0]+        // late: []+        return;+    }++    bb2: {+        // early: []+        // late: [_0]+        _0 = const 0_u32;+        // early: [_0]+        // late: []+        return;+    }+}
tests/mir-opt/precise_liveness.call.dataflow.0.miradded39 + / 0 −
@@ -0,0 +1,39 @@+// MIR for `call` 0 dataflow++fn call() -> u32 {+    let mut _0: u32;+    let mut _1: u32;+    let mut _2: u32;+    let mut _3: u32;+    let mut _4: *const u32;++    bb0: {+        // early: []+        // late: [_1]+        _1 = const 1_u32;+        // early: [_1]+        // late: [_1, _2]+        _2 = const 2_u32;+        // early: [_1, _2]+        // late: [_1, _3]+        _3 = callee(move _1, copy _2) -> [return: bb1, unwind: bb2];+    }++    bb1: {+        // early: [_3]+        // late: [_3, _4]+        _4 = &raw const _3;+        // early: [_3]+        // late: [_0]+        _0 = move _3;+        // early: [_0]+        // late: []+        return;+    }++    bb2 (cleanup): {+        // early: [_3]+        // late: [_3]+        resume;+    }+}
tests/mir-opt/precise_liveness.call_borrowed_field.dataflow.0.miradded35 + / 0 −
@@ -0,0 +1,35 @@+// MIR for `call_borrowed_field` 0 dataflow++fn call_borrowed_field() -> u32 {+    let mut _0: u32;+    let mut _1: (u32,);+    let mut _2: u32;+    let mut _3: *const (u32,);++    bb0: {+        // early: []+        // late: [_1]+        (_1.0: u32) = const 1_u32;+        // early: [_1]+        // late: [_1, _3]+        _3 = &raw const _1;+        // early: [_1]+        // late: [_1, _2]+        _2 = callee(move (_1.0: u32), const 0_u32) -> [return: bb1, unwind: bb2];+    }++    bb1: {+        // early: [_1, _2]+        // late: [_0, _1]+        _0 = move _2;+        // early: [_0]+        // late: []+        return;+    }++    bb2 (cleanup): {+        // early: [_1]+        // late: [_1]+        resume;+    }+}
tests/mir-opt/precise_liveness.call_field.dataflow.0.miradded21 + / 0 −
@@ -0,0 +1,21 @@+// MIR for `call_field` 0 dataflow++fn call_field() -> u32 {+    let mut _0: u32;+    let mut _1: (u32,);++    bb0: {+        // early: []+        // late: [_1]+        (_1.0: u32) = const 1_u32;+        // early: [_1]+        // late: [_0, _1]+        _0 = callee(move (_1.0: u32), const 0_u32) -> [return: bb1, unwind unreachable];+    }++    bb1: {+        // early: [_0]+        // late: []+        return;+    }+}
tests/mir-opt/precise_liveness.call_indirect.dataflow.0.miradded17 + / 0 −
@@ -0,0 +1,17 @@+// MIR for `call_indirect` 0 dataflow++fn call_indirect(_1: *const u32) -> u32 {+    let mut _0: u32;++    bb0: {+        // early: [_1]+        // late: [_0]+        _0 = callee(move (*_1), const 0_u32) -> [return: bb1, unwind unreachable];+    }++    bb1: {+        // early: [_0]+        // late: []+        return;+    }+}
tests/mir-opt/precise_liveness.dead_destinations.dataflow.0.miradded31 + / 0 −
@@ -0,0 +1,31 @@+// MIR for `dead_destinations` 0 dataflow++fn dead_destinations() -> u32 {+    let mut _0: u32;+    let mut _1: u32;+    let mut _2: u32;++    bb0: {+        // early: []+        // late: [_1]+        _1 = const 1_u32;+        // early: []+        // late: [_2]+        _2 = callee(const 1_u32, const 2_u32) -> [return: bb1, unwind: bb2];+    }++    bb1: {+        // early: []+        // late: [_0]+        _0 = const 0_u32;+        // early: [_0]+        // late: []+        return;+    }++    bb2 (cleanup): {+        // early: []+        // late: []+        resume;+    }+}
tests/mir-opt/precise_liveness.dereference.dataflow.0.miradded14 + / 0 −
@@ -0,0 +1,14 @@+// MIR for `dereference` 0 dataflow++fn dereference(_1: *mut u32) -> () {+    let mut _0: ();++    bb0: {+        // early: [_1]+        // late: []+        (*_1) = const 1_u32;+        // early: []+        // late: []+        return;+    }+}
tests/mir-opt/precise_liveness.fields.dataflow.0.miradded25 + / 0 −
@@ -0,0 +1,25 @@+// MIR for `fields` 0 dataflow++fn fields() -> u32 {+    let mut _0: u32;+    let mut _1: (u32, u32);+    let mut _2: *const (u32, u32);++    bb0: {+        // early: []+        // late: [_1]+        (_1.0: u32) = const 1_u32;+        // early: [_1]+        // late: [_1]+        (_1.1: u32) = const 2_u32;+        // early: [_1]+        // late: [_1, _2]+        _2 = &raw const _1;+        // early: [_1]+        // late: [_0, _1]+        _0 = Add(move (_1.0: u32), move (_1.1: u32));+        // early: [_0]+        // late: []+        return;+    }+}
tests/mir-opt/precise_liveness.indexed.dataflow.0.miradded15 + / 0 −
@@ -0,0 +1,15 @@+// MIR for `indexed` 0 dataflow++fn indexed(_1: usize) -> () {+    let mut _0: ();+    let mut _2: [u32; 2];++    bb0: {+        // early: [_1]+        // late: [_2]+        _2[_1] = const 1_u32;+        // early: []+        // late: []+        return;+    }+}
tests/mir-opt/precise_liveness.rsadded357 + / 0 −
@@ -0,0 +1,357 @@+//@ needs-unwind++#![feature(core_intrinsics, custom_mir, rustc_attrs)]+#![crate_type = "lib"]++use std::intrinsics::mir::*;++// A non-borrowed local dies on its last use.+// EMIT_MIR precise_liveness.unborrowed.dataflow.0.mir+#[custom_mir(dialect = "runtime", phase = "optimized")]+#[rustc_mir(rustc_pretty_precise_liveness)]+pub fn unborrowed() -> u32 {+    // CHECK-LABEL: fn unborrowed(+    // CHECK: [[A:_[0-9]+]] = const 1_u32;+    // CHECK: [[B:_[0-9]+]] = const 2_u32;+    // CHECK: // early: [[[A]], [[B]]]+    // CHECK-NEXT: // late: [_0]+    // CHECK-NEXT: _0 = Add(copy [[A]], move [[B]]);+    mir! {+        let a: u32;+        let b: u32;+        {+            a = 1;+            b = 2;+            RET = a + Move(b);+            Return()+        }+    }+}++// A borrowed local only dies on move.+// EMIT_MIR precise_liveness.borrowed.dataflow.0.mir+#[custom_mir(dialect = "runtime", phase = "optimized")]+#[rustc_mir(rustc_pretty_precise_liveness)]+pub fn borrowed() -> u32 {+    // CHECK-LABEL: fn borrowed(+    // CHECK: [[A:_[0-9]+]] = const 1_u32;+    // CHECK: [[B:_[0-9]+]] = const 2_u32;+    // CHECK: = &raw const [[B]];+    // CHECK-NEXT: // early: [[[A]], [[B]]]+    // CHECK-NEXT: // late: [_0, [[A]]]+    // CHECK-NEXT: _0 = Add(copy [[A]], move [[B]]);+    mir! {+        let a: u32;+        let b: u32;+        let a_ptr: *const u32;+        let b_ptr: *const u32;+        {+            a = 1;+            b = 2;+            a_ptr = &raw const a;+            b_ptr = &raw const b;+            RET = a + Move(b);+            Return()+        }+    }+}++// StorageDead kills a lifetime, but it can be re-initialized later.+// EMIT_MIR precise_liveness.storage.dataflow.0.mir+#[custom_mir(dialect = "runtime", phase = "optimized")]+#[rustc_mir(rustc_pretty_precise_liveness)]+pub fn storage() -> u32 {+    // CHECK-LABEL: fn storage(+    // CHECK: // early: [_0, [[BORROWED:_[0-9]+]]]+    // CHECK-NEXT: // late: [_0]+    // CHECK-NEXT: StorageDead([[BORROWED]]);+    // CHECK-NEXT: // early: [_0]+    // CHECK-NEXT: // late: [_0]+    // CHECK-NEXT: StorageLive([[BORROWED]]);+    // CHECK-NEXT: // early: [_0]+    // CHECK-NEXT: // late: [_0, [[BORROWED]]]+    // CHECK-NEXT: [[BORROWED]] = const 3_u32;+    // CHECK-NEXT: // early: [_0, [[BORROWED]]]+    // CHECK-NEXT: // late: [_0]+    // CHECK-NEXT: StorageDead([[BORROWED]]);+    // CHECK-NEXT: // early: [_0]+    // CHECK-NEXT: // late: []+    // CHECK-NEXT: return;+    mir! {+        let borrowed: u32;+        let pointer: *const u32;+        {+            StorageLive(borrowed);+            borrowed = 2;+            pointer = &raw const borrowed;+            RET = borrowed;+            StorageDead(borrowed);+            StorageLive(borrowed);+            borrowed = 3;+            StorageDead(borrowed);+            Return()+        }+    }+}++// Partial initialization starts a lifetime, but only a bare local move ends it.+// EMIT_MIR precise_liveness.fields.dataflow.0.mir+#[custom_mir(dialect = "runtime", phase = "optimized")]+#[rustc_mir(rustc_pretty_precise_liveness)]+pub fn fields() -> u32 {+    // CHECK-LABEL: fn fields(+    // CHECK: // early: []+    // CHECK-NEXT: // late: [[[AGGREGATE:_[0-9]+]]]+    // CHECK-NEXT: ([[AGGREGATE]].0: u32) = const 1_u32;+    // CHECK: = &raw const [[AGGREGATE]];+    // CHECK-NEXT: // early: [[[AGGREGATE]]]+    // CHECK-NEXT: // late: [_0, [[AGGREGATE]]]+    // CHECK-NEXT: _0 = Add(move ([[AGGREGATE]].0: u32), move ([[AGGREGATE]].1: u32));+    mir! {+        let aggregate: (u32, u32);+        let pointer: *const (u32, u32);+        {+            aggregate.0 = 1;+            aggregate.1 = 2;+            pointer = &raw const aggregate;+            RET = Move(aggregate.0) + Move(aggregate.1);+            Return()+        }+    }+}++unsafe extern "C-unwind" {+    fn callee(a: u32, b: u32) -> u32;+}++// Check the behavior around calls:+// - Destination is initialized at the late point.+// - Move arguments have their live range extended to the late point.+// - Copy arguments die at the early point when this is their last use.+// - Destination remains live on unwind edges.+// EMIT_MIR precise_liveness.call.dataflow.0.mir+#[custom_mir(dialect = "runtime", phase = "optimized")]+#[rustc_mir(rustc_pretty_precise_liveness)]+pub fn call() -> u32 {+    // CHECK-LABEL: fn call(+    // CHECK: [[A:_[0-9]+]] = const 1_u32;+    // CHECK: [[B:_[0-9]+]] = const 2_u32;+    // CHECK-NEXT: // early: [[[A]], [[B]]]+    // CHECK-NEXT: // late: [[[A]], [[DESTINATION:_[0-9]+]]]+    // CHECK-NEXT: [[DESTINATION]] = callee(move [[A]], copy [[B]])+    // CHECK: = &raw const [[DESTINATION]];+    // CHECK-NEXT: // early: [[[DESTINATION]]]+    // CHECK-NEXT: // late: [_0]+    // CHECK-NEXT: _0 = move [[DESTINATION]];+    // CHECK: (cleanup): {+    // CHECK-NEXT: // early: [[[DESTINATION]]]+    // CHECK-NEXT: // late: [[[DESTINATION]]]+    // CHECK-NEXT: resume;+    mir! {+        let a: u32;+        let b: u32;+        let destination: u32;+        let pointer: *const u32;+        {+            a = 1;+            b = 2;+            Call(destination = callee(Move(a), b), ReturnTo(done), UnwindCleanup(cleanup))+        }+        done = {+            pointer = &raw const destination;+            RET = Move(destination);+            Return()+        }+        cleanup(cleanup) = {+            UnwindResume()+        }+    }+}++// A value used on only one branch is not live on the other branch.+// EMIT_MIR precise_liveness.branch.dataflow.0.mir+#[custom_mir(dialect = "runtime", phase = "optimized")]+#[rustc_mir(rustc_pretty_precise_liveness)]+pub fn branch(condition: bool) -> u32 {+    // CHECK-LABEL: fn branch(+    // CHECK: [[VALUE:_[0-9]+]] = const 1_u32;+    // CHECK-NEXT: // early: [[[CONDITION:_[0-9]+]], [[VALUE]]]+    // CHECK-NEXT: // late: [[[VALUE]]]+    // CHECK-NEXT: switchInt(copy [[CONDITION]]) -> [1: [[USE:bb[0-9]+]], otherwise: [[SKIP:bb[0-9]+]]];+    // CHECK: [[USE]]: {+    // CHECK-NEXT: // early: [[[VALUE]]]+    // CHECK-NEXT: // late: [_0]+    // CHECK-NEXT: _0 = move [[VALUE]];+    // CHECK: [[SKIP]]: {+    // CHECK-NEXT: // early: []+    // CHECK-NEXT: // late: [_0]+    // CHECK-NEXT: _0 = const 0_u32;+    mir! {+        let value: u32;+        {+            value = 1;+            match condition { true => use_value, _ => skip_value }+        }+        use_value = {+            RET = Move(value);+            Return()+        }+        skip_value = {+            RET = 0;+            Return()+        }+    }+}++// Dead destinations are live only at the late point of the assignment or call.+// EMIT_MIR precise_liveness.dead_destinations.dataflow.0.mir+#[custom_mir(dialect = "runtime", phase = "optimized")]+#[rustc_mir(rustc_pretty_precise_liveness)]+pub fn dead_destinations() -> u32 {+    // CHECK-LABEL: fn dead_destinations(+    // CHECK: // early: []+    // CHECK-NEXT: // late: [[[ASSIGNMENT:_[0-9]+]]]+    // CHECK-NEXT: [[ASSIGNMENT]] = const 1_u32;+    // CHECK-NEXT: // early: []+    // CHECK-NEXT: // late: [[[DESTINATION:_[0-9]+]]]+    // CHECK-NEXT: [[DESTINATION]] = callee(const 1_u32, const 2_u32) -> [return: [[DONE:bb[0-9]+]], unwind: [[CLEANUP:bb[0-9]+]]];+    // CHECK: [[DONE]]: {+    // CHECK-NEXT: // early: []+    // CHECK-NEXT: // late: [_0]+    // CHECK-NEXT: _0 = const 0_u32;+    // CHECK: [[CLEANUP]] (cleanup): {+    // CHECK-NEXT: // early: []+    // CHECK-NEXT: // late: []+    // CHECK-NEXT: resume;+    mir! {+        let assignment: u32;+        let destination: u32;+        {+            assignment = 1;+            Call(destination = callee(1, 2), ReturnTo(done), UnwindCleanup(cleanup))+        }+        done = {+            RET = 0;+            Return()+        }+        cleanup(cleanup) = {+            UnwindResume()+        }+    }+}++// An indexed write allocates the array. The index is a use, not a destination.+// EMIT_MIR precise_liveness.indexed.dataflow.0.mir+#[custom_mir(dialect = "runtime", phase = "optimized")]+#[rustc_mir(rustc_pretty_precise_liveness)]+pub fn indexed(index: usize) {+    // CHECK-LABEL: fn indexed(+    // CHECK: // early: [[[INDEX:_[0-9]+]]]+    // CHECK-NEXT: // late: [[[ARRAY:_[0-9]+]]]+    // CHECK-NEXT: [[ARRAY]][[[INDEX]]] = const 1_u32;+    mir! {+        let array: [u32; 2];+        {+            array[index] = 1;+            Return()+        }+    }+}++// A write through a pointer is a use of the pointer, not a def.+// EMIT_MIR precise_liveness.dereference.dataflow.0.mir+#[custom_mir(dialect = "runtime", phase = "optimized")]+#[rustc_mir(rustc_pretty_precise_liveness)]+pub fn dereference(pointer: *mut u32) {+    // CHECK-LABEL: fn dereference(+    // CHECK: // early: [[[POINTER:_[0-9]+]]]+    // CHECK-NEXT: // late: []+    // CHECK-NEXT: (*[[POINTER]]) = const 1_u32;+    mir! {+        {+            *pointer = 1;+            Return()+        }+    }+}++// A field move keeps the base allocation live through the call's late point.+// EMIT_MIR precise_liveness.call_field.dataflow.0.mir+#[custom_mir(dialect = "runtime", phase = "optimized")]+#[rustc_mir(rustc_pretty_precise_liveness)]+pub fn call_field() -> u32 {+    // CHECK-LABEL: fn call_field(+    // CHECK: ([[AGGREGATE:_[0-9]+]].0: u32) = const 1_u32;+    // CHECK-NEXT: // early: [[[AGGREGATE]]]+    // CHECK-NEXT: // late: [_0, [[AGGREGATE]]]+    // CHECK-NEXT: _0 = callee(move ([[AGGREGATE]].0: u32), const 0_u32)+    // CHECK: // early: [_0]+    // CHECK-NEXT: // late: []+    // CHECK-NEXT: return;+    mir! {+        let aggregate: (u32,);+        {+            aggregate.0 = 1;+            Call(RET = callee(Move(aggregate.0), 0), ReturnTo(done), UnwindUnreachable())+        }+        done = {+            Return()+        }+    }+}++// A borrowed field's base remains live in both successors after a field move.+// EMIT_MIR precise_liveness.call_borrowed_field.dataflow.0.mir+#[custom_mir(dialect = "runtime", phase = "optimized")]+#[rustc_mir(rustc_pretty_precise_liveness)]+pub fn call_borrowed_field() -> u32 {+    // CHECK-LABEL: fn call_borrowed_field(+    // CHECK: = &raw const [[AGGREGATE:_[0-9]+]];+    // CHECK-NEXT: // early: [[[AGGREGATE]]]+    // CHECK-NEXT: // late: [[[AGGREGATE]], [[DESTINATION:_[0-9]+]]]+    // CHECK-NEXT: [[DESTINATION]] = callee(move ([[AGGREGATE]].0: u32), const 0_u32) -> [return: [[DONE:bb[0-9]+]], unwind: [[CLEANUP:bb[0-9]+]]];+    // CHECK: [[DONE]]: {+    // CHECK-NEXT: // early: [[[AGGREGATE]], [[DESTINATION]]]+    // CHECK-NEXT: // late: [_0, [[AGGREGATE]]]+    // CHECK-NEXT: _0 = move [[DESTINATION]];+    // CHECK: [[CLEANUP]] (cleanup): {+    // CHECK-NEXT: // early: [[[AGGREGATE]]]+    // CHECK-NEXT: // late: [[[AGGREGATE]]]+    // CHECK-NEXT: resume;+    mir! {+        let aggregate: (u32,);+        let destination: u32;+        let borrow: *const (u32,);+        {+            aggregate.0 = 1;+            borrow = &raw const aggregate;+            Call(destination = callee(Move(aggregate.0), 0), ReturnTo(done), UnwindCleanup(cleanup))+        }+        done = {+            RET = Move(destination);+            Return()+        }+        cleanup(cleanup) = {+            UnwindResume()+        }+    }+}++// An indirect move does not extend the pointer local's lifetime to the late point.+// EMIT_MIR precise_liveness.call_indirect.dataflow.0.mir+#[custom_mir(dialect = "runtime", phase = "optimized")]+#[rustc_mir(rustc_pretty_precise_liveness)]+pub fn call_indirect(pointer: *const u32) -> u32 {+    // CHECK-LABEL: fn call_indirect(+    // CHECK: // early: [[[POINTER:_[0-9]+]]]+    // CHECK-NEXT: // late: [_0]+    // CHECK-NEXT: _0 = callee(move (*[[POINTER]]), const 0_u32)+    mir! {+        {+            Call(RET = callee(Move(*pointer), 0), ReturnTo(done), UnwindUnreachable())+        }+        done = {+            Return()+        }+    }+}
tests/mir-opt/precise_liveness.storage.dataflow.0.miradded37 + / 0 −
@@ -0,0 +1,37 @@+// MIR for `storage` 0 dataflow++fn storage() -> u32 {+    let mut _0: u32;+    let mut _1: u32;+    let mut _2: *const u32;++    bb0: {+        // early: []+        // late: []+        StorageLive(_1);+        // early: []+        // late: [_1]+        _1 = const 2_u32;+        // early: [_1]+        // late: [_1, _2]+        _2 = &raw const _1;+        // early: [_1]+        // late: [_0, _1]+        _0 = copy _1;+        // early: [_0, _1]+        // late: [_0]+        StorageDead(_1);+        // early: [_0]+        // late: [_0]+        StorageLive(_1);+        // early: [_0]+        // late: [_0, _1]+        _1 = const 3_u32;+        // early: [_0, _1]+        // late: [_0]+        StorageDead(_1);+        // early: [_0]+        // late: []+        return;+    }+}
tests/mir-opt/precise_liveness.unborrowed.dataflow.0.miradded22 + / 0 −
@@ -0,0 +1,22 @@+// MIR for `unborrowed` 0 dataflow++fn unborrowed() -> u32 {+    let mut _0: u32;+    let mut _1: u32;+    let mut _2: u32;++    bb0: {+        // early: []+        // late: [_1]+        _1 = const 1_u32;+        // early: [_1]+        // late: [_1, _2]+        _2 = const 2_u32;+        // early: [_1, _2]+        // late: [_0]+        _0 = Add(copy _1, move _2);+        // early: [_0]+        // late: []+        return;+    }+}