Skip to main content

guppy/petgraph_support/
topo.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use petgraph::{
5    graph::IndexType,
6    prelude::*,
7    visit::{
8        GraphRef, IntoNeighborsDirected, IntoNodeIdentifiers, NodeCompactIndexable, VisitMap,
9        Visitable, Walker,
10    },
11};
12use std::marker::PhantomData;
13
14/// A cycle-aware topological sort of a graph.
15#[derive(Clone, Debug)]
16pub struct TopoWithCycles<Ix> {
17    // This is a map of each node index to its corresponding topo index.
18    reverse_index: Box<[usize]>,
19    // Prevent mixing up index types.
20    _phantom: PhantomData<Ix>,
21}
22
23impl<Ix: IndexType> TopoWithCycles<Ix> {
24    pub fn new<G>(graph: G) -> Self
25    where
26        G: GraphRef
27            + Visitable<NodeId = NodeIndex<Ix>>
28            + IntoNodeIdentifiers
29            + IntoNeighborsDirected<NodeId = NodeIndex<Ix>>
30            + NodeCompactIndexable,
31        G::Map: VisitMap<NodeIndex<Ix>>,
32    {
33        // petgraph's default topo algorithms don't handle cycles. Use DfsPostOrder which does.
34        let mut dfs = DfsPostOrder::empty(graph);
35
36        // A node is a root iff it has no incoming neighbors *other than
37        // itself* -- a self-loop is internal to the node's own (single-
38        // element) SCC and must not disqualify it from being a root. This
39        // matches `Sccs::externals`'s single-node SCC branch in
40        // `petgraph_support::scc`.
41        let roots = graph
42            .node_identifiers()
43            .filter(move |&a| !graph.neighbors_directed(a, Incoming).any(|n| n != a));
44        dfs.stack.extend(roots);
45
46        let mut topo: Vec<NodeIndex<Ix>> = (&mut dfs).iter(graph).collect();
47        // dfs returns its data in postorder (reverse topo order), so reverse that for forward topo
48        // order.
49        topo.reverse();
50
51        // Because the graph is NodeCompactIndexable, the indexes are in the range
52        // (0..graph.node_count()).
53        // Use this property to build a reverse map.
54        let mut reverse_index = vec![0; graph.node_count()];
55        topo.iter().enumerate().for_each(|(topo_ix, node_ix)| {
56            reverse_index[node_ix.index()] = topo_ix;
57        });
58
59        // topo.len cannot possibly exceed graph.node_count().
60        assert!(
61            topo.len() <= graph.node_count(),
62            "topo.len() <= graph.node_count() ({} is actually > {})",
63            topo.len(),
64            graph.node_count(),
65        );
66        if topo.len() < graph.node_count() {
67            // This means there was a multi-node cycle in the graph which caused some nodes to be
68            // skipped: none of its members appears as a root (each has a non-self incoming edge),
69            // so the DFS never starts inside it. (Self-loops on otherwise-root nodes are handled
70            // by the root predicate above, matching `Sccs::externals`.)
71            //
72            // In this case, do a best-effort job: fill in the missing nodes with their reverse
73            // index set to the end of the topo order. We could do something fancier here with sccs,
74            // but for guppy this should never happen in practice. (In fact, the one time this code
75            // was hit there was actually an underlying bug.)
76            //
77            // Cross-check the claim above: every missing node must have at least one non-self
78            // incoming neighbor. If that's not true, the root predicate dropped a node that
79            // should have been a root, and the descendants of that dropped root are now being
80            // mis-placed at the end of the topo order. The proptest above only checks index
81            // uniqueness, not topological correctness, so without this assertion such a
82            // regression would pass tests silently.
83            debug_assert!(
84                graph.node_identifiers().all(|m| {
85                    dfs.finished.is_visited(&m)
86                        || graph.neighbors_directed(m, Incoming).any(|p| p != m)
87                }),
88                "topo fallback: a node was missed by the DFS but has no non-self incoming \
89                 neighbor, which means the root-set predicate dropped a legitimate root",
90            );
91
92            let mut next = topo.len();
93            for n in 0..graph.node_count() {
94                let a = NodeIndex::new(n);
95                if !dfs.finished.is_visited(&a) {
96                    // a is a missing index.
97                    reverse_index[a.index()] = next;
98                    next += 1;
99                }
100            }
101        }
102
103        Self {
104            reverse_index: reverse_index.into_boxed_slice(),
105            _phantom: PhantomData,
106        }
107    }
108
109    /// Sort nodes based on the topo order in self.
110    #[inline]
111    pub fn sort_nodes(&self, nodes: &mut [NodeIndex<Ix>]) {
112        nodes.sort_unstable_by_key(|node_ix| self.topo_ix(*node_ix))
113    }
114
115    #[inline]
116    pub fn topo_ix(&self, node_ix: NodeIndex<Ix>) -> usize {
117        self.reverse_index[node_ix.index()]
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use petgraph::Graph;
125
126    /// A self-loop on a node with no other incoming edges must not
127    /// disqualify it from being a root. Without the fix, the node was
128    /// filtered out of the root set, its descendants were never visited
129    /// by the DFS, and the best-effort fallback placed them in node-
130    /// insertion order -- which can disagree with topological order.
131    #[test]
132    fn topo_self_loop_root_orders_descendants_correctly() {
133        // Insert `b` (index 0) before `a` (index 1) so that node-index
134        // order *disagrees* with topological order: the edge `a -> b`
135        // means `a` should precede `b`.
136        let mut graph = Graph::<(), (), Directed, u32>::new();
137        let b = graph.add_node(());
138        let a = graph.add_node(());
139        graph.add_edge(a, b, ());
140        graph.add_edge(a, a, ());
141
142        let topo = TopoWithCycles::<u32>::new(&graph);
143        assert!(
144            topo.topo_ix(a) < topo.topo_ix(b),
145            "a should precede b in topo order despite the self-loop on a \
146             (got topo_ix(a)={}, topo_ix(b)={})",
147            topo.topo_ix(a),
148            topo.topo_ix(b),
149        );
150    }
151
152    /// The fallback path (`topo.len() < graph.node_count()`) fires for
153    /// multi-node cycles with no external entry. Verifies that the
154    /// debug-only invariant assertion does not false-positive on this
155    /// legitimate case, and that every node still gets a unique topo
156    /// index.
157    #[test]
158    fn topo_multi_node_cycle_no_external_entry_fallback() {
159        // a <-> b, plus an unrelated root c. The cycle {a, b} is
160        // unreachable from c, so neither member is a root and the DFS
161        // never enters the cycle. The fallback places a and b at the
162        // end of the topo order.
163        let mut graph = Graph::<(), (), Directed, u32>::new();
164        let a = graph.add_node(());
165        let b = graph.add_node(());
166        let c = graph.add_node(());
167        graph.add_edge(a, b, ());
168        graph.add_edge(b, a, ());
169
170        let topo = TopoWithCycles::<u32>::new(&graph);
171
172        // All three nodes get unique topo indexes in 0..3.
173        let mut seen = [false; 3];
174        for node in [a, b, c] {
175            let ix = topo.topo_ix(node);
176            assert!(ix < 3, "topo_ix out of range: {ix}");
177            assert!(!seen[ix], "topo_ix {ix} seen twice");
178            seen[ix] = true;
179        }
180
181        // `c` is the only real root, so it must come first.
182        assert_eq!(
183            topo.topo_ix(c),
184            0,
185            "c should be at the start of the topo order (got {})",
186            topo.topo_ix(c),
187        );
188    }
189
190    /// A self-loop on a node that is *also* reachable from a real root
191    /// must not change anything: the existing root drives the DFS and
192    /// the self-loop is ignored.
193    #[test]
194    fn topo_self_loop_on_non_root_is_harmless() {
195        // b -> a, plus a self-loop on a. `b` is the only root; `a` is
196        // visited via b's outgoing edge.
197        let mut graph = Graph::<(), (), Directed, u32>::new();
198        let a = graph.add_node(());
199        let b = graph.add_node(());
200        graph.add_edge(b, a, ());
201        graph.add_edge(a, a, ());
202
203        let topo = TopoWithCycles::<u32>::new(&graph);
204        assert!(
205            topo.topo_ix(b) < topo.topo_ix(a),
206            "b should precede a in topo order (got topo_ix(b)={}, topo_ix(a)={})",
207            topo.topo_ix(b),
208            topo.topo_ix(a),
209        );
210    }
211}
212
213#[cfg(all(test, feature = "proptest1"))]
214mod proptests {
215    use super::*;
216    use proptest::prelude::*;
217
218    proptest! {
219        #[test]
220        fn graph_topo_sort(graph in possibly_cyclic_graph()) {
221            let topo = TopoWithCycles::new(&graph);
222            let mut nodes: Vec<_> = graph.node_indices().collect();
223
224            check_consistency(&topo, graph.node_count());
225
226            topo.sort_nodes(&mut nodes);
227            for (topo_ix, node_ix) in nodes.iter().enumerate() {
228                assert_eq!(topo.topo_ix(*node_ix), topo_ix);
229            }
230
231        }
232    }
233
234    fn possibly_cyclic_graph() -> impl Strategy<Value = Graph<(), ()>> {
235        // Generate a graph in adjacency list form. N nodes, up to N**2 edges.
236        (1..=100usize)
237            .prop_flat_map(|n| {
238                (
239                    Just(n),
240                    prop::collection::vec(prop::collection::vec(0..n, 0..n), n),
241                )
242            })
243            .prop_map(|(n, adj)| {
244                let mut graph =
245                    Graph::<(), ()>::with_capacity(n, adj.iter().map(|x| x.len()).sum());
246                for _ in 0..n {
247                    // Add all the nodes under consideration.
248                    graph.add_node(());
249                }
250                for (src, dsts) in adj.into_iter().enumerate() {
251                    let src = NodeIndex::new(src);
252                    for dst in dsts {
253                        let dst = NodeIndex::new(dst);
254                        graph.update_edge(src, dst, ());
255                    }
256                }
257                graph
258            })
259    }
260
261    fn check_consistency(topo: &TopoWithCycles<u32>, n: usize) {
262        // Ensure that all indexes are covered and unique.
263        let mut seen = vec![false; n];
264        for i in 0..n {
265            let topo_ix = topo.topo_ix(NodeIndex::new(i));
266            assert!(
267                !seen[topo_ix],
268                "topo_ix {topo_ix} should be seen exactly once, but seen twice"
269            );
270            seen[topo_ix] = true;
271        }
272        for (i, &this_seen) in seen.iter().enumerate() {
273            assert!(this_seen, "topo_ix {i} should be seen, but wasn't");
274        }
275    }
276}