use super::ir::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvariantError {
pub messages: Vec<String>,
}
impl InvariantError {
pub fn joined(&self) -> String {
self.messages.join("\n")
}
}
impl std::fmt::Display for InvariantError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.joined())
}
}
impl std::error::Error for InvariantError {}
pub fn check_structural_invariants(body: &SsaBody) -> Vec<String> {
let mut errors = Vec::new();
check_block_ids(body, &mut errors);
check_entry_has_no_preds(body, &mut errors);
check_pred_succ_symmetry(body, &mut errors);
check_terminator_succ_agreement(body, &mut errors);
check_phi_placement_and_arity(body, &mut errors);
check_phi_operand_sources(body, &mut errors);
check_unique_definitions(body, &mut errors);
check_value_def_coverage(body, &mut errors);
check_cfg_node_map(body, &mut errors);
check_reachability(body, &mut errors);
if let Err(e) = check_catch_block_reachability(body) {
errors.extend(e.messages);
}
errors
}
pub fn check_catch_block_reachability(body: &SsaBody) -> Result<(), InvariantError> {
let n = body.blocks.len();
if n == 0 {
return Ok(());
}
let catch_blocks: Vec<BlockId> = body
.blocks
.iter()
.filter(|b| {
b.phis
.iter()
.chain(b.body.iter())
.any(|inst| matches!(inst.op, SsaOp::CatchParam))
})
.map(|b| b.id)
.collect();
if catch_blocks.is_empty() {
return Ok(());
}
let mut reachable = vec![false; n];
let entry_idx = body.entry.0 as usize;
if entry_idx < n {
reachable[entry_idx] = true;
let mut stack: Vec<BlockId> = vec![body.entry];
while let Some(b) = stack.pop() {
for &s in &body.blocks[b.0 as usize].succs {
let sidx = s.0 as usize;
if sidx < n && !reachable[sidx] {
reachable[sidx] = true;
stack.push(s);
}
}
}
}
let exception_targets: std::collections::HashSet<BlockId> = body
.exception_edges
.iter()
.map(|(_, catch)| *catch)
.collect();
let mut messages = Vec::new();
for bid in catch_blocks {
let idx = bid.0 as usize;
let normal = idx < n && reachable[idx];
let via_exception = exception_targets.contains(&bid);
if !normal && !via_exception {
messages.push(format!(
"catch-block orphan: block {:?} carries CatchParam but is neither \
reachable from entry {:?} nor a target of any exception edge",
bid, body.entry
));
}
}
if messages.is_empty() {
Ok(())
} else {
Err(InvariantError { messages })
}
}
fn check_block_ids(body: &SsaBody, errors: &mut Vec<String>) {
for (i, block) in body.blocks.iter().enumerate() {
if block.id.0 as usize != i {
errors.push(format!(
"block at index {i} has mismatched id {:?}",
block.id
));
}
}
}
fn check_entry_has_no_preds(body: &SsaBody, errors: &mut Vec<String>) {
let entry_idx = body.entry.0 as usize;
if entry_idx >= body.blocks.len() {
errors.push(format!("entry {:?} is out of bounds", body.entry));
return;
}
let entry = &body.blocks[entry_idx];
if !entry.preds.is_empty() {
errors.push(format!(
"entry block {:?} has {} predecessor(s): {:?}",
body.entry,
entry.preds.len(),
entry.preds
));
}
}
fn check_pred_succ_symmetry(body: &SsaBody, errors: &mut Vec<String>) {
for block in &body.blocks {
for &succ in &block.succs {
let sidx = succ.0 as usize;
if sidx >= body.blocks.len() {
errors.push(format!(
"block {:?} has out-of-bounds succ {:?}",
block.id, succ
));
continue;
}
if !body.blocks[sidx].preds.contains(&block.id) {
errors.push(format!(
"block {:?} lists succ {:?} but {:?} does not list {:?} as pred",
block.id, succ, succ, block.id
));
}
}
for &pred in &block.preds {
let pidx = pred.0 as usize;
if pidx >= body.blocks.len() {
errors.push(format!(
"block {:?} has out-of-bounds pred {:?}",
block.id, pred
));
continue;
}
if !body.blocks[pidx].succs.contains(&block.id) {
errors.push(format!(
"block {:?} lists pred {:?} but {:?} does not list {:?} as succ",
block.id, pred, pred, block.id
));
}
}
}
}
fn check_terminator_succ_agreement(body: &SsaBody, errors: &mut Vec<String>) {
for block in &body.blocks {
match &block.terminator {
Terminator::Goto(target) => {
if !block.succs.iter().any(|s| s == target) {
errors.push(format!(
"block {:?} Goto({:?}) target not in succs {:?}",
block.id, target, block.succs
));
}
}
Terminator::Branch {
true_blk,
false_blk,
..
} => {
if !block.succs.iter().any(|s| s == true_blk) {
errors.push(format!(
"block {:?} Branch true target {:?} not in succs {:?}",
block.id, true_blk, block.succs
));
}
if !block.succs.iter().any(|s| s == false_blk) {
errors.push(format!(
"block {:?} Branch false target {:?} not in succs {:?}",
block.id, false_blk, block.succs
));
}
}
Terminator::Switch {
targets, default, ..
} => {
for t in targets {
if !block.succs.iter().any(|s| s == t) {
errors.push(format!(
"block {:?} Switch target {:?} not in succs {:?}",
block.id, t, block.succs
));
}
}
if !block.succs.iter().any(|s| s == default) {
errors.push(format!(
"block {:?} Switch default {:?} not in succs {:?}",
block.id, default, block.succs
));
}
}
Terminator::Return(_) | Terminator::Unreachable => {
}
}
}
}
fn check_phi_placement_and_arity(body: &SsaBody, errors: &mut Vec<String>) {
for block in &body.blocks {
for inst in &block.body {
if matches!(inst.op, SsaOp::Phi(_)) {
errors.push(format!(
"block {:?} has a Phi op in body (should be in phis): value {:?}",
block.id, inst.value
));
}
}
for inst in &block.phis {
if !matches!(inst.op, SsaOp::Phi(_)) {
errors.push(format!(
"block {:?} has non-Phi op in phis slot: value {:?}",
block.id, inst.value
));
}
if let SsaOp::Phi(ref ops) = inst.op
&& ops.len() > block.preds.len()
{
errors.push(format!(
"block {:?} phi for {:?} has {} operand(s) > {} pred(s)",
block.id,
inst.value,
ops.len(),
block.preds.len()
));
}
}
}
}
fn check_phi_operand_sources(body: &SsaBody, errors: &mut Vec<String>) {
for block in &body.blocks {
for inst in &block.phis {
if let SsaOp::Phi(ref ops) = inst.op {
for &(pred_bid, operand_value) in ops.iter() {
if !block.preds.contains(&pred_bid) {
errors.push(format!(
"block {:?} phi for {:?} references non-pred {:?} (preds: {:?})",
block.id, inst.value, pred_bid, block.preds
));
}
if (operand_value.0 as usize) >= body.value_defs.len() {
errors.push(format!(
"block {:?} phi for {:?} has operand {:?} out of value_defs range",
block.id, inst.value, operand_value
));
}
}
}
}
}
}
fn check_unique_definitions(body: &SsaBody, errors: &mut Vec<String>) {
let mut seen: std::collections::HashMap<SsaValue, BlockId> =
std::collections::HashMap::with_capacity(body.value_defs.len());
for block in &body.blocks {
for inst in block.phis.iter().chain(block.body.iter()) {
if let Some(prev) = seen.insert(inst.value, block.id) {
errors.push(format!(
"SSA {:?} defined in both {:?} and {:?} — single-assignment violated",
inst.value, prev, block.id
));
}
}
}
}
fn check_value_def_coverage(body: &SsaBody, errors: &mut Vec<String>) {
for block in &body.blocks {
for inst in block.phis.iter().chain(block.body.iter()) {
let idx = inst.value.0 as usize;
if idx >= body.value_defs.len() {
errors.push(format!(
"instruction defining {:?} in block {:?} has no entry in value_defs (len {})",
inst.value,
block.id,
body.value_defs.len()
));
continue;
}
let def = &body.value_defs[idx];
if def.block != block.id {
errors.push(format!(
"value_defs[{}] records block {:?} but instruction lives in block {:?}",
idx, def.block, block.id
));
}
}
}
}
fn check_cfg_node_map(body: &SsaBody, errors: &mut Vec<String>) {
for (&cfg_node, &sv) in body.cfg_node_map.iter() {
let idx = sv.0 as usize;
if idx >= body.value_defs.len() {
errors.push(format!(
"cfg_node_map points {:?} → {:?} which is out of value_defs range",
cfg_node, sv
));
continue;
}
let def = &body.value_defs[idx];
if def.cfg_node != cfg_node {
errors.push(format!(
"cfg_node_map inconsistency: map says {:?} → {:?}, but value_defs[{}].cfg_node = {:?}",
cfg_node, sv, idx, def.cfg_node
));
}
}
}
fn check_reachability(body: &SsaBody, errors: &mut Vec<String>) {
let n = body.blocks.len();
if n == 0 {
errors.push("body has zero blocks".into());
return;
}
let entry_idx = body.entry.0 as usize;
if entry_idx >= n {
return;
}
let mut visited = vec![false; n];
let mut stack: Vec<BlockId> = Vec::new();
let seed = |bid: BlockId, visited: &mut [bool], stack: &mut Vec<BlockId>| {
let idx = bid.0 as usize;
if idx < visited.len() && !visited[idx] {
visited[idx] = true;
stack.push(bid);
}
};
seed(body.entry, &mut visited, &mut stack);
for (_src, catch_target) in &body.exception_edges {
seed(*catch_target, &mut visited, &mut stack);
}
while let Some(bid) = stack.pop() {
let block = &body.blocks[bid.0 as usize];
for &s in &block.succs {
let sidx = s.0 as usize;
if sidx < n && !visited[sidx] {
visited[sidx] = true;
stack.push(s);
}
}
}
for (i, v) in visited.iter().enumerate() {
if !*v {
let block = &body.blocks[i];
errors.push(format!(
"block {:?} is unreachable from entry {:?} or any exception-handler root",
block.id, body.entry
));
}
}
}
pub fn body_fingerprint(body: &SsaBody) -> String {
use std::fmt::Write;
let mut out = String::new();
let _ = writeln!(out, "entry={:?}", body.entry);
let _ = writeln!(out, "blocks={}", body.blocks.len());
for block in &body.blocks {
let _ = writeln!(
out,
" b{:?} preds={} succs={} phis={} body={} term={}",
block.id,
block.preds.len(),
block.succs.len(),
block.phis.len(),
block.body.len(),
terminator_kind(&block.terminator),
);
for inst in &block.phis {
if let SsaOp::Phi(ref ops) = inst.op {
let _ = writeln!(
out,
" phi var={} operands={}",
inst.var_name.as_deref().unwrap_or(""),
ops.len(),
);
}
}
for inst in &block.body {
let _ = writeln!(out, " {}", op_kind(&inst.op));
}
}
out
}
fn terminator_kind(t: &Terminator) -> &'static str {
match t {
Terminator::Goto(_) => "Goto",
Terminator::Branch { .. } => "Branch",
Terminator::Switch { .. } => "Switch",
Terminator::Return(_) => "Return",
Terminator::Unreachable => "Unreachable",
}
}
fn op_kind(op: &SsaOp) -> &'static str {
match op {
SsaOp::Phi(_) => "Phi",
SsaOp::Assign(_) => "Assign",
SsaOp::Call { .. } => "Call",
SsaOp::Source => "Source",
SsaOp::Const(_) => "Const",
SsaOp::Param { .. } => "Param",
SsaOp::SelfParam => "SelfParam",
SsaOp::CatchParam => "CatchParam",
SsaOp::Nop => "Nop",
SsaOp::Undef => "Undef",
SsaOp::FieldProj { .. } => "FieldProj",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cfg::{Cfg, EdgeKind, NodeInfo, StmtKind, TaintMeta};
use crate::ssa::lower_to_ssa;
use petgraph::Graph;
use petgraph::graph::NodeIndex;
fn make_node(kind: StmtKind) -> NodeInfo {
NodeInfo {
kind,
..Default::default()
}
}
fn def(var: &str) -> NodeInfo {
NodeInfo {
taint: TaintMeta {
defines: Some(var.into()),
..Default::default()
},
..make_node(StmtKind::Seq)
}
}
fn use_var(var: &str) -> NodeInfo {
NodeInfo {
taint: TaintMeta {
uses: vec![var.into()],
..Default::default()
},
..make_node(StmtKind::Seq)
}
}
fn assert_well_formed(body: &SsaBody) {
let errs = check_structural_invariants(body);
assert!(
errs.is_empty(),
"structural invariants failed:\n{}",
errs.join("\n")
);
}
#[test]
fn linear_cfg_is_well_formed() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let n1 = cfg.add_node(def("x"));
let n2 = cfg.add_node(use_var("x"));
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 body = lower_to_ssa(&cfg, entry, None, true).unwrap();
assert_well_formed(&body);
}
#[test]
fn diamond_cfg_is_well_formed() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let if_n = cfg.add_node(make_node(StmtKind::If));
let t = cfg.add_node(def("x"));
let f = cfg.add_node(def("x"));
let join = cfg.add_node(use_var("x"));
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, if_n, EdgeKind::Seq);
cfg.add_edge(if_n, t, EdgeKind::True);
cfg.add_edge(if_n, 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 body = lower_to_ssa(&cfg, entry, None, true).unwrap();
assert_well_formed(&body);
let phi_block = body
.blocks
.iter()
.find(|b| !b.phis.is_empty())
.expect("diamond should produce a phi");
for phi in &phi_block.phis {
if let SsaOp::Phi(ref ops) = phi.op {
for (pred, _) in ops {
assert!(
phi_block.preds.iter().any(|p| p == pred),
"phi operand {pred:?} is not a pred of {:?}",
phi_block.id
);
}
}
}
}
#[test]
fn loop_cfg_is_well_formed() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let init = cfg.add_node(def("x"));
let header = cfg.add_node(make_node(StmtKind::Loop));
let body_n = cfg.add_node(def("x"));
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, init, EdgeKind::Seq);
cfg.add_edge(init, header, EdgeKind::Seq);
cfg.add_edge(header, body_n, EdgeKind::True);
cfg.add_edge(body_n, header, EdgeKind::Back);
cfg.add_edge(header, exit, EdgeKind::False);
let body = lower_to_ssa(&cfg, entry, None, true).unwrap();
assert_well_formed(&body);
}
#[test]
fn fingerprint_is_stable_on_double_lowering() {
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let if_n = cfg.add_node(make_node(StmtKind::If));
let t = cfg.add_node(def("x"));
let f = cfg.add_node(def("x"));
let join = cfg.add_node(use_var("x"));
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, if_n, EdgeKind::Seq);
cfg.add_edge(if_n, t, EdgeKind::True);
cfg.add_edge(if_n, 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 a = lower_to_ssa(&cfg, entry, None, true).unwrap();
let b = lower_to_ssa(&cfg, entry, None, true).unwrap();
assert_eq!(body_fingerprint(&a), body_fingerprint(&b));
}
#[test]
fn phis_are_emitted_in_alphabetical_order() {
fn defs(vars: &[&str]) -> NodeInfo {
NodeInfo {
taint: TaintMeta {
defines: Some(vars[0].into()),
..Default::default()
},
..make_node(StmtKind::Seq)
}
}
let mut cfg: Cfg = Graph::new();
let entry = cfg.add_node(make_node(StmtKind::Entry));
let if_n = cfg.add_node(make_node(StmtKind::If));
let t_c = cfg.add_node(defs(&["c"]));
let t_a = cfg.add_node(defs(&["a"]));
let t_b = cfg.add_node(defs(&["b"]));
let f_b = cfg.add_node(defs(&["b"]));
let f_c = cfg.add_node(defs(&["c"]));
let f_a = cfg.add_node(defs(&["a"]));
let join = cfg.add_node(NodeInfo {
taint: TaintMeta {
uses: vec!["a".into(), "b".into(), "c".into()],
..Default::default()
},
..make_node(StmtKind::Seq)
});
let exit = cfg.add_node(make_node(StmtKind::Exit));
cfg.add_edge(entry, if_n, EdgeKind::Seq);
cfg.add_edge(if_n, t_c, EdgeKind::True);
cfg.add_edge(t_c, t_a, EdgeKind::Seq);
cfg.add_edge(t_a, t_b, EdgeKind::Seq);
cfg.add_edge(t_b, join, EdgeKind::Seq);
cfg.add_edge(if_n, f_b, EdgeKind::False);
cfg.add_edge(f_b, f_c, EdgeKind::Seq);
cfg.add_edge(f_c, f_a, EdgeKind::Seq);
cfg.add_edge(f_a, join, EdgeKind::Seq);
cfg.add_edge(join, exit, EdgeKind::Seq);
let body = lower_to_ssa(&cfg, entry, None, true).unwrap();
let join_block = body
.blocks
.iter()
.find(|b| b.phis.len() >= 3)
.expect("join block should carry phis for a, b, c");
let names: Vec<&str> = join_block
.phis
.iter()
.filter_map(|inst| inst.var_name.as_deref())
.collect();
assert_eq!(
names,
vec!["a", "b", "c"],
"phis within a block must be emitted in alphabetical var_name order"
);
}
#[test]
fn broken_pred_succ_symmetry_is_detected() {
use smallvec::smallvec;
let body = SsaBody {
blocks: vec![
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(1),
phis: vec![],
body: vec![],
terminator: Terminator::Unreachable,
preds: smallvec![], succs: smallvec![],
},
],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: Default::default(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let errs = check_structural_invariants(&body);
assert!(
errs.iter().any(|e| e.contains("does not list")),
"expected a symmetry violation, got: {:?}",
errs
);
}
#[test]
fn duplicate_ssa_def_is_detected() {
use smallvec::smallvec;
let dummy_cfg = NodeIndex::new(0);
let body = SsaBody {
blocks: vec![SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![
SsaInst {
value: SsaValue(0),
op: SsaOp::Const(None),
cfg_node: dummy_cfg,
var_name: None,
span: (0, 0),
},
SsaInst {
value: SsaValue(0), op: SsaOp::Const(None),
cfg_node: dummy_cfg,
var_name: None,
span: (0, 0),
},
],
terminator: Terminator::Unreachable,
preds: smallvec![],
succs: smallvec![],
}],
entry: BlockId(0),
value_defs: vec![ValueDef {
var_name: None,
cfg_node: dummy_cfg,
block: BlockId(0),
}],
cfg_node_map: Default::default(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let errs = check_structural_invariants(&body);
assert!(
errs.iter()
.any(|e| e.contains("single-assignment violated")),
"expected a duplicate-def violation, got: {:?}",
errs
);
}
#[test]
fn phi_operand_from_non_pred_is_detected() {
use smallvec::smallvec;
let dummy_cfg = NodeIndex::new(0);
let body = SsaBody {
blocks: vec![
SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![BlockId(1)],
},
SsaBlock {
id: BlockId(1),
phis: vec![SsaInst {
value: SsaValue(0),
op: SsaOp::Phi(smallvec![(BlockId(2), SsaValue(0))]),
cfg_node: dummy_cfg,
var_name: Some("x".into()),
span: (0, 0),
}],
body: vec![],
terminator: Terminator::Unreachable,
preds: smallvec![BlockId(0)],
succs: smallvec![],
},
],
entry: BlockId(0),
value_defs: vec![ValueDef {
var_name: Some("x".into()),
cfg_node: dummy_cfg,
block: BlockId(1),
}],
cfg_node_map: Default::default(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let errs = check_structural_invariants(&body);
assert!(
errs.iter().any(|e| e.contains("references non-pred")),
"expected a phi-operand-source violation, got: {:?}",
errs
);
}
#[test]
fn terminator_disagreeing_with_succs_is_detected() {
use smallvec::smallvec;
let body = SsaBody {
blocks: vec![SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Goto(BlockId(1)),
preds: smallvec![],
succs: smallvec![],
}],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: Default::default(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let errs = check_structural_invariants(&body);
assert!(
errs.iter().any(|e| e.contains("Goto")),
"expected a terminator/succ disagreement, got: {:?}",
errs
);
}
}