Skip to main content

trueno_graph/algorithms/
traversal.rs

1//! Graph traversal algorithms (BFS, DFS, `find_callers`)
2//!
3//! Based on Ligra (Shun & Blelloch, `PPoPP` 2013) frontier-based traversal patterns.
4
5use crate::storage::CsrGraph;
6use crate::NodeId;
7use anyhow::Result;
8use std::collections::{HashSet, VecDeque};
9
10/// Find all functions that transitively call the target function
11///
12/// Uses reverse BFS from target node to find all callers within specified depth.
13///
14/// # Arguments
15///
16/// * `graph` - CSR graph representation
17/// * `target` - Target node to find callers for
18/// * `max_depth` - Maximum traversal depth (0 = direct callers only)
19///
20/// # Returns
21///
22/// Set of all node IDs that call the target (directly or transitively)
23///
24/// # Errors
25///
26/// Returns an error if graph operations fail (e.g., invalid node access).
27///
28/// # Example
29///
30/// ```
31/// use trueno_graph::{CsrGraph, NodeId, find_callers};
32///
33/// let mut graph = CsrGraph::new();
34/// graph.add_edge(NodeId(0), NodeId(2), 1.0).unwrap(); // main → validate
35/// graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap(); // parse → validate
36///
37/// let callers = find_callers(&graph, NodeId(2), 10).unwrap();
38/// assert!(callers.contains(&0)); // main calls validate
39/// assert!(callers.contains(&1)); // parse calls validate
40/// ```
41pub fn find_callers(graph: &CsrGraph, target: NodeId, max_depth: usize) -> Result<Vec<u32>> {
42    let mut visited = HashSet::new();
43    let mut frontier = VecDeque::new();
44    let mut depth = 0;
45
46    // Start BFS from target node
47    frontier.push_back(target.0);
48    visited.insert(target.0);
49
50    while !frontier.is_empty() && depth < max_depth {
51        let level_size = frontier.len();
52
53        for _ in 0..level_size {
54            if let Some(current) = frontier.pop_front() {
55                // Find all incoming neighbors (callers)
56                let callers = graph.incoming_neighbors(NodeId(current))?;
57
58                for &caller in callers {
59                    if !visited.contains(&caller) {
60                        visited.insert(caller);
61                        frontier.push_back(caller);
62                    }
63                }
64            }
65        }
66
67        depth += 1;
68    }
69
70    // Remove target node from result (we want callers, not the target itself)
71    visited.remove(&target.0);
72
73    Ok(visited.into_iter().collect())
74}
75
76/// Breadth-First Search from source node
77///
78/// Standard BFS traversal returning all reachable nodes.
79///
80/// # Arguments
81///
82/// * `graph` - CSR graph representation
83/// * `source` - Starting node for BFS
84///
85/// # Returns
86///
87/// Vec of all node IDs reachable from source
88///
89/// # Errors
90///
91/// Returns an error if graph operations fail (e.g., invalid node access).
92///
93/// # Example
94///
95/// ```
96/// use trueno_graph::{CsrGraph, NodeId, bfs};
97///
98/// let mut graph = CsrGraph::new();
99/// graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
100/// graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
101///
102/// let reachable = bfs(&graph, NodeId(0)).unwrap();
103/// assert_eq!(reachable.len(), 3); // All 3 nodes reachable
104/// ```
105pub fn bfs(graph: &CsrGraph, source: NodeId) -> Result<Vec<u32>> {
106    let mut visited = HashSet::new();
107    let mut queue = VecDeque::new();
108
109    queue.push_back(source.0);
110    visited.insert(source.0);
111
112    while let Some(current) = queue.pop_front() {
113        // Get outgoing neighbors
114        let neighbors = graph.outgoing_neighbors(NodeId(current))?;
115
116        for &neighbor in neighbors {
117            if !visited.contains(&neighbor) {
118                visited.insert(neighbor);
119                queue.push_back(neighbor);
120            }
121        }
122    }
123
124    Ok(visited.into_iter().collect())
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_find_callers_direct() {
133        // Graph: 0 → 2, 1 → 2 (both call 2)
134        let edges = vec![(NodeId(0), NodeId(2), 1.0), (NodeId(1), NodeId(2), 1.0)];
135        let graph = CsrGraph::from_edge_list(&edges).unwrap();
136
137        let callers = find_callers(&graph, NodeId(2), 1).unwrap();
138        assert_eq!(callers.len(), 2);
139        assert!(callers.contains(&0));
140        assert!(callers.contains(&1));
141    }
142
143    #[test]
144    fn test_find_callers_transitive() {
145        // Graph: 0 → 1 → 2 (0 transitively calls 2)
146        let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(1), NodeId(2), 1.0)];
147        let graph = CsrGraph::from_edge_list(&edges).unwrap();
148
149        let callers = find_callers(&graph, NodeId(2), 10).unwrap();
150        assert_eq!(callers.len(), 2);
151        assert!(callers.contains(&0));
152        assert!(callers.contains(&1));
153    }
154
155    #[test]
156    fn test_find_callers_depth_limit() {
157        // Graph: 0 → 1 → 2 → 3
158        let edges = vec![
159            (NodeId(0), NodeId(1), 1.0),
160            (NodeId(1), NodeId(2), 1.0),
161            (NodeId(2), NodeId(3), 1.0),
162        ];
163        let graph = CsrGraph::from_edge_list(&edges).unwrap();
164
165        // Depth 1: only node 2 calls node 3
166        let callers = find_callers(&graph, NodeId(3), 1).unwrap();
167        assert_eq!(callers.len(), 1);
168        assert!(callers.contains(&2));
169
170        // Depth 2: nodes 1 and 2 call node 3
171        let callers = find_callers(&graph, NodeId(3), 2).unwrap();
172        assert_eq!(callers.len(), 2);
173        assert!(callers.contains(&1));
174        assert!(callers.contains(&2));
175
176        // Depth 10: all nodes call node 3
177        let callers = find_callers(&graph, NodeId(3), 10).unwrap();
178        assert_eq!(callers.len(), 3);
179        assert!(callers.contains(&0));
180        assert!(callers.contains(&1));
181        assert!(callers.contains(&2));
182    }
183
184    #[test]
185    fn test_bfs_simple() {
186        let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(1), NodeId(2), 1.0)];
187        let graph = CsrGraph::from_edge_list(&edges).unwrap();
188
189        let reachable = bfs(&graph, NodeId(0)).unwrap();
190        assert_eq!(reachable.len(), 3);
191        assert!(reachable.contains(&0));
192        assert!(reachable.contains(&1));
193        assert!(reachable.contains(&2));
194    }
195
196    #[test]
197    fn test_bfs_disconnected() {
198        let edges = vec![
199            (NodeId(0), NodeId(1), 1.0),
200            (NodeId(2), NodeId(3), 1.0), // Disconnected component
201        ];
202        let graph = CsrGraph::from_edge_list(&edges).unwrap();
203
204        let reachable = bfs(&graph, NodeId(0)).unwrap();
205        assert_eq!(reachable.len(), 2); // Only nodes 0 and 1
206        assert!(reachable.contains(&0));
207        assert!(reachable.contains(&1));
208        assert!(!reachable.contains(&2)); // Not reachable
209    }
210}