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, packed_option::PackedOption};
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
}
}
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LastStores {
regions: SecondaryMap<AliasRegion, PackedOption<Inst>>,
last_fence: PackedOption<Inst>,
}
fn observe(
func: &Function,
observed_stores: &mut FxHashMap<Inst, Observer>,
last_store: PackedOption<Inst>,
observer: Inst,
) {
if let Some(last_store) = last_store.expand() {
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:?}");
}
}
}
impl LastStores {
pub(crate) fn update(
&mut self,
func: &Function,
inst: Inst,
observed_stores: &mut FxHashMap<Inst, Observer>,
) {
let opcode = func.dfg.insts[inst].opcode();
if has_memory_fence_semantics(opcode) {
self.fence(func, inst, observed_stores);
}
else if opcode.can_trap() {
self.observe_others(func, observed_stores, None, inst);
}
else if opcode.can_store() {
if let Some(memflags) = func.dfg.insts[inst].memflags() {
match func.dfg.mem_flags[memflags].alias_region() {
Some(region) => {
observe(func, observed_stores, self.regions[region], inst);
self.regions[region] = inst.into();
if func.dfg.mem_flags[memflags].trap_code().is_some() {
self.observe_others(func, observed_stores, Some(region), inst);
} else {
self.observe_trapping_others(func, observed_stores, region, inst);
}
}
None => {
self.fence(func, inst, observed_stores);
}
}
} else {
self.fence(func, inst, observed_stores);
}
}
else {
match alias_regions_observed(func, inst, opcode) {
AliasRegionsObserved::All => self.observe_others(func, observed_stores, None, inst),
AliasRegionsObserved::Just(region) => {
observe(
func,
observed_stores,
self.last_store_for_region(region),
inst,
);
observe(func, observed_stores, self.last_fence, inst);
}
AliasRegionsObserved::Other => {
observe(func, observed_stores, self.last_fence, inst)
}
AliasRegionsObserved::None => {}
}
}
}
fn observe_others(
&self,
func: &Function,
observed_stores: &mut FxHashMap<Inst, Observer>,
excluding: Option<AliasRegion>,
observer: Inst,
) {
for (region, last_store) in self.regions.iter() {
if excluding.is_none_or(|r| r != region) {
observe(func, observed_stores, *last_store, 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 = |last_store: PackedOption<Inst>| {
last_store
.expand()
.is_some_and(|s| func.dfg.insts[s].memflags_trap_code(&func.dfg).is_some())
};
for (region, last_store) in self.regions.iter() {
if region != excluding && can_trap(*last_store) {
observe(func, observed_stores, *last_store, observer);
}
}
if can_trap(self.last_fence) {
observe(func, observed_stores, self.last_fence, observer);
}
}
fn fence(
&mut self,
func: &Function,
inst: Inst,
observed_stores: &mut FxHashMap<Inst, Observer>,
) {
for (_region, last_store) in self.regions.iter() {
observe(func, observed_stores, *last_store, inst);
}
self.regions.clear();
self.last_fence = inst.into();
}
fn last_store_for_region(&self, region: AliasRegion) -> PackedOption<Inst> {
if self.regions[region].is_some() {
self.regions[region]
} else {
self.last_fence
}
}
fn raw_region_slot(&self, func: &Function, inst: Inst) -> Option<PackedOption<Inst>> {
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: PackedOption<Inst>) {
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].expand() == Some(dead) {
self.regions[region] = prev_region_slot;
}
}
fn get_last_store(&self, func: &Function, inst: Inst) -> PackedOption<Inst> {
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 {
None.into()
}
}
fn meet_from(
&mut self,
func: &Function,
rhs: &LastStores,
loc: Inst,
observed_stores: &mut FxHashMap<Inst, Observer>,
) -> bool {
let LastStores {
regions,
last_fence,
} = self;
let meet = |observed_stores: &mut FxHashMap<Inst, Observer>,
a: &mut PackedOption<Inst>,
b: PackedOption<Inst>|
-> bool {
let old = a.expand();
let new = match (old, b.expand()) {
(None, None) => None,
(Some(a), Some(b)) if a == b => Some(a),
(x, y) => {
observe(func, observed_stores, x.filter(|x| *x != loc).into(), loc);
observe(func, observed_stores, y.filter(|y| *y != loc).into(), loc);
Some(loc)
}
};
*a = new.into();
old != new
};
let mut changed = false;
let max_len = core::cmp::max(regions.keys().len(), rhs.regions.keys().len());
for i in 0..max_len {
let region = AliasRegion::new(i);
changed |= meet(observed_stores, &mut regions[region], rhs.regions[region]);
}
changed |= meet(observed_stores, last_fence, rhs.last_fence);
changed
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct MemoryLoc {
last_store: PackedOption<Inst>,
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<PackedOption<Inst>>,
}
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>,
}
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(),
};
analysis.compute_block_input_states(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, &mut self.observed_stores);
trace!(" updated state = {state:?}");
}
visit_block_succs(func, block, |_inst, succ, _from_table| {
let succ_first_inst = func.layout.block_insts(succ).next().unwrap();
let updated = match self.block_input.get_mut(&succ) {
Some(succ_state) => succ_state.meet_from(
func,
&state,
succ_first_inst,
&mut self.observed_stores,
),
None => {
self.block_input.insert(succ, state.clone());
true
}
};
if updated && queue_set.insert(succ) {
queue.push(succ);
}
});
}
trace!("final observed_stores = {:#?}", self.observed_stores);
}
pub fn block_starting_state(&self, block: Block) -> LastStores {
self.block_input
.get(&block)
.cloned()
.unwrap_or_else(|| LastStores::default())
}
pub fn process_inst(
&mut self,
func: &mut Function,
cfg: &ControlFlowGraph,
state: &mut LastStores,
inst: Inst,
) -> OptResult {
trace!(
"process_inst: {inst}: {}\n\twith last stores: {state:?}\n\twith mem values = {:?}",
func.dfg.display_inst(inst),
self.mem_values,
);
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.get_last_store(func, inst);
if let Some(last_store) = last_store.expand() {
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_loc = MemoryLoc {
last_store: last_store.into(),
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.and_then(|e| e.prev_region_slot) {
state.undo_store(func, last_store, prev);
}
return OptResult::DeadStore {
dead: last_store,
overwriter: inst,
};
}
}
let check_loc = MemoryLoc {
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 Some(last_store) = last_store.expand() {
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 mem_loc = MemoryLoc {
last_store: inst.into(),
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.raw_region_slot(func, inst),
},
);
OptResult::None
} else if opcode.can_load() {
let last_store = state.get_last_store(func, inst);
let load_result = func.dfg.inst_results(inst)[0];
let mem_loc = MemoryLoc {
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
};
let observed_stores_len = self.observed_stores.len();
state.update(func, inst, &mut self.observed_stores);
debug_assert_eq!(
observed_stores_len,
self.observed_stores.len(),
"`compute_block_input_states` should have already found all observed stores, \
but processing {inst} found a new one",
);
result
}
pub fn compute_and_update_aliases(&mut self, func: &mut Function, cfg: &ControlFlowGraph) {
let mut pos = FuncCursor::new(func);
while let Some(block) = pos.next_block() {
let mut state = self.block_starting_state(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();
}
}
}
}
}
}
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,
}
}