use std::collections::HashMap;
use crate::{
analysis::ssa::{
constraints::Constraint,
memory::{MemoryLocation, MemoryState},
symbolic::{SymbolicExpr, SymbolicOp},
CmpKind, ConstValue, SsaFunction, SsaOp, SsaType, SsaVarId,
},
metadata::typesystem::PointerSize,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ControlFlow {
Continue(usize),
Terminal,
Unknown,
}
impl ControlFlow {
#[must_use]
pub fn target(&self) -> Option<usize> {
match self {
Self::Continue(block) => Some(*block),
_ => None,
}
}
#[must_use]
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Terminal)
}
#[must_use]
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
}
#[derive(Debug, Clone, Default)]
pub struct EvaluatorConfig {
pub track_path: bool,
pub track_memory: bool,
pub strict_phi: bool,
}
impl EvaluatorConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn path_aware() -> Self {
Self {
track_path: true,
track_memory: true,
strict_phi: true,
}
}
#[must_use]
pub fn with_memory() -> Self {
Self {
track_path: false,
track_memory: true,
strict_phi: false,
}
}
#[must_use]
pub fn with_path_tracking(mut self) -> Self {
self.track_path = true;
self
}
#[must_use]
pub fn with_memory_tracking(mut self) -> Self {
self.track_memory = true;
self
}
#[must_use]
pub fn with_strict_phi(mut self) -> Self {
self.strict_phi = true;
self
}
}
#[derive(Debug, Clone)]
pub struct ExecutionTrace {
blocks: Vec<usize>,
states: Vec<Option<ConstValue>>,
completed: bool,
limit: usize,
}
impl ExecutionTrace {
#[must_use]
pub fn new(limit: usize) -> Self {
Self {
blocks: Vec::new(),
states: Vec::new(),
completed: false,
limit,
}
}
#[must_use]
pub fn blocks(&self) -> &[usize] {
&self.blocks
}
#[must_use]
pub fn states(&self) -> &[Option<ConstValue>] {
&self.states
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.completed
}
#[must_use]
pub fn len(&self) -> usize {
self.blocks.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
#[must_use]
pub fn last_block(&self) -> Option<usize> {
self.blocks.last().copied()
}
#[must_use]
pub fn hit_limit(&self) -> bool {
self.blocks.len() >= self.limit
}
fn record_block(&mut self, block_idx: usize, state: Option<ConstValue>) {
self.blocks.push(block_idx);
self.states.push(state);
}
fn mark_complete(&mut self) {
self.completed = true;
}
}
#[derive(Debug, Clone)]
pub struct SsaEvaluator<'a> {
ssa: &'a SsaFunction,
values: HashMap<SsaVarId, SymbolicExpr>,
predecessor: Option<usize>,
constraints: HashMap<SsaVarId, Vec<Constraint>>,
config: EvaluatorConfig,
path: Vec<usize>,
memory: MemoryState,
pointer_size: PointerSize,
}
impl<'a> SsaEvaluator<'a> {
#[must_use]
pub fn new(ssa: &'a SsaFunction, ptr_size: PointerSize) -> Self {
Self::with_config(ssa, EvaluatorConfig::default(), ptr_size)
}
#[must_use]
pub fn with_config(
ssa: &'a SsaFunction,
config: EvaluatorConfig,
ptr_size: PointerSize,
) -> Self {
Self {
ssa,
values: HashMap::new(),
predecessor: None,
constraints: HashMap::new(),
config,
path: Vec::new(),
memory: MemoryState::new(),
pointer_size: ptr_size,
}
}
#[must_use]
pub fn path_aware(ssa: &'a SsaFunction, ptr_size: PointerSize) -> Self {
Self::with_config(ssa, EvaluatorConfig::path_aware(), ptr_size)
}
#[must_use]
pub fn with_values(
ssa: &'a SsaFunction,
values: HashMap<SsaVarId, ConstValue>,
ptr_size: PointerSize,
) -> Self {
let exprs = values
.into_iter()
.map(|(k, v)| (k, SymbolicExpr::constant(v)))
.collect();
Self {
ssa,
values: exprs,
predecessor: None,
constraints: HashMap::new(),
config: EvaluatorConfig::default(),
path: Vec::new(),
memory: MemoryState::new(),
pointer_size: ptr_size,
}
}
#[must_use]
pub fn pointer_size(&self) -> PointerSize {
self.pointer_size
}
#[must_use]
pub fn ssa(&self) -> &SsaFunction {
self.ssa
}
#[must_use]
pub fn config(&self) -> &EvaluatorConfig {
&self.config
}
#[must_use]
pub fn path(&self) -> &[usize] {
&self.path
}
pub fn clear_path(&mut self) {
self.path.clear();
}
#[must_use]
pub fn memory_tracking_enabled(&self) -> bool {
self.config.track_memory
}
pub fn set_concrete(&mut self, var: SsaVarId, value: ConstValue) {
self.values.insert(var, SymbolicExpr::constant(value));
}
pub fn set_symbolic(&mut self, var: SsaVarId, name: impl Into<String>) {
self.values.insert(var, SymbolicExpr::named(name));
}
pub fn set_symbolic_expr(&mut self, var: SsaVarId, expr: SymbolicExpr) {
self.values.insert(var, expr);
}
pub fn set_unknown(&mut self, var: SsaVarId) {
self.values.remove(&var);
}
pub fn set(&mut self, var: SsaVarId, value: SymbolicExpr) {
self.values.insert(var, value);
}
#[must_use]
pub fn get(&self, var: SsaVarId) -> Option<&SymbolicExpr> {
self.values.get(&var)
}
#[must_use]
pub fn get_concrete(&self, var: SsaVarId) -> Option<&ConstValue> {
self.values.get(&var).and_then(SymbolicExpr::as_constant)
}
#[must_use]
pub fn get_symbolic(&self, var: SsaVarId) -> Option<&SymbolicExpr> {
self.values.get(&var).filter(|e| !e.is_constant())
}
#[must_use]
pub fn is_concrete(&self, var: SsaVarId) -> bool {
self.values.get(&var).is_some_and(SymbolicExpr::is_constant)
}
#[must_use]
pub fn is_symbolic(&self, var: SsaVarId) -> bool {
self.values.get(&var).is_some_and(|e| !e.is_constant())
}
#[must_use]
pub fn is_unknown(&self, var: SsaVarId) -> bool {
!self.values.contains_key(&var)
}
#[must_use]
pub fn values(&self) -> &HashMap<SsaVarId, SymbolicExpr> {
&self.values
}
#[must_use]
pub fn concrete_values(&self) -> HashMap<SsaVarId, i64> {
self.values
.iter()
.filter_map(|(k, v)| v.as_i64().map(|c| (*k, c)))
.collect()
}
#[must_use]
pub fn const_values(&self) -> HashMap<SsaVarId, ConstValue> {
self.values
.iter()
.filter_map(|(k, v)| v.as_constant().map(|c| (*k, c.clone())))
.collect()
}
pub fn clear(&mut self) {
self.values.clear();
self.predecessor = None;
self.constraints.clear();
}
pub fn add_constraint(&mut self, var: SsaVarId, constraint: Constraint) {
if let Constraint::Equal(ref v) = constraint {
self.values.insert(var, SymbolicExpr::constant(v.clone()));
}
self.constraints.entry(var).or_default().push(constraint);
}
#[must_use]
pub fn constraints(&self, var: SsaVarId) -> &[Constraint] {
self.constraints.get(&var).map_or(&[], |v| v.as_slice())
}
#[must_use]
pub fn has_constraints(&self, var: SsaVarId) -> bool {
self.constraints.get(&var).is_some_and(|v| !v.is_empty())
}
pub fn clear_constraints(&mut self, var: SsaVarId) {
self.constraints.remove(&var);
}
pub fn apply_branch_constraint(&mut self, condition: SsaVarId, took_true_branch: bool) -> bool {
let Some(ssa_var) = self.ssa.variable(condition) else {
return false;
};
let def_site = ssa_var.def_site();
let Some(block) = self.ssa.block(def_site.block) else {
return false;
};
let Some(instr_idx) = def_site.instruction else {
return false;
};
let Some(instr) = block.instruction(instr_idx) else {
return false;
};
self.derive_constraints_from_comparison(instr.op(), took_true_branch)
}
fn derive_constraints_from_comparison(&mut self, op: &SsaOp, took_true_branch: bool) -> bool {
match op {
SsaOp::Ceq { left, right, .. } => {
let left_val = self.get(*left).cloned();
let right_val = self.get(*right).cloned();
if took_true_branch {
match (&left_val, &right_val) {
(Some(l), None) => {
if let Some(v) = l.as_constant() {
self.add_constraint(*right, Constraint::Equal(v.clone()));
true
} else {
false
}
}
(None, Some(r)) => {
if let Some(v) = r.as_constant() {
self.add_constraint(*left, Constraint::Equal(v.clone()));
true
} else {
false
}
}
(Some(l), Some(r)) if l.as_constant() == r.as_constant() => {
true
}
_ => false,
}
} else {
match (&left_val, &right_val) {
(Some(l), None) => {
if let Some(v) = l.as_constant() {
self.add_constraint(*right, Constraint::NotEqual(v.clone()));
true
} else {
false
}
}
(None, Some(r)) => {
if let Some(v) = r.as_constant() {
self.add_constraint(*left, Constraint::NotEqual(v.clone()));
true
} else {
false
}
}
_ => false,
}
}
}
SsaOp::Cgt {
left,
right,
unsigned,
..
} => {
let right_val = self.get(*right).and_then(|e| e.as_constant().cloned());
if took_true_branch {
if let Some(v) = right_val {
if *unsigned {
self.add_constraint(*left, Constraint::GreaterThanUnsigned(v));
} else {
self.add_constraint(*left, Constraint::GreaterThan(v));
}
return true;
}
} else {
if let Some(v) = right_val {
self.add_constraint(*left, Constraint::LessOrEqual(v));
return true;
}
}
false
}
SsaOp::Clt {
left,
right,
unsigned,
..
} => {
let right_val = self.get(*right).and_then(|e| e.as_constant().cloned());
if took_true_branch {
if let Some(v) = right_val {
if *unsigned {
self.add_constraint(*left, Constraint::LessThanUnsigned(v));
} else {
self.add_constraint(*left, Constraint::LessThan(v));
}
return true;
}
} else {
if let Some(v) = right_val {
self.add_constraint(*left, Constraint::GreaterOrEqual(v));
return true;
}
}
false
}
_ => false,
}
}
#[must_use]
pub fn evaluate_condition_with_constraints(&self, condition: SsaVarId) -> Option<bool> {
if let Some(v) = self.get_concrete(condition) {
return Some(!v.is_zero());
}
let ssa_var = self.ssa.variable(condition)?;
let def_site = ssa_var.def_site();
let block = self.ssa.block(def_site.block)?;
let instr_idx = def_site.instruction?;
let instr = block.instruction(instr_idx)?;
self.check_condition_against_constraints(instr.op())
}
fn check_condition_against_constraints(&self, op: &SsaOp) -> Option<bool> {
match op {
SsaOp::Ceq { left, right, .. } => {
let left_constraints = self.constraints(*left);
let right_val = self.get_concrete(*right)?;
for constraint in left_constraints {
match constraint {
Constraint::Equal(v) => {
return Some(v.ceq(right_val).is_some_and(|r| !r.is_zero()));
}
Constraint::NotEqual(v) => {
if v.ceq(right_val).is_some_and(|r| !r.is_zero()) {
return Some(false);
}
}
Constraint::GreaterThan(v) => {
if right_val.cgt(v).is_none_or(|r| r.is_zero()) {
return Some(false);
}
}
Constraint::LessThan(v) => {
if right_val.clt(v).is_none_or(|r| r.is_zero()) {
return Some(false);
}
}
_ => {}
}
}
None
}
SsaOp::Cgt { left, right, .. } => {
let left_constraints = self.constraints(*left);
let right_val = self.get_concrete(*right)?;
for constraint in left_constraints {
match constraint {
Constraint::GreaterThan(v) => {
if v.clt(right_val).is_none_or(|r| r.is_zero()) {
return Some(true);
}
}
Constraint::LessOrEqual(v) => {
if v.cgt(right_val).is_none_or(|r| r.is_zero()) {
return Some(false);
}
}
Constraint::LessThan(v) => {
if v.cgt(right_val).is_none_or(|r| r.is_zero()) {
return Some(false);
}
}
Constraint::Equal(v) => {
return Some(v.cgt(right_val).is_some_and(|r| !r.is_zero()));
}
_ => {}
}
}
None
}
SsaOp::Clt { left, right, .. } => {
let left_constraints = self.constraints(*left);
let right_val = self.get_concrete(*right)?;
for constraint in left_constraints {
match constraint {
Constraint::LessThan(v) => {
if v.cgt(right_val).is_none_or(|r| r.is_zero()) {
return Some(true);
}
}
Constraint::GreaterOrEqual(v) => {
if v.clt(right_val).is_none_or(|r| r.is_zero()) {
return Some(false);
}
}
Constraint::GreaterThan(v) => {
if v.clt(right_val).is_none_or(|r| r.is_zero()) {
return Some(false);
}
}
Constraint::Equal(v) => {
return Some(v.clt(right_val).is_some_and(|r| !r.is_zero()));
}
_ => {}
}
}
None
}
_ => None,
}
}
pub fn set_predecessor(&mut self, pred: Option<usize>) {
self.predecessor = pred;
}
#[must_use]
pub fn predecessor(&self) -> Option<usize> {
self.predecessor
}
pub fn evaluate_phis(&mut self, block_idx: usize) {
let Some(block) = self.ssa.block(block_idx) else {
return;
};
let phi_results: Vec<(SsaVarId, Option<SymbolicExpr>)> = block
.phi_nodes()
.iter()
.map(|phi| {
let result = phi.result();
let value = self.predecessor.and_then(|pred| {
phi.operands()
.iter()
.find(|op| op.predecessor() == pred)
.and_then(|op| self.values.get(&op.value()).cloned())
});
(result, value)
})
.collect();
for (result, value) in phi_results {
if let Some(v) = value {
self.values.insert(result, v);
} else {
self.values.remove(&result);
}
}
}
pub fn evaluate_block(&mut self, block_idx: usize) {
if self.config.track_path {
self.path.push(block_idx);
}
self.evaluate_phis(block_idx);
let Some(block) = self.ssa.block(block_idx) else {
return;
};
for instr in block.instructions() {
self.evaluate_op(instr.op());
}
}
pub fn evaluate_blocks(&mut self, block_indices: &[usize]) {
for &block_idx in block_indices {
self.evaluate_block(block_idx);
}
}
pub fn evaluate_path(&mut self, path: &[usize]) {
for (i, &block_idx) in path.iter().enumerate() {
if i > 0 {
self.set_predecessor(Some(path[i - 1]));
}
self.evaluate_block(block_idx);
}
}
pub fn evaluate_loop_to_fixpoint(
&mut self,
loop_blocks: &[usize],
max_iterations: usize,
) -> usize {
if loop_blocks.is_empty() {
return 0;
}
for iteration in 0..max_iterations {
let snapshot: HashMap<SsaVarId, SymbolicExpr> = self.values.clone();
for (i, &block_idx) in loop_blocks.iter().enumerate() {
if i > 0 {
self.set_predecessor(Some(loop_blocks[i - 1]));
} else if loop_blocks.len() > 1 {
self.set_predecessor(Some(loop_blocks[loop_blocks.len() - 1]));
}
self.evaluate_block(block_idx);
}
if self.values_match(&snapshot) {
return iteration + 1;
}
}
self.widen_unstable_values(loop_blocks);
max_iterations
}
fn values_match(&self, snapshot: &HashMap<SsaVarId, SymbolicExpr>) -> bool {
if self.values.len() != snapshot.len() {
return false;
}
for (var, value) in &self.values {
match snapshot.get(var) {
Some(old_value) => {
match (value.as_constant(), old_value.as_constant()) {
(Some(a), Some(b)) => {
if a != b {
return false;
}
}
(None, None) => {
if format!("{value}") != format!("{old_value}") {
return false;
}
}
_ => return false, }
}
None => return false,
}
}
true
}
fn widen_unstable_values(&mut self, loop_blocks: &[usize]) {
for &block_idx in loop_blocks {
let Some(block) = self.ssa.block(block_idx) else {
continue;
};
for phi in block.phi_nodes() {
self.values.remove(&phi.result());
}
for instr in block.instructions() {
if let Some(dest) = instr.op().dest() {
if let Some(expr) = self.values.get(&dest) {
if !expr.is_constant() {
self.values.remove(&dest);
}
}
}
}
}
}
pub fn evaluate_loop_iterations(&mut self, loop_blocks: &[usize], iterations: usize) {
for _ in 0..iterations {
for (i, &block_idx) in loop_blocks.iter().enumerate() {
if i > 0 {
self.set_predecessor(Some(loop_blocks[i - 1]));
}
self.evaluate_block(block_idx);
}
}
}
pub fn evaluate_op(&mut self, op: &SsaOp) -> Option<SymbolicExpr> {
match op {
SsaOp::Const { dest, value } => {
let expr = SymbolicExpr::constant(value.clone());
self.values.insert(*dest, expr.clone());
Some(expr)
}
SsaOp::Copy { dest, src } => {
let value = self.values.get(src).cloned();
if let Some(v) = value {
self.values.insert(*dest, v.clone());
Some(v)
} else {
self.values.remove(dest);
None
}
}
SsaOp::Add { dest, left, right } => {
self.eval_binary_op(*dest, *left, *right, SymbolicOp::Add)
}
SsaOp::Sub { dest, left, right } => {
self.eval_binary_op(*dest, *left, *right, SymbolicOp::Sub)
}
SsaOp::Mul { dest, left, right } => {
self.eval_binary_op(*dest, *left, *right, SymbolicOp::Mul)
}
SsaOp::Div {
dest,
left,
right,
unsigned,
} => {
let op = if *unsigned {
SymbolicOp::DivU
} else {
SymbolicOp::DivS
};
self.eval_binary_op(*dest, *left, *right, op)
}
SsaOp::Rem {
dest,
left,
right,
unsigned,
} => {
let op = if *unsigned {
SymbolicOp::RemU
} else {
SymbolicOp::RemS
};
self.eval_binary_op(*dest, *left, *right, op)
}
SsaOp::Xor { dest, left, right } => {
self.eval_binary_op(*dest, *left, *right, SymbolicOp::Xor)
}
SsaOp::And { dest, left, right } => {
self.eval_binary_op(*dest, *left, *right, SymbolicOp::And)
}
SsaOp::Or { dest, left, right } => {
self.eval_binary_op(*dest, *left, *right, SymbolicOp::Or)
}
SsaOp::Shl {
dest,
value,
amount,
} => self.eval_binary_op(*dest, *value, *amount, SymbolicOp::Shl),
SsaOp::Shr {
dest,
value,
amount,
unsigned,
} => {
let op = if *unsigned {
SymbolicOp::ShrU
} else {
SymbolicOp::ShrS
};
self.eval_binary_op(*dest, *value, *amount, op)
}
SsaOp::Neg { dest, operand } => self.eval_unary_op(*dest, *operand, SymbolicOp::Neg),
SsaOp::Not { dest, operand } => self.eval_unary_op(*dest, *operand, SymbolicOp::Not),
SsaOp::Ceq { dest, left, right } => {
self.eval_binary_op(*dest, *left, *right, SymbolicOp::Eq)
}
SsaOp::Cgt {
dest,
left,
right,
unsigned,
} => {
let op = if *unsigned {
SymbolicOp::GtU
} else {
SymbolicOp::GtS
};
self.eval_binary_op(*dest, *left, *right, op)
}
SsaOp::Clt {
dest,
left,
right,
unsigned,
} => {
let op = if *unsigned {
SymbolicOp::LtU
} else {
SymbolicOp::LtS
};
self.eval_binary_op(*dest, *left, *right, op)
}
SsaOp::Conv {
dest,
operand,
target,
unsigned,
..
} => {
let value = self.values.get(operand).cloned();
if let Some(expr) = value {
if let Some(v) = expr.as_i64() {
let converted = self.apply_conversion(v, target, *unsigned);
let result = SymbolicExpr::constant(converted);
self.values.insert(*dest, result.clone());
Some(result)
} else {
self.values.insert(*dest, expr.clone());
Some(expr)
}
} else {
self.values.remove(dest);
None
}
}
SsaOp::Call { dest, .. }
| SsaOp::CallVirt { dest, .. }
| SsaOp::CallIndirect { dest, .. } => {
if let Some(d) = dest {
self.values.remove(d);
}
None
}
SsaOp::LoadStaticField { dest, field } => {
if self.config.track_memory {
let location = MemoryLocation::StaticField(*field);
if let Some(stored_var) = self.memory.load(&location) {
if let Some(expr) = self.values.get(&stored_var).cloned() {
self.values.insert(*dest, expr.clone());
return Some(expr);
}
self.values
.insert(*dest, SymbolicExpr::variable(stored_var));
return Some(SymbolicExpr::variable(stored_var));
}
}
self.values.remove(dest);
None
}
SsaOp::StoreStaticField { value, field } => {
if self.config.track_memory {
let location = MemoryLocation::StaticField(*field);
self.memory.store(location, *value, 0);
}
None
}
SsaOp::LoadField {
dest,
object,
field,
} => {
if self.config.track_memory {
let location = MemoryLocation::InstanceField(*object, *field);
if let Some(stored_var) = self.memory.load(&location) {
if let Some(expr) = self.values.get(&stored_var).cloned() {
self.values.insert(*dest, expr.clone());
return Some(expr);
}
self.values
.insert(*dest, SymbolicExpr::variable(stored_var));
return Some(SymbolicExpr::variable(stored_var));
}
}
self.values.remove(dest);
None
}
SsaOp::StoreField {
object,
field,
value,
} => {
if self.config.track_memory {
let location = MemoryLocation::InstanceField(*object, *field);
self.memory.store(location, *value, 0);
}
None
}
SsaOp::NewObj { dest, .. }
| SsaOp::NewArr { dest, .. }
| SsaOp::LoadElement { dest, .. }
| SsaOp::LoadIndirect { dest, .. }
| SsaOp::Box { dest, .. }
| SsaOp::Unbox { dest, .. }
| SsaOp::UnboxAny { dest, .. }
| SsaOp::CastClass { dest, .. }
| SsaOp::IsInst { dest, .. }
| SsaOp::ArrayLength { dest, .. }
| SsaOp::LoadArgAddr { dest, .. }
| SsaOp::LoadLocalAddr { dest, .. }
| SsaOp::LoadToken { dest, .. }
| SsaOp::SizeOf { dest, .. }
| SsaOp::Ckfinite { dest, .. }
| SsaOp::LocalAlloc { dest, .. }
| SsaOp::LoadFunctionPtr { dest, .. }
| SsaOp::LoadVirtFunctionPtr { dest, .. }
| SsaOp::LoadFieldAddr { dest, .. }
| SsaOp::LoadStaticFieldAddr { dest, .. }
| SsaOp::LoadElementAddr { dest, .. }
| SsaOp::LoadObj { dest, .. } => {
self.values.remove(dest);
None
}
_ => None,
}
}
fn eval_binary_op(
&mut self,
dest: SsaVarId,
left: SsaVarId,
right: SsaVarId,
op: SymbolicOp,
) -> Option<SymbolicExpr> {
let left_expr = self.values.get(&left)?;
let right_expr = self.values.get(&right)?;
let result = SymbolicExpr::binary(op, left_expr.clone(), right_expr.clone())
.simplify(self.pointer_size);
let result = self.mask_symbolic_native(result);
self.values.insert(dest, result.clone());
Some(result)
}
fn eval_unary_op(
&mut self,
dest: SsaVarId,
operand: SsaVarId,
op: SymbolicOp,
) -> Option<SymbolicExpr> {
let operand_expr = self.values.get(&operand)?;
let result = SymbolicExpr::unary(op, operand_expr.clone()).simplify(self.pointer_size);
let result = self.mask_symbolic_native(result);
self.values.insert(dest, result.clone());
Some(result)
}
fn mask_symbolic_native(&self, expr: SymbolicExpr) -> SymbolicExpr {
if let Some(cv) = expr.as_constant() {
match cv {
ConstValue::NativeInt(_) | ConstValue::NativeUInt(_) => {
SymbolicExpr::constant(cv.clone().mask_native(self.pointer_size))
}
_ => expr,
}
} else {
expr
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap
)]
fn apply_conversion(&self, value: i64, target: &SsaType, unsigned: bool) -> ConstValue {
match target {
SsaType::I8 => {
if unsigned {
ConstValue::I8((value as u8) as i8)
} else {
ConstValue::I8(value as i8)
}
}
SsaType::U8 | SsaType::Bool => ConstValue::U8(value as u8),
SsaType::I16 => {
if unsigned {
ConstValue::I16((value as u16) as i16)
} else {
ConstValue::I16(value as i16)
}
}
SsaType::U16 => ConstValue::U16(value as u16),
SsaType::I32 => {
if unsigned {
ConstValue::I32((value as u32) as i32)
} else {
ConstValue::I32(value as i32)
}
}
SsaType::U32 => ConstValue::U32(value as u32),
SsaType::NativeInt => match self.pointer_size {
PointerSize::Bit32 => {
if unsigned {
ConstValue::NativeInt(i64::from((value as u32) as i32))
} else {
ConstValue::NativeInt(i64::from(value as i32))
}
}
PointerSize::Bit64 => ConstValue::NativeInt(value),
},
SsaType::NativeUInt => match self.pointer_size {
PointerSize::Bit32 => ConstValue::NativeUInt(u64::from(value as u32)),
PointerSize::Bit64 => ConstValue::NativeUInt(value as u64),
},
SsaType::U64 => ConstValue::U64(value as u64),
#[allow(clippy::cast_precision_loss)]
SsaType::F32 => {
let float_val = if unsigned {
(value as u64) as f32
} else {
value as f32
};
ConstValue::F32(float_val)
}
#[allow(clippy::cast_precision_loss)]
SsaType::F64 => {
let float_val = if unsigned {
(value as u64) as f64
} else {
value as f64
};
ConstValue::F64(float_val)
}
_ => ConstValue::I64(value),
}
}
pub fn resolve_with_trace(&mut self, var: SsaVarId, max_depth: usize) -> Option<SymbolicExpr> {
if let Some(v) = self.values.get(&var) {
return Some(v.clone());
}
if max_depth == 0 {
return None;
}
let ssa_var = self.ssa.variable(var)?;
let def_site = ssa_var.def_site();
let block = self.ssa.block(def_site.block)?;
let instr_idx = def_site.instruction?;
let instr = block.instruction(instr_idx)?;
let op = instr.op();
for operand in op.uses() {
if !self.values.contains_key(&operand) {
if let Some(resolved) = self.resolve_with_trace(operand, max_depth - 1) {
self.values.insert(operand, resolved);
}
}
}
self.evaluate_op(op)
}
pub fn evaluate_with_trace(&mut self, var: SsaVarId, max_depth: usize) -> Option<i64> {
self.resolve_with_trace(var, max_depth)
.and_then(|e| e.as_i64())
}
#[must_use]
pub fn next_block(&self, block_idx: usize) -> ControlFlow {
let Some(block) = self.ssa.block(block_idx) else {
return ControlFlow::Unknown;
};
let terminator = block
.instructions()
.iter()
.rev()
.find(|instr| instr.op().is_terminator());
let Some(instr) = terminator else {
let next_idx = block_idx + 1;
if next_idx < self.ssa.block_count() {
return ControlFlow::Continue(next_idx);
}
return ControlFlow::Unknown;
};
self.evaluate_control_flow(instr.op())
}
fn evaluate_control_flow(&self, op: &SsaOp) -> ControlFlow {
match op {
SsaOp::Jump { target } | SsaOp::Leave { target } => ControlFlow::Continue(*target),
SsaOp::Branch {
condition,
true_target,
false_target,
} => match self.get_concrete(*condition) {
Some(v) => {
if v.is_zero() {
ControlFlow::Continue(*false_target)
} else {
ControlFlow::Continue(*true_target)
}
}
None => ControlFlow::Unknown,
},
SsaOp::BranchCmp {
left,
right,
cmp,
unsigned,
true_target,
false_target,
} => {
let left_val = self.get_concrete(*left);
let right_val = self.get_concrete(*right);
match (left_val, right_val) {
(Some(l), Some(r)) => {
let result = Self::evaluate_comparison(l, r, *cmp, *unsigned);
if result {
ControlFlow::Continue(*true_target)
} else {
ControlFlow::Continue(*false_target)
}
}
_ => ControlFlow::Unknown,
}
}
SsaOp::Switch {
value,
targets,
default,
} => match self.get_concrete(*value).and_then(ConstValue::as_u64) {
Some(v) => {
#[allow(clippy::cast_possible_truncation)]
let idx = v as usize;
if idx < targets.len() {
ControlFlow::Continue(targets[idx])
} else {
ControlFlow::Continue(*default)
}
}
None => ControlFlow::Unknown,
},
SsaOp::Return { .. }
| SsaOp::Throw { .. }
| SsaOp::Rethrow
| SsaOp::EndFinally
| SsaOp::EndFilter { .. } => ControlFlow::Terminal,
_ => ControlFlow::Unknown,
}
}
fn evaluate_comparison(
left: &ConstValue,
right: &ConstValue,
cmp: CmpKind,
unsigned: bool,
) -> bool {
match cmp {
CmpKind::Eq => left.ceq(right).is_some_and(|v| !v.is_zero()),
CmpKind::Ne => left.ceq(right).is_some_and(|v| v.is_zero()),
CmpKind::Lt => if unsigned {
left.clt_un(right)
} else {
left.clt(right)
}
.is_some_and(|v| !v.is_zero()),
CmpKind::Le => {
if unsigned {
left.cgt_un(right)
} else {
left.cgt(right)
}
.is_some_and(|v| v.is_zero())
}
CmpKind::Gt => if unsigned {
left.cgt_un(right)
} else {
left.cgt(right)
}
.is_some_and(|v| !v.is_zero()),
CmpKind::Ge => {
if unsigned {
left.clt_un(right)
} else {
left.clt(right)
}
.is_some_and(|v| v.is_zero())
}
}
}
pub fn execute(
&mut self,
start_block: usize,
state_var: Option<SsaVarId>,
max_steps: usize,
) -> ExecutionTrace {
let mut trace = ExecutionTrace::new(max_steps);
let mut current_block = start_block;
loop {
if trace.hit_limit() {
break;
}
let state = state_var.and_then(|v| self.get_concrete(v).cloned());
trace.record_block(current_block, state);
if let Some(prev) = trace.blocks().iter().rev().nth(1) {
self.set_predecessor(Some(*prev));
}
self.evaluate_block(current_block);
match self.next_block(current_block) {
ControlFlow::Continue(next) => {
current_block = next;
}
ControlFlow::Terminal => {
trace.mark_complete();
break;
}
ControlFlow::Unknown => {
break;
}
}
}
trace
}
pub fn execute_from_entry(&mut self, max_steps: usize) -> ExecutionTrace {
self.execute(0, None, max_steps)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::ssa::{PhiNode, PhiOperand, SsaBlock, SsaInstruction, VariableOrigin};
use crate::analysis::{SsaFunctionBuilder, SsaType};
#[test]
fn test_const_evaluation() {
let (ssa, v0) = {
let mut v0_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
v0_out = b.const_i32(42);
b.ret();
});
});
(ssa, v0_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
assert_eq!(eval.get_concrete(v0).and_then(ConstValue::as_i32), Some(42));
}
#[test]
fn test_add_evaluation() {
let (ssa, v2) = {
let mut v2_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v0 = b.const_i32(10);
let v1 = b.const_i32(32);
v2_out = b.add(v0, v1);
b.ret();
});
});
(ssa, v2_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
assert_eq!(eval.get_concrete(v2).and_then(ConstValue::as_i32), Some(42));
}
#[test]
fn test_xor_mul_pattern() {
let (ssa, current_state, next_state) = {
let mut current_state_out = SsaVarId::new();
let mut next_state_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let current_state = f.arg(0);
current_state_out = current_state;
f.block(0, |b| {
let mul_const = b.const_i32(785121953);
let xor_const = b.const_i32(-934590555);
let mul_result = b.mul(current_state, mul_const);
next_state_out = b.xor(mul_result, xor_const);
b.ret();
});
});
(ssa, current_state_out, next_state_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.set_concrete(current_state, ConstValue::I32(120931986));
eval.evaluate_block(0);
assert!(eval.get_concrete(next_state).is_some());
}
#[test]
fn test_rem_un_evaluation() {
let (ssa, v2) = {
let mut v2_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v0 = b.const_i32(120931986); let v1 = b.const_i32(13);
v2_out = b.rem_un(v0, v1);
b.ret();
});
});
(ssa, v2_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
assert_eq!(eval.get_concrete(v2).and_then(ConstValue::as_i32), Some(6));
}
#[test]
fn test_wrapping_mul() {
let (ssa, v2) = {
let mut v2_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v0 = b.const_i32(i32::MAX);
let v1 = b.const_i32(2);
v2_out = b.mul(v0, v1);
b.ret();
});
});
(ssa, v2_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
assert_eq!(
eval.get_concrete(v2).and_then(ConstValue::as_i32),
Some(i32::MAX.wrapping_mul(2))
);
}
#[test]
fn test_comparison_ops() {
let (ssa, ceq_result, clt_result, cgt_result) = {
let mut ceq_out = SsaVarId::new();
let mut clt_out = SsaVarId::new();
let mut cgt_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v0 = b.const_i32(5);
let v1 = b.const_i32(10);
ceq_out = b.ceq(v0, v1);
clt_out = b.clt(v0, v1);
cgt_out = b.cgt(v0, v1);
b.ret();
});
});
(ssa, ceq_out, clt_out, cgt_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
assert_eq!(
eval.get_concrete(ceq_result).and_then(ConstValue::as_i32),
Some(0)
); assert_eq!(
eval.get_concrete(clt_result).and_then(ConstValue::as_i32),
Some(1)
); assert_eq!(
eval.get_concrete(cgt_result).and_then(ConstValue::as_i32),
Some(0)
); }
#[test]
fn test_set_value_manual() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| b.ret());
});
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
let v0 = SsaVarId::new();
eval.set_concrete(v0, ConstValue::I32(12345));
assert_eq!(
eval.get_concrete(v0).and_then(ConstValue::as_i32),
Some(12345)
);
assert_eq!(
eval.get_concrete(v0).and_then(ConstValue::as_i32),
Some(12345)
);
}
#[test]
fn test_unknown_operand_returns_unknown() {
let (ssa, v2) = {
let mut v2_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let unknown = f.arg(0);
f.block(0, |b| {
let v1 = b.const_i32(10);
v2_out = b.add(unknown, v1);
b.ret();
});
});
(ssa, v2_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
assert!(eval.is_unknown(v2));
assert_eq!(eval.get_concrete(v2), None);
}
#[test]
fn test_symbolic_evaluation() {
let (ssa, arg0, v2) = {
let mut arg0_out = SsaVarId::new();
let mut v2_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
arg0_out = f.arg(0);
f.block(0, |b| {
let v1 = b.const_i32(10);
v2_out = b.add(arg0_out, v1);
b.ret();
});
});
(ssa, arg0_out, v2_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.set_symbolic(arg0, "arg0");
eval.evaluate_block(0);
assert!(eval.is_symbolic(v2));
let expr = eval.get(v2).unwrap();
assert!(format!("{}", expr).contains("arg0"));
}
#[test]
fn test_xor_rem_pattern() {
let (ssa, state_var, result_var) = {
let mut state_out = SsaVarId::new();
let mut result_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
state_out = f.arg(0);
f.block(0, |b| {
let xor_const = b.const_i32(-557527955);
let modulo = b.const_i32(13);
let xored = b.xor(state_out, xor_const);
result_out = b.rem_un(xored, modulo);
b.ret();
});
});
(ssa, state_out, result_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.set_symbolic(state_var, "state");
eval.evaluate_block(0);
assert!(eval.is_symbolic(result_var));
let mut eval2 = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval2.set_concrete(state_var, ConstValue::I32(-638481665_i32));
eval2.evaluate_block(0);
assert!(eval2.is_concrete(result_var));
assert_eq!(
eval2.get_concrete(result_var).and_then(ConstValue::as_i32),
Some(6)
);
}
#[test]
fn test_mixed_operations() {
let (ssa, arg0, result) = {
let mut arg0_out = SsaVarId::new();
let mut result_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
arg0_out = f.arg(0);
f.block(0, |b| {
let const1 = b.const_i32(785121953);
let const2 = b.const_i32(-934590555);
let mul_result = b.mul(arg0_out, const1);
result_out = b.xor(mul_result, const2);
b.ret();
});
});
(ssa, arg0_out, result_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.set_symbolic(arg0, "state");
eval.evaluate_block(0);
assert!(eval.is_symbolic(result));
let mut eval2 = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval2.set_concrete(arg0, ConstValue::I32(120931986));
eval2.evaluate_block(0);
assert!(eval2.is_concrete(result));
}
#[test]
fn test_with_values_constructor() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| b.ret());
});
let v0 = SsaVarId::new();
let v1 = SsaVarId::new();
let mut initial = HashMap::new();
initial.insert(v0, ConstValue::I64(42));
initial.insert(v1, ConstValue::I64(100));
let eval = SsaEvaluator::with_values(&ssa, initial, PointerSize::Bit64);
assert_eq!(eval.get_concrete(v0).and_then(ConstValue::as_i64), Some(42));
assert_eq!(
eval.get_concrete(v1).and_then(ConstValue::as_i64),
Some(100)
);
}
#[test]
fn test_concrete_values_extraction() {
let (ssa, v0, v1) = {
let mut v0_out = SsaVarId::new();
let mut v1_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let arg0 = f.arg(0);
v0_out = arg0;
f.block(0, |b| {
v1_out = b.const_i32(42);
b.ret();
});
});
(ssa, v0_out, v1_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.set_symbolic(v0, "arg0"); eval.evaluate_block(0);
let concrete = eval.concrete_values();
assert!(!concrete.contains_key(&v0)); assert_eq!(concrete.get(&v1), Some(&42)); }
#[test]
fn test_conversion_truncation() {
let (ssa, v1) = {
let mut v1_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v0 = b.const_i32(256); v1_out = b.conv_un(v0, SsaType::U8);
b.ret();
});
});
(ssa, v1_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
assert_eq!(eval.get_concrete(v1).and_then(ConstValue::as_i32), Some(0));
}
#[test]
fn test_conversion_sign_extension() {
let (ssa, v1) = {
let mut v1_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v0 = b.const_i32(255); v1_out = b.conv(v0, SsaType::I8);
b.ret();
});
});
(ssa, v1_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
assert_eq!(eval.get_concrete(v1).and_then(ConstValue::as_i32), Some(-1));
}
#[test]
fn test_constraint_equal() {
let (ssa, arg0) = {
let mut arg0_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
arg0_out = f.arg(0);
f.block(0, |b| {
b.ret();
});
});
(ssa, arg0_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
assert!(eval.is_unknown(arg0));
eval.add_constraint(arg0, Constraint::Equal(ConstValue::I32(42)));
assert!(eval.is_concrete(arg0));
assert_eq!(
eval.get_concrete(arg0).and_then(ConstValue::as_i32),
Some(42)
);
}
#[test]
fn test_constraint_conflicts() {
let c1 = Constraint::Equal(ConstValue::I32(5));
let c2 = Constraint::Equal(ConstValue::I32(10));
assert!(c1.conflicts_with(&c2, PointerSize::Bit64));
let c3 = Constraint::NotEqual(ConstValue::I32(5));
assert!(c1.conflicts_with(&c3, PointerSize::Bit64));
let c4 = Constraint::GreaterThan(ConstValue::I32(5));
assert!(c1.conflicts_with(&c4, PointerSize::Bit64));
let c5 = Constraint::GreaterThan(ConstValue::I32(4));
assert!(!c1.conflicts_with(&c5, PointerSize::Bit64)); }
#[test]
fn test_evaluate_loop_to_fixpoint() {
let (ssa, v0, v1) = {
let mut v0_out = SsaVarId::new();
let mut v1_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
v0_out = b.const_i32(0);
b.jump(1);
});
f.block(1, |b| {
let one = b.const_i32(1);
v1_out = b.add(v0_out, one);
let cond = b.const_true();
b.branch(cond, 2, 3);
});
f.block(2, |b| b.jump(1));
f.block(3, |b| b.ret());
});
(ssa, v0_out, v1_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
assert_eq!(eval.get_concrete(v0).and_then(ConstValue::as_i32), Some(0));
let iterations = eval.evaluate_loop_to_fixpoint(&[1, 2], 5);
assert!(iterations > 0);
assert!(iterations <= 5);
assert_eq!(eval.get_concrete(v1).and_then(ConstValue::as_i32), Some(1));
}
#[test]
fn test_evaluate_loop_to_fixpoint_empty() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| b.ret());
});
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
let iterations = eval.evaluate_loop_to_fixpoint(&[], 10);
assert_eq!(iterations, 0);
}
#[test]
fn test_phi_simple_path_aware() {
let v0 = SsaVarId::new();
let v1 = SsaVarId::new();
let v2 = SsaVarId::new();
let mut ssa = SsaFunction::new(0, 0);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: v0,
value: ConstValue::I32(10),
}));
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 2 }));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
b1.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: v1,
value: ConstValue::I32(20),
}));
b1.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 2 }));
ssa.add_block(b1);
let mut b2 = SsaBlock::new(2);
let mut phi = PhiNode::new(v2, VariableOrigin::Stack(0));
phi.add_operand(PhiOperand::new(v0, 0));
phi.add_operand(PhiOperand::new(v1, 1));
b2.add_phi(phi);
b2.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v2) }));
ssa.add_block(b2);
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
assert_eq!(eval.get_concrete(v0).and_then(ConstValue::as_i32), Some(10));
eval.set_predecessor(Some(0));
eval.evaluate_block(2);
assert_eq!(
eval.get_concrete(v2).and_then(ConstValue::as_i32),
Some(10),
"v2 should be 10 when coming from B0"
);
let mut eval2 = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval2.evaluate_block(1);
assert_eq!(
eval2.get_concrete(v1).and_then(ConstValue::as_i32),
Some(20)
);
eval2.set_predecessor(Some(1));
eval2.evaluate_block(2);
assert_eq!(
eval2.get_concrete(v2).and_then(ConstValue::as_i32),
Some(20),
"v2 should be 20 when coming from B1"
);
}
#[test]
fn test_phi_swap_semantics() {
let v1 = SsaVarId::new();
let v2 = SsaVarId::new();
let v1_prime = SsaVarId::new();
let v2_prime = SsaVarId::new();
let mut ssa = SsaFunction::new(0, 0);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: v1,
value: ConstValue::I32(10),
}));
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: v2,
value: ConstValue::I32(20),
}));
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
let mut phi1 = PhiNode::new(v1_prime, VariableOrigin::Stack(0));
phi1.add_operand(PhiOperand::new(v2, 0));
let mut phi2 = PhiNode::new(v2_prime, VariableOrigin::Stack(1));
phi2.add_operand(PhiOperand::new(v1, 0));
b1.add_phi(phi1);
b1.add_phi(phi2);
b1.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: None }));
ssa.add_block(b1);
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
assert_eq!(eval.get_concrete(v1).and_then(ConstValue::as_i32), Some(10));
assert_eq!(eval.get_concrete(v2).and_then(ConstValue::as_i32), Some(20));
eval.set_predecessor(Some(0));
eval.evaluate_block(1);
assert_eq!(
eval.get_concrete(v1_prime).and_then(ConstValue::as_i32),
Some(20),
"v1' should be 20 (swapped from v2)"
);
assert_eq!(
eval.get_concrete(v2_prime).and_then(ConstValue::as_i32),
Some(10),
"v2' should be 10 (swapped from v1)"
);
}
#[test]
fn test_phi_triple_rotate() {
let a = SsaVarId::new();
let b = SsaVarId::new();
let c = SsaVarId::new();
let a_prime = SsaVarId::new();
let b_prime = SsaVarId::new();
let c_prime = SsaVarId::new();
let mut ssa = SsaFunction::new(0, 0);
let mut blk0 = SsaBlock::new(0);
blk0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: a,
value: ConstValue::I32(1),
}));
blk0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: b,
value: ConstValue::I32(2),
}));
blk0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: c,
value: ConstValue::I32(3),
}));
blk0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(blk0);
let mut blk1 = SsaBlock::new(1);
let mut phi_a = PhiNode::new(a_prime, VariableOrigin::Stack(0));
phi_a.add_operand(PhiOperand::new(c, 0));
blk1.add_phi(phi_a);
let mut phi_b = PhiNode::new(b_prime, VariableOrigin::Stack(1));
phi_b.add_operand(PhiOperand::new(a, 0));
blk1.add_phi(phi_b);
let mut phi_c = PhiNode::new(c_prime, VariableOrigin::Stack(2));
phi_c.add_operand(PhiOperand::new(b, 0));
blk1.add_phi(phi_c);
blk1.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: None }));
ssa.add_block(blk1);
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
eval.set_predecessor(Some(0));
eval.evaluate_block(1);
assert_eq!(
eval.get_concrete(a_prime).and_then(ConstValue::as_i32),
Some(3),
"a' should be 3 (from c)"
);
assert_eq!(
eval.get_concrete(b_prime).and_then(ConstValue::as_i32),
Some(1),
"b' should be 1 (from a)"
);
assert_eq!(
eval.get_concrete(c_prime).and_then(ConstValue::as_i32),
Some(2),
"c' should be 2 (from b)"
);
}
#[test]
fn test_phi_self_reference_blocked() {
let v1 = SsaVarId::new();
let v2 = SsaVarId::new();
let mut ssa = SsaFunction::new(0, 0);
let mut blk0 = SsaBlock::new(0);
blk0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: v1,
value: ConstValue::I32(10),
}));
blk0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(blk0);
let mut blk1 = SsaBlock::new(1);
let mut phi = PhiNode::new(v2, VariableOrigin::Stack(0));
phi.add_operand(PhiOperand::new(v1, 0));
phi.add_operand(PhiOperand::new(v2, 1));
blk1.add_phi(phi);
blk1.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v2) }));
ssa.add_block(blk1);
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
eval.set_predecessor(Some(0));
eval.evaluate_block(1);
assert_eq!(
eval.get_concrete(v2).and_then(ConstValue::as_i32),
Some(10),
"v2 should be 10 from B0"
);
eval.set_predecessor(Some(1));
eval.evaluate_block(1);
assert_eq!(
eval.get_concrete(v2).and_then(ConstValue::as_i32),
Some(10),
"v2 should still be 10 (self-reference)"
);
}
#[test]
fn test_phi_merge_same_values() {
let v0 = SsaVarId::new();
let v1 = SsaVarId::new();
let v2 = SsaVarId::new();
let mut ssa = SsaFunction::new(0, 0);
let mut blk0 = SsaBlock::new(0);
blk0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: v0,
value: ConstValue::I32(42),
}));
blk0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 2 }));
ssa.add_block(blk0);
let mut blk1 = SsaBlock::new(1);
blk1.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: v1,
value: ConstValue::I32(42),
}));
blk1.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 2 }));
ssa.add_block(blk1);
let mut blk2 = SsaBlock::new(2);
let mut phi = PhiNode::new(v2, VariableOrigin::Stack(0));
phi.add_operand(PhiOperand::new(v0, 0));
phi.add_operand(PhiOperand::new(v1, 1));
blk2.add_phi(phi);
blk2.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v2) }));
ssa.add_block(blk2);
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
eval.evaluate_block(1);
eval.evaluate_block(2);
assert_eq!(
eval.get_concrete(v2),
None,
"phi should NOT merge without predecessor (Phase 1 behavior)"
);
let mut eval2 = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval2.evaluate_block(0);
eval2.set_predecessor(Some(0));
eval2.evaluate_block(2);
assert_eq!(
eval2.get_concrete(v2).and_then(ConstValue::as_i32),
Some(42),
"phi should get 42 from predecessor B0"
);
}
#[test]
fn test_phi_merge_different_values_unknown() {
let v0 = SsaVarId::new();
let v1 = SsaVarId::new();
let v2 = SsaVarId::new();
let mut ssa = SsaFunction::new(0, 0);
let mut blk0 = SsaBlock::new(0);
blk0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: v0,
value: ConstValue::I32(10),
}));
blk0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 2 }));
ssa.add_block(blk0);
let mut blk1 = SsaBlock::new(1);
blk1.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: v1,
value: ConstValue::I32(20),
}));
blk1.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 2 }));
ssa.add_block(blk1);
let mut blk2 = SsaBlock::new(2);
let mut phi = PhiNode::new(v2, VariableOrigin::Stack(0));
phi.add_operand(PhiOperand::new(v0, 0));
phi.add_operand(PhiOperand::new(v1, 1));
blk2.add_phi(phi);
blk2.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v2) }));
ssa.add_block(blk2);
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0);
eval.evaluate_block(1);
eval.evaluate_block(2);
assert!(
eval.is_unknown(v2),
"phi should be unknown when operands differ and no predecessor set"
);
}
#[test]
fn test_next_block_jump() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| b.jump(2));
f.block(1, |b| b.ret());
f.block(2, |b| b.ret());
});
let eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
let result = eval.next_block(0);
assert_eq!(result, ControlFlow::Continue(2));
}
#[test]
fn test_next_block_return() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| b.ret());
});
let eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
let result = eval.next_block(0);
assert_eq!(result, ControlFlow::Terminal);
}
#[test]
fn test_next_block_branch_known_true() {
let (ssa, cond) = {
let mut cond_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
cond_out = b.const_true();
b.branch(cond_out, 1, 2);
});
f.block(1, |b| b.ret());
f.block(2, |b| b.ret());
});
(ssa, cond_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0); let result = eval.next_block(0);
assert_eq!(result, ControlFlow::Continue(1));
assert!(eval.is_concrete(cond));
}
#[test]
fn test_next_block_branch_known_false() {
let (ssa, _cond) = {
let mut cond_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
cond_out = b.const_false();
b.branch(cond_out, 1, 2);
});
f.block(1, |b| b.ret());
f.block(2, |b| b.ret());
});
(ssa, cond_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
eval.evaluate_block(0); let result = eval.next_block(0);
assert_eq!(result, ControlFlow::Continue(2));
}
#[test]
fn test_next_block_branch_unknown() {
let (ssa, cond) = {
let mut cond_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
cond_out = f.arg(0); f.block(0, |b| {
b.branch(cond_out, 1, 2);
});
f.block(1, |b| b.ret());
f.block(2, |b| b.ret());
});
(ssa, cond_out)
};
let eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
assert!(eval.is_unknown(cond));
let result = eval.next_block(0);
assert_eq!(result, ControlFlow::Unknown);
}
#[test]
fn test_execute_simple_path() {
let (ssa, v0, v1) = {
let mut v0_out = SsaVarId::new();
let mut v1_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
v0_out = b.const_i32(10);
b.jump(1);
});
f.block(1, |b| {
let five = b.const_i32(5);
v1_out = b.add(v0_out, five);
b.jump(2);
});
f.block(2, |b| b.ret());
});
(ssa, v0_out, v1_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
let trace = eval.execute(0, None, 100);
assert!(trace.is_complete());
assert_eq!(trace.blocks(), &[0, 1, 2]);
assert_eq!(eval.get_concrete(v0).and_then(ConstValue::as_i32), Some(10));
assert_eq!(eval.get_concrete(v1).and_then(ConstValue::as_i32), Some(15));
}
#[test]
fn test_execute_with_branch() {
let (ssa, state, cmp_result) = {
let mut state_out = SsaVarId::new();
let mut cmp_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
state_out = b.const_i32(5);
b.jump(1);
});
f.block(1, |b| {
let five = b.const_i32(5);
cmp_out = b.ceq(state_out, five);
b.branch(cmp_out, 2, 3);
});
f.block(2, |b| b.ret());
f.block(3, |b| b.ret());
});
(ssa, state_out, cmp_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
let trace = eval.execute(0, Some(state), 100);
assert!(trace.is_complete());
assert_eq!(trace.blocks(), &[0, 1, 2]);
assert_eq!(
eval.get_concrete(cmp_result).and_then(ConstValue::as_i32),
Some(1)
); }
#[test]
fn test_execute_max_steps() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| b.jump(0)); });
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
let trace = eval.execute(0, None, 5);
assert!(!trace.is_complete()); assert!(trace.hit_limit()); assert_eq!(trace.len(), 5); }
#[test]
fn test_execute_state_tracking() {
let (ssa, state) = {
let mut state_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
state_out = b.const_i32(10);
b.jump(1);
});
f.block(1, |b| {
b.jump(2);
});
f.block(2, |b| b.ret());
});
(ssa, state_out)
};
let mut eval = SsaEvaluator::new(&ssa, PointerSize::Bit64);
let trace = eval.execute(0, Some(state), 100);
assert!(trace.is_complete());
assert_eq!(trace.blocks(), &[0, 1, 2]);
assert_eq!(trace.states()[0], None); assert_eq!(
trace.states()[1].as_ref().and_then(ConstValue::as_i32),
Some(10)
); assert_eq!(
trace.states()[2].as_ref().and_then(ConstValue::as_i32),
Some(10)
); }
#[test]
fn test_control_flow_result_helpers() {
let cont = ControlFlow::Continue(5);
assert_eq!(cont.target(), Some(5));
assert!(!cont.is_terminal());
assert!(!cont.is_unknown());
let term = ControlFlow::Terminal;
assert_eq!(term.target(), None);
assert!(term.is_terminal());
assert!(!term.is_unknown());
let unknown = ControlFlow::Unknown;
assert_eq!(unknown.target(), None);
assert!(!unknown.is_terminal());
assert!(unknown.is_unknown());
}
#[test]
fn test_execution_trace_helpers() {
let mut trace = ExecutionTrace::new(100);
assert!(trace.is_empty());
assert!(!trace.is_complete());
assert!(!trace.hit_limit());
assert_eq!(trace.last_block(), None);
trace.record_block(0, Some(ConstValue::I32(10)));
trace.record_block(1, Some(ConstValue::I32(20)));
trace.record_block(2, None);
assert_eq!(trace.len(), 3);
assert!(!trace.is_empty());
assert_eq!(trace.blocks(), &[0, 1, 2]);
assert_eq!(
trace.states(),
&[Some(ConstValue::I32(10)), Some(ConstValue::I32(20)), None]
);
assert_eq!(trace.last_block(), Some(2));
trace.mark_complete();
assert!(trace.is_complete());
}
}