rustc_data_structures/graph/scc/
mod.rs

1//! Routine to compute the strongly connected components (SCCs) of a graph.
2//!
3//! Also computes as the resulting DAG if each SCC is replaced with a
4//! node in the graph. This uses [Tarjan's algorithm](
5//! https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm)
6//! that completes in *O*(*n*) time.
7//! Optionally, also annotate the SCC nodes with some commutative data.
8//! Typical examples would include: minimum element in SCC, maximum element
9//! reachable from it, etc.
10
11use std::assert_matches::debug_assert_matches;
12use std::fmt::Debug;
13use std::ops::Range;
14
15use rustc_index::{Idx, IndexSlice, IndexVec};
16use tracing::{debug, instrument};
17
18use crate::fx::FxHashSet;
19use crate::graph::vec_graph::VecGraph;
20use crate::graph::{DirectedGraph, NumEdges, Successors};
21
22#[cfg(test)]
23mod tests;
24
25/// An annotation for an SCC. This can be a representative,
26/// the max/min element of the SCC, or all of the above.
27///
28/// Concretely, the both merge operations must commute, e.g. where `merge`
29/// is `merge_scc` and `merge_reached`: `a.merge(b) == b.merge(a)`
30///
31/// In general, what you want is probably always min/max according
32/// to some ordering, potentially with side constraints (min x such
33/// that P holds).
34pub trait Annotation: Debug + Copy {
35    /// Merge two existing annotations into one during
36    /// path compression.o
37    fn merge_scc(self, other: Self) -> Self;
38
39    /// Merge a successor into this annotation.
40    fn merge_reached(self, other: Self) -> Self;
41
42    fn update_scc(&mut self, other: Self) {
43        *self = self.merge_scc(other)
44    }
45
46    fn update_reachable(&mut self, other: Self) {
47        *self = self.merge_reached(other)
48    }
49}
50
51/// The empty annotation, which does nothing.
52impl Annotation for () {
53    fn merge_reached(self, _other: Self) -> Self {
54        ()
55    }
56    fn merge_scc(self, _other: Self) -> Self {
57        ()
58    }
59}
60
61/// Strongly connected components (SCC) of a graph. The type `N` is
62/// the index type for the graph nodes and `S` is the index type for
63/// the SCCs. We can map from each node to the SCC that it
64/// participates in, and we also have the successors of each SCC.
65pub struct Sccs<N: Idx, S: Idx, A: Annotation = ()> {
66    /// For each node, what is the SCC index of the SCC to which it
67    /// belongs.
68    scc_indices: IndexVec<N, S>,
69
70    /// Data about all the SCCs.
71    scc_data: SccData<S, A>,
72}
73
74/// Information about an invidividual SCC node.
75struct SccDetails<A: Annotation> {
76    /// For this SCC, the range of `all_successors` where its
77    /// successors can be found.
78    range: Range<usize>,
79
80    /// User-specified metadata about the SCC.
81    annotation: A,
82}
83
84// The name of this struct should discourage you from making it public and leaking
85// its representation. This message was left here by one who came before you,
86// who learnt the hard way that making even small changes in representation
87// is difficult when it's publicly inspectable.
88//
89// Obey the law of Demeter!
90struct SccData<S: Idx, A: Annotation> {
91    /// Maps SCC indices to their metadata, including
92    /// offsets into `all_successors`.
93    scc_details: IndexVec<S, SccDetails<A>>,
94
95    /// Contains the successors for all the Sccs, concatenated. The
96    /// range of indices corresponding to a given SCC is found in its
97    /// `scc_details.range`.
98    all_successors: Vec<S>,
99}
100
101impl<N: Idx, S: Idx + Ord> Sccs<N, S, ()> {
102    /// Compute SCCs without annotations.
103    pub fn new(graph: &impl Successors<Node = N>) -> Self {
104        Self::new_with_annotation(graph, |_| ())
105    }
106}
107
108impl<N: Idx, S: Idx + Ord, A: Annotation> Sccs<N, S, A> {
109    /// Compute SCCs and annotate them with a user-supplied annotation
110    pub fn new_with_annotation<F: Fn(N) -> A>(
111        graph: &impl Successors<Node = N>,
112        to_annotation: F,
113    ) -> Self {
114        SccsConstruction::construct(graph, to_annotation)
115    }
116
117    pub fn annotation(&self, scc: S) -> A {
118        self.scc_data.annotation(scc)
119    }
120
121    pub fn scc_indices(&self) -> &IndexSlice<N, S> {
122        &self.scc_indices
123    }
124
125    /// Returns the number of SCCs in the graph.
126    pub fn num_sccs(&self) -> usize {
127        self.scc_data.len()
128    }
129
130    /// Returns an iterator over the SCCs in the graph.
131    ///
132    /// The SCCs will be iterated in **dependency order** (or **post order**),
133    /// meaning that if `S1 -> S2`, we will visit `S2` first and `S1` after.
134    /// This is convenient when the edges represent dependencies: when you visit
135    /// `S1`, the value for `S2` will already have been computed.
136    pub fn all_sccs(&self) -> impl Iterator<Item = S> + 'static {
137        (0..self.scc_data.len()).map(S::new)
138    }
139
140    /// Returns the SCC to which a node `r` belongs.
141    pub fn scc(&self, r: N) -> S {
142        self.scc_indices[r]
143    }
144
145    /// Returns the successors of the given SCC.
146    pub fn successors(&self, scc: S) -> &[S] {
147        self.scc_data.successors(scc)
148    }
149
150    /// Construct the reverse graph of the SCC graph.
151    pub fn reverse(&self) -> VecGraph<S> {
152        VecGraph::new(
153            self.num_sccs(),
154            self.all_sccs()
155                .flat_map(|source| {
156                    self.successors(source).iter().map(move |&target| (target, source))
157                })
158                .collect(),
159        )
160    }
161}
162
163impl<N: Idx, S: Idx + Ord, A: Annotation> DirectedGraph for Sccs<N, S, A> {
164    type Node = S;
165
166    fn num_nodes(&self) -> usize {
167        self.num_sccs()
168    }
169}
170
171impl<N: Idx, S: Idx + Ord, A: Annotation> NumEdges for Sccs<N, S, A> {
172    fn num_edges(&self) -> usize {
173        self.scc_data.all_successors.len()
174    }
175}
176
177impl<N: Idx, S: Idx + Ord, A: Annotation> Successors for Sccs<N, S, A> {
178    fn successors(&self, node: S) -> impl Iterator<Item = Self::Node> {
179        self.successors(node).iter().cloned()
180    }
181}
182
183impl<S: Idx, A: Annotation> SccData<S, A> {
184    /// Number of SCCs,
185    fn len(&self) -> usize {
186        self.scc_details.len()
187    }
188
189    /// Returns the successors of the given SCC.
190    fn successors(&self, scc: S) -> &[S] {
191        &self.all_successors[self.scc_details[scc].range.clone()]
192    }
193
194    /// Creates a new SCC with `successors` as its successors and
195    /// the maximum weight of its internal nodes `scc_max_weight` and
196    /// returns the resulting index.
197    fn create_scc(&mut self, successors: impl IntoIterator<Item = S>, annotation: A) -> S {
198        // Store the successors on `scc_successors_vec`, remembering
199        // the range of indices.
200        let all_successors_start = self.all_successors.len();
201        self.all_successors.extend(successors);
202        let all_successors_end = self.all_successors.len();
203
204        debug!(
205            "create_scc({:?}) successors={:?}, annotation={:?}",
206            self.len(),
207            &self.all_successors[all_successors_start..all_successors_end],
208            annotation
209        );
210
211        let range = all_successors_start..all_successors_end;
212        let metadata = SccDetails { range, annotation };
213        self.scc_details.push(metadata)
214    }
215
216    fn annotation(&self, scc: S) -> A {
217        self.scc_details[scc].annotation
218    }
219}
220
221struct SccsConstruction<'c, G, S, A, F>
222where
223    G: DirectedGraph + Successors,
224    S: Idx,
225    A: Annotation,
226    F: Fn(G::Node) -> A,
227{
228    graph: &'c G,
229
230    /// The state of each node; used during walk to record the stack
231    /// and after walk to record what cycle each node ended up being
232    /// in.
233    node_states: IndexVec<G::Node, NodeState<G::Node, S, A>>,
234
235    /// The stack of nodes that we are visiting as part of the DFS.
236    node_stack: Vec<G::Node>,
237
238    /// The stack of successors: as we visit a node, we mark our
239    /// position in this stack, and when we encounter a successor SCC,
240    /// we push it on the stack. When we complete an SCC, we can pop
241    /// everything off the stack that was found along the way.
242    successors_stack: Vec<S>,
243
244    /// A set used to strip duplicates. As we accumulate successors
245    /// into the successors_stack, we sometimes get duplicate entries.
246    /// We use this set to remove those -- we also keep its storage
247    /// around between successors to amortize memory allocation costs.
248    duplicate_set: FxHashSet<S>,
249
250    scc_data: SccData<S, A>,
251
252    /// A function that constructs an initial SCC annotation
253    /// out of a single node.
254    to_annotation: F,
255}
256
257#[derive(Copy, Clone, Debug)]
258enum NodeState<N, S, A> {
259    /// This node has not yet been visited as part of the DFS.
260    ///
261    /// After SCC construction is complete, this state ought to be
262    /// impossible.
263    NotVisited,
264
265    /// This node is currently being walked as part of our DFS. It is on
266    /// the stack at the depth `depth` and its current annotation is
267    /// `annotation`.
268    ///
269    /// After SCC construction is complete, this state ought to be
270    /// impossible.
271    BeingVisited { depth: usize, annotation: A },
272
273    /// Indicates that this node is a member of the given cycle where
274    /// the merged annotation is `annotation`.
275    /// Note that an SCC can have several cycles, so its final annotation
276    /// is the merged value of all its member annotations.
277    InCycle { scc_index: S, annotation: A },
278
279    /// Indicates that this node is a member of whatever cycle
280    /// `parent` is a member of. This state is transient: whenever we
281    /// see it, we try to overwrite it with the current state of
282    /// `parent` (this is the "path compression" step of a union-find
283    /// algorithm).
284    InCycleWith { parent: N },
285}
286
287/// The state of walking a given node.
288#[derive(Copy, Clone, Debug)]
289enum WalkReturn<S, A> {
290    /// The walk found a cycle, but the entire component is not known to have
291    /// been fully walked yet. We only know the minimum depth of  this
292    /// component in a minimum spanning tree of the graph. This component
293    /// is tentatively represented by the state of the first node of this
294    /// cycle we met, which is at `min_depth`.
295    Cycle { min_depth: usize, annotation: A },
296    /// The SCC and everything reachable from it have been fully walked.
297    /// At this point we know what is inside the SCC as we have visited every
298    /// node reachable from it. The SCC can now be fully represented by its ID.
299    Complete { scc_index: S, annotation: A },
300}
301
302impl<'c, G, S, A, F> SccsConstruction<'c, G, S, A, F>
303where
304    G: DirectedGraph + Successors,
305    S: Idx,
306    F: Fn(G::Node) -> A,
307    A: Annotation,
308{
309    /// Identifies SCCs in the graph `G` and computes the resulting
310    /// DAG. This uses a variant of [Tarjan's
311    /// algorithm][wikipedia]. The high-level summary of the algorithm
312    /// is that we do a depth-first search. Along the way, we keep a
313    /// stack of each node whose successors are being visited. We
314    /// track the depth of each node on this stack (there is no depth
315    /// if the node is not on the stack). When we find that some node
316    /// N with depth D can reach some other node N' with lower depth
317    /// D' (i.e., D' < D), we know that N, N', and all nodes in
318    /// between them on the stack are part of an SCC.
319    ///
320    /// Additionally, we keep track of a current annotation of the SCC.
321    ///
322    /// [wikipedia]: https://bit.ly/2EZIx84
323    fn construct(graph: &'c G, to_annotation: F) -> Sccs<G::Node, S, A> {
324        let num_nodes = graph.num_nodes();
325
326        let mut this = Self {
327            graph,
328            node_states: IndexVec::from_elem_n(NodeState::NotVisited, num_nodes),
329            node_stack: Vec::with_capacity(num_nodes),
330            successors_stack: Vec::new(),
331            scc_data: SccData { scc_details: IndexVec::new(), all_successors: Vec::new() },
332            duplicate_set: FxHashSet::default(),
333            to_annotation,
334        };
335
336        let scc_indices = graph
337            .iter_nodes()
338            .map(|node| match this.start_walk_from(node) {
339                WalkReturn::Complete { scc_index, .. } => scc_index,
340                WalkReturn::Cycle { min_depth, .. } => {
341                    panic!("`start_walk_node({node:?})` returned cycle with depth {min_depth:?}")
342                }
343            })
344            .collect();
345
346        Sccs { scc_indices, scc_data: this.scc_data }
347    }
348
349    fn start_walk_from(&mut self, node: G::Node) -> WalkReturn<S, A> {
350        self.inspect_node(node).unwrap_or_else(|| self.walk_unvisited_node(node))
351    }
352
353    /// Inspect a node during the DFS. We first examine its current
354    /// state -- if it is not yet visited (`NotVisited`), return `None` so
355    /// that the caller might push it onto the stack and start walking its
356    /// successors.
357    ///
358    /// If it is already on the DFS stack it will be in the state
359    /// `BeingVisited`. In that case, we have found a cycle and we
360    /// return the depth from the stack.
361    ///
362    /// Otherwise, we are looking at a node that has already been
363    /// completely visited. We therefore return `WalkReturn::Complete`
364    /// with its associated SCC index.
365    fn inspect_node(&mut self, node: G::Node) -> Option<WalkReturn<S, A>> {
366        Some(match self.find_state(node) {
367            NodeState::InCycle { scc_index, annotation } => {
368                WalkReturn::Complete { scc_index, annotation }
369            }
370
371            NodeState::BeingVisited { depth: min_depth, annotation } => {
372                WalkReturn::Cycle { min_depth, annotation }
373            }
374
375            NodeState::NotVisited => return None,
376
377            NodeState::InCycleWith { parent } => panic!(
378                "`find_state` returned `InCycleWith({parent:?})`, which ought to be impossible"
379            ),
380        })
381    }
382
383    /// Fetches the state of the node `r`. If `r` is recorded as being
384    /// in a cycle with some other node `r2`, then fetches the state
385    /// of `r2` (and updates `r` to reflect current result). This is
386    /// basically the "find" part of a standard union-find algorithm
387    /// (with path compression).
388    fn find_state(&mut self, mut node: G::Node) -> NodeState<G::Node, S, A> {
389        // To avoid recursion we temporarily reuse the `parent` of each
390        // InCycleWith link to encode a downwards link while compressing
391        // the path. After we have found the root or deepest node being
392        // visited, we traverse the reverse links and correct the node
393        // states on the way.
394        //
395        // **Note**: This mutation requires that this is a leaf function
396        // or at least that none of the called functions inspects the
397        // current node states. Luckily, we are a leaf.
398
399        // Remember one previous link. The termination condition when
400        // following links downwards is then simply as soon as we have
401        // found the initial self-loop.
402        let mut previous_node = node;
403
404        // Ultimately propagated to all the transitive parents when following
405        // `InCycleWith` upwards.
406        // This loop performs the downward link encoding mentioned above. Details below!
407        // Note that there are two different states being assigned: the root state, and
408        // a potentially derived version of the root state for non-root nodes in the chain.
409        let (root_state, assigned_state) = {
410            loop {
411                debug!("find_state(r = {node:?} in state {:?})", self.node_states[node]);
412                match self.node_states[node] {
413                    // This must have been the first and only state since it is unexplored*;
414                    // no update needed! * Unless there is a bug :')
415                    s @ NodeState::NotVisited => return s,
416                    // We are in a completely discovered SCC; every node on our path is in that SCC:
417                    s @ NodeState::InCycle { .. } => break (s, s),
418                    // The Interesting Third Base Case: we are a path back to a root node
419                    // still being explored. Now we need that node to keep its state and
420                    // every other node to be recorded as being in whatever component that
421                    // ends up in.
422                    s @ NodeState::BeingVisited { depth, .. } => {
423                        break (s, NodeState::InCycleWith { parent: self.node_stack[depth] });
424                    }
425                    // We are not at the head of a path; keep compressing it!
426                    NodeState::InCycleWith { parent } => {
427                        // We test this, to be extremely sure that we never
428                        // ever break our termination condition for the
429                        // reverse iteration loop.
430                        assert!(node != parent, "Node can not be in cycle with itself");
431
432                        // Store the previous node as an inverted list link
433                        self.node_states[node] = NodeState::InCycleWith { parent: previous_node };
434                        // Update to parent node.
435                        previous_node = node;
436                        node = parent;
437                    }
438                }
439            }
440        };
441
442        // The states form a graph where up to one outgoing link is stored at
443        // each node. Initially in general,
444        //
445        //                                                  E
446        //                                                  ^
447        //                                                  |
448        //                                InCycleWith/BeingVisited/NotVisited
449        //                                                  |
450        //   A-InCycleWith->B-InCycleWith…>C-InCycleWith->D-+
451        //   |
452        //   = node, previous_node
453        //
454        // After the first loop, this will look like
455        //                                                  E
456        //                                                  ^
457        //                                                  |
458        //                                InCycleWith/BeingVisited/NotVisited
459        //                                                  |
460        // +>A<-InCycleWith-B<…InCycleWith-C<-InCycleWith-D-+
461        // | |                             |              |
462        // | InCycleWith                   |              = node
463        // +-+                             =previous_node
464        //
465        // Note in particular that A will be linked to itself in a self-cycle
466        // and no other self-cycles occur due to how InCycleWith is assigned in
467        // the find phase implemented by `walk_unvisited_node`.
468        //
469        // We now want to compress the path, that is assign the state of the
470        // link D-E to all other links.
471        //
472        // We can then walk backwards, starting from `previous_node`, and assign
473        // each node in the list with the updated state. The loop terminates
474        // when we reach the self-cycle.
475
476        // Move backwards until we found the node where we started. We
477        // will know when we hit the state where previous_node == node.
478        loop {
479            // Back at the beginning, we can return. Note that we return the root state.
480            // This is because for components being explored, we would otherwise get a
481            // `node_state[n] = InCycleWith{ parent: n }` and that's wrong.
482            if previous_node == node {
483                return root_state;
484            }
485            debug!("Compressing {node:?} down to {previous_node:?} with state {assigned_state:?}");
486
487            // Update to previous node in the link.
488            match self.node_states[previous_node] {
489                NodeState::InCycleWith { parent: previous } => {
490                    node = previous_node;
491                    previous_node = previous;
492                }
493                // Only InCycleWith nodes were added to the reverse linked list.
494                other => unreachable!("Invalid previous link while compressing cycle: {other:?}"),
495            }
496
497            // Update the node state to the (potentially derived) state.
498            // If the root is still being explored, this is
499            // `InCycleWith{ parent: <root node>}`, otherwise
500            // `assigned_state == root_state`.
501            self.node_states[node] = assigned_state;
502        }
503    }
504
505    /// Walks a node that has never been visited before.
506    ///
507    /// Call this method when `inspect_node` has returned `None`. Having the
508    /// caller decide avoids mutual recursion between the two methods and allows
509    /// us to maintain an allocated stack for nodes on the path between calls.
510    #[instrument(skip(self, initial), level = "debug")]
511    fn walk_unvisited_node(&mut self, initial: G::Node) -> WalkReturn<S, A> {
512        debug!("Walk unvisited node: {initial:?}");
513        struct VisitingNodeFrame<G: DirectedGraph, Successors, A> {
514            node: G::Node,
515            successors: Option<Successors>,
516            depth: usize,
517            min_depth: usize,
518            successors_len: usize,
519            min_cycle_root: G::Node,
520            successor_node: G::Node,
521            /// The annotation for the SCC starting in `node`. It may or may
522            /// not contain other nodes.
523            current_component_annotation: A,
524        }
525
526        // Move the stack to a local variable. We want to utilize the existing allocation and
527        // mutably borrow it without borrowing self at the same time.
528        let mut successors_stack = core::mem::take(&mut self.successors_stack);
529
530        debug_assert_eq!(successors_stack.len(), 0);
531
532        let mut stack: Vec<VisitingNodeFrame<G, _, _>> = vec![VisitingNodeFrame {
533            node: initial,
534            depth: 0,
535            min_depth: 0,
536            successors: None,
537            successors_len: 0,
538            min_cycle_root: initial,
539            successor_node: initial,
540            current_component_annotation: (self.to_annotation)(initial),
541        }];
542
543        let mut return_value = None;
544
545        'recurse: while let Some(frame) = stack.last_mut() {
546            let VisitingNodeFrame {
547                node,
548                depth,
549                successors,
550                successors_len,
551                min_depth,
552                min_cycle_root,
553                successor_node,
554                current_component_annotation,
555            } = frame;
556            let node = *node;
557            let depth = *depth;
558
559            // node is definitely in the current component, add it to the annotation.
560            if node != initial {
561                current_component_annotation.update_scc((self.to_annotation)(node));
562            }
563            debug!(
564                "Visiting {node:?} at depth {depth:?}, annotation: {current_component_annotation:?}"
565            );
566
567            let successors = match successors {
568                Some(successors) => successors,
569                None => {
570                    // This None marks that we still have the initialize this node's frame.
571                    debug!(?depth, ?node);
572
573                    debug_assert_matches!(self.node_states[node], NodeState::NotVisited);
574
575                    // Push `node` onto the stack.
576                    self.node_states[node] = NodeState::BeingVisited {
577                        depth,
578                        annotation: *current_component_annotation,
579                    };
580                    self.node_stack.push(node);
581
582                    // Walk each successor of the node, looking to see if any of
583                    // them can reach a node that is presently on the stack. If
584                    // so, that means they can also reach us.
585                    *successors_len = successors_stack.len();
586                    // Set and return a reference, this is currently empty.
587                    successors.get_or_insert(self.graph.successors(node))
588                }
589            };
590
591            // Now that the successors iterator is initialized, this is a constant for this frame.
592            let successors_len = *successors_len;
593
594            // Construct iterators for the nodes and walk results. There are two cases:
595            // * The walk of a successor node returned.
596            // * The remaining successor nodes.
597            let returned_walk =
598                return_value.take().into_iter().map(|walk| (*successor_node, Some(walk)));
599
600            let successor_walk = successors.map(|successor_node| {
601                debug!(?node, ?successor_node);
602                (successor_node, self.inspect_node(successor_node))
603            });
604            for (successor_node, walk) in returned_walk.chain(successor_walk) {
605                match walk {
606                    // The starting node `node` leads to a cycle whose earliest node,
607                    // `successor_node`, is at `min_depth`. There may be more cycles.
608                    Some(WalkReturn::Cycle {
609                        min_depth: successor_min_depth,
610                        annotation: successor_annotation,
611                    }) => {
612                        debug!(
613                            "Cycle found from {node:?}, minimum depth: {successor_min_depth:?}, annotation: {successor_annotation:?}"
614                        );
615                        // Track the minimum depth we can reach.
616                        assert!(successor_min_depth <= depth);
617                        if successor_min_depth < *min_depth {
618                            debug!(?node, ?successor_min_depth);
619                            *min_depth = successor_min_depth;
620                            *min_cycle_root = successor_node;
621                        }
622                        current_component_annotation.update_scc(successor_annotation);
623                    }
624                    // The starting node `node` is succeeded by a fully identified SCC
625                    // which is now added to the set under `scc_index`.
626                    Some(WalkReturn::Complete {
627                        scc_index: successor_scc_index,
628                        annotation: successor_annotation,
629                    }) => {
630                        debug!(
631                            "Complete; {node:?} is root of complete-visited SCC idx {successor_scc_index:?} with annotation {successor_annotation:?}"
632                        );
633                        // Push the completed SCC indices onto
634                        // the `successors_stack` for later.
635                        debug!(?node, ?successor_scc_index);
636                        successors_stack.push(successor_scc_index);
637                        current_component_annotation.update_reachable(successor_annotation);
638                    }
639                    // `node` has no more (direct) successors; search recursively.
640                    None => {
641                        let depth = depth + 1;
642                        debug!("Recursing down into {successor_node:?} at depth {depth:?}");
643                        debug!(?depth, ?successor_node);
644                        // Remember which node the return value will come from.
645                        frame.successor_node = successor_node;
646                        // Start a new stack frame, then step into it.
647                        stack.push(VisitingNodeFrame {
648                            node: successor_node,
649                            depth,
650                            successors: None,
651                            successors_len: 0,
652                            min_depth: depth,
653                            min_cycle_root: successor_node,
654                            successor_node,
655                            current_component_annotation: (self.to_annotation)(successor_node),
656                        });
657                        continue 'recurse;
658                    }
659                }
660            }
661
662            debug!("Finished walk from {node:?} with annotation: {current_component_annotation:?}");
663
664            // Completed walk, remove `node` from the stack.
665            let r = self.node_stack.pop();
666            debug_assert_eq!(r, Some(node));
667
668            // Remove the frame, it's done.
669            let frame = stack.pop().unwrap();
670            let current_component_annotation = frame.current_component_annotation;
671            debug_assert_eq!(frame.node, node);
672
673            // If `min_depth == depth`, then we are the root of the
674            // cycle: we can't reach anyone further down the stack.
675
676            // Pass the 'return value' down the stack.
677            // We return one frame at a time so there can't be another return value.
678            debug_assert!(return_value.is_none());
679            return_value = Some(if frame.min_depth == depth {
680                // We are at the head of the component.
681
682                // Note that successor stack may have duplicates, so we
683                // want to remove those:
684                let deduplicated_successors = {
685                    let duplicate_set = &mut self.duplicate_set;
686                    duplicate_set.clear();
687                    successors_stack
688                        .drain(successors_len..)
689                        .filter(move |&i| duplicate_set.insert(i))
690                };
691
692                debug!("Creating SCC rooted in {node:?} with successor {:?}", frame.successor_node);
693
694                let scc_index =
695                    self.scc_data.create_scc(deduplicated_successors, current_component_annotation);
696
697                self.node_states[node] =
698                    NodeState::InCycle { scc_index, annotation: current_component_annotation };
699
700                WalkReturn::Complete { scc_index, annotation: current_component_annotation }
701            } else {
702                // We are not the head of the cycle. Return back to our
703                // caller. They will take ownership of the
704                // `self.successors` data that we pushed.
705                self.node_states[node] = NodeState::InCycleWith { parent: frame.min_cycle_root };
706                WalkReturn::Cycle {
707                    min_depth: frame.min_depth,
708                    annotation: current_component_annotation,
709                }
710            });
711        }
712
713        // Keep the allocation we used for successors_stack.
714        self.successors_stack = successors_stack;
715        debug_assert_eq!(self.successors_stack.len(), 0);
716
717        return_value.unwrap()
718    }
719}