| //! 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) |
| } |
| } |