use std::collections::BTreeSet;
use super::witness::{
check, check_with, CheckError, Checked, ClauseSet, ClauseView, Core, IdSet, Model, Step,
WitnessResult,
};
pub const VARS: [&str; 3] = ["a", "b", "c"];
pub const IMPLY_PAIRS: [(usize, usize); 6] = [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)];
pub const CONFLICT_PAIRS: [(usize, usize); 3] = [(0, 1), (0, 2), (1, 2)];
pub const CORE_BOUND: usize = 3;
pub const STEP_CODES: u8 = 9;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SmallGraph {
pub units: [bool; 3],
pub implies: [bool; 6],
pub conflicts: [bool; 3],
}
impl SmallGraph {
#[must_use]
pub fn clause_set(&self) -> ClauseSet {
let mut cs = ClauseSet::default();
for (i, v) in VARS.iter().enumerate() {
if self.units[i] {
cs.units.insert((*v).to_string());
}
}
for (k, (a, b)) in IMPLY_PAIRS.iter().enumerate() {
if self.implies[k] {
cs.implies
.insert((VARS[*a].to_string(), VARS[*b].to_string()));
}
}
for (k, (a, b)) in CONFLICT_PAIRS.iter().enumerate() {
if self.conflicts[k] {
cs.conflicts
.insert((VARS[*a].to_string(), VARS[*b].to_string()));
}
}
cs
}
#[must_use]
pub fn satisfied_by(&self, assign: u8) -> bool {
let t = |i: usize| assign & (1 << i) != 0;
let units = (0..3).all(|i| !self.units[i] || t(i));
let implies = (0..6).all(|k| {
let (a, b) = IMPLY_PAIRS[k];
!self.implies[k] || !t(a) || t(b)
});
let conflicts = (0..3).all(|k| {
let (a, b) = CONFLICT_PAIRS[k];
!self.conflicts[k] || !t(a) || !t(b)
});
units && implies && conflicts
}
#[must_use]
pub fn satisfiable(&self) -> bool {
(0u8..8).any(|m| self.satisfied_by(m))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Bounded<const N: usize> {
ids: [u8; N],
len: usize,
}
impl<const N: usize> Default for Bounded<N> {
fn default() -> Self {
Self {
ids: [0; N],
len: 0,
}
}
}
impl<const N: usize> Bounded<N> {
#[must_use]
pub fn as_slice(&self) -> &[u8] {
&self.ids[..self.len]
}
fn push(&mut self, x: u8) {
assert!(self.len < N, "Bounded<{N}> overflow");
self.ids[self.len] = x;
self.len += 1;
}
}
impl<const N: usize> IdSet<u8> for Bounded<N> {
fn has(&self, x: &u8) -> bool {
self.as_slice().iter().any(|y| y == x)
}
fn put(&mut self, x: u8) {
if !self.has(&x) {
self.push(x);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pairs<const N: usize> {
pairs: [(u8, u8); N],
len: usize,
}
impl<const N: usize> Default for Pairs<N> {
fn default() -> Self {
Self {
pairs: [(0, 0); N],
len: 0,
}
}
}
impl<const N: usize> Pairs<N> {
fn as_slice(&self) -> &[(u8, u8)] {
&self.pairs[..self.len]
}
fn push(&mut self, p: (u8, u8)) {
assert!(self.len < N, "Pairs<{N}> overflow");
self.pairs[self.len] = p;
self.len += 1;
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ArrayClauses {
pub units: Bounded<3>,
pub implies: Pairs<6>,
pub conflicts: Pairs<3>,
}
impl ClauseView<u8> for ArrayClauses {
fn has_unit(&self, v: &u8) -> bool {
self.units.has(v)
}
fn has_implies(&self, a: &u8, b: &u8) -> bool {
self.implies.as_slice().contains(&(*a, *b))
}
fn has_conflict(&self, a: &u8, b: &u8) -> bool {
let k = (*a.min(b), *a.max(b));
self.conflicts.as_slice().contains(&k)
}
fn units<'s>(&'s self) -> impl Iterator<Item = &'s u8>
where
u8: 's,
{
self.units.as_slice().iter()
}
fn implications<'s>(&'s self) -> impl Iterator<Item = (&'s u8, &'s u8)>
where
u8: 's,
{
self.implies.as_slice().iter().map(|(a, b)| (a, b))
}
fn conflict_pairs<'s>(&'s self) -> impl Iterator<Item = (&'s u8, &'s u8)>
where
u8: 's,
{
self.conflicts.as_slice().iter().map(|(a, b)| (a, b))
}
}
impl SmallGraph {
#[must_use]
pub fn array_clauses(&self) -> ArrayClauses {
let mut ac = ArrayClauses::default();
for i in 0..3 {
if self.units[i] {
ac.units.push(id_u8(i));
}
}
for (k, (a, b)) in IMPLY_PAIRS.iter().enumerate() {
if self.implies[k] {
ac.implies.push((id_u8(*a), id_u8(*b)));
}
}
for (k, (a, b)) in CONFLICT_PAIRS.iter().enumerate() {
if self.conflicts[k] {
ac.conflicts.push((id_u8(*a), id_u8(*b)));
}
}
ac
}
}
#[must_use]
pub fn id_string(i: usize) -> String {
VARS[i].to_string()
}
#[must_use]
pub fn id_u8(i: usize) -> u8 {
u8::try_from(i).unwrap_or(u8::MAX)
}
#[must_use]
pub fn step_as<I>(code: u8, id: fn(usize) -> I) -> Step<I> {
match usize::from(code).checked_sub(3) {
None => Step::Unit(id(usize::from(code))),
Some(j) => {
let (a, b) = IMPLY_PAIRS[j % 6];
Step::Implies(id(a), id(b))
}
}
}
#[must_use]
pub fn step(code: u8) -> Step {
step_as(code, id_string)
}
#[must_use]
pub fn core_as<I>(codes: &[u8], conflict: usize, id: fn(usize) -> I) -> WitnessResult<I> {
let (a, b) = CONFLICT_PAIRS[conflict % 3];
let mut steps = Vec::with_capacity(codes.len());
for c in codes {
steps.push(step_as(*c, id));
}
WitnessResult::UnsatCore(Core {
steps,
conflict: (id(a), id(b)),
})
}
#[must_use]
pub fn core(codes: &[u8], conflict: usize) -> WitnessResult {
core_as(codes, conflict, id_string)
}
#[must_use]
pub fn model_as<I>(false_bits: u8, id: fn(usize) -> I) -> WitnessResult<I> {
let mut false_vars = Vec::with_capacity(3);
for i in 0..3 {
if false_bits & (1 << i) != 0 {
false_vars.push(id(i));
}
}
WitnessResult::Model(Model { false_vars })
}
#[must_use]
pub fn model(false_bits: u8) -> WitnessResult {
model_as(false_bits, id_string)
}
pub fn check_string(g: &SmallGraph, r: &WitnessResult) -> Result<Checked, CheckError> {
check(&g.clause_set(), r)
}
pub fn check_u8(
g: &SmallGraph,
r: &WitnessResult<u8>,
) -> Result<Checked<Bounded<3>>, CheckError<u8>> {
check_with(&g.array_clauses(), r)
}
fn semantic<S, E>(
g: &SmallGraph,
verdict: &Result<Checked<S>, E>,
core_ok: fn(&S) -> bool,
) -> bool {
match verdict {
Ok(Checked::Unsat { core }) => !g.satisfiable() && core_ok(core),
Ok(Checked::Sat) => g.satisfiable(),
Err(_) => true,
}
}
#[must_use]
pub fn accepted_verdict_is_semantic(g: &SmallGraph, verdict: &Result<Checked, CheckError>) -> bool {
semantic(g, verdict, core_names_known)
}
#[must_use]
pub fn accepted_verdict_is_semantic_u8(
g: &SmallGraph,
verdict: &Result<Checked<Bounded<3>>, CheckError<u8>>,
) -> bool {
semantic(g, verdict, core_names_known_u8)
}
fn core_names_known(core: &BTreeSet<String>) -> bool {
!core.is_empty() && core.iter().all(|v| VARS.contains(&v.as_str()))
}
fn core_names_known_u8(core: &Bounded<3>) -> bool {
!core.as_slice().is_empty() && core.as_slice().iter().all(|v| usize::from(*v) < VARS.len())
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn the_oracle_decides_the_plant() {
let mut g = SmallGraph {
units: [true, false, false],
..SmallGraph::default()
};
g.implies[0] = true; g.implies[3] = true; g.conflicts[1] = true; assert!(!g.satisfiable());
assert!(accepted_verdict_is_semantic(
&g,
&check_string(&g, &core(&[0, 3, 6], 1))
));
assert!(matches!(
check(&g.clause_set(), &core(&[0, 3, 6], 1)),
Ok(Checked::Unsat { .. })
));
g.conflicts[1] = false;
assert!(g.satisfiable());
assert!(matches!(
check(&g.clause_set(), &model(0)),
Ok(Checked::Sat)
));
}
#[test]
fn semantic_needs_both_halves_of_an_accepted_core() {
let unsat = SmallGraph {
units: [true, false, false],
conflicts: [false; 3],
implies: [false; 6],
};
let mut unsat = unsat;
unsat.implies[0] = true;
unsat.implies[3] = true;
unsat.conflicts[1] = true;
assert!(!unsat.satisfiable());
let sat = SmallGraph::default();
assert!(sat.satisfiable());
let core = |c: u8| -> Result<Checked<u8>, ()> { Ok(Checked::Unsat { core: c }) };
assert!(semantic(&unsat, &core(0), |_| true));
assert!(!semantic(&unsat, &core(0), |_| false));
assert!(!semantic(&sat, &core(0), |_| true));
assert!(!semantic(&sat, &core(0), |_| false));
let ok_sat: Result<Checked<u8>, ()> = Ok(Checked::Sat);
assert!(semantic(&sat, &ok_sat, |_| false));
assert!(!semantic(&unsat, &ok_sat, |_| true));
let refused: Result<Checked<u8>, ()> = Err(());
assert!(semantic(&unsat, &refused, |_| false));
}
#[test]
fn accepted_verdict_is_semantic_refuses_an_unsound_verdict() {
let mut unsat = SmallGraph {
units: [true, false, false],
..SmallGraph::default()
};
unsat.implies[0] = true;
unsat.implies[3] = true;
unsat.conflicts[1] = true;
assert!(!unsat.satisfiable());
let sat = SmallGraph::default();
let set = |v: &[&str]| {
v.iter()
.map(|x| (*x).to_string())
.collect::<BTreeSet<String>>()
};
let unsat_core = |c| -> Result<Checked, CheckError> { Ok(Checked::Unsat { core: c }) };
assert!(accepted_verdict_is_semantic(
&unsat,
&unsat_core(set(&["a"]))
));
assert!(!accepted_verdict_is_semantic(
&sat,
&unsat_core(set(&["a"]))
));
assert!(!accepted_verdict_is_semantic(&unsat, &Ok(Checked::Sat)));
assert!(!accepted_verdict_is_semantic(&unsat, &unsat_core(set(&[]))));
assert!(!accepted_verdict_is_semantic(
&unsat,
&unsat_core(set(&["a", "z"]))
));
let ids = |v: &[u8]| {
let mut b = Bounded::<3>::default();
for x in v {
b.put(*x);
}
b
};
let unsat_u8 =
|c| -> Result<Checked<Bounded<3>>, CheckError<u8>> { Ok(Checked::Unsat { core: c }) };
assert!(accepted_verdict_is_semantic_u8(
&unsat,
&unsat_u8(ids(&[0, 2]))
));
assert!(!accepted_verdict_is_semantic_u8(&sat, &unsat_u8(ids(&[0]))));
assert!(!accepted_verdict_is_semantic_u8(&unsat, &Ok(Checked::Sat)));
assert!(!accepted_verdict_is_semantic_u8(
&unsat,
&unsat_u8(ids(&[]))
));
assert!(!accepted_verdict_is_semantic_u8(
&unsat,
&unsat_u8(ids(&[0, 3]))
));
}
#[test]
fn step_codes_decode_to_their_implication_pair() {
for (j, (a, b)) in IMPLY_PAIRS.iter().enumerate() {
let code = u8::try_from(j + 3).unwrap_or(u8::MAX);
assert_eq!(step_as(code, id_u8), Step::Implies(id_u8(*a), id_u8(*b)));
}
assert_eq!(step_as(2, id_u8), Step::Unit(2));
assert_eq!(step_as(9, id_u8), step_as(3, id_u8));
}
#[test]
fn model_and_step_decode_to_exact_ids() {
let WitnessResult::Model(m) = model(0b110) else {
panic!("model");
};
assert_eq!(m.false_vars, ["b", "c"]);
let WitnessResult::Model(m) = model(0b001) else {
panic!("model");
};
assert_eq!(m.false_vars, ["a"]);
let WitnessResult::Model(m) = model_as(0b101, id_u8) else {
panic!("model");
};
assert_eq!(m.false_vars, [0u8, 2]);
assert_eq!(step(0), Step::Unit("a".to_string()));
assert_eq!(step(2), Step::Unit("c".to_string()));
assert_eq!(step(3), Step::Implies("a".to_string(), "b".to_string()));
assert_eq!(step(8), Step::Implies("c".to_string(), "b".to_string()));
}
fn relabel(v: Result<Checked<Bounded<3>>, CheckError<u8>>) -> Result<Checked, CheckError> {
let s = |i: u8| id_string(usize::from(i));
match v {
Ok(Checked::Sat) => Ok(Checked::Sat),
Ok(Checked::Unsat { core }) => Ok(Checked::Unsat {
core: core.as_slice().iter().map(|v| s(*v)).collect(),
}),
Err(e) => Err(match e {
CheckError::EmptyCore => CheckError::EmptyCore,
CheckError::NotAUnit(a) => CheckError::NotAUnit(s(a)),
CheckError::NoSuchImplication(a, b) => CheckError::NoSuchImplication(s(a), s(b)),
CheckError::PremiseNotDerived { premise, step } => CheckError::PremiseNotDerived {
premise: s(premise),
step,
},
CheckError::NoSuchConflict(a, b) => CheckError::NoSuchConflict(s(a), s(b)),
CheckError::ConflictSideNotDerived(a) => CheckError::ConflictSideNotDerived(s(a)),
CheckError::ModelFalsifiesUnit(a) => CheckError::ModelFalsifiesUnit(s(a)),
CheckError::ModelFalsifiesImplication(a, b) => {
CheckError::ModelFalsifiesImplication(s(a), s(b))
}
CheckError::ModelFalsifiesConflict(a, b) => {
CheckError::ModelFalsifiesConflict(s(a), s(b))
}
}),
}
}
#[test]
fn both_instantiations_agree_on_the_plant() {
let mut g = SmallGraph {
units: [true, false, false],
..SmallGraph::default()
};
g.implies[0] = true;
g.implies[3] = true;
g.conflicts[1] = true;
for (codes, conflict) in [(&[0u8, 3, 6][..], 1usize), (&[0, 6][..], 1), (&[1][..], 0)] {
let s = check_string(&g, &core(codes, conflict));
assert_eq!(relabel(check_u8(&g, &core_as(codes, conflict, id_u8))), s);
}
assert!(matches!(
check_string(&g, &core(&[0, 3, 6], 1)),
Ok(Checked::Unsat { .. })
));
assert!(matches!(
check_string(&g, &core(&[0, 6], 1)),
Err(CheckError::PremiseNotDerived { .. })
));
assert!(matches!(
check_string(&g, &core(&[1], 0)),
Err(CheckError::NotAUnit(_))
));
}
fn graph() -> impl Strategy<Value = SmallGraph> {
(any::<[bool; 3]>(), any::<[bool; 6]>(), any::<[bool; 3]>()).prop_map(
|(units, implies, conflicts)| SmallGraph {
units,
implies,
conflicts,
},
)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(2048))]
#[test]
fn kani_ont_9_1_twin(
g in graph(),
codes in proptest::collection::vec(0u8..STEP_CODES, 1..=CORE_BOUND),
conflict in 0usize..3,
false_bits in 0u8..8,
) {
prop_assert!(accepted_verdict_is_semantic(&g, &check_string(&g, &core(&codes, conflict))));
prop_assert!(accepted_verdict_is_semantic(&g, &check_string(&g, &model(false_bits))));
prop_assert!(accepted_verdict_is_semantic_u8(&g, &check_u8(&g, &core_as(&codes, conflict, id_u8))));
prop_assert!(accepted_verdict_is_semantic_u8(&g, &check_u8(&g, &model_as(false_bits, id_u8))));
prop_assert_eq!(relabel(check_u8(&g, &core_as(&codes, conflict, id_u8))), check_string(&g, &core(&codes, conflict)));
prop_assert_eq!(relabel(check_u8(&g, &model_as(false_bits, id_u8))), check_string(&g, &model(false_bits)));
}
}
#[test]
fn every_step_code_decodes_as_documented() {
let pair = |k: usize| {
let (a, b) = IMPLY_PAIRS[k];
Step::Implies(id_string(a), id_string(b))
};
for code in 0..=u8::MAX {
let k = usize::from(code);
let want = if k < 3 {
Step::Unit(id_string(k))
} else {
pair((k - 3) % 6)
};
assert_eq!(step(code), want, "code {code}");
}
assert_eq!(step(3), pair(0));
assert_eq!(step(8), pair(5));
assert_eq!(step(9), pair(0));
}
}