Remove recursion from sccc walking
This allows constructing the sccc for large that visit many nodes before finding a single cycle of sccc, for example lists. When used to find dependencies in borrow checking the list case is what occurs in very long functions.
This commit is contained in:
parent
355904dca0
commit
eb597f5c4e
1 changed files with 179 additions and 70 deletions
|
@ -231,20 +231,30 @@ where
|
||||||
|
|
||||||
let scc_indices = (0..num_nodes)
|
let scc_indices = (0..num_nodes)
|
||||||
.map(G::Node::new)
|
.map(G::Node::new)
|
||||||
.map(|node| match this.walk_node(0, node) {
|
.map(|node| match this.start_walk_from(node) {
|
||||||
WalkReturn::Complete { scc_index } => scc_index,
|
WalkReturn::Complete { scc_index } => scc_index,
|
||||||
WalkReturn::Cycle { min_depth } => {
|
WalkReturn::Cycle { min_depth } => panic!(
|
||||||
panic!("`walk_node(0, {:?})` returned cycle with depth {:?}", node, min_depth)
|
"`start_walk_node({:?})` returned cycle with depth {:?}",
|
||||||
}
|
node, min_depth
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Sccs { scc_indices, scc_data: this.scc_data }
|
Sccs { scc_indices, scc_data: this.scc_data }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Visits a node during the DFS. We first examine its current
|
fn start_walk_from(&mut self, node: G::Node) -> WalkReturn<S> {
|
||||||
/// state -- if it is not yet visited (`NotVisited`), we can push
|
if let Some(result) = self.inspect_node(node) {
|
||||||
/// it onto the stack and start walking its successors.
|
result
|
||||||
|
} else {
|
||||||
|
self.walk_unvisited_node(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inspect a node during the DFS. We first examine its current
|
||||||
|
/// state -- if it is not yet visited (`NotVisited`), return `None` so
|
||||||
|
/// that the caller might push it onto the stack and start walking its
|
||||||
|
/// successors.
|
||||||
///
|
///
|
||||||
/// If it is already on the DFS stack it will be in the state
|
/// If it is already on the DFS stack it will be in the state
|
||||||
/// `BeingVisited`. In that case, we have found a cycle and we
|
/// `BeingVisited`. In that case, we have found a cycle and we
|
||||||
|
@ -253,20 +263,19 @@ where
|
||||||
/// Otherwise, we are looking at a node that has already been
|
/// Otherwise, we are looking at a node that has already been
|
||||||
/// completely visited. We therefore return `WalkReturn::Complete`
|
/// completely visited. We therefore return `WalkReturn::Complete`
|
||||||
/// with its associated SCC index.
|
/// with its associated SCC index.
|
||||||
fn walk_node(&mut self, depth: usize, node: G::Node) -> WalkReturn<S> {
|
fn inspect_node(&mut self, node: G::Node) -> Option<WalkReturn<S>> {
|
||||||
debug!("walk_node(depth = {:?}, node = {:?})", depth, node);
|
Some(match self.find_state(node) {
|
||||||
match self.find_state(node) {
|
|
||||||
NodeState::InCycle { scc_index } => WalkReturn::Complete { scc_index },
|
NodeState::InCycle { scc_index } => WalkReturn::Complete { scc_index },
|
||||||
|
|
||||||
NodeState::BeingVisited { depth: min_depth } => WalkReturn::Cycle { min_depth },
|
NodeState::BeingVisited { depth: min_depth } => WalkReturn::Cycle { min_depth },
|
||||||
|
|
||||||
NodeState::NotVisited => self.walk_unvisited_node(depth, node),
|
NodeState::NotVisited => return None,
|
||||||
|
|
||||||
NodeState::InCycleWith { parent } => panic!(
|
NodeState::InCycleWith { parent } => panic!(
|
||||||
"`find_state` returned `InCycleWith({:?})`, which ought to be impossible",
|
"`find_state` returned `InCycleWith({:?})`, which ought to be impossible",
|
||||||
parent
|
parent
|
||||||
),
|
),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches the state of the node `r`. If `r` is recorded as being
|
/// Fetches the state of the node `r`. If `r` is recorded as being
|
||||||
|
@ -392,74 +401,174 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Walks a node that has never been visited before.
|
/// Walks a node that has never been visited before.
|
||||||
fn walk_unvisited_node(&mut self, depth: usize, node: G::Node) -> WalkReturn<S> {
|
///
|
||||||
debug!("walk_unvisited_node(depth = {:?}, node = {:?})", depth, node);
|
/// Call this method when `inspect_node` has returned `None`. Having the
|
||||||
|
/// caller decide avoids mutual recursion between the two methods and allows
|
||||||
|
/// us to maintain an allocated stack for nodes on the path between calls.
|
||||||
|
fn walk_unvisited_node(&mut self, initial: G::Node) -> WalkReturn<S> {
|
||||||
|
struct VisitingNodeFrame<G: DirectedGraph, Successors> {
|
||||||
|
node: G::Node,
|
||||||
|
iter: Option<Successors>,
|
||||||
|
depth: usize,
|
||||||
|
min_depth: usize,
|
||||||
|
successors_len: usize,
|
||||||
|
min_cycle_root: G::Node,
|
||||||
|
successor_node: G::Node,
|
||||||
|
}
|
||||||
|
|
||||||
debug_assert!(matches!(self.node_states[node], NodeState::NotVisited));
|
// Move the stack to a local variable. We want to utilize the existing allocation and
|
||||||
|
// mutably borrow it without borrowing self at the same time.
|
||||||
|
let mut successors_stack = core::mem::take(&mut self.successors_stack);
|
||||||
|
debug_assert_eq!(successors_stack.len(), 0);
|
||||||
|
|
||||||
// Push `node` onto the stack.
|
let mut stack: Vec<VisitingNodeFrame<G, _>> = vec![VisitingNodeFrame {
|
||||||
self.node_states[node] = NodeState::BeingVisited { depth };
|
node: initial,
|
||||||
self.node_stack.push(node);
|
depth: 0,
|
||||||
|
min_depth: 0,
|
||||||
|
iter: None,
|
||||||
|
successors_len: 0,
|
||||||
|
min_cycle_root: initial,
|
||||||
|
successor_node: initial,
|
||||||
|
}];
|
||||||
|
|
||||||
// Walk each successor of the node, looking to see if any of
|
let mut return_value = None;
|
||||||
// them can reach a node that is presently on the stack. If
|
|
||||||
// so, that means they can also reach us.
|
'recurse: while let Some(frame) = stack.last_mut() {
|
||||||
let mut min_depth = depth;
|
let VisitingNodeFrame {
|
||||||
let mut min_cycle_root = node;
|
node,
|
||||||
let successors_len = self.successors_stack.len();
|
depth,
|
||||||
for successor_node in self.graph.successors(node) {
|
iter,
|
||||||
debug!("walk_unvisited_node: node = {:?} successor_ode = {:?}", node, successor_node);
|
successors_len,
|
||||||
match self.walk_node(depth + 1, successor_node) {
|
min_depth,
|
||||||
WalkReturn::Cycle { min_depth: successor_min_depth } => {
|
min_cycle_root,
|
||||||
// Track the minimum depth we can reach.
|
successor_node,
|
||||||
assert!(successor_min_depth <= depth);
|
} = frame;
|
||||||
if successor_min_depth < min_depth {
|
|
||||||
|
let node = *node;
|
||||||
|
let depth = *depth;
|
||||||
|
|
||||||
|
let successors = match iter {
|
||||||
|
Some(iter) => iter,
|
||||||
|
None => {
|
||||||
|
// This None marks that we still have the initialize this node's frame.
|
||||||
|
debug!("walk_unvisited_node(depth = {:?}, node = {:?})", depth, node);
|
||||||
|
|
||||||
|
debug_assert!(matches!(self.node_states[node], NodeState::NotVisited));
|
||||||
|
|
||||||
|
// Push `node` onto the stack.
|
||||||
|
self.node_states[node] = NodeState::BeingVisited { depth };
|
||||||
|
self.node_stack.push(node);
|
||||||
|
|
||||||
|
// Walk each successor of the node, looking to see if any of
|
||||||
|
// them can reach a node that is presently on the stack. If
|
||||||
|
// so, that means they can also reach us.
|
||||||
|
*successors_len = successors_stack.len();
|
||||||
|
// Set and return a reference, this is currently empty.
|
||||||
|
iter.get_or_insert(self.graph.successors(node))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now that iter is initialized, this is a constant for this frame.
|
||||||
|
let successors_len = *successors_len;
|
||||||
|
|
||||||
|
// Construct iterators for the nodes and walk results. There are two cases:
|
||||||
|
// * The walk of a successor node returned.
|
||||||
|
// * The remaining successor nodes.
|
||||||
|
let returned_walk =
|
||||||
|
return_value.take().into_iter().map(|walk| (*successor_node, Some(walk)));
|
||||||
|
|
||||||
|
let successor_walk = successors.by_ref().map(|successor_node| {
|
||||||
|
debug!(
|
||||||
|
"walk_unvisited_node: node = {:?} successor_ode = {:?}",
|
||||||
|
node, successor_node
|
||||||
|
);
|
||||||
|
(successor_node, self.inspect_node(successor_node))
|
||||||
|
});
|
||||||
|
|
||||||
|
for (successor_node, walk) in returned_walk.chain(successor_walk) {
|
||||||
|
match walk {
|
||||||
|
Some(WalkReturn::Cycle { min_depth: successor_min_depth }) => {
|
||||||
|
// Track the minimum depth we can reach.
|
||||||
|
assert!(successor_min_depth <= depth);
|
||||||
|
if successor_min_depth < *min_depth {
|
||||||
|
debug!(
|
||||||
|
"walk_unvisited_node: node = {:?} successor_min_depth = {:?}",
|
||||||
|
node, successor_min_depth
|
||||||
|
);
|
||||||
|
*min_depth = successor_min_depth;
|
||||||
|
*min_cycle_root = successor_node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(WalkReturn::Complete { scc_index: successor_scc_index }) => {
|
||||||
|
// Push the completed SCC indices onto
|
||||||
|
// the `successors_stack` for later.
|
||||||
debug!(
|
debug!(
|
||||||
"walk_unvisited_node: node = {:?} successor_min_depth = {:?}",
|
"walk_unvisited_node: node = {:?} successor_scc_index = {:?}",
|
||||||
node, successor_min_depth
|
node, successor_scc_index
|
||||||
);
|
);
|
||||||
min_depth = successor_min_depth;
|
successors_stack.push(successor_scc_index);
|
||||||
min_cycle_root = successor_node;
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
let depth = depth + 1;
|
||||||
|
debug!("walk_node(depth = {:?}, node = {:?})", depth, successor_node);
|
||||||
|
// Remember which node the return value will come from.
|
||||||
|
frame.successor_node = successor_node;
|
||||||
|
// Start a new stack frame the step into it.
|
||||||
|
stack.push(VisitingNodeFrame {
|
||||||
|
node: successor_node,
|
||||||
|
depth,
|
||||||
|
iter: None,
|
||||||
|
successors_len: 0,
|
||||||
|
min_depth: depth,
|
||||||
|
min_cycle_root: successor_node,
|
||||||
|
successor_node: successor_node,
|
||||||
|
});
|
||||||
|
continue 'recurse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WalkReturn::Complete { scc_index: successor_scc_index } => {
|
|
||||||
// Push the completed SCC indices onto
|
|
||||||
// the `successors_stack` for later.
|
|
||||||
debug!(
|
|
||||||
"walk_unvisited_node: node = {:?} successor_scc_index = {:?}",
|
|
||||||
node, successor_scc_index
|
|
||||||
);
|
|
||||||
self.successors_stack.push(successor_scc_index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Completed walk, remove `node` from the stack.
|
||||||
|
let r = self.node_stack.pop();
|
||||||
|
debug_assert_eq!(r, Some(node));
|
||||||
|
|
||||||
|
// Remove the frame, it's done.
|
||||||
|
let frame = stack.pop().unwrap();
|
||||||
|
|
||||||
|
// If `min_depth == depth`, then we are the root of the
|
||||||
|
// cycle: we can't reach anyone further down the stack.
|
||||||
|
|
||||||
|
// Pass the 'return value' down the stack.
|
||||||
|
// We return one frame at a time so there can't be another return value.
|
||||||
|
debug_assert!(return_value.is_none());
|
||||||
|
return_value = Some(if frame.min_depth == depth {
|
||||||
|
// Note that successor stack may have duplicates, so we
|
||||||
|
// want to remove those:
|
||||||
|
let deduplicated_successors = {
|
||||||
|
let duplicate_set = &mut self.duplicate_set;
|
||||||
|
duplicate_set.clear();
|
||||||
|
successors_stack
|
||||||
|
.drain(successors_len..)
|
||||||
|
.filter(move |&i| duplicate_set.insert(i))
|
||||||
|
};
|
||||||
|
let scc_index = self.scc_data.create_scc(deduplicated_successors);
|
||||||
|
self.node_states[node] = NodeState::InCycle { scc_index };
|
||||||
|
WalkReturn::Complete { scc_index }
|
||||||
|
} else {
|
||||||
|
// We are not the head of the cycle. Return back to our
|
||||||
|
// caller. They will take ownership of the
|
||||||
|
// `self.successors` data that we pushed.
|
||||||
|
self.node_states[node] = NodeState::InCycleWith { parent: frame.min_cycle_root };
|
||||||
|
WalkReturn::Cycle { min_depth: frame.min_depth }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completed walk, remove `node` from the stack.
|
// Keep the allocation we used for successors_stack.
|
||||||
let r = self.node_stack.pop();
|
self.successors_stack = successors_stack;
|
||||||
debug_assert_eq!(r, Some(node));
|
debug_assert_eq!(self.successors_stack.len(), 0);
|
||||||
|
|
||||||
// If `min_depth == depth`, then we are the root of the
|
return_value.unwrap()
|
||||||
// cycle: we can't reach anyone further down the stack.
|
|
||||||
if min_depth == depth {
|
|
||||||
// Note that successor stack may have duplicates, so we
|
|
||||||
// want to remove those:
|
|
||||||
let deduplicated_successors = {
|
|
||||||
let duplicate_set = &mut self.duplicate_set;
|
|
||||||
duplicate_set.clear();
|
|
||||||
self.successors_stack
|
|
||||||
.drain(successors_len..)
|
|
||||||
.filter(move |&i| duplicate_set.insert(i))
|
|
||||||
};
|
|
||||||
let scc_index = self.scc_data.create_scc(deduplicated_successors);
|
|
||||||
self.node_states[node] = NodeState::InCycle { scc_index };
|
|
||||||
WalkReturn::Complete { scc_index }
|
|
||||||
} else {
|
|
||||||
// We are not the head of the cycle. Return back to our
|
|
||||||
// caller. They will take ownership of the
|
|
||||||
// `self.successors` data that we pushed.
|
|
||||||
self.node_states[node] = NodeState::InCycleWith { parent: min_cycle_root };
|
|
||||||
WalkReturn::Cycle { min_depth }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue