charon_lib/transform/add_missing_info/sccs.rs
1use std::fmt::Debug;
2use std::mem;
3use std::vec::Vec;
4
5use itertools::Itertools;
6use petgraph::algo::scc::tarjan_scc;
7use petgraph::graphmap::NodeTrait;
8use petgraph::prelude::DiGraphMap;
9use petgraph::visit::{DfsPostOrder, Walker};
10
11use crate::ast::*;
12
13/// Compute the SCCs (Strongly Connected Components) of a set of identifiers, where the order of
14/// the SCCs and the order of the identifiers inside the SCCs attempt to respect as much as
15/// possible the sort order given. The provided `sort_by` should be cheap.
16///
17/// This is used to generate the translated definitions in a consistent and stable manner. For
18/// instance, let's say we extract 4 definitions `f`, `g1`, `g2` and `h`, where:
19/// - `g1` and `g2` are mutually recursive
20/// - `h` calls `g1`
21///
22/// When translating those functions, we group together the mutually recursive
23/// functions, because they have to be extracted in one single group, and thus
24/// apply Tarjan's algorithm on the call graph to find out which functions are
25/// mutually recursive.
26/// The implementation of Tarjan's algorithm we use gives us the Strongly Connected
27/// SCCs of the call graph in an arbitrary order, so we can have:
28/// `[[f], [g1, g2], [h]]`, but also `[[h], [f], [g2, g1]]`, etc.
29///
30/// If the user defined those functions in the order: `f, g1, h, g2`, we want
31/// to reorder them into: `f, g1, g2, h`, so that we can translate the mutually
32/// recursive functions together, while performing a minimal amount of reordering.
33/// And if reordering is not needed, because the user defined those functions
34/// in the order `f, g1, g2, h`, or `g1, g2, f, h` or ... then we want to translate
35/// them in this exact order.
36///
37/// The order in which Tarjan's algorithm generates the SCCs is arbitrary, while we want to keep as
38/// much as possible the original order (the order in which the user generated the ids). For this,
39/// we iterate through the ordered ids and try to add the SCC containing the id to a new list of
40/// SCCs Of course, this SCC might have dependencies, so we need to add the dependencies first (in
41/// which case we have reordering to do). This is what this function does: we add an SCC and its
42/// dependencies to the list of reordered SCCs by doing a depth-first search.
43pub fn ordered_scc<Id: NodeTrait + Debug, O: Ord>(
44 graph: &DiGraphMap<Id, ()>,
45 sort_by: impl Fn(&Id) -> O,
46) -> Vec<Vec<Id>> {
47 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
48 struct SccId(usize);
49
50 let mut sccs = tarjan_scc(graph);
51
52 // Reorder the identifiers inside the SCCs.
53 for scc in sccs.iter_mut() {
54 scc.sort_by_key(&sort_by);
55 }
56
57 // Map the nodes to their SCC index. The map is sorted.
58 let id_to_scc: SeqHashMap<Id, SccId> = sccs
59 .iter()
60 .enumerate()
61 .map(|(i, nodes)| (SccId(i), nodes))
62 .flat_map(|(scc_id, scc_nodes)| scc_nodes.iter().map(move |node| (*node, scc_id)))
63 .sorted_by_key(|(node, _)| sort_by(node))
64 .collect();
65
66 // Compute the graph of the sccs, where there is an edge between sccs if there's an edge
67 // between some nodes of each scc.
68 let mut scc_graph: DiGraphMap<SccId, ()> = DiGraphMap::new();
69 // Add sccs in the order of their earliest element.
70 for (_, &scc_id) in &id_to_scc {
71 scc_graph.add_node(scc_id);
72 }
73 for (scc_id, scc_nodes) in sccs.iter().enumerate().map(|(i, nodes)| (SccId(i), nodes)) {
74 // Add the scc neighbors in the desired node order.
75 for neighbor_scc_id in scc_nodes
76 .iter()
77 .flat_map(|node| graph.neighbors(*node))
78 .sorted_by_key(&sort_by)
79 .map(|neighbor| *id_to_scc.get(&neighbor).unwrap())
80 {
81 scc_graph.add_edge(scc_id, neighbor_scc_id, ());
82 }
83 }
84
85 // Reorder the SCCs among themselves by a post-order visit of the graph: for each scc (in the
86 // order of their earliest node in the desired order), we list it and all the sccs it depends
87 // on we haven't yet included, in postorder. This guarantees that the final order is a
88 // topological sort for the scc DAG. Moreover this will explore other sccs in order too because
89 // we sorted the list of neighbors.
90 let mut reordered_sccs_ids: SeqHashSet<SccId> = SeqHashSet::new();
91 for scc_id in scc_graph.nodes() {
92 for scc_id in DfsPostOrder::new(&scc_graph, scc_id).iter(&scc_graph) {
93 reordered_sccs_ids.insert(scc_id);
94 }
95 }
96
97 // Output the fully reordered SCCs.
98 reordered_sccs_ids
99 .into_iter()
100 .map(|scc_id| mem::take(&mut sccs[scc_id.0]))
101 .collect()
102}
103
104#[cfg(test)]
105mod tests {
106 use petgraph::prelude::DiGraphMap;
107
108 #[test]
109 fn test_reorder_sccs1() {
110 let mut graph = DiGraphMap::new();
111
112 // First nontrivial cc
113 graph.add_edge(1, 2, ());
114 graph.add_edge(2, 1, ());
115 // Second nontrivial cc
116 graph.add_edge(3, 4, ());
117 graph.add_edge(4, 5, ());
118 graph.add_edge(5, 3, ());
119 // Other deps
120 graph.add_edge(1, 0, ());
121 graph.add_edge(0, 3, ());
122 graph.add_edge(1, 3, ());
123
124 let reordered = super::ordered_scc(&graph, |i| *i);
125
126 assert_eq!(reordered, vec![vec![3, 4, 5], vec![0], vec![1, 2],]);
127 }
128}