Skip to main content

analyssa/analysis/
loops.rs

1//! Extended loop analysis infrastructure.
2//!
3//! This module provides comprehensive loop analysis beyond basic natural loop detection,
4//! including preheader identification, latch detection, exit analysis, loop
5//! classification, nesting relationship computation, and induction variable detection.
6//!
7//! # Loop Structure
8//!
9//! A well-formed loop has the following structure:
10//!
11//! ```text
12//!     [preheader]     <- Single entry predecessor (optional, may need insertion)
13//!          |
14//!          v
15//!     [header] <------+  <- Single entry point, dominates all loop nodes
16//!          |          |
17//!          v          |
18//!     [body ...]      |  <- Loop body nodes
19//!          |          |
20//!          v          |
21//!     [latch] --------+  <- Back edge source(s)
22//!          |
23//!          v
24//!     [exit ...]         <- Exit blocks (outside loop, have predecessor in loop)
25//! ```
26//!
27//! # Algorithm: `detect_loops`
28//!
29//! The loop detection algorithm works on any graph implementing the required
30//! traits (`GraphBase`, `Successors`, `Predecessors`), enabling loop detection
31//! on CIL CFGs, SSA CFGs, x86 CFGs, and other graph structures.
32//!
33//! **Phase 1: Back Edge Detection** (O(E * D) where D is dominator query cost):
34//! For each edge `(n -> s)`, if `s` dominates `n`, then `(n, s)` is a back edge
35//! and `s` is a loop header. This identifies all natural loops.
36//!
37//! **Phase 2: Loop Body Expansion** (O(L * B)):
38//! For each back edge `(latch, header)`, expand the loop body starting from
39//! the latch, adding predecessors until the header is reached. The body includes
40//! all nodes that can reach the latch without going through the header.
41//!
42//! **Phase 3: Preheader Identification** (O(N)):
43//! Among the header's predecessors, find the unique predecessor that is NOT
44//! in the loop body. If exactly one such predecessor exists, it's the preheader.
45//!
46//! **Phase 4: Exit Analysis** (O(B * S)):
47//! For each block in the loop body, check its successors. Any successor outside
48//! the body is an exit edge.
49//!
50//! **Phase 5: Loop Classification** (O(E)):
51//! Classify the loop type based on exit edge locations:
52//! - All exits from header = PreTested (while loop)
53//! - All exits from latch = PostTested (do-while loop)
54//! - No exits = Infinite
55//! - Mixed exits or multiple latches = Complex
56//!
57//! **Phase 6: Nesting Computation** (O(L^2)):
58//! For each pair of loops, determine containment by checking if one loop's
59//! header is in another loop's body. Depths are computed by walking parent chains.
60//!
61//! # Induction Variable Detection
62//!
63//! Induction variables are found by analyzing phi nodes at loop headers:
64//! - One operand comes from outside the loop (initial value)
65//! - One operand comes from inside the loop (updated value)
66//!
67//! The update instruction is analyzed to determine the update kind (Add, Sub, Mul)
68//! and stride (constant increment/decrement per iteration).
69//!
70//! # Loop Types
71//!
72//! | Type | Description | Example |
73//! |------|-------------|---------|
74//! | PreTested | Condition at header | `while(cond) { body }` |
75//! | PostTested | Condition at latch | `do { body } while(cond)` |
76//! | Infinite | No exit edges | `while(true) { body }` |
77//! | Complex | Multiple latches or irregular | Irreducible control flow |
78//!
79//! # Canonical Form
80//!
81//! Canonical loops have:
82//! - Single preheader (unique non-loop predecessor to header)
83//! - Single latch (unique back edge to header)
84//!
85//! # Loop Forest
86//!
87//! [`LoopForest`] provides:
88//! - O(1) innermost loop lookup for any block
89//! - Iteration by nesting depth (ascending or descending)
90//! - Loop containment queries via `loop_depth` and `is_in_loop`
91//!
92//! # Complexity
93//!
94//! Full detection: O(B^2 + E + L^2) worst case where B is block count,
95//! E is edge count, and L is loop count. O(B log B) typical for reducible CFGs.
96//!
97//! ```rust
98//! use analyssa::{
99//!     analysis::{detect_loops, SsaCfg},
100//!     graph::{algorithms::compute_dominators, NodeId, RootedGraph},
101//!     testing,
102//! };
103//!
104//! // Works with any graph implementing the traits
105//! let ssa = testing::loop_counter_fixture();
106//! let graph = SsaCfg::from_ssa(&ssa);
107//!
108//! let dominators = compute_dominators(&graph, graph.entry());
109//! let forest = detect_loops(&graph, &dominators);
110//!
111//! // The fixture has a single self-loop headed by block 1.
112//! assert_eq!(forest.loops().len(), 1);
113//! assert_eq!(forest.loops()[0].header, NodeId::new(1));
114//!
115//! // Containment queries: block 1 is in the loop, the entry block is not.
116//! assert!(forest.is_in_loop(NodeId::new(1)));
117//! assert_eq!(forest.loop_depth(NodeId::new(1)), 1);
118//! assert!(!forest.is_in_loop(NodeId::new(0)));
119//! ```
120
121use std::collections::HashMap;
122
123use crate::{
124    bitset::BitSet,
125    graph::{GraphBase, NodeId, Predecessors, Successors, algorithms::DominatorTree},
126    ir::{function::SsaFunction, ops::SsaOp, variable::SsaVarId},
127    target::Target,
128};
129
130/// Classification of loop types based on structure.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum LoopType {
133    /// Pre-tested loop (while): exit condition at header.
134    /// ```text
135    /// while (cond) { body }
136    /// ```
137    PreTested,
138
139    /// Post-tested loop (do-while): exit condition at latch.
140    /// ```text
141    /// do { body } while (cond)
142    /// ```
143    PostTested,
144
145    /// Infinite loop: no exit edges from loop body.
146    /// ```text
147    /// while (true) { body }
148    /// ```
149    Infinite,
150
151    /// Complex loop: multiple latches, irregular exits, or irreducible.
152    Complex,
153}
154
155/// Exit edge information for a loop.
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct LoopExit {
158    /// The block inside the loop that branches out.
159    pub exiting_block: NodeId,
160    /// The block outside the loop that is the exit target.
161    pub exit_block: NodeId,
162}
163
164/// Classification of induction variable update operations.
165///
166/// Describes how an induction variable's value changes each loop iteration.
167/// The kind is determined by analyzing the instruction that computes the
168/// updated value (typically in the loop latch).
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum InductionUpdateKind {
171    /// Induction variable is incremented: `i = i + stride`.
172    Add,
173    /// Induction variable is decremented: `i = i - stride`.
174    Sub,
175    /// Induction variable is scaled: `i = i * stride`.
176    Mul,
177    /// Unknown or complex update pattern that cannot be classified.
178    /// Examples: bitwise operations, division, function calls, or phi-based updates.
179    Unknown,
180}
181
182/// Represents an induction variable in a loop.
183///
184/// An induction variable is a variable whose value changes by a fixed amount
185/// on each iteration of a loop. Classic examples include loop counters (`i++`).
186///
187/// # Structure
188///
189/// An induction variable has:
190/// - A phi node at the loop header that merges the initial and updated values
191/// - An initial value from outside the loop (preheader)
192/// - An updated value computed inside the loop (typically in the latch)
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct InductionVar {
195    /// The phi node result variable at the loop header.
196    pub phi_result: SsaVarId,
197    /// The initial value (from preheader or outside loop).
198    pub init_value: SsaVarId,
199    /// The block providing the initial value.
200    pub init_block: NodeId,
201    /// The updated value (from inside loop, typically latch).
202    pub update_value: SsaVarId,
203    /// The block providing the updated value.
204    pub update_block: NodeId,
205    /// The type of update operation.
206    pub update_kind: InductionUpdateKind,
207    /// The stride (constant value added/subtracted per iteration), if known.
208    pub stride: Option<i64>,
209}
210
211/// Comprehensive loop information.
212///
213/// This extends `NaturalLoop` with additional structural information needed
214/// for loop canonicalization and optimization.
215#[derive(Debug, Clone)]
216pub struct LoopInfo {
217    /// The header block (single entry point, dominates all loop nodes).
218    pub header: NodeId,
219
220    /// All blocks in the loop body (including header).
221    pub body: BitSet,
222
223    /// Back edge sources (blocks that jump to the header from within the loop).
224    pub latches: Vec<NodeId>,
225
226    /// Preheader block if one exists (single non-loop predecessor of header).
227    /// `None` if header has multiple non-loop predecessors or none.
228    pub preheader: Option<NodeId>,
229
230    /// Exit edges from the loop.
231    pub exits: Vec<LoopExit>,
232
233    /// Loop nesting depth (0 = outermost).
234    pub depth: usize,
235
236    /// Classification of the loop type.
237    pub loop_type: LoopType,
238
239    /// Parent loop header, if this loop is nested.
240    pub parent: Option<NodeId>,
241
242    /// Immediate child loop headers.
243    pub children: Vec<NodeId>,
244}
245
246impl LoopInfo {
247    /// Creates a new `LoopInfo` with the given header.
248    #[must_use]
249    pub fn new(header: NodeId, node_count: usize) -> Self {
250        let mut body = BitSet::new(node_count);
251        body.insert(header.index());
252        Self {
253            header,
254            body,
255            latches: Vec::new(),
256            preheader: None,
257            exits: Vec::new(),
258            depth: 0,
259            loop_type: LoopType::Complex,
260            parent: None,
261            children: Vec::new(),
262        }
263    }
264
265    /// Returns true if this loop contains the given block.
266    #[must_use]
267    pub fn contains(&self, node: NodeId) -> bool {
268        self.body.contains(node.index())
269    }
270
271    /// Returns the number of blocks in the loop.
272    #[must_use]
273    pub fn size(&self) -> usize {
274        self.body.count()
275    }
276
277    /// Returns true if the loop has a single latch (canonical form).
278    #[must_use]
279    pub fn has_single_latch(&self) -> bool {
280        self.latches.len() == 1
281    }
282
283    /// Returns the single latch if there is exactly one.
284    #[must_use]
285    pub fn single_latch(&self) -> Option<NodeId> {
286        if self.latches.len() == 1 {
287            self.latches.first().copied()
288        } else {
289            None
290        }
291    }
292
293    /// Returns true if the loop has a preheader (canonical form).
294    #[must_use]
295    pub fn has_preheader(&self) -> bool {
296        self.preheader.is_some()
297    }
298
299    /// Returns true if the loop is in canonical form.
300    ///
301    /// A canonical loop has:
302    /// - A single preheader
303    /// - A single latch
304    #[must_use]
305    pub fn is_canonical(&self) -> bool {
306        self.has_preheader() && self.has_single_latch()
307    }
308
309    /// Returns true if this is an innermost loop (no children).
310    #[must_use]
311    pub fn is_innermost(&self) -> bool {
312        self.children.is_empty()
313    }
314
315    /// Returns true if this is an outermost loop (no parent).
316    #[must_use]
317    pub fn is_outermost(&self) -> bool {
318        self.parent.is_none()
319    }
320
321    /// Returns all exit blocks (blocks outside loop reachable from inside).
322    pub fn exit_blocks(&self) -> impl Iterator<Item = NodeId> + '_ {
323        self.exits.iter().map(|e| e.exit_block)
324    }
325
326    /// Returns all exiting blocks (blocks inside loop that branch out).
327    pub fn exiting_blocks(&self) -> impl Iterator<Item = NodeId> + '_ {
328        self.exits.iter().map(|e| e.exiting_block)
329    }
330
331    /// Returns the number of exits from this loop.
332    #[must_use]
333    pub fn exit_count(&self) -> usize {
334        self.exits.len()
335    }
336
337    /// Returns true if the header is also an exiting block.
338    ///
339    /// This indicates a pre-tested loop (condition at entry).
340    #[must_use]
341    pub fn header_is_exiting(&self) -> bool {
342        self.exits.iter().any(|e| e.exiting_block == self.header)
343    }
344
345    /// Returns true if a latch is also an exiting block.
346    ///
347    /// This indicates a post-tested loop (condition at end).
348    #[must_use]
349    pub fn latch_is_exiting(&self) -> bool {
350        self.exits
351            .iter()
352            .any(|e| self.latches.contains(&e.exiting_block))
353    }
354
355    /// Finds the condition block inside the loop body.
356    ///
357    /// For control-flow flattened code (e.g., ConfuserEx), the actual loop
358    /// condition is often inside a case block rather than at the dispatcher
359    /// header. This method searches for blocks with `Branch` instructions
360    /// within the loop body.
361    ///
362    /// # Returns
363    ///
364    /// - `Some(NodeId)` - The first block found with a conditional branch
365    /// - `None` - No conditional branch found in the loop body
366    #[must_use]
367    pub fn find_condition_in_body<T: Target>(&self, ssa: &SsaFunction<T>) -> Option<NodeId> {
368        for block_idx in self.body.iter() {
369            if let Some(block) = ssa.block(block_idx)
370                && matches!(block.terminator_op(), Some(SsaOp::Branch { .. }))
371            {
372                return Some(NodeId::new(block_idx));
373            }
374        }
375        None
376    }
377
378    /// Finds all conditional blocks within the loop body.
379    ///
380    /// Unlike `find_condition_in_body`, this returns all blocks with
381    /// conditional branches, useful for complex loops with multiple exit points.
382    #[must_use]
383    pub fn find_all_conditions_in_body<T: Target>(&self, ssa: &SsaFunction<T>) -> Vec<NodeId> {
384        self.body
385            .iter()
386            .filter(|&block_idx| {
387                ssa.block(block_idx)
388                    .is_some_and(|b| matches!(b.terminator_op(), Some(SsaOp::Branch { .. })))
389            })
390            .map(NodeId::new)
391            .collect()
392    }
393
394    /// Identifies induction variables in this loop.
395    ///
396    /// An induction variable is identified by finding phi nodes at the loop
397    /// header where:
398    /// - One operand comes from outside the loop (initial value)
399    /// - One operand comes from inside the loop (updated value)
400    ///
401    /// The method attempts to classify the update kind (add, sub, etc.) by
402    /// analyzing the instruction that produces the update value.
403    ///
404    /// # Returns
405    ///
406    /// A vector of [`InductionVar`] structures describing each induction variable.
407    #[must_use]
408    pub fn find_induction_vars<T: Target>(&self, ssa: &SsaFunction<T>) -> Vec<InductionVar> {
409        let mut induction_vars = Vec::new();
410
411        // Get phi nodes at the header
412        let Some(header_block) = ssa.block(self.header.index()) else {
413            return induction_vars;
414        };
415
416        for phi in header_block.phi_nodes() {
417            let operands = phi.operands();
418
419            // Need at least 2 operands (init + update)
420            if operands.len() < 2 {
421                continue;
422            }
423
424            // Find operands from inside vs outside the loop
425            let (inside_ops, outside_ops): (Vec<&_>, Vec<&_>) = operands
426                .iter()
427                .partition(|op| self.body.contains(op.predecessor()));
428
429            // Classic induction variable: 1 init from outside, 1+ updates from inside
430            if outside_ops.len() == 1 && !inside_ops.is_empty() {
431                let (Some(init_op), Some(update_op)) = (outside_ops.first(), inside_ops.first())
432                else {
433                    continue;
434                };
435
436                // Try to determine update kind by analyzing the defining instruction
437                let (update_kind, stride) =
438                    Self::analyze_update_instruction(ssa, update_op.value(), phi.result());
439
440                induction_vars.push(InductionVar {
441                    phi_result: phi.result(),
442                    init_value: init_op.value(),
443                    init_block: NodeId::new(init_op.predecessor()),
444                    update_value: update_op.value(),
445                    update_block: NodeId::new(update_op.predecessor()),
446                    update_kind,
447                    stride,
448                });
449            }
450        }
451
452        induction_vars
453    }
454
455    /// Analyzes an instruction to determine if it's an induction update.
456    ///
457    /// Looks for patterns like `v = phi_result + const` or `v = phi_result - const`.
458    fn analyze_update_instruction<T: Target>(
459        ssa: &SsaFunction<T>,
460        update_var: SsaVarId,
461        phi_result: SsaVarId,
462    ) -> (InductionUpdateKind, Option<i64>) {
463        // Find the instruction that defines update_var
464        let Some(var) = ssa.variable(update_var) else {
465            return (InductionUpdateKind::Unknown, None);
466        };
467        let def_site = var.def_site();
468
469        if def_site.is_phi() {
470            return (InductionUpdateKind::Unknown, None);
471        }
472
473        let Some(block) = ssa.block(def_site.block) else {
474            return (InductionUpdateKind::Unknown, None);
475        };
476
477        let Some(instr_idx) = def_site.instruction else {
478            return (InductionUpdateKind::Unknown, None);
479        };
480
481        let Some(instr) = block.instruction(instr_idx) else {
482            return (InductionUpdateKind::Unknown, None);
483        };
484
485        // Check for Add/Sub patterns
486        match instr.op() {
487            // Check if one operand is the phi result
488            SsaOp::Add { left, right, .. } if *left == phi_result || *right == phi_result => {
489                let other = if *left == phi_result { *right } else { *left };
490                let stride = ssa.try_constant_value(other).and_then(|v| v.as_i64());
491                return (InductionUpdateKind::Add, stride);
492            }
493            // For subtraction, left should be phi_result
494            SsaOp::Sub { left, right, .. } if *left == phi_result => {
495                let stride = ssa.try_constant_value(*right).and_then(|v| v.as_i64());
496                return (InductionUpdateKind::Sub, stride);
497            }
498            SsaOp::Mul { left, right, .. } if *left == phi_result || *right == phi_result => {
499                let other = if *left == phi_result { *right } else { *left };
500                let stride = ssa.try_constant_value(other).and_then(|v| v.as_i64());
501                return (InductionUpdateKind::Mul, stride);
502            }
503            _ => {}
504        }
505
506        (InductionUpdateKind::Unknown, None)
507    }
508}
509
510/// Loop forest containing all loops in a function.
511///
512/// Provides efficient queries for loop membership, nesting, and iteration.
513#[derive(Debug, Clone)]
514pub struct LoopForest {
515    /// All loops indexed by their header block.
516    loops: Vec<LoopInfo>,
517    /// Map from block to the innermost loop containing it.
518    block_to_loop: Vec<Option<usize>>,
519}
520
521impl LoopForest {
522    /// Creates an empty loop forest.
523    #[must_use]
524    pub fn new(block_count: usize) -> Self {
525        Self {
526            loops: Vec::new(),
527            block_to_loop: vec![None; block_count],
528        }
529    }
530
531    /// Adds a loop to the forest.
532    pub fn add_loop(&mut self, loop_info: LoopInfo) {
533        let loop_idx = self.loops.len();
534
535        // Update block-to-loop mapping for all blocks in this loop
536        for block_idx in loop_info.body.iter() {
537            let Some(slot) = self.block_to_loop.get_mut(block_idx) else {
538                continue;
539            };
540            // Only update if this is a more deeply nested loop
541            match *slot {
542                Some(existing_idx) => {
543                    if self
544                        .loops
545                        .get(existing_idx)
546                        .is_some_and(|l| l.depth < loop_info.depth)
547                    {
548                        *slot = Some(loop_idx);
549                    }
550                }
551                None => *slot = Some(loop_idx),
552            }
553        }
554
555        self.loops.push(loop_info);
556    }
557
558    /// Returns all loops in the forest.
559    #[must_use]
560    pub fn loops(&self) -> &[LoopInfo] {
561        &self.loops
562    }
563
564    /// Returns the number of loops.
565    #[must_use]
566    pub fn len(&self) -> usize {
567        self.loops.len()
568    }
569
570    /// Returns true if there are no loops.
571    #[must_use]
572    pub fn is_empty(&self) -> bool {
573        self.loops.is_empty()
574    }
575
576    /// Returns the innermost loop containing the given block.
577    #[must_use]
578    pub fn innermost_loop(&self, block: NodeId) -> Option<&LoopInfo> {
579        let block_idx = block.index();
580        let loop_idx = (*self.block_to_loop.get(block_idx)?)?;
581        self.loops.get(loop_idx)
582    }
583
584    /// Returns the loop with the given header.
585    #[must_use]
586    pub fn loop_for_header(&self, header: NodeId) -> Option<&LoopInfo> {
587        self.loops.iter().find(|l| l.header == header)
588    }
589
590    /// Returns the loop depth for a block (0 if not in any loop).
591    #[must_use]
592    pub fn loop_depth(&self, block: NodeId) -> usize {
593        self.innermost_loop(block)
594            .map_or(0, |l| l.depth.saturating_add(1))
595    }
596
597    /// Returns true if a block is in any loop.
598    #[must_use]
599    pub fn is_in_loop(&self, block: NodeId) -> bool {
600        self.innermost_loop(block).is_some()
601    }
602
603    /// Iterates over all loops in the forest.
604    pub fn iter(&self) -> impl Iterator<Item = &LoopInfo> {
605        self.loops.iter()
606    }
607
608    /// Returns loops sorted by depth (outermost first).
609    #[must_use]
610    pub fn by_depth_ascending(&self) -> Vec<&LoopInfo> {
611        let mut sorted: Vec<_> = self.loops.iter().collect();
612        sorted.sort_by_key(|l| l.depth);
613        sorted
614    }
615
616    /// Returns loops sorted by depth (innermost first).
617    #[must_use]
618    pub fn by_depth_descending(&self) -> Vec<&LoopInfo> {
619        let mut sorted: Vec<_> = self.loops.iter().collect();
620        sorted.sort_by_key(|l| std::cmp::Reverse(l.depth));
621        sorted
622    }
623}
624
625/// Detects all natural loops in a graph using dominance-based back edge detection.
626///
627/// This is the primary entry point for loop detection. It works with any graph
628/// implementing the required traits, enabling loop analysis on various graph types
629/// (CIL CFGs, SSA CFGs, x86 CFGs, etc.).
630///
631/// # Algorithm
632///
633/// The detection algorithm:
634/// 1. Finds back edges using dominance (n -> h where h dominates n)
635/// 2. For each back edge, computes the natural loop body
636/// 3. Computes preheaders, exits, and loop types
637/// 4. Establishes nesting relationships
638///
639/// # Arguments
640///
641/// * `graph` - Any graph implementing `GraphBase + Successors + Predecessors`
642/// * `dominators` - Pre-computed dominator tree for the graph
643///
644/// # Returns
645///
646/// A [`LoopForest`] containing all detected loops with their full analysis.
647///
648/// # Examples
649///
650/// ```rust
651/// use analyssa::{
652///     analysis::{detect_loops, SsaCfg},
653///     graph::{algorithms::compute_dominators, NodeId, RootedGraph},
654///     testing,
655/// };
656///
657/// let ssa = testing::loop_counter_fixture();
658/// let graph = SsaCfg::from_ssa(&ssa);
659///
660/// let dominators = compute_dominators(&graph, graph.entry());
661/// let forest = detect_loops(&graph, &dominators);
662///
663/// let headers: Vec<_> = forest
664///     .loops()
665///     .iter()
666///     .map(|loop_info| (loop_info.header, loop_info.size()))
667///     .collect();
668///
669/// // One loop, headed by block 1, whose body is just that block.
670/// assert_eq!(headers, vec![(NodeId::new(1), 1)]);
671/// ```
672#[must_use]
673pub fn detect_loops<G>(graph: &G, dominators: &DominatorTree) -> LoopForest
674where
675    G: GraphBase + Successors + Predecessors,
676{
677    let block_count = graph.node_count();
678    let mut forest = LoopForest::new(block_count);
679
680    // Collect loops by header
681    let mut loops_by_header: HashMap<NodeId, LoopInfo> = HashMap::new();
682
683    // Find all back edges: edge (n -> h) where h dominates n
684    for node in graph.node_ids() {
685        for succ in graph.successors(node) {
686            // Check if successor dominates current node (back edge)
687            if dominators.dominates(succ, node) {
688                // Found back edge: node -> succ (succ is loop header)
689                let header = succ;
690
691                let loop_info = loops_by_header
692                    .entry(header)
693                    .or_insert_with(|| LoopInfo::new(header, block_count));
694
695                loop_info.latches.push(node);
696                expand_loop_body(graph, loop_info, node);
697            }
698        }
699    }
700
701    // Compute additional loop information for each loop
702    for loop_info in loops_by_header.values_mut() {
703        compute_preheader(graph, loop_info);
704        compute_exits(graph, loop_info);
705        loop_info.loop_type = classify_loop(loop_info);
706    }
707
708    // Convert to Vec and compute nesting relationships
709    let mut loops: Vec<LoopInfo> = loops_by_header.into_values().collect();
710    compute_nesting(&mut loops);
711
712    // Sort by header for deterministic ordering
713    loops.sort_by_key(|l| l.header.index());
714
715    // Add all loops to forest
716    for loop_info in loops {
717        forest.add_loop(loop_info);
718    }
719
720    forest
721}
722
723/// Checks if a graph has any back edges (loops).
724///
725/// This is a fast check that returns as soon as the first back edge is found,
726/// without building the full loop forest. Use this when you only need to know
727/// whether loops exist, not their detailed structure.
728///
729/// # Arguments
730///
731/// * `graph` - Any graph implementing `GraphBase + Successors`
732/// * `dominators` - Pre-computed dominator tree for the graph
733///
734/// # Returns
735///
736/// `true` if at least one back edge exists, `false` otherwise.
737///
738/// # Examples
739///
740/// ```rust
741/// use analyssa::{
742///     analysis::{loops::has_back_edges, SsaCfg},
743///     graph::{algorithms::compute_dominators, RootedGraph},
744///     testing,
745/// };
746///
747/// // A loop fixture has a back edge...
748/// let looping = testing::loop_counter_fixture();
749/// let graph = SsaCfg::from_ssa(&looping);
750/// let dominators = compute_dominators(&graph, graph.entry());
751/// assert!(has_back_edges(&graph, &dominators));
752///
753/// // ...while an acyclic diamond CFG does not.
754/// let diamond = testing::diamond_phi_fixture();
755/// let graph = SsaCfg::from_ssa(&diamond);
756/// let dominators = compute_dominators(&graph, graph.entry());
757/// assert!(!has_back_edges(&graph, &dominators));
758/// ```
759#[must_use]
760pub fn has_back_edges<G>(graph: &G, dominators: &DominatorTree) -> bool
761where
762    G: GraphBase + Successors,
763{
764    for node in graph.node_ids() {
765        for succ in graph.successors(node) {
766            if dominators.dominates(succ, node) {
767                return true;
768            }
769        }
770    }
771    false
772}
773
774/// Expands the loop body to include all nodes that can reach the latch.
775///
776/// Uses a worklist algorithm: starting from the latch, we add
777/// predecessors that aren't the header until we've found all loop body nodes.
778fn expand_loop_body<G>(graph: &G, loop_info: &mut LoopInfo, latch: NodeId)
779where
780    G: Predecessors,
781{
782    if loop_info.body.contains(latch.index()) {
783        return;
784    }
785
786    let mut worklist = vec![latch];
787
788    while let Some(node) = worklist.pop() {
789        if loop_info.body.insert(node.index()) {
790            // Node wasn't in body yet, add its predecessors
791            for pred in graph.predecessors(node) {
792                if pred != loop_info.header && !loop_info.body.contains_checked(pred.index()) {
793                    worklist.push(pred);
794                }
795            }
796        }
797    }
798}
799
800/// Identifies the preheader for a loop.
801///
802/// A preheader is a single predecessor of the header that is outside the loop.
803/// If the header has multiple non-loop predecessors, there is no preheader.
804fn compute_preheader<G>(graph: &G, loop_info: &mut LoopInfo)
805where
806    G: Predecessors,
807{
808    let mut non_loop_preds: Vec<NodeId> = Vec::new();
809
810    for pred in graph.predecessors(loop_info.header) {
811        if !loop_info.body.contains_checked(pred.index()) {
812            non_loop_preds.push(pred);
813        }
814    }
815
816    // Preheader exists only if there's exactly one non-loop predecessor
817    loop_info.preheader = if non_loop_preds.len() == 1 {
818        non_loop_preds.first().copied()
819    } else {
820        None
821    };
822}
823
824/// Computes exit edges for a loop.
825///
826/// An exit edge goes from a block inside the loop to a block outside the loop.
827fn compute_exits<G>(graph: &G, loop_info: &mut LoopInfo)
828where
829    G: Successors,
830{
831    loop_info.exits.clear();
832
833    for body_block_idx in loop_info.body.iter() {
834        let body_block = NodeId::new(body_block_idx);
835        for succ in graph.successors(body_block) {
836            if !loop_info.body.contains_checked(succ.index()) {
837                loop_info.exits.push(LoopExit {
838                    exiting_block: body_block,
839                    exit_block: succ,
840                });
841            }
842        }
843    }
844}
845
846/// Classifies the loop type based on structure.
847fn classify_loop(loop_info: &LoopInfo) -> LoopType {
848    // Check for infinite loop (no exits)
849    if loop_info.exits.is_empty() {
850        return LoopType::Infinite;
851    }
852
853    // Check for multiple latches (complex)
854    if loop_info.latches.len() > 1 {
855        return LoopType::Complex;
856    }
857
858    // Get the single latch
859    let latch = loop_info.single_latch();
860
861    // Check if all exits are from the latch (post-tested / do-while loop)
862    if let Some(latch) = latch {
863        let latch_exits = loop_info
864            .exits
865            .iter()
866            .filter(|e| e.exiting_block == latch)
867            .count();
868
869        if latch_exits == loop_info.exits.len() && latch_exits > 0 {
870            return LoopType::PostTested;
871        }
872    }
873
874    // Check if header is the only exiting block (pre-tested / while loop)
875    let header_exits = loop_info
876        .exits
877        .iter()
878        .filter(|e| e.exiting_block == loop_info.header)
879        .count();
880
881    if header_exits == loop_info.exits.len() && header_exits > 0 {
882        return LoopType::PreTested;
883    }
884
885    // Mixed or irregular exit structure
886    LoopType::Complex
887}
888
889/// Computes loop nesting relationships and depths.
890fn compute_nesting(loops: &mut [LoopInfo]) {
891    let n = loops.len();
892
893    // Build header-to-index mapping
894    let header_to_idx: HashMap<NodeId, usize> = loops
895        .iter()
896        .enumerate()
897        .map(|(i, l)| (l.header, i))
898        .collect();
899
900    // `LoopInfo::size` is a `BitSet` popcount over the whole loop body, and the
901    // sort below called it once per comparison — O(L log L) popcounts per loop,
902    // O(L² log L) overall. Computed once here instead: L popcounts total.
903    let sizes: Vec<usize> = loops.iter().map(LoopInfo::size).collect();
904
905    // For each loop, find its parent (smallest enclosing loop)
906    for i in 0..n {
907        let Some(header) = loops.get(i).map(|l| l.header) else {
908            continue;
909        };
910
911        // The parent is the *minimum* containing loop, so a full sort was never
912        // needed — one linear scan finds it. The `j` tiebreak preserves the
913        // previous behaviour for equally-sized candidates, which matters because
914        // the downstream similarity pipeline requires a deterministic result.
915        let parent_idx = (0..n)
916            .filter(|&j| {
917                j != i
918                    && loops
919                        .get(j)
920                        .is_some_and(|l| l.body.contains(header.index()))
921            })
922            .min_by_key(|&j| (sizes.get(j).copied().unwrap_or(usize::MAX), j));
923
924        if let Some(parent_idx) = parent_idx {
925            let parent_header = match loops.get(parent_idx).map(|l| l.header) {
926                Some(h) => h,
927                None => continue,
928            };
929            if let Some(loop_i) = loops.get_mut(i) {
930                loop_i.parent = Some(parent_header);
931            }
932        }
933    }
934
935    // Compute children from parent relationships
936    for i in 0..n {
937        let parent_opt = loops.get(i).and_then(|l| l.parent);
938        let Some(parent_header) = parent_opt else {
939            continue;
940        };
941        let header_i = match loops.get(i).map(|l| l.header) {
942            Some(h) => h,
943            None => continue,
944        };
945        if let Some(&parent_idx) = header_to_idx.get(&parent_header)
946            && let Some(parent) = loops.get_mut(parent_idx)
947        {
948            parent.children.push(header_i);
949        }
950    }
951
952    // Compute depths from parent chain
953    for i in 0..n {
954        let mut depth: usize = 0;
955        let mut current = loops.get(i).and_then(|l| l.parent);
956        while let Some(parent_header) = current {
957            depth = depth.saturating_add(1);
958            if let Some(&parent_idx) = header_to_idx.get(&parent_header) {
959                current = loops.get(parent_idx).and_then(|l| l.parent);
960            } else {
961                break;
962            }
963        }
964        if let Some(l) = loops.get_mut(i) {
965            l.depth = depth;
966        }
967    }
968}
969
970#[cfg(test)]
971mod tests {
972    use super::*;
973
974    #[test]
975    fn test_loop_info_creation() {
976        let header = NodeId::new(0);
977        let loop_info = LoopInfo::new(header, 10);
978
979        assert_eq!(loop_info.header, header);
980        assert!(loop_info.contains(header));
981        assert_eq!(loop_info.size(), 1);
982        assert!(!loop_info.has_single_latch());
983        assert!(!loop_info.has_preheader());
984        assert!(!loop_info.is_canonical());
985    }
986
987    #[test]
988    fn test_loop_info_canonical() {
989        let header = NodeId::new(1);
990        let mut loop_info = LoopInfo::new(header, 10);
991
992        loop_info.preheader = Some(NodeId::new(0));
993        loop_info.latches.push(NodeId::new(2));
994
995        assert!(loop_info.has_preheader());
996        assert!(loop_info.has_single_latch());
997        assert!(loop_info.is_canonical());
998    }
999
1000    #[test]
1001    fn test_loop_forest() {
1002        let mut forest = LoopForest::new(10);
1003
1004        let mut outer_loop = LoopInfo::new(NodeId::new(1), 10);
1005        outer_loop.body.insert(2);
1006        outer_loop.body.insert(3);
1007        outer_loop.depth = 0;
1008
1009        let mut inner_loop = LoopInfo::new(NodeId::new(2), 10);
1010        inner_loop.body.insert(3);
1011        inner_loop.depth = 1;
1012
1013        forest.add_loop(outer_loop);
1014        forest.add_loop(inner_loop);
1015
1016        assert_eq!(forest.len(), 2);
1017
1018        // Block 3 should be in inner loop (depth 1)
1019        assert_eq!(forest.loop_depth(NodeId::new(3)), 2);
1020
1021        // Block 1 should be in outer loop only
1022        assert_eq!(forest.loop_depth(NodeId::new(1)), 1);
1023
1024        // Block 0 should not be in any loop
1025        assert_eq!(forest.loop_depth(NodeId::new(0)), 0);
1026    }
1027}