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}