Skip to main content

solar_codegen/transform/
frame_promotion.rs

1//! Compiler-local scalar promotion.
2//!
3//! This is Solar's EVM-shaped version of LLVM's mem2reg: promote compiler-owned
4//! local slots from memory traffic into SSA values. The pass is deliberately
5//! conservative. A slot is promotable only when its address is used as the exact
6//! address of full-word `mload`/`mstore` instructions, and the function has no
7//! observations that could make removing that memory traffic visible.
8//!
9//! Safety contract:
10//! - promote only compiler-owned internal-frame or external-local slots
11//! - reject escaped addresses, partial stores, dynamic memory aliases, calls, returndata
12//!   observations, and ABI return-buffer overlap
13//! - preserve SSA values across control flow with explicit phi insertion
14
15use crate::{
16    analysis::CfgInfo,
17    mir::{
18        BlockId, Function, InstId, InstKind, Instruction, MirType, Terminator, Value, ValueId,
19        utils::{self as mir_utils, repair_reachability_phis},
20    },
21    pass::FunctionPass,
22};
23use solar_data_structures::map::{FxHashMap, FxHashSet};
24
25const LOW_MEMORY_START: u64 = 0x80;
26
27/// Statistics for one frame promotion run.
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub struct FramePromotionStats {
30    /// Number of distinct compiler-local slots promoted.
31    pub slots_promoted: usize,
32    /// Number of local-slot loads replaced by SSA values.
33    pub loads_promoted: usize,
34    /// Number of local-slot stores removed.
35    pub stores_promoted: usize,
36    /// Number of phi nodes inserted.
37    pub phis_inserted: usize,
38}
39
40/// A compiler-owned memory slot promoted to SSA.
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
42pub enum PromotedSlot {
43    /// Slot addressed relative to the internal-call frame pointer.
44    InternalFrame(u64),
45    /// Slot addressed in the external entry's compiler-owned low-memory locals.
46    ExternalLocal(u64),
47}
48
49/// Per-slot information produced by frame-slot promotion.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct PromotedSlotSummary {
52    /// Promoted compiler-owned slot.
53    pub slot: PromotedSlot,
54    /// Blocks where the slot had an upward-exposed load before promotion.
55    pub use_blocks: Vec<BlockId>,
56    /// Blocks where the slot was defined before promotion.
57    pub def_blocks: Vec<BlockId>,
58    /// Blocks where SSA phis were inserted.
59    pub phi_blocks: Vec<BlockId>,
60    /// SSA phi values inserted for this slot.
61    pub phi_values: Vec<ValueId>,
62    /// Number of loads replaced by SSA values.
63    pub loads_promoted: usize,
64    /// Number of stores removed.
65    pub stores_promoted: usize,
66}
67
68impl FramePromotionStats {
69    /// Returns the total number of MIR edits made by this pass.
70    pub const fn total(self) -> usize {
71        self.loads_promoted + self.stores_promoted + self.phis_inserted
72    }
73}
74
75/// Promotes non-escaping compiler-local slots to SSA values.
76#[derive(Debug, Default)]
77pub struct FrameSlotPromoter {
78    stats: FramePromotionStats,
79    summaries: Vec<PromotedSlotSummary>,
80}
81
82/// Function pass for internal-frame scalar promotion.
83pub struct FrameSlotPromotionPass;
84
85impl FunctionPass for FrameSlotPromotionPass {
86    fn name(&self) -> &str {
87        "frame-slot-promotion"
88    }
89
90    fn run_on_function(&mut self, func: &mut Function) -> bool {
91        let changed = FrameSlotPromoter::new().run(func).total() != 0;
92        repair_reachability_phis(func);
93        changed
94    }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
98enum PromotableSlot {
99    InternalFrame(u64),
100    ExternalLocal(u64),
101}
102
103impl From<PromotableSlot> for PromotedSlot {
104    fn from(slot: PromotableSlot) -> Self {
105        match slot {
106            PromotableSlot::InternalFrame(offset) => Self::InternalFrame(offset),
107            PromotableSlot::ExternalLocal(addr) => Self::ExternalLocal(addr),
108        }
109    }
110}
111
112#[derive(Clone, Copy, Debug)]
113struct SlotLoad {
114    block: BlockId,
115    inst: InstId,
116}
117
118#[derive(Clone, Copy, Debug)]
119struct SlotStore {
120    block: BlockId,
121    inst: InstId,
122    value: ValueId,
123}
124
125#[derive(Clone, Debug)]
126struct SlotAccessInfo {
127    slot: PromotableSlot,
128    loads: Vec<SlotLoad>,
129    stores: Vec<SlotStore>,
130    use_blocks: FxHashSet<BlockId>,
131    def_blocks: FxHashSet<BlockId>,
132    access_blocks: FxHashSet<BlockId>,
133}
134
135impl SlotAccessInfo {
136    fn new(slot: PromotableSlot) -> Self {
137        Self {
138            slot,
139            loads: Vec::new(),
140            stores: Vec::new(),
141            use_blocks: FxHashSet::default(),
142            def_blocks: FxHashSet::default(),
143            access_blocks: FxHashSet::default(),
144        }
145    }
146
147    fn note_load(&mut self, block: BlockId, inst: InstId) {
148        self.loads.push(SlotLoad { block, inst });
149        self.use_blocks.insert(block);
150        self.access_blocks.insert(block);
151    }
152
153    fn note_store(&mut self, block: BlockId, inst: InstId, value: ValueId) {
154        self.stores.push(SlotStore { block, inst, value });
155        self.def_blocks.insert(block);
156        self.access_blocks.insert(block);
157    }
158
159    fn sorted_use_blocks(&self) -> Vec<BlockId> {
160        sorted_blocks(&self.use_blocks)
161    }
162
163    fn sorted_def_blocks(&self) -> Vec<BlockId> {
164        sorted_blocks(&self.def_blocks)
165    }
166}
167
168#[derive(Clone, Debug)]
169struct PendingPhi {
170    block: BlockId,
171    inst: InstId,
172    value: ValueId,
173    incoming: Vec<(BlockId, ValueId)>,
174}
175
176struct SlotSsaBuilder<'a> {
177    info: &'a SlotAccessInfo,
178    cfg: &'a CfgInfo,
179    inst_results: &'a FxHashMap<InstId, ValueId>,
180    replacements: FxHashMap<ValueId, ValueId>,
181    dead: FxHashSet<InstId>,
182    phis: FxHashMap<BlockId, PendingPhi>,
183    /// Blocks where this slot is live-in. Used to place phis only where the slot
184    /// is actually live (pruned SSA): forcing a phi at a multi-predecessor block
185    /// where the slot is dead can chain back to the entry with no reaching value
186    /// and spuriously abort the whole promotion.
187    live_in: FxHashSet<BlockId>,
188    /// Blocks selected by pruned iterated-dominance-frontier phi placement.
189    phi_blocks: FxHashSet<BlockId>,
190    failed: bool,
191    loads_promoted: usize,
192    stores_promoted: usize,
193}
194
195fn sorted_blocks(blocks: &FxHashSet<BlockId>) -> Vec<BlockId> {
196    let mut blocks: Vec<_> = blocks.iter().copied().collect();
197    blocks.sort_by_key(|block| block.index());
198    blocks
199}
200
201impl FrameSlotPromoter {
202    /// Creates a new compiler-local-slot promoter.
203    pub fn new() -> Self {
204        Self::default()
205    }
206
207    /// Returns the statistics from the most recent run.
208    pub const fn stats(&self) -> FramePromotionStats {
209        self.stats
210    }
211
212    /// Returns per-slot promotion summaries from the most recent run.
213    pub fn summaries(&self) -> &[PromotedSlotSummary] {
214        &self.summaries
215    }
216
217    /// Runs compiler-local-slot promotion on a function.
218    pub fn run(&mut self, func: &mut Function) -> FramePromotionStats {
219        self.stats = FramePromotionStats::default();
220        self.summaries.clear();
221
222        if Self::has_global_observation_barrier(func) {
223            return self.stats;
224        }
225
226        let cfg = CfgInfo::new(func);
227        let slots = Self::collect_promotable_slots(func, &cfg);
228        if slots.is_empty() {
229            return self.stats;
230        };
231
232        for info in slots {
233            let inst_results = func.inst_results();
234            let mut builder = SlotSsaBuilder::new(&info, &cfg, &inst_results);
235            if builder.run(func) {
236                self.stats.slots_promoted += 1;
237                self.stats.loads_promoted += builder.loads_promoted;
238                self.stats.stores_promoted += builder.stores_promoted;
239                self.stats.phis_inserted += builder.phis.len();
240                self.summaries.push(builder.summary());
241                builder.apply(func);
242            }
243        }
244
245        self.stats
246    }
247
248    fn has_global_observation_barrier(func: &Function) -> bool {
249        func.blocks.iter().any(|block| {
250            block.instructions.iter().any(|&inst_id| {
251                matches!(func.instructions[inst_id].kind, InstKind::Gas | InstKind::MSize)
252            })
253        })
254    }
255
256    fn collect_promotable_slots(func: &Function, cfg: &CfgInfo) -> Vec<SlotAccessInfo> {
257        let mut accesses: FxHashMap<PromotableSlot, SlotAccessInfo> = FxHashMap::default();
258
259        for (block_id, block) in func.blocks.iter_enumerated() {
260            if !cfg.is_reachable(block_id) {
261                continue;
262            }
263
264            for &inst_id in &block.instructions {
265                let kind = &func.instructions[inst_id].kind;
266                match *kind {
267                    InstKind::MLoad(addr) => {
268                        if let Some(slot) = Self::promotable_slot(func, addr) {
269                            accesses
270                                .entry(slot)
271                                .or_insert_with(|| SlotAccessInfo::new(slot))
272                                .note_load(block_id, inst_id);
273                        }
274                    }
275                    InstKind::MStore(addr, value) => {
276                        if let Some(slot) = Self::promotable_slot(func, addr) {
277                            accesses
278                                .entry(slot)
279                                .or_insert_with(|| SlotAccessInfo::new(slot))
280                                .note_store(block_id, inst_id, value);
281                        }
282                    }
283                    _ => {}
284                }
285            }
286        }
287
288        let mut slots: Vec<SlotAccessInfo> = accesses
289            .into_values()
290            .filter(|info| !info.loads.is_empty() && !info.stores.is_empty())
291            .filter(|info| match info.slot {
292                PromotableSlot::InternalFrame(offset) => {
293                    Self::internal_frame_slot_safe(func, offset)
294                }
295                PromotableSlot::ExternalLocal(addr) => Self::external_local_slot_safe(func, addr),
296            })
297            .collect();
298        slots.sort_by_key(|info| info.slot);
299        slots
300    }
301
302    fn promotable_slot(func: &Function, value: ValueId) -> Option<PromotableSlot> {
303        Self::internal_frame_offset(func, value)
304            .map(PromotableSlot::InternalFrame)
305            .or_else(|| Self::external_local_addr(func, value).map(PromotableSlot::ExternalLocal))
306    }
307
308    fn external_local_addr(func: &Function, value: ValueId) -> Option<u64> {
309        Self::external_local_addr_with_depth(func, value, 0)
310    }
311
312    fn external_local_addr_with_depth(
313        func: &Function,
314        value: ValueId,
315        depth: usize,
316    ) -> Option<u64> {
317        if depth > 8 {
318            return None;
319        }
320
321        if let Some(addr) = func.value_u64(value)
322            && Self::external_local_addr_in_range(func, addr).is_some()
323        {
324            return Some(addr);
325        }
326
327        let Value::Inst(inst_id) = func.values[value] else { return None };
328        match func.instructions[inst_id].kind {
329            InstKind::Add(a, b) => Self::external_local_add_offset(func, a, b, depth)
330                .or_else(|| Self::external_local_add_offset(func, b, a, depth)),
331            _ => None,
332        }
333    }
334
335    fn external_local_addr_in_range(func: &Function, addr: u64) -> Option<u64> {
336        let local_end = LOW_MEMORY_START.checked_add(func.internal_frame_size)?;
337        (addr >= LOW_MEMORY_START
338            && addr < local_end
339            && (addr - LOW_MEMORY_START).is_multiple_of(32))
340        .then_some(addr)
341    }
342
343    fn external_local_add_offset(
344        func: &Function,
345        base: ValueId,
346        offset: ValueId,
347        depth: usize,
348    ) -> Option<u64> {
349        let base = Self::external_local_addr_with_depth(func, base, depth + 1)?;
350        let addr = base.checked_add(func.value_u64(offset)?)?;
351        Self::external_local_addr_in_range(func, addr)
352    }
353
354    fn internal_frame_offset(func: &Function, value: ValueId) -> Option<u64> {
355        Self::internal_frame_offset_with_depth(func, value, 0)
356    }
357
358    fn internal_frame_offset_with_depth(
359        func: &Function,
360        value: ValueId,
361        depth: usize,
362    ) -> Option<u64> {
363        if depth > 8 {
364            return None;
365        }
366
367        match func.values[value] {
368            Value::Inst(inst_id) => match func.instructions[inst_id].kind {
369                InstKind::InternalFrameAddr(offset) => Some(offset),
370                InstKind::Add(a, b) => Self::internal_frame_add_offset(func, a, b, depth)
371                    .or_else(|| Self::internal_frame_add_offset(func, b, a, depth)),
372                _ => None,
373            },
374            _ => None,
375        }
376    }
377
378    fn internal_frame_add_offset(
379        func: &Function,
380        base: ValueId,
381        offset: ValueId,
382        depth: usize,
383    ) -> Option<u64> {
384        let base = Self::internal_frame_offset_with_depth(func, base, depth + 1)?;
385        base.checked_add(func.value_u64(offset)?)
386    }
387
388    fn external_local_slot_safe(func: &Function, slot_addr: u64) -> bool {
389        if mir_utils::ranges_overlap(
390            slot_addr,
391            32,
392            LOW_MEMORY_START,
393            func.external_static_return_size,
394        ) {
395            return false;
396        }
397
398        for block in func.blocks.iter() {
399            for &inst_id in &block.instructions {
400                if Self::inst_may_observe_external_slot(
401                    func,
402                    &func.instructions[inst_id].kind,
403                    slot_addr,
404                ) {
405                    return false;
406                }
407            }
408            if let Some(term) = &block.terminator
409                && Self::terminator_may_observe_external_slot(func, term, slot_addr)
410            {
411                return false;
412            }
413        }
414
415        true
416    }
417
418    fn internal_frame_slot_safe(func: &Function, slot_offset: u64) -> bool {
419        for block in func.blocks.iter() {
420            for &inst_id in &block.instructions {
421                if Self::inst_may_observe_internal_slot(
422                    func,
423                    &func.instructions[inst_id].kind,
424                    slot_offset,
425                ) {
426                    return false;
427                }
428            }
429            if let Some(term) = &block.terminator
430                && Self::terminator_may_observe_internal_slot(func, term, slot_offset)
431            {
432                return false;
433            }
434        }
435
436        true
437    }
438
439    fn inst_may_observe_internal_slot(func: &Function, kind: &InstKind, slot_offset: u64) -> bool {
440        match *kind {
441            InstKind::MLoad(addr) => {
442                !Self::is_exact_internal_slot_access(func, addr, slot_offset)
443                    && Self::internal_frame_range_may_overlap(func, addr, Some(32), slot_offset)
444            }
445            InstKind::MStore(addr, value) => {
446                (!Self::is_exact_internal_slot_access(func, addr, slot_offset)
447                    && Self::internal_frame_range_may_overlap(func, addr, Some(32), slot_offset))
448                    || Self::internal_frame_offset(func, value) == Some(slot_offset)
449            }
450            InstKind::MStore8(addr, _) => {
451                Self::internal_frame_range_may_overlap(func, addr, Some(1), slot_offset)
452            }
453            InstKind::Keccak256(addr, size)
454            | InstKind::Log0(addr, size)
455            | InstKind::ReturnDataCopy(addr, _, size)
456            | InstKind::CodeCopy(addr, _, size)
457            | InstKind::CalldataCopy(addr, _, size) => Self::internal_frame_range_may_overlap(
458                func,
459                addr,
460                func.value_u64(size),
461                slot_offset,
462            ),
463            InstKind::MCopy(dest, src, size) => {
464                let size = func.value_u64(size);
465                Self::internal_frame_range_may_overlap(func, dest, size, slot_offset)
466                    || Self::internal_frame_range_may_overlap(func, src, size, slot_offset)
467            }
468            InstKind::ExtCodeCopy(_, dest, _, size) => Self::internal_frame_range_may_overlap(
469                func,
470                dest,
471                func.value_u64(size),
472                slot_offset,
473            ),
474            InstKind::Log1(addr, size, _)
475            | InstKind::Log2(addr, size, _, _)
476            | InstKind::Log3(addr, size, _, _, _)
477            | InstKind::Log4(addr, size, _, _, _, _) => Self::internal_frame_range_may_overlap(
478                func,
479                addr,
480                func.value_u64(size),
481                slot_offset,
482            ),
483            InstKind::Call { args_offset, args_size, ret_offset, ret_size, .. }
484            | InstKind::StaticCall { args_offset, args_size, ret_offset, ret_size, .. }
485            | InstKind::DelegateCall { args_offset, args_size, ret_offset, ret_size, .. } => {
486                Self::internal_frame_range_may_overlap(
487                    func,
488                    args_offset,
489                    func.value_u64(args_size),
490                    slot_offset,
491                ) || Self::internal_frame_range_may_overlap(
492                    func,
493                    ret_offset,
494                    func.value_u64(ret_size),
495                    slot_offset,
496                )
497            }
498            InstKind::Add(a, b) => {
499                let exact_frame_addr = Self::internal_frame_add_offset(func, a, b, 0)
500                    .or_else(|| Self::internal_frame_add_offset(func, b, a, 0))
501                    .is_some();
502                !exact_frame_addr
503                    && kind
504                        .operands()
505                        .iter()
506                        .any(|&value| Self::internal_frame_offset(func, value) == Some(slot_offset))
507            }
508            _ => kind
509                .operands()
510                .iter()
511                .any(|&value| Self::internal_frame_offset(func, value) == Some(slot_offset)),
512        }
513    }
514
515    fn terminator_may_observe_internal_slot(
516        func: &Function,
517        term: &Terminator,
518        slot_offset: u64,
519    ) -> bool {
520        match term {
521            Terminator::Revert { offset, size } | Terminator::ReturnData { offset, size } => {
522                Self::internal_frame_range_may_overlap(
523                    func,
524                    *offset,
525                    func.value_u64(*size),
526                    slot_offset,
527                )
528            }
529            _ => term
530                .operands()
531                .iter()
532                .any(|&value| Self::internal_frame_offset(func, value) == Some(slot_offset)),
533        }
534    }
535
536    fn inst_may_observe_external_slot(func: &Function, kind: &InstKind, slot_addr: u64) -> bool {
537        match *kind {
538            InstKind::MLoad(addr) | InstKind::MStore(addr, _) => {
539                !Self::is_exact_external_slot_access(func, addr, slot_addr)
540                    && Self::memory_range_may_overlap(func, addr, Some(32), slot_addr)
541            }
542            InstKind::MStore8(addr, _) => {
543                Self::memory_range_may_overlap(func, addr, Some(1), slot_addr)
544            }
545            InstKind::Keccak256(addr, size)
546            | InstKind::Log0(addr, size)
547            | InstKind::ReturnDataCopy(addr, _, size)
548            | InstKind::CodeCopy(addr, _, size)
549            | InstKind::CalldataCopy(addr, _, size) => {
550                Self::memory_range_may_overlap(func, addr, func.value_u64(size), slot_addr)
551            }
552            InstKind::MCopy(dest, src, size) => {
553                let size = func.value_u64(size);
554                Self::memory_range_may_overlap(func, dest, size, slot_addr)
555                    || Self::memory_range_may_overlap(func, src, size, slot_addr)
556            }
557            InstKind::ExtCodeCopy(_, dest, _, size) => {
558                Self::memory_range_may_overlap(func, dest, func.value_u64(size), slot_addr)
559            }
560            InstKind::Log1(addr, size, _)
561            | InstKind::Log2(addr, size, _, _)
562            | InstKind::Log3(addr, size, _, _, _)
563            | InstKind::Log4(addr, size, _, _, _, _) => {
564                Self::memory_range_may_overlap(func, addr, func.value_u64(size), slot_addr)
565            }
566            InstKind::Call { .. }
567            | InstKind::StaticCall { .. }
568            | InstKind::DelegateCall { .. }
569            | InstKind::InternalCall { .. }
570            | InstKind::Create(_, _, _)
571            | InstKind::Create2(_, _, _, _)
572            | InstKind::MSize => true,
573            _ => false,
574        }
575    }
576
577    fn terminator_may_observe_external_slot(
578        func: &Function,
579        term: &Terminator,
580        slot_addr: u64,
581    ) -> bool {
582        match term {
583            Terminator::Revert { offset, size } | Terminator::ReturnData { offset, size } => {
584                Self::memory_range_may_overlap(func, *offset, func.value_u64(*size), slot_addr)
585            }
586            Terminator::Jump(_)
587            | Terminator::Branch { .. }
588            | Terminator::Switch { .. }
589            | Terminator::Return { .. }
590            | Terminator::Stop
591            | Terminator::Invalid
592            | Terminator::SelfDestruct { .. } => false,
593        }
594    }
595
596    fn is_exact_external_slot_access(func: &Function, addr: ValueId, slot_addr: u64) -> bool {
597        Self::external_local_addr(func, addr) == Some(slot_addr)
598    }
599
600    fn is_exact_internal_slot_access(func: &Function, addr: ValueId, slot_offset: u64) -> bool {
601        Self::internal_frame_offset(func, addr) == Some(slot_offset)
602    }
603
604    fn internal_frame_range_may_overlap(
605        func: &Function,
606        addr: ValueId,
607        size: Option<u64>,
608        slot_offset: u64,
609    ) -> bool {
610        let Some(offset) = Self::internal_frame_offset(func, addr) else { return false };
611        let Some(size) = size else { return true };
612        if size == 0 {
613            return false;
614        }
615        mir_utils::ranges_overlap(offset, size, slot_offset, 32)
616    }
617
618    fn memory_range_may_overlap(
619        func: &Function,
620        addr: ValueId,
621        size: Option<u64>,
622        slot_addr: u64,
623    ) -> bool {
624        let Some(size) = size else { return true };
625        if size == 0 {
626            return false;
627        }
628        let Some(addr) = func.value_u64(addr) else { return true };
629        mir_utils::ranges_overlap(addr, size, slot_addr, 32)
630    }
631}
632
633impl<'a> SlotSsaBuilder<'a> {
634    fn new(
635        info: &'a SlotAccessInfo,
636        cfg: &'a CfgInfo,
637        inst_results: &'a FxHashMap<InstId, ValueId>,
638    ) -> Self {
639        Self {
640            info,
641            cfg,
642            inst_results,
643            replacements: FxHashMap::default(),
644            dead: FxHashSet::default(),
645            phis: FxHashMap::default(),
646            live_in: FxHashSet::default(),
647            phi_blocks: FxHashSet::default(),
648            failed: false,
649            loads_promoted: 0,
650            stores_promoted: 0,
651        }
652    }
653
654    /// Computes the set of blocks where `self.slot` is live-in (a load of the
655    /// slot may observe a value defined before the block).
656    ///
657    /// This is single-variable backward liveness:
658    /// - `gen` (upward-exposed use): a promotable load of the slot precedes any store of the slot
659    ///   in the block.
660    /// - `kill` (def): the block stores the slot, overwriting any entry value.
661    /// - `live_in(b) = gen(b) ∨ (live_out(b) ∧ ¬kill(b))`, with `live_out(b) = ⋁ live_in(succ)`.
662    ///
663    /// Phis are only created at live-in blocks (pruned SSA).
664    fn summary(&self) -> PromotedSlotSummary {
665        let mut phi_blocks = sorted_blocks(&self.phi_blocks);
666        phi_blocks.retain(|block| self.phis.contains_key(block));
667
668        let mut phi_values: Vec<_> = self.phis.values().map(|phi| phi.value).collect();
669        phi_values.sort_by_key(|value| value.index());
670
671        PromotedSlotSummary {
672            slot: self.info.slot.into(),
673            use_blocks: self.info.sorted_use_blocks(),
674            def_blocks: self.info.sorted_def_blocks(),
675            phi_blocks,
676            phi_values,
677            loads_promoted: self.loads_promoted,
678            stores_promoted: self.stores_promoted,
679        }
680    }
681
682    fn compute_live_in(&self, func: &Function) -> FxHashSet<BlockId> {
683        let mut gen_set: FxHashSet<BlockId> = FxHashSet::default();
684        let mut kill: FxHashSet<BlockId> = FxHashSet::default();
685
686        for block in func.blocks.indices() {
687            if !self.cfg.is_reachable(block) {
688                continue;
689            }
690
691            let mut saw_store = false;
692            for &inst_id in &func.blocks[block].instructions {
693                match func.instructions[inst_id].kind {
694                    InstKind::MLoad(addr)
695                        if !saw_store
696                            && FrameSlotPromoter::promotable_slot(func, addr)
697                                == Some(self.info.slot) =>
698                    {
699                        gen_set.insert(block);
700                    }
701                    InstKind::MStore(addr, _)
702                        if FrameSlotPromoter::promotable_slot(func, addr)
703                            == Some(self.info.slot) =>
704                    {
705                        saw_store = true;
706                    }
707                    _ => {}
708                }
709            }
710            if saw_store {
711                kill.insert(block);
712            }
713        }
714
715        // Backward fixpoint. `live_in` only grows, so a block already in the set
716        // can be skipped on later rounds.
717        let mut live_in = gen_set;
718        let mut changed = true;
719        while changed {
720            changed = false;
721            for block in func.blocks.indices() {
722                if !self.cfg.is_reachable(block)
723                    || live_in.contains(&block)
724                    || kill.contains(&block)
725                {
726                    continue;
727                }
728                let live_out = self.cfg.successors(block).iter().any(|succ| live_in.contains(succ));
729                if live_out {
730                    live_in.insert(block);
731                    changed = true;
732                }
733            }
734        }
735        live_in
736    }
737
738    fn compute_phi_blocks(
739        &self,
740        func: &Function,
741        live_in: &FxHashSet<BlockId>,
742    ) -> FxHashSet<BlockId> {
743        let frontiers = self.compute_dominance_frontiers(func);
744        let mut phi_blocks = FxHashSet::default();
745        let mut worklist = sorted_blocks(&self.info.def_blocks);
746
747        while let Some(block) = worklist.pop() {
748            let Some(frontier) = frontiers.get(block.index()) else { continue };
749            for &frontier_block in frontier {
750                if !live_in.contains(&frontier_block) || !phi_blocks.insert(frontier_block) {
751                    continue;
752                }
753                worklist.push(frontier_block);
754            }
755        }
756
757        phi_blocks
758    }
759
760    fn compute_dominance_frontiers(&self, func: &Function) -> Vec<Vec<BlockId>> {
761        let mut frontiers = vec![Vec::new(); func.blocks.len()];
762        for block in func.blocks.indices() {
763            if !self.cfg.is_reachable(block) {
764                continue;
765            }
766
767            let preds: Vec<_> = func.blocks[block]
768                .predecessors
769                .iter()
770                .copied()
771                .filter(|&pred| self.cfg.is_reachable(pred))
772                .collect();
773            if preds.len() < 2 {
774                continue;
775            }
776
777            let Some(idom) = self.cfg.dominators().idom(block) else { continue };
778            for mut runner in preds {
779                while runner != idom {
780                    if !frontiers[runner.index()].contains(&block) {
781                        frontiers[runner.index()].push(block);
782                    }
783
784                    let Some(next) = self.cfg.dominators().idom(runner) else { break };
785                    if next == runner {
786                        break;
787                    }
788                    runner = next;
789                }
790            }
791        }
792
793        for frontier in &mut frontiers {
794            frontier.sort_by_key(|block| block.index());
795        }
796        frontiers
797    }
798
799    fn run(&mut self, func: &mut Function) -> bool {
800        if self.rewrite_single_block(func) || self.failed {
801            return !self.failed;
802        }
803        if self.rewrite_single_store(func) || self.failed {
804            return !self.failed;
805        }
806
807        self.live_in = self.compute_live_in(func);
808        self.phi_blocks = self.compute_phi_blocks(func, &self.live_in);
809        for block in sorted_blocks(&self.phi_blocks) {
810            self.create_phi(func, block);
811        }
812        self.rename_block(func, func.entry_block, None);
813        !self.failed
814    }
815
816    fn apply(self, func: &mut Function) {
817        for pending in self.phis.values() {
818            let mut incoming = pending.incoming.clone();
819            incoming.sort_by_key(|(block, _)| block.index());
820            func.instructions[pending.inst].kind = InstKind::Phi(incoming);
821            let insert_pos = func.blocks[pending.block]
822                .instructions
823                .iter()
824                .take_while(|&&inst_id| matches!(func.instructions[inst_id].kind, InstKind::Phi(_)))
825                .count();
826            func.blocks[pending.block].instructions.insert(insert_pos, pending.inst);
827        }
828
829        func.replace_uses_canonicalized(&self.replacements);
830
831        for block in func.blocks.iter_mut() {
832            block.instructions.retain(|id| !self.dead.contains(id));
833        }
834    }
835
836    fn rewrite_single_block(&mut self, func: &Function) -> bool {
837        if self.info.access_blocks.len() != 1 {
838            return false;
839        }
840        let block = self.info.access_blocks.iter().copied().next().expect("checked len above");
841        if !self.cfg.is_reachable(block) {
842            self.failed = true;
843            return true;
844        }
845
846        let mut current = None;
847        let mut changed = false;
848        for &inst_id in &func.blocks[block].instructions {
849            match func.instructions[inst_id].kind {
850                InstKind::MLoad(addr)
851                    if FrameSlotPromoter::promotable_slot(func, addr) == Some(self.info.slot) =>
852                {
853                    let Some(value) = current else { return false };
854                    self.replace_load(inst_id, value);
855                    changed = true;
856                }
857                InstKind::MStore(addr, value)
858                    if FrameSlotPromoter::promotable_slot(func, addr) == Some(self.info.slot) =>
859                {
860                    current = Some(mir_utils::resolve_replacement(value, &self.replacements));
861                    self.remove_store(inst_id);
862                    changed = true;
863                }
864                _ => {}
865            }
866        }
867        changed
868    }
869
870    fn rewrite_single_store(&mut self, func: &Function) -> bool {
871        let [store] = self.info.stores.as_slice() else { return false };
872        let stored_value = mir_utils::resolve_replacement(store.value, &self.replacements);
873
874        for load in &self.info.loads {
875            let dominated = if load.block == store.block {
876                let Some(store_pos) = Self::inst_position(func, store.block, store.inst) else {
877                    return false;
878                };
879                let Some(load_pos) = Self::inst_position(func, load.block, load.inst) else {
880                    return false;
881                };
882                store_pos < load_pos
883            } else {
884                self.cfg.dominators().dominates(store.block, load.block)
885            };
886
887            if !dominated {
888                return false;
889            }
890        }
891
892        for load in &self.info.loads {
893            self.replace_load(load.inst, stored_value);
894        }
895        self.remove_store(store.inst);
896        true
897    }
898
899    fn inst_position(func: &Function, block: BlockId, inst: InstId) -> Option<usize> {
900        func.blocks[block].instructions.iter().position(|&candidate| candidate == inst)
901    }
902
903    fn replace_load(&mut self, inst_id: InstId, value: ValueId) {
904        if let Some(&load_value) = self.inst_results.get(&inst_id) {
905            self.replacements
906                .insert(load_value, mir_utils::resolve_replacement(value, &self.replacements));
907            self.dead.insert(inst_id);
908            self.loads_promoted += 1;
909        }
910    }
911
912    fn remove_store(&mut self, inst_id: InstId) {
913        self.dead.insert(inst_id);
914        self.stores_promoted += 1;
915    }
916
917    fn rename_block(&mut self, func: &mut Function, block: BlockId, mut current: Option<ValueId>) {
918        if !self.cfg.is_reachable(block) || self.failed {
919            return;
920        }
921        if let Some(phi) = self.phis.get(&block) {
922            current = Some(phi.value);
923        }
924
925        let insts = func.blocks[block].instructions.clone();
926        for inst_id in insts {
927            match func.instructions[inst_id].kind {
928                InstKind::MLoad(addr)
929                    if FrameSlotPromoter::promotable_slot(func, addr) == Some(self.info.slot) =>
930                {
931                    let Some(value) = current else {
932                        self.failed = true;
933                        return;
934                    };
935                    self.replace_load(inst_id, value);
936                }
937                InstKind::MStore(addr, value)
938                    if FrameSlotPromoter::promotable_slot(func, addr) == Some(self.info.slot) =>
939                {
940                    current = Some(mir_utils::resolve_replacement(value, &self.replacements));
941                    self.remove_store(inst_id);
942                }
943                _ => {}
944            }
945        }
946
947        for &succ in self.cfg.successors(block) {
948            if let Some(phi) = self.phis.get_mut(&succ) {
949                let Some(value) = current else {
950                    self.failed = true;
951                    return;
952                };
953                phi.incoming
954                    .push((block, mir_utils::resolve_replacement(value, &self.replacements)));
955            }
956        }
957
958        let children = self.cfg.dominators().children(block).to_vec();
959        for child in children {
960            self.rename_block(func, child, current);
961        }
962    }
963
964    fn create_phi(&mut self, func: &mut Function, block: BlockId) -> ValueId {
965        if let Some(pending) = self.phis.get(&block) {
966            return pending.value;
967        }
968
969        let inst =
970            func.alloc_inst(Instruction::new(InstKind::Phi(Vec::new()), Some(MirType::uint256())));
971        let value = func.alloc_value(Value::Inst(inst));
972        self.phis.insert(
973            block,
974            PendingPhi {
975                block,
976                inst,
977                value,
978                incoming: Vec::with_capacity(func.blocks[block].predecessors.len()),
979            },
980        );
981        value
982    }
983}
984
985#[cfg(test)]
986mod tests {
987    use super::*;
988    use crate::mir::{FunctionBuilder, FunctionId};
989    use solar_interface::Ident;
990
991    fn test_func() -> Function {
992        Function::new(Ident::DUMMY)
993    }
994
995    fn count_active_frame_ops(func: &Function, offset: u64) -> (usize, usize) {
996        let mut loads = 0;
997        let mut stores = 0;
998
999        for block in func.blocks.iter() {
1000            for &inst_id in &block.instructions {
1001                match func.instructions[inst_id].kind {
1002                    InstKind::MLoad(addr)
1003                        if FrameSlotPromoter::internal_frame_offset(func, addr) == Some(offset) =>
1004                    {
1005                        loads += 1;
1006                    }
1007                    InstKind::MStore(addr, _)
1008                        if FrameSlotPromoter::internal_frame_offset(func, addr) == Some(offset) =>
1009                    {
1010                        stores += 1;
1011                    }
1012                    _ => {}
1013                }
1014            }
1015        }
1016
1017        (loads, stores)
1018    }
1019
1020    #[test]
1021    fn promotes_loop_carried_frame_slot() {
1022        let mut func = test_func();
1023        let mut builder = FunctionBuilder::new(&mut func);
1024        let header = builder.create_block();
1025        let body = builder.create_block();
1026        let exit = builder.create_block();
1027
1028        let frame = builder.internal_frame_addr(128);
1029        let zero = builder.imm_u64(0);
1030        let limit = builder.imm_u64(4);
1031        builder.mstore(frame, zero);
1032        builder.jump(header);
1033
1034        builder.switch_to_block(header);
1035        let header_frame = builder.internal_frame_addr(128);
1036        let i = builder.mload(header_frame);
1037        let cond = builder.lt(i, limit);
1038        builder.branch(cond, body, exit);
1039
1040        builder.switch_to_block(body);
1041        let body_frame = builder.internal_frame_addr(128);
1042        let current = builder.mload(body_frame);
1043        let one = builder.imm_u64(1);
1044        let next = builder.add(current, one);
1045        let body_frame = builder.internal_frame_addr(128);
1046        builder.mstore(body_frame, next);
1047        builder.jump(header);
1048
1049        builder.switch_to_block(exit);
1050        let exit_frame = builder.internal_frame_addr(128);
1051        let result = builder.mload(exit_frame);
1052        builder.ret([result]);
1053
1054        let mut pass = FrameSlotPromoter::new();
1055        let stats = pass.run(&mut func);
1056
1057        assert_eq!(stats.slots_promoted, 1);
1058        assert_eq!(stats.loads_promoted, 3);
1059        assert_eq!(stats.stores_promoted, 2);
1060        assert_eq!(stats.phis_inserted, 1);
1061        assert_eq!(count_active_frame_ops(&func, 128), (0, 0));
1062
1063        let Some(Terminator::Return { values }) = &func.blocks[exit].terminator else {
1064            panic!("expected return");
1065        };
1066        assert_ne!(values.as_slice(), &[result]);
1067    }
1068
1069    #[test]
1070    fn skips_escaped_frame_address() {
1071        let mut func = test_func();
1072        let mut builder = FunctionBuilder::new(&mut func);
1073        let frame = builder.internal_frame_addr(128);
1074        let value = builder.imm_u64(42);
1075        let size = builder.imm_u64(32);
1076        builder.mstore(frame, value);
1077        let hash = builder.keccak256(frame, size);
1078        let loaded = builder.mload(frame);
1079        builder.ret([hash, loaded]);
1080
1081        let mut pass = FrameSlotPromoter::new();
1082        let stats = pass.run(&mut func);
1083
1084        assert_eq!(stats.total(), 0);
1085        assert_eq!(count_active_frame_ops(&func, 128), (1, 1));
1086    }
1087
1088    #[test]
1089    fn skips_gas_observed_functions() {
1090        let mut func = test_func();
1091        let mut builder = FunctionBuilder::new(&mut func);
1092        let frame = builder.internal_frame_addr(128);
1093        let value = builder.imm_u64(42);
1094        builder.mstore(frame, value);
1095        builder.gas();
1096        let loaded = builder.mload(frame);
1097        builder.ret([loaded]);
1098
1099        let mut pass = FrameSlotPromoter::new();
1100        let stats = pass.run(&mut func);
1101
1102        assert_eq!(stats.total(), 0);
1103        assert_eq!(count_active_frame_ops(&func, 128), (1, 1));
1104    }
1105
1106    #[test]
1107    fn promotes_across_internal_calls() {
1108        let mut func = test_func();
1109        let mut builder = FunctionBuilder::new(&mut func);
1110        let frame = builder.internal_frame_addr(128);
1111        let value = builder.imm_u64(42);
1112        builder.mstore(frame, value);
1113        builder.internal_call_void(FunctionId::from_usize(0), Vec::new(), 0);
1114        let loaded = builder.mload(frame);
1115        builder.ret([loaded]);
1116
1117        let mut pass = FrameSlotPromoter::new();
1118        let stats = pass.run(&mut func);
1119
1120        assert_eq!(stats.slots_promoted, 1);
1121        assert_eq!(count_active_frame_ops(&func, 128), (0, 0));
1122    }
1123
1124    #[test]
1125    fn promotes_frame_address_with_constant_offset() {
1126        let mut func = test_func();
1127        let mut builder = FunctionBuilder::new(&mut func);
1128        let base = builder.internal_frame_addr(128);
1129        let offset = builder.imm_u64(32);
1130        let addr = builder.add(base, offset);
1131        let value = builder.imm_u64(99);
1132        builder.mstore(addr, value);
1133        let base = builder.internal_frame_addr(128);
1134        let offset = builder.imm_u64(32);
1135        let addr = builder.add(base, offset);
1136        let loaded = builder.mload(addr);
1137        builder.ret([loaded]);
1138
1139        let mut pass = FrameSlotPromoter::new();
1140        let stats = pass.run(&mut func);
1141
1142        assert_eq!(stats.slots_promoted, 1);
1143        assert_eq!(count_active_frame_ops(&func, 160), (0, 0));
1144    }
1145}