Skip to main content

solar_codegen/analysis/
loop_analysis.rs

1//! Loop analysis for MIR.
2//!
3//! This module provides loop detection and analysis for optimization passes:
4//! - Natural loop detection using dominance-based algorithm
5//! - Loop variable identification
6//! - Loop-invariant computation detection
7//! - Loop bound analysis
8
9use crate::{
10    analysis::CfgInfo,
11    mir::{BlockId, Function, InstId, InstKind, Terminator, Value, ValueId},
12};
13use smallvec::SmallVec;
14use solar_data_structures::map::{FxHashMap, FxHashSet};
15
16/// A natural loop in the control flow graph.
17#[derive(Clone, Debug)]
18pub struct Loop {
19    /// The header block (entry point with back edge).
20    pub header: BlockId,
21    /// All blocks in the loop body (including header).
22    pub blocks: FxHashSet<BlockId>,
23    /// Back edges: blocks that jump back to the header.
24    pub back_edges: SmallVec<[BlockId; 2]>,
25    /// Exit blocks: blocks outside the loop that are successors of loop blocks.
26    pub exit_blocks: SmallVec<[BlockId; 2]>,
27    /// Preheader block (if exists): unique predecessor of header outside the loop.
28    pub preheader: Option<BlockId>,
29    /// Loop induction variables.
30    pub induction_vars: Vec<InductionVariable>,
31    /// Instructions that are invariant (don't change within the loop).
32    pub invariant_insts: FxHashSet<InstId>,
33    /// Optional: constant trip count if statically known.
34    pub trip_count: Option<u64>,
35    /// Whether the guard that produced [`Self::trip_count`] is the header's
36    /// terminator. Control enters every iteration at the header, so when this
37    /// holds, non-header blocks only execute after that iteration's bound
38    /// check passed and observe the induction variable strictly below the
39    /// bound; header instructions (and any guard placed deeper in the body)
40    /// still run once more in the exiting partial iteration.
41    pub trip_guard_is_header: bool,
42}
43
44/// An induction variable in a loop.
45#[derive(Clone, Debug)]
46pub struct InductionVariable {
47    /// The value ID of the induction variable (typically a phi node in header).
48    pub value: ValueId,
49    /// Initial value before loop entry.
50    pub init: ValueId,
51    /// Step/stride per iteration. The magnitude only; see [`Self::descending`] for the sign.
52    pub step: ValueId,
53    /// Whether the variable decreases by `step` each iteration (`i = i - step`).
54    pub descending: bool,
55    /// The instruction that computes the next value.
56    pub update_inst: Option<InstId>,
57}
58
59/// Result of loop analysis for a function.
60#[derive(Clone, Debug, Default)]
61pub struct LoopInfo {
62    /// All loops in the function, keyed by header block.
63    pub loops: FxHashMap<BlockId, Loop>,
64    /// Mapping from block to the innermost loop containing it.
65    pub block_to_loop: FxHashMap<BlockId, BlockId>,
66}
67
68impl LoopInfo {
69    /// Returns true if the block is in any loop.
70    #[must_use]
71    pub fn is_in_loop(&self, block: BlockId) -> bool {
72        self.block_to_loop.contains_key(&block)
73    }
74
75    /// Returns the loop containing the given block, if any.
76    #[must_use]
77    pub fn get_loop(&self, block: BlockId) -> Option<&Loop> {
78        self.block_to_loop.get(&block).and_then(|header| self.loops.get(header))
79    }
80
81    /// Returns all loops in the function.
82    pub fn all_loops(&self) -> impl Iterator<Item = &Loop> {
83        self.loops.values()
84    }
85}
86
87/// Loop analyzer that detects and analyzes loops in MIR functions.
88#[derive(Debug, Default)]
89pub struct LoopAnalyzer {
90    cfg: Option<CfgInfo>,
91}
92
93impl LoopAnalyzer {
94    /// Creates a new loop analyzer.
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    /// Returns true if `dominator` dominates `block` in the last analyzed function.
100    #[must_use]
101    pub fn dominates(&self, dominator: BlockId, block: BlockId) -> bool {
102        self.cfg.as_ref().is_some_and(|cfg| cfg.dominators().dominates(dominator, block))
103    }
104
105    /// Analyzes loops in a function.
106    pub fn analyze(&mut self, func: &Function) -> LoopInfo {
107        let mut info = LoopInfo::default();
108
109        self.cfg = Some(CfgInfo::new(func));
110        let loops = self.find_natural_loops(func);
111
112        for mut loop_info in loops {
113            self.find_exit_blocks(func, &mut loop_info);
114            self.find_preheader(func, &mut loop_info);
115            self.analyze_induction_vars(func, &mut loop_info);
116            self.find_invariant_instructions(func, &mut loop_info);
117            self.analyze_trip_count(func, &mut loop_info);
118
119            for &block in &loop_info.blocks {
120                info.block_to_loop.insert(block, loop_info.header);
121            }
122            info.loops.insert(loop_info.header, loop_info);
123        }
124
125        info
126    }
127
128    fn find_natural_loops(&self, func: &Function) -> Vec<Loop> {
129        let mut loops: FxHashMap<BlockId, Loop> = FxHashMap::default();
130        let Some(cfg) = &self.cfg else { return Vec::new() };
131
132        for &block_id in cfg.rpo() {
133            let block = &func.blocks[block_id];
134            if let Some(term) = &block.terminator {
135                for succ in term.successors() {
136                    if cfg.dominators().dominates(succ, block_id) {
137                        let loop_info = loops.entry(succ).or_insert_with(|| Loop {
138                            header: succ,
139                            blocks: FxHashSet::default(),
140                            back_edges: SmallVec::new(),
141                            exit_blocks: SmallVec::new(),
142                            preheader: None,
143                            induction_vars: Vec::new(),
144                            invariant_insts: FxHashSet::default(),
145                            trip_count: None,
146                            trip_guard_is_header: false,
147                        });
148                        loop_info.back_edges.push(block_id);
149                        self.collect_loop_blocks(func, succ, block_id, &mut loop_info.blocks);
150                    }
151                }
152            }
153        }
154
155        loops.into_values().collect()
156    }
157
158    fn collect_loop_blocks(
159        &self,
160        func: &Function,
161        header: BlockId,
162        back_edge_src: BlockId,
163        blocks: &mut FxHashSet<BlockId>,
164    ) {
165        blocks.insert(header);
166        let mut worklist = vec![back_edge_src];
167        while let Some(block) = worklist.pop() {
168            if blocks.insert(block) {
169                for &pred in &func.blocks[block].predecessors {
170                    if !blocks.contains(&pred) {
171                        worklist.push(pred);
172                    }
173                }
174            }
175        }
176    }
177
178    fn find_exit_blocks(&self, func: &Function, loop_info: &mut Loop) {
179        for &block_id in &loop_info.blocks {
180            if let Some(term) = &func.blocks[block_id].terminator {
181                for succ in term.successors() {
182                    if !loop_info.blocks.contains(&succ) && !loop_info.exit_blocks.contains(&succ) {
183                        loop_info.exit_blocks.push(succ);
184                    }
185                }
186            }
187        }
188    }
189
190    fn find_preheader(&self, func: &Function, loop_info: &mut Loop) {
191        let header_preds: Vec<BlockId> = func.blocks[loop_info.header]
192            .predecessors
193            .iter()
194            .filter(|&&pred| !loop_info.blocks.contains(&pred))
195            .copied()
196            .collect();
197
198        if let [preheader] = header_preds.as_slice()
199            && self.is_dedicated_preheader(func, *preheader, loop_info.header)
200        {
201            loop_info.preheader = Some(*preheader);
202        }
203    }
204
205    fn is_dedicated_preheader(&self, func: &Function, block: BlockId, header: BlockId) -> bool {
206        matches!(func.blocks[block].terminator.as_ref(), Some(Terminator::Jump(target)) if *target == header)
207    }
208
209    fn analyze_induction_vars(&self, func: &Function, loop_info: &mut Loop) {
210        for &inst_id in &func.blocks[loop_info.header].instructions {
211            let inst = &func.instructions[inst_id];
212
213            if let InstKind::Phi(incoming) = &inst.kind {
214                let mut init_value: Option<ValueId> = None;
215                let mut step_value: Option<ValueId> = None;
216                let mut conflicting = false;
217
218                for &(block, value) in incoming {
219                    let slot = if loop_info.blocks.contains(&block) {
220                        &mut step_value
221                    } else {
222                        &mut init_value
223                    };
224                    match slot {
225                        None => *slot = Some(value),
226                        Some(existing) if *existing == value => {}
227                        // Distinct updates from different latches (or distinct
228                        // entry values) put the variable off the single-stride
229                        // lattice that trip counts and affine ranges assume.
230                        Some(_) => {
231                            conflicting = true;
232                            break;
233                        }
234                    }
235                }
236                if conflicting {
237                    continue;
238                }
239
240                if let (Some(init), Some(step_val)) = (init_value, step_value) {
241                    let phi_value = self.find_result_value(func, inst_id);
242                    if let Some(phi_val) = phi_value
243                        && let Some(update_inst) =
244                            self.find_update_instruction(func, phi_val, step_val)
245                        && let Some((step_amount, descending)) =
246                            self.get_step_amount(func, update_inst, phi_val)
247                    {
248                        loop_info.induction_vars.push(InductionVariable {
249                            value: phi_val,
250                            init,
251                            step: step_amount,
252                            descending,
253                            update_inst: Some(update_inst),
254                        });
255                    }
256                }
257            }
258        }
259    }
260
261    fn find_update_instruction(
262        &self,
263        func: &Function,
264        phi_val: ValueId,
265        step_val: ValueId,
266    ) -> Option<InstId> {
267        if let Value::Inst(inst_id) = &func.values[step_val] {
268            let inst = &func.instructions[*inst_id];
269            match &inst.kind {
270                InstKind::Add(a, b) if *a == phi_val || *b == phi_val => return Some(*inst_id),
271                InstKind::Sub(a, _) if *a == phi_val => return Some(*inst_id),
272                _ => {}
273            }
274        }
275        None
276    }
277
278    /// Returns the step magnitude and whether the induction variable is descending.
279    fn get_step_amount(
280        &self,
281        func: &Function,
282        inst_id: InstId,
283        phi_val: ValueId,
284    ) -> Option<(ValueId, bool)> {
285        let inst = &func.instructions[inst_id];
286        match &inst.kind {
287            InstKind::Add(a, b) => {
288                let step = if *a == phi_val { *b } else { *a };
289                // A wrapping decrement can be encoded as an addition of a huge
290                // constant (two's-complement negative); classify it as
291                // descending so trip-count and range reasoning bail out.
292                let descending = matches!(
293                    &func.values[step],
294                    Value::Immediate(imm) if imm.as_u256().is_some_and(|v| v.bit(255))
295                );
296                Some((step, descending))
297            }
298            InstKind::Sub(_, b) => Some((*b, true)),
299            _ => None,
300        }
301    }
302
303    fn find_invariant_instructions(&self, func: &Function, loop_info: &mut Loop) {
304        let mut invariant_values: FxHashSet<ValueId> = FxHashSet::default();
305
306        for (value_id, value) in func.values.iter_enumerated() {
307            match value {
308                Value::Immediate(_) | Value::Arg { .. } => {
309                    invariant_values.insert(value_id);
310                }
311                Value::Inst(inst_id) => {
312                    let in_loop = loop_info
313                        .blocks
314                        .iter()
315                        .any(|&block| func.blocks[block].instructions.contains(inst_id));
316                    if !in_loop {
317                        invariant_values.insert(value_id);
318                    }
319                }
320                Value::Undef(_) | Value::Error(_) => {}
321            }
322        }
323
324        let mut changed = true;
325        while changed {
326            changed = false;
327            for &block_id in &loop_info.blocks {
328                for &inst_id in &func.blocks[block_id].instructions {
329                    let inst = &func.instructions[inst_id];
330
331                    if loop_info.invariant_insts.contains(&inst_id) {
332                        continue;
333                    }
334                    if inst.kind.has_side_effects() {
335                        continue;
336                    }
337                    if matches!(inst.kind, InstKind::Phi(_)) {
338                        continue;
339                    }
340
341                    let operands = inst.kind.operands();
342                    if operands.iter().all(|op| invariant_values.contains(op)) {
343                        loop_info.invariant_insts.insert(inst_id);
344                        if let Some(result) = self.find_result_value(func, inst_id) {
345                            invariant_values.insert(result);
346                        }
347                        changed = true;
348                    }
349                }
350            }
351        }
352    }
353
354    fn analyze_trip_count(&self, func: &Function, loop_info: &mut Loop) {
355        if loop_info.induction_vars.len() != 1 {
356            return;
357        }
358
359        let iv = &loop_info.induction_vars[0];
360
361        let init = match &func.values[iv.init] {
362            Value::Immediate(imm) => imm.as_u256(),
363            _ => return,
364        };
365
366        let step = match &func.values[iv.step] {
367            Value::Immediate(imm) => imm.as_u256(),
368            _ => return,
369        };
370
371        let (Some(init), Some(step)) = (init, step) else { return };
372
373        if step.is_zero() {
374            return;
375        }
376
377        // `find_loop_bound` only recognizes `iv < bound` exit guards, which terminate
378        // ascending loops; a descending variable only leaves them by wrapping around.
379        if iv.descending {
380            return;
381        }
382
383        if let Some((bound, guard_block)) = self.find_loop_bound(func, loop_info, iv.value)
384            && bound >= init
385        {
386            let diff = bound - init;
387            let trip = if diff.is_zero() {
388                alloy_primitives::U256::ZERO
389            } else {
390                ((diff - alloy_primitives::U256::from(1)) / step) + alloy_primitives::U256::from(1)
391            };
392            loop_info.trip_count = trip.try_into().ok();
393            loop_info.trip_guard_is_header =
394                loop_info.trip_count.is_some() && guard_block == loop_info.header;
395        }
396    }
397
398    /// Finds the upper bound of an `iv < bound` exit guard and the block whose
399    /// terminator checks it.
400    ///
401    /// Only branches that leave the loop bound the induction variable: the comparison must
402    /// hold on the in-loop (`then`) side and fail on the exit (`else`) side. In-body branches
403    /// with both successors inside the loop say nothing about the iteration space. Returns
404    /// `None` when multiple exiting guards disagree on the bound; when several guards agree,
405    /// the header guard is preferred since it bounds non-header blocks tightly.
406    fn find_loop_bound(
407        &self,
408        func: &Function,
409        loop_info: &Loop,
410        iv_value: ValueId,
411    ) -> Option<(alloy_primitives::U256, BlockId)> {
412        let mut blocks: Vec<BlockId> = loop_info.blocks.iter().copied().collect();
413        blocks.sort_by_key(|block| block.index());
414
415        let mut bound: Option<(alloy_primitives::U256, BlockId)> = None;
416        for block_id in blocks {
417            let Some(Terminator::Branch { condition, then_block, else_block }) =
418                &func.blocks[block_id].terminator
419            else {
420                continue;
421            };
422            if !loop_info.blocks.contains(then_block) || loop_info.blocks.contains(else_block) {
423                continue;
424            }
425            // The bound only limits the induction variable if every completed
426            // iteration passes this guard; a guard that a back edge can bypass
427            // says nothing about the values other blocks observe.
428            if !loop_info.back_edges.iter().all(|&latch| self.dominates(block_id, latch)) {
429                continue;
430            }
431            let Value::Inst(cond_inst) = &func.values[*condition] else { continue };
432            let imm = match &func.instructions[*cond_inst].kind {
433                InstKind::Lt(a, b) if *a == iv_value => *b,
434                InstKind::Gt(a, b) if *b == iv_value => *a,
435                _ => continue,
436            };
437            let Value::Immediate(imm) = &func.values[imm] else { continue };
438            let Some(this_bound) = imm.as_u256() else { continue };
439            match bound {
440                None => bound = Some((this_bound, block_id)),
441                Some((existing, _)) if existing == this_bound => {
442                    if block_id == loop_info.header {
443                        bound = Some((this_bound, block_id));
444                    }
445                }
446                Some(_) => return None,
447            }
448        }
449        bound
450    }
451
452    fn find_result_value(&self, func: &Function, inst_id: InstId) -> Option<ValueId> {
453        for (value_id, value) in func.values.iter_enumerated() {
454            if let Value::Inst(id) = value
455                && *id == inst_id
456            {
457                return Some(value_id);
458            }
459        }
460        None
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use crate::mir::{Function, Immediate, Value};
468    use solar_interface::Ident;
469
470    fn make_test_func() -> Function {
471        Function::new(Ident::DUMMY)
472    }
473
474    #[test]
475    fn test_simple_loop_detection() {
476        let mut func = make_test_func();
477
478        let entry = func.entry_block;
479        let header = func.alloc_block();
480        let body = func.alloc_block();
481        let exit = func.alloc_block();
482
483        func.blocks[entry].terminator = Some(Terminator::Jump(header));
484        func.blocks[header].predecessors.push(entry);
485
486        let cond = func.alloc_value(Value::Immediate(Immediate::bool(true)));
487        func.blocks[header].terminator =
488            Some(Terminator::Branch { condition: cond, then_block: body, else_block: exit });
489        func.blocks[body].predecessors.push(header);
490        func.blocks[exit].predecessors.push(header);
491
492        func.blocks[body].terminator = Some(Terminator::Jump(header));
493        func.blocks[header].predecessors.push(body);
494
495        func.blocks[exit].terminator = Some(Terminator::Stop);
496
497        let mut analyzer = LoopAnalyzer::new();
498        let info = analyzer.analyze(&func);
499
500        assert_eq!(info.loops.len(), 1);
501        let loop_info = info.loops.get(&header).expect("Loop should have header as key");
502        assert!(loop_info.blocks.contains(&header));
503        assert!(loop_info.blocks.contains(&body));
504        assert!(!loop_info.blocks.contains(&exit));
505    }
506}