1use crate::cdcl::SolveResult;
20use crate::cnf::Cnf;
21use crate::sat::{decode_model, prove_unsat, UnsatOutcome};
22use crate::ProofExpr;
23
24fn not(e: ProofExpr) -> ProofExpr {
25 ProofExpr::Not(Box::new(e))
26}
27
28fn conj(mut parts: Vec<ProofExpr>) -> ProofExpr {
31 match parts.len() {
32 0 => {
33 let c = ProofExpr::Atom("__bmc_true".to_string());
34 ProofExpr::Or(Box::new(c.clone()), Box::new(not(c)))
35 }
36 1 => parts.pop().unwrap(),
37 _ => {
38 let mut acc = parts.pop().unwrap();
39 while let Some(p) = parts.pop() {
40 acc = ProofExpr::And(Box::new(p), Box::new(acc));
41 }
42 acc
43 }
44 }
45}
46
47fn unrolled_path(
50 init: &ProofExpr,
51 trans: &dyn Fn(u32) -> ProofExpr,
52 k: u32,
53) -> Vec<ProofExpr> {
54 let mut parts = vec![init.clone()];
55 for i in 0..k {
56 parts.push(trans(i));
57 }
58 parts
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
63pub enum BmcOutcome {
64 CounterexampleAt { k: u32, trace: Vec<(String, bool)> },
66 NoneWithin(u32),
68 Unsupported,
70}
71
72pub fn find_counterexample(
75 init: &ProofExpr,
76 trans: &dyn Fn(u32) -> ProofExpr,
77 property: &dyn Fn(u32) -> ProofExpr,
78 max_k: u32,
79) -> BmcOutcome {
80 for k in 0..=max_k {
81 let mut parts = unrolled_path(init, trans, k);
82 parts.push(not(property(k)));
83 match prove_unsat(&conj(parts)) {
84 UnsatOutcome::Sat(trace) => return BmcOutcome::CounterexampleAt { k, trace },
85 UnsatOutcome::Refuted => continue,
86 UnsatOutcome::Unsupported => return BmcOutcome::Unsupported,
87 }
88 }
89 BmcOutcome::NoneWithin(max_k)
90}
91
92pub fn find_counterexample_incremental(
102 init: &ProofExpr,
103 trans: &dyn Fn(u32) -> ProofExpr,
104 property: &dyn Fn(u32) -> ProofExpr,
105 max_k: u32,
106) -> BmcOutcome {
107 let mut cnf = Cnf::new();
108 if cnf.assert(init).is_none() {
109 return BmcOutcome::Unsupported;
110 }
111 for i in 0..max_k {
112 if cnf.assert(&trans(i)).is_none() {
113 return BmcOutcome::Unsupported;
114 }
115 }
116 let mut bad = Vec::with_capacity(max_k as usize + 1);
120 let mut atom_exprs: Vec<ProofExpr> = vec![init.clone()];
121 for i in 0..max_k {
122 atom_exprs.push(trans(i));
123 }
124 for k in 0..=max_k {
125 let violation = not(property(k));
126 match cnf.encode(&violation) {
127 Some(lit) => bad.push(lit),
128 None => return BmcOutcome::Unsupported,
129 }
130 atom_exprs.push(violation);
131 }
132
133 let decode_cnf = cnf.clone();
134 let mut solver = cnf.into_solver();
135 let refs: Vec<&ProofExpr> = atom_exprs.iter().collect();
136 for (k, &activation) in bad.iter().enumerate() {
137 match solver.solve_under_assumptions(&[activation]) {
138 SolveResult::Sat(model) => {
139 return BmcOutcome::CounterexampleAt {
140 k: k as u32,
141 trace: decode_model(&decode_cnf, &model, &refs),
142 }
143 }
144 SolveResult::Unsat => continue,
145 }
146 }
147 BmcOutcome::NoneWithin(max_k)
148}
149
150fn split_frame(a: &str) -> Option<(&str, &str)> {
161 a.rsplit_once('@')
162}
163
164fn swap_signals(e: &ProofExpr, a: &str, b: &str) -> ProofExpr {
166 match e {
167 ProofExpr::Atom(s) => match split_frame(s) {
168 Some((base, frame)) => {
169 let nb = if base == a {
170 b
171 } else if base == b {
172 a
173 } else {
174 base
175 };
176 ProofExpr::Atom(format!("{nb}@{frame}"))
177 }
178 None => e.clone(),
179 },
180 ProofExpr::Not(x) => not(swap_signals(x, a, b)),
181 ProofExpr::And(x, y) => {
182 ProofExpr::And(Box::new(swap_signals(x, a, b)), Box::new(swap_signals(y, a, b)))
183 }
184 ProofExpr::Or(x, y) => {
185 ProofExpr::Or(Box::new(swap_signals(x, a, b)), Box::new(swap_signals(y, a, b)))
186 }
187 ProofExpr::Iff(x, y) => {
188 ProofExpr::Iff(Box::new(swap_signals(x, a, b)), Box::new(swap_signals(y, a, b)))
189 }
190 ProofExpr::Implies(x, y) => {
191 ProofExpr::Implies(Box::new(swap_signals(x, a, b)), Box::new(swap_signals(y, a, b)))
192 }
193 other => other.clone(),
194 }
195}
196
197fn equivalent(e: &ProofExpr, f: &ProofExpr) -> bool {
200 let xor = ProofExpr::Or(
201 Box::new(ProofExpr::And(Box::new(e.clone()), Box::new(not(f.clone())))),
202 Box::new(ProofExpr::And(Box::new(f.clone()), Box::new(not(e.clone())))),
203 );
204 matches!(prove_unsat(&xor), UnsatOutcome::Refuted)
205}
206
207fn base_signals(e: &ProofExpr, out: &mut std::collections::BTreeSet<String>) {
209 match e {
210 ProofExpr::Atom(s) => {
211 if let Some((base, _)) = split_frame(s) {
212 out.insert(base.to_string());
213 }
214 }
215 ProofExpr::Not(x) => base_signals(x, out),
216 ProofExpr::And(x, y)
217 | ProofExpr::Or(x, y)
218 | ProofExpr::Iff(x, y)
219 | ProofExpr::Implies(x, y) => {
220 base_signals(x, out);
221 base_signals(y, out);
222 }
223 _ => {}
224 }
225}
226
227pub fn temporal_symmetry_pairs(
232 init: &ProofExpr,
233 trans0: &ProofExpr,
234 property0: &ProofExpr,
235) -> Vec<(String, String)> {
236 let mut set = std::collections::BTreeSet::new();
237 base_signals(init, &mut set);
238 base_signals(trans0, &mut set);
239 base_signals(property0, &mut set);
240 let sigs: Vec<String> = set.into_iter().collect();
241 let mut pairs = Vec::new();
242 for i in 0..sigs.len() {
243 for j in (i + 1)..sigs.len() {
244 let (a, b) = (sigs[i].as_str(), sigs[j].as_str());
245 if equivalent(init, &swap_signals(init, a, b))
246 && equivalent(trans0, &swap_signals(trans0, a, b))
247 && equivalent(property0, &swap_signals(property0, a, b))
248 {
249 pairs.push((a.to_string(), b.to_string()));
250 }
251 }
252 }
253 pairs
254}
255
256pub fn find_counterexample_symmetric(
264 init: &ProofExpr,
265 trans: &dyn Fn(u32) -> ProofExpr,
266 property: &dyn Fn(u32) -> ProofExpr,
267 max_k: u32,
268) -> BmcOutcome {
269 let pairs = temporal_symmetry_pairs(init, &trans(0), &property(0));
270 let breaks: Vec<ProofExpr> = pairs
271 .iter()
272 .map(|(a, b)| {
273 ProofExpr::Or(
274 Box::new(not(ProofExpr::Atom(format!("{a}@0")))),
275 Box::new(ProofExpr::Atom(format!("{b}@0"))),
276 )
277 })
278 .collect();
279 for k in 0..=max_k {
280 let mut parts = unrolled_path(init, trans, k);
281 parts.push(not(property(k)));
282 parts.extend(breaks.iter().cloned());
283 match prove_unsat(&conj(parts)) {
284 UnsatOutcome::Sat(trace) => return BmcOutcome::CounterexampleAt { k, trace },
285 UnsatOutcome::Refuted => continue,
286 UnsatOutcome::Unsupported => return BmcOutcome::Unsupported,
287 }
288 }
289 BmcOutcome::NoneWithin(max_k)
290}
291
292#[derive(Clone, Debug, PartialEq, Eq)]
294pub enum InductionOutcome {
295 Proven,
297 CounterexampleAt { k: u32, trace: Vec<(String, bool)> },
299 NotInductive,
301 Unsupported,
303}
304
305pub fn prove_invariant(
314 init: &ProofExpr,
315 trans: &dyn Fn(u32) -> ProofExpr,
316 property: &dyn Fn(u32) -> ProofExpr,
317 k: u32,
318) -> InductionOutcome {
319 for j in 0..k {
321 let mut parts = unrolled_path(init, trans, j);
322 parts.push(not(property(j)));
323 match prove_unsat(&conj(parts)) {
324 UnsatOutcome::Refuted => {}
325 UnsatOutcome::Sat(trace) => {
326 return InductionOutcome::CounterexampleAt { k: j, trace }
327 }
328 UnsatOutcome::Unsupported => return InductionOutcome::Unsupported,
329 }
330 }
331
332 let mut parts = Vec::new();
334 for i in 0..k {
335 parts.push(property(i));
336 parts.push(trans(i));
337 }
338 parts.push(not(property(k)));
339 match prove_unsat(&conj(parts)) {
340 UnsatOutcome::Refuted => InductionOutcome::Proven,
341 UnsatOutcome::Sat(_) => InductionOutcome::NotInductive,
342 UnsatOutcome::Unsupported => InductionOutcome::Unsupported,
343 }
344}
345
346#[derive(Clone, Debug, PartialEq, Eq)]
348pub enum VacuityOutcome {
349 Vacuous,
351 Reachable(Vec<(String, bool)>),
353 Unsupported,
355}
356
357pub fn check_vacuity(antecedent: &ProofExpr) -> VacuityOutcome {
360 match prove_unsat(antecedent) {
361 UnsatOutcome::Refuted => VacuityOutcome::Vacuous,
362 UnsatOutcome::Sat(witness) => VacuityOutcome::Reachable(witness),
363 UnsatOutcome::Unsupported => VacuityOutcome::Unsupported,
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 fn atom(s: &str) -> ProofExpr {
372 ProofExpr::Atom(s.to_string())
373 }
374 fn iff(a: ProofExpr, b: ProofExpr) -> ProofExpr {
375 ProofExpr::Iff(Box::new(a), Box::new(b))
376 }
377
378 fn latched_init() -> ProofExpr {
380 atom("x@0")
381 }
382 fn latched_trans(t: u32) -> ProofExpr {
383 iff(atom(&format!("x@{}", t + 1)), atom(&format!("x@{}", t)))
384 }
385 fn latched_prop(t: u32) -> ProofExpr {
386 atom(&format!("x@{}", t))
387 }
388
389 fn toggle_init() -> ProofExpr {
391 not(atom("q@0"))
392 }
393 fn toggle_trans(t: u32) -> ProofExpr {
394 iff(atom(&format!("q@{}", t + 1)), not(atom(&format!("q@{}", t))))
395 }
396 fn toggle_always_false(t: u32) -> ProofExpr {
397 not(atom(&format!("q@{}", t)))
398 }
399
400 #[test]
401 fn bmc_finds_toggle_violation_at_step_one() {
402 let out = find_counterexample(&toggle_init(), &toggle_trans, &toggle_always_false, 5);
404 match out {
405 BmcOutcome::CounterexampleAt { k, trace } => {
406 assert_eq!(k, 1, "shallowest violation is at step 1");
407 assert!(
408 trace.iter().any(|(n, v)| n == "q@1" && *v),
409 "trace must show q@1 high: {trace:?}"
410 );
411 }
412 other => panic!("expected a counterexample, got {other:?}"),
413 }
414 }
415
416 #[test]
417 fn bmc_no_counterexample_for_latched_invariant() {
418 let out = find_counterexample(&latched_init(), &latched_trans, &latched_prop, 6);
419 assert_eq!(out, BmcOutcome::NoneWithin(6));
420 }
421
422 #[test]
423 fn k_induction_proves_latched_invariant() {
424 let out = prove_invariant(&latched_init(), &latched_trans, &latched_prop, 1);
426 assert_eq!(out, InductionOutcome::Proven);
427 }
428
429 #[test]
430 fn k_induction_finds_toggle_counterexample_in_base() {
431 let out = prove_invariant(&toggle_init(), &toggle_trans, &toggle_always_false, 3);
433 match out {
434 InductionOutcome::CounterexampleAt { k, .. } => assert_eq!(k, 1),
435 other => panic!("expected a base-case counterexample, got {other:?}"),
436 }
437 }
438
439 #[test]
440 fn vacuity_detects_dead_trigger() {
441 let dead = ProofExpr::And(Box::new(atom("req@0")), Box::new(not(atom("req@0"))));
443 assert_eq!(check_vacuity(&dead), VacuityOutcome::Vacuous);
444 }
445
446 #[test]
447 fn vacuity_accepts_a_live_trigger() {
448 match check_vacuity(&atom("req@0")) {
449 VacuityOutcome::Reachable(w) => {
450 assert!(w.iter().any(|(n, v)| n == "req@0" && *v));
451 }
452 other => panic!("a live trigger must be Reachable, got {other:?}"),
453 }
454 }
455
456 #[test]
459 fn incremental_bmc_finds_toggle_violation_at_step_one() {
460 match find_counterexample_incremental(&toggle_init(), &toggle_trans, &toggle_always_false, 5) {
461 BmcOutcome::CounterexampleAt { k, trace } => {
462 assert_eq!(k, 1);
463 assert!(trace.iter().any(|(n, v)| n == "q@1" && *v), "trace: {trace:?}");
464 }
465 other => panic!("expected a counterexample, got {other:?}"),
466 }
467 }
468
469 #[test]
470 fn temporal_symmetry_detected_and_broken_in_bmc() {
471 let sym_init = ProofExpr::Or(Box::new(atom("s0@0")), Box::new(atom("s1@0")));
474 let sym_trans = |t: u32| {
475 conj(vec![
476 iff(atom(&format!("s0@{}", t + 1)), atom(&format!("s0@{}", t))),
477 iff(atom(&format!("s1@{}", t + 1)), atom(&format!("s1@{}", t))),
478 ])
479 };
480 let not_both = |t: u32| {
481 not(ProofExpr::And(
482 Box::new(atom(&format!("s0@{}", t))),
483 Box::new(atom(&format!("s1@{}", t))),
484 ))
485 };
486
487 let pairs = temporal_symmetry_pairs(&sym_init, &sym_trans(0), ¬_both(0));
489 assert_eq!(pairs, vec![("s0".to_string(), "s1".to_string())], "s0 ↔ s1 is a temporal symmetry");
490
491 let plain = find_counterexample(&sym_init, &sym_trans, ¬_both, 4);
493 let broken = find_counterexample_symmetric(&sym_init, &sym_trans, ¬_both, 4);
494 assert!(matches!(plain, BmcOutcome::CounterexampleAt { k: 0, .. }), "plain: {plain:?}");
495 assert!(matches!(broken, BmcOutcome::CounterexampleAt { k: 0, .. }), "broken: {broken:?}");
496
497 let at_least_one =
500 |t: u32| ProofExpr::Or(Box::new(atom(&format!("s0@{}", t))), Box::new(atom(&format!("s1@{}", t))));
501 assert_eq!(
502 find_counterexample_symmetric(&sym_init, &sym_trans, &at_least_one, 5),
503 find_counterexample(&sym_init, &sym_trans, &at_least_one, 5),
504 "symmetric BMC matches plain BMC on a held invariant"
505 );
506
507 assert!(
510 temporal_symmetry_pairs(&toggle_init(), &toggle_trans(0), &toggle_always_false(0)).is_empty(),
511 "a one-signal system has no signal-swap symmetry"
512 );
513 assert_eq!(
514 find_counterexample_symmetric(&toggle_init(), &toggle_trans, &toggle_always_false, 5),
515 find_counterexample(&toggle_init(), &toggle_trans, &toggle_always_false, 5),
516 "with no symmetry, symmetric BMC = plain BMC"
517 );
518 }
519
520 #[test]
521 fn incremental_bmc_no_counterexample_for_latched_invariant() {
522 assert_eq!(
523 find_counterexample_incremental(&latched_init(), &latched_trans, &latched_prop, 6),
524 BmcOutcome::NoneWithin(6)
525 );
526 }
527
528 fn sig(i: usize, t: u32) -> ProofExpr {
529 ProofExpr::Atom(format!("s{i}@{t}"))
530 }
531 fn lit_b(b: bool, e: ProofExpr) -> ProofExpr {
532 if b {
533 e
534 } else {
535 ProofExpr::Not(Box::new(e))
536 }
537 }
538
539 #[test]
540 fn incremental_matches_nonincremental_and_simulation() {
541 let mut state = 0xb5ad_4ece_da1c_e2a9u64;
546 let mut next = || {
547 state ^= state << 13;
548 state ^= state >> 7;
549 state ^= state << 17;
550 state
551 };
552 let n = 3usize;
553 let max_k = 5u32;
554 for _trial in 0..120 {
555 let init_bits: Vec<bool> = (0..n).map(|_| next() & 1 == 0).collect();
556 let src: Vec<usize> = (0..n).map(|_| (next() % n as u64) as usize).collect();
557 let inv: Vec<bool> = (0..n).map(|_| next() & 1 == 0).collect();
558 let p = (next() % n as u64) as usize;
559 let want = next() & 1 == 0;
560
561 let init_expr = conj((0..n).map(|i| lit_b(init_bits[i], sig(i, 0))).collect());
562 let (src_t, inv_t) = (src.clone(), inv.clone());
563 let trans = move |t: u32| {
564 conj((0..n)
565 .map(|i| {
566 let rhs = lit_b(inv_t[i], sig(src_t[i], t));
568 ProofExpr::Iff(Box::new(sig(i, t + 1)), Box::new(rhs))
569 })
570 .collect())
571 };
572 let prop = move |t: u32| lit_b(want, sig(p, t));
573
574 let mut cur = init_bits.clone();
576 let mut expected: Option<u32> = None;
577 for k in 0..=max_k {
578 if (cur[p] == want) == false {
579 expected = Some(k);
580 break;
581 }
582 let nxt: Vec<bool> = (0..n)
583 .map(|i| if inv[i] { cur[src[i]] } else { !cur[src[i]] })
584 .collect();
585 cur = nxt;
586 }
587
588 let k_of = |o: &BmcOutcome| match o {
589 BmcOutcome::CounterexampleAt { k, .. } => Some(*k),
590 BmcOutcome::NoneWithin(_) => None,
591 BmcOutcome::Unsupported => panic!("unexpected Unsupported"),
592 };
593 let ni = find_counterexample(&init_expr, &trans, &prop, max_k);
594 let inc = find_counterexample_incremental(&init_expr, &trans, &prop, max_k);
595 assert_eq!(k_of(&ni), expected, "non-incremental BMC disagrees with simulation");
596 assert_eq!(k_of(&inc), expected, "incremental BMC disagrees with simulation");
597 }
598 }
599}