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}