use crate::{
analysis::{CfgInfo, DominatorTree},
mir::{
BlockId, Function, InstId, InstKind, Instruction, InstructionMetadata, MemoryRegion,
MirType, StorageAlias, Terminator, Value, ValueId,
utils::{self as mir_utils, repair_reachability_phis},
},
pass::FunctionPass,
};
use alloy_primitives::U256;
use solar_data_structures::{
bit_set::DenseBitSet,
map::{FxHashMap, FxHashSet},
newtype_index,
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct LoadPreStats {
pub loads_eliminated: usize,
pub loads_inserted: usize,
}
impl LoadPreStats {
pub const fn total(self) -> usize {
self.loads_eliminated + self.loads_inserted
}
}
#[derive(Debug, Default)]
pub struct LoadRedundancyEliminator {
stats: LoadPreStats,
}
pub struct LoadPrePass;
impl FunctionPass for LoadPrePass {
fn name(&self) -> &str {
"load-pre"
}
fn run_on_function(&mut self, func: &mut Function) -> bool {
LoadRedundancyEliminator::new().run(func).total() != 0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum LoadKey {
Storage(StorageAlias),
Transient(StorageAlias),
Memory(MemAddr),
Keccak(MemAddr, KeccakSize),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct MemAddr {
region: MemoryRegion,
base: Option<ValueId>,
offset: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum KeccakSize {
Const(u64),
Dyn(ValueId),
}
enum GenSource {
LoadResult,
Stored(ValueId),
}
newtype_index! {
struct KeyIdx;
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct KeySet(DenseBitSet<KeyIdx>);
impl KeySet {
fn empty(len: usize) -> Self {
Self(DenseBitSet::new_empty(len))
}
fn full(len: usize) -> Self {
Self(DenseBitSet::new_filled(len))
}
fn insert(&mut self, idx: usize) {
self.0.insert(KeyIdx::from_usize(idx));
}
fn remove(&mut self, idx: usize) {
self.0.remove(KeyIdx::from_usize(idx));
}
fn contains(&self, idx: usize) -> bool {
self.0.contains(KeyIdx::from_usize(idx))
}
fn is_empty(&self) -> bool {
self.0.is_empty()
}
fn intersect_with(&mut self, other: &Self) {
self.0.intersect(&other.0);
}
fn subtract(&mut self, other: &Self) {
self.0.subtract(&other.0);
}
fn union_with(&mut self, other: &Self) {
self.0.union(&other.0);
}
}
struct Candidate {
target: BlockId,
key: LoadKey,
result_ty: MirType,
kind: InstKind,
metadata: InstructionMetadata,
loads: Vec<(InstId, ValueId)>,
incoming: Vec<(BlockId, ValueId)>,
insertions: Vec<BlockId>,
}
#[derive(Clone, Copy, Debug, Default)]
struct LoadPreCostModel;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct LoadPreCost {
saved: i64,
inserted: i64,
phi: i64,
operands: i64,
}
impl LoadPreCost {
const fn is_profitable(self) -> bool {
self.saved > self.inserted + self.phi + self.operands
}
}
struct LoadPreCostInput<'a> {
func: &'a Function,
key: LoadKey,
kind: &'a InstKind,
predecessors: &'a [BlockId],
loads: &'a [(InstId, ValueId)],
insertions: &'a [BlockId],
loop_carried: bool,
needs_phi: bool,
}
impl LoadPreCostModel {
const MEMORY_READ: i64 = 3;
const STORAGE_READ: i64 = 100;
const TRANSIENT_READ: i64 = 100;
const KECCAK_BASE: i64 = 30;
const KECCAK_WORD: i64 = 6;
const NON_LOOP_PHI_EDGE_COPY: i64 = 3;
const CROSS_BLOCK_OPERAND: i64 = 3;
fn estimate(&self, input: LoadPreCostInput<'_>) -> LoadPreCost {
let read = Self::read_cost(input.key);
let saved = read * input.loads.len() as i64 * input.predecessors.len() as i64;
let inserted = read * input.insertions.len() as i64;
let phi = if !input.needs_phi || input.loop_carried {
0
} else {
Self::NON_LOOP_PHI_EDGE_COPY * input.predecessors.len() as i64
};
let operands = Self::inserted_operand_cost(input.func, input.kind, input.insertions);
LoadPreCost { saved, inserted, phi, operands }
}
const fn read_cost(key: LoadKey) -> i64 {
match key {
LoadKey::Storage(_) => Self::STORAGE_READ,
LoadKey::Transient(_) => Self::TRANSIENT_READ,
LoadKey::Memory(_) => Self::MEMORY_READ,
LoadKey::Keccak(_, size) => match size {
KeccakSize::Const(size) => {
Self::KECCAK_BASE + Self::KECCAK_WORD * size.div_ceil(32) as i64
}
KeccakSize::Dyn(_) => Self::KECCAK_BASE + Self::KECCAK_WORD * 2,
},
}
}
fn inserted_operand_cost(func: &Function, kind: &InstKind, insertions: &[BlockId]) -> i64 {
if insertions.is_empty() {
return 0;
}
let cross_block_operands = kind
.operands()
.into_iter()
.filter(|&value| matches!(func.value(value), Value::Inst(_) | Value::Undef(_)))
.count();
Self::CROSS_BLOCK_OPERAND * cross_block_operands as i64 * insertions.len() as i64
}
}
struct Analysis {
keys: Vec<LoadKey>,
key_index: FxHashMap<LoadKey, usize>,
reachable: FxHashSet<BlockId>,
kills: FxHashMap<BlockId, KeySet>,
ins: FxHashMap<BlockId, KeySet>,
outs: FxHashMap<BlockId, KeySet>,
reach: FxHashMap<BlockId, FxHashSet<BlockId>>,
dominators: DominatorTree,
inst_results: FxHashMap<InstId, ValueId>,
inst_blocks: FxHashMap<InstId, BlockId>,
}
impl Analysis {
fn path_kills_key(&self, from: BlockId, to: BlockId, key_idx: usize) -> bool {
if self.kills.is_empty() {
return false;
}
let Some(reachable_from) = self.reach.get(&from) else { return true };
for (&mid, kills) in &self.kills {
if mid == from || !kills.contains(key_idx) {
continue;
}
if reachable_from.contains(&mid)
&& self.reach.get(&mid).is_some_and(|reach| reach.contains(&to))
{
return true;
}
}
false
}
}
struct CandidateCx<'a> {
analysis: &'a Analysis,
eliminated_keys: &'a FxHashSet<(LoadKey, BlockId)>,
inserted_insts: &'a FxHashSet<InstId>,
locate_cache: FxHashMap<(BlockId, usize), Option<ValueId>>,
}
impl LoadRedundancyEliminator {
pub fn new() -> Self {
Self::default()
}
pub const fn stats(&self) -> LoadPreStats {
self.stats
}
pub fn run(&mut self, func: &mut Function) -> LoadPreStats {
self.stats = LoadPreStats::default();
repair_reachability_phis(func);
let rewrite_limit = func.instructions.len().saturating_mul(2).max(64);
let mut rewrites = 0usize;
let mut eliminated_keys: FxHashSet<(LoadKey, BlockId)> = FxHashSet::default();
let mut inserted_insts: FxHashSet<InstId> = FxHashSet::default();
while rewrites < rewrite_limit {
let Some(analysis) = Self::compute_analysis(func) else { break };
let mut cx = CandidateCx {
analysis: &analysis,
eliminated_keys: &eliminated_keys,
inserted_insts: &inserted_insts,
locate_cache: FxHashMap::default(),
};
let batch = self.collect_candidates(func, &mut cx, rewrite_limit - rewrites);
if batch.is_empty() {
break;
}
rewrites += batch.len();
for candidate in batch {
self.apply_candidate(func, candidate, &mut eliminated_keys, &mut inserted_insts);
}
}
self.stats
}
fn compute_analysis(func: &Function) -> Option<Analysis> {
let mut cfg = CfgInfo::new(func);
let rpo = cfg.rpo();
let mut keys = Vec::new();
let mut key_index: FxHashMap<LoadKey, usize> = FxHashMap::default();
for &block in rpo {
for &inst_id in &func.blocks[block].instructions {
if let Some((key, _)) = Self::gen_key_value(func, inst_id) {
key_index.entry(key).or_insert_with(|| {
keys.push(key);
keys.len() - 1
});
}
}
}
if keys.is_empty() {
return None;
}
let key_count = keys.len();
let mut gens = FxHashMap::default();
let mut kills = FxHashMap::default();
for &block in rpo {
let mut gen_set = KeySet::empty(key_count);
let mut kill_set = KeySet::empty(key_count);
for &inst_id in &func.blocks[block].instructions {
if func.instructions[inst_id].kind.has_side_effects() {
for (idx, &key) in keys.iter().enumerate() {
if Self::inst_kills_key(func, inst_id, key) {
kill_set.insert(idx);
gen_set.remove(idx);
}
}
}
if let Some((key, _)) = Self::gen_key_value(func, inst_id)
&& let Some(&idx) = key_index.get(&key)
{
gen_set.insert(idx);
}
}
if !kill_set.is_empty() {
kills.insert(block, kill_set);
}
gens.insert(block, gen_set);
}
let mut ins: FxHashMap<BlockId, KeySet> = FxHashMap::default();
let mut outs: FxHashMap<BlockId, KeySet> = rpo
.iter()
.map(|&block| {
let out = if block == func.entry_block {
gens[&block].clone()
} else {
KeySet::full(key_count)
};
(block, out)
})
.collect();
loop {
let mut changed = false;
for &block in rpo {
let in_set = if block == func.entry_block {
KeySet::empty(key_count)
} else {
let mut acc: Option<KeySet> = None;
for &pred in &func.blocks[block].predecessors {
let Some(out) = outs.get(&pred) else { continue };
match &mut acc {
Some(acc) => acc.intersect_with(out),
None => acc = Some(out.clone()),
}
}
acc.unwrap_or_else(|| KeySet::empty(key_count))
};
let mut out = in_set.clone();
if let Some(kill) = kills.get(&block) {
out.subtract(kill);
}
out.union_with(&gens[&block]);
ins.insert(block, in_set);
if outs.get(&block) != Some(&out) {
outs.insert(block, out);
changed = true;
}
}
if !changed {
break;
}
}
let reach = if kills.is_empty() {
FxHashMap::default()
} else {
cfg.transitive_reachability().clone()
};
Some(Analysis {
keys,
key_index,
reachable: cfg.reachable().clone(),
kills,
ins,
outs,
reach,
dominators: cfg.dominators().clone(),
inst_results: func.inst_results(),
inst_blocks: func.inst_blocks(),
})
}
fn collect_candidates(
&self,
func: &Function,
cx: &mut CandidateCx<'_>,
limit: usize,
) -> Vec<Candidate> {
let mut batch = Vec::new();
let mut modified_blocks: FxHashSet<BlockId> = FxHashSet::default();
let mut eliminated_values: FxHashSet<ValueId> = FxHashSet::default();
'targets: for target in func.blocks.indices() {
if !cx.analysis.reachable.contains(&target) {
continue;
}
let predecessors = func.unique_predecessors(target);
if predecessors.len() < 2
|| predecessors.iter().any(|pred| !cx.analysis.reachable.contains(pred))
{
continue;
}
for (inst, key_idx) in Self::first_loads(func, cx.analysis, target) {
if batch.len() >= limit {
break 'targets;
}
if cx.inserted_insts.contains(&inst) {
continue;
}
let Some(candidate) =
self.candidate_for_load(func, cx, target, inst, key_idx, &predecessors)
else {
continue;
};
if Self::interferes_with_batch(&candidate, &modified_blocks, &eliminated_values) {
continue;
}
modified_blocks.insert(candidate.target);
modified_blocks.extend(candidate.insertions.iter().copied());
eliminated_values.extend(candidate.loads.iter().map(|&(_, value)| value));
batch.push(candidate);
}
}
batch
}
fn interferes_with_batch(
candidate: &Candidate,
modified_blocks: &FxHashSet<BlockId>,
eliminated_values: &FxHashSet<ValueId>,
) -> bool {
modified_blocks.contains(&candidate.target)
|| candidate.insertions.iter().any(|block| modified_blocks.contains(block))
|| candidate.incoming.iter().any(|(_, value)| eliminated_values.contains(value))
|| candidate.loads.iter().any(|&(_, value)| eliminated_values.contains(&value))
|| (!candidate.insertions.is_empty()
&& candidate
.kind
.operands()
.into_iter()
.any(|value| eliminated_values.contains(&value)))
}
fn first_loads(func: &Function, analysis: &Analysis, target: BlockId) -> Vec<(InstId, usize)> {
let key_count = analysis.keys.len();
let mut blocked = KeySet::empty(key_count);
let mut taken: FxHashSet<usize> = FxHashSet::default();
let mut found = Vec::new();
for &inst_id in &func.blocks[target].instructions {
if let Some((key, GenSource::LoadResult)) = Self::gen_key_value(func, inst_id) {
if let Some(&idx) = analysis.key_index.get(&key)
&& !blocked.contains(idx)
&& taken.insert(idx)
{
found.push((inst_id, idx));
}
continue;
}
let kind = &func.instructions[inst_id].kind;
match kind {
InstKind::Gas => break,
InstKind::MSize => {
for (idx, key) in analysis.keys.iter().enumerate() {
if matches!(key, LoadKey::Memory(_) | LoadKey::Keccak(_, _)) {
blocked.insert(idx);
}
}
}
_ if kind.has_side_effects() => {
for (idx, &key) in analysis.keys.iter().enumerate() {
if !blocked.contains(idx) && Self::inst_kills_key(func, inst_id, key) {
blocked.insert(idx);
}
}
}
_ => {}
}
}
found
}
fn same_key_loads_in_target(
func: &Function,
analysis: &Analysis,
target: BlockId,
first_inst: InstId,
key: LoadKey,
) -> Vec<(InstId, ValueId)> {
let mut loads = Vec::new();
let mut past_first = false;
for &inst_id in &func.blocks[target].instructions {
if inst_id == first_inst {
past_first = true;
}
if !past_first {
continue;
}
if inst_id != first_inst {
let kind = &func.instructions[inst_id].kind;
if matches!(kind, InstKind::Gas) {
break;
}
if matches!(kind, InstKind::MSize)
&& matches!(key, LoadKey::Memory(_) | LoadKey::Keccak(_, _))
{
break;
}
if kind.has_side_effects() && Self::inst_kills_key(func, inst_id, key) {
break;
}
}
if let Some((load_key, GenSource::LoadResult)) = Self::gen_key_value(func, inst_id)
&& load_key == key
&& let Some(&value) = analysis.inst_results.get(&inst_id)
{
loads.push((inst_id, value));
}
}
loads
}
fn candidate_for_load(
&self,
func: &Function,
cx: &mut CandidateCx<'_>,
target: BlockId,
inst: InstId,
key_idx: usize,
predecessors: &[BlockId],
) -> Option<Candidate> {
let instruction = &func.instructions[inst];
let result = *cx.analysis.inst_results.get(&inst)?;
let result_ty = instruction.result_ty?;
let key = cx.analysis.keys[key_idx];
let loads = Self::same_key_loads_in_target(func, cx.analysis, target, inst, key);
if loads.is_empty() {
return None;
}
let mut incoming = Vec::with_capacity(predecessors.len());
let mut insertions = Vec::new();
for &pred in predecessors {
if cx.analysis.outs.get(&pred).is_some_and(|out| out.contains(key_idx))
&& let Some(value) = self.locate_value(func, cx, pred, key_idx)
{
incoming.push((pred, value));
continue;
}
if !Self::can_insert_on_edge(func, pred, target) {
return None;
}
if cx.eliminated_keys.contains(&(key, pred)) {
return None;
}
if !Self::operands_dominate_block(func, &instruction.kind, pred, cx.analysis) {
return None;
}
insertions.push(pred);
}
let loop_carried =
Self::is_loop_carried_rewrite(&cx.analysis.dominators, target, &incoming, &insertions);
let needs_phi = !insertions.is_empty()
|| incoming.first().is_none_or(|&(_, first)| {
first == result || incoming.iter().any(|&(_, value)| value != first)
});
let model = LoadPreCostModel;
let cost = model.estimate(LoadPreCostInput {
func,
key,
kind: &instruction.kind,
predecessors,
loads: &loads,
insertions: &insertions,
loop_carried,
needs_phi,
});
if !cost.is_profitable() {
return None;
}
if !insertions.is_empty() {
let loop_insertion = incoming
.iter()
.all(|&(pred, _)| cx.analysis.dominators.dominates(target, pred))
&& insertions.iter().all(|&pred| !cx.analysis.dominators.dominates(target, pred));
let diamond_insertion = Self::is_non_cyclic_diamond_insertion(
&cx.analysis.dominators,
target,
&incoming,
&insertions,
);
if !(loop_insertion || diamond_insertion) || insertions.len() > incoming.len() {
return None;
}
}
Some(Candidate {
target,
key,
result_ty,
kind: instruction.kind.clone(),
metadata: instruction.metadata.clone(),
loads,
incoming,
insertions,
})
}
fn is_loop_carried_rewrite(
dominators: &DominatorTree,
target: BlockId,
incoming: &[(BlockId, ValueId)],
insertions: &[BlockId],
) -> bool {
let has_backedge_incoming =
incoming.iter().any(|&(pred, _)| dominators.dominates(target, pred));
let has_entry_edge = incoming.iter().any(|&(pred, _)| !dominators.dominates(target, pred))
|| !insertions.is_empty();
has_backedge_incoming
&& has_entry_edge
&& insertions.iter().all(|&pred| !dominators.dominates(target, pred))
}
fn is_non_cyclic_diamond_insertion(
dominators: &DominatorTree,
target: BlockId,
incoming: &[(BlockId, ValueId)],
insertions: &[BlockId],
) -> bool {
!incoming.is_empty()
&& !insertions.is_empty()
&& incoming.iter().all(|&(pred, _)| !dominators.dominates(target, pred))
&& insertions.iter().all(|&pred| !dominators.dominates(target, pred))
}
fn locate_value(
&self,
func: &Function,
cx: &mut CandidateCx<'_>,
block: BlockId,
key_idx: usize,
) -> Option<ValueId> {
if let Some(&cached) = cx.locate_cache.get(&(block, key_idx)) {
return cached;
}
let located = self.locate_value_uncached(func, cx, block, key_idx);
cx.locate_cache.insert((block, key_idx), located);
located
}
fn locate_value_uncached(
&self,
func: &Function,
cx: &mut CandidateCx<'_>,
block: BlockId,
key_idx: usize,
) -> Option<ValueId> {
let key = cx.analysis.keys[key_idx];
for &inst_id in func.blocks[block].instructions.iter().rev() {
if let Some((gen_key, source)) = Self::gen_key_value(func, inst_id)
&& gen_key == key
{
return match source {
GenSource::LoadResult => cx.analysis.inst_results.get(&inst_id).copied(),
GenSource::Stored(value) => Some(value),
};
}
if Self::inst_kills_key(func, inst_id, key) {
return None;
}
}
if !cx.analysis.ins.get(&block).is_some_and(|in_set| in_set.contains(key_idx)) {
return None;
}
let idom = cx.analysis.dominators.idom(block)?;
if idom == block {
return None;
}
if cx.analysis.path_kills_key(idom, block, key_idx) {
return None;
}
self.locate_value(func, cx, idom, key_idx)
}
fn apply_candidate(
&mut self,
func: &mut Function,
candidate: Candidate,
eliminated_keys: &mut FxHashSet<(LoadKey, BlockId)>,
inserted_insts: &mut FxHashSet<InstId>,
) {
let Candidate { target, key, result_ty, kind, metadata, loads, mut incoming, insertions } =
candidate;
eliminated_keys.insert((key, target));
let fully_available = insertions.is_empty();
for block in insertions {
let new_inst = func.alloc_inst(Instruction {
kind: kind.clone(),
result_ty: Some(result_ty),
metadata: metadata.clone(),
});
let value = func.alloc_value(Value::Inst(new_inst));
func.blocks[block].instructions.push(new_inst);
incoming.push((block, value));
inserted_insts.insert(new_inst);
self.stats.loads_inserted += 1;
}
incoming.sort_by_key(|(block, _)| block.index());
let first_result = loads[0].1;
let replacement = match incoming.first() {
Some(&(_, first))
if fully_available
&& first != first_result
&& incoming.iter().all(|&(_, value)| value == first) =>
{
first
}
_ => {
let phi_inst =
func.alloc_inst(Instruction::new(InstKind::Phi(incoming), Some(result_ty)));
let phi_value = func.alloc_value(Value::Inst(phi_inst));
let phi_count = func.blocks[target]
.instructions
.iter()
.take_while(|&&inst_id| {
matches!(func.instructions[inst_id].kind, InstKind::Phi(_))
})
.count();
func.blocks[target].instructions.insert(phi_count, phi_inst);
phi_value
}
};
for &(_, load_result) in &loads {
Self::replace_uses(func, load_result, replacement);
}
let load_insts: FxHashSet<InstId> = loads.iter().map(|&(inst_id, _)| inst_id).collect();
func.blocks[target].instructions.retain(|&inst_id| !load_insts.contains(&inst_id));
self.stats.loads_eliminated += loads.len();
}
fn gen_key_value(func: &Function, inst_id: InstId) -> Option<(LoadKey, GenSource)> {
match func.instructions[inst_id].kind {
InstKind::SLoad(slot) => {
Some((LoadKey::Storage(func.storage_alias(inst_id, slot)), GenSource::LoadResult))
}
InstKind::SStore(slot, value) => Some((
LoadKey::Storage(func.storage_alias(inst_id, slot)),
GenSource::Stored(value),
)),
InstKind::TLoad(slot) => {
Some((LoadKey::Transient(func.storage_alias(inst_id, slot)), GenSource::LoadResult))
}
InstKind::TStore(slot, value) => Some((
LoadKey::Transient(func.storage_alias(inst_id, slot)),
GenSource::Stored(value),
)),
InstKind::MLoad(addr) => Self::mem_addr(func, inst_id, addr)
.map(|addr| (LoadKey::Memory(addr), GenSource::LoadResult)),
InstKind::MStore(addr, value) => Self::mem_addr(func, inst_id, addr)
.map(|addr| (LoadKey::Memory(addr), GenSource::Stored(value))),
InstKind::Keccak256(offset, size) => {
let addr = Self::mem_addr(func, inst_id, offset)?;
let size = match func.value_u64(size) {
Some(size) => KeccakSize::Const(size),
None => KeccakSize::Dyn(size),
};
Some((LoadKey::Keccak(addr, size), GenSource::LoadResult))
}
_ => None,
}
}
fn inst_kills_key(func: &Function, inst_id: InstId, key: LoadKey) -> bool {
let kind = &func.instructions[inst_id].kind;
match key {
LoadKey::Storage(alias) => match *kind {
InstKind::SStore(slot, _) => func.storage_alias(inst_id, slot).may_alias(alias),
_ => kind.may_mutate_storage(),
},
LoadKey::Transient(alias) => match *kind {
InstKind::TStore(slot, _) => func.storage_alias(inst_id, slot).may_alias(alias),
_ => kind.may_mutate_transient_storage(),
},
LoadKey::Memory(addr) => Self::memory_write_clobbers(func, inst_id, addr, Some(32)),
LoadKey::Keccak(addr, size) => {
let size = match size {
KeccakSize::Const(size) => Some(size),
KeccakSize::Dyn(_) => None,
};
Self::memory_write_clobbers(func, inst_id, addr, size)
}
}
}
fn memory_write_clobbers(
func: &Function,
inst_id: InstId,
read: MemAddr,
read_size: Option<u64>,
) -> bool {
let kind = &func.instructions[inst_id].kind;
let (dest, write_size) = match *kind {
InstKind::MStore(dest, _) => (dest, Some(32)),
InstKind::MStore8(dest, _) => (dest, Some(1)),
InstKind::MCopy(dest, _, size)
| InstKind::CalldataCopy(dest, _, size)
| InstKind::CodeCopy(dest, _, size)
| InstKind::ReturnDataCopy(dest, _, size) => (dest, func.value_u64(size)),
InstKind::ExtCodeCopy(_, dest, _, size) => (dest, func.value_u64(size)),
_ => return kind.may_mutate_memory(),
};
let write_region = func.instructions[inst_id]
.metadata
.memory_region()
.unwrap_or_else(|| func.memory_region_for_addr(dest));
if read.region != MemoryRegion::Unknown
&& write_region != MemoryRegion::Unknown
&& read.region != write_region
{
return false;
}
let (write_base, write_offset) = Self::memory_addr_base_offset(func, dest);
if read.base != write_base {
return true;
}
let (Some(read_size), Some(write_offset), Some(write_size)) =
(read_size, write_offset, write_size)
else {
return true;
};
mir_utils::ranges_overlap(read.offset, read_size, write_offset, write_size)
}
fn mem_addr(func: &Function, inst_id: InstId, addr: ValueId) -> Option<MemAddr> {
let region = func.instructions[inst_id]
.metadata
.memory_region()
.unwrap_or_else(|| func.memory_region_for_addr(addr));
let (base, offset) = Self::memory_addr_base_offset(func, addr);
Some(MemAddr { region, base, offset: offset? })
}
fn memory_addr_base_offset(func: &Function, addr: ValueId) -> (Option<ValueId>, Option<u64>) {
if let Some((base, offset)) = Self::offset_chain(func, addr, 0) {
if let Some(offset) = mir_utils::u256_to_u64(offset) {
return (Some(base), Some(offset));
}
return (Some(addr), Some(0));
}
match func.value(addr) {
Value::Immediate(imm) => (None, imm.as_u256().and_then(mir_utils::u256_to_u64)),
Value::Arg { .. } | Value::Inst(_) | Value::Undef(_) | Value::Error(_) => {
(Some(addr), Some(0))
}
}
}
fn offset_chain(func: &Function, value: ValueId, depth: usize) -> Option<(ValueId, U256)> {
if depth >= 4 {
return None;
}
match func.value(value) {
Value::Immediate(_) => None,
Value::Arg { .. } | Value::Undef(_) | Value::Error(_) => Some((value, U256::ZERO)),
Value::Inst(inst_id) => match func.instructions[*inst_id].kind {
InstKind::Add(a, b) => {
if let Some(offset) = func.value_u256(b) {
let (base, existing) = Self::offset_chain(func, a, depth + 1)?;
Some((base, existing.wrapping_add(offset)))
} else if let Some(offset) = func.value_u256(a) {
let (base, existing) = Self::offset_chain(func, b, depth + 1)?;
Some((base, existing.wrapping_add(offset)))
} else {
Some((value, U256::ZERO))
}
}
InstKind::Sub(a, b) => {
let offset = func.value_u256(b)?;
let (base, existing) = Self::offset_chain(func, a, depth + 1)?;
Some((base, existing.wrapping_sub(offset)))
}
_ => Some((value, U256::ZERO)),
},
}
}
fn can_insert_on_edge(func: &Function, pred: BlockId, target: BlockId) -> bool {
matches!(func.blocks[pred].terminator, Some(Terminator::Jump(jump_target)) if jump_target == target)
}
fn operands_dominate_block(
func: &Function,
kind: &InstKind,
block: BlockId,
analysis: &Analysis,
) -> bool {
kind.operands().into_iter().all(|value| match func.value(value) {
Value::Immediate(_) | Value::Arg { .. } | Value::Undef(_) | Value::Error(_) => true,
Value::Inst(inst_id) => analysis
.inst_blocks
.get(inst_id)
.is_some_and(|def_block| analysis.dominators.dominates(*def_block, block)),
})
}
fn replace_uses(func: &mut Function, from: ValueId, to: ValueId) {
for inst in func.instructions.iter_mut() {
let mut changed = false;
inst.kind.visit_operands_mut(|value| {
if *value == from {
*value = to;
changed = true;
}
});
if !changed {
continue;
}
if mir_utils::is_memory_inst(&inst.kind) {
inst.metadata.set_memory_region(None);
}
if matches!(
inst.kind,
InstKind::SLoad(_)
| InstKind::SStore(_, _)
| InstKind::TLoad(_)
| InstKind::TStore(_, _)
) {
inst.metadata.set_storage_alias(None);
}
}
for block in func.blocks.iter_mut() {
if let Some(term) = &mut block.terminator {
Self::replace_terminator_uses(term, from, to);
}
}
}
fn replace_terminator_uses(term: &mut Terminator, from: ValueId, to: ValueId) {
let replace = |value: &mut ValueId| {
if *value == from {
*value = to;
}
};
match term {
Terminator::Jump(_) | Terminator::Stop | Terminator::Invalid => {}
Terminator::Branch { condition, .. } => replace(condition),
Terminator::Switch { value, cases, .. } => {
replace(value);
for (case, _) in cases {
replace(case);
}
}
Terminator::Return { values } => {
for value in values {
replace(value);
}
}
Terminator::Revert { offset, size } | Terminator::ReturnData { offset, size } => {
replace(offset);
replace(size);
}
Terminator::SelfDestruct { recipient } => replace(recipient),
}
}
}