1use 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#[derive(Clone, Debug)]
18pub struct Loop {
19 pub header: BlockId,
21 pub blocks: FxHashSet<BlockId>,
23 pub back_edges: SmallVec<[BlockId; 2]>,
25 pub exit_blocks: SmallVec<[BlockId; 2]>,
27 pub preheader: Option<BlockId>,
29 pub induction_vars: Vec<InductionVariable>,
31 pub invariant_insts: FxHashSet<InstId>,
33 pub trip_count: Option<u64>,
35 pub trip_guard_is_header: bool,
42}
43
44#[derive(Clone, Debug)]
46pub struct InductionVariable {
47 pub value: ValueId,
49 pub init: ValueId,
51 pub step: ValueId,
53 pub descending: bool,
55 pub update_inst: Option<InstId>,
57}
58
59#[derive(Clone, Debug, Default)]
61pub struct LoopInfo {
62 pub loops: FxHashMap<BlockId, Loop>,
64 pub block_to_loop: FxHashMap<BlockId, BlockId>,
66}
67
68impl LoopInfo {
69 #[must_use]
71 pub fn is_in_loop(&self, block: BlockId) -> bool {
72 self.block_to_loop.contains_key(&block)
73 }
74
75 #[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 pub fn all_loops(&self) -> impl Iterator<Item = &Loop> {
83 self.loops.values()
84 }
85}
86
87#[derive(Debug, Default)]
89pub struct LoopAnalyzer {
90 cfg: Option<CfgInfo>,
91}
92
93impl LoopAnalyzer {
94 pub fn new() -> Self {
96 Self::default()
97 }
98
99 #[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 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 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 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 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 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 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 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}