use std::collections::{BTreeMap, BTreeSet, VecDeque};
use crate::{
ir::{SsaFunction, SsaOp},
target::Target,
};
pub type Loc = u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Constraint {
AddressOf {
ptr: Loc,
target: Loc,
},
Copy {
dst: Loc,
src: Loc,
},
Load {
dst: Loc,
ptr: Loc,
},
Store {
ptr: Loc,
src: Loc,
},
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct PointsTo {
sets: BTreeMap<Loc, BTreeSet<Loc>>,
}
impl PointsTo {
#[must_use]
pub fn get(&self, loc: Loc) -> &BTreeSet<Loc> {
static EMPTY: BTreeSet<Loc> = BTreeSet::new();
self.sets.get(&loc).unwrap_or(&EMPTY)
}
pub fn iter(&self) -> impl Iterator<Item = (Loc, &BTreeSet<Loc>)> {
self.sets.iter().map(|(loc, set)| (*loc, set))
}
#[must_use]
pub fn reachable_closure<I>(&self, seeds: I) -> BTreeSet<Loc>
where
I: IntoIterator<Item = Loc>,
{
let mut reached: BTreeSet<Loc> = BTreeSet::new();
let mut stack: Vec<Loc> = Vec::new();
for seed in seeds {
if reached.insert(seed) {
stack.push(seed);
}
}
while let Some(loc) = stack.pop() {
for &pointee in self.get(loc) {
if reached.insert(pointee) {
stack.push(pointee);
}
}
}
reached
}
}
#[derive(Default)]
struct DerefConstraints {
loads: BTreeMap<Loc, BTreeSet<Loc>>,
stores: BTreeMap<Loc, BTreeSet<Loc>>,
}
#[derive(Default)]
struct SolverGraph {
sets: BTreeMap<Loc, BTreeSet<Loc>>,
copy_edges: BTreeMap<Loc, BTreeSet<Loc>>,
}
impl SolverGraph {
fn add_pointee(&mut self, loc: Loc, target: Loc) -> bool {
self.sets.entry(loc).or_default().insert(target)
}
fn propagate_copy(
sets: &mut BTreeMap<Loc, BTreeSet<Loc>>,
src: Loc,
dst: Loc,
work: &mut Worklist,
) {
if src == dst {
return;
}
let Some(src_set) = sets.get(&src) else {
return;
};
let additions: Vec<Loc> = match sets.get(&dst) {
Some(dst_set) => src_set.difference(dst_set).copied().collect(),
None => src_set.iter().copied().collect(),
};
if additions.is_empty() {
return;
}
let dst_set = sets.entry(dst).or_default();
for target in additions {
dst_set.insert(target);
}
work.push(dst);
}
fn add_copy_edge(&mut self, src: Loc, dst: Loc, work: &mut Worklist) {
if src == dst {
return;
}
if self.copy_edges.entry(src).or_default().insert(dst) {
Self::propagate_copy(&mut self.sets, src, dst, work);
}
}
}
#[derive(Default)]
struct Worklist {
queue: VecDeque<Loc>,
queued: BTreeSet<Loc>,
}
impl Worklist {
fn push(&mut self, loc: Loc) {
if self.queued.insert(loc) {
self.queue.push_back(loc);
}
}
fn pop(&mut self) -> Option<Loc> {
let loc = self.queue.pop_front()?;
self.queued.remove(&loc);
Some(loc)
}
}
#[must_use]
pub fn solve(constraints: &[Constraint]) -> PointsTo {
let mut graph = SolverGraph::default();
let mut deref = DerefConstraints::default();
let mut work = Worklist::default();
for constraint in constraints {
match *constraint {
Constraint::AddressOf { ptr, target } => {
if graph.add_pointee(ptr, target) {
work.push(ptr);
}
}
Constraint::Copy { dst, src } => {
graph.copy_edges.entry(src).or_default().insert(dst);
if graph.sets.contains_key(&src) {
work.push(src);
}
}
Constraint::Load { dst, ptr } => {
deref.loads.entry(ptr).or_default().insert(dst);
if graph.sets.contains_key(&ptr) {
work.push(ptr);
}
}
Constraint::Store { ptr, src } => {
deref.stores.entry(ptr).or_default().insert(src);
if graph.sets.contains_key(&ptr) {
work.push(ptr);
}
}
}
}
let safety_bound = constraints.len().saturating_mul(64).saturating_add(64);
let mut steps = 0usize;
while let Some(loc) = work.pop() {
if steps > safety_bound {
log::warn!(
"points-to analysis exceeded its step budget ({} constraints, bound {safety_bound}); returning partial result",
constraints.len()
);
break;
}
if let Some(dsts) = graph.copy_edges.get(&loc) {
steps = steps.saturating_add(dsts.len());
for &dst in dsts {
SolverGraph::propagate_copy(&mut graph.sets, loc, dst, &mut work);
}
}
if let Some(dsts) = deref.loads.get(&loc) {
let pointees = graph.sets.get(&loc).cloned().unwrap_or_default();
steps = steps.saturating_add(pointees.len().saturating_mul(dsts.len()));
for pointee in pointees {
for &dst in dsts {
graph.add_copy_edge(pointee, dst, &mut work);
}
}
}
if let Some(srcs) = deref.stores.get(&loc) {
let pointees = graph.sets.get(&loc).cloned().unwrap_or_default();
steps = steps.saturating_add(pointees.len().saturating_mul(srcs.len()));
for pointee in pointees {
for &src in srcs {
graph.add_copy_edge(src, pointee, &mut work);
}
}
}
}
PointsTo { sets: graph.sets }
}
pub const SYNTHETIC_LOC_BASE: Loc = 0xF000_0000;
const LOCAL_LOC_BASE: Loc = SYNTHETIC_LOC_BASE;
const ARG_LOC_BASE: Loc = 0xF800_0000;
const FIELD_LOC_BASE: Loc = 0xFC00_0000;
pub const UNKNOWN_LOC: Loc = 0xFFFF_FFFF;
#[must_use]
pub fn is_synthetic_object(loc: Loc) -> bool {
loc >= SYNTHETIC_LOC_BASE
}
#[derive(Default)]
struct FieldCells {
cells: BTreeMap<(u32, u32), Loc>,
next: u32,
}
impl FieldCells {
fn cell(&mut self, object: u32, member_index: u32) -> Option<Loc> {
if let Some(loc) = self.cells.get(&(object, member_index)) {
return Some(*loc);
}
let loc = FIELD_LOC_BASE.checked_add(self.next)?;
self.next = self.next.saturating_add(1);
self.cells.insert((object, member_index), loc);
Some(loc)
}
}
#[must_use]
pub fn analyze_function<T: Target>(ir: &SsaFunction<T>) -> PointsTo {
let mut constraints: Vec<Constraint> = Vec::new();
let mut field_cells = FieldCells::default();
for block in ir.blocks() {
for phi in block.phi_nodes() {
for operand in phi.operands() {
constraints.push(Constraint::Copy {
dst: phi.result().as_u32(),
src: operand.value().as_u32(),
});
}
}
for instruction in block.instructions() {
match instruction.op() {
SsaOp::Copy { dest, src } => constraints.push(Constraint::Copy {
dst: dest.as_u32(),
src: src.as_u32(),
}),
SsaOp::IntConv { dest, operand, .. }
| SsaOp::IntToPtr { dest, operand, .. }
| SsaOp::PtrToInt { dest, operand, .. }
| SsaOp::IntToFloat { dest, operand, .. }
| SsaOp::FloatToInt { dest, operand, .. }
| SsaOp::FloatConv { dest, operand, .. }
| SsaOp::Bitcast { dest, operand, .. } => {
constraints.push(Constraint::Copy {
dst: dest.as_u32(),
src: operand.as_u32(),
});
}
SsaOp::PtrAdd { dest, base, .. } => {
constraints.push(Constraint::Copy {
dst: dest.as_u32(),
src: base.as_u32(),
});
}
SsaOp::LoadFieldAddr {
dest,
object,
field,
} => {
constraints.push(Constraint::Copy {
dst: dest.as_u32(),
src: object.as_u32(),
});
if let Some(member_index) = T::field_member_index(field)
&& let Some(cell) = field_cells.cell(object.as_u32(), member_index)
{
constraints.push(Constraint::AddressOf {
ptr: dest.as_u32(),
target: cell,
});
}
}
SsaOp::LoadIndirect { dest, addr, .. } => constraints.push(Constraint::Load {
dst: dest.as_u32(),
ptr: addr.as_u32(),
}),
SsaOp::StoreIndirect { addr, value, .. } => constraints.push(Constraint::Store {
ptr: addr.as_u32(),
src: value.as_u32(),
}),
SsaOp::LoadArgAddr { dest, arg_index } => constraints.push(Constraint::AddressOf {
ptr: dest.as_u32(),
target: ARG_LOC_BASE.saturating_add(u32::from(*arg_index)),
}),
SsaOp::LoadLocalAddr { dest, local_index } => {
constraints.push(Constraint::AddressOf {
ptr: dest.as_u32(),
target: LOCAL_LOC_BASE.saturating_add(u32::from(*local_index)),
});
}
other => {
for dest in other.defs() {
constraints.push(Constraint::AddressOf {
ptr: dest.as_u32(),
target: UNKNOWN_LOC,
});
}
}
}
}
}
solve(&constraints)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ir::{
SsaBlock, SsaInstruction,
phi::{PhiNode, PhiOperand},
variable::{DefSite, VariableOrigin},
},
testing::{MOCK_FIELD_UNRESOLVED, MockTarget, MockType},
};
fn ptr_var(
ir: &mut SsaFunction<MockTarget>,
origin: VariableOrigin,
block: usize,
instruction: usize,
) -> crate::ir::SsaVarId {
ir.create_variable(
origin,
0,
DefSite::instruction(block, instruction),
MockType::Ptr,
)
}
fn instr(op: SsaOp<MockTarget>) -> SsaInstruction<MockTarget> {
SsaInstruction::new((), op)
}
#[test]
fn a_phi_merges_the_pointees_of_its_operands() {
let mut ir: SsaFunction<MockTarget> = SsaFunction::new(0, 4);
let mk = |ir: &mut SsaFunction<MockTarget>, idx: u16, block: usize, instr: usize| {
ir.create_variable(
VariableOrigin::Local(idx),
0,
DefSite::instruction(block, instr),
MockType::Ptr,
)
};
let left = mk(&mut ir, 0, 1, 0);
let right = mk(&mut ir, 1, 2, 0);
let merged =
ir.create_variable(VariableOrigin::Local(2), 0, DefSite::phi(3), MockType::Ptr);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ir.add_block(b0);
let mut b1 = SsaBlock::new(1);
b1.add_instruction(SsaInstruction::synthetic(SsaOp::LoadLocalAddr {
dest: left,
local_index: 5,
}));
b1.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 3 }));
ir.add_block(b1);
let mut b2 = SsaBlock::new(2);
b2.add_instruction(SsaInstruction::synthetic(SsaOp::LoadLocalAddr {
dest: right,
local_index: 9,
}));
b2.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 3 }));
ir.add_block(b2);
let mut b3 = SsaBlock::new(3);
let mut phi = PhiNode::new(merged, VariableOrigin::Local(2));
phi.add_operand(PhiOperand::new(left, 1));
phi.add_operand(PhiOperand::new(right, 2));
b3.add_phi(phi);
b3.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: None }));
ir.add_block(b3);
ir.recompute_uses();
let result = analyze_function(&ir);
let pointees = result.get(merged.as_u32());
assert!(
pointees.contains(&(LOCAL_LOC_BASE + 5)),
"the phi must inherit the true arm's pointee; got {pointees:?}"
);
assert!(
pointees.contains(&(LOCAL_LOC_BASE + 9)),
"and the false arm's; got {pointees:?}"
);
}
#[test]
fn an_unmodelled_definition_points_to_unknown() {
let mut ir: SsaFunction<MockTarget> = SsaFunction::new(0, 2);
let a = ir.create_variable(
VariableOrigin::Local(0),
0,
DefSite::instruction(0, 0),
MockType::Ptr,
);
let b = ir.create_variable(
VariableOrigin::Local(1),
0,
DefSite::instruction(0, 1),
MockType::Ptr,
);
let sum = ir.create_variable(
VariableOrigin::Local(2),
0,
DefSite::instruction(0, 2),
MockType::Ptr,
);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::LoadLocalAddr {
dest: a,
local_index: 1,
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::LoadLocalAddr {
dest: b,
local_index: 2,
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Add {
dest: sum,
left: a,
right: b,
flags: None,
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: None }));
ir.add_block(block);
ir.recompute_uses();
let result = analyze_function(&ir);
assert!(
result.get(sum.as_u32()).contains(&UNKNOWN_LOC),
"an unmodelled pointer definition must be unconstrained, not empty"
);
}
#[test]
fn copy_propagates_address_of() {
let pts = solve(&[
Constraint::AddressOf { ptr: 1, target: 10 },
Constraint::Copy { dst: 2, src: 1 },
]);
assert!(pts.get(2).contains(&10));
}
#[test]
fn load_dereferences_pointer() {
let pts = solve(&[
Constraint::AddressOf { ptr: 1, target: 20 }, Constraint::AddressOf {
ptr: 20,
target: 30,
}, Constraint::Load { dst: 2, ptr: 1 }, ]);
assert!(pts.get(2).contains(&30));
}
#[test]
fn store_writes_through_pointer() {
let pts = solve(&[
Constraint::AddressOf { ptr: 1, target: 20 }, Constraint::AddressOf { ptr: 3, target: 30 }, Constraint::Store { ptr: 1, src: 3 }, ]);
assert!(pts.get(20).contains(&30));
}
#[test]
fn deep_copy_chain_converges() {
let mut constraints = vec![Constraint::AddressOf {
ptr: 0,
target: 500,
}];
for dst in 1u32..=64 {
constraints.push(Constraint::Copy {
dst,
src: dst.saturating_sub(1),
});
}
let pts = solve(&constraints);
assert!(pts.get(64).contains(&500));
assert!(pts.get(32).contains(&500));
}
#[test]
fn large_chain_stays_within_step_budget_without_truncation() {
let mut constraints = vec![Constraint::AddressOf {
ptr: 0,
target: 777,
}];
for dst in 1u32..=2000 {
constraints.push(Constraint::Copy {
dst,
src: dst.saturating_sub(1),
});
}
let pts = solve(&constraints);
assert!(
pts.get(2000).contains(&777),
"tail must resolve the address"
);
assert!(pts.get(1000).contains(&777));
}
#[test]
fn store_load_through_pointer_cycle_converges() {
let pts = solve(&[
Constraint::AddressOf {
ptr: 1,
target: 100,
}, Constraint::AddressOf {
ptr: 2,
target: 100,
}, Constraint::Store { ptr: 1, src: 2 }, Constraint::Load { dst: 3, ptr: 1 }, ]);
assert!(pts.get(100).contains(&100));
assert!(pts.get(3).contains(&100));
}
#[test]
fn transitive_copy_chain_converges() {
let pts = solve(&[
Constraint::AddressOf { ptr: 1, target: 99 },
Constraint::Copy { dst: 2, src: 1 },
Constraint::Copy { dst: 3, src: 2 },
]);
assert!(pts.get(3).contains(&99));
assert!(pts.get(4).is_empty());
}
#[test]
fn extracts_local_address_through_copy() {
let mut ir = SsaFunction::<MockTarget>::with_capacity(0, 0, 1, 2);
let p = ptr_var(&mut ir, VariableOrigin::Local(0), 0, 0);
let q = ptr_var(&mut ir, VariableOrigin::Local(1), 0, 1);
let mut block = SsaBlock::with_capacity(0, 0, 2);
block.add_instruction(instr(SsaOp::LoadLocalAddr {
dest: p,
local_index: 5,
}));
block.add_instruction(instr(SsaOp::Copy { dest: q, src: p }));
ir.add_block(block);
ir.recompute_uses();
let pts = analyze_function(&ir);
let local5 = LOCAL_LOC_BASE.saturating_add(5);
assert!(pts.get(p.as_u32()).contains(&local5));
assert!(pts.get(q.as_u32()).contains(&local5));
}
#[test]
fn distinct_fields_do_not_alias_but_resolve_through_object() {
let mut ir = SsaFunction::<MockTarget>::with_capacity(0, 0, 1, 4);
let object = ptr_var(&mut ir, VariableOrigin::Local(0), 0, 0);
let field_a = ptr_var(&mut ir, VariableOrigin::Local(1), 0, 1);
let field_b = ptr_var(&mut ir, VariableOrigin::Local(2), 0, 2);
let mut block = SsaBlock::with_capacity(0, 0, 3);
block.add_instruction(instr(SsaOp::LoadLocalAddr {
dest: object,
local_index: 3,
}));
block.add_instruction(instr(SsaOp::LoadFieldAddr {
dest: field_a,
object,
field: 0,
}));
block.add_instruction(instr(SsaOp::LoadFieldAddr {
dest: field_b,
object,
field: 1,
}));
ir.add_block(block);
ir.recompute_uses();
let pts = analyze_function(&ir);
let local3 = LOCAL_LOC_BASE.saturating_add(3);
let a_set = pts.get(field_a.as_u32());
let b_set = pts.get(field_b.as_u32());
assert!(a_set.contains(&local3), "&o.a resolves through o");
assert!(b_set.contains(&local3), "&o.b resolves through o");
assert_ne!(a_set, b_set, "distinct fields must not alias");
let a_only: BTreeSet<Loc> = a_set.difference(b_set).copied().collect();
let b_only: BTreeSet<Loc> = b_set.difference(a_set).copied().collect();
assert!(
a_only.iter().all(|loc| *loc >= FIELD_LOC_BASE),
"&o.a's exclusive cell is a field cell"
);
assert!(
b_only.iter().all(|loc| *loc >= FIELD_LOC_BASE),
"&o.b's exclusive cell is a field cell"
);
assert!(!a_only.is_empty() && !b_only.is_empty());
}
#[test]
fn unindexed_field_falls_back_to_object_alias() {
let mut ir = SsaFunction::<MockTarget>::with_capacity(0, 0, 1, 2);
let object = ptr_var(&mut ir, VariableOrigin::Local(0), 0, 0);
let field_dest = ptr_var(&mut ir, VariableOrigin::Local(1), 0, 1);
let mut block = SsaBlock::with_capacity(0, 0, 2);
block.add_instruction(instr(SsaOp::LoadLocalAddr {
dest: object,
local_index: 7,
}));
block.add_instruction(instr(SsaOp::LoadFieldAddr {
dest: field_dest,
object,
field: MOCK_FIELD_UNRESOLVED,
}));
ir.add_block(block);
ir.recompute_uses();
let pts = analyze_function(&ir);
let local7 = LOCAL_LOC_BASE.saturating_add(7);
let set = pts.get(field_dest.as_u32());
assert!(set.contains(&local7));
assert!(set.iter().all(|loc| *loc < FIELD_LOC_BASE));
}
}