use analyssa::{
PointerSize,
analysis::{
SsaCfg,
memory::{
AliasResult, ArrayIndex, IndirectLocation, MemoryDefSite, MemoryLocation, MemoryOp,
MemorySsa, MemorySsaStats, MemoryVersion, analyze_alias,
},
},
ir::{
block::SsaBlock,
function::SsaFunction,
instruction::SsaInstruction,
ops::{AtomicAccessWidth, AtomicOrdering, FenceKind, MemoryAccessSemantics, SsaOp},
value::ConstValue,
variable::{DefSite, SsaVarId, VariableOrigin},
},
testing::{MockTarget, MockType},
};
fn local(ssa: &mut SsaFunction<MockTarget>, idx: u16, block: usize, instr: usize) -> SsaVarId {
ssa.create_variable(
VariableOrigin::Local(idx),
0,
DefSite::instruction(block, instr),
MockType::I32,
)
}
fn instr(op: SsaOp<MockTarget>) -> SsaInstruction<MockTarget> {
SsaInstruction::synthetic(op)
}
#[test]
fn memory_location_equality_and_hash() {
let obj = SsaVarId::from_index(0);
let loc1 = MemoryLocation::<MockTarget>::InstanceField(obj, 1u32);
let loc2 = MemoryLocation::<MockTarget>::InstanceField(obj, 1u32);
let loc3 = MemoryLocation::<MockTarget>::InstanceField(obj, 2u32);
assert_eq!(loc1, loc2);
assert_ne!(loc1, loc3);
let static_loc = MemoryLocation::<MockTarget>::StaticField(1u32);
assert_ne!(loc1, static_loc);
}
#[test]
fn alias_analysis_same_location_is_must_alias() {
let obj = SsaVarId::from_index(0);
let loc1 = MemoryLocation::<MockTarget>::InstanceField(obj, 1u32);
let loc2 = MemoryLocation::<MockTarget>::InstanceField(obj, 1u32);
assert_eq!(analyze_alias(&loc1, &loc2), AliasResult::MustAlias);
}
#[test]
fn alias_analysis_different_fields_diff_object() {
let obj = SsaVarId::from_index(0);
let loc1 = MemoryLocation::<MockTarget>::InstanceField(obj, 1u32);
let loc2 = MemoryLocation::<MockTarget>::InstanceField(obj, 2u32);
let result = analyze_alias(&loc1, &loc2);
assert!(result != AliasResult::MustAlias);
let loc3 = MemoryLocation::<MockTarget>::InstanceField(SsaVarId::from_index(1), 1u32);
let _ = analyze_alias(&loc1, &loc3);
}
#[test]
fn alias_analysis_static_vs_instance() {
let static_loc = MemoryLocation::<MockTarget>::StaticField(42u32);
let instance_loc = MemoryLocation::<MockTarget>::InstanceField(SsaVarId::from_index(0), 42u32);
assert_eq!(
analyze_alias(&static_loc, &instance_loc),
AliasResult::NoAlias
);
}
#[test]
fn memory_location_array_and_indirect() {
let array_loc = MemoryLocation::<MockTarget>::ArrayElement(
SsaVarId::from_index(0),
ArrayIndex::Constant(0),
);
let array_loc2 =
MemoryLocation::<MockTarget>::ArrayElement(SsaVarId::from_index(0), ArrayIndex::Unknown);
assert_eq!(
analyze_alias(&array_loc, &array_loc2),
AliasResult::MayAlias
);
}
fn indirect(base: usize, offset_bits: i64, size_bits: Option<u32>) -> MemoryLocation<MockTarget> {
MemoryLocation::Indirect(IndirectLocation::new(
SsaVarId::from_index(base),
None,
0,
offset_bits,
size_bits,
None,
PointerSize::Bit64,
))
}
fn indirect_indexed(
base: usize,
index: usize,
stride_bytes: u64,
offset_bits: i64,
size_bits: Option<u32>,
) -> MemoryLocation<MockTarget> {
MemoryLocation::Indirect(IndirectLocation::new(
SsaVarId::from_index(base),
Some(SsaVarId::from_index(index)),
stride_bytes,
offset_bits,
size_bits,
None,
PointerSize::Bit64,
))
}
#[test]
fn memory_location_indirect_and_unknown() {
let indirect = indirect(1, 0, Some(32));
let unknown = MemoryLocation::<MockTarget>::Unknown;
assert_eq!(analyze_alias(&indirect, &unknown), AliasResult::MayAlias);
assert_eq!(analyze_alias(&unknown, &indirect), AliasResult::MayAlias);
}
#[test]
fn indirect_disjoint_offsets_do_not_alias() {
let low = indirect(1, 0, Some(32));
let high = indirect(1, 32, Some(32));
assert_eq!(analyze_alias(&low, &high), AliasResult::NoAlias);
assert_eq!(analyze_alias(&high, &low), AliasResult::NoAlias);
}
#[test]
fn indirect_overlapping_offsets_may_alias() {
let first = indirect(1, 0, Some(32));
let straddling = indirect(1, 16, Some(32));
assert_eq!(analyze_alias(&first, &straddling), AliasResult::MayAlias);
assert_eq!(analyze_alias(&straddling, &first), AliasResult::MayAlias);
}
#[test]
fn indirect_identical_cells_must_alias() {
let cell = indirect(1, 64, Some(32));
assert_eq!(analyze_alias(&cell, &cell.clone()), AliasResult::MustAlias);
}
#[test]
fn indirect_distinct_bases_may_alias() {
let from_one = indirect(1, 0, Some(32));
let from_two = indirect(2, 0, Some(32));
assert_eq!(analyze_alias(&from_one, &from_two), AliasResult::MayAlias);
}
#[test]
fn indirect_unknown_width_never_must_aliases() {
let unsized_cell = indirect(1, 0, None);
let sized_far_away = indirect(1, 4096, Some(32));
assert_eq!(
analyze_alias(&unsized_cell, &unsized_cell.clone()),
AliasResult::MayAlias,
"an unknown extent cannot prove one cell"
);
assert_eq!(
analyze_alias(&unsized_cell, &sized_far_away),
AliasResult::MayAlias,
"an unknown extent may reach any offset off the same base"
);
}
#[test]
fn distinct_address_spaces_never_alias() {
let flat = MemoryLocation::<MockTarget>::Indirect(IndirectLocation::new(
SsaVarId::from_index(1),
None,
0,
0x30 * 8,
Some(32),
None,
PointerSize::Bit64,
));
let segmented = MemoryLocation::<MockTarget>::Indirect(IndirectLocation::new(
SsaVarId::from_index(1),
None,
0,
0x30 * 8,
Some(32),
Some(1),
PointerSize::Bit64,
));
assert_eq!(
analyze_alias(&flat, &segmented),
AliasResult::NoAlias,
"a segmented access and a flat one name different memory"
);
assert_eq!(
analyze_alias(&segmented, &segmented.clone()),
AliasResult::MustAlias
);
}
#[test]
fn indirect_scaled_index_gates_offset_reasoning() {
let elem_i = indirect_indexed(1, 10, 8, 0, Some(32));
let elem_j = indirect_indexed(1, 11, 8, 0, Some(32));
assert_eq!(
analyze_alias(&elem_i, &elem_j),
AliasResult::MayAlias,
"distinct index values may hold the same number"
);
let elem_i_field0 = indirect_indexed(1, 10, 8, 0, Some(32));
let elem_i_field1 = indirect_indexed(1, 10, 8, 32, Some(32));
assert_eq!(
analyze_alias(&elem_i_field0, &elem_i_field1),
AliasResult::NoAlias,
"one index term cancels, so the constant offsets decide"
);
assert_eq!(
analyze_alias(&elem_i_field0, &elem_i_field0.clone()),
AliasResult::MustAlias
);
let no_index = indirect(1, 0, Some(32));
assert_eq!(
analyze_alias(&elem_i, &no_index),
AliasResult::MayAlias,
"an index that could be zero defeats offset reasoning"
);
let elem_i_stride4 = indirect_indexed(1, 10, 4, 0, Some(32));
assert_eq!(
analyze_alias(&elem_i, &elem_i_stride4),
AliasResult::MayAlias,
"differing strides are not comparable"
);
}
#[test]
fn memory_location_debug_output() {
let loc = MemoryLocation::<MockTarget>::InstanceField(SsaVarId::from_index(5), 3u32);
let debug_str = format!("{loc:?}");
assert!(!debug_str.is_empty());
}
#[test]
fn alias_result_equality() {
assert_eq!(AliasResult::MustAlias, AliasResult::MustAlias);
assert_ne!(AliasResult::MustAlias, AliasResult::NoAlias);
assert_ne!(AliasResult::MayAlias, AliasResult::NoAlias);
}
#[test]
fn memory_ssa_empty_function_stats() {
let ssa = SsaFunction::<MockTarget>::new(0, 0);
let cfg = SsaCfg::from_ssa(&ssa);
let mem_ssa = MemorySsa::<MockTarget>::build(&ssa, &cfg, PointerSize::Bit64);
let stats: MemorySsaStats = mem_ssa.stats();
assert_eq!(stats.store_count, 0);
assert_eq!(stats.load_count, 0);
assert_eq!(stats.location_count, 0);
}
#[test]
fn memory_ssa_with_field_loads_and_stores() {
let mut ssa = SsaFunction::new(0, 2);
let obj = local(&mut ssa, 0, 0, 0);
let val = local(&mut ssa, 1, 0, 1);
let loaded = local(&mut ssa, 2, 0, 2);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::Const {
dest: obj,
value: ConstValue::I32(42),
}));
b0.add_instruction(instr(SsaOp::Const {
dest: val,
value: ConstValue::I32(100),
}));
b0.add_instruction(instr(SsaOp::StoreField {
object: obj,
field: 1u32,
value: val,
}));
b0.add_instruction(instr(SsaOp::LoadField {
dest: loaded,
object: obj,
field: 1u32,
}));
b0.add_instruction(instr(SsaOp::Return {
value: Some(loaded),
}));
ssa.add_block(b0);
ssa.recompute_uses();
let cfg = SsaCfg::from_ssa(&ssa);
let mem_ssa = MemorySsa::<MockTarget>::build(&ssa, &cfg, PointerSize::Bit64);
let stats: MemorySsaStats = mem_ssa.stats();
assert_eq!(stats.store_count, 1);
assert_eq!(stats.load_count, 1);
}
#[test]
fn memory_ssa_new_is_empty() {
let mem_ssa: MemorySsa<MockTarget> = MemorySsa::new();
let stats = mem_ssa.stats();
assert_eq!(stats.store_count, 0);
assert_eq!(stats.load_count, 0);
assert_eq!(stats.memory_phi_count, 0);
assert_eq!(stats.version_count, 0);
}
#[test]
fn memory_ssa_classifies_atomic_and_fence_effects() {
let mut ssa = SsaFunction::new(0, 1);
let addr = local(&mut ssa, 0, 0, 0);
let value = local(&mut ssa, 1, 0, 1);
let old = local(&mut ssa, 2, 0, 2);
let mut block = SsaBlock::new(0);
block.add_instruction(instr(SsaOp::Const {
dest: addr,
value: ConstValue::I32(0),
}));
block.add_instruction(instr(SsaOp::Const {
dest: value,
value: ConstValue::I32(1),
}));
block.add_instruction(instr(SsaOp::AtomicExchange {
dest: old,
addr,
value,
ordering: AtomicOrdering::AcqRel,
width: AtomicAccessWidth::Bits32,
volatile: true,
}));
block.add_instruction(instr(SsaOp::Fence {
kind: FenceKind::Acquire,
}));
block.add_instruction(instr(SsaOp::Return { value: Some(old) }));
ssa.add_block(block);
ssa.recompute_uses();
let cfg = SsaCfg::from_ssa(&ssa);
let mem_ssa = MemorySsa::<MockTarget>::build(&ssa, &cfg, PointerSize::Bit64);
let stats = mem_ssa.stats();
assert_eq!(stats.store_count, 1);
assert_eq!(stats.barrier_count, 1);
assert!(mem_ssa.operations().iter().any(|op| {
op.effects()
.is_some_and(|effects| effects.memory_semantics == MemoryAccessSemantics::Atomic)
}));
}
#[test]
fn memory_ssa_handles_store_in_branch() {
let mut ssa = SsaFunction::new(0, 3);
let obj = local(&mut ssa, 0, 0, 0);
let val = local(&mut ssa, 1, 0, 1);
let cond = local(&mut ssa, 2, 0, 2);
let loaded = local(&mut ssa, 3, 2, 0);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::Const {
dest: obj,
value: ConstValue::I32(42),
}));
b0.add_instruction(instr(SsaOp::Const {
dest: val,
value: ConstValue::I32(100),
}));
b0.add_instruction(instr(SsaOp::Const {
dest: cond,
value: ConstValue::I32(1),
}));
b0.add_instruction(instr(SsaOp::Branch {
condition: cond,
true_target: 1,
false_target: 2,
}));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
b1.add_instruction(instr(SsaOp::StoreField {
object: obj,
field: 5u32,
value: val,
}));
b1.add_instruction(instr(SsaOp::Jump { target: 2 }));
ssa.add_block(b1);
let mut b2 = SsaBlock::new(2);
b2.add_instruction(instr(SsaOp::LoadField {
dest: loaded,
object: obj,
field: 5u32,
}));
b2.add_instruction(instr(SsaOp::Return {
value: Some(loaded),
}));
ssa.add_block(b2);
ssa.recompute_uses();
let cfg = SsaCfg::from_ssa(&ssa);
let mem_ssa = MemorySsa::<MockTarget>::build(&ssa, &cfg, PointerSize::Bit64);
let stats: MemorySsaStats = mem_ssa.stats();
assert!(
stats.store_count >= 1,
"expected at least 1 store, got {}",
stats.store_count
);
assert!(
stats.load_count >= 1,
"expected at least 1 load, got {}",
stats.load_count
);
assert!(
stats.location_count >= 1,
"expected at least 1 location, got {}",
stats.location_count
);
}
fn diamond_over_one_field(store_in_arm2: bool) -> (SsaFunction<MockTarget>, SsaVarId) {
let mut ssa = SsaFunction::new(0, 5);
let obj = local(&mut ssa, 0, 0, 0);
let val = local(&mut ssa, 1, 0, 1);
let cond = local(&mut ssa, 2, 0, 2);
let loaded = local(&mut ssa, 3, 3, 0);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::Const {
dest: obj,
value: ConstValue::I32(42),
}));
b0.add_instruction(instr(SsaOp::Const {
dest: val,
value: ConstValue::I32(100),
}));
b0.add_instruction(instr(SsaOp::Const {
dest: cond,
value: ConstValue::I32(1),
}));
b0.add_instruction(instr(SsaOp::Branch {
condition: cond,
true_target: 1,
false_target: 2,
}));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
b1.add_instruction(instr(SsaOp::StoreField {
object: obj,
field: 5u32,
value: val,
}));
b1.add_instruction(instr(SsaOp::Jump { target: 3 }));
ssa.add_block(b1);
let mut b2 = SsaBlock::new(2);
if store_in_arm2 {
b2.add_instruction(instr(SsaOp::StoreField {
object: obj,
field: 5u32,
value: val,
}));
}
b2.add_instruction(instr(SsaOp::Jump { target: 3 }));
ssa.add_block(b2);
let mut b3 = SsaBlock::new(3);
b3.add_instruction(instr(SsaOp::LoadField {
dest: loaded,
object: obj,
field: 5u32,
}));
b3.add_instruction(instr(SsaOp::Return {
value: Some(loaded),
}));
ssa.add_block(b3);
ssa.recompute_uses();
(ssa, obj)
}
#[test]
fn sibling_subtrees_do_not_leak_memory_versions() {
let (ssa, obj) = diamond_over_one_field(true);
let cfg = SsaCfg::from_ssa(&ssa);
let mem_ssa = MemorySsa::<MockTarget>::build(&ssa, &cfg, PointerSize::Bit64);
let loc = MemoryLocation::<MockTarget>::InstanceField(obj, 5u32);
let b0_exit = mem_ssa.version_at_exit(&loc, 0);
assert!(b0_exit.is_some(), "entry block has an exit version");
assert_eq!(
mem_ssa.version_at_entry(&loc, 1),
b0_exit,
"arm 1 must enter on its dominator's exit version"
);
assert_eq!(
mem_ssa.version_at_entry(&loc, 2),
b0_exit,
"arm 2 must enter on its dominator's exit version"
);
let b1_exit = mem_ssa.version_at_exit(&loc, 1);
let b2_exit = mem_ssa.version_at_exit(&loc, 2);
assert!(
b1_exit.is_some() && b2_exit.is_some(),
"both arms have exits"
);
assert_ne!(b1_exit, b0_exit, "arm 1's store defines a new version");
assert_ne!(b2_exit, b0_exit, "arm 2's store defines a new version");
assert_ne!(
b1_exit, b2_exit,
"the two arms' stores are distinct definitions"
);
}
#[test]
fn merge_phi_operands_track_each_predecessor_exit() {
let (ssa, obj) = diamond_over_one_field(true);
let cfg = SsaCfg::from_ssa(&ssa);
let mem_ssa = MemorySsa::<MockTarget>::build(&ssa, &cfg, PointerSize::Bit64);
let loc = MemoryLocation::<MockTarget>::InstanceField(obj, 5u32);
let phis = mem_ssa.memory_phis(3);
let found = phis.iter().find(|phi| phi.location == loc);
assert!(
found.is_some(),
"the merge block carries a memory phi for the stored location"
);
let Some(phi) = found else { return };
let from_b1 = phi.operand_from(1).map(|op| op.version);
let from_b2 = phi.operand_from(2).map(|op| op.version);
assert!(
from_b1.is_some() && from_b2.is_some(),
"the phi has an operand from each predecessor"
);
assert_eq!(
from_b1,
mem_ssa.version_at_exit(&loc, 1),
"the arm-1 operand is arm 1's exit version"
);
assert_eq!(
from_b2,
mem_ssa.version_at_exit(&loc, 2),
"the arm-2 operand is arm 2's exit version"
);
assert_ne!(
from_b1, from_b2,
"the two arms reach the merge with different versions"
);
}
#[test]
fn non_storing_arm_contributes_its_inherited_version() {
let (ssa, obj) = diamond_over_one_field(false);
let cfg = SsaCfg::from_ssa(&ssa);
let mem_ssa = MemorySsa::<MockTarget>::build(&ssa, &cfg, PointerSize::Bit64);
let loc = MemoryLocation::<MockTarget>::InstanceField(obj, 5u32);
let b0_exit = mem_ssa.version_at_exit(&loc, 0);
assert!(b0_exit.is_some(), "entry block has an exit version");
assert_eq!(
mem_ssa.version_at_entry(&loc, 2),
b0_exit,
"the pass-through arm inherits its dominator's version"
);
assert_eq!(
mem_ssa.version_at_exit(&loc, 2),
b0_exit,
"the pass-through arm defines no memory, so it leaves as it entered"
);
let found = mem_ssa
.memory_phis(3)
.iter()
.find(|phi| phi.location == loc);
assert!(
found.is_some(),
"the merge block carries a memory phi for the stored location"
);
let Some(phi) = found else { return };
let from_storing = phi.operand_from(1).map(|op| op.version);
let from_passthrough = phi.operand_from(2).map(|op| op.version);
assert_eq!(
from_passthrough, b0_exit,
"the pass-through arm must reach the merge on the inherited version"
);
assert_ne!(
from_storing, from_passthrough,
"the storing arm reaches the merge on its own store version"
);
assert_eq!(
from_storing.and_then(|v| mem_ssa.definition(&MemoryVersion::new(loc.clone(), v))),
Some(MemoryDefSite::Store { block: 1, instr: 0 }),
"the storing arm's operand resolves to that arm's store"
);
}
#[test]
fn every_live_memory_version_has_a_definition_site() {
let (ssa, obj) = diamond_over_one_field(true);
let cfg = SsaCfg::from_ssa(&ssa);
let mem_ssa = MemorySsa::<MockTarget>::build(&ssa, &cfg, PointerSize::Bit64);
let loc = MemoryLocation::<MockTarget>::InstanceField(obj, 5u32);
for block in 0..4usize {
for version in [
mem_ssa.version_at_entry(&loc, block),
mem_ssa.version_at_exit(&loc, block),
]
.into_iter()
.flatten()
{
assert!(
mem_ssa
.definition(&MemoryVersion::new(loc.clone(), version))
.is_some(),
"version {version} live at block {block} must have a definition site"
);
}
}
assert_eq!(
mem_ssa
.version_at_entry(&loc, 0)
.and_then(|v| mem_ssa.definition(&MemoryVersion::new(loc.clone(), v))),
Some(MemoryDefSite::Entry),
"the version entering the entry block is the function-entry definition"
);
}
fn stores_through_ptradd_offsets(offsets: &[i64]) -> SsaFunction<MockTarget> {
let mut ssa = SsaFunction::new(0, 32);
let base = local(&mut ssa, 0, 0, 0);
let val = local(&mut ssa, 1, 0, 1);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::Const {
dest: base,
value: ConstValue::I32(0x1000),
}));
b0.add_instruction(instr(SsaOp::Const {
dest: val,
value: ConstValue::I32(7),
}));
let mut addrs = Vec::new();
for (nth, offset) in offsets.iter().enumerate() {
let addr = ssa.create_variable(
VariableOrigin::Local(u16::try_from(nth).unwrap_or(0).saturating_add(10)),
0,
DefSite::instruction(0, nth.saturating_add(2)),
MockType::Ptr,
);
addrs.push(addr);
b0.add_instruction(instr(SsaOp::PtrAdd {
dest: addr,
base,
index: None,
stride: 0,
offset: *offset,
result_type: MockType::Ptr,
}));
}
for addr in addrs {
b0.add_instruction(instr(SsaOp::StoreIndirect {
addr,
value: val,
value_type: MockType::I32,
address_space: None,
}));
}
b0.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b0);
ssa.recompute_uses();
ssa
}
#[test]
fn distinct_stack_slots_get_disjoint_memory_locations() {
let ssa = stores_through_ptradd_offsets(&[0, 8]);
let cfg = SsaCfg::from_ssa(&ssa);
let mem_ssa = MemorySsa::<MockTarget>::build(&ssa, &cfg, PointerSize::Bit64);
let locations: Vec<_> = mem_ssa
.locations()
.iter()
.filter(|loc| matches!(loc, MemoryLocation::Indirect(_)))
.cloned()
.collect();
assert_eq!(
locations.len(),
2,
"the two offsets are two cells, got {locations:?}"
);
let (Some(first), Some(second)) = (locations.first(), locations.get(1)) else {
return;
};
assert_eq!(
analyze_alias(first, second),
AliasResult::NoAlias,
"8 bytes apart with 4-byte accesses cannot overlap"
);
let mut offsets: Vec<i64> = locations
.iter()
.filter_map(|loc| match loc {
MemoryLocation::Indirect(indirect) => Some(indirect.offset_bits),
_ => None,
})
.collect();
offsets.sort_unstable();
assert_eq!(offsets, vec![0, 64], "offsets are decoded in bits");
}
#[test]
fn identical_addresses_from_separate_ptradds_are_one_location() {
let ssa = stores_through_ptradd_offsets(&[16, 16]);
let cfg = SsaCfg::from_ssa(&ssa);
let mem_ssa = MemorySsa::<MockTarget>::build(&ssa, &cfg, PointerSize::Bit64);
let locations: Vec<_> = mem_ssa
.locations()
.iter()
.filter(|loc| matches!(loc, MemoryLocation::Indirect(_)))
.cloned()
.collect();
assert_eq!(
locations.len(),
1,
"two PtrAdds to one address are one cell, got {locations:?}"
);
let stores = mem_ssa
.operations()
.iter()
.filter(|op| matches!(op, MemoryOp::Store { .. }))
.count();
assert_eq!(stores, 2, "both stores are recorded");
let Some(only) = locations.first() else {
return;
};
assert_eq!(analyze_alias(only, &only.clone()), AliasResult::MustAlias);
}
#[test]
fn constant_array_indices_fold_and_separate() {
let mut ssa = SsaFunction::new(0, 6);
let array = local(&mut ssa, 0, 0, 0);
let idx0 = local(&mut ssa, 1, 0, 1);
let idx1 = local(&mut ssa, 2, 0, 2);
let val = local(&mut ssa, 3, 0, 3);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::Const {
dest: array,
value: ConstValue::I32(0x2000),
}));
b0.add_instruction(instr(SsaOp::Const {
dest: idx0,
value: ConstValue::I32(0),
}));
b0.add_instruction(instr(SsaOp::Const {
dest: idx1,
value: ConstValue::I32(1),
}));
b0.add_instruction(instr(SsaOp::Const {
dest: val,
value: ConstValue::I32(9),
}));
b0.add_instruction(instr(SsaOp::StoreElement {
array,
index: idx0,
value: val,
elem_type: MockType::I32,
}));
b0.add_instruction(instr(SsaOp::StoreElement {
array,
index: idx1,
value: val,
elem_type: MockType::I32,
}));
b0.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b0);
ssa.recompute_uses();
let cfg = SsaCfg::from_ssa(&ssa);
let mem_ssa = MemorySsa::<MockTarget>::build(&ssa, &cfg, PointerSize::Bit64);
let elem0 = MemoryLocation::<MockTarget>::ArrayElement(array, ArrayIndex::Constant(0));
let elem1 = MemoryLocation::<MockTarget>::ArrayElement(array, ArrayIndex::Constant(1));
assert!(
mem_ssa.locations().contains(&elem0),
"index 0 folded to a constant, got {:?}",
mem_ssa.locations()
);
assert!(mem_ssa.locations().contains(&elem1));
assert_eq!(analyze_alias(&elem0, &elem1), AliasResult::NoAlias);
}
#[test]
fn distinct_object_ids_may_alias_the_same_field() {
let a = SsaVarId::from_index(0);
let b = SsaVarId::from_index(1);
let via_a = MemoryLocation::<MockTarget>::InstanceField(a, 3u32);
let via_b = MemoryLocation::<MockTarget>::InstanceField(b, 3u32);
assert!(
via_a.may_alias(&via_b),
"distinct SSA ids can hold the same object reference"
);
assert!(
!via_a.must_alias(&via_b),
"but they are not proved to be the same object, so forwarding stays off"
);
}
#[test]
fn different_fields_of_one_object_still_do_not_alias() {
let obj = SsaVarId::from_index(0);
let f3 = MemoryLocation::<MockTarget>::InstanceField(obj, 3u32);
let f4 = MemoryLocation::<MockTarget>::InstanceField(obj, 4u32);
assert!(
!f3.may_alias(&f4),
"distinct fields of a single object are disjoint"
);
assert!(f3.may_alias(&f3.clone()));
assert!(f3.must_alias(&f3.clone()));
}
#[test]
fn distinct_array_ids_may_alias_regardless_of_index() {
let a = SsaVarId::from_index(0);
let b = SsaVarId::from_index(1);
let a0 = MemoryLocation::<MockTarget>::ArrayElement(a, ArrayIndex::Constant(0));
let b7 = MemoryLocation::<MockTarget>::ArrayElement(b, ArrayIndex::Constant(7));
assert!(
a0.may_alias(&b7),
"different array ids may name one array, so the indices prove nothing"
);
let a7 = MemoryLocation::<MockTarget>::ArrayElement(a, ArrayIndex::Constant(7));
assert!(
!a0.may_alias(&a7),
"on a single array, constant indices 0 and 7 are disjoint"
);
}
#[test]
fn an_access_wrapping_the_address_space_does_not_prove_disjointness() {
let base = SsaVarId::from_index(0);
let top_bits = i64::from(i32::MAX) * 8 - 16;
let wrapping = MemoryLocation::<MockTarget>::Indirect(IndirectLocation::new(
base,
None,
0,
top_bits,
Some(64),
None,
PointerSize::Bit32,
));
let low = MemoryLocation::<MockTarget>::Indirect(IndirectLocation::new(
base,
None,
0,
-(i64::from(i32::MAX) + 1) * 8,
Some(64),
None,
PointerSize::Bit32,
));
assert!(
wrapping.may_alias(&low),
"an access that wraps the address space must not be proved disjoint"
);
let a = MemoryLocation::<MockTarget>::Indirect(IndirectLocation::new(
base,
None,
0,
0,
Some(32),
None,
PointerSize::Bit32,
));
let b = MemoryLocation::<MockTarget>::Indirect(IndirectLocation::new(
base,
None,
0,
64,
Some(32),
None,
PointerSize::Bit32,
));
assert!(
!a.may_alias(&b),
"disjoint extents well inside the address space still do not alias"
);
}
#[test]
fn a_function_past_the_memory_ssa_budget_degrades_instead_of_growing() {
const BLOCKS: usize = 3000;
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, BLOCKS + 1);
let base = local(&mut ssa, 0, 0, 0);
let value = local(&mut ssa, 1, 0, 1);
let mut entry = SsaBlock::new(0);
entry.add_instruction(instr(SsaOp::Const {
dest: base,
value: ConstValue::I32(0x1000),
}));
entry.add_instruction(instr(SsaOp::Const {
dest: value,
value: ConstValue::I32(7),
}));
entry.add_instruction(instr(SsaOp::Jump { target: 1 }));
ssa.add_block(entry);
for block_id in 1..=BLOCKS {
let addr = ssa.create_variable(
VariableOrigin::Local(2),
0,
DefSite::instruction(block_id, 0),
MockType::Ptr,
);
let mut block = SsaBlock::new(block_id);
block.add_instruction(instr(SsaOp::PtrAdd {
dest: addr,
base,
index: None,
stride: 0,
offset: (block_id as i64) * 8,
result_type: MockType::Ptr,
}));
block.add_instruction(instr(SsaOp::StoreIndirect {
addr,
value,
value_type: MockType::I32,
address_space: None,
}));
if block_id == BLOCKS {
block.add_instruction(instr(SsaOp::Return { value: None }));
} else {
block.add_instruction(instr(SsaOp::Jump {
target: block_id + 1,
}));
}
ssa.add_block(block);
}
ssa.recompute_uses();
let cfg = SsaCfg::from_ssa(&ssa);
let mem_ssa = MemorySsa::<MockTarget>::build(&ssa, &cfg, PointerSize::Bit64);
assert!(
mem_ssa.locations().is_empty(),
"past the budget the analysis must report nothing rather than build \
a quadratic structure"
);
let unknown = MemoryLocation::<MockTarget>::Unknown;
assert_eq!(mem_ssa.version_at_entry(&unknown, 1), None);
}