1use 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#[derive(Clone, Debug)]
46pub struct LoopOptConfig {
47 pub enable_licm: bool,
49 pub min_licm_profit: u16,
51 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#[derive(Clone, Debug, Default)]
63pub struct LoopOptStats {
64 pub instructions_hoisted: usize,
66}
67
68#[derive(Debug)]
70pub struct LoopOptimizer {
71 config: LoopOptConfig,
72 stats: LoopOptStats,
73}
74
75pub 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 pub fn new(config: LoopOptConfig) -> Self {
99 Self { config, stats: LoopOptStats::default() }
100 }
101
102 #[must_use]
104 pub fn stats(&self) -> &LoopOptStats {
105 &self.stats
106 }
107
108 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 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 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 InstKind::MSize => return false,
288 InstKind::Balance(_)
292 | InstKind::SelfBalance
293 | InstKind::ExtCodeSize(_)
294 | InstKind::ExtCodeHash(_)
295 | InstKind::ReturnDataSize => {
296 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 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 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 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 (
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 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 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 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}