1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub struct FramePromotionStats {
30 pub slots_promoted: usize,
32 pub loads_promoted: usize,
34 pub stores_promoted: usize,
36 pub phis_inserted: usize,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
42pub enum PromotedSlot {
43 InternalFrame(u64),
45 ExternalLocal(u64),
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct PromotedSlotSummary {
52 pub slot: PromotedSlot,
54 pub use_blocks: Vec<BlockId>,
56 pub def_blocks: Vec<BlockId>,
58 pub phi_blocks: Vec<BlockId>,
60 pub phi_values: Vec<ValueId>,
62 pub loads_promoted: usize,
64 pub stores_promoted: usize,
66}
67
68impl FramePromotionStats {
69 pub const fn total(self) -> usize {
71 self.loads_promoted + self.stores_promoted + self.phis_inserted
72 }
73}
74
75#[derive(Debug, Default)]
77pub struct FrameSlotPromoter {
78 stats: FramePromotionStats,
79 summaries: Vec<PromotedSlotSummary>,
80}
81
82pub 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 live_in: FxHashSet<BlockId>,
188 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 pub fn new() -> Self {
204 Self::default()
205 }
206
207 pub const fn stats(&self) -> FramePromotionStats {
209 self.stats
210 }
211
212 pub fn summaries(&self) -> &[PromotedSlotSummary] {
214 &self.summaries
215 }
216
217 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 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 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}