use std::{collections::HashSet, fmt};
use crate::{
analysis::{
CallGraph, ConstantPropagation, ControlFlowGraph, DataFlowSolver, LiveVariables, NodeId,
ReachingDefinitions, SsaConverter, SsaFunction, TypeContext,
},
metadata::{method::Method, typesystem::PointerSize},
test::analysis::expectations::{
CallGraphExpectation, CfgExpectation, DataFlowExpectation, SsaExpectation,
},
CilObject,
};
#[derive(Debug, Clone)]
pub struct VerificationError {
pub component: String,
pub property: String,
pub expected: String,
pub actual: String,
}
impl fmt::Display for VerificationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {}: expected {}, got {}",
self.component, self.property, self.expected, self.actual
)
}
}
impl std::error::Error for VerificationError {}
impl VerificationError {
#[must_use]
pub fn new(
component: impl Into<String>,
property: impl Into<String>,
expected: impl Into<String>,
actual: impl Into<String>,
) -> Self {
Self {
component: component.into(),
property: property.into(),
expected: expected.into(),
actual: actual.into(),
}
}
}
#[must_use]
pub fn verify_cfg(
cfg: &ControlFlowGraph<'_>,
expectation: &CfgExpectation,
) -> Vec<VerificationError> {
let mut errors = Vec::new();
let block_count = cfg.block_count();
if block_count < expectation.min_blocks {
errors.push(VerificationError::new(
"CFG",
"block_count (min)",
format!(">= {}", expectation.min_blocks),
block_count.to_string(),
));
}
if block_count > expectation.max_blocks {
errors.push(VerificationError::new(
"CFG",
"block_count (max)",
format!("<= {}", expectation.max_blocks),
block_count.to_string(),
));
}
let has_loops = cfg.has_loops();
if has_loops != expectation.has_loops {
errors.push(VerificationError::new(
"CFG",
"has_loops",
expectation.has_loops.to_string(),
has_loops.to_string(),
));
}
let exit_count = cfg.exits().len();
if exit_count < expectation.min_exits {
errors.push(VerificationError::new(
"CFG",
"exit_count (min)",
format!(">= {}", expectation.min_exits),
exit_count.to_string(),
));
}
if exit_count > expectation.max_exits {
errors.push(VerificationError::new(
"CFG",
"exit_count (max)",
format!("<= {}", expectation.max_exits),
exit_count.to_string(),
));
}
let entry = cfg.entry();
if cfg.block(entry).is_none() {
errors.push(VerificationError::new(
"CFG",
"entry_block",
"valid",
"missing",
));
}
for exit in cfg.exits() {
if cfg.block(*exit).is_none() {
errors.push(VerificationError::new(
"CFG",
format!("exit_block_{}", exit.index()),
"valid",
"missing",
));
}
}
for node_id in cfg.node_ids() {
if !cfg.dominates(entry, node_id) {
let preds: Vec<_> = cfg.predecessors(node_id).collect();
if !preds.is_empty() {
errors.push(VerificationError::new(
"CFG",
format!("dominator_tree (block {})", node_id.index()),
"entry dominates",
"entry does not dominate",
));
}
}
}
errors
}
#[must_use]
pub fn verify_ssa(
ssa: &SsaFunction,
cfg: &ControlFlowGraph<'_>,
expectation: &SsaExpectation,
) -> Vec<VerificationError> {
let mut errors = Vec::new();
if ssa.num_args() != expectation.num_args {
errors.push(VerificationError::new(
"SSA",
"num_args",
expectation.num_args.to_string(),
ssa.num_args().to_string(),
));
}
let num_locals = ssa.num_locals();
if num_locals < expectation.min_locals {
errors.push(VerificationError::new(
"SSA",
"num_locals (min)",
format!(">= {}", expectation.min_locals),
num_locals.to_string(),
));
}
if num_locals > expectation.max_locals {
errors.push(VerificationError::new(
"SSA",
"num_locals (max)",
format!("<= {}", expectation.max_locals),
num_locals.to_string(),
));
}
let phi_count = ssa.total_phi_count();
if expectation.has_phi_nodes && phi_count == 0 {
errors.push(VerificationError::new(
"SSA",
"has_phi_nodes",
"true (> 0 phis)",
"false (0 phis)",
));
}
if !expectation.has_phi_nodes && phi_count > 0 {
}
if expectation.has_phi_nodes {
if phi_count < expectation.min_phi_count {
errors.push(VerificationError::new(
"SSA",
"phi_count (min)",
format!(">= {}", expectation.min_phi_count),
phi_count.to_string(),
));
}
if phi_count > expectation.max_phi_count {
errors.push(VerificationError::new(
"SSA",
"phi_count (max)",
format!("<= {}", expectation.max_phi_count),
phi_count.to_string(),
));
}
}
if ssa.block_count() != cfg.block_count() {
errors.push(VerificationError::new(
"SSA",
"block_count",
cfg.block_count().to_string(),
ssa.block_count().to_string(),
));
}
let mut seen_ids = HashSet::new();
for var in ssa.variables() {
if !seen_ids.insert(var.id()) {
errors.push(VerificationError::new(
"SSA",
"duplicate_variable_id",
"unique IDs",
format!("duplicate {}", var.id()),
));
break; }
}
for (block_idx, block) in ssa.blocks().iter().enumerate() {
let preds: Vec<_> = cfg.predecessors(NodeId::new(block_idx)).collect();
for (phi_idx, phi) in block.phi_nodes().iter().enumerate() {
for op in phi.operands() {
let pred_node = NodeId::new(op.predecessor());
if !preds.contains(&pred_node) {
errors.push(VerificationError::new(
"SSA",
format!("phi[{}][{}] predecessor", block_idx, phi_idx),
format!("one of {:?}", preds),
format!("block {}", op.predecessor()),
));
}
}
}
}
for var in ssa.variables() {
let def_site = var.def_site();
if !def_site.is_phi() && def_site.block >= ssa.block_count() {
errors.push(VerificationError::new(
"SSA",
format!("variable {} def_site.block", var.id()),
format!("< {}", ssa.block_count()),
def_site.block.to_string(),
));
}
}
errors
}
#[must_use]
pub fn verify_callgraph(
callgraph: &CallGraph,
method: &Method,
expectation: &CallGraphExpectation,
) -> Vec<VerificationError> {
let mut errors = Vec::new();
let token = method.token;
let node = match callgraph.node(token) {
Some(n) => n,
None => {
errors.push(VerificationError::new(
"CallGraph",
"method_node",
"present",
"missing",
));
return errors;
}
};
let call_site_count = node.call_sites.len();
if call_site_count < expectation.min_call_sites {
errors.push(VerificationError::new(
"CallGraph",
"call_site_count (min)",
format!(">= {}", expectation.min_call_sites),
call_site_count.to_string(),
));
}
if call_site_count > expectation.max_call_sites {
errors.push(VerificationError::new(
"CallGraph",
"call_site_count (max)",
format!("<= {}", expectation.max_call_sites),
call_site_count.to_string(),
));
}
let is_leaf = node.is_leaf();
if is_leaf != expectation.is_leaf {
errors.push(VerificationError::new(
"CallGraph",
"is_leaf",
expectation.is_leaf.to_string(),
is_leaf.to_string(),
));
}
let recursive_methods = callgraph.recursive_methods();
let is_recursive = recursive_methods.contains(&token);
if is_recursive != expectation.is_recursive {
errors.push(VerificationError::new(
"CallGraph",
"is_recursive",
expectation.is_recursive.to_string(),
is_recursive.to_string(),
));
}
errors
}
#[must_use]
pub fn verify_dataflow(
ssa: &SsaFunction,
cfg: &ControlFlowGraph<'_>,
expectation: &DataFlowExpectation,
) -> Vec<VerificationError> {
let mut errors = Vec::new();
let mut sccp = ConstantPropagation::new(PointerSize::Bit64);
let sccp_result = sccp.analyze(ssa, cfg);
let has_constants = sccp_result.constant_count() > 0;
if has_constants != expectation.has_constants {
if expectation.has_constants && !has_constants {
errors.push(VerificationError::new(
"DataFlow",
"has_constants",
"true",
"false",
));
}
}
let total_blocks = cfg.block_count();
let reachable_blocks = sccp_result.executable_block_count();
let all_reachable = reachable_blocks >= total_blocks;
if expectation.all_blocks_reachable && !all_reachable {
errors.push(VerificationError::new(
"DataFlow",
"all_blocks_reachable",
"true",
format!(
"false ({}/{} blocks reachable)",
reachable_blocks, total_blocks
),
));
}
if expectation.has_dead_code && all_reachable {
errors.push(VerificationError::new(
"DataFlow",
"has_dead_code",
"true (unreachable blocks)",
format!("false (all {} blocks reachable)", total_blocks),
));
}
let liveness = LiveVariables::new(ssa);
let liveness_solver = DataFlowSolver::new(liveness);
let _liveness_results = liveness_solver.solve(ssa, cfg);
let reaching = ReachingDefinitions::new(ssa);
let reaching_solver = DataFlowSolver::new(reaching);
let _reaching_results = reaching_solver.solve(ssa, cfg);
errors
}
pub fn build_analysis(
method: &Method,
assembly: Option<&CilObject>,
) -> Result<(ControlFlowGraph<'static>, SsaFunction), String> {
let blocks = method
.blocks
.get()
.ok_or_else(|| "Method has no decoded blocks".to_string())?
.clone();
let cfg = ControlFlowGraph::from_basic_blocks(blocks)
.map_err(|e| format!("Failed to build CFG: {}", e))?;
let num_args = method.signature.param_count as usize;
let num_locals = method.local_vars.count();
let type_context = assembly.map(|asm| TypeContext::new(method, asm));
let ssa = SsaConverter::build(&cfg, num_args, num_locals, type_context.as_ref())
.map_err(|e| format!("Failed to build SSA: {}", e))?;
Ok((cfg, ssa))
}