use crate::ast::stmt::{BinaryOpKind, Expr, Literal};
use crate::intern::Symbol;
use logicaffeine_kernel::lia::{fourier_motzkin_unsat, LinearExpr, Rational};
pub(crate) use logicaffeine_kernel::lia::{Constraint, LinearExpr as LinExpr};
fn vidx(s: Symbol) -> i64 {
s.index() as i64
}
pub(crate) fn var(s: Symbol) -> LinearExpr {
LinearExpr::var(vidx(s))
}
pub(crate) fn konst(n: i64) -> LinearExpr {
LinearExpr::constant(Rational::from_i64(n))
}
pub(crate) fn le(a: &LinearExpr, b: &LinearExpr) -> Constraint {
Constraint { expr: a.sub(b), strict: false }
}
pub(crate) fn lt(a: &LinearExpr, b: &LinearExpr) -> Constraint {
Constraint { expr: a.sub(b), strict: true }
}
pub(crate) fn ge(a: &LinearExpr, b: &LinearExpr) -> Constraint {
le(b, a)
}
pub(crate) fn gt(a: &LinearExpr, b: &LinearExpr) -> Constraint {
lt(b, a)
}
pub(crate) fn nonneg(e: &LinearExpr) -> Constraint {
Constraint { expr: e.neg(), strict: false }
}
pub(crate) fn consistent(facts: &[Constraint]) -> bool {
!fourier_motzkin_unsat(facts)
}
pub(crate) fn prove(facts: &[Constraint], goal: &LinearExpr) -> bool {
let neg = goal.add(&LinearExpr::constant(Rational::from_i64(1)));
let mut system: Vec<Constraint> = facts.to_vec();
system.push(Constraint { expr: neg, strict: false });
fourier_motzkin_unsat(&system)
}
pub(crate) fn lin_of(e: &Expr) -> Option<LinearExpr> {
const LIT_CAP: u64 = 1 << 50;
match e {
Expr::Identifier(s) => Some(var(*s)),
Expr::Literal(Literal::Number(n)) => {
(n.unsigned_abs() <= LIT_CAP).then(|| konst(*n))
}
Expr::BinaryOp { op, left, right } => match op {
BinaryOpKind::Add => Some(lin_of(left)?.add(&lin_of(right)?)),
BinaryOpKind::Subtract => Some(lin_of(left)?.sub(&lin_of(right)?)),
BinaryOpKind::Multiply => {
let (l, r) = (lin_of(left)?, lin_of(right)?);
if l.is_constant() {
Some(r.scale(&l.constant))
} else if r.is_constant() {
Some(l.scale(&r.constant))
} else {
None }
}
_ => None,
},
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::intern::Interner;
struct Vars {
i: Interner,
}
impl Vars {
fn new() -> Self {
Vars { i: Interner::new() }
}
fn s(&mut self, name: &str) -> Symbol {
self.i.intern(name)
}
}
fn plus(e: &LinearExpr, n: i64) -> LinearExpr {
e.add(&konst(n))
}
fn term(x: Symbol, c: i64) -> LinearExpr {
var(x).scale(&Rational::from_i64(c))
}
#[test]
fn prove_knapsack_lower_bound() {
let mut v = Vars::new();
let (w, wi) = (v.s("w"), v.s("wi"));
let guard = ge(&var(w), &var(wi)); let goal = var(w).sub(&var(wi)); assert!(prove(&[guard], &goal));
}
#[test]
fn prove_knapsack_upper_bound() {
let mut v = Vars::new();
let (w, wi, cap) = (v.s("w"), v.s("wi"), v.s("cap"));
let facts = vec![le(&var(w), &var(cap)), nonneg(&var(wi))];
let goal = var(cap).sub(&var(w)).add(&var(wi)); assert!(prove(&facts, &goal));
}
#[test]
fn knapsack_upper_unprovable_without_element_bound() {
let mut v = Vars::new();
let (w, wi, cap) = (v.s("w"), v.s("wi"), v.s("cap"));
let facts = vec![le(&var(w), &var(cap))];
let goal = var(cap).sub(&var(w)).add(&var(wi));
assert!(!prove(&facts, &goal));
}
#[test]
fn prove_string_search_window() {
let mut v = Vars::new();
let (i, j, t, p) = (v.s("i"), v.s("j"), v.s("t"), v.s("p"));
let facts = vec![
le(&var(i), &var(t).sub(&var(p))), le(&var(j), &plus(&var(p), -1)), nonneg(&var(i)),
nonneg(&var(j)),
];
let ij = var(i).add(&var(j));
let goal = plus(&var(t), -1).sub(&ij); assert!(prove(&facts, &goal));
}
#[test]
fn prove_graph_bfs_element_bound() {
let mut v = Vars::new();
let (u, n) = (v.s("u"), v.s("n"));
let facts = vec![nonneg(&var(u)), le(&var(u), &plus(&var(n), -1))]; assert!(prove(&facts, &var(u))); let upper = var(n).sub(&plus(&var(u), 1)); assert!(prove(&facts, &upper));
}
#[test]
fn prove_is_not_overeager() {
let mut v = Vars::new();
let (i, n) = (v.s("i"), v.s("n"));
let weak = le(&var(i), &var(n)); let strict_goal = plus(&var(n).sub(&var(i)), -1); assert!(!prove(&[weak], &strict_goal));
let strong = lt(&var(i), &var(n)); let nonstrict_goal = var(n).sub(&var(i)); assert!(prove(&[strong], &nonstrict_goal));
}
#[test]
fn prove_empty_facts_only_proves_trivial() {
let mut v = Vars::new();
let x = v.s("x");
assert!(!prove(&[], &var(x))); assert!(prove(&[], &konst(5))); assert!(!prove(&[], &konst(-1))); }
#[test]
fn prove_flattened_2d_constant_stride() {
let mut v = Vars::new();
let (i, j) = (v.s("i"), v.s("j"));
let facts = vec![
nonneg(&var(i)),
le(&var(i), &konst(9)),
nonneg(&var(j)),
le(&var(j), &konst(9)),
];
let idx0 = term(i, 10).add(&var(j)); assert!(prove(&facts, &idx0)); let upper = konst(99).sub(&idx0); assert!(prove(&facts, &upper));
}
fn num<'a>(arena: &'a crate::arena::Arena<Expr<'a>>, n: i64) -> &'a Expr<'a> {
arena.alloc(Expr::Literal(Literal::Number(n)))
}
fn ident<'a>(arena: &'a crate::arena::Arena<Expr<'a>>, s: Symbol) -> &'a Expr<'a> {
arena.alloc(Expr::Identifier(s))
}
fn binop<'a>(
arena: &'a crate::arena::Arena<Expr<'a>>,
op: BinaryOpKind,
l: &'a Expr<'a>,
r: &'a Expr<'a>,
) -> &'a Expr<'a> {
arena.alloc(Expr::BinaryOp { op, left: l, right: r })
}
#[test]
fn lin_of_extracts_affine_forms() {
let arena = crate::arena::Arena::new();
let mut v = Vars::new();
let (w, wi) = (v.s("w"), v.s("wi"));
let e = binop(
&arena,
BinaryOpKind::Add,
binop(&arena, BinaryOpKind::Subtract, ident(&arena, w), ident(&arena, wi)),
num(&arena, 1),
);
let lin = lin_of(e).unwrap();
assert_eq!(lin, plus(&var(w).sub(&var(wi)), 1));
}
#[test]
fn lin_of_scales_by_constant_factor() {
let arena = crate::arena::Arena::new();
let mut v = Vars::new();
let i = v.s("i");
let e1 = binop(&arena, BinaryOpKind::Multiply, num(&arena, 3), ident(&arena, i));
let e2 = binop(&arena, BinaryOpKind::Multiply, ident(&arena, i), num(&arena, 3));
assert_eq!(lin_of(e1).unwrap(), term(i, 3));
assert_eq!(lin_of(e2).unwrap(), term(i, 3));
}
#[test]
fn lin_of_rejects_nonlinear() {
let arena = crate::arena::Arena::new();
let mut v = Vars::new();
let (i, j) = (v.s("i"), v.s("j"));
let prod = binop(&arena, BinaryOpKind::Multiply, ident(&arena, i), ident(&arena, j));
assert!(lin_of(prod).is_none()); let m = binop(&arena, BinaryOpKind::Modulo, ident(&arena, i), num(&arena, 3));
assert!(lin_of(m).is_none()); }
}
#[cfg(all(test, feature = "verification"))]
mod z3_certifier {
use super::*;
use crate::intern::Interner;
use logicaffeine_verify::{VerificationSession, VerifyExpr, VerifyOp, VerifyType};
fn z3_name(idx: i64) -> String {
format!("v{}", idx)
}
fn as_int(r: &Rational) -> i64 {
r.to_i64()
.expect("certifier only encodes integer constraints")
}
fn encode(e: &LinearExpr) -> VerifyExpr {
let mut acc = VerifyExpr::int(as_int(&e.constant));
for (idx, coeff) in &e.coefficients {
let term = VerifyExpr::binary(
VerifyOp::Mul,
VerifyExpr::int(as_int(coeff)),
VerifyExpr::var(z3_name(*idx)),
);
acc = VerifyExpr::binary(VerifyOp::Add, acc, term);
}
acc
}
fn z3_confirms(facts: &[Constraint], goal: &LinearExpr) -> bool {
let mut session = VerificationSession::new();
let mut idxs: std::collections::HashSet<i64> = std::collections::HashSet::new();
for c in facts {
for k in c.expr.coefficients.keys() {
idxs.insert(*k);
}
}
for k in goal.coefficients.keys() {
idxs.insert(*k);
}
for idx in idxs {
session.declare(&z3_name(idx), VerifyType::Int);
}
for c in facts {
let lhs = encode(&c.expr);
let zero = VerifyExpr::int(0);
let assertion = if c.strict {
VerifyExpr::lt(lhs, zero)
} else {
VerifyExpr::lte(lhs, zero)
};
session.assume(&assertion);
}
session.verify(&VerifyExpr::gte(encode(goal), VerifyExpr::int(0))).is_ok()
}
fn s(i: &mut Interner, name: &str) -> Symbol {
i.intern(name)
}
#[test]
fn z3_confirms_the_hard_loss_relations() {
let mut i = Interner::new();
let (w, wi, cap) = (s(&mut i, "w"), s(&mut i, "wi"), s(&mut i, "cap"));
let f = vec![ge(&var(w), &var(wi))];
let g = var(w).sub(&var(wi));
assert!(prove(&f, &g) && z3_confirms(&f, &g));
let f = vec![le(&var(w), &var(cap)), nonneg(&var(wi))];
let g = var(cap).sub(&var(w)).add(&var(wi));
assert!(prove(&f, &g) && z3_confirms(&f, &g));
let (i2, j, t, p) = (s(&mut i, "i"), s(&mut i, "j"), s(&mut i, "t"), s(&mut i, "p"));
let f = vec![
le(&var(i2), &var(t).sub(&var(p))),
le(&var(j), &var(p).add(&konst(-1))),
nonneg(&var(i2)),
nonneg(&var(j)),
];
let g = var(t).add(&konst(-1)).sub(&var(i2).add(&var(j)));
assert!(prove(&f, &g) && z3_confirms(&f, &g));
let (u, n) = (s(&mut i, "u"), s(&mut i, "n"));
let f = vec![nonneg(&var(u)), le(&var(u), &var(n).add(&konst(-1)))];
let g = var(n).sub(&var(u).add(&konst(1)));
assert!(prove(&f, &g) && z3_confirms(&f, &g));
}
fn mix(n: u64) -> u64 {
let mut x = n.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
x ^= x >> 33;
x = x.wrapping_mul(0xff51afd7ed558ccd);
x ^= x >> 33;
x
}
fn pick(seed: u64, k: u64, lo: i64, hi: i64) -> i64 {
let span = (hi - lo + 1) as u64;
lo + (mix(seed.wrapping_add(k.wrapping_mul(0x9E37_79B9_7F4A_7C15))) % span) as i64
}
#[test]
fn fm_proofs_are_always_z3_valid() {
let mut i = Interner::new();
let (x, y, z) = (s(&mut i, "x"), s(&mut i, "y"), s(&mut i, "z"));
let vars = [x, y, z];
let mk = |seed: u64, base: u64| -> LinearExpr {
let mut e = konst(pick(seed, base + 3, -4, 4));
for (vi, vv) in vars.iter().enumerate() {
let coeff = pick(seed, base + vi as u64, -2, 2);
e = e.add(&term_of(*vv, coeff));
}
e
};
let mut fm_proved = 0u32;
let mut z3_proved = 0u32;
let mut both = 0u32;
let trials = 600u64;
for seed in 0..trials {
let facts: Vec<Constraint> = (0..3)
.map(|fi| Constraint { expr: mk(seed, 10 + fi * 4), strict: false })
.collect();
let goal = mk(seed, 100);
let fm = prove(&facts, &goal);
let z3 = z3_confirms(&facts, &goal);
if fm {
fm_proved += 1;
assert!(
z3,
"UNSOUND: kernel Fourier–Motzkin proved a goal Z3 refutes (seed {}). \
Coefficients are deterministic — reproduce with this seed.",
seed
);
}
if z3 {
z3_proved += 1;
}
if fm && z3 {
both += 1;
}
}
assert!(fm_proved > 0, "engine proved nothing across {} trials", trials);
assert!(
both * 10 >= z3_proved * 4,
"Fourier–Motzkin recall too low: proved {}/{} of Z3-provable goals",
both,
z3_proved
);
}
fn term_of(x: Symbol, c: i64) -> LinearExpr {
var(x).scale(&Rational::from_i64(c))
}
}