use crate::term::{BoolTerm, BvTerm, Sort};
fn var(name: String, w: u32) -> BvTerm {
BvTerm::Var {
name,
sort: Sort::new(w),
}
}
fn c(value: u128, w: u32) -> BvTerm {
BvTerm::Const {
value,
sort: Sort::new(w),
}
}
fn b(t: BvTerm) -> Box<BvTerm> {
Box::new(t)
}
fn eq(a: BvTerm, bb: BvTerm) -> BoolTerm {
BoolTerm::Eq(b(a), b(bb))
}
fn and(a: BoolTerm, bb: BoolTerm) -> BoolTerm {
BoolTerm::And(Box::new(a), Box::new(bb))
}
fn ite(cond: BoolTerm, then_: BvTerm, else_: BvTerm) -> BvTerm {
BvTerm::Ite {
cond: Box::new(cond),
then_: b(then_),
else_: b(else_),
}
}
pub fn queue_overflow(k: usize, cap: u8) -> Vec<BoolTerm> {
let w = 8;
let mut assertions = Vec::with_capacity(k + 2);
assertions.push(eq(var("q_0".into(), w), c(0, w)));
for j in 0..k {
let q = var(format!("q_{j}"), w);
let q_next = var(format!("q_{}", j + 1), w);
let ch = var(format!("ch_{j}"), 8);
let is_producer = eq(ch.clone(), c(0, 8));
let q_nonempty = BoolTerm::Ugt(b(q.clone()), b(c(0, w)));
let next = ite(
is_producer,
BvTerm::Add(b(q.clone()), b(c(1, w))),
ite(q_nonempty, BvTerm::Sub(b(q.clone()), b(c(1, w))), q.clone()),
);
assertions.push(eq(q_next, next));
}
let mut bad = BoolTerm::Ugt(b(var("q_1".into(), w)), b(c(cap as u128, w)));
for j in 2..=k {
bad = BoolTerm::Or(
Box::new(bad),
Box::new(BoolTerm::Ugt(
b(var(format!("q_{j}"), w)),
b(c(cap as u128, w)),
)),
);
}
assertions.push(bad);
assertions
}
pub fn deadlock(k: usize, inverted: bool) -> Vec<BoolTerm> {
let w = 2;
let mut assertions = Vec::new();
for v in ["pa_0", "pb_0", "l1_0", "l2_0"] {
assertions.push(eq(var(v.into(), w), c(0, w)));
}
for j in 0..k {
let pa = var(format!("pa_{j}"), w);
let pb = var(format!("pb_{j}"), w);
let l1 = var(format!("l1_{j}"), w);
let l2 = var(format!("l2_{j}"), w);
let ch = var(format!("ch_{j}"), 8);
let a_steps = eq(ch.clone(), c(0, 8));
let step = |pc: &BvTerm, first: &BvTerm, second: &BvTerm, me: u128| {
let at = |n: u128| eq(pc.clone(), c(n, w));
let free = |l: &BvTerm| eq(l.clone(), c(0, w));
let pc_next = ite(
and(at(0), free(first)),
c(1, w),
ite(
and(at(1), free(second)),
c(2, w),
ite(at(2), c(3, w), pc.clone()),
),
);
let first_next = ite(
and(at(0), free(first)),
c(me, w),
ite(at(2), c(0, w), first.clone()),
);
let second_next = ite(
and(at(1), free(second)),
c(me, w),
ite(at(2), c(0, w), second.clone()),
);
(pc_next, first_next, second_next)
};
let (pa_n, a_l1, a_l2) = step(&pa, &l1, &l2, 1);
let (pb_n, b_first, b_second) = if inverted {
step(&pb, &l2, &l1, 2)
} else {
step(&pb, &l1, &l2, 2)
};
let (b_l1, b_l2) = if inverted {
(b_second, b_first)
} else {
(b_first, b_second)
};
assertions.push(eq(
var(format!("pa_{}", j + 1), w),
ite(a_steps.clone(), pa_n, pa.clone()),
));
assertions.push(eq(
var(format!("pb_{}", j + 1), w),
ite(a_steps.clone(), pb.clone(), pb_n),
));
assertions.push(eq(
var(format!("l1_{}", j + 1), w),
ite(a_steps.clone(), a_l1, b_l1),
));
assertions.push(eq(
var(format!("l2_{}", j + 1), w),
ite(a_steps, a_l2, b_l2),
));
}
let bad_at = |j: usize| {
and(
and(
eq(var(format!("pa_{j}"), w), c(1, w)),
eq(var(format!("pb_{j}"), w), c(1, w)),
),
and(
BoolTerm::Ne(b(var(format!("l1_{j}"), w)), b(c(0, w))),
BoolTerm::Ne(b(var(format!("l2_{j}"), w)), b(c(0, w))),
),
)
};
let mut bad = bad_at(1);
for j in 2..=k {
bad = BoolTerm::Or(Box::new(bad), Box::new(bad_at(j)));
}
assertions.push(bad);
assertions
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solver::{CheckResult, Solver};
fn check(assertions: &[BoolTerm]) -> CheckResult {
let mut s = Solver::new();
for a in assertions {
s.assert(a.clone());
}
s.check()
}
#[test]
fn undersized_queue_overflows_with_replayable_trace() {
let k = 8;
let cap = 3u8;
match check(&queue_overflow(k, cap)) {
CheckResult::Sat(model) => {
let get = |name: &str| {
model
.assignments
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| *v)
.unwrap_or(0)
};
let mut q: u8 = 0;
let mut overflowed = false;
for j in 0..k {
if get(&format!("ch_{j}")) == 0 {
q = q.wrapping_add(1);
} else {
q = q.saturating_sub(1);
}
if q > cap {
overflowed = true;
}
}
assert!(overflowed, "model's schedule must actually overflow");
}
other => panic!("undersized queue must be SAT, got {other:?}"),
}
}
#[test]
fn sized_queue_is_unsat_with_certificate() {
let k = 8;
match check(&queue_overflow(k, k as u8)) {
CheckResult::Unsat(cert) => {
cert.recheck().expect("certificate must re-check offline");
}
other => panic!("sized queue must be UNSAT, got {other:?}"),
}
}
#[test]
fn ordered_locks_have_no_deadlock() {
match check(&deadlock(6, false)) {
CheckResult::Unsat(cert) => {
cert.recheck().expect("certificate must re-check offline");
}
other => panic!("ordered locks must be UNSAT, got {other:?}"),
}
}
#[test]
fn inverted_locks_deadlock() {
match check(&deadlock(6, true)) {
CheckResult::Sat(_) => {}
other => panic!("inverted locks must be SAT, got {other:?}"),
}
}
#[cfg(all(feature = "cadical", not(target_family = "wasm")))]
#[test]
fn cadical_agrees_on_bmc_shapes() {
for (assertions, expect_sat) in [
(queue_overflow(8, 3), true),
(queue_overflow(8, 8), false),
(deadlock(6, true), true),
(deadlock(6, false), false),
] {
let mut s = Solver::new();
for a in &assertions {
s.assert(a.clone());
}
match (s.check_with_cadical(), expect_sat) {
(CheckResult::Sat(_), true) => {}
(CheckResult::Unsat(cert), false) => {
cert.recheck().expect("cadical certificate must re-check");
}
(got, want) => panic!("cadical disagrees: want sat={want}, got {got:?}"),
}
}
}
}