Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a cycle detector for generic Graphs and mir::Bodys #64622

Merged
merged 2 commits into from
Sep 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
}