#![allow(
clippy::collapsible_if,
clippy::if_same_then_else,
clippy::needless_range_loop,
clippy::only_used_in_recursion,
clippy::too_many_arguments,
clippy::type_complexity,
clippy::unnecessary_unwrap
)]
use crate::cfg::{Cfg, EdgeKind, StmtKind};
use petgraph::algo::dominators::{Dominators, simple_fast};
use petgraph::graph::NodeIndex;
use petgraph::prelude::*;
use petgraph::visit::EdgeRef;
use smallvec::SmallVec;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use super::ir::*;
#[allow(clippy::too_many_arguments)]
fn try_lower_field_proj_chain(
callee: &str,
var_stacks: &HashMap<String, Vec<SsaValue>>,
field_interner: &mut crate::ssa::ir::FieldInterner,
block_idx: usize,
block_id: BlockId,
next_value: &mut u32,
ssa_blocks: &mut [SsaBlock],
value_defs: &mut Vec<ValueDef>,
cfg_node: NodeIndex,
span: (usize, usize),
) -> Option<(SsaValue, String)> {
for ch in callee.chars() {
match ch {
'(' | ')' | '[' | ']' | '<' | '>' | '?' | '*' | '&' | ':' | ' ' | '\t' | '\n' | '-'
| '!' | ',' | ';' | '"' | '\'' | '\\' => return None,
_ => {}
}
}
let segments: Vec<&str> = callee.split('.').collect();
if segments.len() < 3 {
return None;
}
if segments.iter().any(|s| s.is_empty()) {
return None;
}
let base = segments[0];
let mut current = *var_stacks.get(base).and_then(|s| s.last())?;
let mut chain_var = base.to_string();
for field_name in &segments[1..segments.len() - 1] {
let fid = field_interner.intern(field_name);
let v = SsaValue(*next_value);
*next_value += 1;
chain_var.push('.');
chain_var.push_str(field_name);
ssa_blocks[block_idx].body.push(SsaInst {
value: v,
op: SsaOp::FieldProj {
receiver: current,
field: fid,
projected_type: None,
},
cfg_node,
var_name: Some(chain_var.clone()),
span,
});
value_defs.push(ValueDef {
var_name: Some(chain_var.clone()),
cfg_node,
block: block_id,
});
current = v;
}
let method = segments.last().unwrap().to_string();
Some((current, method))
}
pub fn lower_to_ssa(
cfg: &Cfg,
entry: NodeIndex,
scope: Option<&str>,
scope_all: bool,
) -> Result<SsaBody, SsaError> {
lower_to_ssa_inner(cfg, entry, scope, scope_all, false, &[], false)
}
pub fn lower_to_ssa_with_params(
cfg: &Cfg,
entry: NodeIndex,
scope: Option<&str>,
scope_all: bool,
formal_params: &[String],
) -> Result<SsaBody, SsaError> {
lower_to_ssa_inner(cfg, entry, scope, scope_all, false, formal_params, true)
}
pub fn lower_to_ssa_scoped_nop(
cfg: &Cfg,
entry: NodeIndex,
scope: Option<&str>,
) -> Result<SsaBody, SsaError> {
lower_to_ssa_inner(cfg, entry, scope, false, true, &[], false)
}
fn lower_to_ssa_inner(
cfg: &Cfg,
entry: NodeIndex,
scope: Option<&str>,
scope_all: bool,
scope_nop: bool,
formal_params: &[String],
with_params: bool,
) -> Result<SsaBody, SsaError> {
if cfg.node_count() == 0 {
return Err(SsaError::EmptyCfg);
}
let traverse_all = scope_all || scope_nop;
let (reachable, filtered_edges, raw_exception_edges) =
collect_reachable(cfg, entry, scope, traverse_all);
let nop_nodes: HashSet<NodeIndex> = if scope_nop {
let in_scope = |node: NodeIndex| -> bool {
let info = &cfg[node];
match scope {
None => info.ast.enclosing_func.is_none(),
Some(name) => info.ast.enclosing_func.as_deref() == Some(name),
}
};
reachable
.iter()
.filter(|&&n| !in_scope(n) && !matches!(cfg[n].kind, StmtKind::Entry | StmtKind::Exit))
.copied()
.collect()
} else {
HashSet::new()
};
if reachable.is_empty() {
return Err(SsaError::EmptyCfg);
}
let (blocks_nodes, block_of_node, block_succs, block_preds) =
form_blocks(cfg, entry, &reachable, &filtered_edges);
let num_blocks = blocks_nodes.len();
if num_blocks == 0 {
return Err(SsaError::EmptyCfg);
}
let (block_graph, block_graph_entry) = build_block_graph(num_blocks, &block_succs, BlockId(0));
let doms = simple_fast(&block_graph, block_graph_entry);
let dom_frontiers = compute_dominance_frontiers(num_blocks, &block_preds, &doms, &block_graph);
let mut var_defs = collect_var_defs(cfg, &blocks_nodes, &nop_nodes);
let external_vars = if scope.is_some() && !scope_all && !scope_nop {
let raw = identify_external_uses(cfg, &blocks_nodes, &var_defs);
reorder_external_vars(raw, formal_params)
} else {
vec![]
};
for var in &external_vars {
var_defs.entry(var.clone()).or_default().insert(0);
}
let phi_placements = insert_phis(&var_defs, &dom_frontiers, num_blocks);
let dom_tree_children = build_dom_tree_children(num_blocks, &doms, &block_graph);
let (
mut ssa_blocks,
mut value_defs,
cfg_node_map,
field_interner,
field_writes,
synthetic_externals,
) = rename_variables(
cfg,
&blocks_nodes,
&block_succs,
&block_preds,
&phi_placements,
&dom_tree_children,
&filtered_edges,
&external_vars,
formal_params,
with_params,
&nop_nodes,
);
fill_undef_phi_operands(
&mut ssa_blocks,
&block_preds,
&mut value_defs,
&blocks_nodes,
);
for bid in 0..num_blocks {
let id = BlockId(bid as u32);
ssa_blocks[bid].id = id;
ssa_blocks[bid].preds = block_preds[bid]
.iter()
.map(|&b| BlockId(b as u32))
.collect();
ssa_blocks[bid].succs = block_succs[bid]
.iter()
.map(|&b| BlockId(b as u32))
.collect();
}
debug_assert_bfs_ordering(&block_preds);
assert_phi_operand_counts(&ssa_blocks, &block_preds);
let exception_edges: Vec<(BlockId, BlockId)> = raw_exception_edges
.iter()
.filter_map(|(src_node, catch_node)| {
let src_block = block_of_node.get(src_node)?;
let catch_block = block_of_node.get(catch_node)?;
Some((BlockId(*src_block as u32), BlockId(*catch_block as u32)))
})
.collect();
let body = SsaBody {
blocks: ssa_blocks,
entry: BlockId(0),
value_defs,
cfg_node_map,
exception_edges,
field_interner,
field_writes,
synthetic_externals,
};
check_catch_block_reachability_gated(&body);
Ok(body)
}
fn check_catch_block_reachability_gated(body: &SsaBody) {
let result = super::invariants::check_catch_block_reachability(body);
if let Err(err) = result {
#[cfg(debug_assertions)]
{
if !catch_invariant_do_not_panic() {
panic!(
"SSA catch-block reachability invariant violated:\n{}",
err.joined()
);
}
}
tracing::warn!(
violations = %err.joined(),
"SSA catch-block reachability invariant violated; proceeding with \
conservative orphan fallback"
);
crate::taint::ssa_transfer::record_engine_note(
crate::engine_notes::EngineNote::SsaLoweringBailed {
reason: format!("catch_block_orphan: {}", err.joined()),
},
);
}
}
#[cfg(debug_assertions)]
thread_local! {
static CATCH_INVARIANT_DO_NOT_PANIC: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(debug_assertions)]
#[allow(dead_code)]
pub(crate) fn set_catch_invariant_do_not_panic(on: bool) {
CATCH_INVARIANT_DO_NOT_PANIC.with(|c| c.set(on));
}
#[cfg(debug_assertions)]
fn catch_invariant_do_not_panic() -> bool {
CATCH_INVARIANT_DO_NOT_PANIC.with(|c| c.get())
}
fn collect_reachable(
cfg: &Cfg,
entry: NodeIndex,
scope: Option<&str>,
scope_all: bool,
) -> (
HashSet<NodeIndex>,
Vec<(NodeIndex, NodeIndex, EdgeKind)>,
Vec<(NodeIndex, NodeIndex)>,
) {
let mut reachable = HashSet::new();
let mut edges = Vec::new();
let mut exception_edges = Vec::new();
let mut queue = VecDeque::new();
let in_scope = |node: NodeIndex| -> bool {
if scope_all {
return true;
}
let info = &cfg[node];
match scope {
None => info.ast.enclosing_func.is_none(),
Some(name) => info.ast.enclosing_func.as_deref() == Some(name),
}
};
if !in_scope(entry) && !scope_all {
if !matches!(cfg[entry].kind, StmtKind::Entry | StmtKind::Exit) {
return (reachable, edges, exception_edges);
}
}
reachable.insert(entry);
queue.push_back(entry);
while let Some(node) = queue.pop_front() {
for edge in cfg.edges(node) {
let kind = *edge.weight();
let target = edge.target();
if matches!(kind, EdgeKind::Exception) {
if (in_scope(target)
|| matches!(cfg[target].kind, StmtKind::Entry | StmtKind::Exit))
&& reachable.insert(target)
{
queue.push_back(target);
}
exception_edges.push((node, target));
continue;
}
if !in_scope(target) && !matches!(cfg[target].kind, StmtKind::Entry | StmtKind::Exit) {
continue;
}
edges.push((node, target, kind));
if reachable.insert(target) {
queue.push_back(target);
}
}
}
(reachable, edges, exception_edges)
}
fn form_blocks(
cfg: &Cfg,
entry: NodeIndex,
reachable: &HashSet<NodeIndex>,
filtered_edges: &[(NodeIndex, NodeIndex, EdgeKind)],
) -> (
Vec<Vec<NodeIndex>>,
HashMap<NodeIndex, usize>,
Vec<Vec<usize>>,
Vec<Vec<usize>>,
) {
let mut successors: HashMap<NodeIndex, Vec<(NodeIndex, EdgeKind)>> = HashMap::new();
let mut in_degree: HashMap<NodeIndex, usize> = HashMap::new();
let mut has_branching_in: HashMap<NodeIndex, bool> = HashMap::new();
for node in reachable {
in_degree.entry(*node).or_insert(0);
has_branching_in.entry(*node).or_insert(false);
}
let is_terminating =
|n: NodeIndex| -> bool { matches!(cfg[n].kind, StmtKind::Return | StmtKind::Throw) };
for &(src, tgt, kind) in filtered_edges {
if is_terminating(src) {
continue;
}
successors.entry(src).or_default().push((tgt, kind));
*in_degree.entry(tgt).or_insert(0) += 1;
if matches!(kind, EdgeKind::True | EdgeKind::False | EdgeKind::Back) {
*has_branching_in.entry(tgt).or_insert(false) = true;
}
}
let mut is_leader: HashSet<NodeIndex> = HashSet::new();
is_leader.insert(entry);
for &node in reachable {
let in_deg = in_degree.get(&node).copied().unwrap_or(0);
if in_deg > 1 || has_branching_in.get(&node).copied().unwrap_or(false) {
is_leader.insert(node);
}
if in_deg == 0 && node != entry {
is_leader.insert(node);
}
let succs = successors.get(&node).map(|s| s.len()).unwrap_or(0);
if succs > 1 {
for &(tgt, _) in successors.get(&node).unwrap_or(&vec![]) {
is_leader.insert(tgt);
}
}
}
let mut blocks_nodes: Vec<Vec<NodeIndex>> = Vec::new();
let mut block_of_node: HashMap<NodeIndex, usize> = HashMap::new();
let mut visited: HashSet<NodeIndex> = HashSet::new();
let mut leader_queue: VecDeque<NodeIndex> = VecDeque::new();
leader_queue.push_back(entry);
let mut leader_visited: HashSet<NodeIndex> = HashSet::new();
leader_visited.insert(entry);
{
let mut bfs_queue: VecDeque<NodeIndex> = VecDeque::new();
let mut bfs_seen: HashSet<NodeIndex> = HashSet::new();
bfs_queue.push_back(entry);
bfs_seen.insert(entry);
while let Some(node) = bfs_queue.pop_front() {
if reachable.contains(&node) && is_leader.contains(&node) && leader_visited.insert(node)
{
leader_queue.push_back(node);
}
if is_terminating(node) {
continue;
}
for edge in cfg.edges(node) {
let tgt = edge.target();
if reachable.contains(&tgt) && bfs_seen.insert(tgt) {
bfs_queue.push_back(tgt);
}
}
}
let mut orphan_leaders: Vec<NodeIndex> = is_leader
.iter()
.copied()
.filter(|n| !leader_visited.contains(n))
.filter(|n| !matches!(cfg[*n].kind, StmtKind::Exit))
.collect();
orphan_leaders.sort_by_key(|n| n.index());
for n in orphan_leaders {
if leader_visited.insert(n) {
leader_queue.push_back(n);
}
}
}
for leader in leader_queue {
if visited.contains(&leader) {
continue;
}
let block_idx = blocks_nodes.len();
let mut block = vec![leader];
visited.insert(leader);
block_of_node.insert(leader, block_idx);
let mut current = leader;
loop {
let succs = successors.get(¤t).cloned().unwrap_or_default();
if succs.len() == 1
&& matches!(succs[0].1, EdgeKind::Seq)
&& !is_leader.contains(&succs[0].0)
{
let next = succs[0].0;
if visited.insert(next) {
block.push(next);
block_of_node.insert(next, block_idx);
current = next;
} else {
break;
}
} else {
break;
}
}
blocks_nodes.push(block);
}
let num_blocks = blocks_nodes.len();
let mut block_succs: Vec<Vec<usize>> = vec![vec![]; num_blocks];
let mut block_preds: Vec<Vec<usize>> = vec![vec![]; num_blocks];
for &(src, tgt, _kind) in filtered_edges {
if is_terminating(src) {
continue;
}
if let (Some(&src_blk), Some(&tgt_blk)) = (block_of_node.get(&src), block_of_node.get(&tgt))
{
if src_blk != tgt_blk && !block_succs[src_blk].contains(&tgt_blk) {
block_succs[src_blk].push(tgt_blk);
block_preds[tgt_blk].push(src_blk);
}
}
}
(blocks_nodes, block_of_node, block_succs, block_preds)
}
fn build_block_graph(
num_blocks: usize,
block_succs: &[Vec<usize>],
_entry: BlockId,
) -> (Graph<BlockId, ()>, NodeIndex) {
let mut g: Graph<BlockId, ()> = Graph::new();
let mut block_nodes: Vec<NodeIndex> = Vec::with_capacity(num_blocks);
for i in 0..num_blocks {
block_nodes.push(g.add_node(BlockId(i as u32)));
}
for (i, succs) in block_succs.iter().enumerate() {
for &s in succs {
g.add_edge(block_nodes[i], block_nodes[s], ());
}
}
let entry_gnode = block_nodes[0]; (g, entry_gnode)
}
fn compute_dominance_frontiers(
num_blocks: usize,
block_preds: &[Vec<usize>],
doms: &Dominators<NodeIndex>,
block_graph: &Graph<BlockId, ()>,
) -> Vec<HashSet<usize>> {
let mut df: Vec<HashSet<usize>> = vec![HashSet::new(); num_blocks];
let block_node: Vec<NodeIndex> = block_graph.node_indices().collect();
for n in 0..num_blocks {
let preds = &block_preds[n];
if preds.len() >= 2 {
for &p in preds {
let mut runner = p;
let n_gnode = block_node[n];
let idom_n = doms.immediate_dominator(n_gnode);
loop {
let runner_gnode = block_node[runner];
if idom_n == Some(runner_gnode) {
break;
}
df[runner].insert(n);
match doms.immediate_dominator(runner_gnode) {
Some(idom_runner) if idom_runner != runner_gnode => {
runner = block_graph[idom_runner].0 as usize;
}
_ => break, }
}
}
}
}
df
}
fn identify_external_uses(
cfg: &Cfg,
blocks_nodes: &[Vec<NodeIndex>],
var_defs: &BTreeMap<String, HashSet<usize>>,
) -> Vec<String> {
let mut used: HashSet<String> = HashSet::new();
for nodes in blocks_nodes {
for &node in nodes {
for u in &cfg[node].taint.uses {
used.insert(u.clone());
}
}
}
let mut external: Vec<String> = used
.into_iter()
.filter(|u| !var_defs.contains_key(u))
.collect();
external.sort(); external
}
pub(crate) fn is_receiver_name(name: &str) -> bool {
matches!(name, "self" | "this")
}
fn reorder_external_vars(external: Vec<String>, formal_params: &[String]) -> Vec<String> {
if formal_params.is_empty() {
return external; }
let ext_set: HashSet<&str> = external.iter().map(|s| s.as_str()).collect();
let formal_set: HashSet<&str> = formal_params.iter().map(|s| s.as_str()).collect();
let mut result = Vec::with_capacity(external.len());
if ext_set.contains("self") || formal_set.contains("self") {
result.push("self".to_string());
} else if ext_set.contains("this") || formal_set.contains("this") {
result.push("this".to_string());
}
for p in formal_params {
if is_receiver_name(p) {
continue;
}
result.push(p.clone());
}
let placed: HashSet<String> = result.iter().cloned().collect();
for v in external {
if placed.contains(&v) {
continue;
}
if !formal_set.contains(v.as_str()) && !is_receiver_name(&v) {
result.push(v);
}
}
result
}
fn collect_var_defs(
cfg: &Cfg,
blocks_nodes: &[Vec<NodeIndex>],
nop_nodes: &HashSet<NodeIndex>,
) -> BTreeMap<String, HashSet<usize>> {
let mut defs: BTreeMap<String, HashSet<usize>> = BTreeMap::new();
for (block_idx, nodes) in blocks_nodes.iter().enumerate() {
for &node in nodes {
if nop_nodes.contains(&node) {
continue;
}
if let Some(ref d) = cfg[node].taint.defines {
defs.entry(d.clone()).or_default().insert(block_idx);
let mut path = d.as_str();
while let Some(dot_pos) = path.rfind('.') {
path = &path[..dot_pos];
defs.entry(path.to_string()).or_default().insert(block_idx);
}
}
for ed in &cfg[node].taint.extra_defines {
defs.entry(ed.clone()).or_default().insert(block_idx);
}
if cfg[node].taint.defines.is_none()
&& cfg[node].call.callee.is_none()
&& cfg[node].kind == StmtKind::Seq
&& cfg[node].taint.uses.len() == 1
{
defs.entry(cfg[node].taint.uses[0].clone())
.or_default()
.insert(block_idx);
}
}
}
defs
}
fn insert_phis(
var_defs: &BTreeMap<String, HashSet<usize>>,
dom_frontiers: &[HashSet<usize>],
_num_blocks: usize,
) -> Vec<BTreeSet<String>> {
let num_blocks = dom_frontiers.len();
let mut phi_placements: Vec<BTreeSet<String>> = vec![BTreeSet::new(); num_blocks];
for (var, def_blocks) in var_defs {
let mut worklist: VecDeque<usize> = def_blocks.iter().copied().collect();
let mut has_phi: HashSet<usize> = HashSet::new();
while let Some(b) = worklist.pop_front() {
for &f in &dom_frontiers[b] {
if has_phi.insert(f) {
phi_placements[f].insert(var.clone());
if !def_blocks.contains(&f) {
worklist.push_back(f);
}
}
}
}
}
phi_placements
}
fn build_dom_tree_children(
num_blocks: usize,
doms: &Dominators<NodeIndex>,
block_graph: &Graph<BlockId, ()>,
) -> Vec<Vec<usize>> {
let mut children: Vec<Vec<usize>> = vec![vec![]; num_blocks];
let block_nodes: Vec<NodeIndex> = block_graph.node_indices().collect();
for i in 0..num_blocks {
if let Some(idom) = doms.immediate_dominator(block_nodes[i]) {
let idom_idx = block_graph[idom].0 as usize;
if idom_idx != i {
children[idom_idx].push(i);
}
}
}
children
}
fn rename_variables(
cfg: &Cfg,
blocks_nodes: &[Vec<NodeIndex>],
block_succs: &[Vec<usize>],
block_preds: &[Vec<usize>],
phi_placements: &[BTreeSet<String>],
dom_tree_children: &[Vec<usize>],
filtered_edges: &[(NodeIndex, NodeIndex, EdgeKind)],
external_vars: &[String],
formal_params: &[String],
with_params: bool,
nop_nodes: &HashSet<NodeIndex>,
) -> (
Vec<SsaBlock>,
Vec<ValueDef>,
HashMap<NodeIndex, SsaValue>,
crate::ssa::ir::FieldInterner,
HashMap<SsaValue, (SsaValue, crate::ssa::ir::FieldId)>,
HashSet<SsaValue>,
) {
let num_blocks = blocks_nodes.len();
let mut next_value: u32 = 0;
let mut value_defs: Vec<ValueDef> = Vec::new();
let mut cfg_node_map: HashMap<NodeIndex, SsaValue> = HashMap::new();
let mut field_interner = crate::ssa::ir::FieldInterner::new();
let mut field_writes: HashMap<SsaValue, (SsaValue, crate::ssa::ir::FieldId)> = HashMap::new();
let mut var_stacks: HashMap<String, Vec<SsaValue>> = HashMap::new();
let mut ssa_blocks: Vec<SsaBlock> = (0..num_blocks)
.map(|i| SsaBlock {
id: BlockId(i as u32),
phis: Vec::new(),
body: Vec::new(),
terminator: Terminator::Unreachable,
preds: SmallVec::new(),
succs: SmallVec::new(),
})
.collect();
let mut phi_values: Vec<BTreeMap<String, SsaValue>> = vec![BTreeMap::new(); num_blocks];
for (block_idx, vars) in phi_placements.iter().enumerate() {
let block_id = BlockId(block_idx as u32);
let cfg_node = blocks_nodes[block_idx][0]; for var in vars {
let v = SsaValue(next_value);
next_value += 1;
value_defs.push(ValueDef {
var_name: Some(var.clone()),
cfg_node,
block: block_id,
});
phi_values[block_idx].insert(var.clone(), v);
ssa_blocks[block_idx].phis.push(SsaInst {
value: v,
op: SsaOp::Phi(SmallVec::new()),
cfg_node,
var_name: Some(var.clone()),
span: cfg[cfg_node].ast.span,
});
}
}
fn process_block(
block_idx: usize,
cfg: &Cfg,
blocks_nodes: &[Vec<NodeIndex>],
block_succs: &[Vec<usize>],
block_preds: &[Vec<usize>],
phi_placements: &[BTreeSet<String>],
dom_tree_children: &[Vec<usize>],
filtered_edges: &[(NodeIndex, NodeIndex, EdgeKind)],
var_stacks: &mut HashMap<String, Vec<SsaValue>>,
ssa_blocks: &mut [SsaBlock],
phi_values: &mut [BTreeMap<String, SsaValue>],
value_defs: &mut Vec<ValueDef>,
cfg_node_map: &mut HashMap<NodeIndex, SsaValue>,
next_value: &mut u32,
nop_nodes: &HashSet<NodeIndex>,
field_interner: &mut crate::ssa::ir::FieldInterner,
field_writes: &mut HashMap<SsaValue, (SsaValue, crate::ssa::ir::FieldId)>,
) {
let block_id = BlockId(block_idx as u32);
let saved: Vec<(String, usize)> = var_stacks
.iter()
.map(|(k, v)| (k.clone(), v.len()))
.collect();
for (var, &v) in &phi_values[block_idx] {
var_stacks.entry(var.clone()).or_default().push(v);
}
for &node in &blocks_nodes[block_idx] {
let info = &cfg[node];
let build_call_args = |info: &crate::cfg::NodeInfo,
var_stacks: &HashMap<String, Vec<SsaValue>>|
-> (Vec<SmallVec<[SsaValue; 2]>>, Option<SsaValue>) {
let receiver = info
.call
.receiver
.as_ref()
.and_then(|r| var_stacks.get(r).and_then(|s| s.last().copied()));
let args = if !info.call.arg_uses.is_empty() {
let mut args: Vec<SmallVec<[SsaValue; 2]>> = info
.call
.arg_uses
.iter()
.map(|arg_idents| {
arg_idents
.iter()
.filter_map(|ident| {
var_stacks.get(ident).and_then(|s| s.last().copied())
})
.collect()
})
.collect();
let arg_uses_flat: HashSet<&str> = info
.call
.arg_uses
.iter()
.flat_map(|g| g.iter().map(|s| s.as_str()))
.collect();
let receiver_ident = info.call.receiver.as_deref();
let implicit: SmallVec<[SsaValue; 2]> = info
.taint
.uses
.iter()
.filter(|u| !arg_uses_flat.contains(u.as_str()))
.filter(|u| Some(u.as_str()) != receiver_ident)
.filter_map(|u| var_stacks.get(u).and_then(|s| s.last().copied()))
.collect();
if !implicit.is_empty() {
args.push(implicit);
}
args
} else {
let all_uses: SmallVec<[SsaValue; 2]> = info
.taint
.uses
.iter()
.filter_map(|u| var_stacks.get(u).and_then(|s| s.last().copied()))
.collect();
if all_uses.is_empty() {
vec![]
} else {
vec![all_uses]
}
};
(args, receiver)
};
let op = if nop_nodes.contains(&node) {
SsaOp::Nop
} else if info.catch_param {
SsaOp::CatchParam
} else if info
.taint
.labels
.iter()
.any(|l| matches!(l, crate::labels::DataLabel::Source(_)))
&& info.call.callee.is_none()
{
SsaOp::Source
} else if info.call.callee.is_some() {
let callee = info.call.callee.as_deref().unwrap_or("").to_string();
let (mut args, mut receiver) = build_call_args(info, var_stacks);
let (final_callee, callee_text) = match try_lower_field_proj_chain(
&callee,
var_stacks,
field_interner,
block_idx,
block_id,
next_value,
ssa_blocks,
value_defs,
node,
info.ast.span,
) {
Some((recv_v, bare_method)) => {
receiver = Some(recv_v);
if let Some(base_ident) = callee.split('.').next() {
if let Some(base_v) = var_stacks.get(base_ident).and_then(|s| s.last())
{
args.retain(|grp| !(grp.len() == 1 && grp.first() == Some(base_v)));
}
}
(bare_method, Some(callee.clone()))
}
None => (callee, None),
};
SsaOp::Call {
callee: final_callee,
callee_text,
args,
receiver,
}
} else if info.taint.defines.is_some()
&& info.taint.uses.is_empty()
&& !info
.taint
.labels
.iter()
.any(|l| matches!(l, crate::labels::DataLabel::Source(_)))
{
SsaOp::Const(info.taint.const_text.clone())
} else if info.taint.defines.is_some() {
let mut uses: SmallVec<[SsaValue; 4]> = info
.taint
.uses
.iter()
.filter_map(|u| var_stacks.get(u).and_then(|s| s.last().copied()))
.collect();
if uses.len() == 1 && info.bin_op.is_some() && info.bin_op_const.is_some() {
let const_val = info.bin_op_const.unwrap();
let const_v = SsaValue(*next_value);
*next_value += 1;
let const_inst = SsaInst {
value: const_v,
op: SsaOp::Const(Some(const_val.to_string())),
cfg_node: node,
var_name: None,
span: info.ast.span,
};
ssa_blocks[block_idx].body.push(const_inst);
value_defs.push(ValueDef {
var_name: None,
cfg_node: node,
block: block_id,
});
uses.push(const_v);
}
SsaOp::Assign(uses)
} else if matches!(info.kind, StmtKind::Return | StmtKind::Throw)
&& !info.taint.uses.is_empty()
{
let uses: SmallVec<[SsaValue; 4]> = info
.taint
.uses
.iter()
.filter_map(|u| var_stacks.get(u).and_then(|s| s.last().copied()))
.collect();
if uses.is_empty() {
SsaOp::Nop
} else {
SsaOp::Assign(uses)
}
} else if matches!(
info.kind,
StmtKind::Entry
| StmtKind::Exit
| StmtKind::If
| StmtKind::Loop
| StmtKind::Break
| StmtKind::Continue
| StmtKind::Return
| StmtKind::Throw
) {
SsaOp::Nop
} else if info.call.callee.is_some() {
let callee = info.call.callee.as_deref().unwrap_or("").to_string();
let (mut args, mut receiver) = build_call_args(info, var_stacks);
let (final_callee, callee_text) = match try_lower_field_proj_chain(
&callee,
var_stacks,
field_interner,
block_idx,
block_id,
next_value,
ssa_blocks,
value_defs,
node,
info.ast.span,
) {
Some((recv_v, bare_method)) => {
receiver = Some(recv_v);
if let Some(base_ident) = callee.split('.').next() {
if let Some(base_v) = var_stacks.get(base_ident).and_then(|s| s.last())
{
args.retain(|grp| !(grp.len() == 1 && grp.first() == Some(base_v)));
}
}
(bare_method, Some(callee.clone()))
}
None => (callee, None),
};
SsaOp::Call {
callee: final_callee,
callee_text,
args,
receiver,
}
} else {
SsaOp::Nop
};
let v = SsaValue(*next_value);
*next_value += 1;
let var_name_for_ssa = if nop_nodes.contains(&node) {
None
} else if info.taint.defines.is_some() {
info.taint.defines.clone()
} else if info.kind == StmtKind::Seq
&& info.call.callee.is_none()
&& info.taint.uses.len() == 1
&& !var_stacks.contains_key(&info.taint.uses[0])
{
Some(info.taint.uses[0].clone())
} else {
None
};
value_defs.push(ValueDef {
var_name: var_name_for_ssa.clone(),
cfg_node: node,
block: block_id,
});
if let Some(ref d) = var_name_for_ssa {
var_stacks.entry(d.clone()).or_default().push(v);
}
cfg_node_map.insert(node, v);
let primary_op_for_extras = if info.taint.extra_defines.is_empty() {
None
} else {
Some(op.clone())
};
ssa_blocks[block_idx].body.push(SsaInst {
value: v,
op,
cfg_node: node,
var_name: var_name_for_ssa.clone(),
span: info.ast.span,
});
if !nop_nodes.contains(&node) {
if let Some(ref d) = info.taint.defines {
let mut current = d.as_str();
let mut child_value = v;
while let Some(dot_pos) = current.rfind('.') {
let parent = ¤t[..dot_pos];
let field_name = ¤t[dot_pos + 1..];
let prior_parent_value: Option<SsaValue> =
var_stacks.get(parent).and_then(|s| s.last().copied());
let synth_v = SsaValue(*next_value);
*next_value += 1;
let synth_uses: SmallVec<[SsaValue; 4]> =
SmallVec::from_elem(child_value, 1);
value_defs.push(ValueDef {
var_name: Some(parent.to_string()),
cfg_node: node,
block: block_id,
});
var_stacks
.entry(parent.to_string())
.or_default()
.push(synth_v);
ssa_blocks[block_idx].body.push(SsaInst {
value: synth_v,
op: SsaOp::Assign(synth_uses),
cfg_node: node,
var_name: Some(parent.to_string()),
span: info.ast.span,
});
if let Some(rcv) = prior_parent_value {
let fid = field_interner.intern(field_name);
field_writes.insert(synth_v, (rcv, fid));
}
child_value = synth_v;
current = parent;
}
}
}
if let Some(ref primary_op) = primary_op_for_extras {
for extra_def in &info.taint.extra_defines {
let ev = SsaValue(*next_value);
*next_value += 1;
value_defs.push(ValueDef {
var_name: Some(extra_def.clone()),
cfg_node: node,
block: block_id,
});
var_stacks.entry(extra_def.clone()).or_default().push(ev);
ssa_blocks[block_idx].body.push(SsaInst {
value: ev,
op: primary_op.clone(),
cfg_node: node,
var_name: Some(extra_def.clone()),
span: info.ast.span,
});
}
}
}
let succs = &block_succs[block_idx];
let last_node = *blocks_nodes[block_idx].last().unwrap();
ssa_blocks[block_idx].terminator = if succs.is_empty() {
let return_node = blocks_nodes[block_idx]
.iter()
.copied()
.find(|&n| cfg[n].kind == StmtKind::Return);
let has_throw_node = blocks_nodes[block_idx]
.iter()
.any(|&n| cfg[n].kind == StmtKind::Throw);
if has_throw_node && return_node.is_none() {
Terminator::Unreachable
} else if let Some(rn) = return_node {
let return_info = &cfg[rn];
if return_info.taint.uses.is_empty() {
let const_text = return_info.taint.const_text.clone();
let const_v = SsaValue(*next_value);
*next_value += 1;
let block_id = BlockId(block_idx as u32);
value_defs.push(ValueDef {
var_name: None,
cfg_node: rn,
block: block_id,
});
ssa_blocks[block_idx].body.push(SsaInst {
value: const_v,
op: SsaOp::Const(const_text),
cfg_node: rn,
var_name: None,
span: return_info.ast.span,
});
Terminator::Return(Some(const_v))
} else {
let from_body = ssa_blocks[block_idx]
.body
.iter()
.rev()
.find(|inst| !matches!(inst.op, SsaOp::Nop))
.map(|inst| inst.value);
Terminator::Return(from_body)
}
} else {
let ret_val = ssa_blocks[block_idx]
.body
.iter()
.rev()
.find(|inst| !matches!(inst.op, SsaOp::Nop))
.map(|inst| inst.value);
Terminator::Return(ret_val)
}
} else if succs.len() == 1 {
Terminator::Goto(BlockId(succs[0] as u32))
} else if succs.len() == 2 {
let cond_node = blocks_nodes[block_idx]
.iter()
.rev()
.find(|&&n| matches!(cfg[n].kind, StmtKind::If | StmtKind::Loop))
.copied()
.unwrap_or(last_node);
let mut true_blk = succs[0];
let mut false_blk = succs[1];
for &(src, tgt, kind) in filtered_edges {
if blocks_nodes[block_idx].contains(&src) {
let tgt_blk_opt = succs.iter().position(|&s| {
blocks_nodes
.get(s)
.is_some_and(|nodes| nodes.contains(&tgt))
});
if let Some(tgt_blk_pos) = tgt_blk_opt {
match kind {
EdgeKind::True => true_blk = succs[tgt_blk_pos],
EdgeKind::False => false_blk = succs[tgt_blk_pos],
_ => {}
}
}
}
}
let cond_info = &cfg[cond_node];
let condition = if cond_info.condition_text.is_some()
&& !cond_info.condition_vars.is_empty()
{
let expr =
crate::constraint::lower::lower_condition_with_stacks(cond_info, var_stacks);
if matches!(expr, crate::constraint::lower::ConditionExpr::Unknown) {
None
} else {
Some(Box::new(expr))
}
} else {
None
};
Terminator::Branch {
cond: cond_node,
true_blk: BlockId(true_blk as u32),
false_blk: BlockId(false_blk as u32),
condition,
}
} else {
let scrutinee = cfg_node_map.get(&last_node).copied().unwrap_or(SsaValue(0));
let targets: SmallVec<[BlockId; 4]> =
succs.iter().skip(1).map(|&s| BlockId(s as u32)).collect();
let default = BlockId(succs[0] as u32);
let case_values: SmallVec<[Option<crate::constraint::domain::ConstValue>; 4]> =
std::iter::repeat_with(|| None)
.take(targets.len())
.collect();
tracing::debug!(
block = block_idx,
num_succs = succs.len(),
"emitting Terminator::Switch for ≥3-way fanout",
);
Terminator::Switch {
scrutinee,
targets,
default,
case_values,
}
};
for &succ in succs {
for (var, &phi_val) in &phi_values[succ] {
let reaching_val = var_stacks.get(var).and_then(|s| s.last().copied());
if let Some(rv) = reaching_val {
for phi in &mut ssa_blocks[succ].phis {
if phi.value == phi_val {
if let SsaOp::Phi(ref mut operands) = phi.op {
operands.push((block_id, rv));
}
}
}
}
}
}
for &child in &dom_tree_children[block_idx] {
process_block(
child,
cfg,
blocks_nodes,
block_succs,
block_preds,
phi_placements,
dom_tree_children,
filtered_edges,
var_stacks,
ssa_blocks,
phi_values,
value_defs,
cfg_node_map,
next_value,
nop_nodes,
field_interner,
field_writes,
);
}
for (var, depth) in &saved {
if let Some(stack) = var_stacks.get_mut(var) {
stack.truncate(*depth);
}
}
let saved_vars: HashSet<&String> = saved.iter().map(|(k, _)| k).collect();
var_stacks.retain(|k, _| saved_vars.contains(k));
}
let mut synthetic_externals: HashSet<SsaValue> = HashSet::new();
let formal_set: HashSet<&str> = formal_params.iter().map(|s| s.as_str()).collect();
let track_synthetic = with_params;
if !external_vars.is_empty() {
let entry_cfg_node = blocks_nodes[0][0];
let mut synthetic_body = Vec::with_capacity(external_vars.len());
let mut positional_idx: usize = 0;
for var in external_vars.iter() {
let v = SsaValue(next_value);
next_value += 1;
value_defs.push(ValueDef {
var_name: Some(var.clone()),
cfg_node: entry_cfg_node,
block: BlockId(0),
});
let is_receiver = is_receiver_name(var);
let op = if is_receiver {
SsaOp::SelfParam
} else {
let op = SsaOp::Param {
index: positional_idx,
};
positional_idx += 1;
op
};
let root_is_formal = var
.split_once('.')
.map(|(root, _)| formal_set.contains(root))
.unwrap_or(false);
if track_synthetic
&& !is_receiver
&& !formal_set.contains(var.as_str())
&& !root_is_formal
{
synthetic_externals.insert(v);
}
synthetic_body.push(SsaInst {
value: v,
op,
cfg_node: entry_cfg_node,
var_name: Some(var.clone()),
span: (0, 0),
});
var_stacks.entry(var.clone()).or_default().push(v);
}
synthetic_body.append(&mut ssa_blocks[0].body);
ssa_blocks[0].body = synthetic_body;
}
process_block(
0, cfg,
blocks_nodes,
block_succs,
block_preds,
phi_placements,
dom_tree_children,
filtered_edges,
&mut var_stacks,
&mut ssa_blocks,
&mut phi_values,
&mut value_defs,
&mut cfg_node_map,
&mut next_value,
nop_nodes,
&mut field_interner,
&mut field_writes,
);
let has_orphans =
(1..num_blocks).any(|bid| block_preds[bid].is_empty() && ssa_blocks[bid].body.is_empty());
if has_orphans {
var_stacks.clear();
for block in &ssa_blocks {
for inst in block.phis.iter().chain(block.body.iter()) {
if let Some(ref name) = inst.var_name {
var_stacks.entry(name.clone()).or_default().push(inst.value);
}
}
}
for bid in 1..num_blocks {
if block_preds[bid].is_empty() && ssa_blocks[bid].body.is_empty() {
process_block(
bid,
cfg,
blocks_nodes,
block_succs,
block_preds,
phi_placements,
dom_tree_children,
filtered_edges,
&mut var_stacks,
&mut ssa_blocks,
&mut phi_values,
&mut value_defs,
&mut cfg_node_map,
&mut next_value,
nop_nodes,
&mut field_interner,
&mut field_writes,
);
}
}
}
(
ssa_blocks,
value_defs,
cfg_node_map,
field_interner,
field_writes,
synthetic_externals,
)
}
fn debug_assert_bfs_ordering(block_preds: &[Vec<usize>]) {
for (i, preds) in block_preds.iter().enumerate() {
if i == 0 {
continue; }
if preds.is_empty() {
continue; }
let has_forward_pred = preds.iter().any(|&p| p < i);
debug_assert!(
has_forward_pred,
"Block {} has no forward predecessor — BFS ordering violated. Preds: {:?}",
i, preds
);
}
}
fn assert_phi_operand_counts(ssa_blocks: &[SsaBlock], block_preds: &[Vec<usize>]) {
use std::collections::HashSet;
for (i, block) in ssa_blocks.iter().enumerate() {
let pred_set: HashSet<u32> = block_preds[i].iter().map(|&p| p as u32).collect();
for phi in &block.phis {
if let SsaOp::Phi(ref operands) = phi.op {
assert_eq!(
operands.len(),
block_preds[i].len(),
"SSA phi operand count does not match predecessor count: block {} phi v{} \
(var={:?}) has {} operands but block has {} predecessors. \
preds={:?}, operand_preds={:?}",
i,
phi.value.0,
phi.var_name,
operands.len(),
block_preds[i].len(),
block_preds[i],
operands.iter().map(|(b, _)| b.0).collect::<Vec<_>>(),
);
let mut seen: HashSet<u32> = HashSet::new();
for (pred_blk, _) in operands.iter() {
assert!(
pred_set.contains(&pred_blk.0),
"SSA phi operand references nonexistent predecessor: block {} phi v{} \
references pred B{} but block predecessors are {:?}",
i,
phi.value.0,
pred_blk.0,
block_preds[i],
);
assert!(
seen.insert(pred_blk.0),
"SSA phi operand duplicates predecessor: block {} phi v{} has two \
operands for pred B{}",
i,
phi.value.0,
pred_blk.0,
);
}
}
}
}
}
fn fill_undef_phi_operands(
ssa_blocks: &mut [SsaBlock],
block_preds: &[Vec<usize>],
value_defs: &mut Vec<ValueDef>,
blocks_nodes: &[Vec<NodeIndex>],
) {
let needs_undef = ssa_blocks.iter().enumerate().any(|(bi, block)| {
block.phis.iter().any(|phi| {
if let SsaOp::Phi(ref operands) = phi.op {
operands.len() < block_preds[bi].len()
} else {
false
}
})
});
if !needs_undef {
return;
}
let anchor_node = blocks_nodes
.first()
.and_then(|b| b.first())
.copied()
.expect("entry block has at least one CFG node");
let undef_val = SsaValue(value_defs.len() as u32);
value_defs.push(ValueDef {
var_name: None,
cfg_node: anchor_node,
block: BlockId(0),
});
ssa_blocks[0].body.push(SsaInst {
value: undef_val,
op: SsaOp::Undef,
cfg_node: anchor_node,
var_name: None,
span: (0, 0),
});
for (bi, block) in ssa_blocks.iter_mut().enumerate() {
for phi in block.phis.iter_mut() {
if let SsaOp::Phi(ref mut operands) = phi.op {
if operands.len() == block_preds[bi].len() {
continue;
}
use std::collections::HashSet;
let present: HashSet<u32> = operands.iter().map(|(b, _)| b.0).collect();
for &pred in &block_preds[bi] {
let pid = pred as u32;
if !present.contains(&pid) {
operands.push((BlockId(pid), undef_val));
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cfg::{EdgeKind, NodeInfo, StmtKind, TaintMeta};
use petgraph::Graph;
fn make_node(kind: StmtKind) -> NodeInfo {
NodeInfo {
kind,
..Default::default()
}
}
#[test]
fn linear_cfg_no_phis() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let n1 = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let n2 = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("y".into()),
uses: vec!["x".into()],
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, n1, EdgeKind::Seq);
cfg.add_edge(n1, n2, EdgeKind::Seq);
cfg.add_edge(n2, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
assert_eq!(ssa.blocks.len(), 1);
assert!(ssa.blocks[0].phis.is_empty());
assert_eq!(ssa.blocks[0].body.len(), 4);
}
#[test]
fn diamond_cfg_produces_phi() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let def_x = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let if_node = cfg.add_node(make_node(StmtKind::If));
let true_node = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let false_node = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let join = cfg.add_node(make_node(StmtKind::Seq));
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, def_x, EdgeKind::Seq);
cfg.add_edge(def_x, if_node, EdgeKind::Seq);
cfg.add_edge(if_node, true_node, EdgeKind::True);
cfg.add_edge(if_node, false_node, EdgeKind::False);
cfg.add_edge(true_node, join, EdgeKind::Seq);
cfg.add_edge(false_node, join, EdgeKind::Seq);
cfg.add_edge(join, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
assert!(ssa.blocks.len() >= 3);
let join_block = ssa
.blocks
.iter()
.find(|b| !b.phis.is_empty())
.expect("should have a block with a phi");
assert_eq!(join_block.phis.len(), 1);
assert_eq!(join_block.phis[0].var_name.as_deref(), Some("x"));
if let SsaOp::Phi(ref operands) = join_block.phis[0].op {
assert_eq!(operands.len(), 2);
} else {
panic!("expected Phi op");
}
}
#[test]
fn loop_cfg_produces_phi() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let def_x = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let loop_header = cfg.add_node(make_node(StmtKind::Loop));
let body = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
uses: vec!["x".into()],
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, def_x, EdgeKind::Seq);
cfg.add_edge(def_x, loop_header, EdgeKind::Seq);
cfg.add_edge(loop_header, body, EdgeKind::True);
cfg.add_edge(body, loop_header, EdgeKind::Back);
cfg.add_edge(loop_header, exit, EdgeKind::False);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
let header_phis: Vec<_> = ssa.blocks.iter().filter(|b| !b.phis.is_empty()).collect();
assert!(
!header_phis.is_empty(),
"loop header should have a phi for x"
);
let x_phi = header_phis[0]
.phis
.iter()
.find(|p| p.var_name.as_deref() == Some("x"));
assert!(x_phi.is_some(), "should have phi for variable x");
}
#[test]
fn multiple_reassignments_distinct_values() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let n1 = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let n2 = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let n3 = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, n1, EdgeKind::Seq);
cfg.add_edge(n1, n2, EdgeKind::Seq);
cfg.add_edge(n2, n3, EdgeKind::Seq);
cfg.add_edge(n3, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
let x_values: Vec<_> = ssa
.value_defs
.iter()
.enumerate()
.filter(|(_, vd)| vd.var_name.as_deref() == Some("x"))
.map(|(i, _)| SsaValue(i as u32))
.collect();
assert_eq!(x_values.len(), 3, "three definitions of x");
let unique: HashSet<_> = x_values.iter().collect();
assert_eq!(unique.len(), 3, "all SsaValues should be distinct");
}
#[test]
fn empty_cfg_returns_error() {
let cfg: Cfg = Graph::new();
let result = lower_to_ssa(&cfg, NodeIndex::new(0), None, true);
assert!(result.is_err());
}
#[test]
fn bfs_ordering_holds_for_linear_cfg() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let a = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let b = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("y".into()),
uses: vec!["x".into()],
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, a, EdgeKind::Seq);
cfg.add_edge(a, b, EdgeKind::Seq);
cfg.add_edge(b, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
assert!(!ssa.blocks.is_empty());
}
#[test]
fn bfs_ordering_holds_for_diamond_cfg() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let def_x = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let if_node = cfg.add_node(make_node(StmtKind::If));
let true_node = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let false_node = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let join = cfg.add_node(make_node(StmtKind::Seq));
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, def_x, EdgeKind::Seq);
cfg.add_edge(def_x, if_node, EdgeKind::Seq);
cfg.add_edge(if_node, true_node, EdgeKind::True);
cfg.add_edge(if_node, false_node, EdgeKind::False);
cfg.add_edge(true_node, join, EdgeKind::Seq);
cfg.add_edge(false_node, join, EdgeKind::Seq);
cfg.add_edge(join, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
let phi_block = ssa.blocks.iter().find(|b| !b.phis.is_empty());
if let Some(block) = phi_block {
assert_eq!(
block.preds.len(),
2,
"join block should have 2 predecessors"
);
for phi in &block.phis {
if let SsaOp::Phi(ref ops) = phi.op {
assert!(
ops.len() <= block.preds.len(),
"phi operands should not exceed predecessor count"
);
}
}
}
}
#[test]
fn bfs_ordering_holds_for_loop_with_back_edge() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let def_x = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let loop_h = cfg.add_node(make_node(StmtKind::Loop));
let body = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
uses: vec!["x".into()],
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, def_x, EdgeKind::Seq);
cfg.add_edge(def_x, loop_h, EdgeKind::Seq);
cfg.add_edge(loop_h, body, EdgeKind::True);
cfg.add_edge(body, loop_h, EdgeKind::Back);
cfg.add_edge(loop_h, exit, EdgeKind::False);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
assert!(!ssa.blocks.is_empty());
}
#[test]
fn orphan_catch_block_does_not_violate_bfs_ordering() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let body = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let catch = cfg.add_node(NodeInfo {
catch_param: true,
taint: TaintMeta {
defines: Some("e".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, body, EdgeKind::Seq);
cfg.add_edge(body, exit, EdgeKind::Seq);
cfg.add_edge(body, catch, EdgeKind::Exception);
cfg.add_edge(catch, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
assert!(!ssa.blocks.is_empty());
}
#[test]
fn phi_operand_count_equals_pred_count_in_diamond() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let if_node = cfg.add_node(make_node(StmtKind::If));
let t = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("v".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let f = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("v".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let join = cfg.add_node(NodeInfo {
taint: TaintMeta {
uses: vec!["v".into()],
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, if_node, EdgeKind::Seq);
cfg.add_edge(if_node, t, EdgeKind::True);
cfg.add_edge(if_node, f, EdgeKind::False);
cfg.add_edge(t, join, EdgeKind::Seq);
cfg.add_edge(f, join, EdgeKind::Seq);
cfg.add_edge(join, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
let phi_block = ssa
.blocks
.iter()
.find(|b| !b.phis.is_empty())
.expect("should have a phi block");
for phi in &phi_block.phis {
if let SsaOp::Phi(ref ops) = phi.op {
assert_eq!(
ops.len(),
phi_block.preds.len(),
"phi operand count should equal predecessor count in a clean diamond"
);
}
}
}
#[test]
fn bfs_assertion_helper_accepts_valid_orderings() {
let block_preds = vec![
vec![], vec![0], vec![0, 1], vec![], vec![2], ];
debug_assert_bfs_ordering(&block_preds);
}
#[test]
fn catch_block_join_phi_has_operand_per_live_predecessor() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let define_x = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let body = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let catch = cfg.add_node(NodeInfo {
catch_param: true,
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let join = cfg.add_node(NodeInfo {
taint: TaintMeta {
uses: vec!["x".into()],
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, define_x, EdgeKind::Seq);
cfg.add_edge(define_x, body, EdgeKind::Seq);
cfg.add_edge(body, join, EdgeKind::Seq);
cfg.add_edge(body, catch, EdgeKind::Exception);
cfg.add_edge(catch, join, EdgeKind::Seq);
cfg.add_edge(join, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
let phi_block = ssa
.blocks
.iter()
.find(|b| {
b.phis
.iter()
.any(|p| p.var_name.as_deref() == Some("x") && matches!(p.op, SsaOp::Phi(_)))
})
.expect("expected a phi for `x` at the catch/normal join");
assert_eq!(
phi_block.preds.len(),
2,
"catch/normal join block must have 2 predecessors, got {}",
phi_block.preds.len()
);
let phi_for_x = phi_block
.phis
.iter()
.find(|p| p.var_name.as_deref() == Some("x"))
.unwrap();
if let SsaOp::Phi(ref operands) = phi_for_x.op {
assert_eq!(
operands.len(),
2,
"phi for `x` at the catch/normal join must have one operand per \
predecessor, got {}",
operands.len()
);
} else {
panic!("expected SsaOp::Phi for `x`");
}
}
#[test]
fn partial_phi_edge_fills_with_undef_sentinel() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let body = cfg.add_node(make_node(StmtKind::Seq));
let catch = cfg.add_node(NodeInfo {
catch_param: true,
taint: TaintMeta {
defines: Some("e".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let join = cfg.add_node(NodeInfo {
taint: TaintMeta {
uses: vec!["e".into()],
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, body, EdgeKind::Seq);
cfg.add_edge(body, join, EdgeKind::Seq);
cfg.add_edge(body, catch, EdgeKind::Exception);
cfg.add_edge(catch, join, EdgeKind::Seq);
cfg.add_edge(join, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
let phi_block = ssa
.blocks
.iter()
.find(|b| b.phis.iter().any(|p| p.var_name.as_deref() == Some("e")))
.expect("expected a phi for `e`");
let phi_for_e = phi_block
.phis
.iter()
.find(|p| p.var_name.as_deref() == Some("e"))
.unwrap();
let operands = match &phi_for_e.op {
SsaOp::Phi(ops) => ops,
_ => panic!("expected SsaOp::Phi for `e`"),
};
assert_eq!(
operands.len(),
phi_block.preds.len(),
"phi for `e` must have one operand per predecessor",
);
let found_inst = |v: SsaValue| -> Option<&SsaInst> {
ssa.blocks
.iter()
.flat_map(|b| b.phis.iter().chain(b.body.iter()))
.find(|i| i.value == v)
};
let any_undef = operands.iter().any(|(_, v)| {
found_inst(*v)
.map(|i| matches!(i.op, SsaOp::Undef))
.unwrap_or(false)
});
assert!(
any_undef,
"phi for `e` at the catch-join must reference SsaOp::Undef \
on the normal-path predecessor edge",
);
}
#[test]
fn phi_assertion_helper_accepts_exact_operand_count() {
let dummy_node = NodeIndex::new(0);
let block = SsaBlock {
id: BlockId(1),
phis: vec![SsaInst {
value: SsaValue(0),
op: SsaOp::Phi(smallvec::smallvec![
(BlockId(0), SsaValue(1)),
(BlockId(2), SsaValue(2)),
]),
cfg_node: dummy_node,
var_name: Some("x".into()),
span: (0, 0),
}],
body: vec![],
terminator: Terminator::Unreachable,
preds: smallvec::smallvec![BlockId(0), BlockId(2)],
succs: smallvec::smallvec![],
};
let block_preds = vec![vec![], vec![0, 2], vec![0]];
assert_phi_operand_counts(
&[
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec::smallvec![],
succs: smallvec::smallvec![BlockId(1)],
},
block,
SsaBlock {
id: BlockId(2),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec::smallvec![BlockId(0)],
succs: smallvec::smallvec![BlockId(1)],
},
],
&block_preds,
);
}
#[test]
#[should_panic(expected = "SSA phi operand count does not match predecessor count")]
fn phi_assertion_helper_rejects_more_operands_than_preds() {
let dummy_node = NodeIndex::new(0);
let block = SsaBlock {
id: BlockId(1),
phis: vec![SsaInst {
value: SsaValue(0),
op: SsaOp::Phi(smallvec::smallvec![
(BlockId(0), SsaValue(1)),
(BlockId(2), SsaValue(2)),
(BlockId(3), SsaValue(3)),
]),
cfg_node: dummy_node,
var_name: Some("x".into()),
span: (0, 0),
}],
body: vec![],
terminator: Terminator::Unreachable,
preds: smallvec::smallvec![BlockId(0), BlockId(2)],
succs: smallvec::smallvec![],
};
let block_preds = vec![vec![], vec![0, 2]];
assert_phi_operand_counts(
&[
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec::smallvec![],
succs: smallvec::smallvec![BlockId(1)],
},
block,
],
&block_preds,
);
}
#[test]
#[should_panic(expected = "SSA phi operand count does not match predecessor count")]
fn phi_assertion_helper_rejects_fewer_operands_than_preds() {
let dummy_node = NodeIndex::new(0);
let block = SsaBlock {
id: BlockId(1),
phis: vec![SsaInst {
value: SsaValue(0),
op: SsaOp::Phi(smallvec::smallvec![(BlockId(0), SsaValue(1))]),
cfg_node: dummy_node,
var_name: Some("e".into()),
span: (0, 0),
}],
body: vec![],
terminator: Terminator::Unreachable,
preds: smallvec::smallvec![BlockId(0), BlockId(2)],
succs: smallvec::smallvec![],
};
let block_preds = vec![vec![], vec![0, 2]];
assert_phi_operand_counts(
&[
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec::smallvec![],
succs: smallvec::smallvec![BlockId(1)],
},
block,
],
&block_preds,
);
}
#[test]
#[should_panic(expected = "SSA phi operand references nonexistent predecessor")]
fn phi_assertion_helper_rejects_wrong_pred_block() {
let dummy_node = NodeIndex::new(0);
let block = SsaBlock {
id: BlockId(1),
phis: vec![SsaInst {
value: SsaValue(0),
op: SsaOp::Phi(smallvec::smallvec![
(BlockId(0), SsaValue(1)),
(BlockId(3), SsaValue(2)),
]),
cfg_node: dummy_node,
var_name: Some("x".into()),
span: (0, 0),
}],
body: vec![],
terminator: Terminator::Unreachable,
preds: smallvec::smallvec![BlockId(0), BlockId(2)],
succs: smallvec::smallvec![],
};
let block_preds = vec![vec![], vec![0, 2]];
assert_phi_operand_counts(
&[
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec::smallvec![],
succs: smallvec::smallvec![BlockId(1)],
},
block,
],
&block_preds,
);
}
#[test]
fn three_successor_collapse_produces_switch() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let branch = cfg.add_node(make_node(StmtKind::If));
let s0 = cfg.add_node(make_node(StmtKind::Seq));
let s1 = cfg.add_node(make_node(StmtKind::Seq));
let s2 = cfg.add_node(make_node(StmtKind::Seq));
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, branch, EdgeKind::Seq);
cfg.add_edge(branch, s0, EdgeKind::True);
cfg.add_edge(branch, s1, EdgeKind::False);
cfg.add_edge(branch, s2, EdgeKind::Seq);
cfg.add_edge(s0, exit, EdgeKind::Seq);
cfg.add_edge(s1, exit, EdgeKind::Seq);
cfg.add_edge(s2, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
assert!(!ssa.blocks.is_empty());
let switch_block = ssa
.blocks
.iter()
.find(|b| matches!(b.terminator, Terminator::Switch { .. }) && b.succs.len() >= 3)
.expect("expected a block with a Switch terminator and ≥3 succs");
assert_eq!(
switch_block.succs.len(),
3,
"≥3-successor lowering must retain all succs on block.succs, got {:?}",
switch_block.succs
);
if let Terminator::Switch {
targets, default, ..
} = &switch_block.terminator
{
assert_eq!(
*default, switch_block.succs[0],
"Switch default must match succs[0]"
);
assert_eq!(
targets.len(),
switch_block.succs.len() - 1,
"Switch targets must cover every succ except default"
);
for (i, t) in targets.iter().enumerate() {
assert_eq!(
*t,
switch_block.succs[i + 1],
"Switch target[{i}] must match succs[{}]",
i + 1
);
}
}
}
#[test]
fn normal_two_successor_produces_branch() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let if_node = cfg.add_node(make_node(StmtKind::If));
let t = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let f = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, if_node, EdgeKind::Seq);
cfg.add_edge(if_node, t, EdgeKind::True);
cfg.add_edge(if_node, f, EdgeKind::False);
cfg.add_edge(t, exit, EdgeKind::Seq);
cfg.add_edge(f, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
let has_branch = ssa
.blocks
.iter()
.any(|b| matches!(b.terminator, Terminator::Branch { .. }));
assert!(
has_branch,
"normal 2-successor case must produce Branch, not Goto"
);
}
#[test]
fn early_return_block_terminates_with_return_not_goto_to_exit() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let if_node = cfg.add_node(NodeInfo {
taint: TaintMeta {
uses: vec!["x".into()],
..Default::default()
},
..make_node(StmtKind::If)
});
let early_ret = cfg.add_node(NodeInfo {
taint: TaintMeta {
const_text: Some("\"\"".to_string()),
..Default::default()
},
..make_node(StmtKind::Return)
});
let tail = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("y".into()),
uses: vec!["x".into()],
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, if_node, EdgeKind::Seq);
cfg.add_edge(if_node, early_ret, EdgeKind::True);
cfg.add_edge(if_node, tail, EdgeKind::False);
cfg.add_edge(early_ret, exit, EdgeKind::Seq);
cfg.add_edge(tail, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
let early_block = ssa
.blocks
.iter()
.find(|b| {
b.body
.iter()
.chain(b.phis.iter())
.any(|inst| inst.cfg_node == early_ret)
})
.expect("early-return CFG node must live in some SSA block");
assert!(
matches!(early_block.terminator, Terminator::Return(_)),
"early-return block must terminate with Return, got {:?}",
early_block.terminator
);
assert!(
early_block.succs.is_empty(),
"early-return block must have no successors at the block level, \
got succs = {:?}",
early_block.succs
);
let tail_block = ssa
.blocks
.iter()
.find(|b| {
b.body
.iter()
.chain(b.phis.iter())
.any(|inst| inst.cfg_node == tail)
})
.expect("tail CFG node must live in some SSA block");
let early_block_id = early_block.id;
assert!(
!tail_block.preds.contains(&early_block_id),
"tail block must not have early-return block as a predecessor; \
merged-return defect would re-emerge. tail.preds = {:?}, \
early_block_id = {:?}",
tail_block.preds,
early_block_id
);
}
#[test]
fn or_chain_rejection_block_terminates_with_return() {
use crate::cfg::build_cfg;
let src = br#"
fn sanitize_path(s: &str) -> String {
if s.contains("..") || s.starts_with('/') || s.starts_with('\\') {
return String::new();
}
s.to_string()
}
"#;
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter::Language::from(tree_sitter_rust::LANGUAGE))
.unwrap();
let tree = parser.parse(src.as_slice(), None).unwrap();
let file_cfg = build_cfg(&tree, src.as_slice(), "rust", "test.rs", None);
let body = if file_cfg.bodies.len() > 1 {
&file_cfg.bodies[1]
} else {
file_cfg.first_body()
};
let cfg = &body.graph;
let entry = body.entry;
let mut rejection_call: Option<NodeIndex> = None;
for idx in cfg.node_indices() {
let info = &cfg[idx];
if info.kind == StmtKind::Call {
if let Some(callee) = &info.call.callee {
if callee == "String::new" || callee.ends_with("String::new") {
rejection_call = Some(idx);
}
}
}
}
let rejection_call = rejection_call
.expect("CFG must contain a String::new() Call node for the rejection arm");
let ssa = lower_to_ssa(cfg, entry, None, true).expect("SSA lowering should succeed");
let rejection_block = ssa
.blocks
.iter()
.find(|b| {
b.body
.iter()
.chain(b.phis.iter())
.any(|inst| inst.cfg_node == rejection_call)
})
.expect("rejection-arm Call must live in some SSA block");
assert!(
rejection_block.succs.is_empty(),
"rejection-arm block must have no block-level successors after \
return-frontier strip; got succs = {:?}",
rejection_block.succs
);
assert!(
matches!(rejection_block.terminator, Terminator::Return(_)),
"rejection-arm block must terminate with Terminator::Return; got {:?}",
rejection_block.terminator
);
}
#[test]
fn c_or_chain_both_return_arms_terminate_with_return() {
use crate::cfg::build_cfg;
let src = br#"
const char *sanitize_path(const char *s) {
if (strstr(s, "..") != NULL || s[0] == '/' || s[0] == '\\') {
return "";
}
return s;
}
"#;
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter::Language::from(tree_sitter_c::LANGUAGE))
.unwrap();
let tree = parser.parse(src.as_slice(), None).unwrap();
let file_cfg = build_cfg(&tree, src.as_slice(), "c", "test.c", None);
let body = file_cfg.first_body();
let cfg = &body.graph;
let entry = body.entry;
let ssa = lower_to_ssa(cfg, entry, None, true).expect("SSA lowering should succeed");
let return_blocks: Vec<&SsaBlock> = ssa
.blocks
.iter()
.filter(|b| matches!(b.terminator, Terminator::Return(_)))
.collect();
assert!(
return_blocks.len() >= 2,
"Expected ≥2 Return-terminated blocks (rejection arm + tail); got {}: {:?}",
return_blocks.len(),
ssa.blocks
.iter()
.map(|b| (b.id, &b.terminator))
.collect::<Vec<_>>()
);
for b in &return_blocks {
assert!(
b.succs.is_empty(),
"Return-terminated block id={:?} has succs={:?}",
b.id,
b.succs
);
}
}
fn fresh_proj_scratch() -> (
std::collections::HashMap<String, Vec<SsaValue>>,
crate::ssa::ir::FieldInterner,
Vec<SsaBlock>,
Vec<ValueDef>,
u32,
) {
let blocks = vec![SsaBlock {
id: BlockId(0),
phis: Vec::new(),
body: Vec::new(),
terminator: Terminator::Unreachable,
preds: SmallVec::new(),
succs: SmallVec::new(),
}];
(
std::collections::HashMap::new(),
crate::ssa::ir::FieldInterner::new(),
blocks,
Vec::new(),
0,
)
}
fn seed_var(
var_stacks: &mut std::collections::HashMap<String, Vec<SsaValue>>,
value_defs: &mut Vec<ValueDef>,
next_value: &mut u32,
name: &str,
) -> SsaValue {
let v = SsaValue(*next_value);
*next_value += 1;
value_defs.push(ValueDef {
var_name: Some(name.into()),
cfg_node: NodeIndex::new(0),
block: BlockId(0),
});
var_stacks.entry(name.into()).or_default().push(v);
v
}
#[test]
fn try_lower_field_proj_chain_too_few_segments_returns_none() {
let (mut vs, mut interner, mut blocks, mut defs, mut nv) = fresh_proj_scratch();
seed_var(&mut vs, &mut defs, &mut nv, "obj");
assert!(
try_lower_field_proj_chain(
"foo",
&vs,
&mut interner,
0,
BlockId(0),
&mut nv,
&mut blocks,
&mut defs,
NodeIndex::new(0),
(0, 0),
)
.is_none()
);
assert!(
try_lower_field_proj_chain(
"obj.method",
&vs,
&mut interner,
0,
BlockId(0),
&mut nv,
&mut blocks,
&mut defs,
NodeIndex::new(0),
(0, 0),
)
.is_none()
);
assert!(blocks[0].body.is_empty());
assert!(interner.is_empty());
}
#[test]
fn try_lower_field_proj_chain_complex_token_returns_none() {
let cases = [
"Foo::bar::baz", "ptr->field.f", "obj.f().g", "vec[0].field", "obj.f.<T>", "obj.f g", "obj?.f.g", ];
let (mut vs, mut interner, mut blocks, mut defs, mut nv) = fresh_proj_scratch();
seed_var(&mut vs, &mut defs, &mut nv, "obj");
for s in &cases {
assert!(
try_lower_field_proj_chain(
s,
&vs,
&mut interner,
0,
BlockId(0),
&mut nv,
&mut blocks,
&mut defs,
NodeIndex::new(0),
(0, 0),
)
.is_none(),
"expected bail on complex callee {s}"
);
}
assert!(blocks[0].body.is_empty());
assert!(interner.is_empty());
}
#[test]
fn try_lower_field_proj_chain_unknown_base_returns_none() {
let (vs, mut interner, mut blocks, mut defs, mut nv) = fresh_proj_scratch();
assert!(
try_lower_field_proj_chain(
"ghost.f.method",
&vs,
&mut interner,
0,
BlockId(0),
&mut nv,
&mut blocks,
&mut defs,
NodeIndex::new(0),
(0, 0),
)
.is_none()
);
assert!(blocks[0].body.is_empty());
assert!(interner.is_empty());
}
#[test]
fn try_lower_field_proj_chain_basic_two_dots_emits_one_proj() {
let (mut vs, mut interner, mut blocks, mut defs, mut nv) = fresh_proj_scratch();
let v_c = seed_var(&mut vs, &mut defs, &mut nv, "c");
let (recv, method) = try_lower_field_proj_chain(
"c.mu.Lock",
&vs,
&mut interner,
0,
BlockId(0),
&mut nv,
&mut blocks,
&mut defs,
NodeIndex::new(0),
(10, 20),
)
.expect("chain decomposition should succeed");
assert_eq!(recv, SsaValue(1));
assert_eq!(method, "Lock");
assert_eq!(blocks[0].body.len(), 1);
let inst = &blocks[0].body[0];
match &inst.op {
SsaOp::FieldProj {
receiver,
field,
projected_type,
} => {
assert_eq!(*receiver, v_c);
assert_eq!(interner.resolve(*field), "mu");
assert!(projected_type.is_none());
}
other => panic!("expected FieldProj, got {other:?}"),
}
assert_eq!(inst.span, (10, 20));
assert_eq!(inst.var_name.as_deref(), Some("c.mu"));
assert_eq!(defs.last().unwrap().var_name.as_deref(), Some("c.mu"));
}
#[test]
fn try_lower_field_proj_chain_three_dots_emits_two_projs_chained() {
let (mut vs, mut interner, mut blocks, mut defs, mut nv) = fresh_proj_scratch();
let v_c = seed_var(&mut vs, &mut defs, &mut nv, "c");
let (recv, method) = try_lower_field_proj_chain(
"c.writer.header.set",
&vs,
&mut interner,
0,
BlockId(0),
&mut nv,
&mut blocks,
&mut defs,
NodeIndex::new(0),
(0, 0),
)
.expect("chain decomposition should succeed");
assert_eq!(method, "set");
assert_eq!(recv, SsaValue(2));
assert_eq!(blocks[0].body.len(), 2, "expected 2 FieldProj ops");
match &blocks[0].body[0].op {
SsaOp::FieldProj {
receiver, field, ..
} => {
assert_eq!(*receiver, v_c);
assert_eq!(interner.resolve(*field), "writer");
}
other => panic!("expected FieldProj, got {other:?}"),
}
match &blocks[0].body[1].op {
SsaOp::FieldProj {
receiver, field, ..
} => {
assert_eq!(*receiver, SsaValue(1)); assert_eq!(interner.resolve(*field), "header");
}
other => panic!("expected FieldProj, got {other:?}"),
}
assert_eq!(blocks[0].body[0].var_name.as_deref(), Some("c.writer"));
assert_eq!(
blocks[0].body[1].var_name.as_deref(),
Some("c.writer.header")
);
}
#[test]
fn try_lower_field_proj_chain_dedupes_field_names() {
let (mut vs, mut interner, mut blocks, mut defs, mut nv) = fresh_proj_scratch();
let v_a = seed_var(&mut vs, &mut defs, &mut nv, "a");
let v_b = seed_var(&mut vs, &mut defs, &mut nv, "b");
let _ = try_lower_field_proj_chain(
"a.shared.f",
&vs,
&mut interner,
0,
BlockId(0),
&mut nv,
&mut blocks,
&mut defs,
NodeIndex::new(0),
(0, 0),
)
.unwrap();
let _ = try_lower_field_proj_chain(
"b.shared.g",
&vs,
&mut interner,
0,
BlockId(0),
&mut nv,
&mut blocks,
&mut defs,
NodeIndex::new(0),
(0, 0),
)
.unwrap();
assert_eq!(blocks[0].body.len(), 2);
let f0 = match &blocks[0].body[0].op {
SsaOp::FieldProj { field, .. } => *field,
_ => panic!(),
};
let f1 = match &blocks[0].body[1].op {
SsaOp::FieldProj { field, .. } => *field,
_ => panic!(),
};
assert_eq!(f0, f1, "dedup should reuse FieldId");
assert_eq!(interner.len(), 1, "only one unique field name interned");
let _ = (v_a, v_b);
}
#[test]
fn try_lower_field_proj_chain_rejects_empty_segments() {
let (mut vs, mut interner, mut blocks, mut defs, mut nv) = fresh_proj_scratch();
seed_var(&mut vs, &mut defs, &mut nv, "x");
for s in [".x.f", "x..f", "x.f."] {
assert!(
try_lower_field_proj_chain(
s,
&vs,
&mut interner,
0,
BlockId(0),
&mut nv,
&mut blocks,
&mut defs,
NodeIndex::new(0),
(0, 0),
)
.is_none(),
"expected bail on {s}"
);
}
assert!(blocks[0].body.is_empty());
}
fn parse_to_first_body(
src: &[u8],
lang: &str,
ts_lang: tree_sitter::Language,
path: &str,
) -> SsaBody {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&ts_lang).unwrap();
let tree = parser.parse(src, None).unwrap();
let file_cfg = crate::cfg::build_cfg(&tree, src, lang, path, None);
let body = if file_cfg.bodies.len() > 1 {
&file_cfg.bodies[1]
} else {
&file_cfg.bodies[0]
};
if body.meta.name.is_some() {
let func_name = body.meta.name.clone().unwrap_or_default();
lower_to_ssa_with_params(
&body.graph,
body.entry,
Some(&func_name),
false,
&body.meta.params,
)
.expect("SSA lowering should succeed")
} else {
lower_to_ssa(&body.graph, body.entry, None, true).expect("SSA lowering should succeed")
}
}
fn collect_field_projs(body: &SsaBody) -> Vec<(SsaValue, SsaValue, String)> {
let mut out = Vec::new();
for blk in &body.blocks {
for inst in blk.phis.iter().chain(blk.body.iter()) {
if let SsaOp::FieldProj {
receiver, field, ..
} = &inst.op
{
out.push((inst.value, *receiver, body.field_name(*field).to_string()));
}
}
}
out
}
fn collect_calls(body: &SsaBody) -> Vec<(String, Option<String>, Option<SsaValue>)> {
let mut out = Vec::new();
for blk in &body.blocks {
for inst in blk.body.iter() {
if let SsaOp::Call {
callee,
callee_text,
receiver,
..
} = &inst.op
{
out.push((callee.clone(), callee_text.clone(), *receiver));
}
}
}
out
}
#[test]
fn phase2_e2e_go_chained_receiver_emits_field_proj() {
let src = b"package p\nfunc f(c *T, k string, v string) { c.writer.header.set(k, v) }\n";
let body = parse_to_first_body(
src,
"go",
tree_sitter::Language::from(tree_sitter_go::LANGUAGE),
"test.go",
);
let projs = collect_field_projs(&body);
assert!(
projs.len() >= 2,
"expected ≥2 FieldProj ops for c.writer.header.<m>; got {projs:?}"
);
let names: Vec<&str> = projs.iter().map(|(_, _, n)| n.as_str()).collect();
assert!(
names.contains(&"writer"),
"missing 'writer' projection in {names:?}"
);
assert!(
names.contains(&"header"),
"missing 'header' projection in {names:?}"
);
let calls = collect_calls(&body);
let bare = calls.iter().find(|(c, _, _)| c == "set");
assert!(
bare.is_some(),
"expected a Call with bare callee 'set'; got {calls:?}"
);
let (_, ctext, recv) = bare.unwrap();
assert!(recv.is_some(), "decomposed call must carry an SSA receiver");
assert_eq!(
ctext.as_deref(),
Some("c.writer.header.set"),
"callee_text should preserve the original textual path"
);
}
#[test]
fn phase2_e2e_python_chained_receiver_emits_field_proj() {
let src = b"def f(obj, p):\n obj.client.session.send(p)\n";
let body = parse_to_first_body(
src,
"python",
tree_sitter::Language::from(tree_sitter_python::LANGUAGE),
"test.py",
);
let projs = collect_field_projs(&body);
let names: Vec<&str> = projs.iter().map(|(_, _, n)| n.as_str()).collect();
assert!(
names.contains(&"client") && names.contains(&"session"),
"expected client + session projections, got {names:?}"
);
let calls = collect_calls(&body);
assert!(
calls.iter().any(|(c, ct, r)| c == "send"
&& ct.as_deref() == Some("obj.client.session.send")
&& r.is_some()),
"expected bare 'send' Call with callee_text retained; got {calls:?}"
);
}
#[test]
fn phase2_e2e_javascript_chained_receiver_emits_field_proj() {
let src = b"function f(obj) { obj.foo.bar.baz(); }";
let body = parse_to_first_body(
src,
"javascript",
tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE),
"test.js",
);
let projs = collect_field_projs(&body);
let names: Vec<&str> = projs.iter().map(|(_, _, n)| n.as_str()).collect();
assert!(
names.contains(&"foo") && names.contains(&"bar"),
"expected foo + bar projections, got {names:?}"
);
}
#[test]
fn phase2_e2e_java_chained_receiver_emits_field_proj() {
let src = b"class C { void f(Object obj) { obj.config.handler.run(); } }";
let body = parse_to_first_body(
src,
"java",
tree_sitter::Language::from(tree_sitter_java::LANGUAGE),
"test.java",
);
let projs = collect_field_projs(&body);
let names: Vec<&str> = projs.iter().map(|(_, _, n)| n.as_str()).collect();
assert!(
names.contains(&"config") && names.contains(&"handler"),
"expected config + handler projections, got {names:?}; full body:\n{body}"
);
let calls = collect_calls(&body);
assert!(
calls.iter().any(|(c, ct, r)| c == "run"
&& ct.as_deref() == Some("obj.config.handler.run")
&& r.is_some()),
"expected bare 'run' Call with callee_text retained; got {calls:?}"
);
}
#[test]
fn phase2_e2e_simple_receiver_no_field_proj() {
let src = b"function f(obj) { obj.foo(); }";
let body = parse_to_first_body(
src,
"javascript",
tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE),
"test.js",
);
assert!(
collect_field_projs(&body).is_empty(),
"single-dot call should not generate FieldProj"
);
let calls = collect_calls(&body);
assert!(
calls.iter().any(|(_, ct, _)| ct.is_none()),
"single-dot Call should have callee_text=None; calls={calls:?}"
);
}
#[test]
fn phase2_e2e_bare_call_no_field_proj() {
let src = b"function f() { foo(1, 2); }";
let body = parse_to_first_body(
src,
"javascript",
tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE),
"test.js",
);
assert!(collect_field_projs(&body).is_empty());
assert!(
body.field_interner.is_empty(),
"no chain → interner stays empty"
);
}
#[test]
fn phase2_e2e_global_root_chain_still_emits_field_proj() {
let src = b"function f() { Math.foo.bar(); }";
let body = parse_to_first_body(
src,
"javascript",
tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE),
"test.js",
);
let projs = collect_field_projs(&body);
let names: Vec<&str> = projs.iter().map(|(_, _, n)| n.as_str()).collect();
assert!(
names.contains(&"foo"),
"expected 'foo' projection (chain root Math is a synthesized external var); got {names:?}"
);
}
#[test]
fn phase2_e2e_rust_method_call_through_field_emits_field_proj() {
let src = b"fn f(c: &T) { c.mu.lock(); }";
let body = parse_to_first_body(
src,
"rust",
tree_sitter::Language::from(tree_sitter_rust::LANGUAGE),
"test.rs",
);
let projs = collect_field_projs(&body);
let names: Vec<&str> = projs.iter().map(|(_, _, n)| n.as_str()).collect();
assert!(
names.contains(&"mu"),
"expected 'mu' projection from c.mu.lock(); got {names:?}; body:\n{body}"
);
let calls = collect_calls(&body);
assert!(
calls
.iter()
.any(|(c, ct, r)| c == "lock" && ct.as_deref() == Some("c.mu.lock") && r.is_some()),
"expected bare 'lock' Call with callee_text='c.mu.lock'; got {calls:?}"
);
}
#[test]
fn phase2_e2e_rust_path_call_does_not_emit_field_proj() {
let src = br#"fn f() { let _ = std::env::var("X"); }"#;
let body = parse_to_first_body(
src,
"rust",
tree_sitter::Language::from(tree_sitter_rust::LANGUAGE),
"test.rs",
);
assert!(
collect_field_projs(&body).is_empty(),
"Rust path expression must not be decomposed into FieldProj"
);
}
#[test]
fn phase2_e2e_field_interner_populated_only_when_chain_emitted() {
let src_chain = b"function f(o) { o.a.b.c(); }";
let src_plain = b"function f(o) { o.foo(); }";
let body_chain = parse_to_first_body(
src_chain,
"javascript",
tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE),
"test.js",
);
let body_plain = parse_to_first_body(
src_plain,
"javascript",
tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE),
"test.js",
);
assert!(
!body_chain.field_interner.is_empty(),
"interner should hold the chain field names"
);
assert!(
body_plain.field_interner.is_empty(),
"single-dot call should not populate interner"
);
}
#[test]
fn phase2_e2e_field_proj_chain_preserves_receiver_dataflow() {
let src = b"function f(c) { c.a.b.m(); }";
let body = parse_to_first_body(
src,
"javascript",
tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE),
"test.js",
);
let projs = collect_field_projs(&body);
assert_eq!(projs.len(), 2, "expected 2 FieldProj ops, got {projs:?}");
let v_first = projs[0].0;
let r_second = projs[1].1;
assert_eq!(
r_second, v_first,
"second FieldProj must chain off the first's value"
);
}
#[test]
fn w1_end_to_end_field_write_records_side_table_when_parent_has_prior_def() {
let src = b"function f(obj) { obj.cache = 42; }";
let body = parse_to_first_body(
src,
"javascript",
tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE),
"test.js",
);
assert!(
!body.field_writes.is_empty(),
"single `obj.cache = 42` on a JS formal must populate \
field_writes via the formal's W1.b synthetic Param; got \
body.field_writes={:?}\nbody:\n{body}",
body.field_writes,
);
for (_rcv, fid) in body.field_writes.values() {
assert_eq!(body.field_interner.resolve(*fid), "cache");
}
}
#[test]
fn w1b_single_write_records_field_write_python() {
let src = b"def f(obj):\n obj.cache = 42\n";
let body = parse_to_first_body(
src,
"python",
tree_sitter::Language::from(tree_sitter_python::LANGUAGE),
"test.py",
);
assert!(
!body.field_writes.is_empty(),
"Python single `obj.cache = 42` must populate field_writes; \
got body.field_writes={:?}\nbody:\n{body}",
body.field_writes,
);
}
#[test]
fn w1b_single_write_records_field_write_rust() {
let src = b"struct O { cache: i32 } fn f(obj: &mut O) { obj.cache = 42; }";
let body = parse_to_first_body(
src,
"rust",
tree_sitter::Language::from(tree_sitter_rust::LANGUAGE),
"test.rs",
);
assert!(
!body.field_writes.is_empty(),
"Rust single `obj.cache = 42` must populate field_writes; \
got body.field_writes={:?}\nbody:\n{body}",
body.field_writes,
);
}
#[test]
fn arrow_with_handler_formal_keeps_param_non_synthetic() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(NodeInfo {
ast: crate::cfg::AstMeta {
enclosing_func: Some("lookup".into()),
..Default::default()
},
..make_node(StmtKind::Entry)
});
let use_node = cfg.add_node(NodeInfo {
taint: TaintMeta {
uses: vec!["userId".into()],
..Default::default()
},
ast: crate::cfg::AstMeta {
enclosing_func: Some("lookup".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(NodeInfo {
ast: crate::cfg::AstMeta {
enclosing_func: Some("lookup".into()),
..Default::default()
},
..make_node(StmtKind::Exit)
});
cfg.add_edge(entry, use_node, EdgeKind::Seq);
cfg.add_edge(use_node, exit, EdgeKind::Seq);
let formals = vec!["userId".to_string()];
let body = lower_to_ssa_with_params(&cfg, entry, Some("lookup"), false, &formals)
.expect("SSA lowering should succeed");
let user_id_param = body
.blocks
.first()
.and_then(|b| {
b.body.iter().find(|inst| {
matches!(inst.op, SsaOp::Param { .. })
&& inst.var_name.as_deref() == Some("userId")
})
})
.expect("userId Param should be present");
assert!(
!body.synthetic_externals.contains(&user_id_param.value),
"real formal `userId` must not be marked synthetic; \
synthetic_externals={:?}",
body.synthetic_externals,
);
}
#[test]
fn w1_end_to_end_plain_assign_records_no_field_write() {
let src = b"function f() { let x = 1; x = 2; }";
let body = parse_to_first_body(
src,
"javascript",
tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE),
"test.js",
);
assert!(
body.field_writes.is_empty(),
"plain assign must not populate field_writes; got {:?}",
body.field_writes,
);
}
#[test]
fn loop_self_assignment_induction_phi_is_distinct() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let init_x = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let header = cfg.add_node(make_node(StmtKind::Loop));
let body = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
uses: vec!["x".into()],
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, init_x, EdgeKind::Seq);
cfg.add_edge(init_x, header, EdgeKind::Seq);
cfg.add_edge(header, body, EdgeKind::True);
cfg.add_edge(body, header, EdgeKind::Back);
cfg.add_edge(header, exit, EdgeKind::False);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
let x_defs: Vec<_> = ssa
.value_defs
.iter()
.filter(|vd| vd.var_name.as_deref() == Some("x"))
.collect();
assert!(
x_defs.len() >= 3,
"expected ≥3 SSA values for x (init, phi, body-redef), got {}",
x_defs.len()
);
let phi_ops = ssa
.blocks
.iter()
.flat_map(|b| b.phis.iter())
.find(|p| p.var_name.as_deref() == Some("x"))
.and_then(|p| match &p.op {
SsaOp::Phi(ops) => Some(ops.clone()),
_ => None,
})
.expect("expected a Phi op for x at the loop header");
assert_eq!(
phi_ops.len(),
2,
"loop header phi for x should have 2 operands, got {}",
phi_ops.len()
);
let unique: HashSet<_> = phi_ops.iter().map(|(_, v)| v).collect();
assert_eq!(
unique.len(),
2,
"phi operands must be distinct (entry vs back-edge), got {:?}",
phi_ops
);
}
#[test]
fn diamond_join_produces_phi_per_variable() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let cond = cfg.add_node(make_node(StmtKind::If));
let true_def = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let true_def2 = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("y".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let false_def = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let false_def2 = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("y".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let join = cfg.add_node(NodeInfo {
taint: TaintMeta {
uses: vec!["x".into(), "y".into()],
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, cond, EdgeKind::Seq);
cfg.add_edge(cond, true_def, EdgeKind::True);
cfg.add_edge(true_def, true_def2, EdgeKind::Seq);
cfg.add_edge(true_def2, join, EdgeKind::Seq);
cfg.add_edge(cond, false_def, EdgeKind::False);
cfg.add_edge(false_def, false_def2, EdgeKind::Seq);
cfg.add_edge(false_def2, join, EdgeKind::Seq);
cfg.add_edge(join, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
let phi_vars: HashSet<&str> = ssa
.blocks
.iter()
.flat_map(|b| b.phis.iter())
.filter_map(|p| p.var_name.as_deref())
.collect();
assert!(
phi_vars.contains("x"),
"expected phi for x at diamond join, got {:?}",
phi_vars
);
assert!(
phi_vars.contains("y"),
"expected phi for y at diamond join, got {:?}",
phi_vars
);
}
#[test]
fn two_branches_with_returns_each_terminates_with_return() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let cond = cfg.add_node(make_node(StmtKind::If));
let r1 = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("r1".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let ret1 = cfg.add_node(NodeInfo {
taint: TaintMeta {
uses: vec!["r1".into()],
..Default::default()
},
..make_node(StmtKind::Return)
});
let r2 = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("r2".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let ret2 = cfg.add_node(NodeInfo {
taint: TaintMeta {
uses: vec!["r2".into()],
..Default::default()
},
..make_node(StmtKind::Return)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, cond, EdgeKind::Seq);
cfg.add_edge(cond, r1, EdgeKind::True);
cfg.add_edge(r1, ret1, EdgeKind::Seq);
cfg.add_edge(ret1, exit, EdgeKind::Seq);
cfg.add_edge(cond, r2, EdgeKind::False);
cfg.add_edge(r2, ret2, EdgeKind::Seq);
cfg.add_edge(ret2, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
let return_blocks = ssa
.blocks
.iter()
.filter(|b| matches!(&b.terminator, Terminator::Return(_)))
.count();
assert_eq!(
return_blocks, 2,
"expected 2 Return-terminated blocks, got {}",
return_blocks
);
}
#[test]
fn conditional_define_only_one_arm_phi_has_undef_operand() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let cond = cfg.add_node(make_node(StmtKind::If));
let true_def = cfg.add_node(NodeInfo {
taint: TaintMeta {
defines: Some("x".into()),
..Default::default()
},
..make_node(StmtKind::Seq)
});
let false_nop = cfg.add_node(make_node(StmtKind::Seq));
let join = cfg.add_node(NodeInfo {
taint: TaintMeta {
uses: vec!["x".into()],
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, cond, EdgeKind::Seq);
cfg.add_edge(cond, true_def, EdgeKind::True);
cfg.add_edge(true_def, join, EdgeKind::Seq);
cfg.add_edge(cond, false_nop, EdgeKind::False);
cfg.add_edge(false_nop, join, EdgeKind::Seq);
cfg.add_edge(join, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
let x_phi_ops = ssa
.blocks
.iter()
.flat_map(|b| b.phis.iter())
.find(|p| p.var_name.as_deref() == Some("x"))
.and_then(|p| match &p.op {
SsaOp::Phi(ops) => Some(ops.clone()),
_ => None,
});
if let Some(ops) = x_phi_ops {
assert_eq!(
ops.len(),
2,
"phi for x at the join must have 2 operands (one per pred), got {}",
ops.len()
);
}
}
#[test]
fn empty_function_body_only_entry_and_exit_lowers_cleanly() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, exit, EdgeKind::Seq);
let ssa = lower_to_ssa(&cfg, entry, None, true).unwrap();
assert!(
!ssa.blocks.is_empty(),
"even an empty body should produce at least one block"
);
}
}