Skip to main content

geographdb_core/algorithms/
loop_detection.rs

1//! Loop Detection via Y-Coordinate Clustering
2//!
3//! This module implements loop detection for Control Flow Graphs using
4//! 3D spatial coordinates, specifically the Y-coordinate (loop_nesting level).
5//!
6//! # Algorithm
7//!
8//! Blocks with Y > 0 are inside at least one loop. Blocks with the same
9//! Y coordinate at non-zero levels belong to the same loop nesting depth.
10//!
11//! # Complexity
12//!
13//! - Time: O(n) single pass through blocks
14//! - Space: O(l) where l is number of unique loop levels
15//!
16//! # Example
17//!
18//! ```rust
19//! use geographdb_core::algorithms::loop_detection::{detect_loops, LoopBlock};
20//!
21//! let blocks = vec![
22//!     LoopBlock { id: 0, x: 0.0, y: 0.0, z: 0.0 },  // Entry (not in loop)
23//!     LoopBlock { id: 1, x: 1.0, y: 0.0, z: 1.0 },  // Branch (not in loop)
24//!     LoopBlock { id: 2, x: 2.0, y: 1.0, z: 0.0 },  // Inside loop (depth 1)
25//!     LoopBlock { id: 3, x: 3.0, y: 1.0, z: 0.0 },  // Inside loop (depth 1)
26//!     LoopBlock { id: 4, x: 4.0, y: 2.0, z: 0.0 },  // Inside nested loop (depth 2)
27//! ];
28//!
29//! let loops = detect_loops(&blocks);
30//! assert_eq!(loops.len(), 2);  // Two loop levels detected
31//! ```
32
33use std::collections::HashMap;
34
35/// A block in the CFG for loop detection
36#[derive(Debug, Clone)]
37pub struct LoopBlock {
38    pub id: u64,
39    pub x: f32, // dominator_depth
40    pub y: f32, // loop_nesting
41    pub z: f32, // branch_count
42}
43
44/// Information about a detected loop
45#[derive(Debug, Clone)]
46pub struct LoopInfo {
47    /// Loop nesting level (1 = outermost loop)
48    pub level: u32,
49    /// Block IDs that belong to this loop
50    pub block_ids: Vec<u64>,
51    /// Entry block ID (block with minimum X at this level)
52    pub entry_block: u64,
53    /// Whether this loop contains nested loops
54    pub has_nested_loops: bool,
55}
56
57/// Result of loop detection analysis
58#[derive(Debug, Clone)]
59pub struct LoopAnalysisResult {
60    /// All detected loops, ordered by nesting level
61    pub loops: Vec<LoopInfo>,
62    /// Maximum loop nesting depth found
63    pub max_depth: u32,
64    /// Total number of blocks inside loops
65    pub blocks_in_loops: usize,
66    /// Total number of blocks outside loops
67    pub blocks_outside_loops: usize,
68}
69
70/// Detect loops in a CFG using Y-coordinate clustering
71///
72/// Blocks with Y > 0 are inside loops. Blocks with the same Y value
73/// at non-zero levels are grouped together as belonging to the same
74/// loop nesting depth.
75///
76/// # Arguments
77/// * `blocks` - Slice of CFG blocks with spatial coordinates
78///
79/// # Returns
80/// Vector of LoopInfo for each detected loop level
81pub fn detect_loops(blocks: &[LoopBlock]) -> Vec<LoopInfo> {
82    // Group blocks by Y coordinate (loop nesting level)
83    let mut level_blocks: HashMap<u32, Vec<&LoopBlock>> = HashMap::new();
84
85    for block in blocks {
86        let level = block.y as u32;
87        if level > 0 {
88            level_blocks.entry(level).or_default().push(block);
89        }
90    }
91
92    // Convert to LoopInfo structures
93    let mut loops: Vec<LoopInfo> = level_blocks
94        .into_iter()
95        .map(|(level, blocks_at_level)| {
96            let block_ids: Vec<u64> = blocks_at_level.iter().map(|b| b.id).collect();
97
98            // Entry block is the one with minimum X (dominator depth)
99            let entry_block = blocks_at_level
100                .iter()
101                .min_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal))
102                .map(|b| b.id)
103                .unwrap_or(0);
104
105            // Check for nested loops (higher levels exist)
106            let has_nested_loops = false; // Will be set in analyze_loops
107
108            LoopInfo {
109                level,
110                block_ids,
111                entry_block,
112                has_nested_loops,
113            }
114        })
115        .collect();
116
117    // Sort by level (outermost first)
118    loops.sort_by_key(|l| l.level);
119
120    // Mark loops that have nested loops
121    let max_level = loops.iter().map(|l| l.level).max().unwrap_or(0);
122    for loop_info in &mut loops {
123        loop_info.has_nested_loops = loop_info.level < max_level;
124    }
125
126    loops
127}
128
129/// Analyze loop structure of a CFG
130///
131/// Provides comprehensive statistics about loop nesting and distribution.
132pub fn analyze_loops(blocks: &[LoopBlock]) -> LoopAnalysisResult {
133    let loops = detect_loops(blocks);
134
135    let max_depth = loops.iter().map(|l| l.level).max().unwrap_or(0);
136    let blocks_in_loops: usize = loops.iter().map(|l| l.block_ids.len()).sum();
137    let blocks_outside_loops = blocks.len() - blocks_in_loops;
138
139    LoopAnalysisResult {
140        loops,
141        max_depth,
142        blocks_in_loops,
143        blocks_outside_loops,
144    }
145}
146
147/// Find the innermost loop containing a specific block
148///
149/// Returns the LoopInfo for the deepest loop that contains the given block.
150pub fn find_innermost_loop_for_block(block_id: u64, loops: &[LoopInfo]) -> Option<&LoopInfo> {
151    loops
152        .iter()
153        .filter(|l| l.block_ids.contains(&block_id))
154        .max_by_key(|l| l.level)
155}
156
157/// Check if a block is inside any loop
158#[inline]
159pub fn is_in_loop(block: &LoopBlock) -> bool {
160    block.y > 0.0
161}
162
163/// Get the loop nesting depth for a block
164#[inline]
165pub fn get_loop_depth(block: &LoopBlock) -> u32 {
166    block.y as u32
167}
168
169/// Find all blocks at a specific loop nesting level
170pub fn get_blocks_at_level(blocks: &[LoopBlock], level: u32) -> Vec<&LoopBlock> {
171    blocks.iter().filter(|b| (b.y as u32) == level).collect()
172}
173
174/// Calculate loop complexity score
175///
176/// Higher scores indicate more complex loop structures.
177/// Score = sum of (level * block_count) for all loops
178pub fn calculate_loop_complexity(loops: &[LoopInfo]) -> u32 {
179    loops
180        .iter()
181        .map(|l| l.level * l.block_ids.len() as u32)
182        .sum()
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_detect_loops_simple() {
191        // Simple loop: entry -> header -> body -> exit
192        let blocks = vec![
193            LoopBlock {
194                id: 0,
195                x: 0.0,
196                y: 0.0,
197                z: 0.0,
198            }, // Entry (not in loop)
199            LoopBlock {
200                id: 1,
201                x: 1.0,
202                y: 1.0,
203                z: 1.0,
204            }, // Loop header
205            LoopBlock {
206                id: 2,
207                x: 2.0,
208                y: 1.0,
209                z: 0.0,
210            }, // Loop body
211            LoopBlock {
212                id: 3,
213                x: 3.0,
214                y: 0.0,
215                z: 0.0,
216            }, // Exit (not in loop)
217        ];
218
219        let loops = detect_loops(&blocks);
220
221        assert_eq!(loops.len(), 1, "Should detect 1 loop");
222        assert_eq!(loops[0].level, 1, "Loop should be at level 1");
223        assert_eq!(loops[0].block_ids.len(), 2, "Loop should have 2 blocks");
224        assert!(loops[0].block_ids.contains(&1), "Should contain header");
225        assert!(loops[0].block_ids.contains(&2), "Should contain body");
226    }
227
228    #[test]
229    fn test_detect_loops_nested() {
230        // Nested loops: outer loop (level 1) containing inner loop (level 2)
231        let blocks = vec![
232            LoopBlock {
233                id: 0,
234                x: 0.0,
235                y: 0.0,
236                z: 0.0,
237            }, // Entry
238            LoopBlock {
239                id: 1,
240                x: 1.0,
241                y: 1.0,
242                z: 1.0,
243            }, // Outer loop header
244            LoopBlock {
245                id: 2,
246                x: 2.0,
247                y: 1.0,
248                z: 0.0,
249            }, // Outer loop body
250            LoopBlock {
251                id: 3,
252                x: 3.0,
253                y: 2.0,
254                z: 1.0,
255            }, // Inner loop header
256            LoopBlock {
257                id: 4,
258                x: 4.0,
259                y: 2.0,
260                z: 0.0,
261            }, // Inner loop body
262            LoopBlock {
263                id: 5,
264                x: 5.0,
265                y: 0.0,
266                z: 0.0,
267            }, // Exit
268        ];
269
270        let loops = detect_loops(&blocks);
271
272        assert_eq!(loops.len(), 2, "Should detect 2 loops");
273        assert_eq!(loops[0].level, 1, "First loop at level 1");
274        assert_eq!(loops[1].level, 2, "Second loop at level 2");
275        assert!(
276            loops[0].has_nested_loops,
277            "Outer loop should have nested loops"
278        );
279        assert!(
280            !loops[1].has_nested_loops,
281            "Inner loop should not have nested loops"
282        );
283    }
284
285    #[test]
286    fn test_detect_loops_multiple_at_same_level() {
287        // Multiple separate loops at same nesting level
288        let blocks = vec![
289            LoopBlock {
290                id: 0,
291                x: 0.0,
292                y: 0.0,
293                z: 0.0,
294            }, // Entry
295            LoopBlock {
296                id: 1,
297                x: 1.0,
298                y: 1.0,
299                z: 1.0,
300            }, // Loop 1 header
301            LoopBlock {
302                id: 2,
303                x: 2.0,
304                y: 1.0,
305                z: 0.0,
306            }, // Loop 1 body
307            LoopBlock {
308                id: 3,
309                x: 3.0,
310                y: 0.0,
311                z: 0.0,
312            }, // Between loops
313            LoopBlock {
314                id: 4,
315                x: 4.0,
316                y: 1.0,
317                z: 1.0,
318            }, // Loop 2 header
319            LoopBlock {
320                id: 5,
321                x: 5.0,
322                y: 1.0,
323                z: 0.0,
324            }, // Loop 2 body
325        ];
326
327        let loops = detect_loops(&blocks);
328
329        // Note: Current implementation groups by level, not by separate loops
330        assert_eq!(loops.len(), 1, "Should detect 1 loop level");
331        assert_eq!(loops[0].block_ids.len(), 4, "All level-1 blocks grouped");
332    }
333
334    #[test]
335    fn test_detect_loops_no_loops() {
336        // No loops - all blocks at level 0
337        let blocks = vec![
338            LoopBlock {
339                id: 0,
340                x: 0.0,
341                y: 0.0,
342                z: 0.0,
343            },
344            LoopBlock {
345                id: 1,
346                x: 1.0,
347                y: 0.0,
348                z: 1.0,
349            },
350            LoopBlock {
351                id: 2,
352                x: 2.0,
353                y: 0.0,
354                z: 0.0,
355            },
356        ];
357
358        let loops = detect_loops(&blocks);
359
360        assert_eq!(loops.len(), 0, "Should detect no loops");
361    }
362
363    #[test]
364    fn test_analyze_loops() {
365        let blocks = vec![
366            LoopBlock {
367                id: 0,
368                x: 0.0,
369                y: 0.0,
370                z: 0.0,
371            }, // Outside
372            LoopBlock {
373                id: 1,
374                x: 1.0,
375                y: 1.0,
376                z: 1.0,
377            }, // In loop
378            LoopBlock {
379                id: 2,
380                x: 2.0,
381                y: 1.0,
382                z: 0.0,
383            }, // In loop
384            LoopBlock {
385                id: 3,
386                x: 3.0,
387                y: 2.0,
388                z: 0.0,
389            }, // In nested loop
390            LoopBlock {
391                id: 4,
392                x: 4.0,
393                y: 0.0,
394                z: 0.0,
395            }, // Outside
396        ];
397
398        let result = analyze_loops(&blocks);
399
400        assert_eq!(result.max_depth, 2, "Max depth should be 2");
401        assert_eq!(result.blocks_in_loops, 3, "3 blocks in loops");
402        assert_eq!(result.blocks_outside_loops, 2, "2 blocks outside loops");
403        assert_eq!(result.loops.len(), 2, "2 loop levels");
404    }
405
406    #[test]
407    fn test_is_in_loop() {
408        let in_loop = LoopBlock {
409            id: 0,
410            x: 1.0,
411            y: 1.0,
412            z: 0.0,
413        };
414        let not_in_loop = LoopBlock {
415            id: 1,
416            x: 0.0,
417            y: 0.0,
418            z: 0.0,
419        };
420
421        assert!(is_in_loop(&in_loop), "Block with y>0 should be in loop");
422        assert!(
423            !is_in_loop(&not_in_loop),
424            "Block with y=0 should not be in loop"
425        );
426    }
427
428    #[test]
429    fn test_get_loop_depth() {
430        let block = LoopBlock {
431            id: 0,
432            x: 0.0,
433            y: 3.0,
434            z: 0.0,
435        };
436        assert_eq!(get_loop_depth(&block), 3, "Loop depth should be 3");
437    }
438
439    #[test]
440    fn test_find_innermost_loop_for_block() {
441        let loops = vec![
442            LoopInfo {
443                level: 1,
444                block_ids: vec![1, 2, 3],
445                entry_block: 1,
446                has_nested_loops: true,
447            },
448            LoopInfo {
449                level: 2,
450                block_ids: vec![2, 3],
451                entry_block: 2,
452                has_nested_loops: false,
453            },
454        ];
455
456        let innermost = find_innermost_loop_for_block(2, &loops);
457        assert!(innermost.is_some(), "Should find loop for block 2");
458        assert_eq!(innermost.unwrap().level, 2, "Should return innermost loop");
459    }
460
461    #[test]
462    fn test_get_blocks_at_level() {
463        let blocks = vec![
464            LoopBlock {
465                id: 0,
466                x: 0.0,
467                y: 0.0,
468                z: 0.0,
469            },
470            LoopBlock {
471                id: 1,
472                x: 1.0,
473                y: 1.0,
474                z: 0.0,
475            },
476            LoopBlock {
477                id: 2,
478                x: 2.0,
479                y: 1.0,
480                z: 0.0,
481            },
482            LoopBlock {
483                id: 3,
484                x: 3.0,
485                y: 2.0,
486                z: 0.0,
487            },
488        ];
489
490        let level_1 = get_blocks_at_level(&blocks, 1);
491        assert_eq!(level_1.len(), 2, "Should have 2 blocks at level 1");
492
493        let level_2 = get_blocks_at_level(&blocks, 2);
494        assert_eq!(level_2.len(), 1, "Should have 1 block at level 2");
495    }
496
497    #[test]
498    fn test_loop_complexity() {
499        let loops = vec![
500            LoopInfo {
501                level: 1,
502                block_ids: vec![1, 2, 3],
503                entry_block: 1,
504                has_nested_loops: true,
505            },
506            LoopInfo {
507                level: 2,
508                block_ids: vec![2, 3],
509                entry_block: 2,
510                has_nested_loops: false,
511            },
512        ];
513
514        // Complexity = 1*3 + 2*2 = 3 + 4 = 7
515        let complexity = calculate_loop_complexity(&loops);
516        assert_eq!(complexity, 7, "Loop complexity should be 7");
517    }
518
519    #[test]
520    fn test_loop_entry_block() {
521        // Entry block should be the one with minimum X
522        let blocks = vec![
523            LoopBlock {
524                id: 0,
525                x: 5.0,
526                y: 1.0,
527                z: 0.0,
528            },
529            LoopBlock {
530                id: 1,
531                x: 2.0,
532                y: 1.0,
533                z: 0.0,
534            }, // Entry (min X)
535            LoopBlock {
536                id: 2,
537                x: 8.0,
538                y: 1.0,
539                z: 0.0,
540            },
541        ];
542
543        let loops = detect_loops(&blocks);
544        assert_eq!(loops[0].entry_block, 1, "Entry should be block with min X");
545    }
546}