use std::{
collections::{HashMap, HashSet},
fmt,
};
use crate::{
analysis::symbolic::ops::SymbolicOp,
ir::{value::ConstValue, variable::SsaVarId},
target::Target,
PointerSize,
};
#[derive(Debug, Clone, PartialEq)]
pub enum SymbolicExpr<T: Target> {
Constant(ConstValue<T>),
Variable(SsaVarId),
NamedVar(String),
Unary {
op: SymbolicOp,
operand: Box<SymbolicExpr<T>>,
},
Binary {
op: SymbolicOp,
left: Box<SymbolicExpr<T>>,
right: Box<SymbolicExpr<T>>,
},
}
const MAX_RECURSIVE_EXPR_DEPTH: usize = 512;
enum CloneTask<'a, T: Target> {
Visit(&'a SymbolicExpr<T>),
BuildUnary(SymbolicOp),
BuildBinary(SymbolicOp),
}
enum EvalTask<'a, T: Target> {
Visit(&'a SymbolicExpr<T>),
EvalUnary(SymbolicOp),
EvalBinary(SymbolicOp),
}
enum SubstituteTask<'a, T: Target> {
Visit(&'a SymbolicExpr<T>),
BuildUnary(SymbolicOp),
BuildBinary(SymbolicOp),
}
impl<T: Target> SymbolicExpr<T> {
fn clone_iterative(&self) -> Self {
let mut tasks = vec![CloneTask::Visit(self)];
let mut values = Vec::new();
while let Some(task) = tasks.pop() {
match task {
CloneTask::Visit(Self::Constant(value)) => {
values.push(Self::Constant(value.clone()));
}
CloneTask::Visit(Self::Variable(var)) => {
values.push(Self::Variable(*var));
}
CloneTask::Visit(Self::NamedVar(name)) => {
values.push(Self::NamedVar(name.clone()));
}
CloneTask::Visit(Self::Unary { op, operand }) => {
tasks.push(CloneTask::BuildUnary(*op));
tasks.push(CloneTask::Visit(operand));
}
CloneTask::Visit(Self::Binary { op, left, right }) => {
tasks.push(CloneTask::BuildBinary(*op));
tasks.push(CloneTask::Visit(right));
tasks.push(CloneTask::Visit(left));
}
CloneTask::BuildUnary(op) => {
let Some(operand) = values.pop() else {
return Self::NamedVar("invalid-symbolic-expression".to_string());
};
values.push(Self::Unary {
op,
operand: Box::new(operand),
});
}
CloneTask::BuildBinary(op) => {
let Some(right) = values.pop() else {
return Self::NamedVar("invalid-symbolic-expression".to_string());
};
let Some(left) = values.pop() else {
return Self::NamedVar("invalid-symbolic-expression".to_string());
};
values.push(Self::Binary {
op,
left: Box::new(left),
right: Box::new(right),
});
}
}
}
values
.pop()
.unwrap_or_else(|| Self::NamedVar("invalid-symbolic-expression".to_string()))
}
fn substitute_var_iterative(&self, var: SsaVarId, replacement: &Self) -> Self {
let mut tasks = vec![SubstituteTask::Visit(self)];
let mut values = Vec::new();
while let Some(task) = tasks.pop() {
match task {
SubstituteTask::Visit(Self::Constant(value)) => {
values.push(Self::Constant(value.clone()));
}
SubstituteTask::Visit(Self::Variable(v)) if *v == var => {
values.push(replacement.clone_iterative());
}
SubstituteTask::Visit(Self::Variable(v)) => {
values.push(Self::Variable(*v));
}
SubstituteTask::Visit(Self::NamedVar(name)) => {
values.push(Self::NamedVar(name.clone()));
}
SubstituteTask::Visit(Self::Unary { op, operand }) => {
tasks.push(SubstituteTask::BuildUnary(*op));
tasks.push(SubstituteTask::Visit(operand));
}
SubstituteTask::Visit(Self::Binary { op, left, right }) => {
tasks.push(SubstituteTask::BuildBinary(*op));
tasks.push(SubstituteTask::Visit(right));
tasks.push(SubstituteTask::Visit(left));
}
SubstituteTask::BuildUnary(op) => {
let Some(operand) = values.pop() else {
return Self::NamedVar("invalid-symbolic-expression".to_string());
};
values.push(Self::Unary {
op,
operand: Box::new(operand),
});
}
SubstituteTask::BuildBinary(op) => {
let Some(right) = values.pop() else {
return Self::NamedVar("invalid-symbolic-expression".to_string());
};
let Some(left) = values.pop() else {
return Self::NamedVar("invalid-symbolic-expression".to_string());
};
values.push(Self::Binary {
op,
left: Box::new(left),
right: Box::new(right),
});
}
}
}
values
.pop()
.unwrap_or_else(|| Self::NamedVar("invalid-symbolic-expression".to_string()))
}
fn substitute_named_iterative(&self, name: &str, replacement: &Self) -> Self {
let mut tasks = vec![SubstituteTask::Visit(self)];
let mut values = Vec::new();
while let Some(task) = tasks.pop() {
match task {
SubstituteTask::Visit(Self::Constant(value)) => {
values.push(Self::Constant(value.clone()));
}
SubstituteTask::Visit(Self::Variable(v)) => {
values.push(Self::Variable(*v));
}
SubstituteTask::Visit(Self::NamedVar(n)) if n == name => {
values.push(replacement.clone_iterative());
}
SubstituteTask::Visit(Self::NamedVar(n)) => {
values.push(Self::NamedVar(n.clone()));
}
SubstituteTask::Visit(Self::Unary { op, operand }) => {
tasks.push(SubstituteTask::BuildUnary(*op));
tasks.push(SubstituteTask::Visit(operand));
}
SubstituteTask::Visit(Self::Binary { op, left, right }) => {
tasks.push(SubstituteTask::BuildBinary(*op));
tasks.push(SubstituteTask::Visit(right));
tasks.push(SubstituteTask::Visit(left));
}
SubstituteTask::BuildUnary(op) => {
let Some(operand) = values.pop() else {
return Self::NamedVar("invalid-symbolic-expression".to_string());
};
values.push(Self::Unary {
op,
operand: Box::new(operand),
});
}
SubstituteTask::BuildBinary(op) => {
let Some(right) = values.pop() else {
return Self::NamedVar("invalid-symbolic-expression".to_string());
};
let Some(left) = values.pop() else {
return Self::NamedVar("invalid-symbolic-expression".to_string());
};
values.push(Self::Binary {
op,
left: Box::new(left),
right: Box::new(right),
});
}
}
}
values
.pop()
.unwrap_or_else(|| Self::NamedVar("invalid-symbolic-expression".to_string()))
}
pub(crate) fn reaches_recursive_depth_limit(&self) -> bool {
self.depth() >= MAX_RECURSIVE_EXPR_DEPTH
}
#[must_use]
pub fn constant(value: ConstValue<T>) -> Self {
Self::Constant(value)
}
#[must_use]
pub fn constant_i64(value: i64) -> Self {
Self::Constant(ConstValue::I64(value))
}
#[must_use]
pub fn constant_i32(value: i32) -> Self {
Self::Constant(ConstValue::I32(value))
}
#[must_use]
pub const fn variable(var: SsaVarId) -> Self {
Self::Variable(var)
}
#[must_use]
pub fn named(name: impl Into<String>) -> Self {
Self::NamedVar(name.into())
}
#[must_use]
pub fn unary(op: SymbolicOp, operand: Self) -> Self {
Self::Unary {
op,
operand: Box::new(operand),
}
}
#[must_use]
pub fn binary(op: SymbolicOp, left: Self, right: Self) -> Self {
Self::Binary {
op,
left: Box::new(left),
right: Box::new(right),
}
}
#[must_use]
pub const fn is_constant(&self) -> bool {
matches!(self, Self::Constant(_))
}
#[must_use]
pub const fn is_variable(&self) -> bool {
matches!(self, Self::Variable(_) | Self::NamedVar(_))
}
#[must_use]
pub const fn as_constant(&self) -> Option<&ConstValue<T>> {
match self {
Self::Constant(v) => Some(v),
_ => None,
}
}
#[must_use]
pub fn as_i64(&self) -> Option<i64> {
match self {
Self::Constant(v) => v.as_i64(),
_ => None,
}
}
#[must_use]
pub const fn as_variable(&self) -> Option<SsaVarId> {
match self {
Self::Variable(v) => Some(*v),
_ => None,
}
}
pub fn for_each_variable<F: FnMut(SsaVarId)>(&self, mut f: F) {
let mut stack = vec![self];
while let Some(expr) = stack.pop() {
match expr {
Self::Constant(_) | Self::NamedVar(_) => {}
Self::Variable(v) => f(*v),
Self::Unary { operand, .. } => stack.push(operand),
Self::Binary { left, right, .. } => {
stack.push(right);
stack.push(left);
}
}
}
}
#[must_use]
pub fn variables(&self) -> HashSet<SsaVarId> {
let mut vars = HashSet::new();
let mut stack = vec![self];
while let Some(expr) = stack.pop() {
match expr {
Self::Constant(_) | Self::NamedVar(_) => {}
Self::Variable(v) => {
vars.insert(*v);
}
Self::Unary { operand, .. } => stack.push(operand),
Self::Binary { left, right, .. } => {
stack.push(right);
stack.push(left);
}
}
}
vars
}
#[must_use]
pub fn named_variables(&self) -> HashSet<String> {
let mut vars = HashSet::new();
let mut stack = vec![self];
while let Some(expr) = stack.pop() {
match expr {
Self::Constant(_) | Self::Variable(_) => {}
Self::NamedVar(name) => {
vars.insert(name.clone());
}
Self::Unary { operand, .. } => stack.push(operand),
Self::Binary { left, right, .. } => {
stack.push(right);
stack.push(left);
}
}
}
vars
}
#[must_use]
pub fn evaluate(
&self,
bindings: &HashMap<SsaVarId, ConstValue<T>>,
ptr_size: PointerSize,
) -> Option<ConstValue<T>> {
let mut tasks = vec![EvalTask::Visit(self)];
let mut values: Vec<Option<ConstValue<T>>> = Vec::new();
while let Some(task) = tasks.pop() {
match task {
EvalTask::Visit(Self::Constant(value)) => values.push(Some(value.clone())),
EvalTask::Visit(Self::Variable(var)) => values.push(bindings.get(var).cloned()),
EvalTask::Visit(Self::NamedVar(_)) => values.push(None),
EvalTask::Visit(Self::Unary { op, operand }) => {
tasks.push(EvalTask::EvalUnary(*op));
tasks.push(EvalTask::Visit(operand));
}
EvalTask::Visit(Self::Binary { op, left, right }) => {
tasks.push(EvalTask::EvalBinary(*op));
tasks.push(EvalTask::Visit(right));
tasks.push(EvalTask::Visit(left));
}
EvalTask::EvalUnary(op) => {
let value = values.pop()??;
values.push(evaluate_unary_typed(op, &value, ptr_size));
}
EvalTask::EvalBinary(op) => {
let right = values.pop()??;
let left = values.pop()??;
values.push(evaluate_binary_typed(op, &left, &right, ptr_size));
}
}
}
values.pop().flatten()
}
#[must_use]
pub fn evaluate_named(
&self,
bindings: &HashMap<&str, ConstValue<T>>,
ptr_size: PointerSize,
) -> Option<ConstValue<T>> {
let mut tasks = vec![EvalTask::Visit(self)];
let mut values: Vec<Option<ConstValue<T>>> = Vec::new();
while let Some(task) = tasks.pop() {
match task {
EvalTask::Visit(Self::Constant(value)) => values.push(Some(value.clone())),
EvalTask::Visit(Self::Variable(_)) => values.push(None),
EvalTask::Visit(Self::NamedVar(name)) => {
values.push(bindings.get(name.as_str()).cloned());
}
EvalTask::Visit(Self::Unary { op, operand }) => {
tasks.push(EvalTask::EvalUnary(*op));
tasks.push(EvalTask::Visit(operand));
}
EvalTask::Visit(Self::Binary { op, left, right }) => {
tasks.push(EvalTask::EvalBinary(*op));
tasks.push(EvalTask::Visit(right));
tasks.push(EvalTask::Visit(left));
}
EvalTask::EvalUnary(op) => {
let value = values.pop()??;
values.push(evaluate_unary_typed(op, &value, ptr_size));
}
EvalTask::EvalBinary(op) => {
let right = values.pop()??;
let left = values.pop()??;
values.push(evaluate_binary_typed(op, &left, &right, ptr_size));
}
}
}
values.pop().flatten()
}
#[must_use]
pub fn substitute(&self, var: SsaVarId, replacement: &Self) -> Self {
self.substitute_var_iterative(var, replacement)
}
#[must_use]
pub fn substitute_named(&self, name: &str, value: i64, ptr_size: PointerSize) -> Self {
self.substitute_named_expr(name, &Self::Constant(ConstValue::I64(value)))
.simplify(ptr_size)
}
#[must_use]
pub fn substitute_named_expr(&self, name: &str, replacement: &Self) -> Self {
self.substitute_named_iterative(name, replacement)
}
#[must_use]
#[allow(clippy::match_same_arms)] pub fn simplify(&self, ptr_size: PointerSize) -> Self {
if self.depth() > MAX_RECURSIVE_EXPR_DEPTH {
return self.clone_iterative();
}
match self {
Self::Constant(_) | Self::Variable(_) | Self::NamedVar(_) => self.clone_iterative(),
Self::Unary { op, operand } => {
let simplified = operand.simplify(ptr_size);
if let Self::Constant(v) = &simplified {
if let Some(result) = evaluate_unary_typed(*op, v, ptr_size) {
return Self::Constant(result);
}
}
if let Self::Unary {
op: inner_op,
operand: inner_operand,
} = &simplified
{
if op == inner_op {
match op {
SymbolicOp::Neg => return inner_operand.clone_iterative(),
SymbolicOp::Not => return inner_operand.clone_iterative(),
_ => {}
}
}
}
Self::Unary {
op: *op,
operand: Box::new(simplified),
}
}
Self::Binary { op, left, right } => {
let left_simp = left.simplify(ptr_size);
let right_simp = right.simplify(ptr_size);
if let (Self::Constant(l), Self::Constant(r)) = (&left_simp, &right_simp) {
if let Some(result) = evaluate_binary_typed(*op, l, r, ptr_size) {
return Self::Constant(result);
}
}
if left_simp == right_simp {
match op {
SymbolicOp::Xor => return Self::Constant(ConstValue::I32(0)),
SymbolicOp::Sub => return Self::Constant(ConstValue::I32(0)),
SymbolicOp::Or => return left_simp,
SymbolicOp::And => return left_simp,
_ => {}
}
}
if *op == SymbolicOp::Xor {
if let Self::Constant(c1) = &right_simp {
if let Self::Binary {
op: SymbolicOp::Xor,
left: inner_left,
right: inner_right,
} = &left_simp
{
if let Self::Constant(c2) = inner_right.as_ref() {
if c1 == c2 {
return (**inner_left).clone();
}
}
if let Self::Constant(c2) = inner_left.as_ref() {
if c1 == c2 {
return (**inner_right).clone();
}
}
}
}
if let Self::Constant(c1) = &left_simp {
if let Self::Binary {
op: SymbolicOp::Xor,
left: inner_left,
right: inner_right,
} = &right_simp
{
if let Self::Constant(c2) = inner_right.as_ref() {
if c1 == c2 {
return (**inner_left).clone();
}
}
if let Self::Constant(c2) = inner_left.as_ref() {
if c1 == c2 {
return (**inner_right).clone();
}
}
}
}
}
if let Self::Constant(r) = &right_simp {
if r.is_zero() {
match op {
SymbolicOp::Add | SymbolicOp::Sub => return left_simp,
SymbolicOp::Mul => return Self::Constant(ConstValue::I32(0)),
SymbolicOp::Xor | SymbolicOp::Or => return left_simp,
SymbolicOp::And => return Self::Constant(ConstValue::I32(0)),
_ => {}
}
} else if r.is_one() {
match op {
SymbolicOp::Mul | SymbolicOp::DivS | SymbolicOp::DivU => {
return left_simp
}
_ => {}
}
} else if r.is_all_ones() {
match op {
SymbolicOp::And => return left_simp,
SymbolicOp::Or => return Self::Constant(r.clone()),
SymbolicOp::Xor => {
return Self::Unary {
op: SymbolicOp::Not,
operand: Box::new(left_simp),
}
}
_ => {}
}
}
}
if let Self::Constant(l) = &left_simp {
if l.is_zero() {
match op {
SymbolicOp::Add => return right_simp,
SymbolicOp::Sub => {
return Self::Unary {
op: SymbolicOp::Neg,
operand: Box::new(right_simp),
}
}
SymbolicOp::Mul => return Self::Constant(ConstValue::I32(0)),
SymbolicOp::Xor | SymbolicOp::Or => return right_simp,
SymbolicOp::And => return Self::Constant(ConstValue::I32(0)),
_ => {}
}
} else if l.is_one() {
if *op == SymbolicOp::Mul {
return right_simp;
}
} else if l.is_all_ones() {
match op {
SymbolicOp::And => return right_simp,
SymbolicOp::Or => return Self::Constant(l.clone()),
SymbolicOp::Xor => {
return Self::Unary {
op: SymbolicOp::Not,
operand: Box::new(right_simp),
}
}
_ => {}
}
}
}
Self::Binary {
op: *op,
left: Box::new(left_simp),
right: Box::new(right_simp),
}
}
}
}
#[must_use]
pub fn depth(&self) -> usize {
let mut max_depth = 0usize;
let mut stack = vec![(self, 0usize)];
while let Some((expr, depth)) = stack.pop() {
max_depth = max_depth.max(depth);
match expr {
Self::Constant(_) | Self::Variable(_) | Self::NamedVar(_) => {}
Self::Unary { operand, .. } => {
stack.push((operand, depth.saturating_add(1)));
}
Self::Binary { left, right, .. } => {
let child_depth = depth.saturating_add(1);
stack.push((right, child_depth));
stack.push((left, child_depth));
}
}
}
max_depth
}
}
impl<T: Target> fmt::Display for SymbolicExpr<T>
where
T::TypeRef: fmt::Display,
T::MethodRef: fmt::Display,
T::FieldRef: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let depth = self.depth();
if depth > MAX_RECURSIVE_EXPR_DEPTH {
return write!(f, "<symbolic-expr depth {depth}>");
}
match self {
Self::Constant(v) => write!(f, "{v}"),
Self::Variable(var) => write!(f, "v{}", var.index()),
Self::NamedVar(name) => write!(f, "{name}"),
Self::Unary { op, operand } => write!(f, "({op}{operand})"),
Self::Binary { op, left, right } => write!(f, "({left} {op} {right})"),
}
}
}
impl<T: Target> From<ConstValue<T>> for SymbolicExpr<T> {
fn from(value: ConstValue<T>) -> Self {
Self::Constant(value)
}
}
impl<T: Target> From<i32> for SymbolicExpr<T> {
fn from(value: i32) -> Self {
Self::Constant(ConstValue::I32(value))
}
}
impl<T: Target> From<i64> for SymbolicExpr<T> {
fn from(value: i64) -> Self {
Self::Constant(ConstValue::I64(value))
}
}
pub fn evaluate_unary_typed<T: Target>(
op: SymbolicOp,
value: &ConstValue<T>,
ptr_size: PointerSize,
) -> Option<ConstValue<T>> {
match op {
SymbolicOp::Neg => value.negate(ptr_size),
SymbolicOp::Not => value.bitwise_not(ptr_size),
_ => None,
}
}
pub fn evaluate_binary_typed<T: Target>(
op: SymbolicOp,
left: &ConstValue<T>,
right: &ConstValue<T>,
ptr_size: PointerSize,
) -> Option<ConstValue<T>> {
match op {
SymbolicOp::Add => left.add(right, ptr_size),
SymbolicOp::Sub => left.sub(right, ptr_size),
SymbolicOp::Mul => left.mul(right, ptr_size),
SymbolicOp::DivS | SymbolicOp::DivU => left.div(right, ptr_size),
SymbolicOp::RemS | SymbolicOp::RemU => left.rem(right, ptr_size),
SymbolicOp::And => left.bitwise_and(right, ptr_size),
SymbolicOp::Or => left.bitwise_or(right, ptr_size),
SymbolicOp::Xor => left.bitwise_xor(right, ptr_size),
SymbolicOp::Shl => left.shl(right, ptr_size),
SymbolicOp::ShrS => left.shr(right, false, ptr_size),
SymbolicOp::ShrU => left.shr(right, true, ptr_size),
SymbolicOp::Eq => left.ceq(right),
SymbolicOp::Ne => left.ceq(right).map(|v| {
if v.is_zero() {
ConstValue::I32(1)
} else {
ConstValue::I32(0)
}
}),
SymbolicOp::LtS => left.clt(right),
SymbolicOp::LtU => left.clt_un(right),
SymbolicOp::GtS => left.cgt(right),
SymbolicOp::GtU => left.cgt_un(right),
SymbolicOp::LeS => {
left.cgt(right).map(|v| {
if v.is_zero() {
ConstValue::I32(1)
} else {
ConstValue::I32(0)
}
})
}
SymbolicOp::LeU => {
left.cgt_un(right).map(|v| {
if v.is_zero() {
ConstValue::I32(1)
} else {
ConstValue::I32(0)
}
})
}
SymbolicOp::GeS => {
left.clt(right).map(|v| {
if v.is_zero() {
ConstValue::I32(1)
} else {
ConstValue::I32(0)
}
})
}
SymbolicOp::GeU => {
left.clt_un(right).map(|v| {
if v.is_zero() {
ConstValue::I32(1)
} else {
ConstValue::I32(0)
}
})
}
SymbolicOp::Neg | SymbolicOp::Not => None,
SymbolicOp::Rol
| SymbolicOp::Ror
| SymbolicOp::Rcl
| SymbolicOp::Rcr
| SymbolicOp::BSwap
| SymbolicOp::BRev
| SymbolicOp::BitScanForward
| SymbolicOp::BitScanReverse
| SymbolicOp::Popcount
| SymbolicOp::Parity => None,
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::{
analysis::symbolic::{expr::SymbolicExpr, ops::SymbolicOp},
ir::{value::ConstValue, variable::SsaVarId},
testing::MockTarget,
PointerSize,
};
fn deep_named_add_chain(depth: usize) -> SymbolicExpr<MockTarget> {
let mut expr = SymbolicExpr::named("state");
for _ in 0..depth {
expr = SymbolicExpr::binary(SymbolicOp::Add, expr, SymbolicExpr::constant_i32(0));
}
expr
}
#[test]
fn deep_expression_queries_do_not_recurse_on_call_stack() {
let expr = deep_named_add_chain(1_024);
assert_eq!(expr.depth(), 1_024);
assert!(expr.named_variables().contains("state"));
assert!(expr.variables().is_empty());
assert_eq!(format!("{expr}"), "<symbolic-expr depth 1024>");
}
#[test]
fn deep_expression_substitution_and_evaluation_are_iterative() {
let expr = deep_named_add_chain(1_024);
let substituted = expr.substitute_named_expr("state", &SymbolicExpr::constant_i32(7));
assert_eq!(
substituted.evaluate(
&HashMap::<SsaVarId, ConstValue<MockTarget>>::new(),
PointerSize::Bit64
),
Some(ConstValue::I32(7))
);
}
#[test]
fn deep_expression_simplify_returns_without_recursive_descent() {
let expr = deep_named_add_chain(1_024);
let simplified = expr.simplify(PointerSize::Bit64);
assert_eq!(simplified.depth(), 1_024);
}
}