use crate::{FxHashMap, FxHashSet};
use crate::{
cursor::{Cursor, CursorPosition, FuncCursor},
dominator_tree::DominatorTree,
flowgraph::ControlFlowGraph,
inst_predicates::{inst_addr_offset_type, inst_store_data, visit_block_succs},
ir::{
AliasRegion, Block, Endianness, Function, Inst, Opcode, Type, Value, immediates::Offset32,
},
post_dominator_tree::PostDominatorTree,
trace,
};
use core::cmp::Ordering;
use cranelift_entity::{EntityRef, SecondaryMap};
use smallvec::SmallVec;
fn has_memory_fence_semantics(op: Opcode) -> bool {
match op {
Opcode::AtomicRmw
| Opcode::AtomicCas
| Opcode::AtomicLoad
| Opcode::AtomicStore
| Opcode::Fence
| Opcode::Debugtrap
| Opcode::SequencePoint => true,
Opcode::Call | Opcode::CallIndirect | Opcode::TryCall | Opcode::TryCallIndirect => true,
_ => false,
}
}
enum AliasRegionsObserved {
All,
Just(AliasRegion),
Other,
None,
}
fn alias_regions_observed(func: &Function, inst: Inst, opcode: Opcode) -> AliasRegionsObserved {
debug_assert_eq!(func.dfg.insts[inst].opcode(), opcode);
if opcode.is_return()
|| opcode.is_call()
|| opcode.can_trap()
|| func.dfg.insts[inst]
.memflags_data(&func.dfg)
.and_then(|flags| flags.trap_code())
.is_some()
{
return AliasRegionsObserved::All;
}
if opcode.can_load() {
if let Some(region) = func.dfg.insts[inst].alias_region(&func.dfg) {
AliasRegionsObserved::Just(region)
} else {
AliasRegionsObserved::Other
}
} else {
AliasRegionsObserved::None
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Observer {
One(Inst),
Many,
}
impl Observer {
fn meet(a: Self, b: Self) -> Self {
match (a, b) {
(Observer::Many, _) | (_, Observer::Many) => Observer::Many,
(Observer::One(a), Observer::One(b)) => {
if a == b {
Observer::One(a)
} else {
Observer::Many
}
}
}
}
}
fn observe(
func: &Function,
observed_stores: &mut FxHashMap<Inst, Observer>,
slot: LastStore,
observer: Inst,
) {
let Some(last_store) = slot.inst() else {
return;
};
if func.dfg.insts[last_store].opcode().can_store() {
let entry = observed_stores
.entry(last_store)
.or_insert(Observer::One(observer));
*entry = Observer::meet(*entry, Observer::One(observer));
trace!(" observed_stores[{last_store:?}] = {entry:?}");
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
enum LastStore {
#[default]
NoStore,
Inst(Inst),
Unknown,
}
impl From<Inst> for LastStore {
#[inline]
fn from(inst: Inst) -> Self {
Self::Inst(inst)
}
}
impl LastStore {
fn meet(a: Self, b: Self) -> Self {
if a == b { a } else { LastStore::Unknown }
}
fn inst(self) -> Option<Inst> {
match self {
LastStore::Inst(i) => Some(i),
LastStore::NoStore | LastStore::Unknown => None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct LastStores {
regions: SecondaryMap<AliasRegion, LastStore>,
last_fence: LastStore,
}
enum InstEffect {
Fence,
Trap,
Store {
region: AliasRegion,
can_trap: bool,
},
Observes(AliasRegionsObserved),
}
fn classify(func: &Function, inst: Inst) -> InstEffect {
let opcode = func.dfg.insts[inst].opcode();
if has_memory_fence_semantics(opcode) {
return InstEffect::Fence;
}
if opcode.can_trap() {
return InstEffect::Trap;
}
if opcode.can_store() {
let Some(memflags) = func.dfg.insts[inst].memflags() else {
return InstEffect::Fence;
};
let Some(region) = func.dfg.mem_flags[memflags].alias_region() else {
return InstEffect::Fence;
};
return InstEffect::Store {
region,
can_trap: func.dfg.mem_flags[memflags].trap_code().is_some(),
};
}
InstEffect::Observes(alias_regions_observed(func, inst, opcode))
}
impl LastStores {
fn update(&mut self, func: &Function, inst: Inst) {
match classify(func, inst) {
InstEffect::Fence => {
self.regions.clear();
self.last_fence = inst.into();
}
InstEffect::Trap | InstEffect::Observes(_) => {}
InstEffect::Store {
region,
can_trap: _,
} => {
self.regions[region] = inst.into();
}
}
}
fn observe_inst(&self, func: &Function, inst: Inst, observed: &mut FxHashMap<Inst, Observer>) {
match classify(func, inst) {
InstEffect::Fence => {
for (_region, slot) in self.regions.iter() {
observe(func, observed, *slot, inst);
}
}
InstEffect::Trap => self.observe_others(func, observed, None, inst),
InstEffect::Store { region, can_trap } => {
observe(func, observed, self.regions[region], inst);
if can_trap {
self.observe_others(func, observed, Some(region), inst);
} else {
self.observe_trapping_others(func, observed, region, inst);
}
}
InstEffect::Observes(AliasRegionsObserved::All) => {
self.observe_others(func, observed, None, inst)
}
InstEffect::Observes(AliasRegionsObserved::Just(region)) => {
observe(func, observed, self.last_store_for_region(region), inst);
observe(func, observed, self.last_fence, inst);
}
InstEffect::Observes(AliasRegionsObserved::Other) => {
observe(func, observed, self.last_fence, inst)
}
InstEffect::Observes(AliasRegionsObserved::None) => {}
}
}
fn observe_others(
&self,
func: &Function,
observed_stores: &mut FxHashMap<Inst, Observer>,
excluding: Option<AliasRegion>,
observer: Inst,
) {
for (region, slot) in self.regions.iter() {
if excluding.is_none_or(|r| r != region) {
observe(func, observed_stores, *slot, observer);
}
}
observe(func, observed_stores, self.last_fence, observer);
}
fn observe_trapping_others(
&self,
func: &Function,
observed_stores: &mut FxHashMap<Inst, Observer>,
excluding: AliasRegion,
observer: Inst,
) {
let can_trap = |slot: LastStore| {
slot.inst()
.is_some_and(|s| func.dfg.insts[s].memflags_trap_code(&func.dfg).is_some())
};
for (region, slot) in self.regions.iter() {
if region != excluding && can_trap(*slot) {
observe(func, observed_stores, *slot, observer);
}
}
if can_trap(self.last_fence) {
observe(func, observed_stores, self.last_fence, observer);
}
}
fn last_store_for_region(&self, region: AliasRegion) -> LastStore {
match self.regions[region] {
LastStore::NoStore => self.last_fence,
slot => slot,
}
}
fn raw_region_slot(&self, func: &Function, inst: Inst) -> Option<LastStore> {
let region = func.dfg.insts[inst].alias_region(&func.dfg)?;
Some(self.regions[region])
}
fn undo_store(&mut self, func: &Function, dead: Inst, prev_region_slot: LastStore) {
debug_assert!(func.dfg.insts[dead].opcode().can_store());
let Some(region) = func.dfg.insts[dead].alias_region(&func.dfg) else {
return;
};
if self.regions[region] == dead.into() {
self.regions[region] = prev_region_slot;
}
}
fn get_last_store(&self, func: &Function, inst: Inst) -> LastStore {
if let Some(memflags) = func.dfg.insts[inst].memflags() {
return match func.dfg.mem_flags[memflags].alias_region() {
None => self.last_fence,
Some(region) => self.last_store_for_region(region),
};
}
let opcode = func.dfg.insts[inst].opcode();
if opcode.can_load() || opcode.can_store() {
inst.into()
} else {
LastStore::NoStore
}
}
fn meet_from(&mut self, rhs: &LastStores) -> bool {
let LastStores {
regions,
last_fence,
} = self;
let mut changed = false;
let mut meet = |a: &mut LastStore, b: LastStore| {
let new = LastStore::meet(*a, b);
changed |= new != *a;
*a = new;
};
let max_len = core::cmp::max(regions.keys().len(), rhs.regions.keys().len());
for i in 0..max_len {
let region = AliasRegion::new(i);
meet(&mut regions[region], rhs.regions[region]);
}
meet(last_fence, rhs.last_fence);
changed
}
}
#[derive(Clone, Debug)]
pub struct MemoryState {
stores: LastStores,
extent: u32,
#[cfg(debug_assertions)]
current_block: Block,
}
const NULL_EXTENT: u32 = 0;
impl MemoryState {
fn extent_for(&self, last_store: LastStore) -> u32 {
match last_store {
LastStore::NoStore | LastStore::Inst(_) => NULL_EXTENT,
LastStore::Unknown => {
debug_assert_ne!(self.extent, NULL_EXTENT);
self.extent
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct MemoryLoc {
last_store: LastStore,
extent: u32,
address: Value,
offset: Offset32,
ty: Type,
extending_opcode: Option<Opcode>,
endianness: Option<Endianness>,
}
#[derive(Clone, Copy, Debug)]
struct KnownValue {
value: Value,
def_inst: Inst,
prev_region_slot: Option<LastStore>,
}
pub enum OptResult {
None,
AliasedLoad(Value),
IdempotentStore,
DeadStore {
dead: Inst,
overwriter: Inst,
},
}
pub struct AliasAnalysis<'a> {
domtree: &'a DominatorTree,
post_dom_tree: Option<PostDominatorTree>,
observed_stores: FxHashMap<Inst, Observer>,
block_input: FxHashMap<Block, LastStores>,
mem_values: FxHashMap<MemoryLoc, KnownValue>,
extents: SmallVec<[u32; 8]>,
next_extent: u32,
#[cfg(debug_assertions)]
scope_blocks: SmallVec<[Block; 8]>,
}
impl<'a> AliasAnalysis<'a> {
pub fn new(func: &Function, domtree: &'a DominatorTree) -> AliasAnalysis<'a> {
trace!("alias analysis input is:\n{func:?}");
assert!(domtree.is_valid());
let mut analysis = AliasAnalysis {
domtree,
post_dom_tree: None,
observed_stores: FxHashMap::default(),
block_input: FxHashMap::default(),
mem_values: FxHashMap::default(),
extents: SmallVec::new(),
next_extent: 0,
#[cfg(debug_assertions)]
scope_blocks: SmallVec::new(),
};
analysis.compute_block_input_states(func);
analysis.verify_fixpoint(func);
analysis.compute_observed_stores(func);
analysis
}
fn post_dominates_maybe_dead_store(
&mut self,
func: &Function,
cfg: &ControlFlowGraph,
overwriter: Inst,
maybe_dead: Inst,
) -> bool {
let (Some(overwriter_block), Some(maybe_dead_block)) = (
func.layout.inst_block(overwriter),
func.layout.inst_block(maybe_dead),
) else {
return false;
};
if overwriter_block == maybe_dead_block {
return func.layout.pp_cmp(overwriter, maybe_dead) != Ordering::Less;
}
self.post_dom_tree
.get_or_insert_with(|| PostDominatorTree::with_cfg(cfg))
.post_dominates(overwriter, maybe_dead, &func.layout)
}
fn compute_block_input_states(&mut self, func: &Function) {
let mut queue = vec![];
let mut queue_set = FxHashSet::default();
let entry = func.layout.entry_block().unwrap();
queue.push(entry);
queue_set.insert(entry);
while let Some(block) = queue.pop() {
queue_set.remove(&block);
let mut state = self
.block_input
.entry(block)
.or_insert_with(|| LastStores::default())
.clone();
trace!("analyzing {block:?}");
trace!(" initial block state = {state:?}");
for inst in func.layout.block_insts(block) {
trace!(" analyzing {inst:?}: {}", func.dfg.display_inst(inst));
state.update(func, inst);
trace!(" updated state = {state:?}");
}
visit_block_succs(func, block, |_inst, succ, _from_table| {
let updated = match self.block_input.get_mut(&succ) {
Some(succ_state) => succ_state.meet_from(&state),
None => {
self.block_input.insert(succ, state.clone());
true
}
};
if updated && queue_set.insert(succ) {
queue.push(succ);
}
});
}
}
fn verify_fixpoint(&self, func: &Function) {
if !cfg!(debug_assertions) {
return;
}
let entry = func.layout.entry_block().unwrap();
let mut expected = FxHashMap::default();
expected.insert(entry, LastStores::default());
for (&block, input) in &self.block_input {
let mut state = input.clone();
for inst in func.layout.block_insts(block) {
state.update(func, inst);
}
visit_block_succs(func, block, |_inst, succ, _from_table| {
match expected.get_mut(&succ) {
Some(succ_state) => {
succ_state.meet_from(&state);
}
None => {
expected.insert(succ, state.clone());
}
}
});
}
for (&block, input) in &self.block_input {
assert_eq!(
expected.get(&block),
Some(input),
"last-store analysis did not reach a fixpoint at {block}",
);
}
assert_eq!(
expected.len(),
self.block_input.len(),
"last-store analysis and its verifier disagree on which blocks are reachable",
);
}
fn single_reachable_pred(&self, cfg: &ControlFlowGraph, block: Block) -> Option<Block> {
let mut preds = cfg
.pred_iter(block)
.map(|pred| pred.block)
.filter(|pred| self.block_input.contains_key(pred));
let first = preds.next()?;
preds.all(|pred| pred == first).then_some(first)
}
fn compute_observed_stores(&mut self, func: &Function) {
let mut observed_stores = FxHashMap::default();
for block in func.layout.blocks() {
if !self.block_input.contains_key(&block) {
continue;
}
let mut state = self.block_input_stores(block);
for inst in func.layout.block_insts(block) {
state.observe_inst(func, inst, &mut observed_stores);
state.update(func, inst);
}
visit_block_succs(func, block, |_inst, succ, _from_table| {
let succ_input = self
.block_input
.get(&succ)
.expect("successors of a reachable block are reachable");
let observer = func.layout.block_insts(succ).next().unwrap();
let max_len =
core::cmp::max(state.regions.keys().len(), succ_input.regions.keys().len());
for i in 0..max_len {
let region = AliasRegion::new(i);
if succ_input.regions[region] == LastStore::Unknown {
observe(func, &mut observed_stores, state.regions[region], observer);
}
}
if succ_input.last_fence == LastStore::Unknown {
observe(func, &mut observed_stores, state.last_fence, observer);
}
});
}
trace!("final observed_stores = {observed_stores:#?}");
self.observed_stores = observed_stores;
}
fn block_input_stores(&self, block: Block) -> LastStores {
match self.block_input.get(&block) {
Some(input) => input.clone(),
None => LastStores::default(),
}
}
pub fn push_scope(&mut self, cfg: &ControlFlowGraph, block: Block) -> MemoryState {
debug_assert!(cfg.is_valid());
#[cfg(debug_assertions)]
{
debug_assert_eq!(
self.domtree.idom(block),
self.scope_blocks.last().copied(),
"`push_scope` must be called in dominator-tree pre-order",
);
self.scope_blocks.push(block);
}
let extent = match self.extents.last() {
Some(parent) if self.single_reachable_pred(cfg, block).is_some() => *parent,
_ => {
self.next_extent += 1;
self.next_extent
}
};
self.extents.push(extent);
MemoryState {
stores: self.block_input_stores(block),
extent,
#[cfg(debug_assertions)]
current_block: block,
}
}
pub fn pop_scope(&mut self) {
#[cfg(debug_assertions)]
self.scope_blocks
.pop()
.expect("`pop_scope` without a matching `push_scope`");
self.extents
.pop()
.expect("`pop_scope` without a matching `push_scope`");
}
pub fn process_inst(
&mut self,
func: &mut Function,
cfg: &ControlFlowGraph,
state: &mut MemoryState,
inst: Inst,
) -> OptResult {
trace!(
"process_inst: {inst}: {}\n\twith last stores: {state:?}",
func.dfg.display_inst(inst),
);
#[cfg(debug_assertions)]
debug_assert_eq!(func.layout.inst_block(inst), Some(state.current_block));
let result = if let Some((address, offset, ty)) = inst_addr_offset_type(func, inst) {
let address = func.dfg.resolve_aliases(address);
let opcode = func.dfg.insts[inst].opcode();
if opcode.can_store() {
let store_data = inst_store_data(func, inst).unwrap();
let store_data = func.dfg.resolve_aliases(store_data);
let last_store = state.stores.get_last_store(func, inst);
if let LastStore::Inst(last_store) = last_store {
if self.observed_stores.get(&last_store).is_none_or(|o| *o == Observer::One(inst))
&& inst != last_store
&& !func.dfg.insts[inst].opcode().can_load()
&& !has_memory_fence_semantics(func.dfg.insts[last_store].opcode())
&& fully_overwrites(func, last_store, inst, address, offset, ty)
&& self.post_dominates_maybe_dead_store(func, cfg, inst, last_store)
{
trace!(
" --> discovered dead store at {last_store}: {}",
func.dfg.display_inst(last_store)
);
let dead_slot = LastStore::Inst(last_store);
let dead_loc = MemoryLoc {
last_store: dead_slot,
extent: state.extent_for(dead_slot),
address,
offset,
ty,
extending_opcode: get_ext_opcode(opcode),
endianness: get_endianness(func, last_store),
};
let dead_entry = self.mem_values.remove(&dead_loc);
if let Some(prev) = dead_entry
.filter(|e| self.domtree.dominates(e.def_inst, inst, &func.layout))
.and_then(|e| e.prev_region_slot)
{
state.stores.undo_store(func, last_store, prev);
}
return OptResult::DeadStore {
dead: last_store,
overwriter: inst,
};
}
}
let check_loc = MemoryLoc {
last_store,
extent: state.extent_for(last_store),
address,
offset,
ty,
extending_opcode: get_ext_opcode(opcode),
endianness: get_endianness(func, inst),
};
if let Some(KnownValue {
def_inst,
value: known_value,
..
}) = self.mem_values.get(&check_loc).cloned()
{
if known_value == store_data
&& self.domtree.dominates(def_inst, inst, &func.layout)
{
trace!(" --> idempotent store of {store_data} to loc {check_loc:?}");
if let LastStore::Inst(last_store) = last_store {
if let Some(observer) = self.observed_stores.get(&inst).copied() {
let entry =
self.observed_stores.entry(last_store).or_insert(observer);
*entry = Observer::meet(*entry, observer);
trace!(" observed_stores[{last_store:?}] = {entry:?}");
}
}
return OptResult::IdempotentStore;
}
}
let this_store = LastStore::Inst(inst);
let mem_loc = MemoryLoc {
last_store: this_store,
extent: state.extent_for(this_store),
address,
offset,
ty,
extending_opcode: get_ext_opcode(opcode),
endianness: get_endianness(func, inst),
};
trace!(" --> updating known values in memory: {mem_loc:?} = {store_data}");
self.mem_values.insert(
mem_loc,
KnownValue {
def_inst: inst,
value: store_data,
prev_region_slot: state.stores.raw_region_slot(func, inst),
},
);
OptResult::None
} else if opcode.can_load() {
let last_store = state.stores.get_last_store(func, inst);
let load_result = func.dfg.inst_results(inst)[0];
let mem_loc = MemoryLoc {
last_store,
extent: state.extent_for(last_store),
address,
offset,
ty,
extending_opcode: get_ext_opcode(opcode),
endianness: get_endianness(func, inst),
};
trace!(" load with last_store at loc {mem_loc:?}");
let aliased = if let Some(KnownValue {
def_inst, value, ..
}) = self.mem_values.get(&mem_loc).cloned()
{
trace!(" see known value {value} from {def_inst}");
if self.domtree.dominates(def_inst, inst, &func.layout) {
trace!(
" --> dominates; inserting value equivalence from {load_result} to {value}",
);
Some(value)
} else {
None
}
} else {
None
};
if aliased.is_none() {
trace!(" --> inserting load result {load_result} at loc {mem_loc:?}");
self.mem_values.insert(
mem_loc,
KnownValue {
def_inst: inst,
value: load_result,
prev_region_slot: None,
},
);
}
match aliased {
Some(value) => {
return OptResult::AliasedLoad(value);
}
None => OptResult::None,
}
} else {
OptResult::None
}
} else {
OptResult::None
};
#[cfg(debug_assertions)]
{
let mut new_observations = FxHashMap::default();
state.stores.observe_inst(func, inst, &mut new_observations);
for observed in new_observations.keys() {
debug_assert!(
self.observed_stores.contains_key(observed),
"`compute_observed_stores` should have already found all observed \
stores, but processing {inst} found that {observed} is observed",
);
}
}
state.stores.update(func, inst);
result
}
pub fn compute_and_update_aliases(&mut self, func: &mut Function, cfg: &ControlFlowGraph) {
let domtree = self.domtree;
let mut pos = FuncCursor::new(func);
let Some(entry) = pos.func.layout.entry_block() else {
return;
};
enum BlockStackEntry {
Visit(Block),
Pop,
}
let mut stack = vec![BlockStackEntry::Visit(entry)];
while let Some(top) = stack.pop() {
let block = match top {
BlockStackEntry::Pop => {
self.pop_scope();
continue;
}
BlockStackEntry::Visit(block) => block,
};
stack.push(BlockStackEntry::Pop);
let mut state = self.push_scope(cfg, block);
pos.goto_top(block);
while let Some(inst) = pos.next_inst() {
match self.process_inst(pos.func, cfg, &mut state, inst) {
OptResult::None => {}
OptResult::AliasedLoad(replaced_result) => {
let result = pos.func.dfg.inst_results(inst)[0];
pos.func.dfg.clear_results(inst);
pos.func.dfg.change_to_alias(result, replaced_result);
pos.remove_inst_and_step_back();
}
OptResult::IdempotentStore => {
pos.remove_inst_and_step_back();
}
OptResult::DeadStore {
dead,
overwriter: _,
} => {
assert!(
!matches!(pos.position(), CursorPosition::At(other) if dead == other)
);
pos.func.layout.remove_inst(dead);
pos.prev_inst();
}
}
}
let children = SmallVec::<[Block; 8]>::from_iter(domtree.children(block));
stack.extend(children.into_iter().rev().map(BlockStackEntry::Visit));
}
}
}
fn get_endianness(func: &Function, inst: Inst) -> Option<Endianness> {
func.dfg.insts[inst]
.memflags_data(&func.dfg)
.and_then(|flags| flags.explicit_endianness())
}
fn get_ext_opcode(op: Opcode) -> Option<Opcode> {
debug_assert!(op.can_load() || op.can_store());
match op {
Opcode::Load | Opcode::Store => None,
_ => Some(op),
}
}
fn fully_overwrites(
func: &Function,
maybe_dead: Inst,
overwriter: Inst,
overwriter_addr: Value,
overwriter_offset: Offset32,
overwriter_ty: Type,
) -> bool {
debug_assert!(!func.dfg.value_is_alias(overwriter_addr));
let maybe_dead_opcode = func.dfg.insts[maybe_dead].opcode();
if !maybe_dead_opcode.can_store() {
return false;
}
let overwriter_opcode = func.dfg.insts[overwriter].opcode();
if get_ext_opcode(maybe_dead_opcode) != get_ext_opcode(overwriter_opcode) {
return false;
}
if func.dfg.insts[maybe_dead].alias_region(&func.dfg)
!= func.dfg.insts[overwriter].alias_region(&func.dfg)
{
return false;
}
if func.dfg.insts[maybe_dead]
.memflags()
.and_then(|f| func.dfg.mem_flags[f].trap_code())
!= func.dfg.insts[overwriter]
.memflags()
.and_then(|f| func.dfg.mem_flags[f].trap_code())
{
return false;
}
match inst_addr_offset_type(func, maybe_dead) {
Some((addr, offset, ty)) => {
func.dfg.resolve_aliases(addr) == overwriter_addr
&& offset == overwriter_offset
&& ty == overwriter_ty
}
None => false,
}
}