use std::collections::HashMap;
use petgraph::graph::NodeIndex;
use super::code_graph::CodeGraph;
use super::constant_prop::{ConstEnv, ConstValue};
#[derive(Debug)]
pub struct InterproceduralConstPropResult {
pub function_const_envs: HashMap<NodeIndex, ConstEnv>,
pub dead_branches: Vec<InterproceduralDeadBranch>,
}
#[derive(Debug, Clone)]
pub struct InterproceduralDeadBranch {
pub function: NodeIndex,
pub parameter: String,
pub constant_value: ConstValue,
pub description: String,
}
#[derive(Debug, Clone)]
pub struct CallSiteArgument {
pub callee: NodeIndex,
pub param_index: usize,
pub param_name: String,
pub value: ConstValue,
}
const MAX_ITERATIONS: usize = 3;
pub fn analyze_interprocedural_constants(graph: &CodeGraph) -> InterproceduralConstPropResult {
let mut envs: HashMap<NodeIndex, ConstEnv> = HashMap::new();
for (idx, _node) in graph.nodes() {
envs.insert(idx, ConstEnv::new());
}
let edges: Vec<(NodeIndex, NodeIndex)> = graph
.edges_with_endpoints()
.map(|(src, tgt, _)| (src, tgt))
.collect();
for _iter in 0..MAX_ITERATIONS {
let mut changed = false;
for &(caller_idx, callee_idx) in &edges {
let caller_env = envs.get(&caller_idx).cloned().unwrap_or_default();
let callee_env = envs.entry(callee_idx).or_default();
let new_env = callee_env.meet(&caller_env);
if new_env != *callee_env {
*callee_env = new_env;
changed = true;
}
}
if !changed {
break;
}
}
let dead_branches = detect_interprocedural_dead_branches(graph, &envs);
InterproceduralConstPropResult {
function_const_envs: envs,
dead_branches,
}
}
pub fn analyze_interprocedural_constants_with_args(
graph: &CodeGraph,
call_site_args: &[CallSiteArgument],
) -> InterproceduralConstPropResult {
let mut envs: HashMap<NodeIndex, ConstEnv> = HashMap::new();
for (idx, _node) in graph.nodes() {
envs.insert(idx, ConstEnv::new());
}
for arg in call_site_args {
let env = envs.entry(arg.callee).or_default();
let current = env.get(&arg.param_name);
let met = current.meet(&arg.value);
env.set(arg.param_name.clone(), met);
}
let edges: Vec<(NodeIndex, NodeIndex)> = graph
.edges_with_endpoints()
.map(|(src, tgt, _)| (src, tgt))
.collect();
for _iter in 0..MAX_ITERATIONS {
let mut changed = false;
for &(caller_idx, callee_idx) in &edges {
let caller_env = envs.get(&caller_idx).cloned().unwrap_or_default();
let callee_env = envs.entry(callee_idx).or_default();
let new_env = callee_env.meet(&caller_env);
if new_env != *callee_env {
*callee_env = new_env;
changed = true;
}
}
if !changed {
break;
}
}
let dead_branches = detect_interprocedural_dead_branches(graph, &envs);
InterproceduralConstPropResult {
function_const_envs: envs,
dead_branches,
}
}
fn detect_interprocedural_dead_branches(
graph: &CodeGraph,
envs: &HashMap<NodeIndex, ConstEnv>,
) -> Vec<InterproceduralDeadBranch> {
let mut dead = Vec::new();
for (idx, node) in graph.nodes() {
let env = match envs.get(&idx) {
Some(e) => e,
None => continue,
};
for (var, val) in &env.bindings {
if let Some(truthy) = val.is_truthy() {
let direction = if truthy {
"always true"
} else {
"always false"
};
dead.push(InterproceduralDeadBranch {
function: idx,
parameter: var.clone(),
constant_value: val.clone(),
description: format!(
"In function '{}', parameter '{}' is {} ({}) from all callers",
node.name,
var,
direction,
format_const(val),
),
});
}
}
}
dead
}
fn format_const(val: &ConstValue) -> String {
match val {
ConstValue::Constant(n) => format!("{}", n),
ConstValue::StringConst(s) => format!("\"{}\"", s),
ConstValue::BoolConst(b) => format!("{}", b),
ConstValue::Top => "top".to_string(),
ConstValue::Bottom => "bottom".to_string(),
}
}
pub fn inject_call_site_constants(
envs: &mut HashMap<NodeIndex, ConstEnv>,
args: &[CallSiteArgument],
) {
for arg in args {
let env = envs.entry(arg.callee).or_default();
let current = env.get(&arg.param_name);
let met = current.meet(&arg.value);
env.set(arg.param_name.clone(), met);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{CodeNode, Language, NodeKind, SourceLocation, Visibility};
use crate::graph::code_graph::CodeGraph;
use crate::graph::constant_prop::{ConstEnv, ConstValue};
fn make_node(name: &str) -> CodeNode {
CodeNode::new(
name.to_string(),
NodeKind::Function,
SourceLocation::new("test.rs".to_string(), 1, 10, 0, 0),
Language::Rust,
Visibility::Public,
)
}
#[test]
fn test_empty_graph() {
let graph = CodeGraph::new();
let result = analyze_interprocedural_constants(&graph);
assert!(result.function_const_envs.is_empty());
assert!(result.dead_branches.is_empty());
}
#[test]
fn test_single_node_no_callers() {
let mut graph = CodeGraph::new();
let _idx = graph.add_node(make_node("main"));
let result = analyze_interprocedural_constants(&graph);
assert_eq!(result.function_const_envs.len(), 1);
assert!(result.dead_branches.is_empty());
}
#[test]
fn test_constant_propagation_single_caller() {
let mut graph = CodeGraph::new();
let caller = make_node("caller");
let callee = make_node("callee");
let caller_idx = graph.add_node(caller);
let callee_idx = graph.add_node(callee);
graph.add_edge_by_index(caller_idx, callee_idx);
let args = vec![CallSiteArgument {
callee: callee_idx,
param_index: 0,
param_name: "x".to_string(),
value: ConstValue::Constant(42),
}];
let result = analyze_interprocedural_constants_with_args(&graph, &args);
let callee_env = result.function_const_envs.get(&callee_idx).unwrap();
assert_eq!(callee_env.get("x"), ConstValue::Constant(42));
}
#[test]
fn test_conflicting_callers_produce_bottom() {
let mut graph = CodeGraph::new();
let caller_a = make_node("caller_a");
let caller_b = make_node("caller_b");
let callee = make_node("callee");
let caller_a_idx = graph.add_node(caller_a);
let caller_b_idx = graph.add_node(caller_b);
let callee_idx = graph.add_node(callee);
graph.add_edge_by_index(caller_a_idx, callee_idx);
graph.add_edge_by_index(caller_b_idx, callee_idx);
let args = vec![
CallSiteArgument {
callee: callee_idx,
param_index: 0,
param_name: "x".to_string(),
value: ConstValue::Constant(1),
},
CallSiteArgument {
callee: callee_idx,
param_index: 0,
param_name: "x".to_string(),
value: ConstValue::Constant(2),
},
];
let result = analyze_interprocedural_constants_with_args(&graph, &args);
let callee_env = result.function_const_envs.get(&callee_idx).unwrap();
assert_eq!(callee_env.get("x"), ConstValue::Bottom);
}
#[test]
fn test_agreeing_callers_propagate() {
let mut graph = CodeGraph::new();
let caller_a = make_node("caller_a");
let caller_b = make_node("caller_b");
let callee = make_node("callee");
let caller_a_idx = graph.add_node(caller_a);
let caller_b_idx = graph.add_node(caller_b);
let callee_idx = graph.add_node(callee);
graph.add_edge_by_index(caller_a_idx, callee_idx);
graph.add_edge_by_index(caller_b_idx, callee_idx);
let args = vec![
CallSiteArgument {
callee: callee_idx,
param_index: 0,
param_name: "x".to_string(),
value: ConstValue::Constant(99),
},
CallSiteArgument {
callee: callee_idx,
param_index: 0,
param_name: "x".to_string(),
value: ConstValue::Constant(99),
},
];
let result = analyze_interprocedural_constants_with_args(&graph, &args);
let callee_env = result.function_const_envs.get(&callee_idx).unwrap();
assert_eq!(callee_env.get("x"), ConstValue::Constant(99));
}
#[test]
fn test_dead_branch_detected_from_constant_param() {
let mut graph = CodeGraph::new();
let caller = make_node("caller");
let callee = make_node("callee");
let caller_idx = graph.add_node(caller);
let callee_idx = graph.add_node(callee);
graph.add_edge_by_index(caller_idx, callee_idx);
let args = vec![CallSiteArgument {
callee: callee_idx,
param_index: 0,
param_name: "debug".to_string(),
value: ConstValue::BoolConst(false),
}];
let result = analyze_interprocedural_constants_with_args(&graph, &args);
assert!(
!result.dead_branches.is_empty(),
"Expected at least one dead branch"
);
let db = result
.dead_branches
.iter()
.find(|d| d.parameter == "debug")
.expect("Expected dead branch for parameter 'debug'");
assert_eq!(db.function, callee_idx);
assert_eq!(db.constant_value, ConstValue::BoolConst(false));
assert!(db.description.contains("always false"));
}
#[test]
fn test_chain_propagation() {
let mut graph = CodeGraph::new();
let a = make_node("a");
let b = make_node("b");
let c = make_node("c");
let a_idx = graph.add_node(a);
let b_idx = graph.add_node(b);
let c_idx = graph.add_node(c);
graph.add_edge_by_index(a_idx, b_idx);
graph.add_edge_by_index(b_idx, c_idx);
let args = vec![
CallSiteArgument {
callee: b_idx,
param_index: 0,
param_name: "x".to_string(),
value: ConstValue::Constant(10),
},
CallSiteArgument {
callee: c_idx,
param_index: 0,
param_name: "x".to_string(),
value: ConstValue::Constant(10),
},
];
let result = analyze_interprocedural_constants_with_args(&graph, &args);
let b_env = result.function_const_envs.get(&b_idx).unwrap();
assert_eq!(b_env.get("x"), ConstValue::Constant(10));
let c_env = result.function_const_envs.get(&c_idx).unwrap();
assert_eq!(c_env.get("x"), ConstValue::Constant(10));
}
#[test]
fn test_inject_call_site_constants() {
let mut envs: HashMap<NodeIndex, ConstEnv> = HashMap::new();
let idx = NodeIndex::new(0);
envs.insert(idx, ConstEnv::new());
let args = vec![
CallSiteArgument {
callee: idx,
param_index: 0,
param_name: "y".to_string(),
value: ConstValue::Constant(7),
},
CallSiteArgument {
callee: idx,
param_index: 0,
param_name: "y".to_string(),
value: ConstValue::Constant(7),
},
];
inject_call_site_constants(&mut envs, &args);
let env = envs.get(&idx).unwrap();
assert_eq!(env.get("y"), ConstValue::Constant(7));
}
}