]> git.lizzy.rs Git - rust.git/commitdiff
Add test for `ResultsCursor`
authorDylan MacKenzie <ecstaticmorse@gmail.com>
Wed, 13 Nov 2019 00:12:15 +0000 (16:12 -0800)
committerDylan MacKenzie <ecstaticmorse@gmail.com>
Wed, 15 Jan 2020 00:13:21 +0000 (16:13 -0800)
This is a unit test that ensures the `seek` functions work correctly.

src/librustc/mir/mod.rs
src/librustc_mir/dataflow/generic/mod.rs
src/librustc_mir/dataflow/generic/tests.rs [new file with mode: 0644]

index ccd2a968ded4cb3e61fbcea42ca56479ca2c08cd..3a7c650c4618c1df20a0b7e8645ab8254bf6d5c4 100644 (file)
@@ -215,6 +215,31 @@ pub fn new(
         }
     }
 
+    /// Returns a partially initialized MIR body containing only a list of basic blocks.
+    ///
+    /// The returned MIR contains no `LocalDecl`s (even for the return place) or source scopes. It
+    /// is only useful for testing but cannot be `#[cfg(test)]` because it is used in a different
+    /// crate.
+    pub fn new_cfg_only(basic_blocks: IndexVec<BasicBlock, BasicBlockData<'tcx>>) -> Self {
+        Body {
+            phase: MirPhase::Build,
+            basic_blocks,
+            source_scopes: IndexVec::new(),
+            yield_ty: None,
+            generator_drop: None,
+            generator_layout: None,
+            local_decls: IndexVec::new(),
+            user_type_annotations: IndexVec::new(),
+            arg_count: 0,
+            spread_arg: None,
+            span: DUMMY_SP,
+            control_flow_destroyed: Vec::new(),
+            generator_kind: None,
+            var_debug_info: Vec::new(),
+            ignore_interior_mut_in_const_validation: false,
+        }
+    }
+
     #[inline]
     pub fn basic_blocks(&self) -> &IndexVec<BasicBlock, BasicBlockData<'tcx>> {
         &self.basic_blocks
index 1b628f2c7eb8edbb8f397c609e4d0da17d9270ff..a80e7375482ed48b43b1cbaeb7ffb2406f6342fa 100644 (file)
@@ -307,3 +307,6 @@ fn kill(&mut self, elem: T) {
         self.remove(elem);
     }
 }
+
+#[cfg(test)]
+mod tests;
diff --git a/src/librustc_mir/dataflow/generic/tests.rs b/src/librustc_mir/dataflow/generic/tests.rs
new file mode 100644 (file)
index 0000000..add9bfd
--- /dev/null
@@ -0,0 +1,328 @@
+//! A test for the logic that updates the state in a `ResultsCursor` during seek.
+
+use rustc::mir::{self, BasicBlock, Location};
+use rustc::ty;
+use rustc_index::bit_set::BitSet;
+use rustc_index::vec::IndexVec;
+
+use super::*;
+use crate::dataflow::BottomValue;
+
+/// Returns `true` if the given location points to a `Call` terminator that can return
+/// successfully.
+fn is_call_terminator_non_diverging(body: &mir::Body<'_>, loc: Location) -> bool {
+    loc == body.terminator_loc(loc.block)
+        && matches!(
+            body[loc.block].terminator().kind,
+            mir::TerminatorKind::Call { destination: Some(_), ..  }
+        )
+}
+
+/// Creates a `mir::Body` with a few disconnected basic blocks.
+///
+/// This is the `Body` that will be used by the `MockAnalysis` below. The shape of its CFG is not
+/// important.
+fn mock_body() -> mir::Body<'static> {
+    let span = syntax_pos::DUMMY_SP;
+    let source_info = mir::SourceInfo { scope: mir::OUTERMOST_SOURCE_SCOPE, span };
+
+    let mut blocks = IndexVec::new();
+    let mut block = |n, kind| {
+        let nop = mir::Statement { source_info, kind: mir::StatementKind::Nop };
+
+        blocks.push(mir::BasicBlockData {
+            statements: std::iter::repeat(&nop).cloned().take(n).collect(),
+            terminator: Some(mir::Terminator { source_info, kind }),
+            is_cleanup: false,
+        })
+    };
+
+    let dummy_place = mir::Place { local: mir::RETURN_PLACE, projection: ty::List::empty() };
+
+    block(4, mir::TerminatorKind::Return);
+    block(1, mir::TerminatorKind::Return);
+    block(
+        2,
+        mir::TerminatorKind::Call {
+            func: mir::Operand::Copy(dummy_place.clone()),
+            args: vec![],
+            destination: Some((dummy_place.clone(), mir::START_BLOCK)),
+            cleanup: None,
+            from_hir_call: false,
+        },
+    );
+    block(3, mir::TerminatorKind::Return);
+    block(0, mir::TerminatorKind::Return);
+    block(
+        4,
+        mir::TerminatorKind::Call {
+            func: mir::Operand::Copy(dummy_place.clone()),
+            args: vec![],
+            destination: Some((dummy_place.clone(), mir::START_BLOCK)),
+            cleanup: None,
+            from_hir_call: false,
+        },
+    );
+
+    mir::Body::new_cfg_only(blocks)
+}
+
+/// A dataflow analysis whose state is unique at every possible `SeekTarget`.
+///
+/// Uniqueness is achieved by having a *locally* unique effect before and after each statement and
+/// terminator (see `effect_at_target`) while ensuring that the entry set for each block is
+/// *globally* unique (see `mock_entry_set`).
+///
+/// For example, a `BasicBlock` with ID `2` and a `Call` terminator has the following state at each
+/// location ("+x" indicates that "x" is added to the state).
+///
+/// | Location               | Before            | After  |
+/// |------------------------|-------------------|--------|
+/// | (on_entry)             | {102}                     ||
+/// | Statement 0            | +0                | +1     |
+/// | statement 1            | +2                | +3     |
+/// | `Call` terminator      | +4                | +5     |
+/// | (on unwind)            | {102,0,1,2,3,4,5}         ||
+/// | (on successful return) | +6                        ||
+///
+/// The `102` in the block's entry set is derived from the basic block index and ensures that the
+/// expected state is unique across all basic blocks. Remember, it is generated by
+/// `mock_entry_sets`, not from actually running `MockAnalysis` to fixpoint.
+struct MockAnalysis<'tcx> {
+    body: &'tcx mir::Body<'tcx>,
+}
+
+impl MockAnalysis<'tcx> {
+    const BASIC_BLOCK_OFFSET: usize = 100;
+
+    /// The entry set for each `BasicBlock` is the ID of that block offset by a fixed amount to
+    /// avoid colliding with the statement/terminator effects.
+    fn mock_entry_set(self, bb: BasicBlock) -> BitSet<usize> {
+        let mut ret = BitSet::new_empty(self.bits_per_block(body));
+        ret.insert(Self::BASIC_BLOCK_OFFSET + bb.index());
+        ret
+    }
+
+    fn mock_entry_sets(&self) -> IndexVec<BasicBlock, BitSet<usize>> {
+        let empty = BitSet::new_empty(self.bits_per_block(body));
+        let mut ret = IndexVec::from_elem(empty, &self.body.basic_blocks());
+
+        for (bb, _) in self.body.basic_blocks().iter_enumerated() {
+            ret[bb] = self.mock_entry_set(bb);
+        }
+
+        ret
+    }
+
+    /// Returns the index that should be added to the dataflow state at the given target.
+    ///
+    /// This index is only unique within a given basic block. `SeekAfter` and
+    /// `SeekAfterAssumeCallReturns` have the same effect unless `target` is a `Call` terminator.
+    fn effect_at_target(&self, target: SeekTarget) -> Option<usize> {
+        use SeekTarget::*;
+
+        let idx = match target {
+            BlockStart(_) => return None,
+
+            AfterAssumeCallReturns(loc) if is_call_terminator_non_diverging(self.body, loc) => {
+                loc.statement_index * 2 + 2
+            }
+
+            Before(loc) => loc.statement_index * 2,
+            After(loc) | AfterAssumeCallReturns(loc) => loc.statement_index * 2 + 1,
+        };
+
+        assert!(idx < Self::BASIC_BLOCK_OFFSET, "Too many statements in basic block");
+        Some(idx)
+    }
+
+    /// Returns the expected state at the given `SeekTarget`.
+    ///
+    /// This is the union of index of the target basic block, the index assigned to the
+    /// target statement or terminator, and the indices of all preceding statements in the target
+    /// basic block.
+    ///
+    /// For example, the expected state when calling
+    /// `seek_before(Location { block: 2, statement_index: 2 })` would be `[102, 0, 1, 2, 3, 4]`.
+    fn expected_state_at_target(&self, target: SeekTarget) -> BitSet<usize> {
+        let mut ret = BitSet::new_empty(self.bits_per_block(self.body));
+        ret.insert(Self::BASIC_BLOCK_OFFSET + target.block().index());
+
+        if let Some(target_effect) = self.effect_at_target(target) {
+            for i in 0..=target_effect {
+                ret.insert(i);
+            }
+        }
+
+        ret
+    }
+}
+
+impl BottomValue for MockAnalysis<'tcx> {
+    const BOTTOM_VALUE: bool = false;
+}
+
+impl AnalysisDomain<'tcx> for MockAnalysis<'tcx> {
+    type Idx = usize;
+
+    const NAME: &'static str = "mock";
+
+    fn bits_per_block(&self, body: &mir::Body<'tcx>) -> usize {
+        Self::BASIC_BLOCK_OFFSET + body.basic_blocks().len()
+    }
+
+    fn initialize_start_block(&self, _: &mir::Body<'tcx>, _: &mut BitSet<Self::Idx>) {
+        unimplemented!("This is never called since `MockAnalysis` is never iterated to fixpoint");
+    }
+}
+
+impl Analysis<'tcx> for MockAnalysis<'tcx> {
+    fn apply_statement_effect(
+        &self,
+        state: &mut BitSet<Self::Idx>,
+        _statement: &mir::Statement<'tcx>,
+        location: Location,
+    ) {
+        let idx = SeekTarget::After(location).effect(self.body).unwrap();
+        assert!(state.insert(idx));
+    }
+
+    fn apply_before_statement_effect(
+        &self,
+        state: &mut BitSet<Self::Idx>,
+        _statement: &mir::Statement<'tcx>,
+        location: Location,
+    ) {
+        let idx = SeekTarget::Before(location).effect(self.body).unwrap();
+        assert!(state.insert(idx));
+    }
+
+    fn apply_terminator_effect(
+        &self,
+        state: &mut BitSet<Self::Idx>,
+        _terminator: &mir::Terminator<'tcx>,
+        location: Location,
+    ) {
+        let idx = SeekTarget::After(location).effect(self.body).unwrap();
+        assert!(state.insert(idx));
+    }
+
+    fn apply_before_terminator_effect(
+        &self,
+        state: &mut BitSet<Self::Idx>,
+        _terminator: &mir::Terminator<'tcx>,
+        location: Location,
+    ) {
+        let idx = SeekTarget::Before(location).effect(self.body).unwrap();
+        assert!(state.insert(idx));
+    }
+
+    fn apply_call_return_effect(
+        &self,
+        state: &mut BitSet<Self::Idx>,
+        block: BasicBlock,
+        _func: &mir::Operand<'tcx>,
+        _args: &[mir::Operand<'tcx>],
+        _return_place: &mir::Place<'tcx>,
+    ) {
+        let location = self.body.terminator_loc(block);
+        let idx = SeekTarget::AfterAssumeCallReturns(location).effect(self.body).unwrap();
+        assert!(state.insert(idx));
+    }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum SeekTarget {
+    BlockStart(BasicBlock),
+    Before(Location),
+    After(Location),
+    AfterAssumeCallReturns(Location),
+}
+
+impl SeekTarget {
+    fn block(&self) -> BasicBlock {
+        use SeekTarget::*;
+
+        match *self {
+            BlockStart(block) => block,
+            Before(loc) | After(loc) | AfterAssumeCallReturns(loc) => loc.block,
+        }
+    }
+
+    /// An iterator over all possible `SeekTarget`s in a given block in order, starting with
+    /// `BlockStart`.
+    ///
+    /// This includes both `After` and `AfterAssumeCallReturns` for every `Location`.
+    fn iter_in_block(body: &mir::Body<'_>, block: BasicBlock) -> impl Iterator<Item = Self> {
+        let statements_and_terminator = (0..=body[block].statements.len())
+            .flat_map(|i| (0..3).map(move |j| (i, j)))
+            .map(move |(i, kind)| {
+                let loc = Location { block, statement_index: i };
+                match kind {
+                    0 => SeekTarget::Before(loc),
+                    1 => SeekTarget::After(loc),
+                    2 => SeekTarget::AfterAssumeCallReturns(loc),
+                    _ => unreachable!(),
+                }
+            });
+
+        std::iter::once(SeekTarget::BlockStart(block)).chain(statements_and_terminator)
+    }
+}
+
+#[test]
+fn cursor_seek() {
+    let body = mock_body();
+    let body = &body;
+    let analysis = MockAnalysis { body };
+
+    let mut cursor = Results { entry_sets: analysis.mock_entry_sets(), analysis }.into_cursor(body);
+
+    // Sanity check: the mock call return effect is unique and actually being applied.
+    let call_terminator_loc = Location { block: BasicBlock::from_usize(2), statement_index: 2 };
+    assert!(is_call_terminator_non_diverging(body, call_terminator_loc));
+
+    let call_return_effect = cursor
+        .analysis()
+        .effect_at_target(SeekTarget::AfterAssumeCallReturns(call_terminator_loc))
+        .unwrap();
+    assert_ne!(call_return_effect, SeekTarget::After(call_terminator_loc).effect(body).unwrap());
+
+    cursor.seek_after(call_terminator_loc);
+    assert!(!cursor.get().contains(call_return_effect));
+    cursor.seek_after_assume_call_returns(call_terminator_loc);
+    assert!(cursor.get().contains(call_return_effect));
+
+    let every_target = || {
+        body.basic_blocks()
+            .iter_enumerated()
+            .flat_map(|(bb, _)| SeekTarget::iter_in_block(body, bb))
+    };
+
+    let mut seek_to_target = |targ| {
+        use SeekTarget::*;
+
+        match targ {
+            BlockStart(block) => cursor.seek_to_block_start(block),
+            Before(loc) => cursor.seek_before(loc),
+            After(loc) => cursor.seek_after(loc),
+            AfterAssumeCallReturns(loc) => cursor.seek_after_assume_call_returns(loc),
+        }
+
+        assert_eq!(cursor.get(), &cursor.analysis().expected_state_at_target(targ));
+    };
+
+    // Seek *to* every possible `SeekTarget` *from* every possible `SeekTarget`.
+    //
+    // By resetting the cursor to `from` each time it changes, we end up checking some edges twice.
+    // What we really want is an Eulerian cycle for the complete digraph over all possible
+    // `SeekTarget`s, but it's not worth spending the time to compute it.
+    for from in every_target() {
+        seek_to_target(from);
+
+        for to in every_target() {
+            seek_to_target(to);
+            seek_to_target(from);
+        }
+    }
+}