Skip to main content

miden_assembly/linker/
callgraph.rs

1use alloc::{
2    collections::{BTreeMap, BTreeSet, VecDeque},
3    vec::Vec,
4};
5
6use crate::GlobalItemIndex;
7
8/// Represents the inability to construct a topological ordering of the nodes in a [CallGraph]
9/// due to a cycle in the graph, which can happen due to recursion.
10#[derive(Debug)]
11pub struct CycleError(BTreeSet<GlobalItemIndex>);
12
13impl CycleError {
14    pub fn new(nodes: impl IntoIterator<Item = GlobalItemIndex>) -> Self {
15        Self(nodes.into_iter().collect())
16    }
17
18    pub fn into_node_ids(self) -> impl ExactSizeIterator<Item = GlobalItemIndex> {
19        self.0.into_iter()
20    }
21}
22
23// CALL GRAPH
24// ================================================================================================
25
26/// A [CallGraph] is a directed, acyclic graph which represents all of the edges between procedures
27/// formed by a caller/callee relationship.
28///
29/// More precisely, this graph can be used to perform the following analyses:
30///
31/// - What is the maximum call stack depth for a program?
32/// - Are there any recursive procedure calls?
33/// - Are there procedures which are unreachable from the program entrypoint?, i.e. dead code
34/// - What is the set of procedures which are reachable from a given procedure, and which of those
35///   are (un)conditionally called?
36///
37/// A [CallGraph] is the actual graph underpinning the conceptual "module graph" of the linker, and
38/// the two are intrinsically linked to one another (i.e. a [CallGraph] is meaningless without
39/// the corresponding [super::Linker] state).
40#[derive(Default, Clone)]
41pub struct CallGraph {
42    /// The adjacency matrix for procedures in the call graph
43    nodes: BTreeMap<GlobalItemIndex, Vec<GlobalItemIndex>>,
44}
45
46impl CallGraph {
47    /// Gets the set of edges from the given caller to its callees in the graph.
48    pub fn out_edges(&self, gid: GlobalItemIndex) -> &[GlobalItemIndex] {
49        self.nodes.get(&gid).map(Vec::as_slice).unwrap_or(&[])
50    }
51
52    /// Inserts a node in the graph for `id`, if not already present.
53    ///
54    /// Returns the set of [GlobalItemIndex] which are the outbound neighbors of `id` in the
55    /// graph, i.e. the callees of a call-like instruction.
56    pub fn get_or_insert_node(&mut self, id: GlobalItemIndex) -> &mut Vec<GlobalItemIndex> {
57        self.nodes.entry(id).or_default()
58    }
59
60    /// Add an edge in the call graph from `caller` to `callee`.
61    ///
62    /// This operation is unchecked, i.e. it is possible to introduce cycles in the graph using it.
63    /// As a result, it is essential that the caller either know that adding the edge does _not_
64    /// introduce a cycle, or that [Self::toposort] is run once the graph is built, in order to
65    /// verify that the graph is valid and has no cycles.
66    ///
67    /// Returns an error if adding the edge would introduce a trivial self-cycle.
68    pub fn add_edge(
69        &mut self,
70        caller: GlobalItemIndex,
71        callee: GlobalItemIndex,
72    ) -> Result<(), CycleError> {
73        if caller == callee {
74            return Err(CycleError::new([caller]));
75        }
76
77        // Make sure the callee is in the graph
78        self.get_or_insert_node(callee);
79        // Make sure the caller is in the graph
80        let callees = self.get_or_insert_node(caller);
81        // If the caller already references the callee, we're done
82        if callees.contains(&callee) {
83            return Ok(());
84        }
85
86        callees.push(callee);
87        Ok(())
88    }
89
90    /// Returns the number of predecessors of `id` in the graph, i.e.
91    /// the number of procedures which call `id`.
92    pub fn num_predecessors(&self, id: GlobalItemIndex) -> usize {
93        self.nodes.iter().filter(|(_, out_edges)| out_edges.contains(&id)).count()
94    }
95
96    /// Construct the topological ordering of all nodes in the call graph.
97    ///
98    /// Uses Kahn's algorithm with pre-computed in-degrees for O(V + E) complexity.
99    ///
100    /// Returns `Err` if a cycle is detected in the graph
101    pub fn toposort(&self) -> Result<Vec<GlobalItemIndex>, CycleError> {
102        if self.nodes.is_empty() {
103            return Ok(vec![]);
104        }
105
106        let num_nodes = self.nodes.len();
107        let mut output = Vec::with_capacity(num_nodes);
108
109        // Compute in-degree for each node: O(V + E)
110        let mut in_degree: BTreeMap<GlobalItemIndex, usize> =
111            self.nodes.keys().map(|&k| (k, 0)).collect();
112        for out_edges in self.nodes.values() {
113            for &succ in out_edges {
114                *in_degree.entry(succ).or_default() += 1;
115            }
116        }
117
118        // Seed the queue with all zero-in-degree nodes: O(V)
119        let mut queue: VecDeque<GlobalItemIndex> =
120            in_degree.iter().filter(|&(_, &deg)| deg == 0).map(|(&n, _)| n).collect();
121
122        // Kahn's algorithm: process each node exactly once, each edge exactly once → O(V + E)
123        while let Some(id) = queue.pop_front() {
124            output.push(id);
125            for &mid in self.out_edges(id) {
126                let deg = in_degree.get_mut(&mid).unwrap();
127                *deg -= 1;
128                if *deg == 0 {
129                    queue.push_back(mid);
130                }
131            }
132        }
133
134        // If not all nodes were visited, the remaining nodes participate in cycles
135        if output.len() != num_nodes {
136            let visited: BTreeSet<GlobalItemIndex> = output.iter().copied().collect();
137            let mut in_cycle = BTreeSet::default();
138            for (&n, out_edges) in self.nodes.iter() {
139                if visited.contains(&n) {
140                    continue;
141                }
142                in_cycle.insert(n);
143                for &succ in out_edges {
144                    if !visited.contains(&succ) {
145                        in_cycle.insert(succ);
146                    }
147                }
148            }
149            Err(CycleError(in_cycle))
150        } else {
151            Ok(output)
152        }
153    }
154
155    /// Gets a new graph which is a subgraph of `self` containing all of the nodes reachable from
156    /// `root`, and nothing else.
157    pub fn subgraph(&self, root: GlobalItemIndex) -> Self {
158        let mut worklist = VecDeque::from_iter([root]);
159        let mut graph = Self::default();
160        let mut visited = BTreeSet::default();
161
162        while let Some(gid) = worklist.pop_front() {
163            if !visited.insert(gid) {
164                continue;
165            }
166
167            let new_successors = graph.get_or_insert_node(gid);
168            let prev_successors = self.out_edges(gid);
169            worklist.extend(prev_successors.iter().cloned());
170            new_successors.extend_from_slice(prev_successors);
171        }
172
173        graph
174    }
175
176    /// Computes the set of nodes in this graph which can reach `root`.
177    fn reverse_reachable(&self, root: GlobalItemIndex) -> BTreeSet<GlobalItemIndex> {
178        // Build reverse adjacency map: O(V + E)
179        let mut predecessors: BTreeMap<GlobalItemIndex, Vec<GlobalItemIndex>> =
180            self.nodes.keys().map(|&k| (k, Vec::new())).collect();
181        for (&node, out_edges) in self.nodes.iter() {
182            for &succ in out_edges {
183                predecessors.entry(succ).or_default().push(node);
184            }
185        }
186
187        // BFS on reverse graph: O(V + E)
188        let mut worklist = VecDeque::from_iter([root]);
189        let mut visited = BTreeSet::default();
190
191        while let Some(gid) = worklist.pop_front() {
192            if !visited.insert(gid) {
193                continue;
194            }
195
196            if let Some(preds) = predecessors.get(&gid) {
197                worklist.extend(preds.iter().copied());
198            }
199        }
200
201        visited
202    }
203
204    /// Constructs the topological ordering of nodes in the call graph, for which `caller` is an
205    /// ancestor.
206    ///
207    /// Uses Kahn's algorithm with pre-computed in-degrees for O(V + E) complexity.
208    ///
209    /// # Errors
210    /// Returns an error if a cycle is detected in the graph.
211    pub fn toposort_caller(
212        &self,
213        caller: GlobalItemIndex,
214    ) -> Result<Vec<GlobalItemIndex>, CycleError> {
215        // Build a subgraph of `self` containing only those nodes reachable from `caller`
216        let subgraph = self.subgraph(caller);
217        let num_nodes = subgraph.nodes.len();
218        let mut output = Vec::with_capacity(num_nodes);
219
220        // Compute in-degree for each node in the subgraph: O(V + E)
221        let mut in_degree: BTreeMap<GlobalItemIndex, usize> =
222            subgraph.nodes.keys().map(|&k| (k, 0)).collect();
223        for out_edges in subgraph.nodes.values() {
224            for &succ in out_edges {
225                *in_degree.entry(succ).or_default() += 1;
226            }
227        }
228
229        // Check if any cycle closes back to `caller` (i.e. caller has predecessors in its
230        // own reachable subgraph)
231        let caller_has_predecessors = in_degree.get(&caller).copied().unwrap_or(0) > 0;
232
233        // Force `caller` as the root by zeroing its in-degree (equivalent to removing
234        // all back-edges to `caller`)
235        in_degree.insert(caller, 0);
236
237        // Seed queue with `caller` as the sole root
238        let mut queue = VecDeque::from_iter([caller]);
239
240        // Kahn's algorithm: O(V + E)
241        while let Some(id) = queue.pop_front() {
242            output.push(id);
243            for &mid in subgraph.out_edges(id) {
244                // Skip back-edges to caller (already processed as root)
245                if mid == caller {
246                    continue;
247                }
248                let deg = in_degree.get_mut(&mid).unwrap();
249                *deg -= 1;
250                if *deg == 0 {
251                    queue.push_back(mid);
252                }
253            }
254        }
255
256        // Detect cycles: either caller had predecessors in its subgraph (a cycle closes
257        // back to it), or not all nodes were reachable (an internal cycle)
258        let has_cycle = caller_has_predecessors || output.len() != num_nodes;
259        if has_cycle {
260            let visited: BTreeSet<GlobalItemIndex> = output.iter().copied().collect();
261            let mut in_cycle = BTreeSet::default();
262
263            // Collect nodes not processed by the sort (they're in internal cycles)
264            for (&n, out_edges) in subgraph.nodes.iter() {
265                if !visited.contains(&n) {
266                    in_cycle.insert(n);
267                    for &succ in out_edges {
268                        if !visited.contains(&succ) {
269                            in_cycle.insert(succ);
270                        }
271                    }
272                }
273            }
274
275            // If caller has back-edges, include all nodes participating in the cycle
276            // through caller
277            if caller_has_predecessors {
278                in_cycle.extend(subgraph.reverse_reachable(caller));
279            }
280
281            Err(CycleError(in_cycle))
282        } else {
283            Ok(output)
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use crate::{GlobalItemIndex, ModuleIndex, ast::ItemIndex};
292
293    const A: ModuleIndex = ModuleIndex::const_new(1);
294    const B: ModuleIndex = ModuleIndex::const_new(2);
295    const P1: ItemIndex = ItemIndex::const_new(1);
296    const P2: ItemIndex = ItemIndex::const_new(2);
297    const P3: ItemIndex = ItemIndex::const_new(3);
298    const A1: GlobalItemIndex = GlobalItemIndex { module: A, index: P1 };
299    const A2: GlobalItemIndex = GlobalItemIndex { module: A, index: P2 };
300    const A3: GlobalItemIndex = GlobalItemIndex { module: A, index: P3 };
301    const B1: GlobalItemIndex = GlobalItemIndex { module: B, index: P1 };
302    const B2: GlobalItemIndex = GlobalItemIndex { module: B, index: P2 };
303    const B3: GlobalItemIndex = GlobalItemIndex { module: B, index: P3 };
304
305    #[test]
306    fn callgraph_add_edge() {
307        let graph = callgraph_simple();
308
309        // Verify the graph structure
310        assert_eq!(graph.num_predecessors(A1), 0);
311        assert_eq!(graph.num_predecessors(B1), 0);
312        assert_eq!(graph.num_predecessors(A2), 1);
313        assert_eq!(graph.num_predecessors(B2), 2);
314        assert_eq!(graph.num_predecessors(B3), 1);
315        assert_eq!(graph.num_predecessors(A3), 2);
316
317        assert_eq!(graph.out_edges(A1), &[A2]);
318        assert_eq!(graph.out_edges(B1), &[B2]);
319        assert_eq!(graph.out_edges(A2), &[B2, A3]);
320        assert_eq!(graph.out_edges(B2), &[B3]);
321        assert_eq!(graph.out_edges(A3), &[]);
322        assert_eq!(graph.out_edges(B3), &[A3]);
323    }
324
325    #[test]
326    fn callgraph_add_edge_with_cycle() {
327        let graph = callgraph_cycle();
328
329        // Verify the graph structure
330        assert_eq!(graph.num_predecessors(A1), 0);
331        assert_eq!(graph.num_predecessors(B1), 0);
332        assert_eq!(graph.num_predecessors(A2), 2);
333        assert_eq!(graph.num_predecessors(B2), 2);
334        assert_eq!(graph.num_predecessors(B3), 1);
335        assert_eq!(graph.num_predecessors(A3), 1);
336
337        assert_eq!(graph.out_edges(A1), &[A2]);
338        assert_eq!(graph.out_edges(B1), &[B2]);
339        assert_eq!(graph.out_edges(A2), &[B2]);
340        assert_eq!(graph.out_edges(B2), &[B3]);
341        assert_eq!(graph.out_edges(A3), &[A2]);
342        assert_eq!(graph.out_edges(B3), &[A3]);
343    }
344
345    #[test]
346    fn callgraph_subgraph() {
347        let graph = callgraph_simple();
348        let subgraph = graph.subgraph(A2);
349
350        assert_eq!(subgraph.nodes.keys().copied().collect::<Vec<_>>(), vec![A2, A3, B2, B3]);
351    }
352
353    #[test]
354    fn callgraph_with_cycle_subgraph() {
355        let graph = callgraph_cycle();
356        let subgraph = graph.subgraph(A2);
357
358        assert_eq!(subgraph.nodes.keys().copied().collect::<Vec<_>>(), vec![A2, A3, B2, B3]);
359    }
360
361    #[test]
362    fn callgraph_toposort() {
363        let graph = callgraph_simple();
364
365        let sorted = graph.toposort().expect("expected valid topological ordering");
366        assert_eq!(sorted.as_slice(), &[A1, B1, A2, B2, B3, A3]);
367    }
368
369    #[test]
370    fn callgraph_toposort_caller() {
371        let graph = callgraph_simple();
372
373        let sorted = graph.toposort_caller(A2).expect("expected valid topological ordering");
374        assert_eq!(sorted.as_slice(), &[A2, B2, B3, A3]);
375    }
376
377    #[test]
378    fn callgraph_with_cycle_toposort() {
379        let graph = callgraph_cycle();
380
381        let err = graph.toposort().expect_err("expected topological sort to fail with cycle");
382        assert_eq!(err.0.into_iter().collect::<Vec<_>>(), &[A2, A3, B2, B3]);
383    }
384
385    #[test]
386    fn callgraph_toposort_caller_with_reachable_cycle() {
387        let graph = callgraph_cycle();
388
389        let err = graph
390            .toposort_caller(A1)
391            .expect_err("expected toposort_caller to fail when a reachable cycle exists");
392        assert_eq!(err.0.into_iter().collect::<Vec<_>>(), &[A2, A3, B2, B3]);
393    }
394
395    #[test]
396    fn callgraph_toposort_caller_root_closing_cycle() {
397        let graph = callgraph_cycle();
398
399        let err = graph
400            .toposort_caller(A2)
401            .expect_err("expected toposort_caller to detect cycle closing back into root");
402        assert_eq!(err.0.into_iter().collect::<Vec<_>>(), &[A2, A3, B2, B3]);
403    }
404
405    #[test]
406    fn callgraph_add_edge_with_self_cycle_is_error() {
407        let mut graph = CallGraph::default();
408
409        let err = graph.add_edge(A1, A1).expect_err("expected self-edge to be rejected");
410        assert_eq!(err.0.into_iter().collect::<Vec<_>>(), &[A1]);
411    }
412
413    #[test]
414    fn callgraph_rootless_cycle_toposort_is_error() {
415        let mut graph = CallGraph::default();
416        graph.add_edge(A1, B1).expect("A1 -> B1 must be accepted");
417        graph.add_edge(B1, A1).expect("B1 -> A1 must be accepted");
418
419        let err = graph.toposort().expect_err("expected topological sort to fail with cycle");
420        assert_eq!(err.0.into_iter().collect::<Vec<_>>(), &[A1, B1]);
421    }
422
423    #[test]
424    fn callgraph_toposort_whole_graph_cycle_without_roots() {
425        let graph = callgraph_cycle_without_roots();
426        let err = graph.toposort().expect_err(
427            "expected topological sort to fail when every node is blocked behind a cycle",
428        );
429        assert_eq!(err.0.into_iter().collect::<Vec<_>>(), &[A1, A2, A3]);
430    }
431
432    /// a::a1 -> a::a2 -> a::a3
433    ///            |        ^
434    ///            v        |
435    /// b::b1 -> b::b2 -> b::b3
436    fn callgraph_simple() -> CallGraph {
437        // Construct the graph
438        let mut graph = CallGraph::default();
439        graph.add_edge(A1, A2).expect("A1 -> A2 must be accepted");
440        graph.add_edge(B1, B2).expect("B1 -> B2 must be accepted");
441        graph.add_edge(A2, B2).expect("A2 -> B2 must be accepted");
442        graph.add_edge(A2, A3).expect("A2 -> A3 must be accepted");
443        graph.add_edge(B2, B3).expect("B2 -> B3 must be accepted");
444        graph.add_edge(B3, A3).expect("B3 -> A3 must be accepted");
445
446        graph
447    }
448
449    /// a::a1 -> a::a2 <- a::a3
450    ///            |        ^
451    ///            v        |
452    /// b::b1 -> b::b2 -> b::b3
453    fn callgraph_cycle() -> CallGraph {
454        // Construct the graph
455        let mut graph = CallGraph::default();
456        graph.add_edge(A1, A2).expect("A1 -> A2 must be accepted");
457        graph.add_edge(B1, B2).expect("B1 -> B2 must be accepted");
458        graph.add_edge(A2, B2).expect("A2 -> B2 must be accepted");
459        graph.add_edge(B2, B3).expect("B2 -> B3 must be accepted");
460        graph.add_edge(B3, A3).expect("B3 -> A3 must be accepted");
461        graph.add_edge(A3, A2).expect("A3 -> A2 must be accepted");
462
463        graph
464    }
465
466    /// a::a1 -> a::a2 -> a::a3
467    ///   ^                 |
468    ///   +-----------------+
469    ///
470    /// Every node has in-degree 1, so Kahn's algorithm starts with an empty queue.
471    fn callgraph_cycle_without_roots() -> CallGraph {
472        let mut graph = CallGraph::default();
473        graph.add_edge(A1, A2).expect("A1 -> A2 must be accepted");
474        graph.add_edge(A2, A3).expect("A2 -> A3 must be accepted");
475        graph.add_edge(A3, A1).expect("A3 -> A1 must be accepted");
476
477        graph
478    }
479}