use crate::eval::bv_sort;
use crate::lowering;
use crate::{BoolTerm, BvTerm, CheckResult, Solver, Sort};
use std::collections::{HashMap, HashSet};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SmtError {
Parse(String),
Unsupported(String),
Solver(String),
}
impl std::fmt::Display for SmtError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SmtError::Parse(m) => write!(f, "parse error: {m}"),
SmtError::Unsupported(m) => write!(f, "unsupported: {m}"),
SmtError::Solver(m) => write!(f, "solver error: {m}"),
}
}
}
impl std::error::Error for SmtError {}
#[derive(Clone, Debug)]
pub struct Outcome {
pub result: Option<CheckResult>,
pub declared: Vec<(String, u32)>,
}
enum Sexp {
Atom(String),
List(Vec<Sexp>),
}
fn as_atom(s: &Sexp) -> Option<&str> {
match s {
Sexp::Atom(a) => Some(a.as_str()),
Sexp::List(_) => None,
}
}
fn tokenize(src: &str) -> Vec<String> {
let mut toks = Vec::new();
let mut chars = src.chars().peekable();
while let Some(&c) = chars.peek() {
match c {
c if c.is_whitespace() => {
chars.next();
}
';' => {
for c in chars.by_ref() {
if c == '\n' {
break;
}
}
}
'(' | ')' => {
chars.next();
toks.push(c.to_string());
}
'|' => {
chars.next();
let mut s = String::from("|");
for c in chars.by_ref() {
s.push(c);
if c == '|' {
break;
}
}
toks.push(s);
}
_ => {
let mut s = String::new();
while let Some(&c) = chars.peek() {
if c.is_whitespace() || c == '(' || c == ')' || c == ';' {
break;
}
s.push(c);
chars.next();
}
toks.push(s);
}
}
}
toks
}
fn parse_sexps(toks: &[String]) -> Result<Vec<Sexp>, SmtError> {
let mut pos = 0;
let mut out = Vec::new();
while pos < toks.len() {
let (s, next) = parse_one(toks, pos)?;
out.push(s);
pos = next;
}
Ok(out)
}
fn parse_one(toks: &[String], pos: usize) -> Result<(Sexp, usize), SmtError> {
let tok = toks
.get(pos)
.ok_or_else(|| SmtError::Parse("unexpected end of input".into()))?;
match tok.as_str() {
"(" => {
let mut items = Vec::new();
let mut p = pos + 1;
loop {
match toks.get(p) {
None => return Err(SmtError::Parse("unclosed '('".into())),
Some(t) if t == ")" => return Ok((Sexp::List(items), p + 1)),
Some(_) => {
let (s, next) = parse_one(toks, p)?;
items.push(s);
p = next;
}
}
}
}
")" => Err(SmtError::Parse("unexpected ')'".into())),
_ => Ok((Sexp::Atom(tok.clone()), pos + 1)),
}
}
struct Ctx {
decls: HashMap<String, u32>,
bool_decls: HashSet<String>,
}
impl Ctx {
fn width_of(t: &BvTerm) -> Result<u32, SmtError> {
bv_sort(t)
.map(|s| s.width)
.map_err(|e| SmtError::Solver(format!("ill-sorted subterm: {e:?}")))
}
fn is_bool_sexp(&self, s: &Sexp) -> bool {
match s {
Sexp::Atom(a) => a == "true" || a == "false" || self.bool_decls.contains(a.as_str()),
Sexp::List(items) => matches!(
items.first().and_then(as_atom),
Some(
"and"
| "or"
| "not"
| "=>"
| "="
| "distinct"
| "bvult"
| "bvule"
| "bvugt"
| "bvuge"
| "bvslt"
| "bvsle"
| "bvsgt"
| "bvsge"
)
),
}
}
fn parse_bv(&self, s: &Sexp) -> Result<BvTerm, SmtError> {
match s {
Sexp::Atom(a) => self.parse_bv_atom(a),
Sexp::List(items) => {
let head = items
.first()
.ok_or_else(|| SmtError::Parse("empty term list".into()))?;
match head {
Sexp::Atom(a) if a == "_" => parse_indexed_const(items),
Sexp::List(_) => self.parse_indexed_app(items),
Sexp::Atom(op) => self.parse_bv_op(op, &items[1..]),
}
}
}
}
fn parse_bv_atom(&self, a: &str) -> Result<BvTerm, SmtError> {
if let Some(hex) = a.strip_prefix("#x") {
if hex.is_empty() || !hex.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(SmtError::Parse(format!("bad hex literal '{a}'")));
}
let width = (hex.len() as u32) * 4;
let value = u128::from_str_radix(hex, 16)
.map_err(|_| SmtError::Parse(format!("hex literal too wide: '{a}'")))?;
return Ok(const_bv(value, width));
}
if let Some(bin) = a.strip_prefix("#b") {
if bin.is_empty() || !bin.chars().all(|c| c == '0' || c == '1') {
return Err(SmtError::Parse(format!("bad binary literal '{a}'")));
}
let width = bin.len() as u32;
let value = u128::from_str_radix(bin, 2)
.map_err(|_| SmtError::Parse(format!("binary literal too wide: '{a}'")))?;
return Ok(const_bv(value, width));
}
match self.decls.get(a) {
Some(&width) => Ok(BvTerm::Var {
name: a.to_string(),
sort: Sort::new(width),
}),
None => Err(SmtError::Parse(format!("unknown symbol '{a}'"))),
}
}
fn parse_bv_op(&self, op: &str, args: &[Sexp]) -> Result<BvTerm, SmtError> {
match op {
"bvadd" => self.fold_bv(op, args, |a, b| BvTerm::Add(Box::new(a), Box::new(b))),
"bvsub" => self.fold_bv(op, args, |a, b| BvTerm::Sub(Box::new(a), Box::new(b))),
"bvmul" => self.fold_bv(op, args, |a, b| BvTerm::Mul(Box::new(a), Box::new(b))),
"bvand" => self.fold_bv(op, args, |a, b| BvTerm::And(Box::new(a), Box::new(b))),
"bvor" => self.fold_bv(op, args, |a, b| BvTerm::Or(Box::new(a), Box::new(b))),
"bvxor" => self.fold_bv(op, args, |a, b| BvTerm::Xor(Box::new(a), Box::new(b))),
"concat" => self.fold_bv(op, args, |a, b| BvTerm::Concat(Box::new(a), Box::new(b))),
"bvudiv" => {
let (a, b) = self.two_bv(op, args)?;
Ok(BvTerm::Udiv(Box::new(a), Box::new(b)))
}
"bvshl" => {
let (a, b) = self.two_bv(op, args)?;
Ok(BvTerm::Shl(Box::new(a), Box::new(b)))
}
"bvlshr" => {
let (a, b) = self.two_bv(op, args)?;
Ok(BvTerm::Lshr(Box::new(a), Box::new(b)))
}
"bvashr" => {
let (a, b) = self.two_bv(op, args)?;
Ok(BvTerm::Ashr(Box::new(a), Box::new(b)))
}
"bvnot" => {
let x = self.one_bv(op, args)?;
let w = Self::width_of(&x)?;
Ok(lowering::bvnot(x, w))
}
"bvneg" => {
let x = self.one_bv(op, args)?;
let w = Self::width_of(&x)?;
Ok(lowering::bvneg(x, w))
}
"bvurem" => {
let (a, b) = self.two_bv(op, args)?;
let w = Self::width_of(&a)?;
Ok(lowering::bvurem(a, b, w))
}
"bvsdiv" => {
let (a, b) = self.two_bv(op, args)?;
let w = Self::width_of(&a)?;
Ok(lowering::bvsdiv(a, b, w))
}
"bvsrem" => {
let (a, b) = self.two_bv(op, args)?;
let w = Self::width_of(&a)?;
Ok(lowering::bvsrem(a, b, w))
}
"ite" => {
if args.len() != 3 {
return Err(SmtError::Parse(format!(
"ite takes 3 args, got {}",
args.len()
)));
}
let cond = self.parse_bool(&args[0])?;
let then_ = self.parse_bv(&args[1])?;
let else_ = self.parse_bv(&args[2])?;
Ok(BvTerm::Ite {
cond: Box::new(cond),
then_: Box::new(then_),
else_: Box::new(else_),
})
}
other => Err(SmtError::Unsupported(format!(
"bitvector operator '{other}'"
))),
}
}
fn parse_indexed_app(&self, items: &[Sexp]) -> Result<BvTerm, SmtError> {
let Sexp::List(op) = &items[0] else {
return Err(SmtError::Parse("expected indexed operator".into()));
};
if op.first().and_then(as_atom) != Some("_") {
return Err(SmtError::Unsupported("non-'_' indexed operator".into()));
}
let name = op
.get(1)
.and_then(as_atom)
.ok_or_else(|| SmtError::Parse("missing indexed operator name".into()))?;
let idx = |n: usize| -> Result<u32, SmtError> {
op.get(n)
.and_then(as_atom)
.and_then(|s| s.parse::<u32>().ok())
.ok_or_else(|| SmtError::Parse(format!("bad index {n} on '{name}'")))
};
let arg = |this: &Self| -> Result<BvTerm, SmtError> {
if items.len() != 2 {
return Err(SmtError::Parse(format!("'{name}' takes 1 argument")));
}
this.parse_bv(&items[1])
};
match name {
"extract" => {
let (hi, lo) = (idx(2)?, idx(3)?);
Ok(BvTerm::Extract {
hi,
lo,
arg: Box::new(arg(self)?),
})
}
"zero_extend" => Ok(BvTerm::ZeroExt {
by: idx(2)?,
arg: Box::new(arg(self)?),
}),
"sign_extend" => Ok(BvTerm::SignExt {
by: idx(2)?,
arg: Box::new(arg(self)?),
}),
"rotate_right" => {
let k = idx(2)?;
let x = arg(self)?;
let w = Self::width_of(&x)?;
Ok(BvTerm::Rotr(Box::new(x), Box::new(const_bv(k as u128, w))))
}
"rotate_left" => {
let k = idx(2)?;
let x = arg(self)?;
let w = Self::width_of(&x)?;
Ok(lowering::bvrotl(x, const_bv(k as u128, w), w))
}
other => Err(SmtError::Unsupported(format!("indexed operator '{other}'"))),
}
}
fn parse_bool(&self, s: &Sexp) -> Result<BoolTerm, SmtError> {
match s {
Sexp::Atom(a) => match a.as_str() {
"true" => Ok(BoolTerm::Eq(
Box::new(const_bv(0, 1)),
Box::new(const_bv(0, 1)),
)),
"false" => Ok(BoolTerm::Eq(
Box::new(const_bv(0, 1)),
Box::new(const_bv(1, 1)),
)),
name if self.bool_decls.contains(name) => Ok(BoolTerm::Eq(
Box::new(BvTerm::Var {
name: name.to_string(),
sort: Sort::new(1),
}),
Box::new(const_bv(1, 1)),
)),
other => Err(SmtError::Unsupported(format!("boolean atom '{other}'"))),
},
Sexp::List(items) => {
let op = items
.first()
.and_then(as_atom)
.ok_or_else(|| SmtError::Parse("bad predicate".into()))?;
let args = &items[1..];
match op {
"=" if args.iter().all(|a| self.is_bool_sexp(a)) => {
if args.len() < 2 {
return Err(SmtError::Parse("'=' needs two arguments".into()));
}
let terms = args
.iter()
.map(|a| self.parse_bool(a))
.collect::<Result<Vec<_>, _>>()?;
let mut acc: Option<BoolTerm> = None;
for w in terms.windows(2) {
let e = iff(&w[0], &w[1]);
acc = Some(match acc {
None => e,
Some(p) => BoolTerm::And(Box::new(p), Box::new(e)),
});
}
Ok(acc.expect("len >= 2"))
}
"=" => self.chain_eq(args),
"=>" => {
if args.is_empty() {
return Err(SmtError::Parse("'=>' needs arguments".into()));
}
let mut terms = args
.iter()
.map(|a| self.parse_bool(a))
.collect::<Result<Vec<_>, _>>()?;
let mut acc = terms.pop().expect("non-empty");
while let Some(p) = terms.pop() {
acc = BoolTerm::Or(Box::new(BoolTerm::Not(Box::new(p))), Box::new(acc));
}
Ok(acc)
}
"distinct" => self.all_distinct(args),
"bvult" => self.cmp(args, BoolTerm::Ult),
"bvule" => self.cmp(args, BoolTerm::Ule),
"bvugt" => self.cmp(args, BoolTerm::Ugt),
"bvuge" => self.cmp(args, BoolTerm::Uge),
"bvslt" => self.cmp(args, BoolTerm::Slt),
"bvsle" => self.cmp(args, BoolTerm::Sle),
"bvsgt" => self.cmp(args, BoolTerm::Sgt),
"bvsge" => self.cmp(args, BoolTerm::Sge),
"not" => {
if args.len() != 1 {
return Err(SmtError::Parse("not takes 1 arg".into()));
}
Ok(BoolTerm::Not(Box::new(self.parse_bool(&args[0])?)))
}
"and" => {
self.fold_bool("and", args, |a, b| BoolTerm::And(Box::new(a), Box::new(b)))
}
"or" => {
self.fold_bool("or", args, |a, b| BoolTerm::Or(Box::new(a), Box::new(b)))
}
other => Err(SmtError::Unsupported(format!("predicate '{other}'"))),
}
}
}
}
fn chain_eq(&self, args: &[Sexp]) -> Result<BoolTerm, SmtError> {
let bvs = self.parse_bv_list(args)?;
if bvs.len() < 2 {
return Err(SmtError::Parse("'=' needs at least 2 arguments".into()));
}
let eq = |a: &BvTerm, b: &BvTerm| BoolTerm::Eq(Box::new(a.clone()), Box::new(b.clone()));
let mut acc = eq(&bvs[0], &bvs[1]);
for pair in bvs.windows(2).skip(1) {
acc = BoolTerm::And(Box::new(acc), Box::new(eq(&pair[0], &pair[1])));
}
Ok(acc)
}
fn all_distinct(&self, args: &[Sexp]) -> Result<BoolTerm, SmtError> {
let bvs = self.parse_bv_list(args)?;
if bvs.len() < 2 {
return Err(SmtError::Parse(
"'distinct' needs at least 2 arguments".into(),
));
}
let mut acc: Option<BoolTerm> = None;
for i in 0..bvs.len() {
for j in (i + 1)..bvs.len() {
let ne = BoolTerm::Ne(Box::new(bvs[i].clone()), Box::new(bvs[j].clone()));
acc = Some(match acc {
None => ne,
Some(prev) => BoolTerm::And(Box::new(prev), Box::new(ne)),
});
}
}
Ok(acc.expect("len >= 2 guarantees at least one pair"))
}
fn cmp(
&self,
args: &[Sexp],
f: impl Fn(Box<BvTerm>, Box<BvTerm>) -> BoolTerm,
) -> Result<BoolTerm, SmtError> {
let (a, b) = self.two_bv("comparison", args)?;
Ok(f(Box::new(a), Box::new(b)))
}
fn parse_bv_list(&self, args: &[Sexp]) -> Result<Vec<BvTerm>, SmtError> {
args.iter().map(|a| self.parse_bv(a)).collect()
}
fn fold_bv(
&self,
op: &str,
args: &[Sexp],
f: impl Fn(BvTerm, BvTerm) -> BvTerm,
) -> Result<BvTerm, SmtError> {
let mut it = self.parse_bv_list(args)?.into_iter();
let mut acc = it
.next()
.ok_or_else(|| SmtError::Parse(format!("'{op}' needs at least 1 argument")))?;
for next in it {
acc = f(acc, next);
}
Ok(acc)
}
fn fold_bool(
&self,
op: &str,
args: &[Sexp],
f: impl Fn(BoolTerm, BoolTerm) -> BoolTerm,
) -> Result<BoolTerm, SmtError> {
let mut acc: Option<BoolTerm> = None;
for a in args {
let t = self.parse_bool(a)?;
acc = Some(match acc {
None => t,
Some(prev) => f(prev, t),
});
}
acc.ok_or_else(|| SmtError::Parse(format!("'{op}' needs at least 1 argument")))
}
fn one_bv(&self, op: &str, args: &[Sexp]) -> Result<BvTerm, SmtError> {
if args.len() != 1 {
return Err(SmtError::Parse(format!(
"'{op}' takes 1 argument, got {}",
args.len()
)));
}
self.parse_bv(&args[0])
}
fn two_bv(&self, op: &str, args: &[Sexp]) -> Result<(BvTerm, BvTerm), SmtError> {
if args.len() != 2 {
return Err(SmtError::Parse(format!(
"'{op}' takes 2 arguments, got {}",
args.len()
)));
}
Ok((self.parse_bv(&args[0])?, self.parse_bv(&args[1])?))
}
}
fn const_bv(value: u128, width: u32) -> BvTerm {
BvTerm::Const {
value,
sort: Sort::new(width),
}
}
fn parse_indexed_const(items: &[Sexp]) -> Result<BvTerm, SmtError> {
let name = items
.get(1)
.and_then(as_atom)
.ok_or_else(|| SmtError::Parse("bad '(_ bvN W)' constant".into()))?;
let digits = name
.strip_prefix("bv")
.ok_or_else(|| SmtError::Unsupported(format!("indexed identifier '(_ {name} ...)'")))?;
let value = digits
.parse::<u128>()
.map_err(|_| SmtError::Parse(format!("bad bitvector constant '{name}'")))?;
let width = items
.get(2)
.and_then(as_atom)
.and_then(|s| s.parse::<u32>().ok())
.ok_or_else(|| SmtError::Parse("missing width in '(_ bvN W)'".into()))?;
Ok(const_bv(value, width))
}
fn parse_sort(s: &Sexp) -> Result<u32, SmtError> {
if let Sexp::List(items) = s
&& items.len() == 3
&& as_atom(&items[0]) == Some("_")
&& as_atom(&items[1]) == Some("BitVec")
{
return as_atom(&items[2])
.and_then(|w| w.parse::<u32>().ok())
.ok_or_else(|| SmtError::Parse("bad BitVec width".into()));
}
if as_atom(s) == Some("Bool") {
return Ok(1);
}
Err(SmtError::Unsupported("non-BitVec sort".into()))
}
pub fn solve_str(input: &str) -> Result<Outcome, SmtError> {
let toks = tokenize(input);
let sexps = parse_sexps(&toks)?;
let mut ctx = Ctx {
decls: HashMap::new(),
bool_decls: HashSet::new(),
};
let mut declared: Vec<(String, u32)> = Vec::new();
let mut solver = Solver::new();
let mut result: Option<CheckResult> = None;
for s in &sexps {
let items = match s {
Sexp::List(items) if !items.is_empty() => items,
_ => return Err(SmtError::Parse("expected a command list".into())),
};
let cmd = as_atom(&items[0]).ok_or_else(|| SmtError::Parse("bad command head".into()))?;
match cmd {
"set-logic" | "set-info" | "set-option" => {}
"declare-const" => {
let name = decl_name(items, 1)?;
let sort = item(items, 2, "declare-const")?;
let width = parse_sort(sort)?;
if as_atom(sort) == Some("Bool") {
ctx.bool_decls.insert(name.clone());
}
ctx.decls.insert(name.clone(), width);
declared.push((name, width));
}
"declare-fun" => {
let name = decl_name(items, 1)?;
match item(items, 2, "declare-fun")? {
Sexp::List(p) if p.is_empty() => {}
_ => {
return Err(SmtError::Unsupported(
"declare-fun with parameters (uninterpreted function)".into(),
));
}
}
let width = parse_sort(item(items, 3, "declare-fun")?)?;
ctx.decls.insert(name.clone(), width);
declared.push((name, width));
}
"assert" => {
let term = ctx.parse_bool(item(items, 1, "assert")?)?;
solver.assert(term);
}
"check-sat" => {
result = Some(solver.check());
}
"get-model" | "get-value" => {}
"exit" => break,
"push" | "pop" | "reset" | "reset-assertions" | "get-assertions" | "get-unsat-core"
| "get-proof" => {
return Err(SmtError::Unsupported(format!("command '{cmd}'")));
}
other => return Err(SmtError::Unsupported(format!("command '{other}'"))),
}
}
Ok(Outcome { result, declared })
}
fn item<'a>(items: &'a [Sexp], n: usize, cmd: &str) -> Result<&'a Sexp, SmtError> {
items
.get(n)
.ok_or_else(|| SmtError::Parse(format!("'{cmd}' is missing an argument")))
}
fn decl_name(items: &[Sexp], n: usize) -> Result<String, SmtError> {
as_atom(item(items, n, "declaration")?)
.map(str::to_string)
.ok_or_else(|| SmtError::Parse("declaration name must be a symbol".into()))
}
fn iff(a: &BoolTerm, b: &BoolTerm) -> BoolTerm {
BoolTerm::And(
Box::new(BoolTerm::Or(
Box::new(BoolTerm::Not(Box::new(a.clone()))),
Box::new(b.clone()),
)),
Box::new(BoolTerm::Or(
Box::new(BoolTerm::Not(Box::new(b.clone()))),
Box::new(a.clone()),
)),
)
}
#[cfg(test)]
mod tests {
#[test]
fn bool_declared_const_reads_as_bv1_equality() {
let src = "(set-logic QF_BV)(declare-const p Bool)(assert p)(check-sat)";
match solve_str(src).unwrap().result.unwrap() {
CheckResult::Sat(_) => {}
other => panic!("a free Bool must be satisfiable, got {other:?}"),
}
}
#[test]
fn bool_const_is_falsifiable_too() {
let src = "(set-logic QF_BV)(declare-const p Bool)(assert p)(assert (not p))(check-sat)";
match solve_str(src).unwrap().result.unwrap() {
CheckResult::Unsat(_) => {}
other => panic!("p AND NOT p must be UNSAT, got {other:?}"),
}
}
#[test]
fn implication_is_right_associative() {
let src = "(set-logic QF_BV)\
(declare-const a Bool)(declare-const b Bool)(declare-const c Bool)\
(assert a)(assert b)(assert (not c))(assert (=> a b c))(check-sat)";
match solve_str(src).unwrap().result.unwrap() {
CheckResult::Unsat(_) => {}
other => panic!("(=> a b c) with a,b true and c false must be UNSAT, got {other:?}"),
}
let ok = "(set-logic QF_BV)\
(declare-const a Bool)(declare-const b Bool)(declare-const c Bool)\
(assert (=> a b c))(check-sat)";
match solve_str(ok).unwrap().result.unwrap() {
CheckResult::Sat(_) => {}
other => panic!("(=> a b c) alone must be satisfiable, got {other:?}"),
}
}
#[test]
fn real_verus_by_bit_vector_vc_discharges() {
let src = include_str!("../tests/fixtures/verus_gale_cpu_mask_slice.smt2");
match solve_str(src).unwrap().result.unwrap() {
CheckResult::Unsat(cert) => {
cert.recheck()
.expect("Verus's own VC must yield a re-checkable certificate");
}
other => panic!("Verus's cpu_mask by(bit_vector) VC must be UNSAT, got {other:?}"),
}
}
use super::*;
use crate::Model;
fn verdict(src: &str) -> CheckResult {
solve_str(src)
.unwrap_or_else(|e| panic!("reader error: {e}"))
.result
.expect("script had a (check-sat)")
}
fn model_of(m: &Model, name: &str) -> Option<u128> {
m.assignments
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| *v)
}
#[test]
fn add_zero_identity_negation_is_unsat() {
let src = "
(set-logic QF_BV)
(declare-const x (_ BitVec 8))
(assert (not (= (bvadd x #x00) x)))
(check-sat)";
assert!(matches!(verdict(src), CheckResult::Unsat(_)));
}
#[test]
fn equality_to_constant_is_sat_with_model() {
let src = "
(declare-const x (_ BitVec 32))
(assert (= x #x0000002a))
(check-sat)
(get-model)";
let out = solve_str(src).unwrap();
assert_eq!(out.declared, vec![("x".to_string(), 32)]);
match out.result {
Some(CheckResult::Sat(m)) => assert_eq!(model_of(&m, "x"), Some(0x2a)),
other => panic!("expected sat with x=0x2a, got {other:?}"),
}
}
#[test]
fn udiv_by_zero_equivalence_is_unsat() {
let src = "
(declare-const x (_ BitVec 8))
(assert (not (= (bvudiv x #x00) #xff)))
(check-sat)";
assert!(matches!(verdict(src), CheckResult::Unsat(_)));
}
#[test]
fn signed_and_unsigned_compare_disagree() {
let src = "
(declare-const x (_ BitVec 8))
(assert (bvult #x7f x))
(assert (bvslt x #x7f))
(check-sat)
(get-value (x))";
match verdict(src) {
CheckResult::Sat(m) => assert_eq!(model_of(&m, "x"), Some(0x80)),
other => panic!("expected sat with x=0x80, got {other:?}"),
}
}
#[test]
fn ite_selects_branch() {
let src = "
(declare-const x (_ BitVec 8))
(assert (= (ite (bvult x #x0a) #x01 #x00) #x01))
(check-sat)";
match verdict(src) {
CheckResult::Sat(m) => assert!(model_of(&m, "x").unwrap() < 0x0a),
other => panic!("expected sat, got {other:?}"),
}
}
#[test]
fn bvsdiv_signed_division() {
let src = "
(declare-const x (_ BitVec 8))
(assert (= x #xfe))
(assert (= (bvsdiv x #x02) #xff))
(check-sat)";
match verdict(src) {
CheckResult::Sat(m) => assert_eq!(model_of(&m, "x"), Some(0xfe)),
other => panic!("expected sat, got {other:?}"),
}
}
#[test]
fn rotate_left_constant_amount() {
let src = "
(declare-const x (_ BitVec 8))
(assert (= ((_ rotate_left 1) #x81) x))
(check-sat)
(get-model)";
match verdict(src) {
CheckResult::Sat(m) => assert_eq!(model_of(&m, "x"), Some(0x03)),
other => panic!("expected sat with x=0x03, got {other:?}"),
}
}
#[test]
fn rotate_right_constant_amount() {
let src = "
(declare-const x (_ BitVec 8))
(assert (= ((_ rotate_right 1) #x81) x))
(check-sat)";
match verdict(src) {
CheckResult::Sat(m) => assert_eq!(model_of(&m, "x"), Some(0xc0)),
other => panic!("expected sat with x=0xc0, got {other:?}"),
}
}
#[test]
fn zero_extend_and_concat() {
let src = "
(declare-const x (_ BitVec 8))
(assert (= x #xff))
(assert (= ((_ zero_extend 8) x) #x00ff))
(check-sat)";
assert!(matches!(verdict(src), CheckResult::Sat(_)));
}
#[test]
fn declare_fun_nullary_is_accepted() {
let src = "
(declare-fun y () (_ BitVec 16))
(assert (= y #x00ff))
(check-sat)
(get-model)";
match verdict(src) {
CheckResult::Sat(m) => assert_eq!(model_of(&m, "y"), Some(0xff)),
other => panic!("expected sat, got {other:?}"),
}
}
#[test]
fn unsupported_operator_is_rejected() {
let src = "
(declare-const x (_ BitVec 8))
(assert (= (bvfoo x x) #x00))
(check-sat)";
assert!(matches!(solve_str(src), Err(SmtError::Unsupported(_))));
}
#[test]
fn unsupported_command_is_rejected() {
let src = "(push 1)(check-sat)";
assert!(matches!(solve_str(src), Err(SmtError::Unsupported(_))));
}
#[test]
fn parse_error_on_unbalanced_parens() {
let src = "(declare-const x (_ BitVec 8)) (assert (= x #x00)";
assert!(matches!(solve_str(src), Err(SmtError::Parse(_))));
}
#[test]
fn unknown_symbol_is_a_parse_error() {
let src = "
(declare-const x (_ BitVec 8))
(assert (= x y))
(check-sat)";
assert!(matches!(solve_str(src), Err(SmtError::Parse(_))));
}
}