Skip to content

Commit

Permalink
Rollup merge of rust-lang#64622 - ecstatic-morse:cycle-detector, r=ol…
Browse files Browse the repository at this point in the history
…i-obk

Add a cycle detector for generic `Graph`s and `mir::Body`s

Cycle detection is one way to differentiate the upcoming `const_loop` feature flag (rust-lang#52000) from the `const_if_match` one (rust-lang#49146). It would be possible to use the existing implementation of strongly-connected components for this but less efficient.

The ["tri-color" terminology](http://www.cs.cornell.edu/courses/cs2112/2012sp/lectures/lec24/lec24-12sp.html) is common in introductory data structures and algorithms courses: black nodes are settled, grey nodes are visited, and white nodes have no state. This particular implementation is iterative and uses a well-known technique where "node settled" events are kept on the stack alongside nodes to visit. When a settled event is popped, we know that all successors of that node have been visited and themselves settled. If we encounter a successor node that has been visited (is on the stack) but not yet settled, we have found a cycle.

r? @eddyb
  • Loading branch information
Centril authored Sep 24, 2019
2 parents f7d5c13 + c9e4816 commit 07cf9ff
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 1 deletion.
6 changes: 6 additions & 0 deletions src/librustc/mir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,12 @@ impl<'tcx> Body<'tcx> {
dominators(self)
}

/// Returns `true` if a cycle exists in the control-flow graph that is reachable from the
/// `START_BLOCK`.
pub fn is_cfg_cyclic(&self) -> bool {
graph::is_cyclic(self)
}

#[inline]
pub fn local_kind(&self, local: Local) -> LocalKind {
let index = local.as_usize();
Expand Down
204 changes: 203 additions & 1 deletion src/librustc_data_structures/graph/iterate/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::super::indexed_vec::IndexVec;
use super::{DirectedGraph, WithNumNodes, WithSuccessors};
use super::{DirectedGraph, WithNumNodes, WithSuccessors, WithStartNode};
use crate::bit_set::BitSet;

#[cfg(test)]
Expand Down Expand Up @@ -85,3 +85,205 @@ where
Some(n)
}
}

/// Allows searches to terminate early with a value.
#[derive(Clone, Copy, Debug)]
pub enum ControlFlow<T> {
Break(T),
Continue,
}

/// The status of a node in the depth-first search.
///
/// See the documentation of `TriColorDepthFirstSearch` to see how a node's status is updated
/// during DFS.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NodeStatus {
/// This node has been examined by the depth-first search but is not yet `Settled`.
///
/// Also referred to as "gray" or "discovered" nodes in [CLR][].
///
/// [CLR]: https://en.wikipedia.org/wiki/Introduction_to_Algorithms
Visited,

/// This node and all nodes reachable from it have been examined by the depth-first search.
///
/// Also referred to as "black" or "finished" nodes in [CLR][].
///
/// [CLR]: https://en.wikipedia.org/wiki/Introduction_to_Algorithms
Settled,
}

struct Event<N> {
node: N,
becomes: NodeStatus,
}

/// A depth-first search that also tracks when all successors of a node have been examined.
///
/// This is based on the DFS described in [Introduction to Algorithms (1st ed.)][CLR], hereby
/// referred to as **CLR**. However, we use the terminology in [`NodeStatus`][] above instead of
/// "discovered"/"finished" or "white"/"grey"/"black". Each node begins the search with no status,
/// becomes `Visited` when it is first examined by the DFS and is `Settled` when all nodes
/// reachable from it have been examined. This allows us to differentiate between "tree", "back"
/// and "forward" edges (see [`TriColorVisitor::node_examined`]).
///
/// Unlike the pseudocode in [CLR][], this implementation is iterative and does not use timestamps.
/// We accomplish this by storing `Event`s on the stack that result in a (possible) state change
/// for each node. A `Visited` event signifies that we should examine this node if it has not yet
/// been `Visited` or `Settled`. When a node is examined for the first time, we mark it as
/// `Visited` and push a `Settled` event for it on stack followed by `Visited` events for all of
/// its predecessors, scheduling them for examination. Multiple `Visited` events for a single node
/// may exist on the stack simultaneously if a node has multiple predecessors, but only one
/// `Settled` event will ever be created for each node. After all `Visited` events for a node's
/// successors have been popped off the stack (as well as any new events triggered by visiting
/// those successors), we will pop off that node's `Settled` event.
///
/// [CLR]: https://en.wikipedia.org/wiki/Introduction_to_Algorithms
/// [`NodeStatus`]: ./enum.NodeStatus.html
/// [`TriColorVisitor::node_examined`]: ./trait.TriColorVisitor.html#method.node_examined
pub struct TriColorDepthFirstSearch<'graph, G>
where
G: ?Sized + DirectedGraph + WithNumNodes + WithSuccessors,
{
graph: &'graph G,
stack: Vec<Event<G::Node>>,
visited: BitSet<G::Node>,
settled: BitSet<G::Node>,
}

impl<G> TriColorDepthFirstSearch<'graph, G>
where
G: ?Sized + DirectedGraph + WithNumNodes + WithSuccessors,
{
pub fn new(graph: &'graph G) -> Self {
TriColorDepthFirstSearch {
graph,
stack: vec![],
visited: BitSet::new_empty(graph.num_nodes()),
settled: BitSet::new_empty(graph.num_nodes()),
}
}

/// Performs a depth-first search, starting from the given `root`.
///
/// This won't visit nodes that are not reachable from `root`.
pub fn run_from<V>(mut self, root: G::Node, visitor: &mut V) -> Option<V::BreakVal>
where
V: TriColorVisitor<G>,
{
use NodeStatus::{Visited, Settled};

self.stack.push(Event { node: root, becomes: Visited });

loop {
match self.stack.pop()? {
Event { node, becomes: Settled } => {
let not_previously_settled = self.settled.insert(node);
assert!(not_previously_settled, "A node should be settled exactly once");
if let ControlFlow::Break(val) = visitor.node_settled(node) {
return Some(val);
}
}

Event { node, becomes: Visited } => {
let not_previously_visited = self.visited.insert(node);
let prior_status = if not_previously_visited {
None
} else if self.settled.contains(node) {
Some(Settled)
} else {
Some(Visited)
};

if let ControlFlow::Break(val) = visitor.node_examined(node, prior_status) {
return Some(val);
}

// If this node has already been examined, we are done.
if prior_status.is_some() {
continue;
}

// Otherwise, push a `Settled` event for this node onto the stack, then
// schedule its successors for examination.
self.stack.push(Event { node, becomes: Settled });
for succ in self.graph.successors(node) {
self.stack.push(Event { node: succ, becomes: Visited });
}
}
}
}
}
}

impl<G> TriColorDepthFirstSearch<'graph, G>
where
G: ?Sized + DirectedGraph + WithNumNodes + WithSuccessors + WithStartNode,
{
/// Performs a depth-first search, starting from `G::start_node()`.
///
/// This won't visit nodes that are not reachable from the start node.
pub fn run_from_start<V>(self, visitor: &mut V) -> Option<V::BreakVal>
where
V: TriColorVisitor<G>,
{
let root = self.graph.start_node();
self.run_from(root, visitor)
}
}

/// What to do when a node is examined or becomes `Settled` during DFS.
pub trait TriColorVisitor<G>
where
G: ?Sized + DirectedGraph,
{
/// The value returned by this search.
type BreakVal;

/// Called when a node is examined by the depth-first search.
///
/// By checking the value of `prior_status`, this visitor can determine whether the edge
/// leading to this node was a tree edge (`None`), forward edge (`Some(Settled)`) or back edge
/// (`Some(Visited)`). For a full explanation of each edge type, see the "Depth-first Search"
/// chapter in [CLR][] or [wikipedia][].
///
/// If you want to know *both* nodes linked by each edge, you'll need to modify
/// `TriColorDepthFirstSearch` to store a `source` node for each `Visited` event.
///
/// [wikipedia]: https://en.wikipedia.org/wiki/Depth-first_search#Output_of_a_depth-first_search
/// [CLR]: https://en.wikipedia.org/wiki/Introduction_to_Algorithms
fn node_examined(
&mut self,
_target: G::Node,
_prior_status: Option<NodeStatus>,
) -> ControlFlow<Self::BreakVal> {
ControlFlow::Continue
}

/// Called after all nodes reachable from this one have been examined.
fn node_settled(&mut self, _target: G::Node) -> ControlFlow<Self::BreakVal> {
ControlFlow::Continue
}
}

/// This `TriColorVisitor` looks for back edges in a graph, which indicate that a cycle exists.
pub struct CycleDetector;

impl<G> TriColorVisitor<G> for CycleDetector
where
G: ?Sized + DirectedGraph,
{
type BreakVal = ();

fn node_examined(
&mut self,
_node: G::Node,
prior_status: Option<NodeStatus>,
) -> ControlFlow<Self::BreakVal> {
match prior_status {
Some(NodeStatus::Visited) => ControlFlow::Break(()),
_ => ControlFlow::Continue,
}
}
}
11 changes: 11 additions & 0 deletions src/librustc_data_structures/graph/iterate/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,14 @@ fn diamond_post_order() {
let result = post_order_from(&graph, 0);
assert_eq!(result, vec![3, 1, 2, 0]);
}

#[test]
fn is_cyclic() {
use super::super::is_cyclic;

let diamond_acyclic = TestGraph::new(0, &[(0, 1), (0, 2), (1, 3), (2, 3)]);
let diamond_cyclic = TestGraph::new(0, &[(0, 1), (1, 2), (2, 3), (3, 0)]);

assert!(!is_cyclic(&diamond_acyclic));
assert!(is_cyclic(&diamond_cyclic));
}
10 changes: 10 additions & 0 deletions src/librustc_data_structures/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,13 @@ where
+ WithNumNodes,
{
}

/// Returns `true` if the graph has a cycle that is reachable from the start node.
pub fn is_cyclic<G>(graph: &G) -> bool
where
G: ?Sized + DirectedGraph + WithStartNode + WithSuccessors + WithNumNodes,
{
iterate::TriColorDepthFirstSearch::new(graph)
.run_from_start(&mut iterate::CycleDetector)
.is_some()
}

0 comments on commit 07cf9ff

Please sign in to comment.