Skip to main content

logicaffeine_kernel/
omega.rs

1//! Omega Test: True Integer Arithmetic Decision Procedure
2//!
3//! This module implements the Omega test for linear integer arithmetic,
4//! handling the discrete nature of integers correctly.
5//!
6//! # Difference from LIA
7//!
8//! Unlike [`crate::lia`] (which uses rational arithmetic), this module
9//! handles integers with proper semantics:
10//!
11//! - `x > 1` becomes `x >= 2` (strict to non-strict for integers)
12//! - `3x <= 10` implies `x <= 3` (integer division with floor)
13//! - `2x = 5` is unsatisfiable (odd number cannot equal even expression)
14//!
15//! # Algorithm
16//!
17//! The algorithm is similar to Fourier-Motzkin elimination but with
18//! integer-aware semantics:
19//!
20//! 1. **Normalize**: Scale constraints and normalize by GCD
21//! 2. **Convert strict**: Transform `<` to `<=` using integer shift
22//! 3. **Eliminate**: Fourier-Motzkin with integer coefficient handling
23//! 4. **Check**: Verify constant constraints for contradictions
24//!
25//! # When to Use
26//!
27//! Use omega when you need exact integer semantics. Use lia when
28//! rational arithmetic is acceptable (faster but may miss integer-specific
29//! unsatisfiability).
30//!
31//! # Exactness
32//!
33//! Coefficients are arbitrary-precision ([`BigInt`]): Fourier-Motzkin
34//! combinations multiply coefficients pairwise, and the verdict feeds trusted
35//! reflection reductions, so a wrapped product would flip satisfiable into
36//! unsatisfiable.
37
38use std::collections::{BTreeMap, HashSet};
39
40use logicaffeine_base::numeric::BigInt;
41
42use crate::reify::{extract_binary_app, extract_slit, extract_sname, extract_svar, VarInterner};
43use crate::term::Term;
44
45/// Integer linear expression of the form c + a₁x₁ + a₂x₂ + ... + aₙxₙ.
46///
47/// Similar to [`crate::lia::LinearExpr`] but uses integer coefficients
48/// instead of rationals for exact integer arithmetic.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct IntExpr {
51    /// The constant term c.
52    pub constant: BigInt,
53    /// Maps variable index to its integer coefficient (sparse representation).
54    pub coeffs: BTreeMap<i64, BigInt>,
55}
56
57impl IntExpr {
58    /// Create a constant expression
59    pub fn constant(c: impl Into<BigInt>) -> Self {
60        IntExpr {
61            constant: c.into(),
62            coeffs: BTreeMap::new(),
63        }
64    }
65
66    /// Create a single variable expression: 1*x_idx + 0
67    pub fn var(idx: i64) -> Self {
68        let mut coeffs = BTreeMap::new();
69        coeffs.insert(idx, BigInt::from_i64(1));
70        IntExpr {
71            constant: BigInt::zero(),
72            coeffs,
73        }
74    }
75
76    /// Add two expressions
77    pub fn add(&self, other: &Self) -> Self {
78        let mut result = self.clone();
79        result.constant = result.constant.add(&other.constant);
80        for (&v, c) in &other.coeffs {
81            let entry = result.coeffs.entry(v).or_insert_with(BigInt::zero);
82            *entry = entry.add(c);
83            if entry.is_zero() {
84                result.coeffs.remove(&v);
85            }
86        }
87        result
88    }
89
90    /// Negate an expression
91    pub fn neg(&self) -> Self {
92        IntExpr {
93            constant: self.constant.negated(),
94            coeffs: self.coeffs.iter().map(|(&v, c)| (v, c.negated())).collect(),
95        }
96    }
97
98    /// Subtract two expressions
99    pub fn sub(&self, other: &Self) -> Self {
100        self.add(&other.neg())
101    }
102
103    /// Scale by an integer constant
104    pub fn scale(&self, k: impl Into<BigInt>) -> Self {
105        let k = k.into();
106        if k.is_zero() {
107            return IntExpr::constant(0);
108        }
109        IntExpr {
110            constant: self.constant.mul(&k),
111            coeffs: self
112                .coeffs
113                .iter()
114                .map(|(&v, c)| (v, c.mul(&k)))
115                .filter(|(_, c)| !c.is_zero())
116                .collect(),
117        }
118    }
119
120    /// Check if this is a constant expression (no variables)
121    pub fn is_constant(&self) -> bool {
122        self.coeffs.is_empty()
123    }
124
125    /// Get coefficient of a variable (0 if not present)
126    pub fn get_coeff(&self, var: i64) -> BigInt {
127        self.coeffs.get(&var).cloned().unwrap_or_else(BigInt::zero)
128    }
129}
130
131/// Integer constraint representing `expr <= 0` or `expr < 0`.
132///
133/// For integers, strict inequalities can be converted to non-strict:
134/// `x < k` is equivalent to `x <= k - 1`.
135#[derive(Debug, Clone)]
136pub struct IntConstraint {
137    /// The linear expression (constraint is expr OP 0).
138    pub expr: IntExpr,
139    /// If true, this is a strict inequality (`< 0`).
140    /// If false, this is a non-strict inequality (`<= 0`).
141    pub strict: bool,
142}
143
144impl IntConstraint {
145    /// Check if a constant constraint is satisfied
146    pub fn is_satisfied_constant(&self) -> bool {
147        if !self.expr.is_constant() {
148            return true; // Can't determine yet
149        }
150        let c = &self.expr.constant;
151        if self.strict {
152            c.is_negative() // c < 0
153        } else {
154            !c.is_positive() // c ≤ 0
155        }
156    }
157
158    /// Normalize by GCD of all coefficients
159    pub fn normalize(&mut self) {
160        let g = self
161            .expr
162            .coeffs
163            .values()
164            .chain(std::iter::once(&self.expr.constant))
165            .filter(|x| !x.is_zero())
166            .fold(BigInt::zero(), |a, b| gcd(&a, &b.abs()));
167
168        if g > BigInt::from_i64(1) {
169            self.expr.constant = exact_div(&self.expr.constant, &g);
170            for v in self.expr.coeffs.values_mut() {
171                *v = exact_div(v, &g);
172            }
173        }
174    }
175}
176
177/// GCD using the Euclidean algorithm (arguments non-negative).
178fn gcd(a: &BigInt, b: &BigInt) -> BigInt {
179    if b.is_zero() {
180        a.clone()
181    } else {
182        let (_, r) = a.div_rem(b).expect("gcd divisor is nonzero");
183        gcd(b, &r)
184    }
185}
186
187/// Divide exactly (the divisor is a common divisor of the dividend).
188fn exact_div(a: &BigInt, g: &BigInt) -> BigInt {
189    a.div_rem(g).expect("gcd is nonzero").0
190}
191
192/// Reify a Syntax term to an integer linear expression.
193///
194/// Converts the deep embedding (Syntax) into an integer linear expression.
195/// Similar to [`crate::lia::reify_linear`] but produces integer coefficients.
196///
197/// # Supported Forms
198///
199/// - `SLit n` - Integer literal becomes a constant
200/// - `SVar i` - De Bruijn variable becomes a linear variable
201/// - `SName "x"` - Named global becomes a linear variable (interned)
202/// - `add`, `sub`, `mul` - Arithmetic operations (mul only if one operand is constant)
203///
204/// Every term reified for one goal (hypotheses and conclusion alike) must
205/// share one `vars` interner, or their variable indices will not line up.
206///
207/// # Returns
208///
209/// `Some(expr)` on success, `None` if the term is non-linear or malformed.
210pub fn reify_int_linear(term: &Term, vars: &mut VarInterner) -> Option<IntExpr> {
211    // SLit n -> constant
212    if let Some(n) = extract_slit(term) {
213        return Some(IntExpr::constant(n));
214    }
215
216    // SVar i -> variable
217    if let Some(i) = extract_svar(term) {
218        return Some(IntExpr::var(i));
219    }
220
221    // SName "x" -> named variable (global constant treated as free variable)
222    if let Some(name) = extract_sname(term) {
223        return Some(IntExpr::var(vars.intern(&name)));
224    }
225
226    // Binary operations
227    if let Some((op, a, b)) = extract_binary_app(term) {
228        match op.as_str() {
229            "add" => {
230                let la = reify_int_linear(&a, vars)?;
231                let lb = reify_int_linear(&b, vars)?;
232                return Some(la.add(&lb));
233            }
234            "sub" => {
235                let la = reify_int_linear(&a, vars)?;
236                let lb = reify_int_linear(&b, vars)?;
237                return Some(la.sub(&lb));
238            }
239            "mul" => {
240                let la = reify_int_linear(&a, vars)?;
241                let lb = reify_int_linear(&b, vars)?;
242                // Only linear if one side is constant
243                if la.is_constant() {
244                    return Some(lb.scale(la.constant));
245                }
246                if lb.is_constant() {
247                    return Some(la.scale(lb.constant));
248                }
249                return None; // Non-linear
250            }
251            _ => return None,
252        }
253    }
254
255    None
256}
257
258/// Extract comparison from goal: (SApp (SApp (SName "Lt"|"Le"|"Gt"|"Ge") lhs) rhs)
259pub fn extract_comparison(term: &Term) -> Option<(String, Term, Term)> {
260    if let Some((rel, lhs, rhs)) = extract_binary_app(term) {
261        match rel.as_str() {
262            "Lt" | "Le" | "Gt" | "Ge" | "lt" | "le" | "gt" | "ge" => {
263                return Some((rel, lhs, rhs));
264            }
265            _ => {}
266        }
267    }
268    None
269}
270
271/// Convert a goal to constraints for validity checking using integer semantics.
272///
273/// Key difference from lia: strict inequalities are converted for integers.
274/// - x < k becomes x <= k - 1 (since x must be an integer)
275/// - x > k becomes x >= k + 1
276///
277/// To prove a goal is valid, we check if its negation is unsatisfiable.
278pub fn goal_to_negated_constraint(rel: &str, lhs: &IntExpr, rhs: &IntExpr) -> Option<IntConstraint> {
279    // diff = lhs - rhs
280    let diff = lhs.sub(rhs);
281    let one = IntExpr::constant(1);
282
283    match rel {
284        // Lt: a < b valid iff NOT(a >= b)
285        // For integers: a >= b means a - b >= 0
286        // We check if a - b >= 0 is satisfiable
287        // Constraint form for unsatisfiability check: -(a - b) <= 0, i.e., (b - a) <= 0
288        "Lt" | "lt" => Some(IntConstraint {
289            expr: rhs.sub(lhs),
290            strict: false,
291        }),
292
293        // Le: a <= b valid iff NOT(a > b)
294        // For integers: a > b means a - b >= 1 (strict to non-strict!)
295        // So negation is: a - b >= 1, i.e., a - b - 1 >= 0
296        // Constraint: -(a - b - 1) <= 0, i.e., (b - a + 1) <= 0
297        // Equivalently: (b - a) <= -1
298        "Le" | "le" => Some(IntConstraint {
299            expr: rhs.sub(lhs).add(&one),
300            strict: false,
301        }),
302
303        // Gt: a > b valid iff NOT(a <= b)
304        // For integers: a <= b means a - b <= 0
305        // Constraint: (a - b) <= 0
306        "Gt" | "gt" => Some(IntConstraint {
307            expr: diff,
308            strict: false,
309        }),
310
311        // Ge: a >= b valid iff NOT(a < b)
312        // For integers: a < b means a - b <= -1 (strict to non-strict!)
313        // Constraint: (a - b) <= -1, i.e., (a - b + 1) <= 0
314        "Ge" | "ge" => Some(IntConstraint {
315            expr: diff.add(&one),
316            strict: false,
317        }),
318
319        _ => None,
320    }
321}
322
323/// Check if integer constraints are unsatisfiable using the Omega test.
324///
325/// This is the main entry point for the omega decision procedure. It uses
326/// integer-aware Fourier-Motzkin elimination to check for contradictions.
327///
328/// # Integer Semantics
329///
330/// Unlike rational Fourier-Motzkin, this procedure:
331/// - Normalizes constraints by their GCD
332/// - Handles strict inequalities by integer shift (`< k` becomes `<= k-1`)
333/// - Detects integer-specific unsatisfiability
334///
335/// # Returns
336///
337/// - `true` if no integer assignment satisfies all constraints (unsatisfiable)
338/// - `false` if the constraints may be satisfiable
339///
340/// # Usage for Validity
341///
342/// To prove a goal G is valid over integers, check if NOT(G) is unsatisfiable.
343/// If `omega_unsat(negation_constraints)` returns true, the goal is valid.
344pub fn omega_unsat(constraints: &[IntConstraint]) -> bool {
345    if constraints.is_empty() {
346        return false;
347    }
348
349    // Normalize all constraints
350    let mut current: Vec<IntConstraint> = constraints.to_vec();
351    for c in &mut current {
352        c.normalize();
353    }
354
355    // Check for immediate contradictions
356    for c in &current {
357        if c.expr.is_constant() && !c.is_satisfied_constant() {
358            return true;
359        }
360    }
361
362    // Collect all variables
363    let vars: Vec<i64> = current
364        .iter()
365        .flat_map(|c| c.expr.coeffs.keys().copied())
366        .collect::<HashSet<_>>()
367        .into_iter()
368        .collect();
369
370    // Eliminate each variable
371    for var in vars {
372        current = eliminate_variable_int(&current, var);
373
374        // Early termination: check for constant contradictions
375        for c in &current {
376            if c.expr.is_constant() && !c.is_satisfied_constant() {
377                return true;
378            }
379        }
380    }
381
382    // Check all remaining constant constraints
383    current
384        .iter()
385        .any(|c| c.expr.is_constant() && !c.is_satisfied_constant())
386}
387
388/// Eliminate a variable from constraints using integer-aware Fourier-Motzkin.
389fn eliminate_variable_int(constraints: &[IntConstraint], var: i64) -> Vec<IntConstraint> {
390    let mut lower: Vec<(IntExpr, BigInt)> = vec![]; // (rest, |coeff|) for lower bounds
391    let mut upper: Vec<(IntExpr, BigInt)> = vec![]; // (rest, coeff) for upper bounds
392    let mut independent: Vec<IntConstraint> = vec![];
393
394    for c in constraints {
395        let coeff = c.expr.get_coeff(var);
396        if coeff.is_zero() {
397            independent.push(c.clone());
398        } else {
399            // c.expr = coeff*var + rest <= 0
400            let mut rest = c.expr.clone();
401            rest.coeffs.remove(&var);
402
403            if coeff.is_positive() {
404                // coeff*var + rest <= 0
405                // var <= -rest/coeff (upper bound)
406                upper.push((rest, coeff));
407            } else {
408                // coeff*var + rest <= 0, coeff < 0
409                // |coeff|*(-var) + rest <= 0
410                // -var <= -rest/|coeff|
411                // var >= rest/|coeff| (lower bound)
412                lower.push((rest, coeff.negated()));
413            }
414        }
415    }
416
417    // Combine lower and upper bounds
418    // If lo/a <= var <= -hi/b, then lo/a <= -hi/b
419    // Multiply out: b*lo <= -a*hi
420    // Rearrange: b*lo + a*hi <= 0
421    for (lo_rest, lo_coeff) in &lower {
422        for (hi_rest, hi_coeff) in &upper {
423            // Lower: var >= lo_rest / lo_coeff (lo_coeff is positive)
424            // Upper: var <= -hi_rest / hi_coeff (hi_coeff is positive)
425            // Combined: lo_rest / lo_coeff <= -hi_rest / hi_coeff
426            // => hi_coeff * lo_rest <= -lo_coeff * hi_rest
427            // => hi_coeff * lo_rest + lo_coeff * hi_rest <= 0
428            let new_expr = lo_rest
429                .scale(hi_coeff.clone())
430                .add(&hi_rest.scale(lo_coeff.clone()));
431
432            let mut new_constraint = IntConstraint {
433                expr: new_expr,
434                strict: false,
435            };
436            new_constraint.normalize();
437            independent.push(new_constraint);
438        }
439    }
440
441    independent
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    #[test]
449    fn test_int_expr_add() {
450        let x = IntExpr::var(0);
451        let y = IntExpr::var(1);
452        let sum = x.add(&y);
453        assert!(!sum.is_constant());
454        assert_eq!(sum.get_coeff(0), BigInt::from_i64(1));
455        assert_eq!(sum.get_coeff(1), BigInt::from_i64(1));
456    }
457
458    #[test]
459    fn test_int_expr_cancel() {
460        let x = IntExpr::var(0);
461        let neg_x = x.neg();
462        let zero = x.add(&neg_x);
463        assert!(zero.is_constant());
464        assert!(zero.constant.is_zero());
465    }
466
467    #[test]
468    fn test_constraint_satisfied() {
469        // -1 <= 0 is satisfied
470        let c1 = IntConstraint {
471            expr: IntExpr::constant(-1),
472            strict: false,
473        };
474        assert!(c1.is_satisfied_constant());
475
476        // 1 <= 0 is NOT satisfied
477        let c2 = IntConstraint {
478            expr: IntExpr::constant(1),
479            strict: false,
480        };
481        assert!(!c2.is_satisfied_constant());
482
483        // 0 <= 0 is satisfied
484        let c3 = IntConstraint {
485            expr: IntExpr::constant(0),
486            strict: false,
487        };
488        assert!(c3.is_satisfied_constant());
489    }
490
491    #[test]
492    fn test_omega_constant() {
493        // 1 <= 0 is unsat
494        let constraints = vec![IntConstraint {
495            expr: IntExpr::constant(1),
496            strict: false,
497        }];
498        assert!(omega_unsat(&constraints));
499
500        // -1 <= 0 is sat
501        let constraints2 = vec![IntConstraint {
502            expr: IntExpr::constant(-1),
503            strict: false,
504        }];
505        assert!(!omega_unsat(&constraints2));
506    }
507
508    #[test]
509    fn test_x_lt_x_plus_1() {
510        // x < x + 1 is always true for integers
511        // To prove: negation x >= x + 1 is unsat
512        // x >= x + 1 means x - x >= 1 means 0 >= 1 which is false
513
514        // Negation constraint: (x+1) - x <= 0 = 1 <= 0
515        let constraint = IntConstraint {
516            expr: IntExpr::constant(1),
517            strict: false,
518        };
519        assert!(omega_unsat(&[constraint]));
520    }
521}