logicaffeine_proof/omega_solve.rs
1//! `omega` — linear INTEGER arithmetic, the discreteness-aware layer over the
2//! rational Farkas core.
3//!
4//! Rational Fourier-Motzkin ([`crate::linarith_solve`]) decides satisfiability
5//! over ℚ; it is sound for ℤ but INCOMPLETE, because it cannot use the one fact
6//! that distinguishes the integers: a strict `a < b` is a non-strict `a+1 ≤ b`,
7//! since nothing lives strictly between consecutive integers. The classic
8//! witness is `x < y ∧ y < x+1` — rationally satisfiable (`x=0, y=½`), integer
9//! UNSAT.
10//!
11//! This module supplies exactly that missing step. It rewrites every strict
12//! hypothesis `a < b` to `a+1 ≤ b` through the kernel axiom `lt_succ_le` (a
13//! certified [`InferenceRule::LtSuccLe`] node), then hands the enlarged set of
14//! `≤`-facts to the existing Farkas refutation ([`crate::engine::cert_farkas`]).
15//! The result is one kernel-checked `⊥` derivation, so `omega` is exactly as
16//! trusted as `linarith` — it just sees more contradictions.
17
18use crate::engine::{as_le_pair, cert_farkas, le_eq};
19use crate::{DerivationTree, InferenceRule, ProofExpr, ProofTerm};
20
21/// If `e` is a strict inequality `lt(a, b) = true`, return `(a, b)`.
22fn as_lt_pair(e: &ProofExpr) -> Option<(ProofTerm, ProofTerm)> {
23 if let ProofExpr::Identity(lhs, rhs) = e {
24 if let (ProofTerm::Function(name, args), ProofTerm::Constant(t)) = (lhs, rhs) {
25 if name == "lt" && args.len() == 2 && t == "true" {
26 return Some((args[0].clone(), args[1].clone()));
27 }
28 }
29 }
30 None
31}
32
33/// `a + 1` as a proof term.
34fn succ(a: &ProofTerm) -> ProofTerm {
35 ProofTerm::Function("add".to_string(), vec![a.clone(), ProofTerm::Constant("1".to_string())])
36}
37
38/// If `t` is `add(b, 1)`, return `b` — the shape `lt_add1_le` cancels.
39fn as_succ(t: &ProofTerm) -> Option<ProofTerm> {
40 if let ProofTerm::Function(name, args) = t {
41 if name == "add" && args.len() == 2 && args[1] == ProofTerm::Constant("1".to_string()) {
42 return Some(args[0].clone());
43 }
44 }
45 None
46}
47
48/// Rewrite a known fact to an equivalent (or stronger) `≤`-fact for the Farkas
49/// core. A `≤`-fact passes through. A strict `a < b` becomes `≤` via integer
50/// discreteness: when the bound is already `b′+1` we cancel it to the clean
51/// `a ≤ b′` (`lt_add1_le`), keeping the reconstructed terms small; otherwise it
52/// tightens to `a+1 ≤ b` (`lt_succ_le`). Anything else is dropped.
53fn to_le_fact(prop: &ProofExpr, proof: &DerivationTree) -> Option<(ProofExpr, DerivationTree)> {
54 if as_le_pair(prop).is_some() {
55 return Some((prop.clone(), proof.clone()));
56 }
57 if let Some((a, b)) = as_lt_pair(prop) {
58 if let Some(b_inner) = as_succ(&b) {
59 // a < b′ + 1 ⊢ a ≤ b′
60 let concl = le_eq(a, b_inner);
61 let tree =
62 DerivationTree::new(concl.clone(), InferenceRule::LtAdd1Le, vec![proof.clone()]);
63 return Some((concl, tree));
64 }
65 // a < b ⊢ a + 1 ≤ b
66 let concl = le_eq(succ(&a), b);
67 let tree = DerivationTree::new(concl.clone(), InferenceRule::LtSuccLe, vec![proof.clone()]);
68 return Some((concl, tree));
69 }
70 None
71}
72
73/// Refute a set of integer hypotheses (`≤` and strict `<`), returning a
74/// kernel-checked `⊥` derivation when they are jointly unsatisfiable over ℤ.
75///
76/// Strictly stronger than [`cert_farkas`]: it first discreteness-tightens every
77/// strict hypothesis, so it catches contradictions the rational core misses.
78/// Returns `None` when the (tightened) system is rationally satisfiable — i.e.
79/// genuinely has an integer model on the fragment covered here.
80pub(crate) fn omega_close(known: &[(ProofExpr, DerivationTree)]) -> Option<DerivationTree> {
81 let le_facts: Vec<(ProofExpr, DerivationTree)> = known
82 .iter()
83 .filter_map(|(p, t)| to_le_fact(p, t))
84 .collect();
85 // Only worth the extra pass if at least one strict fact was tightened;
86 // otherwise the caller's own `cert_farkas` already tried this exact set.
87 let tightened = known.iter().any(|(p, _)| as_lt_pair(p).is_some());
88 if !tightened {
89 return None;
90 }
91 cert_farkas(&le_facts)
92}
93
94/// Does `omega_close` see hypotheses at all — any `≤`/`<` fact? Used by the
95/// tactic to give a precise "not an arithmetic goal" error.
96pub(crate) fn has_arith_facts(known: &[(ProofExpr, DerivationTree)]) -> bool {
97 known
98 .iter()
99 .any(|(p, _)| as_le_pair(p).is_some() || as_lt_pair(p).is_some())
100}