Skip to main content

geographdb_core/algorithms/
natural_loops.rs

1//! Natural Loop Detection using Dominance Information
2//!
3//! Finds natural loops in a CFG using back-edges and dominators.
4//!
5//! # Definitions
6//!
7//! - **Back-edge**: Edge from node A to node B where B dominates A
8//! - **Natural Loop**: Loop with a single entry point (header)
9//! - **Header**: First node in loop, dominates all other loop nodes
10//!
11//! # Algorithm
12//!
13//! 1. Find all back-edges using dominator information
14//! 2. For each back-edge, find loop body (nodes that can reach source without going through header)
15//! 3. Return set of natural loops
16
17use crate::algorithms::astar::CfgGraphNode;
18use crate::algorithms::dominance::{compute_dominance, dominates};
19use std::collections::{HashMap, HashSet, VecDeque};
20
21/// A natural loop in the CFG
22#[derive(Debug, Clone)]
23pub struct NaturalLoop {
24    /// Loop header (entry point)
25    pub header: u64,
26    /// Loop latch (node with back-edge to header)
27    pub latch: u64,
28    /// All nodes in the loop body
29    pub body: HashSet<u64>,
30    /// Predecessors outside the loop
31    pub pre_header: HashSet<u64>,
32}
33
34/// Result of natural loop analysis
35#[derive(Debug, Clone)]
36pub struct NaturalLoopResult {
37    /// All natural loops found
38    pub loops: Vec<NaturalLoop>,
39    /// Back-edges (source, target) pairs
40    pub back_edges: Vec<(u64, u64)>,
41}
42
43/// Find all back-edges in the CFG
44///
45/// A back-edge is an edge from node A to node B where B dominates A.
46pub fn find_back_edges(nodes: &[CfgGraphNode], entry_id: u64) -> Vec<(u64, u64)> {
47    let dom_result = compute_dominance(nodes, entry_id);
48    let mut back_edges = Vec::new();
49
50    for node in nodes {
51        for &succ in &node.successors {
52            if dominates(succ, node.id, &dom_result) {
53                back_edges.push((node.id, succ));
54            }
55        }
56    }
57
58    back_edges
59}
60
61/// Find all natural loops in the CFG
62///
63/// # Arguments
64/// * `nodes` - CFG nodes
65/// * `entry_id` - Entry node ID
66///
67/// # Returns
68/// NaturalLoopResult with all loops and back-edges
69pub fn find_natural_loops(nodes: &[CfgGraphNode], entry_id: u64) -> NaturalLoopResult {
70    let back_edges = find_back_edges(nodes, entry_id);
71    let mut loops = Vec::new();
72
73    // Build predecessor map
74    let mut predecessors: HashMap<u64, Vec<u64>> = HashMap::new();
75    for node in nodes {
76        for &succ in &node.successors {
77            predecessors.entry(succ).or_default().push(node.id);
78        }
79    }
80
81    // Find natural loop for each back-edge
82    for (source, target) in &back_edges {
83        let loop_body = find_loop_body(nodes, *source, *target, &predecessors);
84
85        // Find pre-header (predecessors outside loop)
86        let mut pre_header = HashSet::new();
87        for &node_id in &loop_body {
88            if let Some(preds) = predecessors.get(&node_id) {
89                for &pred in preds {
90                    if !loop_body.contains(&pred) {
91                        pre_header.insert(pred);
92                    }
93                }
94            }
95        }
96
97        loops.push(NaturalLoop {
98            header: *target,
99            latch: *source,
100            body: loop_body,
101            pre_header,
102        });
103    }
104
105    NaturalLoopResult { loops, back_edges }
106}
107
108/// Find the body of a natural loop given a back-edge
109fn find_loop_body(
110    _nodes: &[CfgGraphNode],
111    latch: u64,
112    header: u64,
113    predecessors: &HashMap<u64, Vec<u64>>,
114) -> HashSet<u64> {
115    let mut body = HashSet::new();
116    body.insert(latch);
117    body.insert(header);
118
119    // Work backwards from latch to find all nodes that can reach latch
120    // without going through header
121    let mut worklist: VecDeque<u64> = VecDeque::new();
122    worklist.push_back(latch);
123
124    while let Some(node_id) = worklist.pop_front() {
125        if let Some(preds) = predecessors.get(&node_id) {
126            for &pred in preds {
127                if !body.contains(&pred) {
128                    body.insert(pred);
129                    worklist.push_back(pred);
130                }
131            }
132        }
133    }
134
135    body
136}
137
138/// Check if a node is a loop header (has incoming back-edge)
139pub fn is_loop_header(node_id: u64, back_edges: &[(u64, u64)]) -> bool {
140    back_edges.iter().any(|&(_, target)| target == node_id)
141}
142
143/// Get the nesting depth of a loop (how many loops contain it)
144pub fn get_loop_nesting_depth(loop_header: u64, loops: &[NaturalLoop]) -> usize {
145    let mut depth = 0;
146    for loop_ in loops {
147        if loop_.header != loop_header && loop_.body.contains(&loop_header) {
148            depth += 1;
149        }
150    }
151    depth
152}
153
154/// Find innermost loop containing a node (smallest loop body)
155pub fn find_innermost_loop_for_node(node_id: u64, loops: &[NaturalLoop]) -> Option<&NaturalLoop> {
156    loops
157        .iter()
158        .filter(|l| l.body.contains(&node_id))
159        .min_by_key(|l| l.body.len())
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_find_back_edges_no_loops() {
168        // Linear: 0 -> 1 -> 2
169        let nodes = vec![
170            CfgGraphNode {
171                id: 0,
172                x: 0.0,
173                y: 0.0,
174                z: 0.0,
175                successors: vec![1],
176            },
177            CfgGraphNode {
178                id: 1,
179                x: 1.0,
180                y: 0.0,
181                z: 0.0,
182                successors: vec![2],
183            },
184            CfgGraphNode {
185                id: 2,
186                x: 2.0,
187                y: 0.0,
188                z: 0.0,
189                successors: vec![],
190            },
191        ];
192
193        let back_edges = find_back_edges(&nodes, 0);
194        assert_eq!(back_edges.len(), 0);
195    }
196
197    #[test]
198    fn test_find_back_edges_simple_loop() {
199        // Loop: 0 -> 1 -> 2 -> 1
200        let nodes = vec![
201            CfgGraphNode {
202                id: 0,
203                x: 0.0,
204                y: 0.0,
205                z: 0.0,
206                successors: vec![1],
207            },
208            CfgGraphNode {
209                id: 1,
210                x: 1.0,
211                y: 0.0,
212                z: 0.0,
213                successors: vec![2],
214            },
215            CfgGraphNode {
216                id: 2,
217                x: 2.0,
218                y: 0.0,
219                z: 0.0,
220                successors: vec![1],
221            },
222        ];
223
224        let back_edges = find_back_edges(&nodes, 0);
225        assert_eq!(back_edges.len(), 1);
226        assert_eq!(back_edges[0], (2, 1));
227    }
228
229    #[test]
230    fn test_find_natural_loops_simple() {
231        // Loop: 0 -> 1 -> 2 -> 1
232        let nodes = vec![
233            CfgGraphNode {
234                id: 0,
235                x: 0.0,
236                y: 0.0,
237                z: 0.0,
238                successors: vec![1],
239            },
240            CfgGraphNode {
241                id: 1,
242                x: 1.0,
243                y: 0.0,
244                z: 0.0,
245                successors: vec![2],
246            },
247            CfgGraphNode {
248                id: 2,
249                x: 2.0,
250                y: 0.0,
251                z: 0.0,
252                successors: vec![1],
253            },
254        ];
255
256        let result = find_natural_loops(&nodes, 0);
257        assert_eq!(result.loops.len(), 1);
258        assert_eq!(result.back_edges.len(), 1);
259
260        let loop_ = &result.loops[0];
261        assert_eq!(loop_.header, 1);
262        assert_eq!(loop_.latch, 2);
263        assert!(loop_.body.contains(&1));
264        assert!(loop_.body.contains(&2));
265    }
266
267    #[test]
268    fn test_find_natural_loops_nested() {
269        // Nested loops: outer 0->1->2->1, inner 1->3->4->3
270        let nodes = vec![
271            CfgGraphNode {
272                id: 0,
273                x: 0.0,
274                y: 0.0,
275                z: 0.0,
276                successors: vec![1],
277            },
278            CfgGraphNode {
279                id: 1,
280                x: 1.0,
281                y: 0.0,
282                z: 0.0,
283                successors: vec![2, 3],
284            },
285            CfgGraphNode {
286                id: 2,
287                x: 2.0,
288                y: 0.0,
289                z: 0.0,
290                successors: vec![1],
291            },
292            CfgGraphNode {
293                id: 3,
294                x: 3.0,
295                y: 0.0,
296                z: 0.0,
297                successors: vec![4],
298            },
299            CfgGraphNode {
300                id: 4,
301                x: 4.0,
302                y: 0.0,
303                z: 0.0,
304                successors: vec![3],
305            },
306        ];
307
308        let result = find_natural_loops(&nodes, 0);
309        assert!(result.loops.len() >= 2);
310    }
311
312    #[test]
313    fn test_is_loop_header() {
314        let back_edges = vec![(2, 1), (4, 3)];
315        assert!(is_loop_header(1, &back_edges));
316        assert!(is_loop_header(3, &back_edges));
317        assert!(!is_loop_header(0, &back_edges));
318    }
319
320    #[test]
321    fn test_find_innermost_loop_for_node() {
322        let loops = vec![
323            NaturalLoop {
324                header: 1,
325                latch: 2,
326                body: vec![1, 2, 3].into_iter().collect(),
327                pre_header: vec![0].into_iter().collect(),
328            },
329            NaturalLoop {
330                header: 2,
331                latch: 3,
332                body: vec![2, 3].into_iter().collect(),
333                pre_header: vec![1].into_iter().collect(),
334            },
335        ];
336
337        // Innermost = smallest loop containing the node
338        let innermost = find_innermost_loop_for_node(3, &loops);
339        assert!(innermost.is_some());
340        assert_eq!(innermost.unwrap().header, 2);
341    }
342}