Skip to main content

solar_codegen/transform/
loop_opt.rs

1//! Loop Optimization passes for MIR.
2//!
3//! This module provides loop optimizations for MIR.
4//!
5//! **Loop Invariant Code Motion (LICM)** moves computations that don't change
6//! within a loop to the preheader block, reducing redundant work.
7//!
8//! ## Gas Savings
9//!
10//! This optimization is particularly important for EVM:
11//! - LICM: Avoids recomputing `arr.length` each iteration (MLOAD/SLOAD costs)
12
13use crate::{
14    analysis::{AffineExpr, Loop, LoopAnalyzer, ScalarEvolution},
15    mir::{
16        BlockId, Function, InstId, InstKind, StorageAlias, Terminator, Value, ValueId,
17        utils as mir_utils,
18    },
19    pass::FunctionPass,
20};
21use alloy_primitives::U256;
22use solar_data_structures::map::FxHashSet;
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25enum StorageSpace {
26    Persistent,
27    Transient,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31struct AffineRange {
32    base: Option<ValueId>,
33    start: i128,
34    end: i128,
35}
36
37#[derive(Clone, Copy)]
38struct LoopOptContext<'a> {
39    loop_data: &'a Loop,
40    scev: &'a ScalarEvolution,
41    analyzer: &'a LoopAnalyzer,
42}
43
44/// Loop optimization pass configuration.
45#[derive(Clone, Debug)]
46pub struct LoopOptConfig {
47    /// Enable Loop Invariant Code Motion.
48    pub enable_licm: bool,
49    /// Minimum estimated gas saved per iteration before an instruction is considered a LICM root.
50    pub min_licm_profit: u16,
51    /// Maximum number of instructions hoisted from one loop.
52    pub max_licm_hoisted_insts: usize,
53}
54
55impl Default for LoopOptConfig {
56    fn default() -> Self {
57        Self { enable_licm: true, min_licm_profit: 0, max_licm_hoisted_insts: usize::MAX }
58    }
59}
60
61/// Statistics from loop optimization.
62#[derive(Clone, Debug, Default)]
63pub struct LoopOptStats {
64    /// Number of instructions hoisted out of loops.
65    pub instructions_hoisted: usize,
66}
67
68/// Loop optimizer.
69#[derive(Debug)]
70pub struct LoopOptimizer {
71    config: LoopOptConfig,
72    stats: LoopOptStats,
73}
74
75/// Function pass for loop-invariant code motion.
76pub struct LicmPass;
77
78impl FunctionPass for LicmPass {
79    fn name(&self) -> &str {
80        "licm"
81    }
82
83    fn run_on_function(&mut self, func: &mut Function) -> bool {
84        let config =
85            LoopOptConfig { enable_licm: true, min_licm_profit: 3, max_licm_hoisted_insts: 8 };
86        LoopOptimizer::new(config).optimize(func).instructions_hoisted != 0
87    }
88}
89
90impl Default for LoopOptimizer {
91    fn default() -> Self {
92        Self::new(LoopOptConfig::default())
93    }
94}
95
96impl LoopOptimizer {
97    /// Creates a new loop optimizer with the given configuration.
98    pub fn new(config: LoopOptConfig) -> Self {
99        Self { config, stats: LoopOptStats::default() }
100    }
101
102    /// Returns the optimization statistics.
103    #[must_use]
104    pub fn stats(&self) -> &LoopOptStats {
105        &self.stats
106    }
107
108    /// Runs all enabled loop optimizations on a function.
109    pub fn optimize(&mut self, func: &mut Function) -> &LoopOptStats {
110        self.stats = LoopOptStats::default();
111        func.annotate_storage_aliases(mir_utils::StorageAliasScope::StorageAndTransient);
112
113        let mut analyzer = LoopAnalyzer::new();
114        let loop_info = analyzer.analyze(func);
115
116        if loop_info.loops.is_empty() {
117            return &self.stats;
118        }
119
120        let loop_headers: Vec<BlockId> = loop_info.loops.keys().copied().collect();
121
122        for header in loop_headers {
123            if let Some(loop_data) = loop_info.loops.get(&header)
124                && self.config.enable_licm
125            {
126                self.apply_licm(func, loop_data, &analyzer);
127            }
128        }
129
130        &self.stats
131    }
132
133    fn apply_licm(&mut self, func: &mut Function, loop_data: &Loop, analyzer: &LoopAnalyzer) {
134        let Some(preheader) = loop_data.preheader else { return };
135        if self.loop_observes_gas(func, loop_data) {
136            return;
137        }
138
139        let scev = ScalarEvolution::analyze(func, loop_data);
140        let ctx = LoopOptContext { loop_data, scev: &scev, analyzer };
141        let mut roots: Vec<InstId> = loop_data
142            .invariant_insts
143            .iter()
144            .copied()
145            .filter(|&inst_id| {
146                self.can_hoist_safely(func, inst_id, ctx)
147                    && self.is_profitable_licm_root(func, inst_id, ctx)
148            })
149            .collect();
150        roots.sort_by(|&a, &b| {
151            self.licm_profit(func, b)
152                .cmp(&self.licm_profit(func, a))
153                .then_with(|| a.index().cmp(&b.index()))
154        });
155
156        let mut selected = FxHashSet::default();
157        for root in roots {
158            let mut closure = Vec::new();
159            let mut visiting = FxHashSet::default();
160            if !self.collect_hoist_closure(func, root, ctx, &selected, &mut visiting, &mut closure)
161            {
162                continue;
163            }
164
165            let new_count = closure.iter().filter(|&&inst_id| !selected.contains(&inst_id)).count();
166            if selected.len() + new_count > self.config.max_licm_hoisted_insts {
167                continue;
168            }
169            selected.extend(closure);
170        }
171
172        if selected.is_empty() {
173            return;
174        }
175
176        let mut hoistable: Vec<InstId> = selected.into_iter().collect();
177        hoistable.sort_by_key(|inst_id| inst_id.index());
178        let ordered = self.topological_sort_instructions(func, &hoistable);
179
180        for inst_id in ordered {
181            // An enclosing loop's earlier hoist may have already moved the
182            // instruction out of these blocks; pushing it again would schedule
183            // the same instruction in two blocks.
184            let mut removed = false;
185            for &block_id in &loop_data.blocks {
186                let block = &mut func.blocks[block_id];
187                if let Some(pos) = block.instructions.iter().position(|&id| id == inst_id) {
188                    block.instructions.remove(pos);
189                    removed = true;
190                    break;
191                }
192            }
193            if removed {
194                func.blocks[preheader].instructions.push(inst_id);
195                self.stats.instructions_hoisted += 1;
196            }
197        }
198    }
199
200    fn collect_hoist_closure(
201        &self,
202        func: &Function,
203        inst_id: InstId,
204        ctx: LoopOptContext<'_>,
205        selected: &FxHashSet<InstId>,
206        visiting: &mut FxHashSet<InstId>,
207        out: &mut Vec<InstId>,
208    ) -> bool {
209        if selected.contains(&inst_id) {
210            return true;
211        }
212        if out.contains(&inst_id) {
213            return true;
214        }
215        if !visiting.insert(inst_id) {
216            return false;
217        }
218        if !self.can_hoist_safely(func, inst_id, ctx) {
219            return false;
220        }
221
222        let inst = &func.instructions[inst_id];
223        for operand in inst.kind.operands() {
224            if let Value::Inst(dep_inst) = func.value(operand)
225                && self.inst_in_loop(func, *dep_inst, ctx.loop_data)
226                && !self.collect_hoist_closure(func, *dep_inst, ctx, selected, visiting, out)
227            {
228                return false;
229            }
230        }
231
232        out.push(inst_id);
233        true
234    }
235
236    fn can_hoist_safely(&self, func: &Function, inst_id: InstId, ctx: LoopOptContext<'_>) -> bool {
237        let inst = &func.instructions[inst_id];
238
239        if inst.kind.has_side_effects() {
240            return false;
241        }
242        if matches!(inst.kind, InstKind::Phi(_)) {
243            return false;
244        }
245        match inst.kind {
246            // Hoisting memory reads expands memory earlier (and unconditionally), which any
247            // MSIZE in the function could observe; on top of the dependence checks they must
248            // also be guaranteed to execute so a zero-trip loop cannot start trapping (OOG
249            // from speculated memory expansion) or paying for work it never did.
250            InstKind::MLoad(addr) => {
251                return !self.function_observes_msize(func)
252                    && self.hoist_execution_guaranteed(func, inst_id, ctx)
253                    && !self.loop_may_mutate_memory_range(func, ctx, addr, Some(32));
254            }
255            InstKind::Keccak256(offset, size) => {
256                return !self.function_observes_msize(func)
257                    && self.hoist_execution_guaranteed(func, inst_id, ctx)
258                    && !self.loop_may_mutate_memory_range(
259                        func,
260                        ctx,
261                        offset,
262                        self.const_addr(func, size),
263                    );
264            }
265            InstKind::SLoad(slot) => {
266                return self.hoist_execution_guaranteed(func, inst_id, ctx)
267                    && !self.loop_may_mutate_storage_slot(
268                        func,
269                        ctx,
270                        inst_id,
271                        slot,
272                        StorageSpace::Persistent,
273                    );
274            }
275            InstKind::TLoad(slot) => {
276                return self.hoist_execution_guaranteed(func, inst_id, ctx)
277                    && !self.loop_may_mutate_storage_slot(
278                        func,
279                        ctx,
280                        inst_id,
281                        slot,
282                        StorageSpace::Transient,
283                    );
284            }
285            // MSIZE observes every memory expansion, including from other hoisted
286            // instructions; never move it.
287            InstKind::MSize => return false,
288            // Environment reads that calls or creates can change: balances move with value
289            // transfers, code size/hash change on deploy/selfdestruct, and every external
290            // call rewrites the return-data buffer.
291            InstKind::Balance(_)
292            | InstKind::SelfBalance
293            | InstKind::ExtCodeSize(_)
294            | InstKind::ExtCodeHash(_)
295            | InstKind::ReturnDataSize => {
296                // Also require guaranteed execution: speculating a cold
297                // BALANCE/EXTCODESIZE/EXTCODEHASH into the preheader of a
298                // zero-trip loop wastes 2600 gas.
299                return self.hoist_execution_guaranteed(func, inst_id, ctx)
300                    && !self.loop_contains_call_or_create(func, ctx.loop_data);
301            }
302            _ => {}
303        }
304        true
305    }
306
307    /// Returns true if hoisting `inst_id` into the preheader cannot make it execute when the
308    /// original loop would not have executed it.
309    ///
310    /// This holds when the instruction's block dominates every (live) exiting block, or when
311    /// the loop is known to complete at least one iteration that executes the instruction:
312    /// a verified trip count of at least one, a single exiting block (so the trip-count guard
313    /// is the only way out), and the instruction dominating every backedge.
314    fn hoist_execution_guaranteed(
315        &self,
316        func: &Function,
317        inst_id: InstId,
318        ctx: LoopOptContext<'_>,
319    ) -> bool {
320        let loop_data = ctx.loop_data;
321        let Some(inst_block) = loop_data
322            .blocks
323            .iter()
324            .copied()
325            .find(|&block| func.blocks[block].instructions.contains(&inst_id))
326        else {
327            return false;
328        };
329
330        let exiting = self.live_exiting_blocks(func, loop_data);
331        // No live exit means the loop only terminates by running out of gas,
332        // which consumes the entire gas budget regardless of what executes
333        // beforehand, so any placement is observationally equivalent.
334        if exiting.is_empty() {
335            return true;
336        }
337        if exiting.iter().all(|&block| ctx.analyzer.dominates(inst_block, block)) {
338            return true;
339        }
340
341        loop_data.trip_count.is_some_and(|trip| trip >= 1)
342            && exiting.len() == 1
343            && loop_data.back_edges.iter().all(|&latch| ctx.analyzer.dominates(inst_block, latch))
344    }
345
346    /// Returns the in-loop blocks from which the loop can actually exit.
347    ///
348    /// Branches whose condition is a constant that always picks the in-loop successor cannot
349    /// leave the loop and are ignored.
350    fn live_exiting_blocks(&self, func: &Function, loop_data: &Loop) -> Vec<BlockId> {
351        let mut exiting = Vec::new();
352        for &block_id in &loop_data.blocks {
353            let Some(term) = &func.blocks[block_id].terminator else { continue };
354            let escapes = match term {
355                Terminator::Branch { condition, then_block, else_block } => {
356                    match self.const_condition(func, *condition) {
357                        Some(true) => !loop_data.blocks.contains(then_block),
358                        Some(false) => !loop_data.blocks.contains(else_block),
359                        None => {
360                            !loop_data.blocks.contains(then_block)
361                                || !loop_data.blocks.contains(else_block)
362                        }
363                    }
364                }
365                _ => term.successors().iter().any(|succ| !loop_data.blocks.contains(succ)),
366            };
367            if escapes {
368                exiting.push(block_id);
369            }
370        }
371        exiting
372    }
373
374    fn function_observes_msize(&self, func: &Function) -> bool {
375        func.blocks.iter().any(|block| {
376            block
377                .instructions
378                .iter()
379                .any(|&inst_id| matches!(func.instructions[inst_id].kind, InstKind::MSize))
380        })
381    }
382
383    fn loop_contains_call_or_create(&self, func: &Function, loop_data: &Loop) -> bool {
384        loop_data.blocks.iter().any(|&block_id| {
385            func.blocks[block_id].instructions.iter().any(|&inst_id| {
386                matches!(
387                    func.instructions[inst_id].kind,
388                    InstKind::Call { .. }
389                        | InstKind::StaticCall { .. }
390                        | InstKind::DelegateCall { .. }
391                        | InstKind::InternalCall { .. }
392                        | InstKind::Create(_, _, _)
393                        | InstKind::Create2(_, _, _, _)
394                )
395            })
396        })
397    }
398
399    fn inst_in_loop(&self, func: &Function, inst_id: InstId, loop_data: &Loop) -> bool {
400        loop_data.blocks.iter().any(|&block| func.blocks[block].instructions.contains(&inst_id))
401    }
402
403    fn licm_profit(&self, func: &Function, inst_id: InstId) -> u16 {
404        match func.instructions[inst_id].kind {
405            InstKind::SLoad(_) => 100,
406            InstKind::TLoad(_) => 100,
407            InstKind::Keccak256(_, _) => 30,
408            InstKind::Exp(_, _) => 10,
409            InstKind::Mul(_, _)
410            | InstKind::Div(_, _)
411            | InstKind::SDiv(_, _)
412            | InstKind::Mod(_, _)
413            | InstKind::SMod(_, _)
414            | InstKind::AddMod(_, _, _)
415            | InstKind::MulMod(_, _, _) => 5,
416            InstKind::MLoad(_) | InstKind::CalldataLoad(_) => 3,
417            _ => 0,
418        }
419    }
420
421    fn is_profitable_licm_root(
422        &self,
423        func: &Function,
424        inst_id: InstId,
425        ctx: LoopOptContext<'_>,
426    ) -> bool {
427        self.licm_profit(func, inst_id) >= self.config.min_licm_profit
428            || (self.loop_has_known_multiple_iterations(ctx.loop_data)
429                && self.is_affine_address_base_used_in_loop(func, inst_id, ctx))
430            || (self.inst_dominates_loop_backedges(func, inst_id, ctx.loop_data, ctx.analyzer)
431                && self.is_affine_address_base_used_in_loop(func, inst_id, ctx))
432    }
433
434    fn loop_has_known_multiple_iterations(&self, loop_data: &Loop) -> bool {
435        loop_data.trip_count.is_some_and(|trip_count| trip_count > 1)
436    }
437
438    fn loop_observes_gas(&self, func: &Function, loop_data: &Loop) -> bool {
439        for &block_id in &loop_data.blocks {
440            for &inst_id in &func.blocks[block_id].instructions {
441                if matches!(func.instructions[inst_id].kind, InstKind::Gas) {
442                    return true;
443                }
444            }
445        }
446        false
447    }
448
449    fn inst_dominates_loop_backedges(
450        &self,
451        func: &Function,
452        inst_id: InstId,
453        loop_data: &Loop,
454        analyzer: &LoopAnalyzer,
455    ) -> bool {
456        let Some(inst_block) = loop_data
457            .blocks
458            .iter()
459            .copied()
460            .find(|&block| func.blocks[block].instructions.contains(&inst_id))
461        else {
462            return false;
463        };
464        loop_data.back_edges.iter().all(|&latch| analyzer.dominates(inst_block, latch))
465    }
466
467    fn loop_may_mutate_memory_range(
468        &self,
469        func: &Function,
470        ctx: LoopOptContext<'_>,
471        load_addr: ValueId,
472        load_width: Option<u64>,
473    ) -> bool {
474        for &block_id in &ctx.loop_data.blocks {
475            for &inst_id in &func.blocks[block_id].instructions {
476                match func.instructions[inst_id].kind {
477                    InstKind::MStore(addr, _)
478                        if self.memory_ranges_may_alias(
479                            func, ctx, load_addr, load_width, addr, 32, block_id,
480                        ) =>
481                    {
482                        return true;
483                    }
484                    InstKind::MStore8(addr, _)
485                        if self.memory_ranges_may_alias(
486                            func, ctx, load_addr, load_width, addr, 1, block_id,
487                        ) =>
488                    {
489                        return true;
490                    }
491                    InstKind::MCopy(_, _, _)
492                    | InstKind::CalldataCopy(_, _, _)
493                    | InstKind::CodeCopy(_, _, _)
494                    | InstKind::ReturnDataCopy(_, _, _)
495                    | InstKind::ExtCodeCopy(_, _, _, _)
496                    | InstKind::Call { .. }
497                    | InstKind::StaticCall { .. }
498                    | InstKind::DelegateCall { .. }
499                    | InstKind::InternalCall { .. } => return true,
500                    InstKind::MSize => return true,
501                    _ => {}
502                }
503            }
504        }
505        false
506    }
507
508    fn loop_may_mutate_storage_slot(
509        &self,
510        func: &Function,
511        ctx: LoopOptContext<'_>,
512        load_inst: InstId,
513        load_slot: ValueId,
514        space: StorageSpace,
515    ) -> bool {
516        let Some(load_alias) =
517            self.storage_alias_for_loop_value(func, load_inst, load_slot, ctx.loop_data)
518        else {
519            return true;
520        };
521        if !self.can_use_storage_alias_for_licm(load_alias, ctx.loop_data) {
522            return true;
523        }
524
525        for &block_id in &ctx.loop_data.blocks {
526            for &inst_id in &func.blocks[block_id].instructions {
527                match (space, &func.instructions[inst_id].kind) {
528                    (StorageSpace::Persistent, InstKind::SStore(slot, _))
529                    | (StorageSpace::Transient, InstKind::TStore(slot, _)) => {
530                        let Some(store_alias) =
531                            self.storage_alias_for_loop_value(func, inst_id, *slot, ctx.loop_data)
532                        else {
533                            return true;
534                        };
535                        if !self.can_use_storage_alias_for_licm(store_alias, ctx.loop_data) {
536                            return true;
537                        }
538                        if load_alias.may_alias(store_alias) {
539                            return true;
540                        }
541                    }
542                    // STATICCALL cannot write storage or transient storage, even reentrantly;
543                    // it only clobbers memory (the return buffer).
544                    (
545                        _,
546                        InstKind::Call { .. }
547                        | InstKind::DelegateCall { .. }
548                        | InstKind::InternalCall { .. }
549                        | InstKind::Create(_, _, _)
550                        | InstKind::Create2(_, _, _, _),
551                    ) => return true,
552                    _ => {}
553                }
554            }
555        }
556        false
557    }
558
559    #[allow(clippy::too_many_arguments)]
560    fn memory_ranges_may_alias(
561        &self,
562        func: &Function,
563        ctx: LoopOptContext<'_>,
564        load_addr: ValueId,
565        load_width: Option<u64>,
566        write_addr: ValueId,
567        write_width: u64,
568        write_block: BlockId,
569    ) -> bool {
570        match (self.const_addr(func, load_addr), load_width, self.const_addr(func, write_addr)) {
571            (Some(load), Some(load_width), Some(write)) => {
572                mir_utils::ranges_overlap(load, load_width, write, write_width)
573            }
574            _ => {
575                let Some(load_width) = load_width else { return true };
576                // The hoist candidate's address is loop-invariant, so its
577                // position never tightens the range.
578                let Some(load) = self.affine_range(func, ctx, load_addr, load_width, None) else {
579                    return true;
580                };
581                let Some(write) =
582                    self.affine_range(func, ctx, write_addr, write_width, Some(write_block))
583                else {
584                    return true;
585                };
586                if load.base != write.base {
587                    return true;
588                }
589                load.start < write.end && write.start < load.end
590            }
591        }
592    }
593
594    fn affine_range(
595        &self,
596        func: &Function,
597        ctx: LoopOptContext<'_>,
598        value: ValueId,
599        width: u64,
600        inst_block: Option<BlockId>,
601    ) -> Option<AffineRange> {
602        let expr = ctx.scev.get(value).cloned().or_else(|| self.const_affine_expr(func, value))?;
603        // Non-header blocks only execute after the header guard passed in
604        // their iteration, so they observe the induction variable strictly
605        // below the bound; everything else (header instructions, deeper
606        // guards, unknown position) also runs in the exiting partial
607        // iteration and sees one more stride.
608        let tight = ctx.loop_data.trip_guard_is_header
609            && inst_block.is_some_and(|block| block != ctx.loop_data.header);
610        self.affine_expr_range(func, ctx.loop_data, expr, width, tight)
611    }
612
613    fn affine_expr_range(
614        &self,
615        func: &Function,
616        loop_data: &Loop,
617        expr: AffineExpr,
618        width: u64,
619        tight: bool,
620    ) -> Option<AffineRange> {
621        let mut start = expr.constant;
622        let mut end = expr.constant;
623        if !expr.terms.is_empty() {
624            let trip_count = i128::from(loop_data.trip_count?);
625            if trip_count == 0 {
626                return None;
627            }
628            let strides = if tight { trip_count.checked_sub(1)? } else { trip_count };
629            for term in expr.terms {
630                let iv = loop_data.induction_vars.iter().find(|iv| iv.value == term.value)?;
631                // `last_iv` below assumes the variable grows from `init`; a descending
632                // variable instead shrinks (and may wrap), so its range is unknown here.
633                if iv.descending {
634                    return None;
635                }
636                let init = self.const_i128(func, iv.init)?;
637                let step = self.const_i128(func, iv.step)?;
638                let first = init.checked_mul(term.scale)?;
639                let last_iv = init.checked_add(step.checked_mul(strides)?)?;
640                let last = last_iv.checked_mul(term.scale)?;
641                start = start.checked_add(first.min(last))?;
642                end = end.checked_add(first.max(last))?;
643            }
644        }
645
646        Some(AffineRange { base: expr.base, start, end: end.checked_add(i128::from(width))? })
647    }
648
649    fn const_affine_expr(&self, func: &Function, value: ValueId) -> Option<AffineExpr> {
650        Some(AffineExpr {
651            base: None,
652            constant: self.const_i128(func, value)?,
653            terms: Default::default(),
654        })
655    }
656
657    fn const_addr(&self, func: &Function, value: ValueId) -> Option<u64> {
658        match func.value(value) {
659            Value::Immediate(imm) => imm.as_u256()?.try_into().ok(),
660            Value::Arg { .. } | Value::Inst(_) | Value::Undef(_) | Value::Error(_) => None,
661        }
662    }
663
664    fn const_condition(&self, func: &Function, value: ValueId) -> Option<bool> {
665        match func.value(value) {
666            Value::Immediate(imm) => Some(!imm.as_u256()?.is_zero()),
667            Value::Arg { .. } | Value::Inst(_) | Value::Undef(_) | Value::Error(_) => None,
668        }
669    }
670
671    fn const_i128(&self, func: &Function, value: ValueId) -> Option<i128> {
672        match func.value(value) {
673            Value::Immediate(imm) => u256_to_i128(imm.as_u256()?),
674            Value::Arg { .. } | Value::Inst(_) | Value::Undef(_) | Value::Error(_) => None,
675        }
676    }
677
678    fn storage_alias_for_loop_value(
679        &self,
680        func: &Function,
681        inst_id: InstId,
682        value: ValueId,
683        loop_data: &Loop,
684    ) -> Option<StorageAlias> {
685        let alias = func.instructions[inst_id]
686            .metadata
687            .storage_alias()
688            .unwrap_or_else(|| StorageAlias::for_value(func, value));
689        if let Some(base) = alias.symbolic_base()
690            && self.value_defined_in_loop(func, base, loop_data)
691        {
692            return None;
693        }
694        Some(alias)
695    }
696
697    fn can_use_storage_alias_for_licm(&self, alias: StorageAlias, loop_data: &Loop) -> bool {
698        matches!(alias, StorageAlias::Slot(_)) || self.loop_has_known_multiple_iterations(loop_data)
699    }
700
701    fn value_defined_in_loop(&self, func: &Function, value: ValueId, loop_data: &Loop) -> bool {
702        match func.value(value) {
703            Value::Inst(inst_id) => self.inst_in_loop(func, *inst_id, loop_data),
704            Value::Undef(_) | Value::Error(_) => true,
705            Value::Arg { .. } | Value::Immediate(_) => false,
706        }
707    }
708
709    fn is_affine_address_base_used_in_loop(
710        &self,
711        func: &Function,
712        inst_id: InstId,
713        ctx: LoopOptContext<'_>,
714    ) -> bool {
715        let Some(result) = func.inst_result_value(inst_id) else { return false };
716        for &block_id in &ctx.loop_data.blocks {
717            for &user_inst in &func.blocks[block_id].instructions {
718                let kind = &func.instructions[user_inst].kind;
719                let address_operands: smallvec::SmallVec<[ValueId; 2]> = match kind {
720                    InstKind::MLoad(addr)
721                    | InstKind::MStore(addr, _)
722                    | InstKind::MStore8(addr, _)
723                    | InstKind::SLoad(addr)
724                    | InstKind::SStore(addr, _)
725                    | InstKind::TLoad(addr)
726                    | InstKind::TStore(addr, _)
727                    | InstKind::CalldataLoad(addr) => smallvec::smallvec![*addr],
728                    InstKind::Keccak256(addr, _)
729                    | InstKind::CalldataCopy(addr, _, _)
730                    | InstKind::CodeCopy(addr, _, _)
731                    | InstKind::ReturnDataCopy(addr, _, _) => smallvec::smallvec![*addr],
732                    InstKind::MCopy(dst, src, _) => smallvec::smallvec![*dst, *src],
733                    InstKind::ExtCodeCopy(_, dst, _, _) => smallvec::smallvec![*dst],
734                    _ => continue,
735                };
736
737                for address in address_operands {
738                    if self.value_feeds_affine_address(func, ctx, result, address, 0) {
739                        return true;
740                    }
741                }
742            }
743        }
744        false
745    }
746
747    fn value_feeds_affine_address(
748        &self,
749        func: &Function,
750        ctx: LoopOptContext<'_>,
751        needle: ValueId,
752        value: ValueId,
753        depth: usize,
754    ) -> bool {
755        if value == needle {
756            return true;
757        }
758        if depth >= 4 || ctx.scev.get(value).is_none() {
759            return false;
760        }
761
762        let Value::Inst(inst_id) = func.value(value) else { return false };
763        if !self.inst_in_loop(func, *inst_id, ctx.loop_data) {
764            return false;
765        }
766        func.instructions[*inst_id]
767            .kind
768            .operands()
769            .iter()
770            .copied()
771            .any(|operand| self.value_feeds_affine_address(func, ctx, needle, operand, depth + 1))
772    }
773
774    fn topological_sort_instructions(&self, func: &Function, insts: &[InstId]) -> Vec<InstId> {
775        let inst_set: FxHashSet<InstId> = insts.iter().copied().collect();
776        let mut result = Vec::new();
777        let mut visited = FxHashSet::default();
778
779        fn visit(
780            func: &Function,
781            inst_id: InstId,
782            inst_set: &FxHashSet<InstId>,
783            visited: &mut FxHashSet<InstId>,
784            result: &mut Vec<InstId>,
785        ) {
786            if visited.contains(&inst_id) {
787                return;
788            }
789            visited.insert(inst_id);
790
791            let inst = &func.instructions[inst_id];
792            for operand in inst.kind.operands() {
793                if let Value::Inst(dep_inst) = &func.values[operand]
794                    && inst_set.contains(dep_inst)
795                {
796                    visit(func, *dep_inst, inst_set, visited, result);
797                }
798            }
799            result.push(inst_id);
800        }
801
802        for &inst_id in insts {
803            visit(func, inst_id, &inst_set, &mut visited, &mut result);
804        }
805
806        result
807    }
808}
809
810fn u256_to_i128(value: U256) -> Option<i128> {
811    if value <= U256::from(i128::MAX as u128) { Some(value.to::<u128>() as i128) } else { None }
812}
813
814#[cfg(test)]
815mod tests {
816    use super::*;
817    use crate::mir::{FunctionBuilder, MirType, Terminator};
818    use solar_interface::Ident;
819
820    #[test]
821    fn licm_hoists_profitable_invariant_mul() {
822        let mut func = Function::new(Ident::DUMMY);
823        let mul_value;
824        let entry;
825        let header;
826        let body;
827
828        {
829            let mut builder = FunctionBuilder::new(&mut func);
830            let x = builder.add_param(MirType::uint256());
831            let seven = builder.imm_u64(7);
832            let cond = builder.imm_bool(true);
833
834            entry = builder.current_block();
835            header = builder.create_block();
836            body = builder.create_block();
837            let exit = builder.create_block();
838
839            builder.jump(header);
840
841            builder.switch_to_block(header);
842            builder.branch(cond, body, exit);
843
844            builder.switch_to_block(body);
845            mul_value = builder.mul(x, seven);
846            builder.jump(header);
847
848            builder.switch_to_block(exit);
849            builder.stop();
850        }
851
852        let Value::Inst(mul_inst) = func.value(mul_value) else {
853            panic!("mul should be an instruction");
854        };
855        let mul_inst = *mul_inst;
856        assert!(func.blocks[body].instructions.contains(&mul_inst));
857
858        let config =
859            LoopOptConfig { enable_licm: true, min_licm_profit: 5, max_licm_hoisted_insts: 4 };
860        let mut optimizer = LoopOptimizer::new(config);
861        optimizer.optimize(&mut func);
862
863        assert!(func.blocks[entry].instructions.contains(&mul_inst));
864        assert!(!func.blocks[body].instructions.contains(&mul_inst));
865        assert!(matches!(func.blocks[header].terminator, Some(Terminator::Branch { .. })));
866    }
867
868    #[test]
869    fn licm_hoists_mload_past_non_overlapping_const_store() {
870        let mut func = Function::new(Ident::DUMMY);
871        let load_value;
872        let entry;
873        let body;
874
875        {
876            let mut builder = FunctionBuilder::new(&mut func);
877            let zero = builder.imm_u64(0);
878            let sixty_four = builder.imm_u64(64);
879            let value = builder.imm_u64(1);
880            let cond = builder.imm_bool(true);
881
882            entry = builder.current_block();
883            let header = builder.create_block();
884            body = builder.create_block();
885            let exit = builder.create_block();
886
887            builder.jump(header);
888
889            builder.switch_to_block(header);
890            builder.branch(cond, body, exit);
891
892            builder.switch_to_block(body);
893            load_value = builder.mload(zero);
894            builder.mstore(sixty_four, value);
895            builder.jump(header);
896
897            builder.switch_to_block(exit);
898            builder.ret(vec![load_value]);
899        }
900
901        let Value::Inst(load_inst) = func.value(load_value) else {
902            panic!("mload should be an instruction");
903        };
904        let load_inst = *load_inst;
905        assert!(func.blocks[body].instructions.contains(&load_inst));
906
907        let config =
908            LoopOptConfig { enable_licm: true, min_licm_profit: 3, max_licm_hoisted_insts: 4 };
909        let mut optimizer = LoopOptimizer::new(config);
910        optimizer.optimize(&mut func);
911
912        assert!(func.blocks[entry].instructions.contains(&load_inst));
913        assert!(!func.blocks[body].instructions.contains(&load_inst));
914    }
915
916    #[test]
917    fn licm_keeps_mload_inside_loop_when_store_overlaps() {
918        let mut func = Function::new(Ident::DUMMY);
919        let load_value;
920        let body;
921
922        {
923            let mut builder = FunctionBuilder::new(&mut func);
924            let zero = builder.imm_u64(0);
925            let overlapping = builder.imm_u64(16);
926            let value = builder.imm_u64(1);
927            let cond = builder.imm_bool(true);
928
929            let header = builder.create_block();
930            body = builder.create_block();
931            let exit = builder.create_block();
932
933            builder.jump(header);
934
935            builder.switch_to_block(header);
936            builder.branch(cond, body, exit);
937
938            builder.switch_to_block(body);
939            load_value = builder.mload(zero);
940            builder.mstore(overlapping, value);
941            builder.jump(header);
942
943            builder.switch_to_block(exit);
944            builder.ret(vec![load_value]);
945        }
946
947        let Value::Inst(load_inst) = func.value(load_value) else {
948            panic!("mload should be an instruction");
949        };
950        let load_inst = *load_inst;
951
952        let config =
953            LoopOptConfig { enable_licm: true, min_licm_profit: 3, max_licm_hoisted_insts: 4 };
954        let mut optimizer = LoopOptimizer::new(config);
955        optimizer.optimize(&mut func);
956
957        assert!(func.blocks[body].instructions.contains(&load_inst));
958    }
959
960    #[test]
961    fn licm_hoists_keccak_past_non_overlapping_const_store() {
962        let mut func = Function::new(Ident::DUMMY);
963        let hash_value;
964        let entry;
965        let body;
966
967        {
968            let mut builder = FunctionBuilder::new(&mut func);
969            let zero = builder.imm_u64(0);
970            let thirty_two = builder.imm_u64(32);
971            let sixty_four = builder.imm_u64(64);
972            let value = builder.imm_u64(1);
973            let cond = builder.imm_bool(true);
974
975            entry = builder.current_block();
976            let header = builder.create_block();
977            body = builder.create_block();
978            let exit = builder.create_block();
979
980            builder.jump(header);
981
982            builder.switch_to_block(header);
983            builder.branch(cond, body, exit);
984
985            builder.switch_to_block(body);
986            hash_value = builder.keccak256(zero, thirty_two);
987            builder.mstore(sixty_four, value);
988            builder.jump(header);
989
990            builder.switch_to_block(exit);
991            builder.ret(vec![hash_value]);
992        }
993
994        let Value::Inst(hash_inst) = func.value(hash_value) else {
995            panic!("keccak256 should be an instruction");
996        };
997        let hash_inst = *hash_inst;
998
999        let config =
1000            LoopOptConfig { enable_licm: true, min_licm_profit: 5, max_licm_hoisted_insts: 4 };
1001        let mut optimizer = LoopOptimizer::new(config);
1002        optimizer.optimize(&mut func);
1003
1004        assert!(func.blocks[entry].instructions.contains(&hash_inst));
1005        assert!(!func.blocks[body].instructions.contains(&hash_inst));
1006    }
1007
1008    #[test]
1009    fn licm_keeps_keccak_inside_loop_when_store_overlaps() {
1010        let mut func = Function::new(Ident::DUMMY);
1011        let hash_value;
1012        let body;
1013
1014        {
1015            let mut builder = FunctionBuilder::new(&mut func);
1016            let zero = builder.imm_u64(0);
1017            let thirty_two = builder.imm_u64(32);
1018            let overlapping = builder.imm_u64(16);
1019            let value = builder.imm_u64(1);
1020            let cond = builder.imm_bool(true);
1021
1022            let header = builder.create_block();
1023            body = builder.create_block();
1024            let exit = builder.create_block();
1025
1026            builder.jump(header);
1027
1028            builder.switch_to_block(header);
1029            builder.branch(cond, body, exit);
1030
1031            builder.switch_to_block(body);
1032            hash_value = builder.keccak256(zero, thirty_two);
1033            builder.mstore(overlapping, value);
1034            builder.jump(header);
1035
1036            builder.switch_to_block(exit);
1037            builder.ret(vec![hash_value]);
1038        }
1039
1040        let Value::Inst(hash_inst) = func.value(hash_value) else {
1041            panic!("keccak256 should be an instruction");
1042        };
1043        let hash_inst = *hash_inst;
1044
1045        let config =
1046            LoopOptConfig { enable_licm: true, min_licm_profit: 5, max_licm_hoisted_insts: 4 };
1047        let mut optimizer = LoopOptimizer::new(config);
1048        optimizer.optimize(&mut func);
1049
1050        assert!(func.blocks[body].instructions.contains(&hash_inst));
1051    }
1052
1053    #[test]
1054    fn licm_hoists_sload_past_different_const_slot_store() {
1055        let mut func = Function::new(Ident::DUMMY);
1056        let load_value;
1057        let entry;
1058        let body;
1059
1060        {
1061            let mut builder = FunctionBuilder::new(&mut func);
1062            let slot_zero = builder.imm_u64(0);
1063            let slot_one = builder.imm_u64(1);
1064            let value = builder.imm_u64(1);
1065            let cond = builder.imm_bool(true);
1066
1067            entry = builder.current_block();
1068            let header = builder.create_block();
1069            body = builder.create_block();
1070            let exit = builder.create_block();
1071
1072            builder.jump(header);
1073
1074            builder.switch_to_block(header);
1075            builder.branch(cond, body, exit);
1076
1077            builder.switch_to_block(body);
1078            load_value = builder.sload(slot_zero);
1079            builder.sstore(slot_one, value);
1080            builder.jump(header);
1081
1082            builder.switch_to_block(exit);
1083            builder.ret(vec![load_value]);
1084        }
1085
1086        let Value::Inst(load_inst) = func.value(load_value) else {
1087            panic!("sload should be an instruction");
1088        };
1089        let load_inst = *load_inst;
1090
1091        let config =
1092            LoopOptConfig { enable_licm: true, min_licm_profit: 5, max_licm_hoisted_insts: 4 };
1093        let mut optimizer = LoopOptimizer::new(config);
1094        optimizer.optimize(&mut func);
1095
1096        assert!(func.blocks[entry].instructions.contains(&load_inst));
1097        assert!(!func.blocks[body].instructions.contains(&load_inst));
1098    }
1099
1100    #[test]
1101    fn licm_keeps_sload_inside_loop_when_store_uses_same_slot() {
1102        let mut func = Function::new(Ident::DUMMY);
1103        let load_value;
1104        let body;
1105
1106        {
1107            let mut builder = FunctionBuilder::new(&mut func);
1108            let slot = builder.imm_u64(0);
1109            let value = builder.imm_u64(1);
1110            let cond = builder.imm_bool(true);
1111
1112            let header = builder.create_block();
1113            body = builder.create_block();
1114            let exit = builder.create_block();
1115
1116            builder.jump(header);
1117
1118            builder.switch_to_block(header);
1119            builder.branch(cond, body, exit);
1120
1121            builder.switch_to_block(body);
1122            load_value = builder.sload(slot);
1123            builder.sstore(slot, value);
1124            builder.jump(header);
1125
1126            builder.switch_to_block(exit);
1127            builder.ret(vec![load_value]);
1128        }
1129
1130        let Value::Inst(load_inst) = func.value(load_value) else {
1131            panic!("sload should be an instruction");
1132        };
1133        let load_inst = *load_inst;
1134
1135        let config =
1136            LoopOptConfig { enable_licm: true, min_licm_profit: 5, max_licm_hoisted_insts: 4 };
1137        let mut optimizer = LoopOptimizer::new(config);
1138        optimizer.optimize(&mut func);
1139
1140        assert!(func.blocks[body].instructions.contains(&load_inst));
1141    }
1142
1143    #[test]
1144    fn licm_does_not_move_work_across_gas_observer() {
1145        let mut func = Function::new(Ident::DUMMY);
1146        let mul_value;
1147        let body;
1148
1149        {
1150            let mut builder = FunctionBuilder::new(&mut func);
1151            let x = builder.add_param(MirType::uint256());
1152            let seven = builder.imm_u64(7);
1153            let cond = builder.imm_bool(true);
1154
1155            let header = builder.create_block();
1156            body = builder.create_block();
1157            let exit = builder.create_block();
1158
1159            builder.jump(header);
1160
1161            builder.switch_to_block(header);
1162            builder.branch(cond, body, exit);
1163
1164            builder.switch_to_block(body);
1165            builder.gas();
1166            mul_value = builder.mul(x, seven);
1167            builder.jump(header);
1168
1169            builder.switch_to_block(exit);
1170            builder.ret(vec![mul_value]);
1171        }
1172
1173        let Value::Inst(mul_inst) = func.value(mul_value) else {
1174            panic!("mul should be an instruction");
1175        };
1176        let mul_inst = *mul_inst;
1177
1178        let config =
1179            LoopOptConfig { enable_licm: true, min_licm_profit: 5, max_licm_hoisted_insts: 4 };
1180        let mut optimizer = LoopOptimizer::new(config);
1181        optimizer.optimize(&mut func);
1182
1183        assert!(func.blocks[body].instructions.contains(&mul_inst));
1184    }
1185}