| //! A framework that can express both [gen-kill] and generic dataflow problems. |
| //! |
| //! To use this framework, implement the [`Analysis`] trait. There used to be a `GenKillAnalysis` |
| //! alternative trait for gen-kill analyses that would pre-compute the transfer function for each |
| //! block. It was intended as an optimization, but it ended up not being any faster than |
| //! `Analysis`. |
| //! |
| //! The `impls` module contains several examples of dataflow analyses. |
| //! |
| //! Then call `iterate_to_fixpoint` on your type that impls `Analysis` to get a `Results`. From |
| //! there, you can use a `ResultsCursor` to inspect the fixpoint solution to your dataflow problem |
| //! (good for inspecting a small number of locations), or implement the `ResultsVisitor` interface |
| //! and use `visit_results` (good for inspecting many or all locations). The following example uses |
| //! the `ResultsCursor` approach. |
| //! |
| //! ```ignore (cross-crate-imports) |
| //! use rustc_const_eval::dataflow::Analysis; // Makes `iterate_to_fixpoint` available. |
| //! |
| //! fn do_my_analysis(tcx: TyCtxt<'tcx>, body: &mir::Body<'tcx>) { |
| //! let analysis = MyAnalysis::new() |
| //! .iterate_to_fixpoint(tcx, body, None) |
| //! .into_results_cursor(body); |
| //! |
| //! // Print the dataflow state *after* each statement in the start block. |
| //! for (_, statement_index) in body.block_data[START_BLOCK].statements.iter_enumerated() { |
| //! cursor.seek_after(Location { block: START_BLOCK, statement_index }); |
| //! let state = cursor.get(); |
| //! println!("{:?}", state); |
| //! } |
| //! } |
| //! ``` |
| //! |
| //! [gen-kill]: https://en.wikipedia.org/wiki/Data-flow_analysis#Bit_vector_problems |
| |
| use rustc_index::bit_set::{DenseBitSet, MixedBitSet}; |
| use rustc_index::{Idx, IndexVec}; |
| use rustc_middle::bug; |
| use rustc_middle::mir::{ |
| self, BasicBlock, BasicBlockData, CallReturnPlaces, Location, TerminatorEdges, |
| }; |
| use rustc_middle::ty::TyCtxt; |
| use tracing::error; |
| |
| use self::graphviz::write_graphviz_results; |
| use super::fmt::DebugWithContext; |
| |
| mod cursor; |
| mod direction; |
| pub mod fmt; |
| pub mod graphviz; |
| pub mod lattice; |
| mod results; |
| mod visitor; |
| |
| pub use self::cursor::ResultsCursor; |
| pub use self::direction::{Backward, Direction, Forward}; |
| pub use self::lattice::{JoinSemiLattice, MaybeReachable}; |
| pub use self::results::{EntryStates, Results}; |
| pub use self::visitor::{ResultsVisitor, visit_results}; |
| |
| /// Analysis domains are all bitsets of various kinds. This trait holds |
| /// operations needed by all of them. |
| pub trait BitSetExt<T> { |
| fn contains(&self, elem: T) -> bool; |
| } |
| |
| impl<T: Idx> BitSetExt<T> for DenseBitSet<T> { |
| fn contains(&self, elem: T) -> bool { |
| self.contains(elem) |
| } |
| } |
| |
| impl<T: Idx> BitSetExt<T> for MixedBitSet<T> { |
| fn contains(&self, elem: T) -> bool { |
| self.contains(elem) |
| } |
| } |
| |
| /// A dataflow problem with an arbitrarily complex transfer function. |
| /// |
| /// This trait specifies the lattice on which this analysis operates (the domain), its |
| /// initial value at the entry point of each basic block, and various operations. |
| /// |
| /// # Convergence |
| /// |
| /// When implementing this trait it's possible to choose a transfer function such that the analysis |
| /// does not reach fixpoint. To guarantee convergence, your transfer functions must maintain the |
| /// following invariant: |
| /// |
| /// > If the dataflow state **before** some point in the program changes to be greater |
| /// than the prior state **before** that point, the dataflow state **after** that point must |
| /// also change to be greater than the prior state **after** that point. |
| /// |
| /// This invariant guarantees that the dataflow state at a given point in the program increases |
| /// monotonically until fixpoint is reached. Note that this monotonicity requirement only applies |
| /// to the same point in the program at different points in time. The dataflow state at a given |
| /// point in the program may or may not be greater than the state at any preceding point. |
| pub trait Analysis<'tcx> { |
| /// The type that holds the dataflow state at any given point in the program. |
| type Domain: Clone + JoinSemiLattice; |
| |
| /// The direction of this analysis. Either `Forward` or `Backward`. |
| type Direction: Direction = Forward; |
| |
| /// Auxiliary data used for analyzing `SwitchInt` terminators, if necessary. |
| type SwitchIntData = !; |
| |
| /// A descriptive name for this analysis. Used only for debugging. |
| /// |
| /// This name should be brief and contain no spaces, periods or other characters that are not |
| /// suitable as part of a filename. |
| const NAME: &'static str; |
| |
| /// Returns the initial value of the dataflow state upon entry to each basic block. |
| fn bottom_value(&self, body: &mir::Body<'tcx>) -> Self::Domain; |
| |
| /// Mutates the initial value of the dataflow state upon entry to the `START_BLOCK`. |
| /// |
| /// For backward analyses, initial state (besides the bottom value) is not yet supported. Trying |
| /// to mutate the initial state will result in a panic. |
| // |
| // FIXME: For backward dataflow analyses, the initial state should be applied to every basic |
| // block where control flow could exit the MIR body (e.g., those terminated with `return` or |
| // `resume`). It's not obvious how to handle `yield` points in coroutines, however. |
| fn initialize_start_block(&self, body: &mir::Body<'tcx>, state: &mut Self::Domain); |
| |
| /// Given an `EffectIndex`, calls the appropriate `apply_*` method in the |
| /// {early,primary} x {statement,terminator} space. |
| /// |
| /// Do not override this; instead override one or more of the `apply_*` methods. |
| #[inline] |
| fn apply_effect<'mir>( |
| &self, |
| state: &mut Self::Domain, |
| block: BasicBlock, |
| block_data: &'mir BasicBlockData<'tcx>, |
| idx: EffectIndex, |
| ) { |
| let statement_index = idx.statement_index; |
| let terminator_index = block_data.statements.len(); |
| let loc = Location { block, statement_index }; |
| let is_terminator = statement_index == terminator_index; |
| |
| if !is_terminator { |
| let statement = &block_data.statements[statement_index]; |
| match idx.effect { |
| Effect::Early => self.apply_early_statement_effect(state, statement, loc), |
| Effect::Primary => self.apply_primary_statement_effect(state, statement, loc), |
| } |
| } else { |
| let terminator = block_data.terminator(); |
| match idx.effect { |
| Effect::Early => self.apply_early_terminator_effect(state, terminator, loc), |
| Effect::Primary => { |
| self.apply_primary_terminator_effect(state, terminator, loc); |
| } |
| } |
| } |
| } |
| |
| /// Updates the current dataflow state with an "early" effect, i.e. one |
| /// that occurs immediately before the given statement. |
| /// |
| /// This method is useful if the consumer of the results of this analysis only needs to observe |
| /// *part* of the effect of a statement (e.g. for two-phase borrows). As a general rule, |
| /// analyses should not implement this without also implementing |
| /// `apply_primary_statement_effect`. |
| fn apply_early_statement_effect( |
| &self, |
| _state: &mut Self::Domain, |
| _statement: &mir::Statement<'tcx>, |
| _location: Location, |
| ) { |
| } |
| |
| /// Updates the current dataflow state with the effect of evaluating a statement. |
| fn apply_primary_statement_effect( |
| &self, |
| state: &mut Self::Domain, |
| statement: &mir::Statement<'tcx>, |
| location: Location, |
| ); |
| |
| /// Updates the current dataflow state with an effect that occurs immediately *before* the |
| /// given terminator. |
| /// |
| /// This method is useful if the consumer of the results of this analysis needs only to observe |
| /// *part* of the effect of a terminator (e.g. for two-phase borrows). As a general rule, |
| /// analyses should not implement this without also implementing |
| /// `apply_primary_terminator_effect`. |
| fn apply_early_terminator_effect( |
| &self, |
| _state: &mut Self::Domain, |
| _terminator: &mir::Terminator<'tcx>, |
| _location: Location, |
| ) { |
| } |
| |
| /// Gets the terminator edges. Used by forward analyses only. Called *before* |
| /// `apply_primary_terminator_effect` is applied; this might seem strange but in practice |
| /// `MaybeInitializedPlaces` needs that ordering and other analyses work with either ordering. |
| fn get_terminator_edges<'mir>( |
| &self, |
| _state: &Self::Domain, |
| terminator: &'mir mir::Terminator<'tcx>, |
| _location: Location, |
| ) -> TerminatorEdges<'mir, 'tcx> { |
| terminator.edges() |
| } |
| |
| /// Updates the current dataflow state with the effect of evaluating a terminator. |
| /// |
| /// The effect of a successful return from a `Call` terminator should **not** be accounted for |
| /// in this function. That should go in `apply_call_return_effect`. For example, in the |
| /// `InitializedPlaces` analyses, the return place for a function call is not marked as |
| /// initialized here. |
| fn apply_primary_terminator_effect( |
| &self, |
| _state: &mut Self::Domain, |
| _terminator: &mir::Terminator<'tcx>, |
| _location: Location, |
| ) { |
| } |
| |
| /* Edge-specific effects */ |
| |
| /// Updates the current dataflow state with the effect of a successful return from a `Call` |
| /// terminator. |
| /// |
| /// This is separate from `apply_primary_terminator_effect` to properly track state across |
| /// unwind edges. |
| fn apply_call_return_effect( |
| &self, |
| _state: &mut Self::Domain, |
| _block: BasicBlock, |
| _return_places: CallReturnPlaces<'_, 'tcx>, |
| ) { |
| } |
| |
| /// Used to update the current dataflow state with the effect of taking a particular branch in |
| /// a `SwitchInt` terminator. |
| /// |
| /// Unlike the other edge-specific effects, which are allowed to mutate `Self::Domain` |
| /// directly, overriders of this method must return a `Self::SwitchIntData` value (wrapped in |
| /// `Some`). The `apply_switch_int_edge_effect` method will then be called once for each |
| /// outgoing edge and will have access to the dataflow state that will be propagated along that |
| /// edge, and also the `Self::SwitchIntData` value. |
| /// |
| /// This interface is somewhat more complex than the other visitor-like "effect" methods. |
| /// However, it is both more ergonomic—callers don't need to recompute or cache information |
| /// about a given `SwitchInt` terminator for each one of its edges—and more efficient—the |
| /// engine doesn't need to clone the exit state for a block unless |
| /// `get_switch_int_data` is actually called. |
| fn get_switch_int_data( |
| &self, |
| _block: mir::BasicBlock, |
| _targets: &mir::SwitchTargets, |
| _discr: &mir::Operand<'tcx>, |
| ) -> Option<Self::SwitchIntData> { |
| None |
| } |
| |
| /// See comments on `get_switch_int_data`. |
| fn apply_switch_int_edge_effect( |
| &self, |
| _state: &mut Self::Domain, |
| _data: &mut Self::SwitchIntData, |
| _target_idx: SwitchTargetIndex, |
| ) { |
| unreachable!(); |
| } |
| |
| /* Extension methods */ |
| |
| /// Finds the fixpoint for this dataflow problem. |
| /// |
| /// You shouldn't need to override this. Its purpose is to enable method chaining like so: |
| /// |
| /// ```ignore (cross-crate-imports) |
| /// let results = MyAnalysis::new(tcx, body) |
| /// .iterate_to_fixpoint(tcx, body, None) |
| /// .into_results_cursor(body); |
| /// ``` |
| /// You can optionally add a `pass_name` to the graphviz output for this particular run of a |
| /// dataflow analysis. Some analyses are run multiple times in the compilation pipeline. |
| /// Without a `pass_name` to differentiates them, only the results for the latest run will be |
| /// saved. |
| fn iterate_to_fixpoint<'mir>( |
| self, |
| tcx: TyCtxt<'tcx>, |
| body: &'mir mir::Body<'tcx>, |
| pass_name: Option<&'static str>, |
| ) -> Results<'tcx, Self> |
| where |
| Self: Sized, |
| Self::Domain: DebugWithContext<Self>, |
| { |
| let mut entry_states = |
| IndexVec::from_fn_n(|_| self.bottom_value(body), body.basic_blocks.len()); |
| self.initialize_start_block(body, &mut entry_states[mir::START_BLOCK]); |
| |
| if Self::Direction::IS_BACKWARD && entry_states[mir::START_BLOCK] != self.bottom_value(body) |
| { |
| bug!("`initialize_start_block` is not yet supported for backward dataflow analyses"); |
| } |
| |
| // Forward analyses use a reverse postorder (`rpo`). Every reachable basic block has a |
| // *rank*: its position within `rpo`. Rank order is dataflow order: for every edge A -> B |
| // that is not a back edge, rank(A) < rank(B). This is independent of basic block numbering |
| // (which depends on the vagaries of CFG construction). |
| // |
| // The CFG traversal uses a "min-rank" algorithm. First, all reachable basic blocks are |
| // marked as dirty. The loop-head invariant is that `curr_rank` always points to the |
| // minimum-rank dirty block in `rpo`. Before processing that block we mark it as clean. If |
| // the processing dirties a block with a rank lower than or equal to `curr_rank` (via a |
| // back edge, which could be an edge-to-self) then `curr_rank` is set to that |
| // lower-or-equal rank. After the block is processed, if `curr_rank` doesn't point to a |
| // dirty block it is moved to the next dirty block, and we iterate again. |
| // |
| // This algorithm ensures each basic block is processed only after all its dirty |
| // predecessors (ignoring back edges). When a back edge dirties an earlier block we return |
| // to that earlier block immediately, which avoids processing later blocks with possibly |
| // soon-to-be-stale information. Loop-free code is processed in a single pass. |
| // |
| // Backward analyses: we want a postorder instead of a reverse postorder, but we also want |
| // to avoid the cost of adding a `postorder` field to `mir::basic_blocks::Cache`. We can |
| // fake a postorder traversal cheaply by using a reverse postorder and flipping the rank |
| // mapping. There is also one wrinkle involving unreachable blocks; see below. |
| |
| rustc_index::newtype_index! { |
| #[orderable] |
| #[debug_format = "bbr{}"] |
| struct BasicBlockRank {} |
| } |
| |
| let rpo: &[BasicBlock] = body.basic_blocks.reverse_postorder(); |
| let last = rpo.len() - 1; |
| |
| let mut ranks: IndexVec<BasicBlock, Option<BasicBlockRank>> = |
| IndexVec::from_elem_n(None, body.basic_blocks.len()); |
| for (i, &bb) in rpo.iter().enumerate() { |
| let rank = if Self::Direction::IS_FORWARD { i } else { last - i }; |
| ranks[bb] = Some(BasicBlockRank::new(rank)); |
| } |
| |
| let mut dirty: DenseBitSet<BasicBlockRank> = DenseBitSet::new_filled(rpo.len()); |
| let mut curr_rank = BasicBlockRank::ZERO; |
| |
| // `state` is not actually used between iterations; this is just an optimization to avoid |
| // reallocating every iteration. |
| let mut state = self.bottom_value(body); |
| |
| loop { |
| let i = curr_rank.as_usize(); |
| let bb = rpo[if Self::Direction::IS_FORWARD { i } else { last - i }]; |
| debug_assert!(dirty.contains(curr_rank)); // check invariant |
| dirty.remove(curr_rank); // invariant temporarily broken |
| |
| state.clone_from(&entry_states[bb]); |
| let prop = |target: BasicBlock, state: &Self::Domain| { |
| // A backward analysis may encounter an unreachable block, because a predecessor |
| // of a reachable block may be unreachable. Ignore any such block. (In contrast, in |
| // a forward analysis any successor of a reachable block must be reachable.) |
| let target_rank = ranks[target]; |
| if Self::Direction::IS_BACKWARD && target_rank.is_none() { |
| return; |
| } |
| let target_rank = target_rank.unwrap(); |
| |
| let set_changed = entry_states[target].join(state); |
| if set_changed { |
| dirty.insert(target_rank); |
| curr_rank = curr_rank.min(target_rank); |
| } |
| }; |
| Self::Direction::apply_effects_in_block(&self, body, &mut state, bb, &body[bb], prop); |
| |
| match dirty.first_set_at_or_after(curr_rank) { |
| Some(rank) => curr_rank = rank, // broken invariant re-established |
| None => break, // no more dirty blocks; finish |
| } |
| } |
| |
| let results = Results { analysis: self, entry_states }; |
| |
| if tcx.sess.opts.unstable_opts.dump_mir_dataflow { |
| let res = write_graphviz_results(tcx, body, &results, pass_name); |
| if let Err(e) = res { |
| error!("Failed to write graphviz dataflow results: {}", e); |
| } |
| } |
| |
| results |
| } |
| } |
| |
| #[derive(Debug, Clone, Copy)] |
| pub enum SwitchTargetIndex { |
| // Index of a normal switch target. |
| Normal(usize), |
| // The final "otherwise" fallback target. |
| Otherwise, |
| } |
| |
| /// The legal operations for a transfer function in a gen/kill problem. |
| pub trait GenKill<T> { |
| /// Inserts `elem` into the state vector. |
| fn gen_(&mut self, elem: T); |
| |
| /// Removes `elem` from the state vector. |
| fn kill(&mut self, elem: T); |
| |
| /// Calls `gen` for each element in `elems`. |
| fn gen_all(&mut self, elems: impl IntoIterator<Item = T>) { |
| for elem in elems { |
| self.gen_(elem); |
| } |
| } |
| |
| /// Calls `kill` for each element in `elems`. |
| fn kill_all(&mut self, elems: impl IntoIterator<Item = T>) { |
| for elem in elems { |
| self.kill(elem); |
| } |
| } |
| } |
| |
| impl<T: Idx> GenKill<T> for DenseBitSet<T> { |
| fn gen_(&mut self, elem: T) { |
| self.insert(elem); |
| } |
| |
| fn kill(&mut self, elem: T) { |
| self.remove(elem); |
| } |
| } |
| |
| impl<T: Idx> GenKill<T> for MixedBitSet<T> { |
| fn gen_(&mut self, elem: T) { |
| self.insert(elem); |
| } |
| |
| fn kill(&mut self, elem: T) { |
| self.remove(elem); |
| } |
| } |
| |
| impl<T, S: GenKill<T>> GenKill<T> for MaybeReachable<S> { |
| fn gen_(&mut self, elem: T) { |
| match self { |
| // If the state is not reachable, adding an element does nothing. |
| MaybeReachable::Unreachable => {} |
| MaybeReachable::Reachable(set) => set.gen_(elem), |
| } |
| } |
| |
| fn kill(&mut self, elem: T) { |
| match self { |
| // If the state is not reachable, killing an element does nothing. |
| MaybeReachable::Unreachable => {} |
| MaybeReachable::Reachable(set) => set.kill(elem), |
| } |
| } |
| } |
| |
| // NOTE: DO NOT CHANGE VARIANT ORDER. The derived `Ord` impls rely on the current order. |
| #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] |
| enum Effect { |
| /// The "early" effect (e.g., `apply_early_statement_effect`) for a statement/terminator. |
| Early, |
| |
| /// The "primary" effect (e.g., `apply_primary_statement_effect`) for a statement/terminator. |
| Primary, |
| } |
| |
| impl Effect { |
| const fn at_index(self, statement_index: usize) -> EffectIndex { |
| EffectIndex { effect: self, statement_index } |
| } |
| } |
| |
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| pub struct EffectIndex { |
| statement_index: usize, |
| effect: Effect, |
| } |
| |
| #[cfg(test)] |
| mod tests; |