use std::collections::HashMap;
use crate::{
analysis::cfg::SsaCfg,
graph::{
algorithms::{compute_dominators, DominatorTree},
NodeId, RootedGraph,
},
ir::{
function::SsaFunction,
variable::{DefSite, SsaVarId},
},
target::Target,
BitSet,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifierDefSite {
pub block: usize,
pub kind: DefKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DefKind {
Phi(usize),
Instruction(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerifierError {
UndefinedUse {
block: usize,
instr_idx: usize,
var: SsaVarId,
},
MissingPhiOperand {
block: usize,
phi_idx: usize,
missing_pred: usize,
},
ExtraPhiOperand {
block: usize,
phi_idx: usize,
extra_pred: usize,
},
DuplicateDefinition {
var: SsaVarId,
def1: VerifierDefSite,
def2: VerifierDefSite,
},
OrphanVariable {
var: SsaVarId,
},
UnregisteredVariable {
var: SsaVarId,
},
MissingTerminator {
block: usize,
},
PhiInEntryBlock {
block: usize,
phi_idx: usize,
},
DominanceViolation {
var: SsaVarId,
def_block: usize,
use_block: usize,
},
TerminatorNotLast {
block: usize,
instr_idx: usize,
instr_count: usize,
},
IntraBlockCycle {
block: usize,
use_instr: usize,
def_instr: usize,
var: SsaVarId,
},
PlaceholderVariable {
block: usize,
location: String,
},
SelfReferentialInstruction {
block: usize,
instr_idx: usize,
var: SsaVarId,
},
}
impl std::fmt::Display for VerifierError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UndefinedUse {
block,
instr_idx,
var,
} => write!(
f,
"Block {block}: instruction {instr_idx} uses undefined variable {var:?}"
),
Self::MissingPhiOperand {
block,
phi_idx,
missing_pred,
} => write!(
f,
"Block {block}: phi {phi_idx} missing operand for predecessor {missing_pred}"
),
Self::ExtraPhiOperand {
block,
phi_idx,
extra_pred,
} => write!(
f,
"Block {block}: phi {phi_idx} has operand for non-predecessor {extra_pred}"
),
Self::DuplicateDefinition { var, def1, def2 } => write!(
f,
"Variable {var:?} defined twice: at block {} ({:?}) and block {} ({:?})",
def1.block, def1.kind, def2.block, def2.kind
),
Self::OrphanVariable { var } => {
write!(f, "Variable {var:?} in variables vec but not defined in any block")
}
Self::UnregisteredVariable { var } => write!(
f,
"Variable {var:?} used in instruction but not in variables vec"
),
Self::MissingTerminator { block } => {
write!(f, "Block {block}: has successors but no terminator")
}
Self::PhiInEntryBlock { block, phi_idx } => {
write!(f, "Block {block}: phi {phi_idx} in entry block")
}
Self::DominanceViolation {
var,
def_block,
use_block,
} => write!(
f,
"Variable {var:?}: def in block {def_block} does not dominate use in block {use_block}"
),
Self::TerminatorNotLast {
block,
instr_idx,
instr_count,
} => write!(
f,
"Block {block}: terminator at position {instr_idx}/{instr_count} is not last"
),
Self::IntraBlockCycle {
block,
use_instr,
def_instr,
var,
} => write!(
f,
"Block {block}: instruction {use_instr} uses {var:?} defined at instruction {def_instr}"
),
Self::PlaceholderVariable { block, location } => write!(
f,
"Block {block}: placeholder variable ID (usize::MAX) at {location}"
),
Self::SelfReferentialInstruction {
block,
instr_idx,
var,
} => write!(
f,
"Block {block}: instruction {instr_idx} has self-referential use of {var:?}"
),
}
}
}
impl std::error::Error for VerifierError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum VerifyLevel {
Quick,
Standard,
Full,
}
pub struct SsaVerifier<'a, T: Target> {
ssa: &'a SsaFunction<T>,
errors: Vec<VerifierError>,
}
impl<'a, T: Target> SsaVerifier<'a, T> {
#[must_use]
pub fn new(ssa: &'a SsaFunction<T>) -> Self {
Self {
ssa,
errors: Vec::new(),
}
}
pub fn verify(mut self, level: VerifyLevel) -> Vec<VerifierError> {
self.errors.clear();
self.check_single_definition();
self.check_block_structure();
self.check_no_placeholders_or_self_refs();
if level >= VerifyLevel::Standard {
let cfg = SsaCfg::from_ssa(self.ssa);
let definitions = self.collect_definitions();
self.check_phi_operands(&cfg);
self.check_defined_before_use(&definitions);
self.check_registered_variables();
if level >= VerifyLevel::Full {
let dom_tree = compute_dominators(&cfg, cfg.entry());
self.check_dominance(&cfg, &dom_tree, &definitions);
}
}
self.errors
}
fn check_single_definition(&mut self) {
let mut definitions: HashMap<SsaVarId, VerifierDefSite> = HashMap::new();
for (block_idx, block) in self.ssa.blocks().iter().enumerate() {
for (phi_idx, phi) in block.phi_nodes().iter().enumerate() {
let var = phi.result();
let site = VerifierDefSite {
block: block_idx,
kind: DefKind::Phi(phi_idx),
};
if let Some(prev) = definitions.get(&var) {
self.errors.push(VerifierError::DuplicateDefinition {
var,
def1: prev.clone(),
def2: site,
});
} else {
definitions.insert(var, site);
}
}
for (instr_idx, instr) in block.instructions().iter().enumerate() {
if let Some(dest) = instr.op().dest() {
let site = VerifierDefSite {
block: block_idx,
kind: DefKind::Instruction(instr_idx),
};
if let Some(prev) = definitions.get(&dest) {
self.errors.push(VerifierError::DuplicateDefinition {
var: dest,
def1: prev.clone(),
def2: site,
});
} else {
definitions.insert(dest, site);
}
}
}
}
}
fn check_block_structure(&mut self) {
for (block_idx, block) in self.ssa.blocks().iter().enumerate() {
let instrs = block.instructions();
let instr_count = instrs.len();
for (instr_idx, instr) in instrs.iter().enumerate() {
if instr.op().is_terminator() && instr_idx < instr_count.saturating_sub(1) {
self.errors.push(VerifierError::TerminatorNotLast {
block: block_idx,
instr_idx,
instr_count,
});
}
}
let mut def_indices: HashMap<SsaVarId, usize> = HashMap::new();
for (instr_idx, instr) in instrs.iter().enumerate() {
if let Some(dest) = instr.op().dest() {
def_indices.insert(dest, instr_idx);
}
}
for (instr_idx, instr) in instrs.iter().enumerate() {
for used_var in instr.op().uses() {
if let Some(&def_idx) = def_indices.get(&used_var) {
if def_idx >= instr_idx {
self.errors.push(VerifierError::IntraBlockCycle {
block: block_idx,
use_instr: instr_idx,
def_instr: def_idx,
var: used_var,
});
}
}
}
}
}
}
fn collect_definitions(&self) -> HashMap<SsaVarId, (usize, DefSite)> {
let mut defs: HashMap<SsaVarId, (usize, DefSite)> = HashMap::new();
for var in self.ssa.variables() {
defs.insert(var.id(), (var.def_site().block, var.def_site()));
}
for (block_idx, block) in self.ssa.blocks().iter().enumerate() {
for phi in block.phi_nodes() {
defs.entry(phi.result())
.or_insert((block_idx, DefSite::phi(block_idx)));
}
for (instr_idx, instr) in block.instructions().iter().enumerate() {
if let Some(dest) = instr.op().dest() {
defs.entry(dest)
.or_insert((block_idx, DefSite::instruction(block_idx, instr_idx)));
}
}
}
defs
}
fn check_phi_operands(&mut self, cfg: &SsaCfg<'_, T>) {
let block_count = self.ssa.block_count();
for (block_idx, block) in self.ssa.blocks().iter().enumerate() {
let pred_list = cfg.block_predecessors(block_idx);
let max_phi_pred = block
.phi_nodes()
.iter()
.flat_map(|phi| phi.operands().iter().map(|op| op.predecessor()))
.max()
.unwrap_or(0);
let capacity = block_count.max(max_phi_pred.saturating_add(1)).max(1);
let mut preds = BitSet::new(capacity);
for &p in pred_list {
if p < capacity {
preds.insert(p);
}
}
for (phi_idx, phi) in block.phi_nodes().iter().enumerate() {
if block_idx == 0 && preds.is_empty() {
self.errors.push(VerifierError::PhiInEntryBlock {
block: block_idx,
phi_idx,
});
continue;
}
let mut operand_preds = BitSet::new(capacity);
for op in phi.operands() {
let pred = op.predecessor();
operand_preds.insert(pred);
}
for pred in preds.iter() {
if !operand_preds.contains(pred) {
self.errors.push(VerifierError::MissingPhiOperand {
block: block_idx,
phi_idx,
missing_pred: pred,
});
}
}
for op_pred in operand_preds.iter() {
if !preds.contains(op_pred) {
self.errors.push(VerifierError::ExtraPhiOperand {
block: block_idx,
phi_idx,
extra_pred: op_pred,
});
}
}
}
}
}
fn check_defined_before_use(&mut self, definitions: &HashMap<SsaVarId, (usize, DefSite)>) {
for (block_idx, block) in self.ssa.blocks().iter().enumerate() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
for used_var in instr.op().uses() {
if !definitions.contains_key(&used_var) {
self.errors.push(VerifierError::UndefinedUse {
block: block_idx,
instr_idx,
var: used_var,
});
}
}
}
}
}
fn check_registered_variables(&mut self) {
let variable_count = self.ssa.variable_count();
let max_block_var = self
.ssa
.blocks()
.iter()
.flat_map(|b| {
let phi_ids = b.phi_nodes().iter().map(|p| p.result().index());
let instr_ids = b
.instructions()
.iter()
.filter_map(|i| i.op().dest().map(|d| d.index()));
phi_ids.chain(instr_ids)
})
.max()
.unwrap_or(0);
let max_reg_var = self
.ssa
.variables()
.iter()
.map(|v| v.id().index())
.max()
.unwrap_or(0);
let capacity = max_block_var
.saturating_add(1)
.max(max_reg_var.saturating_add(1))
.max(variable_count)
.max(1);
let mut registered = BitSet::new(capacity);
for v in self.ssa.variables() {
registered.insert(v.id().index());
}
for block in self.ssa.blocks() {
for phi in block.phi_nodes() {
let idx = phi.result().index();
if idx >= capacity || !registered.contains(idx) {
self.errors
.push(VerifierError::UnregisteredVariable { var: phi.result() });
}
}
for instr in block.instructions() {
if let Some(dest) = instr.op().dest() {
let idx = dest.index();
if idx >= capacity || !registered.contains(idx) {
self.errors
.push(VerifierError::UnregisteredVariable { var: dest });
}
}
}
}
let mut block_defined = BitSet::new(capacity);
for block in self.ssa.blocks() {
for phi in block.phi_nodes() {
let idx = phi.result().index();
if idx < capacity {
block_defined.insert(idx);
}
}
for instr in block.instructions() {
if let Some(dest) = instr.op().dest() {
let idx = dest.index();
if idx < capacity {
block_defined.insert(idx);
}
}
}
}
for var in self.ssa.variables() {
if var.version() == 0 && var.def_site().instruction.is_none() {
continue;
}
if !block_defined.contains(var.id().index()) {
self.errors
.push(VerifierError::OrphanVariable { var: var.id() });
}
}
}
fn check_no_placeholders_or_self_refs(&mut self) {
for (block_idx, block) in self.ssa.blocks().iter().enumerate() {
for (phi_idx, phi) in block.phi_nodes().iter().enumerate() {
if phi.result().is_placeholder() {
self.errors.push(VerifierError::PlaceholderVariable {
block: block_idx,
location: format!("phi {phi_idx} result"),
});
}
for operand in phi.operands() {
if operand.value().is_placeholder() {
self.errors.push(VerifierError::PlaceholderVariable {
block: block_idx,
location: format!(
"phi {phi_idx} operand from B{}",
operand.predecessor()
),
});
}
}
}
for (instr_idx, instr) in block.instructions().iter().enumerate() {
let op = instr.op();
if let Some(dest) = op.dest() {
if dest.is_placeholder() {
self.errors.push(VerifierError::PlaceholderVariable {
block: block_idx,
location: format!("instruction {instr_idx} dest"),
});
}
if op.uses().contains(&dest) {
self.errors.push(VerifierError::SelfReferentialInstruction {
block: block_idx,
instr_idx,
var: dest,
});
}
}
for used_var in op.uses() {
if used_var.is_placeholder() {
self.errors.push(VerifierError::PlaceholderVariable {
block: block_idx,
location: format!("instruction {instr_idx} operand"),
});
}
}
}
}
}
fn check_dominance(
&mut self,
cfg: &SsaCfg<'_, T>,
dom_tree: &DominatorTree,
definitions: &HashMap<SsaVarId, (usize, DefSite)>,
) {
let block_count = self.ssa.block_count().max(1);
let mut reachable = BitSet::new(block_count);
let mut worklist = vec![0usize];
while let Some(block_idx) = worklist.pop() {
if block_idx < block_count && reachable.insert(block_idx) {
for &succ in cfg.block_successors(block_idx) {
if succ < block_count {
worklist.push(succ);
}
}
}
}
for (block_idx, block) in self.ssa.blocks().iter().enumerate() {
if !reachable.contains(block_idx) {
continue;
}
for instr in block.instructions() {
for used_var in instr.op().uses() {
if let Some(&(def_block, _)) = definitions.get(&used_var) {
if !reachable.contains(def_block) {
continue;
}
let def_node = NodeId::new(def_block);
let use_node = NodeId::new(block_idx);
if def_node.index() < dom_tree.node_count()
&& use_node.index() < dom_tree.node_count()
&& !dom_tree.dominates(def_node, use_node)
{
self.errors.push(VerifierError::DominanceViolation {
var: used_var,
def_block,
use_block: block_idx,
});
}
}
}
}
for phi in block.phi_nodes() {
for operand in phi.operands() {
let used_var = operand.value();
let pred_block = operand.predecessor();
if let Some(&(def_block, _)) = definitions.get(&used_var) {
if !reachable.contains(def_block) || !reachable.contains(pred_block) {
continue;
}
let def_node = NodeId::new(def_block);
let pred_node = NodeId::new(pred_block);
if def_node.index() < dom_tree.node_count()
&& pred_node.index() < dom_tree.node_count()
&& !dom_tree.dominates(def_node, pred_node)
{
self.errors.push(VerifierError::DominanceViolation {
var: used_var,
def_block,
use_block: pred_block,
});
}
}
}
}
}
}
}