use std::collections::BTreeMap;
use crate::{
ir::{ConstValue, SsaFunction, SsaOp, SsaVarId},
pointer::PointerSize,
target::Target,
};
const MAX_ADDRESS_DEPTH: usize = 8;
fn canonical_offset_bits(offset_bits: i64, ptr_size: PointerSize) -> i64 {
let bytes = offset_bits.div_euclid(8);
let sub_byte = offset_bits.rem_euclid(8);
ptr_size
.mask_signed(bytes)
.saturating_mul(8)
.saturating_add(sub_byte)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AddressExpr {
pub base: SsaVarId,
pub index: Option<SsaVarId>,
pub stride_bytes: u64,
pub offset_bits: i64,
}
impl AddressExpr {
#[must_use]
fn identity(value: SsaVarId) -> Self {
Self {
base: value,
index: None,
stride_bytes: 0,
offset_bits: 0,
}
}
}
#[must_use]
pub fn normalize_address<T: Target>(
ir: &SsaFunction<T>,
value: SsaVarId,
ptr_size: PointerSize,
) -> AddressExpr {
normalize_inner(ir, value, MAX_ADDRESS_DEPTH, ptr_size)
}
fn normalize_inner<T: Target>(
ir: &SsaFunction<T>,
value: SsaVarId,
depth: usize,
ptr_size: PointerSize,
) -> AddressExpr {
if depth == 0 {
return AddressExpr::identity(value);
}
let Some(op) = ir.get_definition(value) else {
return AddressExpr::identity(value);
};
match op {
SsaOp::Copy { src, .. }
| SsaOp::IntConv { operand: src, .. }
| SsaOp::IntToPtr { operand: src, .. }
| SsaOp::PtrToInt { operand: src, .. }
| SsaOp::Bitcast { operand: src, .. } => {
normalize_inner(ir, *src, depth.saturating_sub(1), ptr_size)
}
SsaOp::PtrAdd {
base,
index,
stride,
offset,
..
} => {
let base_expr = normalize_inner(ir, *base, depth.saturating_sub(1), ptr_size);
let Some(off_bits) = offset.checked_mul(8) else {
return AddressExpr::identity(value);
};
let mut expr = fold_offset(base_expr, off_bits, value, ptr_size);
if expr.base == value {
return expr;
}
if let Some(index) = index {
if expr.index.is_some() {
return AddressExpr::identity(value);
}
expr.index = Some(*index);
expr.stride_bytes = *stride;
}
expr
}
SsaOp::Add { left, right, .. } => normalize_add(ir, value, *left, *right, depth, ptr_size),
SsaOp::Sub { left, right, .. } => {
let Some(offset) = const_i64(ir, *right).and_then(|v| v.checked_mul(8)) else {
return AddressExpr::identity(value);
};
let base = normalize_inner(ir, *left, depth.saturating_sub(1), ptr_size);
let Some(negated) = offset.checked_neg() else {
return AddressExpr::identity(value);
};
fold_offset(base, negated, value, ptr_size)
}
_ => AddressExpr::identity(value),
}
}
fn normalize_add<T: Target>(
ir: &SsaFunction<T>,
value: SsaVarId,
left: SsaVarId,
right: SsaVarId,
depth: usize,
ptr_size: PointerSize,
) -> AddressExpr {
if let Some(offset) = const_i64(ir, right).and_then(|v| v.checked_mul(8)) {
return fold_offset(
normalize_inner(ir, left, depth.saturating_sub(1), ptr_size),
offset,
value,
ptr_size,
);
}
if let Some(offset) = const_i64(ir, left).and_then(|v| v.checked_mul(8)) {
return fold_offset(
normalize_inner(ir, right, depth.saturating_sub(1), ptr_size),
offset,
value,
ptr_size,
);
}
if let Some((index, stride)) = scaled_index(ir, right) {
let mut base = normalize_inner(ir, left, depth.saturating_sub(1), ptr_size);
if base.index.is_none() {
base.index = Some(index);
base.stride_bytes = stride;
return base;
}
}
if let Some((index, stride)) = scaled_index(ir, left) {
let mut base = normalize_inner(ir, right, depth.saturating_sub(1), ptr_size);
if base.index.is_none() {
base.index = Some(index);
base.stride_bytes = stride;
return base;
}
}
AddressExpr::identity(value)
}
fn fold_offset(
mut base: AddressExpr,
offset: i64,
value: SsaVarId,
ptr_size: PointerSize,
) -> AddressExpr {
match base.offset_bits.checked_add(offset) {
Some(folded) => {
base.offset_bits = canonical_offset_bits(folded, ptr_size);
base
}
None => AddressExpr::identity(value),
}
}
#[must_use]
pub fn scaled_index<T: Target>(ir: &SsaFunction<T>, value: SsaVarId) -> Option<(SsaVarId, u64)> {
match ir.get_definition(value)? {
SsaOp::Mul { left, right, .. } => {
if let Some(stride) = const_u64(ir, *right) {
return Some((*left, stride));
}
if let Some(stride) = const_u64(ir, *left) {
return Some((*right, stride));
}
None
}
SsaOp::Shl { value, amount, .. } => {
let amount = u32::try_from(const_u64(ir, *amount)?).ok()?;
1_u64.checked_shl(amount).map(|stride| (*value, stride))
}
_ => None,
}
}
#[must_use]
pub fn const_i64<T: Target>(ir: &SsaFunction<T>, value: SsaVarId) -> Option<i64> {
match ir.get_definition(value)? {
SsaOp::Const { value, .. } => const_value_i64(value),
_ => None,
}
}
#[must_use]
pub fn const_u64<T: Target>(ir: &SsaFunction<T>, value: SsaVarId) -> Option<u64> {
u64::try_from(const_i64(ir, value)?).ok()
}
#[must_use]
pub fn const_value_i64<T: Target>(value: &ConstValue<T>) -> Option<i64> {
match value {
ConstValue::I8(value) => Some(i64::from(*value)),
ConstValue::I16(value) => Some(i64::from(*value)),
ConstValue::I32(value) => Some(i64::from(*value)),
ConstValue::I64(value) => Some(*value),
ConstValue::U8(value) => Some(i64::from(*value)),
ConstValue::U16(value) => Some(i64::from(*value)),
ConstValue::U32(value) => Some(i64::from(*value)),
ConstValue::U64(value) => i64::try_from(*value).ok(),
ConstValue::NativeInt(value) => Some(*value),
ConstValue::NativeUInt(value) => i64::try_from(*value).ok(),
ConstValue::True => Some(1),
ConstValue::False => Some(0),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AliasKey {
pub base_value: u32,
pub index: Option<u32>,
pub stride_bytes: u64,
pub offset_bits: i64,
}
#[must_use]
pub fn alias_keys_for_function<T: Target>(
ir: &SsaFunction<T>,
ptr_size: PointerSize,
) -> BTreeMap<u32, AliasKey> {
let mut keys = BTreeMap::new();
for block in ir.blocks() {
for instruction in block.instructions() {
let dest = match instruction.op() {
SsaOp::Copy { dest, .. }
| SsaOp::IntConv { dest, .. }
| SsaOp::IntToPtr { dest, .. }
| SsaOp::PtrToInt { dest, .. }
| SsaOp::Bitcast { dest, .. }
| SsaOp::Add { dest, .. }
| SsaOp::Sub { dest, .. } => *dest,
_ => continue,
};
let address = normalize_address(ir, dest, ptr_size);
if address.index.is_none() && address.base == dest && address.offset_bits == 0 {
continue;
}
keys.insert(
dest.as_u32(),
AliasKey {
base_value: address.base.as_u32(),
index: address.index.map(SsaVarId::as_u32),
stride_bytes: address.stride_bytes,
offset_bits: address.offset_bits,
},
);
}
}
keys
}
#[must_use]
pub fn alias_key_for_value(value: SsaVarId, keys: &BTreeMap<u32, AliasKey>) -> Option<AliasKey> {
let id = value.as_u32();
keys.get(&id).copied().or(Some(AliasKey {
base_value: id,
index: None,
stride_bytes: 0,
offset_bits: 0,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ir::{
SsaBlock, SsaInstruction,
variable::{DefSite, VariableOrigin},
},
testing::{MockTarget, MockType},
};
fn function() -> SsaFunction<MockTarget> {
SsaFunction::<MockTarget>::with_capacity(0, 0, 1, 8)
}
fn var(ir: &mut SsaFunction<MockTarget>, origin: VariableOrigin) -> SsaVarId {
ir.create_variable(origin, 0, DefSite::entry(), MockType::Unknown)
}
fn instr(op: SsaOp<MockTarget>) -> SsaInstruction<MockTarget> {
SsaInstruction::new((), op)
}
#[test]
fn const_value_i64_covers_booleans_and_integers() {
assert_eq!(const_value_i64(&ConstValue::<MockTarget>::True), Some(1));
assert_eq!(const_value_i64(&ConstValue::<MockTarget>::False), Some(0));
assert_eq!(
const_value_i64(&ConstValue::<MockTarget>::I32(-7)),
Some(-7)
);
assert_eq!(
const_value_i64(&ConstValue::<MockTarget>::U64(u64::MAX)),
None,
"an out-of-range unsigned constant does not convert"
);
}
#[test]
fn normalize_folds_base_plus_constant() {
let mut ir = function();
let base = var(&mut ir, VariableOrigin::Argument(0));
let offset = var(&mut ir, VariableOrigin::Local(0));
let addr = var(&mut ir, VariableOrigin::Local(1));
let mut block = SsaBlock::with_capacity(0, 0, 4);
block.add_instruction(instr(SsaOp::Const {
dest: offset,
value: ConstValue::I32(4),
}));
block.add_instruction(instr(SsaOp::Add {
dest: addr,
left: base,
right: offset,
flags: None,
}));
ir.add_block(block);
ir.recompute_uses();
let address = normalize_address(&ir, addr, PointerSize::Bit64);
assert_eq!(address.base, base);
assert_eq!(address.index, None);
assert_eq!(address.offset_bits, 32);
}
#[test]
fn both_lowerings_of_one_displacement_canonicalise_together() {
fn address_for(value: ConstValue<MockTarget>, ptr_size: PointerSize) -> AddressExpr {
let mut ir = function();
let base = var(&mut ir, VariableOrigin::Argument(0));
let offset = var(&mut ir, VariableOrigin::Local(1));
let addr = var(&mut ir, VariableOrigin::Local(2));
let mut block = SsaBlock::with_capacity(0, 0, 2);
block.add_instruction(instr(SsaOp::Const {
dest: offset,
value,
}));
block.add_instruction(instr(SsaOp::Add {
dest: addr,
left: base,
right: offset,
flags: None,
}));
ir.add_block(block);
ir.recompute_uses();
normalize_address(&ir, addr, ptr_size)
}
let wrapped = address_for(ConstValue::U32(0xFFFF_FFF8), PointerSize::Bit32);
let signed = address_for(ConstValue::I32(-8), PointerSize::Bit32);
assert_eq!(
wrapped, signed,
"on a 32-bit target 0xFFFF_FFF8 and -8 are the same displacement"
);
assert_eq!(signed.offset_bits, -64, "-8 bytes is -64 bits");
let wide_wrapped = address_for(ConstValue::U32(0xFFFF_FFF8), PointerSize::Bit64);
let wide_signed = address_for(ConstValue::I32(-8), PointerSize::Bit64);
assert_ne!(
wide_wrapped, wide_signed,
"on a 64-bit target +4294967288 and -8 are distinct addresses"
);
assert_eq!(wide_signed.offset_bits, -64);
}
#[test]
fn canonicalisation_follows_the_target_width() {
assert_eq!(canonical_offset_bits(-64, PointerSize::Bit32), -64);
assert_eq!(
canonical_offset_bits(4_294_967_288 * 8, PointerSize::Bit32),
-64
);
assert_eq!(canonical_offset_bits(65_528 * 8, PointerSize::Bit16), -64);
assert_eq!(
canonical_offset_bits(4_294_967_288 * 8, PointerSize::Bit64),
4_294_967_288 * 8
);
assert_eq!(canonical_offset_bits(-64 + 3, PointerSize::Bit32), -64 + 3);
}
#[test]
fn ordinary_displacements_still_fold() {
for (value, expected_bits) in [
(ConstValue::<MockTarget>::I32(-8), -64i64),
(ConstValue::<MockTarget>::I32(16), 128),
(ConstValue::<MockTarget>::I32(4096), 32768),
] {
let mut ir = function();
let base = var(&mut ir, VariableOrigin::Argument(0));
let offset = var(&mut ir, VariableOrigin::Local(1));
let addr = var(&mut ir, VariableOrigin::Local(2));
let mut block = SsaBlock::with_capacity(0, 0, 2);
block.add_instruction(instr(SsaOp::Const {
dest: offset,
value: value.clone(),
}));
block.add_instruction(instr(SsaOp::Add {
dest: addr,
left: base,
right: offset,
flags: None,
}));
ir.add_block(block);
ir.recompute_uses();
let address = normalize_address(&ir, addr, PointerSize::Bit64);
assert_eq!(
address.base, base,
"{value:?} should decompose onto the base"
);
assert_eq!(address.offset_bits, expected_bits, "for {value:?}");
}
}
#[test]
fn normalize_records_scaled_index() {
let mut ir = function();
let base = var(&mut ir, VariableOrigin::Argument(0));
let index = var(&mut ir, VariableOrigin::Argument(1));
let stride = var(&mut ir, VariableOrigin::Local(2));
let scaled = var(&mut ir, VariableOrigin::Local(3));
let addr = var(&mut ir, VariableOrigin::Local(4));
let mut block = SsaBlock::with_capacity(0, 0, 4);
block.add_instruction(instr(SsaOp::Const {
dest: stride,
value: ConstValue::I32(8),
}));
block.add_instruction(instr(SsaOp::Mul {
dest: scaled,
left: index,
right: stride,
flags: None,
}));
block.add_instruction(instr(SsaOp::Add {
dest: addr,
left: base,
right: scaled,
flags: None,
}));
ir.add_block(block);
ir.recompute_uses();
let address = normalize_address(&ir, addr, PointerSize::Bit64);
assert_eq!(address.base, base);
assert_eq!(address.index, Some(index));
assert_eq!(address.stride_bytes, 8);
assert_eq!(address.offset_bits, 0);
}
#[test]
fn normalize_reads_ptradd_directly() {
let mut ir = function();
let base = var(&mut ir, VariableOrigin::Argument(0));
let index = var(&mut ir, VariableOrigin::Argument(1));
let addr = var(&mut ir, VariableOrigin::Local(0));
let mut block = SsaBlock::with_capacity(0, 0, 1);
block.add_instruction(instr(SsaOp::PtrAdd {
dest: addr,
base,
index: Some(index),
stride: 4,
offset: 8,
result_type: MockType::Ptr,
}));
ir.add_block(block);
ir.recompute_uses();
let address = normalize_address(&ir, addr, PointerSize::Bit64);
assert_eq!(address.base, base);
assert_eq!(address.index, Some(index));
assert_eq!(address.stride_bytes, 4);
assert_eq!(address.offset_bits, 64);
}
#[test]
fn normalize_unknown_value_is_identity() {
let ir = function();
let value = SsaVarId::from_index(7);
let address = normalize_address(&ir, value, PointerSize::Bit64);
assert_eq!(address.base, value);
assert_eq!(address.index, None);
assert_eq!(address.offset_bits, 0);
}
#[test]
fn alias_key_for_value_falls_back_to_identity() {
let keys = BTreeMap::new();
let value = SsaVarId::from_index(3);
assert_eq!(
alias_key_for_value(value, &keys),
Some(AliasKey {
base_value: 3,
index: None,
stride_bytes: 0,
offset_bits: 0,
})
);
}
#[test]
fn alias_key_records_scaled_index_cell() {
let mut ir = function();
let base = var(&mut ir, VariableOrigin::Argument(0));
let index = var(&mut ir, VariableOrigin::Argument(1));
let stride = ir.create_variable(
VariableOrigin::Local(0),
0,
DefSite::instruction(0, 0),
MockType::Unknown,
);
let scaled = ir.create_variable(
VariableOrigin::Local(1),
0,
DefSite::instruction(0, 1),
MockType::Unknown,
);
let addr = ir.create_variable(
VariableOrigin::Local(2),
0,
DefSite::instruction(0, 2),
MockType::Unknown,
);
let mut block = SsaBlock::with_capacity(0, 0, 3);
block.add_instruction(instr(SsaOp::Const {
dest: stride,
value: ConstValue::I32(8),
}));
block.add_instruction(instr(SsaOp::Mul {
dest: scaled,
left: index,
right: stride,
flags: None,
}));
block.add_instruction(instr(SsaOp::Add {
dest: addr,
left: base,
right: scaled,
flags: None,
}));
ir.add_block(block);
ir.recompute_uses();
let keys = alias_keys_for_function(&ir, PointerSize::Bit64);
assert_eq!(
keys.get(&addr.as_u32()),
Some(&AliasKey {
base_value: base.as_u32(),
index: Some(index.as_u32()),
stride_bytes: 8,
offset_bits: 0,
})
);
}
}