1use crate::{
18 analysis::{Loop, LoopAnalyzer},
19 mir::{
20 BlockId, Function, Immediate, InstId, InstKind, Instruction, MirType, StorageAlias,
21 Terminator, Value, ValueId, utils as mir_utils,
22 },
23 pass::FunctionPass,
24};
25use alloy_primitives::U256;
26use solar_data_structures::map::FxHashMap;
27
28const LOW_MEMORY_START: u64 = 0x80;
29
30#[derive(Clone, Debug, Default)]
32pub struct StoragePromotionStats {
33 pub loops_promoted: usize,
35 pub loads_promoted: usize,
37 pub stores_promoted: usize,
39}
40
41#[derive(Debug, Default)]
43pub struct StorageScalarPromoter {
44 stats: StoragePromotionStats,
45}
46
47pub struct StorageScalarPromotionPass;
49
50impl FunctionPass for StorageScalarPromotionPass {
51 fn name(&self) -> &str {
52 "storage-promotion"
53 }
54
55 fn run_on_function(&mut self, func: &mut Function) -> bool {
56 let mut promoter = StorageScalarPromoter::new();
57 let stats = promoter.run(func);
58 stats.loops_promoted + stats.loads_promoted + stats.stores_promoted != 0
59 }
60}
61
62#[derive(Clone, Debug)]
63struct Candidate {
64 slot_value: ValueId,
65 slot: StorageAlias,
66 preheader: BlockId,
67 init_store: Option<InstId>,
68 needs_initial_load: bool,
69}
70
71#[derive(Clone, Debug)]
72struct PromotedCandidate {
73 candidate: Candidate,
74 temp_addr: ValueId,
75 dirty_addr: Option<ValueId>,
76 dirty_value: Option<ValueId>,
77}
78
79impl StorageScalarPromoter {
80 pub fn new() -> Self {
82 Self::default()
83 }
84
85 #[must_use]
87 pub const fn stats(&self) -> &StoragePromotionStats {
88 &self.stats
89 }
90
91 pub fn run(&mut self, func: &mut Function) -> &StoragePromotionStats {
93 self.stats = StoragePromotionStats::default();
94
95 if func.selector.is_none() {
98 return &self.stats;
99 }
100
101 func.annotate_storage_aliases(mir_utils::StorageAliasScope::Storage);
102
103 loop {
110 let mut analyzer = LoopAnalyzer::new();
111 let loop_info = analyzer.analyze(func);
112 let mut loops: Vec<Loop> = loop_info.all_loops().cloned().collect();
113 loops.sort_by_key(|loop_data| loop_data.header.index());
114
115 let mut promoted = false;
116 for loop_data in loops {
117 if let Some(candidates) = self.find_initialized_candidates(func, &loop_data) {
118 self.promote_initialized_candidates(func, &loop_data, &candidates);
119 } else if let Some(candidate) = self.find_candidate(func, &loop_data) {
120 self.promote_loop(func, &loop_data, &candidate);
121 } else {
122 continue;
123 }
124 promoted = true;
125 break;
126 }
127 if !promoted {
128 break;
129 }
130 }
131
132 &self.stats
133 }
134
135 fn find_candidate(&self, func: &Function, loop_data: &Loop) -> Option<Candidate> {
136 let preheader = loop_data.preheader?;
137 if loop_data.exit_blocks.is_empty() || !self.has_isolated_promotable_exits(func, loop_data)
138 {
139 return None;
140 }
141 if !self.loop_has_no_unpromotable_side_effects(func, loop_data) {
142 return None;
143 }
144
145 let mut slot: Option<StorageAlias> = None;
146 let mut slot_value: Option<ValueId> = None;
147 let mut saw_loop_store = false;
148
149 for &block_id in &loop_data.blocks {
150 for &inst_id in &func.blocks[block_id].instructions {
151 if let InstKind::SStore(store_slot, _) = &func.instructions[inst_id].kind {
152 let store_key =
153 self.storage_alias_for_loop_value(func, *store_slot, loop_data)?;
154 match slot {
155 Some(existing) if existing != store_key => return None,
156 Some(_) => {}
157 None => {
158 slot = Some(store_key);
159 slot_value = Some(*store_slot);
160 }
161 }
162 saw_loop_store = true;
163 }
164 }
165 }
166
167 if !saw_loop_store {
168 return None;
169 }
170
171 let (slot, slot_value) = (slot?, slot_value?);
172 let rewrite_blocks = self.promotion_block_ids(func, loop_data);
173 if !self.loop_storage_accesses_are_safe(func, &rewrite_blocks, &slot) {
174 return None;
175 }
176 let saw_loop_load = rewrite_blocks.iter().any(|&block_id| {
177 func.blocks[block_id].instructions.iter().any(|&inst_id| {
178 matches!(
179 &func.instructions[inst_id].kind,
180 InstKind::SLoad(load_slot) if func.storage_alias(inst_id, *load_slot) == slot
181 )
182 })
183 });
184 let init_store = self.find_preheader_init_store(func, preheader, &slot);
185 if let Some(init_store) = init_store
186 && !self.preheader_tail_is_safe(func, preheader, init_store, &slot)
187 {
188 return None;
189 }
190 let slot_value = if let Some(init_store) = init_store {
191 self.store_slot(func, init_store)?
192 } else if self.value_defined_in_loop(func, slot_value, loop_data) {
193 return None;
194 } else {
195 slot_value
196 };
197 let needs_initial_load = init_store.is_none() && saw_loop_load;
198
199 Some(Candidate { slot_value, slot, preheader, init_store, needs_initial_load })
200 }
201
202 fn find_initialized_candidates(
203 &self,
204 func: &Function,
205 loop_data: &Loop,
206 ) -> Option<Vec<Candidate>> {
207 let preheader = loop_data.preheader?;
208 if loop_data.exit_blocks.is_empty() || !self.has_isolated_promotable_exits(func, loop_data)
209 {
210 return None;
211 }
212 if !self.loop_has_no_unpromotable_side_effects(func, loop_data) {
213 return None;
214 }
215
216 let mut stores: FxHashMap<StorageAlias, ValueId> = FxHashMap::default();
217 for &block_id in &loop_data.blocks {
218 for &inst_id in &func.blocks[block_id].instructions {
219 if let InstKind::SStore(store_slot, _) = &func.instructions[inst_id].kind {
220 let store_key =
221 self.storage_alias_for_loop_value(func, *store_slot, loop_data)?;
222 stores.entry(store_key).or_insert(*store_slot);
223 }
224 }
225 }
226
227 if stores.len() <= 1 {
228 return None;
229 }
230
231 let keys: Vec<StorageAlias> = stores.keys().copied().collect();
236 if keys.iter().enumerate().any(|(i, a)| keys[i + 1..].iter().any(|b| a.may_alias(*b))) {
237 return None;
238 }
239
240 let mut candidates = Vec::with_capacity(stores.len());
241 for (slot, _) in stores {
242 let init_store = self.find_preheader_init_store(func, preheader, &slot)?;
243 let slot_value = self.store_slot(func, init_store)?;
244 candidates.push(Candidate {
245 slot_value,
246 slot,
247 preheader,
248 init_store: Some(init_store),
249 needs_initial_load: false,
250 });
251 }
252 candidates.sort_by_key(|candidate| candidate.init_store.map(|inst| inst.index()));
253
254 let rewrite_blocks = self.promotion_block_ids(func, loop_data);
255 if !self.loop_storage_accesses_are_safe_for_candidates(func, &rewrite_blocks, &candidates) {
256 return None;
257 }
258 if !self.preheader_tail_is_safe_for_candidates(func, preheader, &candidates) {
259 return None;
260 }
261
262 Some(candidates)
263 }
264
265 fn has_isolated_promotable_exits(&self, func: &Function, loop_data: &Loop) -> bool {
266 loop_data.exit_blocks.iter().all(|&exit| {
267 func.blocks[exit].predecessors.iter().all(|pred| loop_data.blocks.contains(pred))
268 && if self.exit_rolls_back(func, exit) {
269 self.rollback_exit_has_no_observable_effects(func, exit)
270 } else {
271 !matches!(func.blocks[exit].terminator, Some(Terminator::SelfDestruct { .. }))
272 }
273 })
274 }
275
276 fn exit_rolls_back(&self, func: &Function, exit: BlockId) -> bool {
279 matches!(
280 func.blocks[exit].terminator,
281 Some(Terminator::Revert { .. } | Terminator::Invalid)
282 )
283 }
284
285 fn rollback_exit_has_no_observable_effects(&self, func: &Function, exit: BlockId) -> bool {
289 func.blocks[exit].instructions.iter().all(|&inst_id| {
290 !matches!(
291 &func.instructions[inst_id].kind,
292 InstKind::Call { .. }
293 | InstKind::StaticCall { .. }
294 | InstKind::DelegateCall { .. }
295 | InstKind::InternalCall { .. }
296 | InstKind::Create(_, _, _)
297 | InstKind::Create2(_, _, _, _)
298 | InstKind::Gas
299 )
300 })
301 }
302
303 fn promotion_block_ids(&self, func: &Function, loop_data: &Loop) -> Vec<BlockId> {
307 let mut blocks: Vec<BlockId> = loop_data.blocks.iter().copied().collect();
308 blocks.extend(
309 loop_data.exit_blocks.iter().copied().filter(|&exit| self.exit_rolls_back(func, exit)),
310 );
311 blocks
312 }
313
314 fn loop_has_no_unpromotable_side_effects(&self, func: &Function, loop_data: &Loop) -> bool {
315 for &block_id in &loop_data.blocks {
316 if matches!(
317 func.blocks[block_id].terminator,
318 Some(
319 Terminator::Return { .. }
320 | Terminator::Revert { .. }
321 | Terminator::ReturnData { .. }
322 | Terminator::Stop
323 | Terminator::SelfDestruct { .. }
324 | Terminator::Invalid
325 )
326 ) {
327 return false;
328 }
329
330 for &inst_id in &func.blocks[block_id].instructions {
331 let inst = &func.instructions[inst_id];
332 match &inst.kind {
333 InstKind::SLoad(_) | InstKind::SStore(_, _) => {}
334 InstKind::TStore(_, _)
335 | InstKind::Call { .. }
336 | InstKind::StaticCall { .. }
337 | InstKind::DelegateCall { .. }
338 | InstKind::InternalCall { .. }
339 | InstKind::Create(_, _, _)
340 | InstKind::Create2(_, _, _, _)
341 | InstKind::Gas => return false,
342 _ => {}
343 }
344 }
345 }
346 true
347 }
348
349 fn loop_storage_accesses_are_safe(
350 &self,
351 func: &Function,
352 blocks: &[BlockId],
353 candidate: &StorageAlias,
354 ) -> bool {
355 for &block_id in blocks {
356 for &inst_id in &func.blocks[block_id].instructions {
357 match &func.instructions[inst_id].kind {
358 InstKind::SLoad(slot) => {
359 let alias = func.storage_alias(inst_id, *slot);
360 if alias != *candidate && candidate.may_alias(alias) {
361 return false;
362 }
363 }
364 InstKind::SStore(slot, _) => {
365 let alias = func.storage_alias(inst_id, *slot);
366 if alias != *candidate {
367 return false;
368 }
369 }
370 _ => {}
371 }
372 }
373 }
374 true
375 }
376
377 fn loop_storage_accesses_are_safe_for_candidates(
378 &self,
379 func: &Function,
380 blocks: &[BlockId],
381 candidates: &[Candidate],
382 ) -> bool {
383 for &block_id in blocks {
384 for &inst_id in &func.blocks[block_id].instructions {
385 match &func.instructions[inst_id].kind {
386 InstKind::SLoad(slot) => {
387 let alias = func.storage_alias(inst_id, *slot);
388 if self.candidate_index(candidates, &alias).is_none()
389 && candidates.iter().any(|candidate| candidate.slot.may_alias(alias))
390 {
391 return false;
392 }
393 }
394 InstKind::SStore(slot, _) => {
395 let alias = func.storage_alias(inst_id, *slot);
396 if self.candidate_index(candidates, &alias).is_none() {
397 return false;
398 }
399 }
400 _ => {}
401 }
402 }
403 }
404 true
405 }
406
407 fn find_preheader_init_store(
408 &self,
409 func: &Function,
410 preheader: BlockId,
411 slot: &StorageAlias,
412 ) -> Option<InstId> {
413 func.blocks[preheader].instructions.iter().rev().copied().find(|&inst_id| {
414 matches!(
415 &func.instructions[inst_id].kind,
416 InstKind::SStore(store_slot, _) if func.storage_alias(inst_id, *store_slot) == *slot
417 )
418 })
419 }
420
421 fn store_slot(&self, func: &Function, inst_id: InstId) -> Option<ValueId> {
422 match func.instructions[inst_id].kind {
423 InstKind::SStore(slot, _) => Some(slot),
424 _ => None,
425 }
426 }
427
428 fn preheader_tail_is_safe(
429 &self,
430 func: &Function,
431 preheader: BlockId,
432 init_store: InstId,
433 slot: &StorageAlias,
434 ) -> bool {
435 let Some(init_pos) =
436 func.blocks[preheader].instructions.iter().position(|&inst_id| inst_id == init_store)
437 else {
438 return false;
439 };
440
441 for &inst_id in &func.blocks[preheader].instructions[init_pos + 1..] {
442 match &func.instructions[inst_id].kind {
443 InstKind::SLoad(load_slot) => {
444 let alias = func.storage_alias(inst_id, *load_slot);
445 if alias != *slot && slot.may_alias(alias) {
446 return false;
447 }
448 }
449 InstKind::MStore(_, _) | InstKind::MStore8(_, _) | InstKind::MCopy(_, _, _) => {}
450 kind if kind.has_side_effects() => return false,
451 InstKind::Gas => return false,
452 _ => {}
453 }
454 }
455
456 true
457 }
458
459 fn preheader_tail_is_safe_for_candidates(
460 &self,
461 func: &Function,
462 preheader: BlockId,
463 candidates: &[Candidate],
464 ) -> bool {
465 let Some(first_init) = candidates
466 .iter()
467 .filter_map(|candidate| candidate.init_store)
468 .filter_map(|inst_id| {
469 func.blocks[preheader]
470 .instructions
471 .iter()
472 .position(|&candidate| candidate == inst_id)
473 })
474 .min()
475 else {
476 return false;
477 };
478
479 for &inst_id in &func.blocks[preheader].instructions[first_init + 1..] {
480 match &func.instructions[inst_id].kind {
481 InstKind::SLoad(load_slot) | InstKind::SStore(load_slot, _) => {
482 let alias = func.storage_alias(inst_id, *load_slot);
483 if self.candidate_index(candidates, &alias).is_none()
484 && candidates.iter().any(|candidate| candidate.slot.may_alias(alias))
485 {
486 return false;
487 }
488 }
489 InstKind::MStore(_, _) | InstKind::MStore8(_, _) | InstKind::MCopy(_, _, _) => {}
490 kind if kind.has_side_effects() => return false,
491 InstKind::Gas => return false,
492 _ => {}
493 }
494 }
495
496 true
497 }
498
499 fn promote_initialized_candidates(
500 &mut self,
501 func: &mut Function,
502 loop_data: &Loop,
503 candidates: &[Candidate],
504 ) {
505 let promoted: Vec<_> = candidates
506 .iter()
507 .cloned()
508 .map(|candidate| PromotedCandidate {
509 candidate,
510 temp_addr: self.allocate_temp_addr(func),
511 dirty_addr: None,
512 dirty_value: None,
513 })
514 .collect();
515
516 let rewrite_blocks = self.promotion_block_ids(func, loop_data);
517 self.rewrite_preheader_multi(func, &promoted);
518 self.rewrite_loop_body_multi(func, &rewrite_blocks, &promoted);
519
520 for &exit in &loop_data.exit_blocks {
521 if self.exit_rolls_back(func, exit) {
522 continue;
523 }
524 for promoted in &promoted {
525 self.insert_final_store(
526 func,
527 exit,
528 promoted.candidate.slot_value,
529 promoted.temp_addr,
530 );
531 }
532 }
533
534 self.stats.loops_promoted += 1;
535 }
536
537 fn rewrite_preheader_multi(&mut self, func: &mut Function, promoted: &[PromotedCandidate]) {
538 let Some(preheader) = promoted.first().map(|candidate| candidate.candidate.preheader)
539 else {
540 return;
541 };
542
543 let mut temps: FxHashMap<StorageAlias, (ValueId, usize)> = FxHashMap::default();
544 for candidate in promoted {
545 if let Some(init_store) = candidate.candidate.init_store
546 && let InstKind::SStore(_, init) = &func.instructions[init_store].kind
547 {
548 let init_pos = func.blocks[preheader]
549 .instructions
550 .iter()
551 .position(|&inst_id| inst_id == init_store)
552 .expect("candidate init store should be in the preheader");
553 temps.insert(candidate.candidate.slot, (candidate.temp_addr, init_pos));
554 func.instructions[init_store].kind = InstKind::MStore(candidate.temp_addr, *init);
555 func.instructions[init_store].metadata.set_storage_alias(None);
556 self.stats.stores_promoted += 1;
557 }
558 }
559
560 let inst_ids = func.blocks[preheader].instructions.clone();
561 for (pos, inst_id) in inst_ids.into_iter().enumerate() {
562 if let InstKind::SLoad(load_slot) = &func.instructions[inst_id].kind {
563 let alias = func.storage_alias(inst_id, *load_slot);
564 if let Some(&(temp_addr, init_pos)) = temps.get(&alias)
565 && pos > init_pos
566 {
567 func.instructions[inst_id].kind = InstKind::MLoad(temp_addr);
568 func.instructions[inst_id].metadata.set_storage_alias(None);
569 self.stats.loads_promoted += 1;
570 }
571 }
572 }
573 }
574
575 fn rewrite_loop_body_multi(
576 &mut self,
577 func: &mut Function,
578 blocks: &[BlockId],
579 promoted: &[PromotedCandidate],
580 ) {
581 let temps: FxHashMap<StorageAlias, ValueId> =
582 promoted.iter().map(|promoted| (promoted.candidate.slot, promoted.temp_addr)).collect();
583
584 for &block_id in blocks {
585 for &inst_id in &func.blocks[block_id].instructions {
586 let replacement = match &func.instructions[inst_id].kind {
587 InstKind::SLoad(slot) => {
588 let alias = func.storage_alias(inst_id, *slot);
589 temps.get(&alias).copied().map(InstKind::MLoad)
590 }
591 InstKind::SStore(slot, value) => {
592 let alias = func.storage_alias(inst_id, *slot);
593 temps.get(&alias).copied().map(|temp| InstKind::MStore(temp, *value))
594 }
595 _ => None,
596 };
597
598 if let Some(new_kind) = replacement {
599 match new_kind {
600 InstKind::MLoad(_) => self.stats.loads_promoted += 1,
601 InstKind::MStore(_, _) => self.stats.stores_promoted += 1,
602 _ => {}
603 }
604 func.instructions[inst_id].kind = new_kind;
605 func.instructions[inst_id].metadata.set_storage_alias(None);
606 }
607 }
608 }
609 }
610
611 fn promote_loop(&mut self, func: &mut Function, loop_data: &Loop, candidate: &Candidate) {
612 let temp_addr = self.allocate_temp_addr(func);
613 let dirty_addr = candidate.init_store.is_none().then(|| self.allocate_temp_addr(func));
614 let dirty_value = dirty_addr.map(|_| self.bool_word(func, true));
615 let promoted =
616 PromotedCandidate { candidate: candidate.clone(), temp_addr, dirty_addr, dirty_value };
617
618 self.rewrite_preheader(func, &promoted);
619
620 for block_id in self.promotion_block_ids(func, loop_data) {
621 let track_dirty = loop_data.blocks.contains(&block_id);
624 let mut index = 0;
625 while index < func.blocks[block_id].instructions.len() {
626 let inst_id = func.blocks[block_id].instructions[index];
627 let replacement = match &func.instructions[inst_id].kind {
628 InstKind::SLoad(slot)
629 if func.storage_alias(inst_id, *slot) == candidate.slot =>
630 {
631 Some(InstKind::MLoad(temp_addr))
632 }
633 InstKind::SStore(slot, value)
634 if func.storage_alias(inst_id, *slot) == candidate.slot =>
635 {
636 Some(InstKind::MStore(temp_addr, *value))
637 }
638 _ => None,
639 };
640
641 if let Some(new_kind) = replacement {
642 match new_kind {
643 InstKind::MLoad(_) => self.stats.loads_promoted += 1,
644 InstKind::MStore(_, _) => self.stats.stores_promoted += 1,
645 _ => {}
646 }
647 func.instructions[inst_id].kind = new_kind;
648 func.instructions[inst_id].metadata.set_storage_alias(None);
649 if track_dirty
650 && let (Some(dirty_addr), Some(dirty_value)) =
651 (promoted.dirty_addr, promoted.dirty_value)
652 && matches!(func.instructions[inst_id].kind, InstKind::MStore(_, _))
653 {
654 let dirty_store =
655 self.alloc_void_inst(func, InstKind::MStore(dirty_addr, dirty_value));
656 func.blocks[block_id].instructions.insert(index + 1, dirty_store);
657 index += 1;
658 }
659 }
660 index += 1;
661 }
662 }
663
664 for &exit in &loop_data.exit_blocks {
665 if self.exit_rolls_back(func, exit) {
666 continue;
667 }
668 if let Some(dirty_addr) = promoted.dirty_addr {
669 self.insert_conditional_final_store(
670 func,
671 exit,
672 candidate.slot_value,
673 temp_addr,
674 dirty_addr,
675 );
676 } else {
677 self.insert_final_store(func, exit, candidate.slot_value, temp_addr);
678 }
679 }
680
681 self.stats.loops_promoted += 1;
682 }
683
684 fn rewrite_preheader(&mut self, func: &mut Function, promoted: &PromotedCandidate) {
685 let candidate = &promoted.candidate;
686 match candidate.init_store {
687 Some(init_store) => {
688 if let InstKind::SStore(_, init) = &func.instructions[init_store].kind {
689 func.instructions[init_store].kind =
690 InstKind::MStore(promoted.temp_addr, *init);
691 func.instructions[init_store].metadata.set_storage_alias(None);
692 self.stats.stores_promoted += 1;
693 }
694
695 let inst_ids = func.blocks[candidate.preheader].instructions.clone();
696 let mut rewrite = false;
697 for inst_id in inst_ids {
698 if inst_id == init_store {
699 rewrite = true;
700 continue;
701 }
702 if !rewrite {
703 continue;
704 }
705 if let InstKind::SLoad(load_slot) = &func.instructions[inst_id].kind
706 && func.storage_alias(inst_id, *load_slot) == candidate.slot
707 {
708 func.instructions[inst_id].kind = InstKind::MLoad(promoted.temp_addr);
709 func.instructions[inst_id].metadata.set_storage_alias(None);
710 self.stats.loads_promoted += 1;
711 }
712 }
713 }
714 None => {
715 let insert_pos = func.blocks[candidate.preheader].instructions.len();
716 let mut inserted = 0;
717
718 if candidate.needs_initial_load {
719 let (load_inst, load_value) = self.alloc_inst_value(
720 func,
721 InstKind::SLoad(candidate.slot_value),
722 MirType::uint256(),
723 );
724 let store_inst = self
725 .alloc_void_inst(func, InstKind::MStore(promoted.temp_addr, load_value));
726 func.blocks[candidate.preheader]
727 .instructions
728 .insert(insert_pos + inserted, load_inst);
729 inserted += 1;
730 func.blocks[candidate.preheader]
731 .instructions
732 .insert(insert_pos + inserted, store_inst);
733 inserted += 1;
734 }
735
736 if let Some(dirty_addr) = promoted.dirty_addr {
737 let false_word = self.bool_word(func, false);
738 let dirty_store =
739 self.alloc_void_inst(func, InstKind::MStore(dirty_addr, false_word));
740 func.blocks[candidate.preheader]
741 .instructions
742 .insert(insert_pos + inserted, dirty_store);
743 }
744 }
745 }
746 }
747
748 fn insert_final_store(
749 &mut self,
750 func: &mut Function,
751 exit: BlockId,
752 slot_value: ValueId,
753 temp_addr: ValueId,
754 ) {
755 let load_inst =
756 func.alloc_inst(Instruction::new(InstKind::MLoad(temp_addr), Some(MirType::uint256())));
757 let load_value = func.alloc_value(Value::Inst(load_inst));
758 let store_inst =
759 func.alloc_inst(Instruction::new(InstKind::SStore(slot_value, load_value), None));
760
761 let insert_pos = func.blocks[exit]
762 .instructions
763 .iter()
764 .take_while(|&&inst_id| matches!(func.instructions[inst_id].kind, InstKind::Phi(_)))
765 .count();
766 func.blocks[exit].instructions.insert(insert_pos, store_inst);
767 func.blocks[exit].instructions.insert(insert_pos, load_inst);
768 }
769
770 fn insert_conditional_final_store(
771 &mut self,
772 func: &mut Function,
773 exit: BlockId,
774 slot_value: ValueId,
775 temp_addr: ValueId,
776 dirty_addr: ValueId,
777 ) {
778 let continuation = func.alloc_block();
779 let store_block = func.alloc_block();
780
781 let old_instructions = std::mem::take(&mut func.blocks[exit].instructions);
782 let old_terminator = func.blocks[exit].terminator.take();
783 let old_successors =
784 old_terminator.as_ref().map(Terminator::successors).unwrap_or_default();
785
786 let split_pos = old_instructions
788 .iter()
789 .take_while(|&&inst_id| matches!(func.instructions[inst_id].kind, InstKind::Phi(_)))
790 .count();
791 let mut exit_instructions = old_instructions[..split_pos].to_vec();
792 let continuation_instructions = old_instructions[split_pos..].to_vec();
793
794 let dirty_load_inst =
795 func.alloc_inst(Instruction::new(InstKind::MLoad(dirty_addr), Some(MirType::Bool)));
796 let dirty_value = func.alloc_value(Value::Inst(dirty_load_inst));
797 exit_instructions.push(dirty_load_inst);
798
799 func.blocks[exit].instructions = exit_instructions;
800 func.blocks[exit].terminator = Some(Terminator::Branch {
801 condition: dirty_value,
802 then_block: store_block,
803 else_block: continuation,
804 });
805
806 let load_inst =
807 func.alloc_inst(Instruction::new(InstKind::MLoad(temp_addr), Some(MirType::uint256())));
808 let load_value = func.alloc_value(Value::Inst(load_inst));
809 let store_inst =
810 func.alloc_inst(Instruction::new(InstKind::SStore(slot_value, load_value), None));
811
812 func.blocks[store_block].predecessors.push(exit);
813 func.blocks[store_block].instructions.push(load_inst);
814 func.blocks[store_block].instructions.push(store_inst);
815 func.blocks[store_block].terminator = Some(Terminator::Jump(continuation));
816
817 func.blocks[continuation].predecessors.push(exit);
818 func.blocks[continuation].predecessors.push(store_block);
819 func.blocks[continuation].instructions = continuation_instructions;
820 func.blocks[continuation].terminator = old_terminator;
821
822 self.redirect_successor_phi_incoming(func, exit, continuation, &old_successors);
823 for successor in old_successors {
824 for pred in &mut func.blocks[successor].predecessors {
825 if *pred == exit {
826 *pred = continuation;
827 }
828 }
829 }
830 }
831
832 fn allocate_temp_addr(&self, func: &mut Function) -> ValueId {
833 let frame_offset = func.internal_frame_size.max(func.external_static_return_size);
834 let temp_addr = LOW_MEMORY_START + frame_offset;
835 func.internal_frame_size = func.internal_frame_size.max(frame_offset + 32);
836 func.alloc_value(Value::Immediate(Immediate::uint256(U256::from(temp_addr))))
837 }
838
839 fn redirect_successor_phi_incoming(
840 &self,
841 func: &mut Function,
842 old_pred: BlockId,
843 new_pred: BlockId,
844 successors: &[BlockId],
845 ) {
846 for &successor in successors {
847 for idx in 0..func.blocks[successor].instructions.len() {
848 let inst_id = func.blocks[successor].instructions[idx];
849 if !matches!(func.instructions[inst_id].kind, InstKind::Phi(_)) {
850 break;
851 }
852 let InstKind::Phi(incoming) = &mut func.instructions[inst_id].kind else {
853 continue;
854 };
855 for (pred, _) in incoming {
856 if *pred == old_pred {
857 *pred = new_pred;
858 }
859 }
860 }
861 }
862 }
863
864 fn bool_word(&self, func: &mut Function, value: bool) -> ValueId {
865 func.alloc_value(Value::Immediate(Immediate::bool(value)))
866 }
867
868 fn alloc_inst_value(
869 &self,
870 func: &mut Function,
871 kind: InstKind,
872 ty: MirType,
873 ) -> (InstId, ValueId) {
874 let inst = func.alloc_inst(Instruction::new(kind, Some(ty)));
875 let value = func.alloc_value(Value::Inst(inst));
876 (inst, value)
877 }
878
879 fn alloc_void_inst(&self, func: &mut Function, kind: InstKind) -> InstId {
882 func.alloc_inst(Instruction::new(kind, None))
883 }
884
885 fn storage_alias_for_loop_value(
886 &self,
887 func: &Function,
888 value: ValueId,
889 loop_data: &Loop,
890 ) -> Option<StorageAlias> {
891 let alias = StorageAlias::for_value(func, value);
892 if let Some(base) = alias.symbolic_base()
893 && self.value_defined_in_loop(func, base, loop_data)
894 {
895 return None;
896 }
897 Some(alias)
898 }
899
900 fn value_defined_in_loop(&self, func: &Function, value: ValueId, loop_data: &Loop) -> bool {
901 match func.value(value) {
902 Value::Inst(inst_id) => loop_data
903 .blocks
904 .iter()
905 .any(|&block_id| func.blocks[block_id].instructions.contains(inst_id)),
906 Value::Undef(_) | Value::Error(_) => true,
907 Value::Arg { .. } | Value::Immediate(_) => false,
908 }
909 }
910
911 fn candidate_index(&self, candidates: &[Candidate], alias: &StorageAlias) -> Option<usize> {
912 candidates.iter().position(|candidate| candidate.slot == *alias)
913 }
914}
915
916#[cfg(test)]
917mod tests {
918 use super::*;
919 use crate::mir::{Immediate, Terminator};
920 use solar_interface::Ident;
921
922 struct TestLoop {
923 func: Function,
924 entry_store: InstId,
925 body_load: InstId,
926 body_store: InstId,
927 exit: BlockId,
928 }
929
930 struct NoInitLoop {
931 func: Function,
932 body_load: InstId,
933 body_store: InstId,
934 exit: BlockId,
935 }
936
937 fn imm(func: &mut Function, value: u64) -> ValueId {
938 func.alloc_value(Value::Immediate(Immediate::uint256(U256::from(value))))
939 }
940
941 fn inst_value(
942 func: &mut Function,
943 block: BlockId,
944 kind: InstKind,
945 ty: Option<MirType>,
946 ) -> (InstId, ValueId) {
947 let inst = func.alloc_inst(Instruction::new(kind, ty));
948 func.blocks[block].instructions.push(inst);
949 let value = func.alloc_value(Value::Inst(inst));
950 (inst, value)
951 }
952
953 fn inst(func: &mut Function, block: BlockId, kind: InstKind, ty: Option<MirType>) -> InstId {
954 inst_value(func, block, kind, ty).0
955 }
956
957 fn make_storage_loop(external: bool) -> TestLoop {
958 let mut func = Function::new(Ident::DUMMY);
959 if external {
960 func.selector = Some([0, 0, 0, 1]);
961 }
962
963 let entry = func.entry_block;
964 let header = func.alloc_block();
965 let body = func.alloc_block();
966 let update = func.alloc_block();
967 let exit = func.alloc_block();
968
969 let slot = imm(&mut func, 0);
970 let one = imm(&mut func, 1);
971 let two = imm(&mut func, 2);
972 let cond = imm(&mut func, 1);
973
974 let entry_store = inst(&mut func, entry, InstKind::SStore(slot, one), None);
975 func.blocks[entry].terminator = Some(Terminator::Jump(header));
976 func.blocks[header].predecessors.push(entry);
977
978 func.blocks[header].terminator =
979 Some(Terminator::Branch { condition: cond, then_block: body, else_block: exit });
980 func.blocks[body].predecessors.push(header);
981 func.blocks[exit].predecessors.push(header);
982
983 let body_load = inst(&mut func, body, InstKind::SLoad(slot), Some(MirType::uint256()));
984 let loaded = match func.values.iter_enumerated().find_map(|(value_id, value)| match value {
985 Value::Inst(inst_id) if *inst_id == body_load => Some(value_id),
986 _ => None,
987 }) {
988 Some(value) => value,
989 None => panic!("missing load result"),
990 };
991 let mul = inst(&mut func, body, InstKind::Mul(loaded, two), Some(MirType::uint256()));
992 let product =
993 match func.values.iter_enumerated().find_map(|(value_id, value)| match value {
994 Value::Inst(inst_id) if *inst_id == mul => Some(value_id),
995 _ => None,
996 }) {
997 Some(value) => value,
998 None => panic!("missing product result"),
999 };
1000 let body_store = inst(&mut func, body, InstKind::SStore(slot, product), None);
1001 func.blocks[body].terminator = Some(Terminator::Jump(update));
1002 func.blocks[update].predecessors.push(body);
1003
1004 func.blocks[update].terminator = Some(Terminator::Jump(header));
1005 func.blocks[header].predecessors.push(update);
1006
1007 func.blocks[exit].terminator = Some(Terminator::Stop);
1008
1009 TestLoop { func, entry_store, body_load, body_store, exit }
1010 }
1011
1012 fn make_storage_loop_without_init() -> NoInitLoop {
1013 let mut func = Function::new(Ident::DUMMY);
1014 func.selector = Some([0, 0, 0, 1]);
1015
1016 let entry = func.entry_block;
1017 let header = func.alloc_block();
1018 let body = func.alloc_block();
1019 let update = func.alloc_block();
1020 let exit = func.alloc_block();
1021
1022 let slot = imm(&mut func, 0);
1023 let two = imm(&mut func, 2);
1024 let cond = imm(&mut func, 1);
1025
1026 func.blocks[entry].terminator = Some(Terminator::Jump(header));
1027 func.blocks[header].predecessors.push(entry);
1028
1029 func.blocks[header].terminator =
1030 Some(Terminator::Branch { condition: cond, then_block: body, else_block: exit });
1031 func.blocks[body].predecessors.push(header);
1032 func.blocks[exit].predecessors.push(header);
1033
1034 let body_load = inst(&mut func, body, InstKind::SLoad(slot), Some(MirType::uint256()));
1035 let loaded = match func.values.iter_enumerated().find_map(|(value_id, value)| match value {
1036 Value::Inst(inst_id) if *inst_id == body_load => Some(value_id),
1037 _ => None,
1038 }) {
1039 Some(value) => value,
1040 None => panic!("missing load result"),
1041 };
1042 let add = inst(&mut func, body, InstKind::Add(loaded, two), Some(MirType::uint256()));
1043 let sum = match func.values.iter_enumerated().find_map(|(value_id, value)| match value {
1044 Value::Inst(inst_id) if *inst_id == add => Some(value_id),
1045 _ => None,
1046 }) {
1047 Some(value) => value,
1048 None => panic!("missing sum result"),
1049 };
1050 let body_store = inst(&mut func, body, InstKind::SStore(slot, sum), None);
1051 func.blocks[body].terminator = Some(Terminator::Jump(update));
1052 func.blocks[update].predecessors.push(body);
1053
1054 func.blocks[update].terminator = Some(Terminator::Jump(header));
1055 func.blocks[header].predecessors.push(update);
1056
1057 func.blocks[exit].terminator = Some(Terminator::Stop);
1058
1059 NoInitLoop { func, body_load, body_store, exit }
1060 }
1061
1062 fn make_store_only_loop_without_init() -> NoInitLoop {
1063 let mut func = Function::new(Ident::DUMMY);
1064 func.selector = Some([0, 0, 0, 1]);
1065
1066 let entry = func.entry_block;
1067 let header = func.alloc_block();
1068 let body = func.alloc_block();
1069 let update = func.alloc_block();
1070 let exit = func.alloc_block();
1071
1072 let slot = imm(&mut func, 0);
1073 let value = imm(&mut func, 2);
1074 let cond = imm(&mut func, 1);
1075
1076 func.blocks[entry].terminator = Some(Terminator::Jump(header));
1077 func.blocks[header].predecessors.push(entry);
1078
1079 func.blocks[header].terminator =
1080 Some(Terminator::Branch { condition: cond, then_block: body, else_block: exit });
1081 func.blocks[body].predecessors.push(header);
1082 func.blocks[exit].predecessors.push(header);
1083
1084 let body_store = inst(&mut func, body, InstKind::SStore(slot, value), None);
1085 func.blocks[body].terminator = Some(Terminator::Jump(update));
1086 func.blocks[update].predecessors.push(body);
1087
1088 func.blocks[update].terminator = Some(Terminator::Jump(header));
1089 func.blocks[header].predecessors.push(update);
1090
1091 func.blocks[exit].terminator = Some(Terminator::Stop);
1092
1093 NoInitLoop { func, body_load: body_store, body_store, exit }
1094 }
1095
1096 fn make_symbolic_storage_loop() -> TestLoop {
1097 let mut func = Function::new(Ident::DUMMY);
1098 func.selector = Some([0, 0, 0, 1]);
1099 func.params.push(MirType::uint256());
1100
1101 let entry = func.entry_block;
1102 let header = func.alloc_block();
1103 let body = func.alloc_block();
1104 let update = func.alloc_block();
1105 let exit = func.alloc_block();
1106
1107 let slot = func.alloc_value(Value::Arg { index: 0, ty: MirType::uint256() });
1108 let one = imm(&mut func, 1);
1109 let two = imm(&mut func, 2);
1110 let cond = imm(&mut func, 1);
1111
1112 let entry_store = inst(&mut func, entry, InstKind::SStore(slot, one), None);
1113 func.blocks[entry].terminator = Some(Terminator::Jump(header));
1114 func.blocks[header].predecessors.push(entry);
1115
1116 func.blocks[header].terminator =
1117 Some(Terminator::Branch { condition: cond, then_block: body, else_block: exit });
1118 func.blocks[body].predecessors.push(header);
1119 func.blocks[exit].predecessors.push(header);
1120
1121 let (body_load, loaded) =
1122 inst_value(&mut func, body, InstKind::SLoad(slot), Some(MirType::uint256()));
1123 let (_, product) =
1124 inst_value(&mut func, body, InstKind::Mul(loaded, two), Some(MirType::uint256()));
1125 let body_store = inst(&mut func, body, InstKind::SStore(slot, product), None);
1126 func.blocks[body].terminator = Some(Terminator::Jump(update));
1127 func.blocks[update].predecessors.push(body);
1128
1129 func.blocks[update].terminator = Some(Terminator::Jump(header));
1130 func.blocks[header].predecessors.push(update);
1131
1132 func.blocks[exit].terminator = Some(Terminator::Stop);
1133
1134 TestLoop { func, entry_store, body_load, body_store, exit }
1135 }
1136
1137 fn make_loop_variant_symbolic_storage_loop() -> NoInitLoop {
1138 let mut func = Function::new(Ident::DUMMY);
1139 func.selector = Some([0, 0, 0, 1]);
1140 func.params.push(MirType::uint256());
1141
1142 let entry = func.entry_block;
1143 let header = func.alloc_block();
1144 let body = func.alloc_block();
1145 let update = func.alloc_block();
1146 let exit = func.alloc_block();
1147
1148 let seed = func.alloc_value(Value::Arg { index: 0, ty: MirType::uint256() });
1149 let zero = imm(&mut func, 0);
1150 let two = imm(&mut func, 2);
1151 let cond = imm(&mut func, 1);
1152
1153 func.blocks[entry].terminator = Some(Terminator::Jump(header));
1154 func.blocks[header].predecessors.push(entry);
1155
1156 func.blocks[header].terminator =
1157 Some(Terminator::Branch { condition: cond, then_block: body, else_block: exit });
1158 func.blocks[body].predecessors.push(header);
1159 func.blocks[exit].predecessors.push(header);
1160
1161 let (_, slot) =
1162 inst_value(&mut func, body, InstKind::Add(seed, zero), Some(MirType::uint256()));
1163 let (body_load, loaded) =
1164 inst_value(&mut func, body, InstKind::SLoad(slot), Some(MirType::uint256()));
1165 let (_, sum) =
1166 inst_value(&mut func, body, InstKind::Add(loaded, two), Some(MirType::uint256()));
1167 let body_store = inst(&mut func, body, InstKind::SStore(slot, sum), None);
1168 func.blocks[body].terminator = Some(Terminator::Jump(update));
1169 func.blocks[update].predecessors.push(body);
1170
1171 func.blocks[update].terminator = Some(Terminator::Jump(header));
1172 func.blocks[header].predecessors.push(update);
1173
1174 func.blocks[exit].terminator = Some(Terminator::Stop);
1175
1176 NoInitLoop { func, body_load, body_store, exit }
1177 }
1178
1179 fn make_symbolic_loop_with_possibly_aliasing_load() -> TestLoop {
1180 let mut test = make_symbolic_storage_loop();
1181 let const_slot = imm(&mut test.func, 0);
1182 let body = test
1183 .func
1184 .blocks
1185 .iter_enumerated()
1186 .find_map(|(block_id, block)| {
1187 block.instructions.contains(&test.body_load).then_some(block_id)
1188 })
1189 .expect("missing body block");
1190 inst(&mut test.func, body, InstKind::SLoad(const_slot), Some(MirType::uint256()));
1191 test
1192 }
1193
1194 #[test]
1195 fn promotes_external_storage_update_loop() {
1196 let mut test = make_storage_loop(true);
1197 let mut pass = StorageScalarPromoter::new();
1198 let stats = pass.run(&mut test.func);
1199
1200 assert_eq!(stats.loops_promoted, 1);
1201 assert_eq!(stats.loads_promoted, 1);
1202 assert_eq!(stats.stores_promoted, 2);
1203 assert!(matches!(test.func.instructions[test.entry_store].kind, InstKind::MStore(_, _)));
1204 assert!(matches!(test.func.instructions[test.body_load].kind, InstKind::MLoad(_)));
1205 assert!(matches!(test.func.instructions[test.body_store].kind, InstKind::MStore(_, _)));
1206 assert!(matches!(
1207 test.func.instructions[test.func.blocks[test.exit].instructions[0]].kind,
1208 InstKind::MLoad(_)
1209 ));
1210 assert!(matches!(
1211 test.func.instructions[test.func.blocks[test.exit].instructions[1]].kind,
1212 InstKind::SStore(_, _)
1213 ));
1214 }
1215
1216 #[test]
1217 fn skips_non_external_functions() {
1218 let mut test = make_storage_loop(false);
1219 let mut pass = StorageScalarPromoter::new();
1220 let stats = pass.run(&mut test.func);
1221
1222 assert_eq!(stats.loops_promoted, 0);
1223 assert!(matches!(test.func.instructions[test.entry_store].kind, InstKind::SStore(_, _)));
1224 assert!(matches!(test.func.instructions[test.body_load].kind, InstKind::SLoad(_)));
1225 assert!(matches!(test.func.instructions[test.body_store].kind, InstKind::SStore(_, _)));
1226 }
1227
1228 #[test]
1229 fn promotes_storage_update_loop_without_preheader_store() {
1230 let mut test = make_storage_loop_without_init();
1231 let entry = test.func.entry_block;
1232 let mut pass = StorageScalarPromoter::new();
1233 let stats = pass.run(&mut test.func);
1234
1235 assert_eq!(stats.loops_promoted, 1);
1236 assert_eq!(stats.loads_promoted, 1);
1237 assert_eq!(stats.stores_promoted, 1);
1238 assert!(matches!(
1239 test.func.instructions[test.func.blocks[entry].instructions[0]].kind,
1240 InstKind::SLoad(_)
1241 ));
1242 assert!(matches!(
1243 test.func.instructions[test.func.blocks[entry].instructions[1]].kind,
1244 InstKind::MStore(_, _)
1245 ));
1246 assert!(matches!(
1247 test.func.instructions[test.func.blocks[entry].instructions[2]].kind,
1248 InstKind::MStore(_, _)
1249 ));
1250 assert!(matches!(test.func.instructions[test.body_load].kind, InstKind::MLoad(_)));
1251 assert!(matches!(test.func.instructions[test.body_store].kind, InstKind::MStore(_, _)));
1252
1253 let dirty_store_pos = test.func.blocks.iter().find_map(|block| {
1254 block
1255 .instructions
1256 .iter()
1257 .position(|&inst_id| inst_id == test.body_store)
1258 .map(|pos| (block, pos + 1))
1259 });
1260 let Some((body_block, dirty_store_pos)) = dirty_store_pos else {
1261 panic!("missing promoted body store");
1262 };
1263 assert!(matches!(
1264 test.func.instructions[body_block.instructions[dirty_store_pos]].kind,
1265 InstKind::MStore(_, _)
1266 ));
1267
1268 assert!(matches!(
1269 test.func.instructions[test.func.blocks[test.exit].instructions[0]].kind,
1270 InstKind::MLoad(_)
1271 ));
1272 let Some(Terminator::Branch { then_block, else_block, .. }) =
1273 test.func.blocks[test.exit].terminator.as_ref()
1274 else {
1275 panic!("dirty exit should branch");
1276 };
1277 assert!(matches!(
1278 test.func.instructions[test.func.blocks[*then_block].instructions[0]].kind,
1279 InstKind::MLoad(_)
1280 ));
1281 assert!(matches!(
1282 test.func.instructions[test.func.blocks[*then_block].instructions[1]].kind,
1283 InstKind::SStore(_, _)
1284 ));
1285 assert_eq!(test.func.blocks[*else_block].terminator, Some(Terminator::Stop));
1286 }
1287
1288 #[test]
1289 fn promotes_store_only_loop_without_preheader_store() {
1290 let mut test = make_store_only_loop_without_init();
1291 let entry = test.func.entry_block;
1292 let mut pass = StorageScalarPromoter::new();
1293 let stats = pass.run(&mut test.func);
1294
1295 assert_eq!(stats.loops_promoted, 1);
1296 assert_eq!(stats.loads_promoted, 0);
1297 assert_eq!(stats.stores_promoted, 1);
1298 assert_eq!(test.func.blocks[entry].instructions.len(), 1);
1299 assert!(matches!(
1300 test.func.instructions[test.func.blocks[entry].instructions[0]].kind,
1301 InstKind::MStore(_, _)
1302 ));
1303 assert!(matches!(test.func.instructions[test.body_store].kind, InstKind::MStore(_, _)));
1304
1305 let Some(Terminator::Branch { then_block, else_block, .. }) =
1306 test.func.blocks[test.exit].terminator.as_ref()
1307 else {
1308 panic!("dirty exit should branch");
1309 };
1310 assert!(matches!(
1311 test.func.instructions[test.func.blocks[*then_block].instructions[0]].kind,
1312 InstKind::MLoad(_)
1313 ));
1314 assert!(matches!(
1315 test.func.instructions[test.func.blocks[*then_block].instructions[1]].kind,
1316 InstKind::SStore(_, _)
1317 ));
1318 assert_eq!(test.func.blocks[*else_block].terminator, Some(Terminator::Stop));
1319 }
1320
1321 #[test]
1322 fn promotes_invariant_symbolic_storage_slot() {
1323 let mut test = make_symbolic_storage_loop();
1324 let mut pass = StorageScalarPromoter::new();
1325 let stats = pass.run(&mut test.func);
1326
1327 assert_eq!(stats.loops_promoted, 1);
1328 assert_eq!(stats.loads_promoted, 1);
1329 assert_eq!(stats.stores_promoted, 2);
1330 assert!(matches!(test.func.instructions[test.entry_store].kind, InstKind::MStore(_, _)));
1331 assert!(matches!(test.func.instructions[test.body_load].kind, InstKind::MLoad(_)));
1332 assert!(matches!(test.func.instructions[test.body_store].kind, InstKind::MStore(_, _)));
1333 }
1334
1335 #[test]
1336 fn skips_loop_variant_symbolic_storage_slot() {
1337 let mut test = make_loop_variant_symbolic_storage_loop();
1338 let mut pass = StorageScalarPromoter::new();
1339 let stats = pass.run(&mut test.func);
1340
1341 assert_eq!(stats.loops_promoted, 0);
1342 assert!(matches!(test.func.instructions[test.body_load].kind, InstKind::SLoad(_)));
1343 assert!(matches!(test.func.instructions[test.body_store].kind, InstKind::SStore(_, _)));
1344 }
1345
1346 #[test]
1347 fn skips_symbolic_slot_with_possibly_aliasing_storage_load() {
1348 let mut test = make_symbolic_loop_with_possibly_aliasing_load();
1349 let mut pass = StorageScalarPromoter::new();
1350 let stats = pass.run(&mut test.func);
1351
1352 assert_eq!(stats.loops_promoted, 0);
1353 assert!(matches!(test.func.instructions[test.entry_store].kind, InstKind::SStore(_, _)));
1354 assert!(matches!(test.func.instructions[test.body_load].kind, InstKind::SLoad(_)));
1355 assert!(matches!(test.func.instructions[test.body_store].kind, InstKind::SStore(_, _)));
1356 }
1357}