Skip to main content

logicaffeine_kernel/
ring.rs

1//! Ring Tactic: Polynomial Equality by Normalization
2//!
3//! This module implements the `ring` decision procedure, which proves polynomial
4//! equalities by converting terms to canonical polynomial form and comparing them.
5//!
6//! # Algorithm
7//!
8//! The ring tactic works in three steps:
9//! 1. **Reification**: Convert Syntax terms to internal polynomial representation
10//! 2. **Normalization**: Expand and combine like terms into canonical form
11//! 3. **Comparison**: Check if normalized forms are structurally equal
12//!
13//! # Supported Operations
14//!
15//! - Addition (`add`)
16//! - Subtraction (`sub`)
17//! - Multiplication (`mul`)
18//!
19//! Division and modulo are not polynomial operations and are rejected.
20//!
21//! # Canonical Form
22//!
23//! Polynomials are stored as a map from monomials to coefficients.
24//! Monomials are maps from variable indices to exponents.
25//! BTreeMap ensures deterministic ordering for canonical comparison.
26//!
27//! # Exactness
28//!
29//! Coefficients and exponents are arbitrary-precision ([`BigInt`]): the
30//! verdict feeds trusted reflection reductions, so the arithmetic must be
31//! exact at every magnitude — a wrapped coefficient or exponent would equate
32//! unequal polynomials.
33
34use std::collections::BTreeMap;
35
36use logicaffeine_base::numeric::BigInt;
37
38use crate::reify::{extract_binary_app, extract_slit, extract_sname, extract_svar, VarInterner};
39use crate::term::Term;
40
41/// A monomial is a product of variables with their powers.
42///
43/// Example: x^2 * y^3 is represented as {0: 2, 1: 3}
44/// The constant monomial (1) is represented as an empty map.
45///
46/// Uses BTreeMap for deterministic ordering (canonical form).
47#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub struct Monomial {
49    /// Maps variable index to its exponent.
50    /// Variables with exponent 0 are omitted.
51    powers: BTreeMap<i64, BigInt>,
52}
53
54impl Monomial {
55    /// The constant monomial (1)
56    pub fn one() -> Self {
57        Monomial {
58            powers: BTreeMap::new(),
59        }
60    }
61
62    /// A single variable: x_i^1
63    pub fn var(index: i64) -> Self {
64        let mut powers = BTreeMap::new();
65        powers.insert(index, BigInt::from_i64(1));
66        Monomial { powers }
67    }
68
69    /// Multiply two monomials by adding their exponents.
70    ///
71    /// For monomials m1 = x^a * y^b and m2 = x^c * z^d,
72    /// the product is x^(a+c) * y^b * z^d.
73    pub fn mul(&self, other: &Monomial) -> Monomial {
74        let mut result = self.powers.clone();
75        for (var, exp) in &other.powers {
76            let entry = result.entry(*var).or_insert_with(BigInt::zero);
77            *entry = entry.add(exp);
78        }
79        Monomial { powers: result }
80    }
81}
82
83/// A polynomial is a sum of monomials with integer coefficients.
84///
85/// Example: 2*x^2 + 3*x*y - 5 is {x^2: 2, x*y: 3, 1: -5}
86///
87/// Uses BTreeMap for deterministic ordering (canonical form).
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct Polynomial {
90    /// Maps monomials to their coefficients.
91    /// Terms with coefficient 0 are omitted.
92    terms: BTreeMap<Monomial, BigInt>,
93}
94
95impl Polynomial {
96    /// The additive identity (zero polynomial).
97    ///
98    /// Represented as an empty map of terms.
99    pub fn zero() -> Self {
100        Polynomial {
101            terms: BTreeMap::new(),
102        }
103    }
104
105    /// Create a constant polynomial from an integer.
106    ///
107    /// Returns the zero polynomial if `c` is 0.
108    pub fn constant(c: impl Into<BigInt>) -> Self {
109        let c = c.into();
110        if c.is_zero() {
111            return Self::zero();
112        }
113        let mut terms = BTreeMap::new();
114        terms.insert(Monomial::one(), c);
115        Polynomial { terms }
116    }
117
118    /// A single variable: x_i
119    pub fn var(index: i64) -> Self {
120        let mut terms = BTreeMap::new();
121        terms.insert(Monomial::var(index), BigInt::from_i64(1));
122        Polynomial { terms }
123    }
124
125    /// Add two polynomials
126    pub fn add(&self, other: &Polynomial) -> Polynomial {
127        let mut result = self.terms.clone();
128        for (mono, coeff) in &other.terms {
129            let entry = result.entry(mono.clone()).or_insert_with(BigInt::zero);
130            *entry = entry.add(coeff);
131            if entry.is_zero() {
132                result.remove(mono);
133            }
134        }
135        Polynomial { terms: result }
136    }
137
138    /// Negate a polynomial
139    pub fn neg(&self) -> Polynomial {
140        let mut result = BTreeMap::new();
141        for (mono, coeff) in &self.terms {
142            result.insert(mono.clone(), coeff.negated());
143        }
144        Polynomial { terms: result }
145    }
146
147    /// Subtract two polynomials
148    pub fn sub(&self, other: &Polynomial) -> Polynomial {
149        self.add(&other.neg())
150    }
151
152    /// Multiply two polynomials
153    pub fn mul(&self, other: &Polynomial) -> Polynomial {
154        let mut result = Polynomial::zero();
155        for (m1, c1) in &self.terms {
156            for (m2, c2) in &other.terms {
157                let mono = m1.mul(m2);
158                let coeff = c1.mul(c2);
159                let entry = result.terms.entry(mono).or_insert_with(BigInt::zero);
160                *entry = entry.add(&coeff);
161            }
162        }
163        // Clean up zero coefficients
164        result.terms.retain(|_, c| !c.is_zero());
165        result
166    }
167
168    /// Check equality in canonical form.
169    /// Since BTreeMap maintains sorted order and we remove zeros,
170    /// structural equality is semantic equality.
171    pub fn canonical_eq(&self, other: &Polynomial) -> bool {
172        self.terms == other.terms
173    }
174}
175
176/// Error during reification of a term to polynomial form.
177#[derive(Debug)]
178pub enum ReifyError {
179    /// Term contains operations not supported in polynomial arithmetic.
180    ///
181    /// This includes division, modulo, and unknown function symbols.
182    NonPolynomial(String),
183    /// Term has an unexpected structure that cannot be parsed.
184    MalformedTerm,
185}
186
187/// Reify a Syntax term into a polynomial representation.
188///
189/// This function converts the deep embedding of terms (Syntax) into
190/// the internal polynomial representation used for normalization.
191///
192/// # Supported Term Forms
193///
194/// - `SLit n` - Integer literal becomes a constant polynomial
195/// - `SVar i` - De Bruijn variable becomes a polynomial variable
196/// - `SName "x"` - Named global becomes a polynomial variable (interned)
197/// - `SApp (SApp (SName "add") a) b` - Addition of two terms
198/// - `SApp (SApp (SName "sub") a) b` - Subtraction of two terms
199/// - `SApp (SApp (SName "mul") a) b` - Multiplication of two terms
200///
201/// # Errors
202///
203/// Returns [`ReifyError::NonPolynomial`] for unsupported operations (div, mod)
204/// or unknown function symbols.
205///
206/// # Named Variables
207///
208/// Named globals draw their indices from `vars`, so distinct names get
209/// distinct variables and repeated names get the same one. Every term
210/// reified for one goal must share one interner — comparing polynomials
211/// reified under different interners is meaningless.
212pub fn reify(term: &Term, vars: &mut VarInterner) -> Result<Polynomial, ReifyError> {
213    // SLit n -> constant
214    if let Some(n) = extract_slit(term) {
215        return Ok(Polynomial::constant(n));
216    }
217
218    // SVar i -> variable
219    if let Some(i) = extract_svar(term) {
220        return Ok(Polynomial::var(i));
221    }
222
223    // SName "x" -> treat as variable (global constant)
224    if let Some(name) = extract_sname(term) {
225        return Ok(Polynomial::var(vars.intern(&name)));
226    }
227
228    // SApp (SApp (SName "op") a) b -> binary operation
229    if let Some((op, a, b)) = extract_binary_app(term) {
230        match op.as_str() {
231            "add" => {
232                let pa = reify(&a, vars)?;
233                let pb = reify(&b, vars)?;
234                return Ok(pa.add(&pb));
235            }
236            "sub" => {
237                let pa = reify(&a, vars)?;
238                let pb = reify(&b, vars)?;
239                return Ok(pa.sub(&pb));
240            }
241            "mul" => {
242                let pa = reify(&a, vars)?;
243                let pb = reify(&b, vars)?;
244                return Ok(pa.mul(&pb));
245            }
246            "div" | "mod" => {
247                return Err(ReifyError::NonPolynomial(format!(
248                    "Operation '{}' is not supported in ring",
249                    op
250                )));
251            }
252            _ => {
253                return Err(ReifyError::NonPolynomial(format!(
254                    "Unknown operation '{}'",
255                    op
256                )));
257            }
258        }
259    }
260
261    // Cannot reify this term
262    Err(ReifyError::NonPolynomial(
263        "Unrecognized term structure".to_string(),
264    ))
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn test_polynomial_constant() {
273        let p = Polynomial::constant(42);
274        assert_eq!(p, Polynomial::constant(42));
275    }
276
277    #[test]
278    fn test_polynomial_add() {
279        let x = Polynomial::var(0);
280        let y = Polynomial::var(1);
281        let sum1 = x.add(&y);
282        let sum2 = y.add(&x);
283        assert!(sum1.canonical_eq(&sum2), "x+y should equal y+x");
284    }
285
286    #[test]
287    fn test_polynomial_mul() {
288        let x = Polynomial::var(0);
289        let y = Polynomial::var(1);
290        let prod1 = x.mul(&y);
291        let prod2 = y.mul(&x);
292        assert!(prod1.canonical_eq(&prod2), "x*y should equal y*x");
293    }
294
295    #[test]
296    fn test_polynomial_distributivity() {
297        let x = Polynomial::var(0);
298        let y = Polynomial::var(1);
299        let z = Polynomial::var(2);
300
301        // x*(y+z) should equal x*y + x*z
302        let lhs = x.mul(&y.add(&z));
303        let rhs = x.mul(&y).add(&x.mul(&z));
304        assert!(lhs.canonical_eq(&rhs));
305    }
306
307    #[test]
308    fn test_polynomial_subtraction() {
309        let x = Polynomial::var(0);
310        let result = x.sub(&x);
311        assert!(result.canonical_eq(&Polynomial::zero()));
312    }
313
314    #[test]
315    fn test_collatz_algebra() {
316        // 3(2k+1) + 1 = 6k + 4
317        let k = Polynomial::var(0);
318        let two = Polynomial::constant(2);
319        let three = Polynomial::constant(3);
320        let one = Polynomial::constant(1);
321        let four = Polynomial::constant(4);
322        let six = Polynomial::constant(6);
323
324        // LHS: 3*(2*k + 1) + 1
325        let two_k = two.mul(&k);
326        let two_k_plus_1 = two_k.add(&one);
327        let three_times = three.mul(&two_k_plus_1);
328        let lhs = three_times.add(&one);
329
330        // RHS: 6*k + 4
331        let six_k = six.mul(&k);
332        let rhs = six_k.add(&four);
333
334        assert!(lhs.canonical_eq(&rhs), "3(2k+1)+1 should equal 6k+4");
335    }
336}