use std::collections::{HashMap, HashSet};
use smallvec::SmallVec;
use crate::summary::points_to::{AliasKind, AliasPosition, PointsToSummary};
use crate::symbol::Lang;
use super::ir::{SsaBody, SsaOp, SsaValue, Terminator};
fn build_op_map(ssa: &SsaBody) -> HashMap<SsaValue, SsaOp> {
let mut map = HashMap::with_capacity(ssa.num_values());
for block in &ssa.blocks {
for inst in block.phis.iter().chain(block.body.iter()) {
map.insert(inst.value, inst.op.clone());
}
}
map
}
fn build_var_name_map(ssa: &SsaBody) -> HashMap<SsaValue, Option<String>> {
let mut map = HashMap::with_capacity(ssa.num_values());
for block in &ssa.blocks {
for inst in block.phis.iter().chain(block.body.iter()) {
map.insert(inst.value, inst.var_name.clone());
}
}
map
}
#[derive(Clone, Debug)]
struct ParamHit {
ssa_index: usize,
var_name: Option<String>,
}
fn trace_to_param_hit(
v: SsaValue,
op_map: &HashMap<SsaValue, SsaOp>,
var_names: &HashMap<SsaValue, Option<String>>,
visited: &mut HashSet<SsaValue>,
) -> Option<ParamHit> {
if !visited.insert(v) {
return None;
}
match op_map.get(&v)? {
SsaOp::Param { index } => Some(ParamHit {
ssa_index: *index,
var_name: var_names.get(&v).cloned().flatten(),
}),
SsaOp::Assign(uses) => {
for u in uses {
if let Some(hit) = trace_to_param_hit(*u, op_map, var_names, visited) {
return Some(hit);
}
}
None
}
SsaOp::Phi(operands) => {
for (_, pv) in operands {
if let Some(hit) = trace_to_param_hit(*pv, op_map, var_names, visited) {
return Some(hit);
}
}
None
}
_ => None,
}
}
fn param_hit_to_formal_index(hit: &ParamHit, params_by_name: &HashMap<String, usize>) -> usize {
if let Some(name) = &hit.var_name
&& let Some(&idx) = params_by_name.get(name)
{
return idx;
}
hit.ssa_index
}
fn base_of_path(name: &str) -> &str {
let dot = name.find('.');
let bracket = name.find('[');
let end = match (dot, bracket) {
(Some(d), Some(b)) => d.min(b),
(Some(d), None) => d,
(None, Some(b)) => b,
(None, None) => return name,
};
&name[..end]
}
fn is_receiver_name_local(name: &str) -> bool {
matches!(name, "self" | "this")
}
fn trace_to_fresh_alloc(
v: SsaValue,
op_map: &HashMap<SsaValue, SsaOp>,
lang: Option<Lang>,
visited: &mut HashSet<SsaValue>,
) -> bool {
if !visited.insert(v) {
return false;
}
let Some(op) = op_map.get(&v) else {
return false;
};
match op {
SsaOp::Const(Some(text)) => crate::ssa::heap::is_container_literal_public(text),
SsaOp::Call { callee, .. } => lang
.map(|l| crate::ssa::heap::is_container_constructor(callee, l))
.unwrap_or(false),
SsaOp::Assign(uses) => uses
.iter()
.any(|u| trace_to_fresh_alloc(*u, op_map, lang, visited)),
SsaOp::Phi(operands) => operands
.iter()
.any(|(_, pv)| trace_to_fresh_alloc(*pv, op_map, lang, visited)),
_ => false,
}
}
fn returns_fresh_allocation(
ssa: &SsaBody,
op_map: &HashMap<SsaValue, SsaOp>,
lang: Option<Lang>,
) -> bool {
for block in &ssa.blocks {
let Terminator::Return(Some(v)) = block.terminator else {
continue;
};
let mut visited = HashSet::new();
if trace_to_fresh_alloc(v, op_map, lang, &mut visited) {
return true;
}
}
false
}
pub fn analyse_param_points_to(
ssa: &SsaBody,
param_info: &[(usize, String, SsaValue)],
formal_param_count: usize,
formal_param_names: Option<&[String]>,
lang: Option<Lang>,
) -> PointsToSummary {
let mut summary = PointsToSummary::empty();
let op_map = build_op_map(ssa);
let var_names = build_var_name_map(ssa);
if returns_fresh_allocation(ssa, &op_map, lang) {
summary.returns_fresh_alloc = true;
}
if formal_param_count == 0 {
return summary;
}
let mut params_by_name: HashMap<String, usize> = HashMap::new();
if let Some(names) = formal_param_names {
let mut pos: usize = 0;
for name in names {
if is_receiver_name_local(name) {
continue;
}
if pos >= formal_param_count {
break;
}
params_by_name.insert(name.clone(), pos);
pos += 1;
}
}
if formal_param_names.is_none() {
for (idx, name, _) in param_info {
params_by_name.insert(name.clone(), *idx);
}
}
for block in &ssa.blocks {
for inst in block.body.iter() {
let SsaOp::Assign(uses) = &inst.op else {
continue;
};
let Some(name) = inst.var_name.as_ref() else {
continue;
};
if !name.contains('.') && !name.contains('[') {
continue;
}
let base = base_of_path(name);
let Some(&target_idx) = params_by_name.get(base) else {
continue;
};
if target_idx >= formal_param_count {
continue;
}
for u in uses {
let mut visited = HashSet::new();
let Some(hit) = trace_to_param_hit(*u, &op_map, &var_names, &mut visited) else {
continue;
};
let src_idx = param_hit_to_formal_index(&hit, ¶ms_by_name);
if src_idx >= formal_param_count {
continue;
}
if src_idx == target_idx {
continue;
}
summary.insert(
AliasPosition::Param(src_idx as u32),
AliasPosition::Param(target_idx as u32),
AliasKind::MayAlias,
);
if summary.overflow {
return summary;
}
}
}
}
let mut return_param_indices: SmallVec<[usize; 4]> = SmallVec::new();
for block in &ssa.blocks {
let Terminator::Return(Some(v)) = block.terminator else {
continue;
};
let mut visited = HashSet::new();
if let Some(hit) = trace_to_param_hit(v, &op_map, &var_names, &mut visited) {
let idx = param_hit_to_formal_index(&hit, ¶ms_by_name);
if idx < formal_param_count && !return_param_indices.contains(&idx) {
return_param_indices.push(idx);
}
}
}
for idx in return_param_indices {
summary.insert(
AliasPosition::Param(idx as u32),
AliasPosition::Return,
AliasKind::MayAlias,
);
if summary.overflow {
return summary;
}
}
summary
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ssa::ir::{BlockId, SsaBlock, SsaInst};
use petgraph::graph::NodeIndex;
use smallvec::smallvec;
fn mk_body(blocks: Vec<SsaBlock>, num_values: u32) -> SsaBody {
use crate::ssa::ir::ValueDef;
let value_defs = (0..num_values)
.map(|_| ValueDef {
var_name: None,
cfg_node: NodeIndex::new(0),
block: BlockId(0),
})
.collect();
SsaBody {
blocks,
entry: BlockId(0),
value_defs,
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
}
}
fn inst(v: u32, op: SsaOp, var_name: Option<&str>) -> SsaInst {
SsaInst {
value: SsaValue(v),
op,
cfg_node: NodeIndex::new(0),
var_name: var_name.map(String::from),
span: (0, 0),
}
}
#[test]
fn field_write_param_to_param_emits_edge() {
let block = SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![
inst(0, SsaOp::Param { index: 0 }, Some("a")),
inst(1, SsaOp::Param { index: 1 }, Some("b")),
inst(2, SsaOp::Assign(smallvec![SsaValue(0)]), Some("b.data")),
inst(3, SsaOp::Assign(smallvec![SsaValue(2)]), Some("b")),
],
terminator: Terminator::Return(None),
preds: smallvec![],
succs: smallvec![],
};
let body = mk_body(vec![block], 4);
let pinfo = vec![
(0usize, "a".to_string(), SsaValue(0)),
(1usize, "b".to_string(), SsaValue(1)),
];
let s = analyse_param_points_to(&body, &pinfo, 2, None, None);
assert!(!s.overflow, "unexpected overflow: {s:?}");
assert!(
s.edges.iter().any(|e| e.source == AliasPosition::Param(0)
&& e.target == AliasPosition::Param(1)
&& e.kind == AliasKind::MayAlias),
"expected Param(0) → Param(1) edge, got {s:?}"
);
}
#[test]
fn return_alias_emits_edge() {
let block = SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![inst(0, SsaOp::Param { index: 0 }, Some("a"))],
terminator: Terminator::Return(Some(SsaValue(0))),
preds: smallvec![],
succs: smallvec![],
};
let body = mk_body(vec![block], 1);
let pinfo = vec![(0usize, "a".to_string(), SsaValue(0))];
let s = analyse_param_points_to(&body, &pinfo, 1, None, None);
assert!(!s.overflow);
assert_eq!(s.edges.len(), 1);
assert_eq!(s.edges[0].source, AliasPosition::Param(0));
assert_eq!(s.edges[0].target, AliasPosition::Return);
}
#[test]
fn self_alias_is_dropped() {
let block = SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![
inst(0, SsaOp::Param { index: 0 }, Some("b")),
inst(1, SsaOp::Assign(smallvec![SsaValue(0)]), Some("b.x")),
inst(2, SsaOp::Assign(smallvec![SsaValue(1)]), Some("b.data")),
],
terminator: Terminator::Return(None),
preds: smallvec![],
succs: smallvec![],
};
let body = mk_body(vec![block], 3);
let pinfo = vec![(0usize, "b".to_string(), SsaValue(0))];
let s = analyse_param_points_to(&body, &pinfo, 1, None, None);
assert!(
s.is_empty(),
"self-alias edges should not be emitted: {s:?}"
);
}
#[test]
fn out_of_range_param_rejected() {
let block = SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![
inst(0, SsaOp::Param { index: 5 }, Some("capture")),
inst(1, SsaOp::Param { index: 1 }, Some("b")),
inst(2, SsaOp::Assign(smallvec![SsaValue(0)]), Some("b.data")),
],
terminator: Terminator::Return(None),
preds: smallvec![],
succs: smallvec![],
};
let body = mk_body(vec![block], 3);
let pinfo = vec![
(5usize, "capture".to_string(), SsaValue(0)),
(1usize, "b".to_string(), SsaValue(1)),
];
let s = analyse_param_points_to(&body, &pinfo, 2, None, None);
assert!(
s.is_empty(),
"synthetic captures past formal arity must not emit edges: {s:?}"
);
}
#[test]
fn bounded_graph_overflows_at_cap() {
let n = (crate::summary::points_to::MAX_ALIAS_EDGES + 2) as u32;
let mut insts = Vec::new();
let mut phi_operands: SmallVec<[(BlockId, SsaValue); 2]> = SmallVec::new();
for i in 0..n {
insts.push(inst(
i,
SsaOp::Param { index: i as usize },
Some(&format!("p{i}")),
));
phi_operands.push((BlockId(0), SsaValue(i)));
}
let phi_v = n;
insts.push(inst(phi_v, SsaOp::Phi(phi_operands), Some("ret")));
let block = SsaBlock {
id: BlockId(0),
phis: vec![],
body: insts,
terminator: Terminator::Return(Some(SsaValue(phi_v))),
preds: smallvec![],
succs: smallvec![],
};
let body = mk_body(vec![block], n + 1);
let pinfo: Vec<(usize, String, SsaValue)> = (0..n as usize)
.map(|i| (i, format!("p{i}"), SsaValue(i as u32)))
.collect();
let s = analyse_param_points_to(&body, &pinfo, n as usize, None, None);
assert!(!s.overflow);
assert_eq!(s.edges.len(), 1);
}
#[test]
fn fresh_container_literal_return_sets_flag() {
let block = SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![inst(0, SsaOp::Const(Some("[]".to_string())), None)],
terminator: Terminator::Return(Some(SsaValue(0))),
preds: smallvec![],
succs: smallvec![],
};
let body = mk_body(vec![block], 1);
let s = analyse_param_points_to(&body, &[], 0, None, Some(Lang::JavaScript));
assert!(s.returns_fresh_alloc);
assert!(s.edges.is_empty());
}
#[test]
fn constructor_return_sets_flag() {
let block = SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![inst(
0,
SsaOp::Call {
callee: "list".to_string(),
callee_text: None,
args: vec![],
receiver: None,
},
None,
)],
terminator: Terminator::Return(Some(SsaValue(0))),
preds: smallvec![],
succs: smallvec![],
};
let body = mk_body(vec![block], 1);
let s = analyse_param_points_to(&body, &[], 0, None, Some(Lang::Python));
assert!(s.returns_fresh_alloc);
}
#[test]
fn return_of_param_does_not_set_fresh_flag() {
let block = SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![inst(0, SsaOp::Param { index: 0 }, Some("a"))],
terminator: Terminator::Return(Some(SsaValue(0))),
preds: smallvec![],
succs: smallvec![],
};
let body = mk_body(vec![block], 1);
let pinfo = vec![(0usize, "a".to_string(), SsaValue(0))];
let s = analyse_param_points_to(&body, &pinfo, 1, None, Some(Lang::JavaScript));
assert!(
!s.returns_fresh_alloc,
"param-only return must not set fresh-alloc flag"
);
assert!(
s.edges
.iter()
.any(|e| e.source == AliasPosition::Param(0) && e.target == AliasPosition::Return),
"expected Param(0) → Return edge, got {s:?}"
);
}
}