Skip to main content

solar_codegen/transform/
storage_promotion.rs

1//! Storage scalar promotion for simple loop-carried storage updates.
2//!
3//! This pass recognizes loops that repeatedly update one or more exact storage
4//! slots and rewrites the loop to update memory-backed scalars instead. Final
5//! values are stored back to storage once on each clean loop exit.
6//!
7//! Safety contract:
8//! - promote only exact storage aliases that are loop-invariant
9//! - promote multiple slots only when they are pairwise provably disjoint
10//! - reject loops with calls, unknown storage writes, or non-isolated exits
11//! - flush dirty promoted values before any clean observable exit
12//! - skip the flush on revert exits: `revert`/`invalid` roll back every storage write of the frame,
13//!   so the unflushed slot is unobservable there; reads of the promoted slot on those paths are
14//!   rewritten to the memory temp so revert data still sees the current value
15//! - leave loop-variant mapping/array slots in storage
16
17use 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/// Statistics from storage scalar promotion.
31#[derive(Clone, Debug, Default)]
32pub struct StoragePromotionStats {
33    /// Number of loops promoted.
34    pub loops_promoted: usize,
35    /// Number of storage loads rewritten to memory loads.
36    pub loads_promoted: usize,
37    /// Number of storage stores rewritten to memory stores.
38    pub stores_promoted: usize,
39}
40
41/// Promotes loop-carried storage values to memory-backed scalars.
42#[derive(Debug, Default)]
43pub struct StorageScalarPromoter {
44    stats: StoragePromotionStats,
45}
46
47/// Function pass for loop-carried storage scalar promotion.
48pub 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    /// Creates a new storage scalar promotion pass.
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Returns statistics for the most recent run.
86    #[must_use]
87    pub const fn stats(&self) -> &StoragePromotionStats {
88        &self.stats
89    }
90
91    /// Runs the pass on one function.
92    pub fn run(&mut self, func: &mut Function) -> &StoragePromotionStats {
93        self.stats = StoragePromotionStats::default();
94
95        // The pass currently introduces absolute low-memory temporaries, so it
96        // only handles externally callable runtime entries.
97        if func.selector.is_none() {
98            return &self.stats;
99        }
100
101        func.annotate_storage_aliases(mir_utils::StorageAliasScope::Storage);
102
103        // Promoting a loop can split its exit blocks and relocate the final
104        // stores into new blocks, which invalidates the block sets of every
105        // enclosing loop. Recompute loop info after each successful promotion
106        // so later promotions reason about an accurate CFG. This terminates:
107        // each promotion rewrites all of its loop's storage stores to memory
108        // stores and only inserts new storage stores outside that loop.
109        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        // Distinct alias keys can still name the same runtime slot (for
232        // example two symbolic mapping slots whose keys happen to be equal).
233        // Promoting them to separate memory temps would desynchronize loads
234        // and stores, so require every pair to be provably disjoint.
235        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    /// Returns true if the exit block ends by rolling back all storage writes
277    /// of the frame, which makes skipping the promoted-value flush sound.
278    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    /// A rollback exit must not contain calls (a callee could observe the
286    /// unflushed slot and leak that observation into the revert data) or
287    /// other instructions whose results escape the rolled-back frame.
288    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    /// Blocks whose storage accesses are checked and rewritten by promotion:
304    /// the loop body plus rollback exits, whose reads of the promoted slot
305    /// must observe the memory temp instead of the stale storage value.
306    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            // Rollback exits never flush, so their rewritten stores do not
622            // need to update the dirty flag.
623            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        // Keep existing exit phis in place; only the non-phi tail moves behind the dirty check.
787        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    /// Allocates an instruction that produces no value, so no result [`Value`]
880    /// entry is created for it.
881    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}