use crate::eval::{self, Env, EvalError};
use crate::term::{BoolTerm, BvTerm, Sort};
use std::collections::BTreeMap;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum ArrayTerm {
Var { name: String },
Store {
array: Box<ArrayTerm>,
index: Box<ExtBvTerm>,
value: Box<ExtBvTerm>,
},
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum ExtBvTerm {
Core(BvTerm),
Op(Box<ExtOp>),
Select {
array: Box<ArrayTerm>,
index: Box<ExtBvTerm>,
},
PureCall {
name: String,
args: Vec<ExtBvTerm>,
sort: Sort,
},
}
#[derive(Clone, Debug)]
#[allow(missing_docs)]
#[non_exhaustive]
pub enum ExtOp {
Add(ExtBvTerm, ExtBvTerm),
Sub(ExtBvTerm, ExtBvTerm),
Mul(ExtBvTerm, ExtBvTerm),
Udiv(ExtBvTerm, ExtBvTerm),
And(ExtBvTerm, ExtBvTerm),
Or(ExtBvTerm, ExtBvTerm),
Xor(ExtBvTerm, ExtBvTerm),
Shl(ExtBvTerm, ExtBvTerm),
Lshr(ExtBvTerm, ExtBvTerm),
Ashr(ExtBvTerm, ExtBvTerm),
Rotr(ExtBvTerm, ExtBvTerm),
Extract { hi: u32, lo: u32, arg: ExtBvTerm },
Concat(ExtBvTerm, ExtBvTerm),
ZeroExt { by: u32, arg: ExtBvTerm },
SignExt { by: u32, arg: ExtBvTerm },
}
#[derive(Clone, Debug)]
#[allow(missing_docs)]
#[non_exhaustive]
pub enum ExtBoolTerm {
Eq(ExtBvTerm, ExtBvTerm),
Ne(ExtBvTerm, ExtBvTerm),
Ult(ExtBvTerm, ExtBvTerm),
Ule(ExtBvTerm, ExtBvTerm),
Ugt(ExtBvTerm, ExtBvTerm),
Uge(ExtBvTerm, ExtBvTerm),
Slt(ExtBvTerm, ExtBvTerm),
Sle(ExtBvTerm, ExtBvTerm),
Sgt(ExtBvTerm, ExtBvTerm),
Sge(ExtBvTerm, ExtBvTerm),
Not(Box<ExtBoolTerm>),
And(Box<ExtBoolTerm>, Box<ExtBoolTerm>),
Or(Box<ExtBoolTerm>, Box<ExtBoolTerm>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SliverError {
NonConcreteIndex,
BadArraySort,
InconsistentCall {
name: String,
},
}
pub fn lower(assertions: &[ExtBoolTerm]) -> Result<Vec<BoolTerm>, SliverError> {
lower_traced(assertions).map(|l| l.assertions)
}
#[derive(Clone, Debug, Default)]
pub struct Lowered {
pub assertions: Vec<BoolTerm>,
pub reads: Vec<(String, String, u32)>,
pub calls: Vec<(String, String, Vec<BvTerm>, Sort)>,
}
pub fn lower_traced(assertions: &[ExtBoolTerm]) -> Result<Lowered, SliverError> {
let mut low = Lowering::default();
let mut out = Vec::with_capacity(assertions.len());
for a in assertions {
out.push(low.lower_bool(a)?);
}
out.extend(low.array_congruence());
out.extend(low.congruence());
Ok(Lowered {
assertions: out,
reads: low.read_trace,
calls: low.call_trace,
})
}
fn as_const(t: &BvTerm) -> Option<u128> {
match t {
BvTerm::Const { value, .. } => Some(*value),
_ => None,
}
}
struct CallSite {
args: Vec<BvTerm>,
result: BvTerm,
}
struct ArrayRead {
index: BvTerm,
value: BvTerm,
}
#[derive(Default)]
struct Lowering {
next_id: usize,
read_memo: BTreeMap<(String, String), BvTerm>,
read_trace: Vec<(String, String, u32)>,
array_reads: BTreeMap<String, Vec<ArrayRead>>,
call_memo: BTreeMap<(String, String), BvTerm>,
calls_by_name: BTreeMap<String, Vec<CallSite>>,
call_sigs: BTreeMap<String, (Vec<u32>, u32)>,
call_trace: Vec<(String, String, Vec<BvTerm>, Sort)>,
}
impl Lowering {
fn lower_bool(&mut self, t: &ExtBoolTerm) -> Result<BoolTerm, SliverError> {
let cmp = |this: &mut Self, a: &ExtBvTerm, b: &ExtBvTerm| {
Ok((Box::new(this.lower_bv(a)?), Box::new(this.lower_bv(b)?)))
};
Ok(match t {
ExtBoolTerm::Eq(a, b) => {
let (a, b) = cmp(self, a, b)?;
BoolTerm::Eq(a, b)
}
ExtBoolTerm::Ne(a, b) => {
let (a, b) = cmp(self, a, b)?;
BoolTerm::Ne(a, b)
}
ExtBoolTerm::Ult(a, b) => {
let (a, b) = cmp(self, a, b)?;
BoolTerm::Ult(a, b)
}
ExtBoolTerm::Ule(a, b) => {
let (a, b) = cmp(self, a, b)?;
BoolTerm::Ule(a, b)
}
ExtBoolTerm::Ugt(a, b) => {
let (a, b) = cmp(self, a, b)?;
BoolTerm::Ugt(a, b)
}
ExtBoolTerm::Uge(a, b) => {
let (a, b) = cmp(self, a, b)?;
BoolTerm::Uge(a, b)
}
ExtBoolTerm::Slt(a, b) => {
let (a, b) = cmp(self, a, b)?;
BoolTerm::Slt(a, b)
}
ExtBoolTerm::Sle(a, b) => {
let (a, b) = cmp(self, a, b)?;
BoolTerm::Sle(a, b)
}
ExtBoolTerm::Sgt(a, b) => {
let (a, b) = cmp(self, a, b)?;
BoolTerm::Sgt(a, b)
}
ExtBoolTerm::Sge(a, b) => {
let (a, b) = cmp(self, a, b)?;
BoolTerm::Sge(a, b)
}
ExtBoolTerm::Not(x) => BoolTerm::Not(Box::new(self.lower_bool(x)?)),
ExtBoolTerm::And(a, b) => {
BoolTerm::And(Box::new(self.lower_bool(a)?), Box::new(self.lower_bool(b)?))
}
ExtBoolTerm::Or(a, b) => {
BoolTerm::Or(Box::new(self.lower_bool(a)?), Box::new(self.lower_bool(b)?))
}
})
}
fn lower_bv(&mut self, t: &ExtBvTerm) -> Result<BvTerm, SliverError> {
Ok(match t {
ExtBvTerm::Core(bv) => bv.clone(),
ExtBvTerm::Op(op) => self.lower_op(op)?,
ExtBvTerm::Select { array, index } => {
let j = self.lower_index(index)?;
self.resolve_select(array, &j)?
}
ExtBvTerm::PureCall { name, args, sort } => {
let largs = args
.iter()
.map(|a| self.lower_bv(a))
.collect::<Result<Vec<_>, _>>()?;
self.ackermann(name, largs, *sort)?
}
})
}
fn lower_op(&mut self, op: &ExtOp) -> Result<BvTerm, SliverError> {
let bin = |this: &mut Self, a: &ExtBvTerm, b: &ExtBvTerm| {
Ok((Box::new(this.lower_bv(a)?), Box::new(this.lower_bv(b)?)))
};
Ok(match op {
ExtOp::Add(a, b) => {
let (a, b) = bin(self, a, b)?;
BvTerm::Add(a, b)
}
ExtOp::Sub(a, b) => {
let (a, b) = bin(self, a, b)?;
BvTerm::Sub(a, b)
}
ExtOp::Mul(a, b) => {
let (a, b) = bin(self, a, b)?;
BvTerm::Mul(a, b)
}
ExtOp::Udiv(a, b) => {
let (a, b) = bin(self, a, b)?;
BvTerm::Udiv(a, b)
}
ExtOp::And(a, b) => {
let (a, b) = bin(self, a, b)?;
BvTerm::And(a, b)
}
ExtOp::Or(a, b) => {
let (a, b) = bin(self, a, b)?;
BvTerm::Or(a, b)
}
ExtOp::Xor(a, b) => {
let (a, b) = bin(self, a, b)?;
BvTerm::Xor(a, b)
}
ExtOp::Shl(a, b) => {
let (a, b) = bin(self, a, b)?;
BvTerm::Shl(a, b)
}
ExtOp::Lshr(a, b) => {
let (a, b) = bin(self, a, b)?;
BvTerm::Lshr(a, b)
}
ExtOp::Ashr(a, b) => {
let (a, b) = bin(self, a, b)?;
BvTerm::Ashr(a, b)
}
ExtOp::Rotr(a, b) => {
let (a, b) = bin(self, a, b)?;
BvTerm::Rotr(a, b)
}
ExtOp::Extract { hi, lo, arg } => BvTerm::Extract {
hi: *hi,
lo: *lo,
arg: Box::new(self.lower_bv(arg)?),
},
ExtOp::Concat(a, b) => {
let (a, b) = bin(self, a, b)?;
BvTerm::Concat(a, b)
}
ExtOp::ZeroExt { by, arg } => BvTerm::ZeroExt {
by: *by,
arg: Box::new(self.lower_bv(arg)?),
},
ExtOp::SignExt { by, arg } => BvTerm::SignExt {
by: *by,
arg: Box::new(self.lower_bv(arg)?),
},
})
}
fn resolve_select(&mut self, array: &ArrayTerm, j: &BvTerm) -> Result<BvTerm, SliverError> {
match array {
ArrayTerm::Var { name } => Ok(self.read_at(name, j)),
ArrayTerm::Store {
array,
index,
value,
} => {
let i = self.lower_index(index)?;
let v = self.lower_bv(value)?;
if eval::bv_sort(&v)
.map_err(|_| SliverError::BadArraySort)?
.width
!= 8
{
return Err(SliverError::BadArraySort);
}
let rest = self.resolve_select(array, j)?;
Ok(match (as_const(&i), as_const(j)) {
(Some(a), Some(b)) => {
if a == b {
v
} else {
rest
}
}
_ => BvTerm::Ite {
cond: Box::new(BoolTerm::Eq(Box::new(i), Box::new(j.clone()))),
then_: Box::new(v),
else_: Box::new(rest),
},
})
}
}
}
fn read_at(&mut self, array: &str, index: &BvTerm) -> BvTerm {
let key = (array.to_string(), format!("{index:?}"));
if let Some(v) = self.read_memo.get(&key) {
return v.clone();
}
let name = match as_const(index) {
Some(c) => format!("$sel:{array}:{c}"),
None => {
let n = format!("$sel:{array}:#{}", self.next_id);
self.next_id += 1;
n
}
};
let v = BvTerm::Var {
name: name.clone(),
sort: Sort::new(8),
};
self.read_memo.insert(key, v.clone());
if let Some(c) = as_const(index) {
self.read_trace.push((name, array.to_string(), c as u32));
}
self.array_reads
.entry(array.to_string())
.or_default()
.push(ArrayRead {
index: index.clone(),
value: v.clone(),
});
v
}
fn lower_index(&mut self, index: &ExtBvTerm) -> Result<BvTerm, SliverError> {
let lowered = self.lower_bv(index)?;
let w = eval::bv_sort(&lowered)
.map_err(|_| SliverError::BadArraySort)?
.width;
if w != 32 {
return Err(SliverError::BadArraySort);
}
Ok(lowered)
}
fn ackermann(
&mut self,
name: &str,
args: Vec<BvTerm>,
sort: Sort,
) -> Result<BvTerm, SliverError> {
let widths = args
.iter()
.map(|a| eval::bv_sort(a).map(|s| s.width))
.collect::<Result<Vec<_>, _>>()
.map_err(|_| SliverError::InconsistentCall {
name: name.to_string(),
})?;
let sig = (widths, sort.width);
match self.call_sigs.get(name) {
Some(prev) if *prev != sig => {
return Err(SliverError::InconsistentCall {
name: name.to_string(),
});
}
None => {
self.call_sigs.insert(name.to_string(), sig);
}
_ => {}
}
let key = (name.to_string(), format!("{args:?}"));
if let Some(v) = self.call_memo.get(&key) {
return Ok(v.clone());
}
let var_name = format!("$uf:{name}:{}", self.next_id);
self.next_id += 1;
let result = BvTerm::Var {
name: var_name.clone(),
sort,
};
self.call_memo.insert(key, result.clone());
self.calls_by_name
.entry(name.to_string())
.or_default()
.push(CallSite {
args: args.clone(),
result: result.clone(),
});
self.call_trace
.push((var_name, name.to_string(), args, sort));
Ok(result)
}
fn array_congruence(&self) -> Vec<BoolTerm> {
let mut out = Vec::new();
for reads in self.array_reads.values() {
for i in 0..reads.len() {
for j in (i + 1)..reads.len() {
let (a, b) = (&reads[i], &reads[j]);
if let (Some(x), Some(y)) = (as_const(&a.index), as_const(&b.index)) {
debug_assert_ne!(x, y, "identical indices must share a read variable");
continue; }
let ante = BoolTerm::Eq(Box::new(a.index.clone()), Box::new(b.index.clone()));
let concl = BoolTerm::Eq(Box::new(a.value.clone()), Box::new(b.value.clone()));
out.push(BoolTerm::Or(
Box::new(BoolTerm::Not(Box::new(ante))),
Box::new(concl),
));
}
}
}
out
}
fn congruence(&self) -> Vec<BoolTerm> {
let mut out = Vec::new();
for sites in self.calls_by_name.values() {
for i in 0..sites.len() {
for j in (i + 1)..sites.len() {
let (a, b) = (&sites[i], &sites[j]);
if a.args.len() != b.args.len() {
continue; }
let mut ante: Option<BoolTerm> = None;
for (x, y) in a.args.iter().zip(&b.args) {
let eq = BoolTerm::Eq(Box::new(x.clone()), Box::new(y.clone()));
ante = Some(match ante {
None => eq,
Some(p) => BoolTerm::And(Box::new(p), Box::new(eq)),
});
}
let concl =
BoolTerm::Eq(Box::new(a.result.clone()), Box::new(b.result.clone()));
out.push(match ante {
Some(p) => {
BoolTerm::Or(Box::new(BoolTerm::Not(Box::new(p))), Box::new(concl))
}
None => concl,
});
}
}
}
out
}
}
#[derive(Clone, Debug, Default)]
pub struct SliverModel {
pub env: Env,
pub arrays: BTreeMap<String, BTreeMap<u32, u8>>,
pub calls: BTreeMap<(String, Vec<u128>), u128>,
pub base_fill: u8,
}
fn mask(width: u32) -> u128 {
if width >= 128 {
u128::MAX
} else {
(1u128 << width) - 1
}
}
fn concretize_bv(t: &ExtBvTerm, model: &SliverModel) -> Result<BvTerm, EvalError> {
Ok(match t {
ExtBvTerm::Core(bv) => bv.clone(),
ExtBvTerm::Op(op) => concretize_op(op, model)?,
ExtBvTerm::Select { array, index } => {
let idx = eval::eval_bv(&concretize_bv(index, model)?, &model.env)? as u32;
let v = eval_array(array, idx, model)?;
BvTerm::Const {
value: v as u128,
sort: Sort::new(8),
}
}
ExtBvTerm::PureCall { name, args, sort } => {
let argv = args
.iter()
.map(|a| eval::eval_bv(&concretize_bv(a, model)?, &model.env))
.collect::<Result<Vec<_>, _>>()?;
let v = model.calls.get(&(name.clone(), argv)).copied().unwrap_or(0);
BvTerm::Const {
value: v & mask(sort.width),
sort: *sort,
}
}
})
}
fn concretize_op(op: &ExtOp, model: &SliverModel) -> Result<BvTerm, EvalError> {
let bin = |a: &ExtBvTerm, b: &ExtBvTerm| {
Ok::<_, EvalError>((
Box::new(concretize_bv(a, model)?),
Box::new(concretize_bv(b, model)?),
))
};
Ok(match op {
ExtOp::Add(a, b) => {
let (a, b) = bin(a, b)?;
BvTerm::Add(a, b)
}
ExtOp::Sub(a, b) => {
let (a, b) = bin(a, b)?;
BvTerm::Sub(a, b)
}
ExtOp::Mul(a, b) => {
let (a, b) = bin(a, b)?;
BvTerm::Mul(a, b)
}
ExtOp::Udiv(a, b) => {
let (a, b) = bin(a, b)?;
BvTerm::Udiv(a, b)
}
ExtOp::And(a, b) => {
let (a, b) = bin(a, b)?;
BvTerm::And(a, b)
}
ExtOp::Or(a, b) => {
let (a, b) = bin(a, b)?;
BvTerm::Or(a, b)
}
ExtOp::Xor(a, b) => {
let (a, b) = bin(a, b)?;
BvTerm::Xor(a, b)
}
ExtOp::Shl(a, b) => {
let (a, b) = bin(a, b)?;
BvTerm::Shl(a, b)
}
ExtOp::Lshr(a, b) => {
let (a, b) = bin(a, b)?;
BvTerm::Lshr(a, b)
}
ExtOp::Ashr(a, b) => {
let (a, b) = bin(a, b)?;
BvTerm::Ashr(a, b)
}
ExtOp::Rotr(a, b) => {
let (a, b) = bin(a, b)?;
BvTerm::Rotr(a, b)
}
ExtOp::Extract { hi, lo, arg } => BvTerm::Extract {
hi: *hi,
lo: *lo,
arg: Box::new(concretize_bv(arg, model)?),
},
ExtOp::Concat(a, b) => {
let (a, b) = bin(a, b)?;
BvTerm::Concat(a, b)
}
ExtOp::ZeroExt { by, arg } => BvTerm::ZeroExt {
by: *by,
arg: Box::new(concretize_bv(arg, model)?),
},
ExtOp::SignExt { by, arg } => BvTerm::SignExt {
by: *by,
arg: Box::new(concretize_bv(arg, model)?),
},
})
}
fn eval_array(array: &ArrayTerm, idx: u32, model: &SliverModel) -> Result<u8, EvalError> {
match array {
ArrayTerm::Var { name } => Ok(model
.arrays
.get(name)
.and_then(|m| m.get(&idx))
.copied()
.unwrap_or(model.base_fill)),
ArrayTerm::Store {
array,
index,
value,
} => {
let i = eval::eval_bv(&concretize_bv(index, model)?, &model.env)? as u32;
if i == idx {
Ok(eval::eval_bv(&concretize_bv(value, model)?, &model.env)? as u8)
} else {
eval_array(array, idx, model)
}
}
}
}
fn concretize_bool(t: &ExtBoolTerm, model: &SliverModel) -> Result<BoolTerm, EvalError> {
let cmp = |a: &ExtBvTerm, b: &ExtBvTerm| {
Ok::<_, EvalError>((
Box::new(concretize_bv(a, model)?),
Box::new(concretize_bv(b, model)?),
))
};
Ok(match t {
ExtBoolTerm::Eq(a, b) => {
let (a, b) = cmp(a, b)?;
BoolTerm::Eq(a, b)
}
ExtBoolTerm::Ne(a, b) => {
let (a, b) = cmp(a, b)?;
BoolTerm::Ne(a, b)
}
ExtBoolTerm::Ult(a, b) => {
let (a, b) = cmp(a, b)?;
BoolTerm::Ult(a, b)
}
ExtBoolTerm::Ule(a, b) => {
let (a, b) = cmp(a, b)?;
BoolTerm::Ule(a, b)
}
ExtBoolTerm::Ugt(a, b) => {
let (a, b) = cmp(a, b)?;
BoolTerm::Ugt(a, b)
}
ExtBoolTerm::Uge(a, b) => {
let (a, b) = cmp(a, b)?;
BoolTerm::Uge(a, b)
}
ExtBoolTerm::Slt(a, b) => {
let (a, b) = cmp(a, b)?;
BoolTerm::Slt(a, b)
}
ExtBoolTerm::Sle(a, b) => {
let (a, b) = cmp(a, b)?;
BoolTerm::Sle(a, b)
}
ExtBoolTerm::Sgt(a, b) => {
let (a, b) = cmp(a, b)?;
BoolTerm::Sgt(a, b)
}
ExtBoolTerm::Sge(a, b) => {
let (a, b) = cmp(a, b)?;
BoolTerm::Sge(a, b)
}
ExtBoolTerm::Not(x) => BoolTerm::Not(Box::new(concretize_bool(x, model)?)),
ExtBoolTerm::And(a, b) => BoolTerm::And(
Box::new(concretize_bool(a, model)?),
Box::new(concretize_bool(b, model)?),
),
ExtBoolTerm::Or(a, b) => BoolTerm::Or(
Box::new(concretize_bool(a, model)?),
Box::new(concretize_bool(b, model)?),
),
})
}
pub fn eval_ext_bool(t: &ExtBoolTerm, model: &SliverModel) -> Result<bool, EvalError> {
eval::eval_bool(&concretize_bool(t, model)?, &model.env)
}
pub fn eval_const(t: &ExtBvTerm) -> Option<u128> {
fn ground_bv(t: &ExtBvTerm) -> bool {
match t {
ExtBvTerm::Core(bv) => ground_core(bv),
ExtBvTerm::Op(op) => ground_op(op),
ExtBvTerm::Select { array, index } => ground_array(array) && ground_bv(index),
ExtBvTerm::PureCall { .. } => false,
}
}
fn ground_array(a: &ArrayTerm) -> bool {
match a {
ArrayTerm::Var { .. } => true, ArrayTerm::Store {
array,
index,
value,
} => ground_array(array) && ground_bv(index) && ground_bv(value),
}
}
fn ground_core(t: &BvTerm) -> bool {
use crate::term::BvTerm as B;
match t {
B::Const { .. } => true,
B::Var { .. } => false,
B::Add(a, b)
| B::Sub(a, b)
| B::Mul(a, b)
| B::Udiv(a, b)
| B::Urem(a, b)
| B::And(a, b)
| B::Or(a, b)
| B::Xor(a, b)
| B::Shl(a, b)
| B::Lshr(a, b)
| B::Ashr(a, b)
| B::Rotr(a, b)
| B::Concat(a, b) => ground_core(a) && ground_core(b),
B::Extract { arg, .. } | B::ZeroExt { arg, .. } | B::SignExt { arg, .. } => {
ground_core(arg)
}
B::Ite { cond, then_, else_ } => {
ground_bool_core(cond) && ground_core(then_) && ground_core(else_)
}
}
}
fn ground_bool_core(t: &BoolTerm) -> bool {
use crate::term::BoolTerm as T;
match t {
T::Eq(a, b)
| T::Ne(a, b)
| T::Ult(a, b)
| T::Ule(a, b)
| T::Ugt(a, b)
| T::Uge(a, b)
| T::Slt(a, b)
| T::Sle(a, b)
| T::Sgt(a, b)
| T::Sge(a, b) => ground_core(a) && ground_core(b),
T::Not(x) => ground_bool_core(x),
T::And(a, b) | T::Or(a, b) => ground_bool_core(a) && ground_bool_core(b),
}
}
fn ground_op(op: &ExtOp) -> bool {
match op {
ExtOp::Add(a, b)
| ExtOp::Sub(a, b)
| ExtOp::Mul(a, b)
| ExtOp::Udiv(a, b)
| ExtOp::And(a, b)
| ExtOp::Or(a, b)
| ExtOp::Xor(a, b)
| ExtOp::Shl(a, b)
| ExtOp::Lshr(a, b)
| ExtOp::Ashr(a, b)
| ExtOp::Rotr(a, b)
| ExtOp::Concat(a, b) => ground_bv(a) && ground_bv(b),
ExtOp::Extract { arg, .. }
| ExtOp::ZeroExt { by: _, arg }
| ExtOp::SignExt { by: _, arg } => ground_bv(arg),
}
}
if !ground_bv(t) {
return None;
}
let zero = SliverModel::default();
let ones = SliverModel {
base_fill: 0xFF,
..Default::default()
};
let v0 = eval_ext_bv(t, &zero).ok()?;
let v1 = eval_ext_bv(t, &ones).ok()?;
if v0 == v1 { Some(v0) } else { None }
}
pub fn eval_ext_bv(t: &ExtBvTerm, model: &SliverModel) -> Result<u128, EvalError> {
eval::eval_bv(&concretize_bv(t, model)?, &model.env)
}
#[cfg(test)]
mod build {
use super::*;
pub fn c8(v: u128) -> ExtBvTerm {
ExtBvTerm::Core(BvTerm::Const {
value: v,
sort: Sort::new(8),
})
}
pub fn i32c(v: u128) -> ExtBvTerm {
ExtBvTerm::Core(BvTerm::Const {
value: v,
sort: Sort::new(32),
})
}
pub fn v8(name: &str) -> ExtBvTerm {
ExtBvTerm::Core(BvTerm::Var {
name: name.into(),
sort: Sort::new(8),
})
}
pub fn v32(name: &str) -> ExtBvTerm {
ExtBvTerm::Core(BvTerm::Var {
name: name.into(),
sort: Sort::new(32),
})
}
pub fn arr(name: &str) -> ArrayTerm {
ArrayTerm::Var { name: name.into() }
}
pub fn store(a: ArrayTerm, index: ExtBvTerm, value: ExtBvTerm) -> ArrayTerm {
ArrayTerm::Store {
array: Box::new(a),
index: Box::new(index),
value: Box::new(value),
}
}
pub fn select(a: ArrayTerm, index: ExtBvTerm) -> ExtBvTerm {
ExtBvTerm::Select {
array: Box::new(a),
index: Box::new(index),
}
}
pub fn call(name: &str, args: Vec<ExtBvTerm>) -> ExtBvTerm {
ExtBvTerm::PureCall {
name: name.into(),
args,
sort: Sort::new(8),
}
}
pub fn solve(core: &[BoolTerm]) -> crate::solver::CheckResult {
let mut s = crate::solver::Solver::new();
for a in core {
s.assert(a.clone());
}
s.check()
}
}
#[cfg(test)]
mod array {
use super::build::*;
use super::*;
use crate::solver::CheckResult;
#[test]
fn recent_write_at_matching_index_wins() {
let a = store(store(arr("a"), i32c(1), c8(10)), i32c(2), c8(20));
let q = ExtBoolTerm::Eq(select(a, i32c(2)), c8(20));
assert!(matches!(solve(&lower(&[q]).unwrap()), CheckResult::Sat(_)));
}
#[test]
fn overwrite_then_reject_stale_value() {
let a = store(arr("a"), i32c(3), c8(42));
let q = ExtBoolTerm::Ne(select(a, i32c(3)), c8(42));
assert!(matches!(
solve(&lower(&[q]).unwrap()),
CheckResult::Unsat(_)
));
}
#[test]
fn miss_falls_through_to_base_read() {
let a = || store(arr("a"), i32c(1), c8(9));
let must_be_9 = ExtBoolTerm::Eq(select(a(), i32c(2)), c8(9));
assert!(matches!(
solve(&lower(&[must_be_9]).unwrap()),
CheckResult::Sat(_)
));
let cannot_be_9 = ExtBoolTerm::Ne(select(a(), i32c(2)), c8(9));
assert!(matches!(
solve(&lower(&[cannot_be_9]).unwrap()),
CheckResult::Sat(_)
));
}
#[test]
fn same_index_read_is_memoized_to_one_variable() {
let ne = ExtBoolTerm::Ne(select(arr("a"), i32c(5)), select(arr("a"), i32c(5)));
assert!(matches!(
solve(&lower(&[ne]).unwrap()),
CheckResult::Unsat(_)
));
let lowered = lower_traced(&[ExtBoolTerm::Eq(
select(arr("a"), i32c(5)),
select(arr("a"), i32c(5)),
)])
.unwrap();
assert_eq!(lowered.reads.len(), 1, "a[5] must map to one read variable");
}
#[test]
fn eval_const_ground_store_hit() {
let a = store(arr("m"), i32c(4), c8(0x5A));
let t = select(a, i32c(4));
assert_eq!(eval_const(&t), Some(0x5A));
}
#[test]
fn eval_const_base_read_is_not_constant() {
let a = store(arr("m"), i32c(4), c8(1));
let t = select(a, i32c(7)); assert_eq!(eval_const(&t), None);
}
#[test]
fn eval_const_free_var_is_not_constant() {
let t = select(store(arr("m"), v32("i"), c8(1)), i32c(0));
assert_eq!(eval_const(&t), None);
assert_eq!(eval_const(&v32("x")), None);
}
#[test]
fn eval_const_ground_arith() {
let t = ExtBvTerm::Op(Box::new(ExtOp::Add(i32c(40), i32c(2))));
assert_eq!(eval_const(&t), Some(42));
}
#[test]
fn symbolic_index_on_select_lowers() {
let q = ExtBoolTerm::Eq(select(arr("a"), v32("i")), c8(0));
assert!(
lower(&[q]).is_ok(),
"a symbolic select must lower, not error"
);
}
#[test]
fn symbolic_index_on_store_lowers() {
let a = store(arr("a"), v32("i"), c8(1));
let q = ExtBoolTerm::Eq(select(a, i32c(0)), c8(0));
assert!(
lower(&[q]).is_ok(),
"a symbolic store must lower, not error"
);
}
#[test]
fn concrete_indices_still_cost_nothing() {
let a = store(store(arr("a"), i32c(4), c8(1)), i32c(7), c8(2));
let core = lower(&[ExtBoolTerm::Eq(select(a, i32c(4)), c8(1))]).unwrap();
let dump = format!("{core:?}");
assert!(
!dump.contains("Ite"),
"concrete store-chain must settle statically, got: {dump}"
);
assert_eq!(core.len(), 1, "no congruence for distinct constant indices");
}
#[test]
fn symbolic_index_builds_the_read_over_write_mux() {
let a = store(arr("a"), v32("i"), c8(1));
let core = lower(&[ExtBoolTerm::Eq(select(a, v32("j")), c8(0))]).unwrap();
assert!(
format!("{core:?}").contains("Ite"),
"symbolic aliasing must lower to an Ite over Eq(i, j)"
);
}
#[test]
fn bad_index_width_is_rejected() {
let q = ExtBoolTerm::Eq(select(arr("a"), c8(0)), c8(0));
assert_eq!(lower(&[q]).err(), Some(SliverError::BadArraySort));
}
#[test]
fn bad_value_width_is_rejected() {
let a = store(arr("a"), i32c(0), i32c(7));
let q = ExtBoolTerm::Eq(select(a, i32c(0)), c8(7));
assert_eq!(lower(&[q]).err(), Some(SliverError::BadArraySort));
}
#[test]
fn output_is_pure_core_no_sliver_remains() {
let a = store(arr("mem"), i32c(0), c8(1));
let q = ExtBoolTerm::Ult(select(a, i32c(0)), c8(200));
let core = lower(&[q]).unwrap();
assert!(!core.is_empty());
assert!(!matches!(solve(&core), CheckResult::Unknown));
}
}
#[cfg(test)]
mod uf {
use super::build::*;
use super::*;
use crate::solver::CheckResult;
#[test]
fn congruence_forces_equal_results_on_equal_args() {
let q = vec![
ExtBoolTerm::Eq(call("f", vec![v8("x")]), c8(5)),
ExtBoolTerm::Eq(call("f", vec![v8("y")]), c8(7)),
ExtBoolTerm::Eq(v8("x"), v8("y")),
];
assert!(matches!(solve(&lower(&q).unwrap()), CheckResult::Unsat(_)));
}
#[test]
fn distinct_args_leave_results_free() {
let q = vec![
ExtBoolTerm::Eq(call("f", vec![v8("x")]), c8(5)),
ExtBoolTerm::Eq(call("f", vec![v8("y")]), c8(7)),
ExtBoolTerm::Ne(v8("x"), v8("y")),
];
assert!(matches!(solve(&lower(&q).unwrap()), CheckResult::Sat(_)));
}
#[test]
fn identical_call_sites_share_a_variable() {
let q = ExtBoolTerm::Ne(call("f", vec![v8("x")]), call("f", vec![v8("x")]));
let lowered = lower_traced(&[q]).unwrap();
assert_eq!(lowered.calls.len(), 1, "identical sites reuse one variable");
assert_eq!(lowered.assertions.len(), 1, "no congruence clause expected");
assert!(matches!(solve(&lowered.assertions), CheckResult::Unsat(_)));
}
#[test]
fn two_distinct_sites_emit_one_congruence_clause() {
let q = vec![
ExtBoolTerm::Eq(call("f", vec![v8("x")]), c8(1)),
ExtBoolTerm::Eq(call("f", vec![v8("y")]), c8(2)),
];
let lowered = lower_traced(&q).unwrap();
assert_eq!(lowered.assertions.len(), 3);
assert_eq!(lowered.calls.len(), 2);
}
#[test]
fn nested_calls_are_ackermannized() {
let q = vec![
ExtBoolTerm::Eq(call("f", vec![call("f", vec![v8("x")])]), v8("x")),
ExtBoolTerm::Eq(call("f", vec![v8("x")]), v8("x")),
];
assert!(matches!(solve(&lower(&q).unwrap()), CheckResult::Sat(_)));
}
#[test]
fn inconsistent_arity_is_rejected() {
let q = vec![
ExtBoolTerm::Eq(call("f", vec![v8("x")]), c8(1)),
ExtBoolTerm::Eq(call("f", vec![v8("x"), v8("y")]), c8(2)),
];
assert_eq!(
lower(&q).err(),
Some(SliverError::InconsistentCall { name: "f".into() })
);
}
#[test]
fn inconsistent_arg_width_is_rejected() {
let wide = ExtBvTerm::Core(BvTerm::Var {
name: "w".into(),
sort: Sort::new(32),
});
let q = vec![
ExtBoolTerm::Eq(call("f", vec![v8("x")]), c8(1)),
ExtBoolTerm::Ult(call("f", vec![wide]), c8(2)),
];
assert_eq!(
lower(&q).err(),
Some(SliverError::InconsistentCall { name: "f".into() })
);
}
#[test]
fn determinism_is_byte_for_byte() {
let q = vec![
ExtBoolTerm::Eq(call("g", vec![v8("x"), v8("y")]), c8(1)),
ExtBoolTerm::Eq(call("g", vec![v8("y"), v8("x")]), c8(2)),
ExtBoolTerm::Eq(select(store(arr("a"), i32c(4), c8(9)), i32c(4)), c8(9)),
];
let one = format!("{:?}", lower(&q).unwrap());
let two = format!("{:?}", lower(&q).unwrap());
assert_eq!(one, two);
}
}
#[cfg(test)]
mod lowering {
use super::build::*;
use super::*;
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
}
fn uf_result(name: &str, argv: &[u128]) -> u128 {
let mut h = 1469598103934665603u64 ^ name.len() as u64;
for b in name.bytes() {
h = (h ^ b as u64).wrapping_mul(1099511628211);
}
for a in argv {
h = (h ^ *a as u64).wrapping_mul(1099511628211);
}
(h & 0xFF) as u128
}
fn gen_bv(rng: &mut Rng, depth: u32) -> ExtBvTerm {
if depth == 0 || rng.below(2) == 0 {
return match rng.below(4) {
0 => c8(rng.below(256) as u128),
1 => v8(if rng.below(2) == 0 { "x" } else { "y" }),
2 => {
let depth = rng.below(3) as u32;
let a = gen_array(rng, depth);
select(a, i32c(rng.below(4) as u128))
}
_ => {
if rng.below(2) == 0 {
call("f", vec![gen_bv(rng, depth.saturating_sub(1))])
} else {
call(
"g",
vec![
gen_bv(rng, depth.saturating_sub(1)),
gen_bv(rng, depth.saturating_sub(1)),
],
)
}
}
};
}
let a = Box::new(gen_bv(rng, depth - 1));
let b = Box::new(gen_bv(rng, depth - 1));
match rng.below(4) {
0 => ExtBvTerm::Op(Box::new(ExtOp::Add(*a, *b))),
1 => ExtBvTerm::Op(Box::new(ExtOp::Xor(*a, *b))),
2 => ExtBvTerm::Op(Box::new(ExtOp::And(*a, *b))),
_ => ExtBvTerm::Op(Box::new(ExtOp::Sub(*a, *b))),
}
}
fn gen_array(rng: &mut Rng, depth: u32) -> ArrayTerm {
let base = arr(if rng.below(2) == 0 { "a" } else { "b" });
(0..depth).fold(base, |acc, _| {
store(acc, i32c(rng.below(4) as u128), gen_bv(rng, 1))
})
}
fn gen_bool(rng: &mut Rng, depth: u32) -> ExtBoolTerm {
if depth == 0 {
let (a, b) = (gen_bv(rng, 2), gen_bv(rng, 2));
return match rng.below(4) {
0 => ExtBoolTerm::Eq(a, b),
1 => ExtBoolTerm::Ne(a, b),
2 => ExtBoolTerm::Ult(a, b),
_ => ExtBoolTerm::Ule(a, b),
};
}
match rng.below(3) {
0 => ExtBoolTerm::Not(Box::new(gen_bool(rng, depth - 1))),
1 => ExtBoolTerm::And(
Box::new(gen_bool(rng, depth - 1)),
Box::new(gen_bool(rng, depth - 1)),
),
_ => ExtBoolTerm::Or(
Box::new(gen_bool(rng, depth - 1)),
Box::new(gen_bool(rng, depth - 1)),
),
}
}
fn core_env(model: &mut SliverModel, lowered: &Lowered) -> Env {
let mut env = model.env.clone();
for (var, arrname, idx) in &lowered.reads {
let v = model
.arrays
.get(arrname)
.and_then(|m| m.get(idx))
.copied()
.unwrap_or(0);
env.insert(var.clone(), v as u128);
}
for (var, name, args, sort) in &lowered.calls {
let argv: Vec<u128> = args
.iter()
.map(|a| eval::eval_bv(a, &env).unwrap())
.collect();
let result = *model
.calls
.entry((name.clone(), argv))
.or_insert_with_key(|(n, av)| uf_result(n, av) & mask(sort.width));
env.insert(var.clone(), result);
}
env
}
#[test]
fn sliver_lowering_preserves_every_model() {
let mut rng = Rng(0x5117_5EED_0000_0001);
let mut checked = 0usize;
for _ in 0..400 {
let n = 1 + rng.below(3) as usize;
let query: Vec<ExtBoolTerm> = (0..n).map(|_| gen_bool(&mut rng, 2)).collect();
let lowered = match lower_traced(&query) {
Ok(l) => l,
Err(_) => continue, };
let mut model = SliverModel::default();
model.env.insert("x".into(), rng.below(256) as u128);
model.env.insert("y".into(), rng.below(256) as u128);
for name in ["a", "b"] {
let mut cells = BTreeMap::new();
for idx in 0..4u32 {
cells.insert(idx, rng.below(256) as u8);
}
model.arrays.insert(name.into(), cells);
}
let env = core_env(&mut model, &lowered);
let ext_sat = query.iter().all(|a| eval_ext_bool(a, &model).unwrap());
let core_sat = lowered
.assertions
.iter()
.all(|a| eval::eval_bool(a, &env).unwrap());
assert_eq!(
ext_sat, core_sat,
"lowering changed the truth of a model\nquery: {query:?}"
);
checked += 1;
}
assert!(
checked > 300,
"generator produced too few in-sliver queries"
);
}
#[test]
fn evaluator_reads_most_recent_write() {
let mut model = SliverModel::default();
model
.arrays
.insert("a".into(), BTreeMap::from([(7u32, 3u8)]));
let t = select(store(arr("a"), i32c(7), c8(99)), i32c(7));
assert_eq!(eval_ext_bv(&t, &model).unwrap(), 99);
let t2 = select(store(arr("a"), i32c(1), c8(99)), i32c(7));
assert_eq!(eval_ext_bv(&t2, &model).unwrap(), 3);
}
}