use crate::cdcl::SolveResult;
use crate::cnf::Cnf;
use crate::sat::{decode_model, prove_unsat, UnsatOutcome};
use crate::ProofExpr;
fn not(e: ProofExpr) -> ProofExpr {
ProofExpr::Not(Box::new(e))
}
fn conj(mut parts: Vec<ProofExpr>) -> ProofExpr {
match parts.len() {
0 => {
let c = ProofExpr::Atom("__bmc_true".to_string());
ProofExpr::Or(Box::new(c.clone()), Box::new(not(c)))
}
1 => parts.pop().unwrap(),
_ => {
let mut acc = parts.pop().unwrap();
while let Some(p) = parts.pop() {
acc = ProofExpr::And(Box::new(p), Box::new(acc));
}
acc
}
}
}
fn unrolled_path(
init: &ProofExpr,
trans: &dyn Fn(u32) -> ProofExpr,
k: u32,
) -> Vec<ProofExpr> {
let mut parts = vec![init.clone()];
for i in 0..k {
parts.push(trans(i));
}
parts
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BmcOutcome {
CounterexampleAt { k: u32, trace: Vec<(String, bool)> },
NoneWithin(u32),
Unsupported,
}
pub fn find_counterexample(
init: &ProofExpr,
trans: &dyn Fn(u32) -> ProofExpr,
property: &dyn Fn(u32) -> ProofExpr,
max_k: u32,
) -> BmcOutcome {
for k in 0..=max_k {
let mut parts = unrolled_path(init, trans, k);
parts.push(not(property(k)));
match prove_unsat(&conj(parts)) {
UnsatOutcome::Sat(trace) => return BmcOutcome::CounterexampleAt { k, trace },
UnsatOutcome::Refuted => continue,
UnsatOutcome::Unsupported => return BmcOutcome::Unsupported,
}
}
BmcOutcome::NoneWithin(max_k)
}
pub fn find_counterexample_incremental(
init: &ProofExpr,
trans: &dyn Fn(u32) -> ProofExpr,
property: &dyn Fn(u32) -> ProofExpr,
max_k: u32,
) -> BmcOutcome {
let mut cnf = Cnf::new();
if cnf.assert(init).is_none() {
return BmcOutcome::Unsupported;
}
for i in 0..max_k {
if cnf.assert(&trans(i)).is_none() {
return BmcOutcome::Unsupported;
}
}
let mut bad = Vec::with_capacity(max_k as usize + 1);
let mut atom_exprs: Vec<ProofExpr> = vec![init.clone()];
for i in 0..max_k {
atom_exprs.push(trans(i));
}
for k in 0..=max_k {
let violation = not(property(k));
match cnf.encode(&violation) {
Some(lit) => bad.push(lit),
None => return BmcOutcome::Unsupported,
}
atom_exprs.push(violation);
}
let decode_cnf = cnf.clone();
let mut solver = cnf.into_solver();
let refs: Vec<&ProofExpr> = atom_exprs.iter().collect();
for (k, &activation) in bad.iter().enumerate() {
match solver.solve_under_assumptions(&[activation]) {
SolveResult::Sat(model) => {
return BmcOutcome::CounterexampleAt {
k: k as u32,
trace: decode_model(&decode_cnf, &model, &refs),
}
}
SolveResult::Unsat => continue,
}
}
BmcOutcome::NoneWithin(max_k)
}
fn split_frame(a: &str) -> Option<(&str, &str)> {
a.rsplit_once('@')
}
fn swap_signals(e: &ProofExpr, a: &str, b: &str) -> ProofExpr {
match e {
ProofExpr::Atom(s) => match split_frame(s) {
Some((base, frame)) => {
let nb = if base == a {
b
} else if base == b {
a
} else {
base
};
ProofExpr::Atom(format!("{nb}@{frame}"))
}
None => e.clone(),
},
ProofExpr::Not(x) => not(swap_signals(x, a, b)),
ProofExpr::And(x, y) => {
ProofExpr::And(Box::new(swap_signals(x, a, b)), Box::new(swap_signals(y, a, b)))
}
ProofExpr::Or(x, y) => {
ProofExpr::Or(Box::new(swap_signals(x, a, b)), Box::new(swap_signals(y, a, b)))
}
ProofExpr::Iff(x, y) => {
ProofExpr::Iff(Box::new(swap_signals(x, a, b)), Box::new(swap_signals(y, a, b)))
}
ProofExpr::Implies(x, y) => {
ProofExpr::Implies(Box::new(swap_signals(x, a, b)), Box::new(swap_signals(y, a, b)))
}
other => other.clone(),
}
}
fn equivalent(e: &ProofExpr, f: &ProofExpr) -> bool {
let xor = ProofExpr::Or(
Box::new(ProofExpr::And(Box::new(e.clone()), Box::new(not(f.clone())))),
Box::new(ProofExpr::And(Box::new(f.clone()), Box::new(not(e.clone())))),
);
matches!(prove_unsat(&xor), UnsatOutcome::Refuted)
}
fn base_signals(e: &ProofExpr, out: &mut std::collections::BTreeSet<String>) {
match e {
ProofExpr::Atom(s) => {
if let Some((base, _)) = split_frame(s) {
out.insert(base.to_string());
}
}
ProofExpr::Not(x) => base_signals(x, out),
ProofExpr::And(x, y)
| ProofExpr::Or(x, y)
| ProofExpr::Iff(x, y)
| ProofExpr::Implies(x, y) => {
base_signals(x, out);
base_signals(y, out);
}
_ => {}
}
}
pub fn temporal_symmetry_pairs(
init: &ProofExpr,
trans0: &ProofExpr,
property0: &ProofExpr,
) -> Vec<(String, String)> {
let mut set = std::collections::BTreeSet::new();
base_signals(init, &mut set);
base_signals(trans0, &mut set);
base_signals(property0, &mut set);
let sigs: Vec<String> = set.into_iter().collect();
let mut pairs = Vec::new();
for i in 0..sigs.len() {
for j in (i + 1)..sigs.len() {
let (a, b) = (sigs[i].as_str(), sigs[j].as_str());
if equivalent(init, &swap_signals(init, a, b))
&& equivalent(trans0, &swap_signals(trans0, a, b))
&& equivalent(property0, &swap_signals(property0, a, b))
{
pairs.push((a.to_string(), b.to_string()));
}
}
}
pairs
}
pub fn find_counterexample_symmetric(
init: &ProofExpr,
trans: &dyn Fn(u32) -> ProofExpr,
property: &dyn Fn(u32) -> ProofExpr,
max_k: u32,
) -> BmcOutcome {
let pairs = temporal_symmetry_pairs(init, &trans(0), &property(0));
let breaks: Vec<ProofExpr> = pairs
.iter()
.map(|(a, b)| {
ProofExpr::Or(
Box::new(not(ProofExpr::Atom(format!("{a}@0")))),
Box::new(ProofExpr::Atom(format!("{b}@0"))),
)
})
.collect();
for k in 0..=max_k {
let mut parts = unrolled_path(init, trans, k);
parts.push(not(property(k)));
parts.extend(breaks.iter().cloned());
match prove_unsat(&conj(parts)) {
UnsatOutcome::Sat(trace) => return BmcOutcome::CounterexampleAt { k, trace },
UnsatOutcome::Refuted => continue,
UnsatOutcome::Unsupported => return BmcOutcome::Unsupported,
}
}
BmcOutcome::NoneWithin(max_k)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InductionOutcome {
Proven,
CounterexampleAt { k: u32, trace: Vec<(String, bool)> },
NotInductive,
Unsupported,
}
pub fn prove_invariant(
init: &ProofExpr,
trans: &dyn Fn(u32) -> ProofExpr,
property: &dyn Fn(u32) -> ProofExpr,
k: u32,
) -> InductionOutcome {
for j in 0..k {
let mut parts = unrolled_path(init, trans, j);
parts.push(not(property(j)));
match prove_unsat(&conj(parts)) {
UnsatOutcome::Refuted => {}
UnsatOutcome::Sat(trace) => {
return InductionOutcome::CounterexampleAt { k: j, trace }
}
UnsatOutcome::Unsupported => return InductionOutcome::Unsupported,
}
}
let mut parts = Vec::new();
for i in 0..k {
parts.push(property(i));
parts.push(trans(i));
}
parts.push(not(property(k)));
match prove_unsat(&conj(parts)) {
UnsatOutcome::Refuted => InductionOutcome::Proven,
UnsatOutcome::Sat(_) => InductionOutcome::NotInductive,
UnsatOutcome::Unsupported => InductionOutcome::Unsupported,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VacuityOutcome {
Vacuous,
Reachable(Vec<(String, bool)>),
Unsupported,
}
pub fn check_vacuity(antecedent: &ProofExpr) -> VacuityOutcome {
match prove_unsat(antecedent) {
UnsatOutcome::Refuted => VacuityOutcome::Vacuous,
UnsatOutcome::Sat(witness) => VacuityOutcome::Reachable(witness),
UnsatOutcome::Unsupported => VacuityOutcome::Unsupported,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn atom(s: &str) -> ProofExpr {
ProofExpr::Atom(s.to_string())
}
fn iff(a: ProofExpr, b: ProofExpr) -> ProofExpr {
ProofExpr::Iff(Box::new(a), Box::new(b))
}
fn latched_init() -> ProofExpr {
atom("x@0")
}
fn latched_trans(t: u32) -> ProofExpr {
iff(atom(&format!("x@{}", t + 1)), atom(&format!("x@{}", t)))
}
fn latched_prop(t: u32) -> ProofExpr {
atom(&format!("x@{}", t))
}
fn toggle_init() -> ProofExpr {
not(atom("q@0"))
}
fn toggle_trans(t: u32) -> ProofExpr {
iff(atom(&format!("q@{}", t + 1)), not(atom(&format!("q@{}", t))))
}
fn toggle_always_false(t: u32) -> ProofExpr {
not(atom(&format!("q@{}", t)))
}
#[test]
fn bmc_finds_toggle_violation_at_step_one() {
let out = find_counterexample(&toggle_init(), &toggle_trans, &toggle_always_false, 5);
match out {
BmcOutcome::CounterexampleAt { k, trace } => {
assert_eq!(k, 1, "shallowest violation is at step 1");
assert!(
trace.iter().any(|(n, v)| n == "q@1" && *v),
"trace must show q@1 high: {trace:?}"
);
}
other => panic!("expected a counterexample, got {other:?}"),
}
}
#[test]
fn bmc_no_counterexample_for_latched_invariant() {
let out = find_counterexample(&latched_init(), &latched_trans, &latched_prop, 6);
assert_eq!(out, BmcOutcome::NoneWithin(6));
}
#[test]
fn k_induction_proves_latched_invariant() {
let out = prove_invariant(&latched_init(), &latched_trans, &latched_prop, 1);
assert_eq!(out, InductionOutcome::Proven);
}
#[test]
fn k_induction_finds_toggle_counterexample_in_base() {
let out = prove_invariant(&toggle_init(), &toggle_trans, &toggle_always_false, 3);
match out {
InductionOutcome::CounterexampleAt { k, .. } => assert_eq!(k, 1),
other => panic!("expected a base-case counterexample, got {other:?}"),
}
}
#[test]
fn vacuity_detects_dead_trigger() {
let dead = ProofExpr::And(Box::new(atom("req@0")), Box::new(not(atom("req@0"))));
assert_eq!(check_vacuity(&dead), VacuityOutcome::Vacuous);
}
#[test]
fn vacuity_accepts_a_live_trigger() {
match check_vacuity(&atom("req@0")) {
VacuityOutcome::Reachable(w) => {
assert!(w.iter().any(|(n, v)| n == "req@0" && *v));
}
other => panic!("a live trigger must be Reachable, got {other:?}"),
}
}
#[test]
fn incremental_bmc_finds_toggle_violation_at_step_one() {
match find_counterexample_incremental(&toggle_init(), &toggle_trans, &toggle_always_false, 5) {
BmcOutcome::CounterexampleAt { k, trace } => {
assert_eq!(k, 1);
assert!(trace.iter().any(|(n, v)| n == "q@1" && *v), "trace: {trace:?}");
}
other => panic!("expected a counterexample, got {other:?}"),
}
}
#[test]
fn temporal_symmetry_detected_and_broken_in_bmc() {
let sym_init = ProofExpr::Or(Box::new(atom("s0@0")), Box::new(atom("s1@0")));
let sym_trans = |t: u32| {
conj(vec![
iff(atom(&format!("s0@{}", t + 1)), atom(&format!("s0@{}", t))),
iff(atom(&format!("s1@{}", t + 1)), atom(&format!("s1@{}", t))),
])
};
let not_both = |t: u32| {
not(ProofExpr::And(
Box::new(atom(&format!("s0@{}", t))),
Box::new(atom(&format!("s1@{}", t))),
))
};
let pairs = temporal_symmetry_pairs(&sym_init, &sym_trans(0), ¬_both(0));
assert_eq!(pairs, vec![("s0".to_string(), "s1".to_string())], "s0 ↔ s1 is a temporal symmetry");
let plain = find_counterexample(&sym_init, &sym_trans, ¬_both, 4);
let broken = find_counterexample_symmetric(&sym_init, &sym_trans, ¬_both, 4);
assert!(matches!(plain, BmcOutcome::CounterexampleAt { k: 0, .. }), "plain: {plain:?}");
assert!(matches!(broken, BmcOutcome::CounterexampleAt { k: 0, .. }), "broken: {broken:?}");
let at_least_one =
|t: u32| ProofExpr::Or(Box::new(atom(&format!("s0@{}", t))), Box::new(atom(&format!("s1@{}", t))));
assert_eq!(
find_counterexample_symmetric(&sym_init, &sym_trans, &at_least_one, 5),
find_counterexample(&sym_init, &sym_trans, &at_least_one, 5),
"symmetric BMC matches plain BMC on a held invariant"
);
assert!(
temporal_symmetry_pairs(&toggle_init(), &toggle_trans(0), &toggle_always_false(0)).is_empty(),
"a one-signal system has no signal-swap symmetry"
);
assert_eq!(
find_counterexample_symmetric(&toggle_init(), &toggle_trans, &toggle_always_false, 5),
find_counterexample(&toggle_init(), &toggle_trans, &toggle_always_false, 5),
"with no symmetry, symmetric BMC = plain BMC"
);
}
#[test]
fn incremental_bmc_no_counterexample_for_latched_invariant() {
assert_eq!(
find_counterexample_incremental(&latched_init(), &latched_trans, &latched_prop, 6),
BmcOutcome::NoneWithin(6)
);
}
fn sig(i: usize, t: u32) -> ProofExpr {
ProofExpr::Atom(format!("s{i}@{t}"))
}
fn lit_b(b: bool, e: ProofExpr) -> ProofExpr {
if b {
e
} else {
ProofExpr::Not(Box::new(e))
}
}
#[test]
fn incremental_matches_nonincremental_and_simulation() {
let mut state = 0xb5ad_4ece_da1c_e2a9u64;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let n = 3usize;
let max_k = 5u32;
for _trial in 0..120 {
let init_bits: Vec<bool> = (0..n).map(|_| next() & 1 == 0).collect();
let src: Vec<usize> = (0..n).map(|_| (next() % n as u64) as usize).collect();
let inv: Vec<bool> = (0..n).map(|_| next() & 1 == 0).collect();
let p = (next() % n as u64) as usize;
let want = next() & 1 == 0;
let init_expr = conj((0..n).map(|i| lit_b(init_bits[i], sig(i, 0))).collect());
let (src_t, inv_t) = (src.clone(), inv.clone());
let trans = move |t: u32| {
conj((0..n)
.map(|i| {
let rhs = lit_b(inv_t[i], sig(src_t[i], t));
ProofExpr::Iff(Box::new(sig(i, t + 1)), Box::new(rhs))
})
.collect())
};
let prop = move |t: u32| lit_b(want, sig(p, t));
let mut cur = init_bits.clone();
let mut expected: Option<u32> = None;
for k in 0..=max_k {
if (cur[p] == want) == false {
expected = Some(k);
break;
}
let nxt: Vec<bool> = (0..n)
.map(|i| if inv[i] { cur[src[i]] } else { !cur[src[i]] })
.collect();
cur = nxt;
}
let k_of = |o: &BmcOutcome| match o {
BmcOutcome::CounterexampleAt { k, .. } => Some(*k),
BmcOutcome::NoneWithin(_) => None,
BmcOutcome::Unsupported => panic!("unexpected Unsupported"),
};
let ni = find_counterexample(&init_expr, &trans, &prop, max_k);
let inc = find_counterexample_incremental(&init_expr, &trans, &prop, max_k);
assert_eq!(k_of(&ni), expected, "non-incremental BMC disagrees with simulation");
assert_eq!(k_of(&inc), expected, "incremental BMC disagrees with simulation");
}
}
}