Skip to main content

solar_codegen/transform/
load_pre.rs

1//! Dataflow-based redundancy elimination and PRE for memory-dependent reads.
2//!
3//! CSE reuses state-dependent reads only along the dominator tree. This pass removes the
4//! join redundancies that dominance cannot see, driven by an available-expressions
5//! dataflow over storage, transient-storage, memory, and keccak read keys:
6//!
7//! 1. **Full redundancy**: a read available at the end of every predecessor (computed in both arms
8//!    of a diamond, or live around a loop backedge) and recomputed at the join becomes a phi over
9//!    the per-predecessor values.
10//! 2. **Store-to-load forwarding across joins**: a store in one arm makes the value known on that
11//!    path, so the join reload merges stored and loaded values.
12//! 3. **Partial redundancy**: a read available on some predecessors is inserted at the end of the
13//!    jump-terminated remaining predecessors, then handled as a full redundancy.
14//!
15//! # Keys
16//!
17//! One key universe per function:
18//! - `(Storage, alias)`: genned by `sload` (load result) and `sstore` (forwarded stored value);
19//!   killed by may-aliasing `sstore` and by calls and creates. `STATICCALL` cannot mutate storage
20//!   or transient storage and does not kill them.
21//! - `(Transient, alias)`: same with `tload`/`tstore`.
22//! - `(Memory, canonical address, width 32)`: genned by `mload` and by `mstore` at the same
23//!   canonical address (forwarded stored value); killed by overlapping `mstore`/`mstore8`, by
24//!   copies with an overlapping or unknown destination range, and by every call (including
25//!   `STATICCALL`, which writes its return buffer).
26//! - `(Keccak, canonical offset, size or dynamic-size value)`: genned by `keccak256`; killed like a
27//!   memory read over its range. Reads with a non-constant size key the size operand so reads of
28//!   different lengths never unify.
29//!
30//! # Availability dataflow
31//!
32//! Standard available expressions over the finite key universe:
33//! `OUT(b) = (IN(b) - KILL(b)) | GEN(b)`, `IN(b)` = intersection of `OUT` over reachable
34//! predecessors (the entry starts empty). `OUT` is optimistically initialized to the full
35//! universe for non-entry blocks; this is required for loop headers, where the backedge
36//! must not pessimistically erase availability before the fixpoint settles. The greatest
37//! fixpoint of this system is the standard, sound all-paths solution.
38//!
39//! # Value location
40//!
41//! A key available at a predecessor's end is usable only if a concrete value can be
42//! located: scan the predecessor backwards for a gen before any kill (load result or
43//! forwarded store value). If the block neither gens nor kills the key, recurse to its
44//! immediate dominator, provided the dataflow has the key in `IN(pred)` and no block on
45//! any CFG path between the dominator and the predecessor may kill the key. The path
46//! purity check is what keeps the walk sound: availability alone does not imply the value
47//! is uniform, because a non-dominating path can kill and re-gen the key with a different
48//! value (e.g. an `sstore` in one arm of a diamond between the dominator and the
49//! predecessor). If no concrete value can be located (the value only exists as a
50//! cross-path merge), the predecessor is treated as unavailable.
51//!
52//! # Safety of rewrites
53//!
54//! A join load is a candidate only if no kill of its key precedes it in the join block.
55//! For that scan, `gas` is additionally treated as a kill in all spaces and `msize` as a
56//! kill for memory and keccak keys: a partial-redundancy insertion moves the read to the
57//! end of a predecessor, so everything in the join block above the original load executes
58//! after the moved read, and a `gas`/`msize` read there would observe the moved load's gas
59//! and memory-expansion effects early. Removal-only (fully redundant) rewrites do not move
60//! reads, but we keep the single conservative scan for both cases for simplicity.
61//!
62//! An inserted load reads exactly the state the original would have read on that path: it
63//! sits at the end of the predecessor (nothing follows it but the jump), and the join
64//! prefix above the original load contains no kills of the key.
65//!
66//! # Termination
67//!
68//! The same discipline as [`pre`](super::pre):
69//! 1. Instructions inserted by this run are never picked as rewrite candidates, so every rewrite
70//!    retires a load that existed when the run started.
71//! 2. A `(key, block)` pair never gets an insertion after an elimination in that block, preventing
72//!    ping-pong between mutually-preceding joins.
73//! 3. A function-size-derived rewrite budget backstops the above.
74
75use crate::{
76    analysis::{CfgInfo, DominatorTree},
77    mir::{
78        BlockId, Function, InstId, InstKind, Instruction, InstructionMetadata, MemoryRegion,
79        MirType, StorageAlias, Terminator, Value, ValueId,
80        utils::{self as mir_utils, repair_reachability_phis},
81    },
82    pass::FunctionPass,
83};
84use alloy_primitives::U256;
85use solar_data_structures::{
86    bit_set::DenseBitSet,
87    map::{FxHashMap, FxHashSet},
88    newtype_index,
89};
90
91/// Statistics for load PRE.
92#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
93pub struct LoadPreStats {
94    /// Number of join-block loads replaced by phis or available values.
95    pub loads_eliminated: usize,
96    /// Number of compensating loads inserted into predecessors.
97    pub loads_inserted: usize,
98}
99
100impl LoadPreStats {
101    /// Returns the total number of MIR edits made by this pass.
102    pub const fn total(self) -> usize {
103        self.loads_eliminated + self.loads_inserted
104    }
105}
106
107/// Dataflow-based redundancy eliminator for memory-dependent reads.
108#[derive(Debug, Default)]
109pub struct LoadRedundancyEliminator {
110    stats: LoadPreStats,
111}
112
113/// Function pass for load PRE.
114pub struct LoadPrePass;
115
116impl FunctionPass for LoadPrePass {
117    fn name(&self) -> &str {
118        "load-pre"
119    }
120
121    fn run_on_function(&mut self, func: &mut Function) -> bool {
122        LoadRedundancyEliminator::new().run(func).total() != 0
123    }
124}
125
126/// A normalized key for a state-dependent read.
127#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
128enum LoadKey {
129    Storage(StorageAlias),
130    Transient(StorageAlias),
131    /// A 32-byte memory read at a canonical address.
132    Memory(MemAddr),
133    Keccak(MemAddr, KeccakSize),
134}
135
136/// A canonical memory address: an optional symbolic base plus a known offset.
137/// A `None` base is an absolute address.
138#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
139struct MemAddr {
140    region: MemoryRegion,
141    base: Option<ValueId>,
142    offset: u64,
143}
144
145/// The size of a keccak read: a known constant or the canonical size operand.
146#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
147enum KeccakSize {
148    Const(u64),
149    Dyn(ValueId),
150}
151
152/// Where a gen's value comes from.
153enum GenSource {
154    /// The instruction's own result (a load).
155    LoadResult,
156    /// The forwarded stored value (a store).
157    Stored(ValueId),
158}
159
160newtype_index! {
161    struct KeyIdx;
162}
163
164/// A dense bitset over key-universe indices.
165#[derive(Clone, Debug, PartialEq, Eq)]
166struct KeySet(DenseBitSet<KeyIdx>);
167
168impl KeySet {
169    fn empty(len: usize) -> Self {
170        Self(DenseBitSet::new_empty(len))
171    }
172
173    fn full(len: usize) -> Self {
174        Self(DenseBitSet::new_filled(len))
175    }
176
177    fn insert(&mut self, idx: usize) {
178        self.0.insert(KeyIdx::from_usize(idx));
179    }
180
181    fn remove(&mut self, idx: usize) {
182        self.0.remove(KeyIdx::from_usize(idx));
183    }
184
185    fn contains(&self, idx: usize) -> bool {
186        self.0.contains(KeyIdx::from_usize(idx))
187    }
188
189    fn is_empty(&self) -> bool {
190        self.0.is_empty()
191    }
192
193    fn intersect_with(&mut self, other: &Self) {
194        self.0.intersect(&other.0);
195    }
196
197    fn subtract(&mut self, other: &Self) {
198        self.0.subtract(&other.0);
199    }
200
201    fn union_with(&mut self, other: &Self) {
202        self.0.union(&other.0);
203    }
204}
205
206/// A join-block load rewrite: replace `inst` with a phi over `incoming`, after
207/// inserting copies of `kind` at the end of the `insertions` predecessors.
208struct Candidate {
209    target: BlockId,
210    key: LoadKey,
211    result_ty: MirType,
212    kind: InstKind,
213    metadata: InstructionMetadata,
214    loads: Vec<(InstId, ValueId)>,
215    incoming: Vec<(BlockId, ValueId)>,
216    insertions: Vec<BlockId>,
217}
218
219/// A small static profitability model for load PRE insertions.
220///
221/// The model intentionally works in approximate dynamic path cost rather than
222/// MIR instruction count: a join-block reload executes on every predecessor
223/// path, while a compensating load only executes on the paths where the value
224/// was unavailable.
225#[derive(Clone, Copy, Debug, Default)]
226struct LoadPreCostModel;
227
228#[derive(Clone, Copy, Debug, PartialEq, Eq)]
229struct LoadPreCost {
230    saved: i64,
231    inserted: i64,
232    phi: i64,
233    operands: i64,
234}
235
236impl LoadPreCost {
237    const fn is_profitable(self) -> bool {
238        self.saved > self.inserted + self.phi + self.operands
239    }
240}
241
242struct LoadPreCostInput<'a> {
243    func: &'a Function,
244    key: LoadKey,
245    kind: &'a InstKind,
246    predecessors: &'a [BlockId],
247    loads: &'a [(InstId, ValueId)],
248    insertions: &'a [BlockId],
249    loop_carried: bool,
250    needs_phi: bool,
251}
252
253impl LoadPreCostModel {
254    const MEMORY_READ: i64 = 3;
255    const STORAGE_READ: i64 = 100;
256    const TRANSIENT_READ: i64 = 100;
257    const KECCAK_BASE: i64 = 30;
258    const KECCAK_WORD: i64 = 6;
259    const NON_LOOP_PHI_EDGE_COPY: i64 = 3;
260    const CROSS_BLOCK_OPERAND: i64 = 3;
261
262    fn estimate(&self, input: LoadPreCostInput<'_>) -> LoadPreCost {
263        let read = Self::read_cost(input.key);
264        let saved = read * input.loads.len() as i64 * input.predecessors.len() as i64;
265        let inserted = read * input.insertions.len() as i64;
266        let phi = if !input.needs_phi || input.loop_carried {
267            0
268        } else {
269            Self::NON_LOOP_PHI_EDGE_COPY * input.predecessors.len() as i64
270        };
271        let operands = Self::inserted_operand_cost(input.func, input.kind, input.insertions);
272        LoadPreCost { saved, inserted, phi, operands }
273    }
274
275    const fn read_cost(key: LoadKey) -> i64 {
276        match key {
277            LoadKey::Storage(_) => Self::STORAGE_READ,
278            LoadKey::Transient(_) => Self::TRANSIENT_READ,
279            LoadKey::Memory(_) => Self::MEMORY_READ,
280            LoadKey::Keccak(_, size) => match size {
281                KeccakSize::Const(size) => {
282                    Self::KECCAK_BASE + Self::KECCAK_WORD * size.div_ceil(32) as i64
283                }
284                KeccakSize::Dyn(_) => Self::KECCAK_BASE + Self::KECCAK_WORD * 2,
285            },
286        }
287    }
288
289    fn inserted_operand_cost(func: &Function, kind: &InstKind, insertions: &[BlockId]) -> i64 {
290        if insertions.is_empty() {
291            return 0;
292        }
293        let cross_block_operands = kind
294            .operands()
295            .into_iter()
296            .filter(|&value| matches!(func.value(value), Value::Inst(_) | Value::Undef(_)))
297            .count();
298        Self::CROSS_BLOCK_OPERAND * cross_block_operands as i64 * insertions.len() as i64
299    }
300}
301
302/// Per-round analysis shared by candidate collection.
303struct Analysis {
304    keys: Vec<LoadKey>,
305    key_index: FxHashMap<LoadKey, usize>,
306    reachable: FxHashSet<BlockId>,
307    /// Per-block keys killed at any point in the block; only blocks that kill
308    /// something have an entry.
309    kills: FxHashMap<BlockId, KeySet>,
310    /// Availability at block entry, over all paths from the entry block.
311    ins: FxHashMap<BlockId, KeySet>,
312    /// Availability at block exit.
313    outs: FxHashMap<BlockId, KeySet>,
314    /// CFG path reachability for the value-location purity check; empty when
315    /// no block kills any key.
316    reach: FxHashMap<BlockId, FxHashSet<BlockId>>,
317    dominators: DominatorTree,
318    inst_results: FxHashMap<InstId, ValueId>,
319    inst_blocks: FxHashMap<InstId, BlockId>,
320}
321
322impl Analysis {
323    /// Returns true if any block on a CFG path from `from` to `to` (excluding
324    /// `from` itself, whose effects the caller scans directly) may kill `key`.
325    fn path_kills_key(&self, from: BlockId, to: BlockId, key_idx: usize) -> bool {
326        if self.kills.is_empty() {
327            return false;
328        }
329        let Some(reachable_from) = self.reach.get(&from) else { return true };
330        for (&mid, kills) in &self.kills {
331            if mid == from || !kills.contains(key_idx) {
332                continue;
333            }
334            if reachable_from.contains(&mid)
335                && self.reach.get(&mid).is_some_and(|reach| reach.contains(&to))
336            {
337                return true;
338            }
339        }
340        false
341    }
342}
343
344/// Mutable state threaded through one candidate-collection round.
345struct CandidateCx<'a> {
346    analysis: &'a Analysis,
347    eliminated_keys: &'a FxHashSet<(LoadKey, BlockId)>,
348    inserted_insts: &'a FxHashSet<InstId>,
349    locate_cache: FxHashMap<(BlockId, usize), Option<ValueId>>,
350}
351
352impl LoadRedundancyEliminator {
353    /// Creates a new load PRE pass.
354    pub fn new() -> Self {
355        Self::default()
356    }
357
358    /// Returns statistics from the most recent run.
359    pub const fn stats(&self) -> LoadPreStats {
360        self.stats
361    }
362
363    /// Runs load PRE to a fixed point under the rewrite budget.
364    pub fn run(&mut self, func: &mut Function) -> LoadPreStats {
365        self.stats = LoadPreStats::default();
366        repair_reachability_phis(func);
367
368        let rewrite_limit = func.instructions.len().saturating_mul(2).max(64);
369        let mut rewrites = 0usize;
370        let mut eliminated_keys: FxHashSet<(LoadKey, BlockId)> = FxHashSet::default();
371        let mut inserted_insts: FxHashSet<InstId> = FxHashSet::default();
372
373        while rewrites < rewrite_limit {
374            let Some(analysis) = Self::compute_analysis(func) else { break };
375            let mut cx = CandidateCx {
376                analysis: &analysis,
377                eliminated_keys: &eliminated_keys,
378                inserted_insts: &inserted_insts,
379                locate_cache: FxHashMap::default(),
380            };
381            let batch = self.collect_candidates(func, &mut cx, rewrite_limit - rewrites);
382            if batch.is_empty() {
383                break;
384            }
385            rewrites += batch.len();
386            for candidate in batch {
387                self.apply_candidate(func, candidate, &mut eliminated_keys, &mut inserted_insts);
388            }
389        }
390
391        self.stats
392    }
393
394    /// Computes the key universe, the per-block gen/kill summaries, and the
395    /// availability fixpoint. Returns `None` if no read is trackable.
396    fn compute_analysis(func: &Function) -> Option<Analysis> {
397        let mut cfg = CfgInfo::new(func);
398        let rpo = cfg.rpo();
399
400        // The key universe: every key genned in a reachable block.
401        let mut keys = Vec::new();
402        let mut key_index: FxHashMap<LoadKey, usize> = FxHashMap::default();
403        for &block in rpo {
404            for &inst_id in &func.blocks[block].instructions {
405                if let Some((key, _)) = Self::gen_key_value(func, inst_id) {
406                    key_index.entry(key).or_insert_with(|| {
407                        keys.push(key);
408                        keys.len() - 1
409                    });
410                }
411            }
412        }
413        if keys.is_empty() {
414            return None;
415        }
416        let key_count = keys.len();
417
418        // Per-block summaries: GEN holds keys genned after the last kill, KILL
419        // holds keys killed at any point.
420        let mut gens = FxHashMap::default();
421        let mut kills = FxHashMap::default();
422        for &block in rpo {
423            let mut gen_set = KeySet::empty(key_count);
424            let mut kill_set = KeySet::empty(key_count);
425            for &inst_id in &func.blocks[block].instructions {
426                if func.instructions[inst_id].kind.has_side_effects() {
427                    for (idx, &key) in keys.iter().enumerate() {
428                        if Self::inst_kills_key(func, inst_id, key) {
429                            kill_set.insert(idx);
430                            gen_set.remove(idx);
431                        }
432                    }
433                }
434                // A store both kills aliases and gens its exact key; the gen
435                // wins for the exact key because the slot then holds the
436                // stored value.
437                if let Some((key, _)) = Self::gen_key_value(func, inst_id)
438                    && let Some(&idx) = key_index.get(&key)
439                {
440                    gen_set.insert(idx);
441                }
442            }
443            if !kill_set.is_empty() {
444                kills.insert(block, kill_set);
445            }
446            gens.insert(block, gen_set);
447        }
448
449        // Availability fixpoint with optimistic initialization.
450        let mut ins: FxHashMap<BlockId, KeySet> = FxHashMap::default();
451        let mut outs: FxHashMap<BlockId, KeySet> = rpo
452            .iter()
453            .map(|&block| {
454                let out = if block == func.entry_block {
455                    gens[&block].clone()
456                } else {
457                    KeySet::full(key_count)
458                };
459                (block, out)
460            })
461            .collect();
462        loop {
463            let mut changed = false;
464            for &block in rpo {
465                let in_set = if block == func.entry_block {
466                    KeySet::empty(key_count)
467                } else {
468                    let mut acc: Option<KeySet> = None;
469                    for &pred in &func.blocks[block].predecessors {
470                        // Unreachable predecessors never execute and cannot
471                        // contribute a path.
472                        let Some(out) = outs.get(&pred) else { continue };
473                        match &mut acc {
474                            Some(acc) => acc.intersect_with(out),
475                            None => acc = Some(out.clone()),
476                        }
477                    }
478                    acc.unwrap_or_else(|| KeySet::empty(key_count))
479                };
480                let mut out = in_set.clone();
481                if let Some(kill) = kills.get(&block) {
482                    out.subtract(kill);
483                }
484                out.union_with(&gens[&block]);
485                ins.insert(block, in_set);
486                if outs.get(&block) != Some(&out) {
487                    outs.insert(block, out);
488                    changed = true;
489                }
490            }
491            if !changed {
492                break;
493            }
494        }
495
496        let reach = if kills.is_empty() {
497            FxHashMap::default()
498        } else {
499            cfg.transitive_reachability().clone()
500        };
501
502        Some(Analysis {
503            keys,
504            key_index,
505            reachable: cfg.reachable().clone(),
506            kills,
507            ins,
508            outs,
509            reach,
510            dominators: cfg.dominators().clone(),
511            inst_results: func.inst_results(),
512            inst_blocks: func.inst_blocks(),
513        })
514    }
515
516    /// Collects non-interfering candidates from one analysis snapshot so they
517    /// can be applied as a batch.
518    fn collect_candidates(
519        &self,
520        func: &Function,
521        cx: &mut CandidateCx<'_>,
522        limit: usize,
523    ) -> Vec<Candidate> {
524        let mut batch = Vec::new();
525        // Candidates whose analysis would be invalidated by an earlier
526        // candidate in this batch are deferred to the next round.
527        let mut modified_blocks: FxHashSet<BlockId> = FxHashSet::default();
528        let mut eliminated_values: FxHashSet<ValueId> = FxHashSet::default();
529
530        'targets: for target in func.blocks.indices() {
531            if !cx.analysis.reachable.contains(&target) {
532                continue;
533            }
534            let predecessors = func.unique_predecessors(target);
535            if predecessors.len() < 2
536                || predecessors.iter().any(|pred| !cx.analysis.reachable.contains(pred))
537            {
538                continue;
539            }
540
541            for (inst, key_idx) in Self::first_loads(func, cx.analysis, target) {
542                if batch.len() >= limit {
543                    break 'targets;
544                }
545                // Termination rule 1: never rewrite a load this run inserted.
546                if cx.inserted_insts.contains(&inst) {
547                    continue;
548                }
549                let Some(candidate) =
550                    self.candidate_for_load(func, cx, target, inst, key_idx, &predecessors)
551                else {
552                    continue;
553                };
554                if Self::interferes_with_batch(&candidate, &modified_blocks, &eliminated_values) {
555                    continue;
556                }
557                modified_blocks.insert(candidate.target);
558                modified_blocks.extend(candidate.insertions.iter().copied());
559                eliminated_values.extend(candidate.loads.iter().map(|&(_, value)| value));
560                batch.push(candidate);
561            }
562        }
563
564        batch
565    }
566
567    /// Returns true if applying earlier candidates in the batch invalidates
568    /// this candidate's analysis: its blocks were already rewritten, or it
569    /// references a value whose defining load the batch removes.
570    fn interferes_with_batch(
571        candidate: &Candidate,
572        modified_blocks: &FxHashSet<BlockId>,
573        eliminated_values: &FxHashSet<ValueId>,
574    ) -> bool {
575        modified_blocks.contains(&candidate.target)
576            || candidate.insertions.iter().any(|block| modified_blocks.contains(block))
577            || candidate.incoming.iter().any(|(_, value)| eliminated_values.contains(value))
578            || candidate.loads.iter().any(|&(_, value)| eliminated_values.contains(&value))
579            || (!candidate.insertions.is_empty()
580                && candidate
581                    .kind
582                    .operands()
583                    .into_iter()
584                    .any(|value| eliminated_values.contains(&value)))
585    }
586
587    /// Returns, in program order, the first load of each key in `target` that
588    /// no kill of that key precedes.
589    ///
590    /// `gas` and `msize` conservatively end or restrict the scan: a
591    /// partial-redundancy insertion moves the read to a predecessor's end, so
592    /// it must not cross a `gas` (any space) or `msize` (memory and keccak)
593    /// observation in the join prefix.
594    fn first_loads(func: &Function, analysis: &Analysis, target: BlockId) -> Vec<(InstId, usize)> {
595        let key_count = analysis.keys.len();
596        let mut blocked = KeySet::empty(key_count);
597        let mut taken: FxHashSet<usize> = FxHashSet::default();
598        let mut found = Vec::new();
599
600        for &inst_id in &func.blocks[target].instructions {
601            if let Some((key, GenSource::LoadResult)) = Self::gen_key_value(func, inst_id) {
602                if let Some(&idx) = analysis.key_index.get(&key)
603                    && !blocked.contains(idx)
604                    && taken.insert(idx)
605                {
606                    found.push((inst_id, idx));
607                }
608                continue;
609            }
610            let kind = &func.instructions[inst_id].kind;
611            match kind {
612                // `gas` blocks every space, so nothing after it can be a
613                // candidate.
614                InstKind::Gas => break,
615                InstKind::MSize => {
616                    for (idx, key) in analysis.keys.iter().enumerate() {
617                        if matches!(key, LoadKey::Memory(_) | LoadKey::Keccak(_, _)) {
618                            blocked.insert(idx);
619                        }
620                    }
621                }
622                _ if kind.has_side_effects() => {
623                    // Kills block their keys; a store's own-key gen is also a
624                    // kill here (the value differs from the predecessor-end
625                    // state), which the may-alias check already covers.
626                    for (idx, &key) in analysis.keys.iter().enumerate() {
627                        if !blocked.contains(idx) && Self::inst_kills_key(func, inst_id, key) {
628                            blocked.insert(idx);
629                        }
630                    }
631                }
632                _ => {}
633            }
634        }
635
636        found
637    }
638
639    fn same_key_loads_in_target(
640        func: &Function,
641        analysis: &Analysis,
642        target: BlockId,
643        first_inst: InstId,
644        key: LoadKey,
645    ) -> Vec<(InstId, ValueId)> {
646        let mut loads = Vec::new();
647        let mut past_first = false;
648
649        for &inst_id in &func.blocks[target].instructions {
650            if inst_id == first_inst {
651                past_first = true;
652            }
653            if !past_first {
654                continue;
655            }
656
657            if inst_id != first_inst {
658                let kind = &func.instructions[inst_id].kind;
659                if matches!(kind, InstKind::Gas) {
660                    break;
661                }
662                if matches!(kind, InstKind::MSize)
663                    && matches!(key, LoadKey::Memory(_) | LoadKey::Keccak(_, _))
664                {
665                    break;
666                }
667                if kind.has_side_effects() && Self::inst_kills_key(func, inst_id, key) {
668                    break;
669                }
670            }
671
672            if let Some((load_key, GenSource::LoadResult)) = Self::gen_key_value(func, inst_id)
673                && load_key == key
674                && let Some(&value) = analysis.inst_results.get(&inst_id)
675            {
676                loads.push((inst_id, value));
677            }
678        }
679
680        loads
681    }
682
683    fn candidate_for_load(
684        &self,
685        func: &Function,
686        cx: &mut CandidateCx<'_>,
687        target: BlockId,
688        inst: InstId,
689        key_idx: usize,
690        predecessors: &[BlockId],
691    ) -> Option<Candidate> {
692        let instruction = &func.instructions[inst];
693        let result = *cx.analysis.inst_results.get(&inst)?;
694        let result_ty = instruction.result_ty?;
695        let key = cx.analysis.keys[key_idx];
696        let loads = Self::same_key_loads_in_target(func, cx.analysis, target, inst, key);
697        if loads.is_empty() {
698            return None;
699        }
700
701        let mut incoming = Vec::with_capacity(predecessors.len());
702        let mut insertions = Vec::new();
703        for &pred in predecessors {
704            if cx.analysis.outs.get(&pred).is_some_and(|out| out.contains(key_idx))
705                && let Some(value) = self.locate_value(func, cx, pred, key_idx)
706            {
707                incoming.push((pred, value));
708                continue;
709            }
710            // The key is unavailable on this predecessor; a compensating load
711            // can only go on an edge that needs no splitting.
712            if !Self::can_insert_on_edge(func, pred, target) {
713                return None;
714            }
715            // Termination rule 2: never insert a key into a block it was
716            // previously eliminated from in this run.
717            if cx.eliminated_keys.contains(&(key, pred)) {
718                return None;
719            }
720            if !Self::operands_dominate_block(func, &instruction.kind, pred, cx.analysis) {
721                return None;
722            }
723            insertions.push(pred);
724        }
725
726        let loop_carried =
727            Self::is_loop_carried_rewrite(&cx.analysis.dominators, target, &incoming, &insertions);
728        let needs_phi = !insertions.is_empty()
729            || incoming.first().is_none_or(|&(_, first)| {
730                first == result || incoming.iter().any(|&(_, value)| value != first)
731            });
732        let model = LoadPreCostModel;
733        let cost = model.estimate(LoadPreCostInput {
734            func,
735            key,
736            kind: &instruction.kind,
737            predecessors,
738            loads: &loads,
739            insertions: &insertions,
740            loop_carried,
741            needs_phi,
742        });
743        if !cost.is_profitable() {
744            return None;
745        }
746
747        if !insertions.is_empty() {
748            // Insertions must be structurally safe; profitability is decided
749            // by `LoadPreCostModel` above.
750            let loop_insertion = incoming
751                .iter()
752                .all(|&(pred, _)| cx.analysis.dominators.dominates(target, pred))
753                && insertions.iter().all(|&pred| !cx.analysis.dominators.dominates(target, pred));
754            let diamond_insertion = Self::is_non_cyclic_diamond_insertion(
755                &cx.analysis.dominators,
756                target,
757                &incoming,
758                &insertions,
759            );
760            if !(loop_insertion || diamond_insertion) || insertions.len() > incoming.len() {
761                return None;
762            }
763        }
764
765        Some(Candidate {
766            target,
767            key,
768            result_ty,
769            kind: instruction.kind.clone(),
770            metadata: instruction.metadata.clone(),
771            loads,
772            incoming,
773            insertions,
774        })
775    }
776
777    fn is_loop_carried_rewrite(
778        dominators: &DominatorTree,
779        target: BlockId,
780        incoming: &[(BlockId, ValueId)],
781        insertions: &[BlockId],
782    ) -> bool {
783        let has_backedge_incoming =
784            incoming.iter().any(|&(pred, _)| dominators.dominates(target, pred));
785        let has_entry_edge = incoming.iter().any(|&(pred, _)| !dominators.dominates(target, pred))
786            || !insertions.is_empty();
787        has_backedge_incoming
788            && has_entry_edge
789            && insertions.iter().all(|&pred| !dominators.dominates(target, pred))
790    }
791
792    fn is_non_cyclic_diamond_insertion(
793        dominators: &DominatorTree,
794        target: BlockId,
795        incoming: &[(BlockId, ValueId)],
796        insertions: &[BlockId],
797    ) -> bool {
798        !incoming.is_empty()
799            && !insertions.is_empty()
800            && incoming.iter().all(|&(pred, _)| !dominators.dominates(target, pred))
801            && insertions.iter().all(|&pred| !dominators.dominates(target, pred))
802    }
803
804    /// Locates the concrete value of `key` at the end of `block`, walking the
805    /// dominator tree when the block is transparent for the key.
806    fn locate_value(
807        &self,
808        func: &Function,
809        cx: &mut CandidateCx<'_>,
810        block: BlockId,
811        key_idx: usize,
812    ) -> Option<ValueId> {
813        if let Some(&cached) = cx.locate_cache.get(&(block, key_idx)) {
814            return cached;
815        }
816        let located = self.locate_value_uncached(func, cx, block, key_idx);
817        cx.locate_cache.insert((block, key_idx), located);
818        located
819    }
820
821    fn locate_value_uncached(
822        &self,
823        func: &Function,
824        cx: &mut CandidateCx<'_>,
825        block: BlockId,
826        key_idx: usize,
827    ) -> Option<ValueId> {
828        let key = cx.analysis.keys[key_idx];
829        for &inst_id in func.blocks[block].instructions.iter().rev() {
830            if let Some((gen_key, source)) = Self::gen_key_value(func, inst_id)
831                && gen_key == key
832            {
833                // A store's exact-key gen wins over its own kill: the slot
834                // holds the stored value from this point on.
835                return match source {
836                    GenSource::LoadResult => cx.analysis.inst_results.get(&inst_id).copied(),
837                    GenSource::Stored(value) => Some(value),
838                };
839            }
840            if Self::inst_kills_key(func, inst_id, key) {
841                return None;
842            }
843        }
844
845        // The block is transparent for the key: the value at its end is the
846        // value at its entry, which the dataflow must prove available on all
847        // paths before the dominator walk may locate it.
848        if !cx.analysis.ins.get(&block).is_some_and(|in_set| in_set.contains(key_idx)) {
849            return None;
850        }
851        let idom = cx.analysis.dominators.idom(block)?;
852        if idom == block {
853            return None;
854        }
855        // Path purity: a non-dominating path between the dominator and this
856        // block could kill and re-gen the key with a different value, which
857        // availability alone does not rule out.
858        if cx.analysis.path_kills_key(idom, block, key_idx) {
859            return None;
860        }
861        self.locate_value(func, cx, idom, key_idx)
862    }
863
864    fn apply_candidate(
865        &mut self,
866        func: &mut Function,
867        candidate: Candidate,
868        eliminated_keys: &mut FxHashSet<(LoadKey, BlockId)>,
869        inserted_insts: &mut FxHashSet<InstId>,
870    ) {
871        let Candidate { target, key, result_ty, kind, metadata, loads, mut incoming, insertions } =
872            candidate;
873
874        eliminated_keys.insert((key, target));
875
876        let fully_available = insertions.is_empty();
877        for block in insertions {
878            let new_inst = func.alloc_inst(Instruction {
879                kind: kind.clone(),
880                result_ty: Some(result_ty),
881                metadata: metadata.clone(),
882            });
883            let value = func.alloc_value(Value::Inst(new_inst));
884            func.blocks[block].instructions.push(new_inst);
885            incoming.push((block, value));
886            inserted_insts.insert(new_inst);
887            self.stats.loads_inserted += 1;
888        }
889        incoming.sort_by_key(|(block, _)| block.index());
890
891        // A fully-available key whose predecessors all locate the same value
892        // needs no phi: that value's def dominates every predecessor and
893        // therefore the join itself.
894        let first_result = loads[0].1;
895        let replacement = match incoming.first() {
896            Some(&(_, first))
897                if fully_available
898                    && first != first_result
899                    && incoming.iter().all(|&(_, value)| value == first) =>
900            {
901                first
902            }
903            _ => {
904                let phi_inst =
905                    func.alloc_inst(Instruction::new(InstKind::Phi(incoming), Some(result_ty)));
906                let phi_value = func.alloc_value(Value::Inst(phi_inst));
907                let phi_count = func.blocks[target]
908                    .instructions
909                    .iter()
910                    .take_while(|&&inst_id| {
911                        matches!(func.instructions[inst_id].kind, InstKind::Phi(_))
912                    })
913                    .count();
914                func.blocks[target].instructions.insert(phi_count, phi_inst);
915                phi_value
916            }
917        };
918
919        for &(_, load_result) in &loads {
920            Self::replace_uses(func, load_result, replacement);
921        }
922        let load_insts: FxHashSet<InstId> = loads.iter().map(|&(inst_id, _)| inst_id).collect();
923        func.blocks[target].instructions.retain(|&inst_id| !load_insts.contains(&inst_id));
924        self.stats.loads_eliminated += loads.len();
925    }
926
927    // ----- Keys, gens, and kills -----
928
929    /// Returns the key an instruction gens and where its value comes from.
930    fn gen_key_value(func: &Function, inst_id: InstId) -> Option<(LoadKey, GenSource)> {
931        match func.instructions[inst_id].kind {
932            InstKind::SLoad(slot) => {
933                Some((LoadKey::Storage(func.storage_alias(inst_id, slot)), GenSource::LoadResult))
934            }
935            InstKind::SStore(slot, value) => Some((
936                LoadKey::Storage(func.storage_alias(inst_id, slot)),
937                GenSource::Stored(value),
938            )),
939            InstKind::TLoad(slot) => {
940                Some((LoadKey::Transient(func.storage_alias(inst_id, slot)), GenSource::LoadResult))
941            }
942            InstKind::TStore(slot, value) => Some((
943                LoadKey::Transient(func.storage_alias(inst_id, slot)),
944                GenSource::Stored(value),
945            )),
946            InstKind::MLoad(addr) => Self::mem_addr(func, inst_id, addr)
947                .map(|addr| (LoadKey::Memory(addr), GenSource::LoadResult)),
948            InstKind::MStore(addr, value) => Self::mem_addr(func, inst_id, addr)
949                .map(|addr| (LoadKey::Memory(addr), GenSource::Stored(value))),
950            InstKind::Keccak256(offset, size) => {
951                let addr = Self::mem_addr(func, inst_id, offset)?;
952                let size = match func.value_u64(size) {
953                    Some(size) => KeccakSize::Const(size),
954                    None => KeccakSize::Dyn(size),
955                };
956                Some((LoadKey::Keccak(addr, size), GenSource::LoadResult))
957            }
958            _ => None,
959        }
960    }
961
962    /// Returns true if an instruction may invalidate the value of `key`.
963    fn inst_kills_key(func: &Function, inst_id: InstId, key: LoadKey) -> bool {
964        let kind = &func.instructions[inst_id].kind;
965        match key {
966            LoadKey::Storage(alias) => match *kind {
967                InstKind::SStore(slot, _) => func.storage_alias(inst_id, slot).may_alias(alias),
968                // Calls and creates may re-enter and mutate storage;
969                // STATICCALL cannot.
970                _ => kind.may_mutate_storage(),
971            },
972            LoadKey::Transient(alias) => match *kind {
973                InstKind::TStore(slot, _) => func.storage_alias(inst_id, slot).may_alias(alias),
974                _ => kind.may_mutate_transient_storage(),
975            },
976            LoadKey::Memory(addr) => Self::memory_write_clobbers(func, inst_id, addr, Some(32)),
977            LoadKey::Keccak(addr, size) => {
978                let size = match size {
979                    KeccakSize::Const(size) => Some(size),
980                    KeccakSize::Dyn(_) => None,
981                };
982                Self::memory_write_clobbers(func, inst_id, addr, size)
983            }
984        }
985    }
986
987    /// Returns true if a memory-writing instruction may overlap the read
988    /// range; reads with an unknown size are clobbered by any write that the
989    /// region split cannot rule out.
990    fn memory_write_clobbers(
991        func: &Function,
992        inst_id: InstId,
993        read: MemAddr,
994        read_size: Option<u64>,
995    ) -> bool {
996        let kind = &func.instructions[inst_id].kind;
997        let (dest, write_size) = match *kind {
998            InstKind::MStore(dest, _) => (dest, Some(32)),
999            InstKind::MStore8(dest, _) => (dest, Some(1)),
1000            InstKind::MCopy(dest, _, size)
1001            | InstKind::CalldataCopy(dest, _, size)
1002            | InstKind::CodeCopy(dest, _, size)
1003            | InstKind::ReturnDataCopy(dest, _, size) => (dest, func.value_u64(size)),
1004            InstKind::ExtCodeCopy(_, dest, _, size) => (dest, func.value_u64(size)),
1005            // Every call clobbers tracked memory, including STATICCALL: its
1006            // return buffer write is a memory effect even in a static context.
1007            _ => return kind.may_mutate_memory(),
1008        };
1009
1010        let write_region = func.instructions[inst_id]
1011            .metadata
1012            .memory_region()
1013            .unwrap_or_else(|| func.memory_region_for_addr(dest));
1014        if read.region != MemoryRegion::Unknown
1015            && write_region != MemoryRegion::Unknown
1016            && read.region != write_region
1017        {
1018            return false;
1019        }
1020        let (write_base, write_offset) = Self::memory_addr_base_offset(func, dest);
1021        if read.base != write_base {
1022            return true;
1023        }
1024        let (Some(read_size), Some(write_offset), Some(write_size)) =
1025            (read_size, write_offset, write_size)
1026        else {
1027            return true;
1028        };
1029        mir_utils::ranges_overlap(read.offset, read_size, write_offset, write_size)
1030    }
1031
1032    fn mem_addr(func: &Function, inst_id: InstId, addr: ValueId) -> Option<MemAddr> {
1033        let region = func.instructions[inst_id]
1034            .metadata
1035            .memory_region()
1036            .unwrap_or_else(|| func.memory_region_for_addr(addr));
1037        let (base, offset) = Self::memory_addr_base_offset(func, addr);
1038        Some(MemAddr { region, base, offset: offset? })
1039    }
1040
1041    fn memory_addr_base_offset(func: &Function, addr: ValueId) -> (Option<ValueId>, Option<u64>) {
1042        if let Some((base, offset)) = Self::offset_chain(func, addr, 0) {
1043            if let Some(offset) = mir_utils::u256_to_u64(offset) {
1044                return (Some(base), Some(offset));
1045            }
1046            return (Some(addr), Some(0));
1047        }
1048        match func.value(addr) {
1049            Value::Immediate(imm) => (None, imm.as_u256().and_then(mir_utils::u256_to_u64)),
1050            Value::Arg { .. } | Value::Inst(_) | Value::Undef(_) | Value::Error(_) => {
1051                (Some(addr), Some(0))
1052            }
1053        }
1054    }
1055
1056    /// Splits `value` into a symbolic base plus a constant offset by walking
1057    /// constant `add`/`sub` chains, so syntactically different addresses of
1058    /// the same location unify.
1059    fn offset_chain(func: &Function, value: ValueId, depth: usize) -> Option<(ValueId, U256)> {
1060        if depth >= 4 {
1061            return None;
1062        }
1063        match func.value(value) {
1064            Value::Immediate(_) => None,
1065            Value::Arg { .. } | Value::Undef(_) | Value::Error(_) => Some((value, U256::ZERO)),
1066            Value::Inst(inst_id) => match func.instructions[*inst_id].kind {
1067                InstKind::Add(a, b) => {
1068                    if let Some(offset) = func.value_u256(b) {
1069                        let (base, existing) = Self::offset_chain(func, a, depth + 1)?;
1070                        Some((base, existing.wrapping_add(offset)))
1071                    } else if let Some(offset) = func.value_u256(a) {
1072                        let (base, existing) = Self::offset_chain(func, b, depth + 1)?;
1073                        Some((base, existing.wrapping_add(offset)))
1074                    } else {
1075                        Some((value, U256::ZERO))
1076                    }
1077                }
1078                InstKind::Sub(a, b) => {
1079                    let offset = func.value_u256(b)?;
1080                    let (base, existing) = Self::offset_chain(func, a, depth + 1)?;
1081                    Some((base, existing.wrapping_sub(offset)))
1082                }
1083                _ => Some((value, U256::ZERO)),
1084            },
1085        }
1086    }
1087
1088    // ----- CFG helpers -----
1089
1090    fn can_insert_on_edge(func: &Function, pred: BlockId, target: BlockId) -> bool {
1091        matches!(func.blocks[pred].terminator, Some(Terminator::Jump(jump_target)) if jump_target == target)
1092    }
1093
1094    fn operands_dominate_block(
1095        func: &Function,
1096        kind: &InstKind,
1097        block: BlockId,
1098        analysis: &Analysis,
1099    ) -> bool {
1100        kind.operands().into_iter().all(|value| match func.value(value) {
1101            Value::Immediate(_) | Value::Arg { .. } | Value::Undef(_) | Value::Error(_) => true,
1102            Value::Inst(inst_id) => analysis
1103                .inst_blocks
1104                .get(inst_id)
1105                .is_some_and(|def_block| analysis.dominators.dominates(*def_block, block)),
1106        })
1107    }
1108
1109    // ----- Rewriting -----
1110
1111    fn replace_uses(func: &mut Function, from: ValueId, to: ValueId) {
1112        for inst in func.instructions.iter_mut() {
1113            let mut changed = false;
1114            inst.kind.visit_operands_mut(|value| {
1115                if *value == from {
1116                    *value = to;
1117                    changed = true;
1118                }
1119            });
1120            if !changed {
1121                continue;
1122            }
1123            // Operand-derived metadata is stale once the operand changes.
1124            if mir_utils::is_memory_inst(&inst.kind) {
1125                inst.metadata.set_memory_region(None);
1126            }
1127            if matches!(
1128                inst.kind,
1129                InstKind::SLoad(_)
1130                    | InstKind::SStore(_, _)
1131                    | InstKind::TLoad(_)
1132                    | InstKind::TStore(_, _)
1133            ) {
1134                inst.metadata.set_storage_alias(None);
1135            }
1136        }
1137
1138        for block in func.blocks.iter_mut() {
1139            if let Some(term) = &mut block.terminator {
1140                Self::replace_terminator_uses(term, from, to);
1141            }
1142        }
1143    }
1144
1145    fn replace_terminator_uses(term: &mut Terminator, from: ValueId, to: ValueId) {
1146        let replace = |value: &mut ValueId| {
1147            if *value == from {
1148                *value = to;
1149            }
1150        };
1151
1152        match term {
1153            Terminator::Jump(_) | Terminator::Stop | Terminator::Invalid => {}
1154            Terminator::Branch { condition, .. } => replace(condition),
1155            Terminator::Switch { value, cases, .. } => {
1156                replace(value);
1157                for (case, _) in cases {
1158                    replace(case);
1159                }
1160            }
1161            Terminator::Return { values } => {
1162                for value in values {
1163                    replace(value);
1164                }
1165            }
1166            Terminator::Revert { offset, size } | Terminator::ReturnData { offset, size } => {
1167                replace(offset);
1168                replace(size);
1169            }
1170            Terminator::SelfDestruct { recipient } => replace(recipient),
1171        }
1172    }
1173}