Skip to main content

geographdb_core/algorithms/
transitive.rs

1//! Transitive Closure and Reduction
2//!
3//! Computes reachability information for graphs.
4//!
5//! # Definitions
6//!
7//! - **Transitive Closure**: For each node, which nodes are reachable
8//! - **Transitive Reduction**: Minimal graph with same reachability
9//!
10//! # Applications
11//!
12//! - Reachability queries (can A reach B?)
13//! - Impact analysis (what is affected by this change?)
14//! - Graph simplification (remove redundant edges)
15//! - Dependency analysis
16
17use crate::algorithms::astar::CfgGraphNode;
18use std::collections::{HashMap, HashSet};
19
20/// Result of transitive closure computation
21#[derive(Debug, Clone)]
22pub struct ReachabilityResult {
23    /// For each node, set of reachable nodes
24    pub reachable: HashMap<u64, HashSet<u64>>,
25    /// Total number of reachability pairs
26    pub total_pairs: usize,
27}
28
29/// Compute transitive closure using BFS from each node
30///
31/// # Arguments
32/// * `nodes` - CFG nodes
33///
34/// # Returns
35/// ReachabilityResult with reachability information
36///
37/// # Complexity
38/// O(n * (n + e)) time, O(n²) space
39pub fn transitive_closure(nodes: &[CfgGraphNode]) -> ReachabilityResult {
40    let node_map: HashMap<u64, &CfgGraphNode> = nodes.iter().map(|n| (n.id, n)).collect();
41    let mut reachable: HashMap<u64, HashSet<u64>> = HashMap::new();
42    let mut total_pairs = 0;
43
44    for node in nodes {
45        let mut reachable_from_node: HashSet<u64> = HashSet::new();
46        let mut worklist = vec![node.id];
47
48        while let Some(current) = worklist.pop() {
49            if let Some(current_node) = node_map.get(&current) {
50                for &succ in &current_node.successors {
51                    if !reachable_from_node.contains(&succ) {
52                        reachable_from_node.insert(succ);
53                        worklist.push(succ);
54                    }
55                }
56            }
57        }
58
59        total_pairs += reachable_from_node.len();
60        reachable.insert(node.id, reachable_from_node);
61    }
62
63    ReachabilityResult {
64        reachable,
65        total_pairs,
66    }
67}
68
69/// Compute transitive reduction
70///
71/// Removes redundant edges while preserving reachability.
72/// An edge (u, v) is redundant if there's another path from u to v.
73///
74/// # Arguments
75/// * `nodes` - CFG nodes
76///
77/// # Returns
78/// Vector of edges to keep (non-redundant edges)
79///
80/// # Complexity
81/// O(n * (n + e)) time
82pub fn transitive_reduction(nodes: &[CfgGraphNode]) -> Vec<(u64, u64)> {
83    let closure = transitive_closure(nodes);
84    let mut keep_edges: Vec<(u64, u64)> = Vec::new();
85
86    for node in nodes {
87        for &succ in &node.successors {
88            // Check if there's another path from node to succ
89            let mut has_alternative_path = false;
90
91            for &other_succ in &node.successors {
92                if other_succ != succ {
93                    // Check if succ is reachable from other_succ
94                    if let Some(other_reachable) = closure.reachable.get(&other_succ) {
95                        if other_reachable.contains(&succ) {
96                            has_alternative_path = true;
97                            break;
98                        }
99                    }
100                }
101            }
102
103            if !has_alternative_path {
104                keep_edges.push((node.id, succ));
105            }
106        }
107    }
108
109    keep_edges
110}
111
112/// Check if node b is reachable from node a
113pub fn is_reachable(from: u64, to: u64, closure: &ReachabilityResult) -> bool {
114    closure
115        .reachable
116        .get(&from)
117        .map(|r| r.contains(&to))
118        .unwrap_or(false)
119}
120
121/// Get all nodes reachable from a given node
122pub fn get_reachable_from(node: u64, closure: &ReachabilityResult) -> HashSet<u64> {
123    closure.reachable.get(&node).cloned().unwrap_or_default()
124}
125
126/// Get all nodes that can reach a given node (reverse reachability)
127pub fn get_reachable_to(node: u64, closure: &ReachabilityResult) -> HashSet<u64> {
128    let mut result = HashSet::new();
129    for (&from, reachable) in &closure.reachable {
130        if reachable.contains(&node) {
131            result.insert(from);
132        }
133    }
134    result
135}
136
137/// Count ancestors of a node (nodes that can reach it)
138pub fn count_ancestors(node: u64, closure: &ReachabilityResult) -> usize {
139    get_reachable_to(node, closure).len()
140}
141
142/// Count descendants of a node (nodes it can reach)
143pub fn count_descendants(node: u64, closure: &ReachabilityResult) -> usize {
144    get_reachable_from(node, closure).len()
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_transitive_closure_linear() {
153        // Linear: 0 -> 1 -> 2 -> 3
154        let nodes = vec![
155            CfgGraphNode {
156                id: 0,
157                x: 0.0,
158                y: 0.0,
159                z: 0.0,
160                successors: vec![1],
161            },
162            CfgGraphNode {
163                id: 1,
164                x: 1.0,
165                y: 0.0,
166                z: 0.0,
167                successors: vec![2],
168            },
169            CfgGraphNode {
170                id: 2,
171                x: 2.0,
172                y: 0.0,
173                z: 0.0,
174                successors: vec![3],
175            },
176            CfgGraphNode {
177                id: 3,
178                x: 3.0,
179                y: 0.0,
180                z: 0.0,
181                successors: vec![],
182            },
183        ];
184
185        let result = transitive_closure(&nodes);
186
187        assert_eq!(result.reachable[&0].len(), 3);
188        assert!(result.reachable[&0].contains(&1));
189        assert!(result.reachable[&0].contains(&3));
190        assert_eq!(result.reachable[&3].len(), 0);
191    }
192
193    #[test]
194    fn test_transitive_closure_branching() {
195        // Diamond: 0 -> 1 -> 3, 0 -> 2 -> 3
196        let nodes = vec![
197            CfgGraphNode {
198                id: 0,
199                x: 0.0,
200                y: 0.0,
201                z: 0.0,
202                successors: vec![1, 2],
203            },
204            CfgGraphNode {
205                id: 1,
206                x: 1.0,
207                y: 0.0,
208                z: 0.0,
209                successors: vec![3],
210            },
211            CfgGraphNode {
212                id: 2,
213                x: 2.0,
214                y: 0.0,
215                z: 0.0,
216                successors: vec![3],
217            },
218            CfgGraphNode {
219                id: 3,
220                x: 3.0,
221                y: 0.0,
222                z: 0.0,
223                successors: vec![],
224            },
225        ];
226
227        let result = transitive_closure(&nodes);
228
229        assert_eq!(result.reachable[&0].len(), 3);
230        assert!(result.reachable[&0].contains(&1));
231        assert!(result.reachable[&0].contains(&2));
232        assert!(result.reachable[&0].contains(&3));
233    }
234
235    #[test]
236    fn test_transitive_reduction_linear() {
237        // Linear: 0 -> 1 -> 2 -> 3 (no redundant edges)
238        let nodes = vec![
239            CfgGraphNode {
240                id: 0,
241                x: 0.0,
242                y: 0.0,
243                z: 0.0,
244                successors: vec![1],
245            },
246            CfgGraphNode {
247                id: 1,
248                x: 1.0,
249                y: 0.0,
250                z: 0.0,
251                successors: vec![2],
252            },
253            CfgGraphNode {
254                id: 2,
255                x: 2.0,
256                y: 0.0,
257                z: 0.0,
258                successors: vec![3],
259            },
260            CfgGraphNode {
261                id: 3,
262                x: 3.0,
263                y: 0.0,
264                z: 0.0,
265                successors: vec![],
266            },
267        ];
268
269        let edges = transitive_reduction(&nodes);
270        assert_eq!(edges.len(), 3); // All edges kept
271    }
272
273    #[test]
274    fn test_transitive_reduction_with_redundant() {
275        // 0 -> 1, 0 -> 2, 1 -> 2 (edge 0->2 is redundant)
276        let nodes = vec![
277            CfgGraphNode {
278                id: 0,
279                x: 0.0,
280                y: 0.0,
281                z: 0.0,
282                successors: vec![1, 2],
283            },
284            CfgGraphNode {
285                id: 1,
286                x: 1.0,
287                y: 0.0,
288                z: 0.0,
289                successors: vec![2],
290            },
291            CfgGraphNode {
292                id: 2,
293                x: 2.0,
294                y: 0.0,
295                z: 0.0,
296                successors: vec![],
297            },
298        ];
299
300        let edges = transitive_reduction(&nodes);
301        assert_eq!(edges.len(), 2); // 0->2 removed
302        assert!(edges.contains(&(0, 1)));
303        assert!(edges.contains(&(1, 2)));
304    }
305
306    #[test]
307    fn test_is_reachable() {
308        let nodes = vec![
309            CfgGraphNode {
310                id: 0,
311                x: 0.0,
312                y: 0.0,
313                z: 0.0,
314                successors: vec![1],
315            },
316            CfgGraphNode {
317                id: 1,
318                x: 1.0,
319                y: 0.0,
320                z: 0.0,
321                successors: vec![2],
322            },
323            CfgGraphNode {
324                id: 2,
325                x: 2.0,
326                y: 0.0,
327                z: 0.0,
328                successors: vec![],
329            },
330        ];
331
332        let closure = transitive_closure(&nodes);
333
334        assert!(is_reachable(0, 2, &closure));
335        assert!(is_reachable(0, 1, &closure));
336        assert!(!is_reachable(2, 0, &closure));
337    }
338
339    #[test]
340    fn test_count_ancestors_descendants() {
341        let nodes = vec![
342            CfgGraphNode {
343                id: 0,
344                x: 0.0,
345                y: 0.0,
346                z: 0.0,
347                successors: vec![1, 2],
348            },
349            CfgGraphNode {
350                id: 1,
351                x: 1.0,
352                y: 0.0,
353                z: 0.0,
354                successors: vec![3],
355            },
356            CfgGraphNode {
357                id: 2,
358                x: 2.0,
359                y: 0.0,
360                z: 0.0,
361                successors: vec![3],
362            },
363            CfgGraphNode {
364                id: 3,
365                x: 3.0,
366                y: 0.0,
367                z: 0.0,
368                successors: vec![],
369            },
370        ];
371
372        let closure = transitive_closure(&nodes);
373
374        assert_eq!(count_descendants(0, &closure), 3);
375        assert_eq!(count_ancestors(3, &closure), 3);
376        assert_eq!(count_descendants(3, &closure), 0);
377    }
378
379    #[test]
380    fn test_get_reachable_from() {
381        let nodes = vec![
382            CfgGraphNode {
383                id: 0,
384                x: 0.0,
385                y: 0.0,
386                z: 0.0,
387                successors: vec![1, 2],
388            },
389            CfgGraphNode {
390                id: 1,
391                x: 1.0,
392                y: 0.0,
393                z: 0.0,
394                successors: vec![],
395            },
396            CfgGraphNode {
397                id: 2,
398                x: 2.0,
399                y: 0.0,
400                z: 0.0,
401                successors: vec![],
402            },
403        ];
404
405        let closure = transitive_closure(&nodes);
406        let reachable = get_reachable_from(0, &closure);
407
408        assert_eq!(reachable.len(), 2);
409        assert!(reachable.contains(&1));
410        assert!(reachable.contains(&2));
411    }
412
413    #[test]
414    fn test_total_pairs() {
415        let nodes = vec![
416            CfgGraphNode {
417                id: 0,
418                x: 0.0,
419                y: 0.0,
420                z: 0.0,
421                successors: vec![1],
422            },
423            CfgGraphNode {
424                id: 1,
425                x: 1.0,
426                y: 0.0,
427                z: 0.0,
428                successors: vec![2],
429            },
430            CfgGraphNode {
431                id: 2,
432                x: 2.0,
433                y: 0.0,
434                z: 0.0,
435                successors: vec![],
436            },
437        ];
438
439        let result = transitive_closure(&nodes);
440
441        // 0 reaches 1,2 (2 pairs), 1 reaches 2 (1 pair), 2 reaches nothing
442        assert_eq!(result.total_pairs, 3);
443    }
444}