Skip to content

Commit e00bd27

Browse files
authored
Rollup merge of rust-lang#64622 - ecstatic-morse:cycle-detector, r=oli-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
2 parents 4a58b14 + c9e4816 commit e00bd27

File tree

4 files changed

+230
-1
lines changed

4 files changed

+230
-1
lines changed

src/librustc/mir/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,12 @@ impl<'tcx> Body<'tcx> {
262262
dominators(self)
263263
}
264264

265+
/// Returns `true` if a cycle exists in the control-flow graph that is reachable from the
266+
/// `START_BLOCK`.
267+
pub fn is_cfg_cyclic(&self) -> bool {
268+
graph::is_cyclic(self)
269+
}
270+
265271
#[inline]
266272
pub fn local_kind(&self, local: Local) -> LocalKind {
267273
let index = local.as_usize();

src/librustc_data_structures/graph/iterate/mod.rs

Lines changed: 203 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::super::indexed_vec::IndexVec;
2-
use super::{DirectedGraph, WithNumNodes, WithSuccessors};
2+
use super::{DirectedGraph, WithNumNodes, WithSuccessors, WithStartNode};
33
use crate::bit_set::BitSet;
44

55
#[cfg(test)]
@@ -85,3 +85,205 @@ where
8585
Some(n)
8686
}
8787
}
88+
89+
/// Allows searches to terminate early with a value.
90+
#[derive(Clone, Copy, Debug)]
91+
pub enum ControlFlow<T> {
92+
Break(T),
93+
Continue,
94+
}
95+
96+
/// The status of a node in the depth-first search.
97+
///
98+
/// See the documentation of `TriColorDepthFirstSearch` to see how a node's status is updated
99+
/// during DFS.
100+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101+
pub enum NodeStatus {
102+
/// This node has been examined by the depth-first search but is not yet `Settled`.
103+
///
104+
/// Also referred to as "gray" or "discovered" nodes in [CLR][].
105+
///
106+
/// [CLR]: https://en.wikipedia.org/wiki/Introduction_to_Algorithms
107+
Visited,
108+
109+
/// This node and all nodes reachable from it have been examined by the depth-first search.
110+
///
111+
/// Also referred to as "black" or "finished" nodes in [CLR][].
112+
///
113+
/// [CLR]: https://en.wikipedia.org/wiki/Introduction_to_Algorithms
114+
Settled,
115+
}
116+
117+
struct Event<N> {
118+
node: N,
119+
becomes: NodeStatus,
120+
}
121+
122+
/// A depth-first search that also tracks when all successors of a node have been examined.
123+
///
124+
/// This is based on the DFS described in [Introduction to Algorithms (1st ed.)][CLR], hereby
125+
/// referred to as **CLR**. However, we use the terminology in [`NodeStatus`][] above instead of
126+
/// "discovered"/"finished" or "white"/"grey"/"black". Each node begins the search with no status,
127+
/// becomes `Visited` when it is first examined by the DFS and is `Settled` when all nodes
128+
/// reachable from it have been examined. This allows us to differentiate between "tree", "back"
129+
/// and "forward" edges (see [`TriColorVisitor::node_examined`]).
130+
///
131+
/// Unlike the pseudocode in [CLR][], this implementation is iterative and does not use timestamps.
132+
/// We accomplish this by storing `Event`s on the stack that result in a (possible) state change
133+
/// for each node. A `Visited` event signifies that we should examine this node if it has not yet
134+
/// been `Visited` or `Settled`. When a node is examined for the first time, we mark it as
135+
/// `Visited` and push a `Settled` event for it on stack followed by `Visited` events for all of
136+
/// its predecessors, scheduling them for examination. Multiple `Visited` events for a single node
137+
/// may exist on the stack simultaneously if a node has multiple predecessors, but only one
138+
/// `Settled` event will ever be created for each node. After all `Visited` events for a node's
139+
/// successors have been popped off the stack (as well as any new events triggered by visiting
140+
/// those successors), we will pop off that node's `Settled` event.
141+
///
142+
/// [CLR]: https://en.wikipedia.org/wiki/Introduction_to_Algorithms
143+
/// [`NodeStatus`]: ./enum.NodeStatus.html
144+
/// [`TriColorVisitor::node_examined`]: ./trait.TriColorVisitor.html#method.node_examined
145+
pub struct TriColorDepthFirstSearch<'graph, G>
146+
where
147+
G: ?Sized + DirectedGraph + WithNumNodes + WithSuccessors,
148+
{
149+
graph: &'graph G,
150+
stack: Vec<Event<G::Node>>,
151+
visited: BitSet<G::Node>,
152+
settled: BitSet<G::Node>,
153+
}
154+
155+
impl<G> TriColorDepthFirstSearch<'graph, G>
156+
where
157+
G: ?Sized + DirectedGraph + WithNumNodes + WithSuccessors,
158+
{
159+
pub fn new(graph: &'graph G) -> Self {
160+
TriColorDepthFirstSearch {
161+
graph,
162+
stack: vec![],
163+
visited: BitSet::new_empty(graph.num_nodes()),
164+
settled: BitSet::new_empty(graph.num_nodes()),
165+
}
166+
}
167+
168+
/// Performs a depth-first search, starting from the given `root`.
169+
///
170+
/// This won't visit nodes that are not reachable from `root`.
171+
pub fn run_from<V>(mut self, root: G::Node, visitor: &mut V) -> Option<V::BreakVal>
172+
where
173+
V: TriColorVisitor<G>,
174+
{
175+
use NodeStatus::{Visited, Settled};
176+
177+
self.stack.push(Event { node: root, becomes: Visited });
178+
179+
loop {
180+
match self.stack.pop()? {
181+
Event { node, becomes: Settled } => {
182+
let not_previously_settled = self.settled.insert(node);
183+
assert!(not_previously_settled, "A node should be settled exactly once");
184+
if let ControlFlow::Break(val) = visitor.node_settled(node) {
185+
return Some(val);
186+
}
187+
}
188+
189+
Event { node, becomes: Visited } => {
190+
let not_previously_visited = self.visited.insert(node);
191+
let prior_status = if not_previously_visited {
192+
None
193+
} else if self.settled.contains(node) {
194+
Some(Settled)
195+
} else {
196+
Some(Visited)
197+
};
198+
199+
if let ControlFlow::Break(val) = visitor.node_examined(node, prior_status) {
200+
return Some(val);
201+
}
202+
203+
// If this node has already been examined, we are done.
204+
if prior_status.is_some() {
205+
continue;
206+
}
207+
208+
// Otherwise, push a `Settled` event for this node onto the stack, then
209+
// schedule its successors for examination.
210+
self.stack.push(Event { node, becomes: Settled });
211+
for succ in self.graph.successors(node) {
212+
self.stack.push(Event { node: succ, becomes: Visited });
213+
}
214+
}
215+
}
216+
}
217+
}
218+
}
219+
220+
impl<G> TriColorDepthFirstSearch<'graph, G>
221+
where
222+
G: ?Sized + DirectedGraph + WithNumNodes + WithSuccessors + WithStartNode,
223+
{
224+
/// Performs a depth-first search, starting from `G::start_node()`.
225+
///
226+
/// This won't visit nodes that are not reachable from the start node.
227+
pub fn run_from_start<V>(self, visitor: &mut V) -> Option<V::BreakVal>
228+
where
229+
V: TriColorVisitor<G>,
230+
{
231+
let root = self.graph.start_node();
232+
self.run_from(root, visitor)
233+
}
234+
}
235+
236+
/// What to do when a node is examined or becomes `Settled` during DFS.
237+
pub trait TriColorVisitor<G>
238+
where
239+
G: ?Sized + DirectedGraph,
240+
{
241+
/// The value returned by this search.
242+
type BreakVal;
243+
244+
/// Called when a node is examined by the depth-first search.
245+
///
246+
/// By checking the value of `prior_status`, this visitor can determine whether the edge
247+
/// leading to this node was a tree edge (`None`), forward edge (`Some(Settled)`) or back edge
248+
/// (`Some(Visited)`). For a full explanation of each edge type, see the "Depth-first Search"
249+
/// chapter in [CLR][] or [wikipedia][].
250+
///
251+
/// If you want to know *both* nodes linked by each edge, you'll need to modify
252+
/// `TriColorDepthFirstSearch` to store a `source` node for each `Visited` event.
253+
///
254+
/// [wikipedia]: https://en.wikipedia.org/wiki/Depth-first_search#Output_of_a_depth-first_search
255+
/// [CLR]: https://en.wikipedia.org/wiki/Introduction_to_Algorithms
256+
fn node_examined(
257+
&mut self,
258+
_target: G::Node,
259+
_prior_status: Option<NodeStatus>,
260+
) -> ControlFlow<Self::BreakVal> {
261+
ControlFlow::Continue
262+
}
263+
264+
/// Called after all nodes reachable from this one have been examined.
265+
fn node_settled(&mut self, _target: G::Node) -> ControlFlow<Self::BreakVal> {
266+
ControlFlow::Continue
267+
}
268+
}
269+
270+
/// This `TriColorVisitor` looks for back edges in a graph, which indicate that a cycle exists.
271+
pub struct CycleDetector;
272+
273+
impl<G> TriColorVisitor<G> for CycleDetector
274+
where
275+
G: ?Sized + DirectedGraph,
276+
{
277+
type BreakVal = ();
278+
279+
fn node_examined(
280+
&mut self,
281+
_node: G::Node,
282+
prior_status: Option<NodeStatus>,
283+
) -> ControlFlow<Self::BreakVal> {
284+
match prior_status {
285+
Some(NodeStatus::Visited) => ControlFlow::Break(()),
286+
_ => ControlFlow::Continue,
287+
}
288+
}
289+
}

src/librustc_data_structures/graph/iterate/tests.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,14 @@ fn diamond_post_order() {
99
let result = post_order_from(&graph, 0);
1010
assert_eq!(result, vec![3, 1, 2, 0]);
1111
}
12+
13+
#[test]
14+
fn is_cyclic() {
15+
use super::super::is_cyclic;
16+
17+
let diamond_acyclic = TestGraph::new(0, &[(0, 1), (0, 2), (1, 3), (2, 3)]);
18+
let diamond_cyclic = TestGraph::new(0, &[(0, 1), (1, 2), (2, 3), (3, 0)]);
19+
20+
assert!(!is_cyclic(&diamond_acyclic));
21+
assert!(is_cyclic(&diamond_cyclic));
22+
}

src/librustc_data_structures/graph/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,13 @@ where
8181
+ WithNumNodes,
8282
{
8383
}
84+
85+
/// Returns `true` if the graph has a cycle that is reachable from the start node.
86+
pub fn is_cyclic<G>(graph: &G) -> bool
87+
where
88+
G: ?Sized + DirectedGraph + WithStartNode + WithSuccessors + WithNumNodes,
89+
{
90+
iterate::TriColorDepthFirstSearch::new(graph)
91+
.run_from_start(&mut iterate::CycleDetector)
92+
.is_some()
93+
}

0 commit comments

Comments
 (0)