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 ¤t {
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(¤t, var);
373
374 // Early termination: check for constant contradictions
375 for c in ¤t {
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}