blob: a72bb3494be4c23b3dc535ba25814472908bf571 [file]
//! 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;