Skip to main content

dfir_lang/graph/
graph_algorithms.rs

1//! General graph algorithm utility functions
2
3use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
4use std::hash::Hash;
5
6use slotmap::{Key, SecondaryMap, SparseSecondaryMap};
7
8/// Topologically sorts a set of nodes. Returns a list where the order of `Id`s will agree with
9/// the order of any path through the graph.
10///
11/// This succeeds if the input is a directed acyclic graph (DAG).
12///
13/// If the input has a cycle, an `Err` will be returned containing the cycle. Each node in the
14/// cycle will be listed exactly once.
15///
16/// <https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search>
17pub fn topo_sort<Id, PredsIter>(
18    node_ids: impl IntoIterator<Item = Id>,
19    mut preds_fn: impl FnMut(Id) -> PredsIter,
20) -> Result<Vec<Id>, Vec<Id>>
21where
22    Id: Copy + Eq + Hash,
23    PredsIter: IntoIterator<Item = Id>,
24{
25    let (mut marked, mut order) = Default::default();
26
27    fn pred_dfs_postorder<Id, PredsIter>(
28        node_id: Id,
29        preds_fn: &mut impl FnMut(Id) -> PredsIter,
30        marked: &mut HashMap<Id, bool>, // `false` => temporary, `true` => permanent.
31        order: &mut Vec<Id>,
32    ) -> Result<(), ()>
33    where
34        Id: Copy + Eq + Hash,
35        PredsIter: IntoIterator<Item = Id>,
36    {
37        match marked.get(&node_id) {
38            Some(_permanent @ true) => Ok(()),
39            Some(_temporary @ false) => {
40                // Cycle found!
41                order.clear();
42                order.push(node_id);
43                Err(())
44            }
45            None => {
46                marked.insert(node_id, false);
47                for next_pred in (preds_fn)(node_id) {
48                    pred_dfs_postorder(next_pred, preds_fn, marked, order).map_err(|()| {
49                        if order.len() == 1 || order.first().unwrap() != order.last().unwrap() {
50                            order.push(node_id);
51                        }
52                    })?;
53                }
54                order.push(node_id);
55                marked.insert(node_id, true);
56                Ok(())
57            }
58        }
59    }
60
61    for node_id in node_ids {
62        if pred_dfs_postorder(node_id, &mut preds_fn, &mut marked, &mut order).is_err() {
63            // Cycle found.
64            let end = order.last().unwrap();
65            let beg = order.iter().position(|n| n == end).unwrap();
66            order.drain(0..=beg);
67            return Err(order);
68        }
69    }
70
71    Ok(order)
72}
73
74/// Validates the topo sort.
75///
76/// Returns `Ok(())` if it is valid. Returns `Err((pred, succ))` where `pred` comes after `succ` when the sort is
77/// invalid. Panics if a node returned by the `pred_fn` is missing from the sort.
78pub fn validate_topo_sort<Id, PredsIter>(
79    topo_sort: impl IntoIterator<Item = Id>,
80    mut preds_fn: impl FnMut(Id) -> PredsIter,
81) -> Result<(), (Id, Id)>
82where
83    Id: Copy + Eq + Ord,
84    PredsIter: IntoIterator<Item = Id>,
85{
86    let indexed = topo_sort
87        .into_iter()
88        .enumerate()
89        .map(|(i, n)| (n, i))
90        .collect::<BTreeMap<_, _>>();
91    for (&succ, &succ_idx) in indexed.iter() {
92        for pred in (preds_fn)(succ) {
93            let Some(&pred_idx) = indexed.get(&pred) else {
94                panic!("predecessor not in topo sort");
95            };
96            if succ_idx <= pred_idx {
97                // Violation in topo sort.
98                return Err((pred, succ));
99            }
100        }
101    }
102    Ok(())
103}
104
105/// Datastructure for merging subgraphs while maintaining topological sort order.
106///
107/// Maintains a global topo-sorted Vec of all operators. Each subgraph (merged group)
108/// occupies a contiguous range in this Vec. Merging two groups combines their ranges
109/// and re-sorts the affected window so groups remain contiguous and correctly ordered.
110pub struct SubgraphMerge<K>
111where
112    K: Key,
113{
114    /// Predecessor edges in the quotient DAG (per representative).
115    subgraph_preds: SecondaryMap<K, Vec<K>>,
116    /// All operators in global topo-sort order (fixed length, reshuffled in windows).
117    /// Invariant: subgraphs are contiguous & non-overlapping ranges in this vec.
118    toposort_node: Vec<K>,
119    /// Reverse index: SG representative node -> index (in toposort_node).
120    /// Invariant: `K` is both the representative node and the first node in the SG.
121    sg_idx: SparseSecondaryMap<K, usize>,
122    /// SG representative node -> SG len.
123    /// The subgraph's nodes are `toposort_node[index..index+len]`.
124    /// Invariant: the subgraph ranges are complete and non-overlapping.
125    sg_len: SparseSecondaryMap<K, usize>,
126
127    /// Union-find for subgraph membership.
128    subgraph_unionfind: crate::union_find::UnionFind<K>,
129
130    /// Per-representative: set of representatives that this subgraph must not merge with.
131    /// Maintained symmetrically: if `enemies[a]` contains `b`, then `enemies[b]` contains `a`.
132    enemies: SecondaryMap<K, HashSet<K>>,
133}
134
135impl<K> SubgraphMerge<K>
136where
137    K: Key,
138{
139    /// Creates a new `SubgraphMerge` from nodes and their predecessor edges.
140    ///
141    /// `enemies` specifies pairs of nodes that must never be placed in the same subgraph.
142    /// These are checked in O(1) during [`Self::try_merge`] and maintained as representatives
143    /// change.
144    ///
145    /// Returns `Err` with a cycle if the input graph is not a DAG.
146    pub fn new<PredsIter>(
147        keys: impl IntoIterator<Item = K>,
148        mut preds_fn: impl FnMut(K) -> PredsIter,
149        enemies_iter: impl IntoIterator<Item = (K, K)>,
150    ) -> Result<Self, Vec<K>>
151    where
152        PredsIter: IntoIterator<Item = K>,
153    {
154        let subgraph_preds = keys
155            .into_iter()
156            .map(|k| (k, (preds_fn)(k).into_iter().collect()))
157            .collect::<SecondaryMap<K, Vec<K>>>();
158        let toposort_node =
159            topo_sort(subgraph_preds.keys(), |k| subgraph_preds[k].iter().copied())?;
160        let sg_idx = toposort_node
161            .iter()
162            .enumerate()
163            .map(|(i, &k)| (k, i))
164            .collect();
165        let sg_len = toposort_node.iter().map(|&k| (k, 1)).collect();
166        let subgraph_unionfind = crate::union_find::UnionFind::with_capacity(toposort_node.len());
167
168        let mut enemies = SecondaryMap::<K, HashSet<K>>::new();
169        for (a, b) in enemies_iter {
170            assert_ne!(a, b, "no-merge pair must not contain the same node twice");
171            enemies.entry(a).unwrap().or_default().insert(b);
172            enemies.entry(b).unwrap().or_default().insert(a);
173        }
174
175        Ok(Self {
176            subgraph_preds,
177            toposort_node,
178            sg_idx,
179            sg_len,
180            subgraph_unionfind,
181            enemies,
182        })
183    }
184
185    /// Find the representative of the subgraph containing `k`.
186    pub fn find(&mut self, k: K) -> K {
187        self.subgraph_unionfind.find(k)
188    }
189
190    /// Returns true if `u` and `v` are in the same subgraph.
191    pub fn same_set(&mut self, u: K, v: K) -> bool {
192        self.subgraph_unionfind.same_set(u, v)
193    }
194
195    /// Iterates all subgraph representatives with their topo-sorted operator slices,
196    /// in topological order (by position in `toposort_node`).
197    pub fn subgraphs(&self) -> impl Iterator<Item = &[K]> {
198        let mut i = 0;
199        std::iter::from_fn(move || {
200            let Some(&sg_node) = self.toposort_node.get(i) else {
201                debug_assert_eq!(i, self.toposort_node.len());
202                return None;
203            };
204            debug_assert_eq!(i, self.sg_idx[sg_node]);
205            let sg_len = self.sg_len[sg_node];
206            let sg_slice = &self.toposort_node[i..i + sg_len];
207            i += sg_len;
208            Some(sg_slice)
209        })
210    }
211
212    /// Attempts to merge the subgraphs containing `u` and `v`.
213    /// Returns `false` if merging would create a cycle in the subgraph DAG,
214    /// or if the merge is forbidden by a no-merge constraint.
215    pub fn try_merge(&mut self, u: K, v: K) -> bool {
216        // 0. Set up `u` and `v` to be in order, and subgraph representatives.
217
218        // Ensure `u` and `v` are subgraph representatives.
219        let u = self.subgraph_unionfind.find(u);
220        let v = self.subgraph_unionfind.find(v);
221        if u == v {
222            // Short circuit no-op case. Guards against weird `u == v` aliasing.
223            return true;
224        }
225
226        // O(1) no-merge constraint check.
227        if self
228            .enemies
229            .get(u)
230            .is_some_and(|enemy_set| enemy_set.contains(&v))
231        {
232            return false;
233        }
234
235        // Ensure `u` is before `v` in topo order.
236        let (u, v) = if self.sg_idx[u] < self.sg_idx[v] {
237            (u, v)
238        } else {
239            (v, u)
240        };
241        // Get the member nodes of `u` and `v`, and the `window`. Pulling references here does ensure that
242        // `toposort_node` remains unchanged until we properly merge `u_nodes` and `v_nodes`.
243        let (u_nodes, v_nodes, window) = {
244            let (u_idx, u_len) = (self.sg_idx[u], self.sg_len[u]);
245            let (v_idx, v_len) = (self.sg_idx[v], self.sg_len[v]);
246            (
247                &self.toposort_node[u_idx..u_idx + u_len],
248                &self.toposort_node[v_idx..v_idx + v_len],
249                u_idx..v_idx + v_len,
250            )
251        };
252
253        // 1. Cycle check: can `v` reach `u` via predecessor edges?
254        // Only groups within `window` can be on such a path. Direct predecessor edges from `v` to `u` become
255        // self-loops after merge and are not real cycles, so we skip direct `u -> v` edges.
256
257        let mut stack = vec![v];
258        let mut visited = HashSet::<_>::from_iter([v]);
259
260        while let Some(x) = stack.pop() {
261            for &p in self.subgraph_preds[x].iter() {
262                let root_p = self.subgraph_unionfind.find(p);
263
264                if root_p == u {
265                    if x == v {
266                        // Ignore `u -> v` direct edge, not a real cycle.
267                        continue;
268                    }
269                    // Cycle found, return false.
270                    return false;
271                }
272
273                // Prune: group must be within the `window`.
274                if window.contains(&self.sg_idx[root_p]) && visited.insert(root_p) {
275                    stack.push(root_p);
276                }
277            }
278        }
279
280        // 2. Perform merge in union-find and append predecessors.
281        // `u` will be the new representative.
282        {
283            // `UnionFind::union` ensures the first arg's representative will represent the new merged group. `u` is before
284            // `v` in the topo order, and `u` is already its own representative. This ensures that `u` stays at the *start*
285            // of its subgraph group, so the `idx..idx+len` slice is the whole subgraph.
286            let _new_root = self.subgraph_unionfind.union(u, v);
287            debug_assert_eq!(u, _new_root);
288            let v_preds = &mut self.subgraph_preds.remove(v).unwrap();
289            let u_preds = &mut self.subgraph_preds[u];
290            u_preds.append(v_preds);
291            // Update all preds to be representatives (from past unioning). Delete any self-edges.
292            u_preds.retain_mut(|x| {
293                *x = self.subgraph_unionfind.find(*x);
294                *x != u // Retain only non-self edges.
295            });
296            // Remove any duplicates (may have be created from past unioning).
297            u_preds.sort_unstable();
298            u_preds.dedup();
299        }
300        // Remove subsumed `v` and grow `u`'s length.
301        {
302            self.sg_idx.remove(v).unwrap();
303            let v_len = self.sg_len.remove(v).unwrap();
304            // Set `u`'s len to the combined size. (Note: `sg_idx[u]` still needs updating, below after re-sort).
305            self.sg_len[u] += v_len;
306        }
307        // Merge enemies: remap v's enemies to point to u.
308        for w in self.enemies.remove(v).into_iter().flatten() {
309            debug_assert_ne!(
310                w, u,
311                "`w` in an enemy of `v`, so it can't be `w == u`, since we are merging `u` and `v`"
312            );
313            // Add `w`` to `u`'s enemies.
314            self.enemies.entry(u).unwrap().or_default().insert(w);
315            // Add `u` to `w`'s enemies. Remove `v`.
316            // `w` enemies guaranteed to exist by the symmetric invariant: if `v`'s enemies contain `w``, then `w`'s
317            // enemies contain `v`.
318            let w_enemies = self.enemies.get_mut(w).unwrap();
319            let _removed = w_enemies.remove(&v);
320            debug_assert!(_removed);
321            w_enemies.insert(u);
322        }
323
324        // 3. Re-sort groups in `window`.
325        // Topo-sort groups in the window by their quotient edges.
326        {
327            let sorted_groups = {
328                let reps_in_window = self.toposort_node[window.clone()]
329                    .iter()
330                    .map(|&k| self.subgraph_unionfind.find(k))
331                    .collect::<BTreeSet<_>>();
332
333                // We borrow fields separately to allow the closure to call `find()` (which needs `&mut`) while also reading
334                // `subgraph_preds` and `sg_idx` (via `&`).
335                // Only predecessor groups whose range overlaps the window are included - groups entirely outside the window
336                // have their ordering already satisfied.
337                let subgraph_preds = &self.subgraph_preds;
338                let subgraph_unionfind = &mut self.subgraph_unionfind;
339                let sg_idx = &self.sg_idx;
340                topo_sort(reps_in_window, |k| {
341                    subgraph_preds[k]
342                    .iter()
343                    .map(|&p| subgraph_unionfind.find(p))
344                    .filter(|&p| window.contains(&sg_idx[p])) // Prune to window.
345                    .collect::<Vec<_>>()
346                    .into_iter()
347                })
348                .expect("bug: cycle check passed but re-toposort found cycle")
349            };
350
351            // Rebuild the window: lay out each group's operators in sorted group order.
352            // All groups except `u` (new root) have contiguous operators at their current range. `u`'s operators will be
353            // `u_nodes` *and* `v_nodes`.
354            let mut buf = Vec::with_capacity(window.len());
355            for &group in &sorted_groups {
356                if group == u {
357                    buf.extend_from_slice(u_nodes);
358                    buf.extend_from_slice(v_nodes);
359                } else {
360                    let g_idx = self.sg_idx[group];
361                    let g_len = self.sg_len[group];
362                    buf.extend_from_slice(&self.toposort_node[g_idx..g_idx + g_len]);
363                }
364            }
365            self.toposort_node[window.clone()].copy_from_slice(&buf);
366
367            // Update reverse index `sg_idx` start positions (`sg_len` already correct).
368            let mut pos = window.start;
369            for &group in &sorted_groups {
370                self.sg_idx[group] = pos;
371                pos += self.sg_len[group];
372            }
373            debug_assert_eq!(window.end, pos);
374        }
375
376        true
377    }
378}
379
380#[cfg(test)]
381mod test {
382    use std::collections::{BTreeMap, BTreeSet};
383
384    use itertools::Itertools;
385    use slotmap::SlotMap;
386
387    use super::*;
388
389    #[test]
390    pub fn test_toposort() {
391        let edges = [
392            (5, 11),
393            (11, 2),
394            (11, 9),
395            (11, 10),
396            (7, 11),
397            (7, 8),
398            (8, 9),
399            (3, 8),
400            (3, 10),
401        ];
402
403        // https://commons.wikimedia.org/wiki/File:Directed_acyclic_graph_2.svg
404        let sort = topo_sort([2, 3, 5, 7, 8, 9, 10, 11], |v| {
405            edges
406                .iter()
407                .filter(move |&&(_, dst)| v == dst)
408                .map(|&(src, _)| src)
409        });
410        assert!(
411            sort.is_ok(),
412            "Did not expect cycle: {:?}",
413            sort.unwrap_err()
414        );
415
416        let sort = sort.unwrap();
417        println!("{:?}", sort);
418
419        let position: BTreeMap<_, _> = sort.iter().enumerate().map(|(i, &x)| (x, i)).collect();
420        for (src, dst) in edges.iter() {
421            assert!(position[src] < position[dst]);
422        }
423    }
424
425    #[test]
426    pub fn test_toposort_cycle() {
427        // https://commons.wikimedia.org/wiki/File:Directed_graph,_cyclic.svg
428        //          ┌────►C──────┐
429        //          │            │
430        //          │            ▼
431        // A───────►B            E ─────►F
432        //          ▲            │
433        //          │            │
434        //          └─────D◄─────┘
435        let edges = [
436            ('A', 'B'),
437            ('B', 'C'),
438            ('C', 'E'),
439            ('D', 'B'),
440            ('E', 'F'),
441            ('E', 'D'),
442        ];
443        let ids = edges
444            .iter()
445            .flat_map(|&(a, b)| [a, b])
446            .collect::<BTreeSet<_>>();
447        let cycle_rotations = BTreeSet::from_iter([
448            ['B', 'C', 'E', 'D'],
449            ['C', 'E', 'D', 'B'],
450            ['E', 'D', 'B', 'C'],
451            ['D', 'B', 'C', 'E'],
452        ]);
453
454        let permutations = ids.iter().copied().permutations(ids.len());
455        for permutation in permutations {
456            let result = topo_sort(permutation.iter().copied(), |v| {
457                edges
458                    .iter()
459                    .filter(move |&&(_, dst)| v == dst)
460                    .map(|&(src, _)| src)
461            });
462            assert!(result.is_err());
463            let cycle = result.unwrap_err();
464            assert!(
465                cycle_rotations.contains(&*cycle),
466                "cycle: {:?}, vertex order: {:?}",
467                cycle,
468                permutation
469            );
470        }
471    }
472
473    #[test]
474    pub fn test_validate_topo_sort_valid() {
475        // Simple linear chain: A -> B -> C -> D
476        // Valid topo sort: [A, B, C, D]
477        let edges: &[(i32, i32)] = &[(1, 2), (2, 3), (3, 4)];
478
479        let result = validate_topo_sort([1, 2, 3, 4], |v| {
480            edges
481                .iter()
482                .filter(move |&&(_src, dst)| v == dst)
483                .map(|&(src, _)| src)
484        });
485        assert_eq!(result, Ok(()));
486    }
487
488    #[test]
489    pub fn test_validate_topo_sort_invalid() {
490        // Edges: A -> B -> C -> D
491        // Invalid topo sort: [A, C, B, D] (C appears before B, but B is a predecessor of C)
492        let edges: &[(i32, i32)] = &[(1, 2), (2, 3), (3, 4)];
493
494        let result = validate_topo_sort([1, 3, 2, 4], |v| {
495            edges
496                .iter()
497                .filter(move |&&(_src, dst)| v == dst)
498                .map(|&(src, _)| src)
499        });
500        assert!(result.is_err());
501    }
502
503    #[test]
504    pub fn test_validate_topo_sort_diamond() {
505        // Diamond graph:
506        //     1
507        //    / \
508        //   2   3
509        //    \ /
510        //     4
511        let edges: &[(i32, i32)] = &[(1, 2), (1, 3), (2, 4), (3, 4)];
512
513        // Valid sort: [1, 2, 3, 4]
514        let result = validate_topo_sort([1, 2, 3, 4], |v| {
515            edges
516                .iter()
517                .filter(move |&&(_src, dst)| v == dst)
518                .map(|&(src, _)| src)
519        });
520        assert_eq!(result, Ok(()));
521
522        // Also valid: [1, 3, 2, 4]
523        let result = validate_topo_sort([1, 3, 2, 4], |v| {
524            edges
525                .iter()
526                .filter(move |&&(_src, dst)| v == dst)
527                .map(|&(src, _)| src)
528        });
529        assert_eq!(result, Ok(()));
530
531        // Invalid: [4, 1, 2, 3] (4 appears before its predecessors)
532        let result = validate_topo_sort([4, 1, 2, 3], |v| {
533            edges
534                .iter()
535                .filter(move |&&(_src, dst)| v == dst)
536                .map(|&(src, _)| src)
537        });
538        assert!(result.is_err());
539    }
540
541    #[test]
542    pub fn test_subgraph_merge_basic() {
543        let mut preds = SlotMap::new();
544
545        let a = preds.insert(vec![]);
546        let b = preds.insert(vec![]);
547        let c = preds.insert(vec![]);
548        let d = preds.insert(vec![]);
549        let e = preds.insert(vec![]);
550        let f = preds.insert(vec![]);
551
552        preds[b].push(a);
553        preds[c].push(b);
554        preds[d].push(b);
555        preds[e].push(c);
556        preds[e].push(d);
557        preds[f].push(e);
558
559        let mut merge = SubgraphMerge::new(
560            preds.keys(),
561            |v| preds[v].iter().copied(),
562            std::iter::empty(),
563        )
564        .unwrap();
565
566        assert!(merge.try_merge(a, a)); // No-op.
567        //        ┌──► C ──┐
568        //        │        ▼
569        // A ───► B        E ───► F
570        //        │        ▲
571        //        └──► D ──┘
572        assert!(merge.try_merge(b, c));
573        assert!(merge.try_merge(b, c)); // No-op.
574        // A ───► BC ────► E ───► F
575        //        │        ▲
576        //        └──► D ──┘
577        assert!(!merge.try_merge(c, e)); // Rejected due to `D` outside-cycle.
578
579        assert!(merge.try_merge(d, e));
580        assert!(merge.try_merge(c, e)); // Now valid since `D` is no longer outside.
581    }
582
583    #[test]
584    pub fn test_subgraph_merge_enemies() {
585        let mut preds = SlotMap::new();
586
587        // A ───► B ───► C ───► D
588        let a = preds.insert(vec![]);
589        let b = preds.insert(vec![]);
590        let c = preds.insert(vec![]);
591        let d = preds.insert(vec![]);
592
593        preds[b].push(a);
594        preds[c].push(b);
595        preds[d].push(c);
596
597        // B and C are enemies (must not merge).
598        let mut merge =
599            SubgraphMerge::new(preds.keys(), |v| preds[v].iter().copied(), [(b, c)]).unwrap();
600
601        // Direct enemy pair: rejected.
602        assert!(!merge.try_merge(b, c));
603
604        // Non-enemy pairs: allowed.
605        assert!(merge.try_merge(a, b));
606
607        // Now A and B are merged. C is still an enemy of the AB group.
608        assert!(!merge.try_merge(a, c));
609        assert!(!merge.try_merge(b, c));
610
611        // D is not an enemy of anyone.
612        assert!(merge.try_merge(c, d));
613
614        // After C and D merge, the CD group is still an enemy of AB.
615        assert!(!merge.try_merge(a, d));
616        assert!(!merge.try_merge(b, d));
617    }
618}