Skip to main content

solar_codegen/transform/
indvar_simplify.rs

1//! Induction-variable simplification and strength reduction.
2//!
3//! This pass recognizes loop-local address expressions of the form
4//! `base + iv * stride + constant` and replaces their loop uses with a
5//! loop-carried pointer phi:
6//!
7//! ```text
8//! ptr = phi [preheader: base + init * stride + constant], [latch: ptr + step * stride]
9//! ```
10//!
11//! The initial implementation is deliberately narrow. It requires canonical
12//! loops with a dedicated preheader, a single latch, and a single additive
13//! induction-variable update. That gives later loop optimizations a real
14//! ScalarEvolution-backed transform without guessing from ad hoc instruction
15//! patterns.
16//!
17//! Safety contract:
18//! - require canonical loops with a preheader and a single latch
19//! - rewrite only affine address expressions derived from the recognized induction variable
20//! - preserve the original address value when it is still used outside the loop
21
22use crate::{
23    analysis::{AffineExpr, Loop, LoopAnalyzer, ScalarEvolution},
24    mir::{
25        BlockId, Function, Immediate, InstId, InstKind, Instruction, MirType, Value, ValueId,
26        utils as mir_utils,
27    },
28    pass::FunctionPass,
29};
30use alloy_primitives::U256;
31use solar_data_structures::map::FxHashMap;
32
33/// Statistics from induction-variable simplification.
34#[derive(Clone, Debug, Default)]
35pub struct IndVarSimplifyStats {
36    /// Number of loop-carried pointer phis inserted.
37    pub pointer_phis_inserted: usize,
38    /// Number of loop-local address uses replaced.
39    pub address_uses_replaced: usize,
40}
41
42impl IndVarSimplifyStats {
43    /// Returns the total number of MIR changes performed.
44    #[must_use]
45    pub const fn total(&self) -> usize {
46        self.pointer_phis_inserted + self.address_uses_replaced
47    }
48}
49
50/// Performs conservative induction-variable strength reduction.
51#[derive(Debug, Default)]
52pub struct IndVarSimplifier {
53    stats: IndVarSimplifyStats,
54}
55
56/// Function pass for induction-variable simplification and strength reduction.
57pub struct IndVarSimplifyPass;
58
59impl FunctionPass for IndVarSimplifyPass {
60    fn name(&self) -> &str {
61        "indvar-simplify"
62    }
63
64    fn run_on_function(&mut self, func: &mut Function) -> bool {
65        IndVarSimplifier::new().run(func).total() != 0
66    }
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
70struct AddressKey {
71    base: ValueId,
72    iv: ValueId,
73    scale: i128,
74    constant: i128,
75}
76
77impl IndVarSimplifier {
78    /// Creates a new induction-variable simplifier.
79    #[must_use]
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// Returns statistics from the last run.
85    #[must_use]
86    pub const fn stats(&self) -> &IndVarSimplifyStats {
87        &self.stats
88    }
89
90    /// Runs induction-variable simplification once over `func`.
91    pub fn run(&mut self, func: &mut Function) -> &IndVarSimplifyStats {
92        self.stats = IndVarSimplifyStats::default();
93
94        let mut analyzer = LoopAnalyzer::new();
95        let loop_info = analyzer.analyze(func);
96        let mut loops: Vec<_> = loop_info.loops.values().cloned().collect();
97        loops.sort_by_key(|loop_data| loop_data.header.index());
98
99        for loop_data in loops {
100            self.run_loop(func, &loop_data);
101        }
102
103        &self.stats
104    }
105
106    fn run_loop(&mut self, func: &mut Function, loop_data: &Loop) {
107        let Some(preheader) = loop_data.preheader else { return };
108        let [latch] = loop_data.back_edges.as_slice() else { return };
109        let [iv] = loop_data.induction_vars.as_slice() else { return };
110        let Some(step) = self.additive_step(func, iv.value, iv.update_inst) else {
111            return;
112        };
113
114        let scev = ScalarEvolution::analyze(func, loop_data);
115        let inst_results = func.inst_results();
116        let mut candidates: FxHashMap<AddressKey, Vec<ValueId>> = FxHashMap::default();
117
118        let mut blocks: Vec<_> = loop_data.blocks.iter().copied().collect();
119        blocks.sort_by_key(|block| block.index());
120        for block in blocks {
121            let insts = func.blocks[block].instructions.clone();
122            for inst_id in insts {
123                let Some(&value) = inst_results.get(&inst_id) else { continue };
124                if !self.is_reducible_result(func, inst_id) {
125                    continue;
126                }
127                let Some(key) = self.address_key(&scev, value, iv.value) else {
128                    continue;
129                };
130                let Some(delta) = key.scale.checked_mul(step) else { continue };
131                if delta <= 0 || !self.has_non_address_loop_use(func, loop_data, value) {
132                    continue;
133                }
134                candidates.entry(key).or_default().push(value);
135            }
136        }
137
138        if candidates.is_empty() {
139            return;
140        }
141
142        let mut replacements = FxHashMap::default();
143        for (key, values) in candidates {
144            let Some(pointer) =
145                self.materialize_pointer_phi(func, loop_data, preheader, *latch, key)
146            else {
147                continue;
148            };
149            for value in values {
150                replacements.insert(value, pointer);
151            }
152        }
153
154        if replacements.is_empty() {
155            return;
156        }
157
158        self.stats.address_uses_replaced += self.replace_loop_uses(func, loop_data, &replacements);
159    }
160
161    fn additive_step(
162        &self,
163        func: &Function,
164        iv_value: ValueId,
165        update_inst: Option<InstId>,
166    ) -> Option<i128> {
167        let update_inst = update_inst?;
168        let InstKind::Add(a, b) = func.instructions[update_inst].kind else {
169            return None;
170        };
171        let step = if a == iv_value {
172            b
173        } else if b == iv_value {
174            a
175        } else {
176            return None;
177        };
178        self.value_i128(func, step)
179    }
180
181    fn address_key(
182        &self,
183        scev: &ScalarEvolution,
184        value: ValueId,
185        iv_value: ValueId,
186    ) -> Option<AddressKey> {
187        let AffineExpr { base, constant, terms } = scev.get(value)?.clone();
188        let base = base?;
189        let [term] = terms.as_slice() else { return None };
190        if term.value != iv_value || term.scale <= 0 {
191            return None;
192        }
193        if constant < 0 {
194            return None;
195        }
196        Some(AddressKey { base, iv: iv_value, scale: term.scale, constant })
197    }
198
199    fn materialize_pointer_phi(
200        &mut self,
201        func: &mut Function,
202        loop_data: &Loop,
203        preheader: BlockId,
204        latch: BlockId,
205        key: AddressKey,
206    ) -> Option<ValueId> {
207        let iv = loop_data.induction_vars.iter().find(|iv| iv.value == key.iv)?;
208        let init = self.value_i128(func, iv.init)?;
209        let init_offset = init.checked_mul(key.scale)?.checked_add(key.constant)?;
210        let delta = self.additive_step(func, key.iv, iv.update_inst)?.checked_mul(key.scale)?;
211        if delta <= 0 {
212            return None;
213        }
214
215        let initial = self.build_base_plus_offset(func, preheader, key.base, init_offset)?;
216        let phi_inst = func.alloc_inst(Instruction::new(
217            InstKind::Phi(vec![(preheader, initial)]),
218            Some(MirType::uint256()),
219        ));
220        let phi_value = func.alloc_value(Value::Inst(phi_inst));
221        self.insert_header_phi(func, loop_data.header, phi_inst);
222
223        let delta = self.offset_value(func, delta)?;
224        let next = self.append_inst_value(
225            func,
226            latch,
227            InstKind::Add(phi_value, delta),
228            Some(MirType::uint256()),
229        );
230        let InstKind::Phi(incoming) = &mut func.instructions[phi_inst].kind else {
231            return None;
232        };
233        incoming.push((latch, next));
234        self.stats.pointer_phis_inserted += 1;
235        Some(phi_value)
236    }
237
238    fn build_base_plus_offset(
239        &self,
240        func: &mut Function,
241        block: BlockId,
242        base: ValueId,
243        offset: i128,
244    ) -> Option<ValueId> {
245        if offset == 0 {
246            return Some(base);
247        }
248        let offset = self.offset_value(func, offset)?;
249        Some(self.append_inst_value(
250            func,
251            block,
252            InstKind::Add(base, offset),
253            Some(MirType::uint256()),
254        ))
255    }
256
257    fn offset_value(&self, func: &mut Function, offset: i128) -> Option<ValueId> {
258        if offset < 0 {
259            return None;
260        }
261        Some(func.alloc_value(Value::Immediate(Immediate::uint256(U256::from(offset as u128)))))
262    }
263
264    fn append_inst_value(
265        &self,
266        func: &mut Function,
267        block: BlockId,
268        kind: InstKind,
269        ty: Option<MirType>,
270    ) -> ValueId {
271        let inst = func.alloc_inst(Instruction::new(kind, ty));
272        func.blocks[block].instructions.push(inst);
273        func.alloc_value(Value::Inst(inst))
274    }
275
276    fn insert_header_phi(&self, func: &mut Function, header: BlockId, phi_inst: InstId) {
277        let insert_pos = func.blocks[header]
278            .instructions
279            .iter()
280            .take_while(|&&inst_id| matches!(func.instructions[inst_id].kind, InstKind::Phi(_)))
281            .count();
282        func.blocks[header].instructions.insert(insert_pos, phi_inst);
283    }
284
285    fn is_reducible_result(&self, func: &Function, inst_id: InstId) -> bool {
286        if func.instructions[inst_id].result_ty != Some(MirType::uint256()) {
287            return false;
288        }
289        matches!(
290            func.instructions[inst_id].kind,
291            InstKind::Add(_, _) | InstKind::Sub(_, _) | InstKind::Mul(_, _) | InstKind::Shl(_, _)
292        )
293    }
294
295    fn value_i128(&self, func: &Function, value: ValueId) -> Option<i128> {
296        let value = match func.value(value) {
297            Value::Immediate(imm) => imm.as_u256()?,
298            _ => return None,
299        };
300        if value <= U256::from(i128::MAX as u128) { Some(value.to::<u128>() as i128) } else { None }
301    }
302
303    fn has_non_address_loop_use(&self, func: &Function, loop_data: &Loop, value: ValueId) -> bool {
304        for &block in &loop_data.blocks {
305            for &inst_id in &func.blocks[block].instructions {
306                let kind = &func.instructions[inst_id].kind;
307                if kind.operands().contains(&value) && !Self::is_address_builder(kind) {
308                    return true;
309                }
310            }
311            if func.blocks[block]
312                .terminator
313                .as_ref()
314                .is_some_and(|term| term.operands().contains(&value))
315            {
316                return true;
317            }
318        }
319        false
320    }
321
322    fn is_address_builder(kind: &InstKind) -> bool {
323        matches!(
324            kind,
325            InstKind::Add(_, _) | InstKind::Sub(_, _) | InstKind::Mul(_, _) | InstKind::Shl(_, _)
326        )
327    }
328
329    fn replace_loop_uses(
330        &self,
331        func: &mut Function,
332        loop_data: &Loop,
333        replacements: &FxHashMap<ValueId, ValueId>,
334    ) -> usize {
335        let mut replaced = 0;
336        for &block in &loop_data.blocks {
337            let insts = func.blocks[block].instructions.clone();
338            for inst_id in insts {
339                replaced += mir_utils::replace_inst_uses(
340                    &mut func.instructions[inst_id].kind,
341                    replacements,
342                );
343            }
344            if let Some(term) = &mut func.blocks[block].terminator {
345                replaced += mir_utils::replace_terminator_uses(term, replacements);
346            }
347        }
348        replaced
349    }
350}