aplite 0.1.0

Bounded in-process Boolean, bit-vector and array constraint solver
Documentation
use crate::{
    Env, Limits, Scalar, Sort, Verdict, and, boolv, bv, eq, node, not, solve, solve_seeded, var,
};
use std::collections::BTreeSet;

#[test]
fn seeds_reproduce_and_vary_unconstrained_inputs() {
    let x = var("x".into(), Sort::Bv(64));
    let flag = var("flag".into(), Sort::Bool);
    let context = Env::from([("x".into(), x), ("flag".into(), flag)]);
    let mut words = BTreeSet::new();
    let mut flags = BTreeSet::new();
    for seed in 0..64 {
        let a = solve_seeded(&boolv(true), &context, Limits::default(), seed);
        let b = solve_seeded(&boolv(true), &context, Limits::default(), seed);
        assert_eq!(a.verdict, Verdict::Sat);
        assert!(a.original_formula_validated);
        assert_eq!(a.assignments, b.assignments);
        assert_eq!(a.context_values, b.context_values);
        let Scalar::Bv { value, .. } = a.assignments["x"] else {
            panic!()
        };
        let Scalar::Bool(flag) = a.assignments["flag"] else {
            panic!()
        };
        words.insert(value);
        flags.insert(flag);
    }
    assert_eq!(words.len(), 64);
    assert_eq!(flags.len(), 2);
}

#[test]
fn constrained_models_preserve_relations_and_context_expressions() {
    let x = var("x".into(), Sort::Bv(4));
    let y = var("y".into(), Sort::Bv(4));
    let sum = node(Sort::Bv(4), "bvadd", vec![x.clone(), y.clone()]);
    let formula = and(
        eq(sum.clone(), bv(4, 9)),
        node(Sort::Bool, "bvult", vec![x.clone(), bv(4, 4)]),
    );
    let context = Env::from([("sum".into(), sum)]);
    let mut seen = BTreeSet::new();
    for seed in 0..128 {
        let result = solve_seeded(&formula, &context, Limits::default(), seed);
        assert_eq!(result.verdict, Verdict::Sat);
        assert!(result.original_formula_validated);
        let Scalar::Bv { value: x, .. } = result.assignments["x"] else {
            panic!()
        };
        let Scalar::Bv { value: y, .. } = result.assignments["y"] else {
            panic!()
        };
        assert!(x < 4);
        assert_eq!((x + y) & 15, 9);
        assert_eq!(
            result.context_values["sum"],
            Scalar::Bv { width: 4, value: 9 }
        );
        seen.insert(x);
    }
    assert_eq!(seen.len(), 4);
}

#[test]
fn seeds_cannot_change_unsatisfiability_or_unique_models() {
    let x = var("x".into(), Sort::Bv(8));
    let fixed = eq(x.clone(), bv(8, 37));
    let impossible = and(fixed.clone(), not(fixed.clone()));
    for seed in [0, 1, 42, u64::MAX] {
        assert_eq!(
            solve_seeded(&impossible, &Env::new(), Limits::default(), seed).verdict,
            Verdict::Unsat
        );
        let result = solve_seeded(&fixed, &Env::new(), Limits::default(), seed);
        assert_eq!(result.verdict, Verdict::Sat);
        assert_eq!(
            result.assignments["x"],
            Scalar::Bv {
                width: 8,
                value: 37
            }
        );
    }
    assert_eq!(
        solve(&impossible, &Env::new(), Limits::default()).verdict,
        Verdict::Unsat
    );
}

#[test]
fn unsupported_input_and_exhausted_budgets_are_unknown_without_models() {
    let x = var("x".into(), Sort::Bv(8));
    let context = Env::from([("x".into(), x)]);
    for limits in [
        Limits {
            max_work: 0,
            ..Limits::default()
        },
        Limits {
            max_variables: 1,
            ..Limits::default()
        },
        Limits {
            max_clauses: 0,
            ..Limits::default()
        },
    ] {
        let result = solve_seeded(&boolv(true), &context, limits, 0);
        assert_eq!(result.verdict, Verdict::Unknown);
        assert!(!result.original_formula_validated);
        assert!(result.assignments.is_empty());
    }
    let memory = Env::from([("m".into(), var("m".into(), Sort::Mem(4, 8)))]);
    let result = solve_seeded(&boolv(true), &memory, Limits::default(), 0);
    assert_eq!(result.verdict, Verdict::Unknown);
    assert!(result.reason.unwrap().contains("scalar"));
    let invalid = node(Sort::Bool, "unsupported", vec![]);
    assert_eq!(
        solve_seeded(&invalid, &Env::new(), Limits::default(), 0).verdict,
        Verdict::Unknown
    );
}

#[test]
fn seeded_search_matches_every_two_boolean_truth_table() {
    let x = var("x".into(), Sort::Bool);
    let y = var("y".into(), Sort::Bool);
    let context = Env::from([("x".into(), x.clone()), ("y".into(), y.clone())]);
    for mask in 0..16u8 {
        let mut formula = boolv(false);
        for row in 0..4u8 {
            if mask & (1 << row) != 0 {
                let case = and(
                    eq(x.clone(), boolv(row & 1 != 0)),
                    eq(y.clone(), boolv(row & 2 != 0)),
                );
                formula = node(Sort::Bool, "or", vec![formula, case]);
            }
        }
        for seed in 0..16 {
            let result = solve_seeded(&formula, &context, Limits::default(), seed);
            if mask == 0 {
                assert_eq!(result.verdict, Verdict::Unsat);
            } else {
                assert_eq!(result.verdict, Verdict::Sat);
                assert!(result.original_formula_validated);
                let Scalar::Bool(x) = result.assignments["x"] else {
                    panic!()
                };
                let Scalar::Bool(y) = result.assignments["y"] else {
                    panic!()
                };
                let row = u8::from(x) | (u8::from(y) << 1);
                assert_ne!(mask & (1 << row), 0);
            }
        }
    }
}