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
27use std::collections::BTreeMap;
28
29use crate::term::{Literal, Term};
30
31/// A monomial is a product of variables with their powers.
32///
33/// Example: x^2 * y^3 is represented as {0: 2, 1: 3}
34/// The constant monomial (1) is represented as an empty map.
35///
36/// Uses BTreeMap for deterministic ordering (canonical form).
37#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub struct Monomial {
39    /// Maps variable index to its exponent.
40    /// Variables with exponent 0 are omitted.
41    powers: BTreeMap<i64, u32>,
42}
43
44impl Monomial {
45    /// The constant monomial (1)
46    pub fn one() -> Self {
47        Monomial {
48            powers: BTreeMap::new(),
49        }
50    }
51
52    /// A single variable: x_i^1
53    pub fn var(index: i64) -> Self {
54        let mut powers = BTreeMap::new();
55        powers.insert(index, 1);
56        Monomial { powers }
57    }
58
59    /// Multiply two monomials by adding their exponents.
60    ///
61    /// For monomials m1 = x^a * y^b and m2 = x^c * z^d,
62    /// the product is x^(a+c) * y^b * z^d.
63    pub fn mul(&self, other: &Monomial) -> Monomial {
64        let mut result = self.powers.clone();
65        for (var, exp) in &other.powers {
66            *result.entry(*var).or_insert(0) += exp;
67        }
68        Monomial { powers: result }
69    }
70}
71
72/// A polynomial is a sum of monomials with integer coefficients.
73///
74/// Example: 2*x^2 + 3*x*y - 5 is {x^2: 2, x*y: 3, 1: -5}
75///
76/// Uses BTreeMap for deterministic ordering (canonical form).
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct Polynomial {
79    /// Maps monomials to their coefficients.
80    /// Terms with coefficient 0 are omitted.
81    terms: BTreeMap<Monomial, i64>,
82}
83
84impl Polynomial {
85    /// The additive identity (zero polynomial).
86    ///
87    /// Represented as an empty map of terms.
88    pub fn zero() -> Self {
89        Polynomial {
90            terms: BTreeMap::new(),
91        }
92    }
93
94    /// Create a constant polynomial from an integer.
95    ///
96    /// Returns the zero polynomial if `c` is 0.
97    pub fn constant(c: i64) -> Self {
98        if c == 0 {
99            return Self::zero();
100        }
101        let mut terms = BTreeMap::new();
102        terms.insert(Monomial::one(), c);
103        Polynomial { terms }
104    }
105
106    /// A single variable: x_i
107    pub fn var(index: i64) -> Self {
108        let mut terms = BTreeMap::new();
109        terms.insert(Monomial::var(index), 1);
110        Polynomial { terms }
111    }
112
113    /// Add two polynomials
114    pub fn add(&self, other: &Polynomial) -> Polynomial {
115        let mut result = self.terms.clone();
116        for (mono, coeff) in &other.terms {
117            let entry = result.entry(mono.clone()).or_insert(0);
118            *entry += coeff;
119            if *entry == 0 {
120                result.remove(mono);
121            }
122        }
123        Polynomial { terms: result }
124    }
125
126    /// Negate a polynomial
127    pub fn neg(&self) -> Polynomial {
128        let mut result = BTreeMap::new();
129        for (mono, coeff) in &self.terms {
130            result.insert(mono.clone(), -coeff);
131        }
132        Polynomial { terms: result }
133    }
134
135    /// Subtract two polynomials
136    pub fn sub(&self, other: &Polynomial) -> Polynomial {
137        self.add(&other.neg())
138    }
139
140    /// Multiply two polynomials
141    pub fn mul(&self, other: &Polynomial) -> Polynomial {
142        let mut result = Polynomial::zero();
143        for (m1, c1) in &self.terms {
144            for (m2, c2) in &other.terms {
145                let mono = m1.mul(m2);
146                let coeff = c1 * c2;
147                let entry = result.terms.entry(mono).or_insert(0);
148                *entry += coeff;
149            }
150        }
151        // Clean up zero coefficients
152        result.terms.retain(|_, c| *c != 0);
153        result
154    }
155
156    /// Check equality in canonical form.
157    /// Since BTreeMap maintains sorted order and we remove zeros,
158    /// structural equality is semantic equality.
159    pub fn canonical_eq(&self, other: &Polynomial) -> bool {
160        self.terms == other.terms
161    }
162}
163
164/// Error during reification of a term to polynomial form.
165#[derive(Debug)]
166pub enum ReifyError {
167    /// Term contains operations not supported in polynomial arithmetic.
168    ///
169    /// This includes division, modulo, and unknown function symbols.
170    NonPolynomial(String),
171    /// Term has an unexpected structure that cannot be parsed.
172    MalformedTerm,
173}
174
175/// Reify a Syntax term into a polynomial representation.
176///
177/// This function converts the deep embedding of terms (Syntax) into
178/// the internal polynomial representation used for normalization.
179///
180/// # Supported Term Forms
181///
182/// - `SLit n` - Integer literal becomes a constant polynomial
183/// - `SVar i` - De Bruijn variable becomes a polynomial variable
184/// - `SName "x"` - Named global becomes a polynomial variable (hashed)
185/// - `SApp (SApp (SName "add") a) b` - Addition of two terms
186/// - `SApp (SApp (SName "sub") a) b` - Subtraction of two terms
187/// - `SApp (SApp (SName "mul") a) b` - Multiplication of two terms
188///
189/// # Errors
190///
191/// Returns [`ReifyError::NonPolynomial`] for unsupported operations (div, mod)
192/// or unknown function symbols.
193///
194/// # Named Variables
195///
196/// Named variables (via SName) are converted to unique negative indices
197/// to avoid collision with De Bruijn indices (which are non-negative).
198pub fn reify(term: &Term) -> Result<Polynomial, ReifyError> {
199    // SLit n -> constant
200    if let Some(n) = extract_slit(term) {
201        return Ok(Polynomial::constant(n));
202    }
203
204    // SVar i -> variable
205    if let Some(i) = extract_svar(term) {
206        return Ok(Polynomial::var(i));
207    }
208
209    // SName "x" -> treat as variable (global constant)
210    if let Some(name) = extract_sname(term) {
211        // Use negative indices for named globals to distinguish from SVar
212        let hash = name_to_var_index(&name);
213        return Ok(Polynomial::var(hash));
214    }
215
216    // SApp (SApp (SName "op") a) b -> binary operation
217    if let Some((op, a, b)) = extract_binary_app(term) {
218        match op.as_str() {
219            "add" => {
220                let pa = reify(&a)?;
221                let pb = reify(&b)?;
222                return Ok(pa.add(&pb));
223            }
224            "sub" => {
225                let pa = reify(&a)?;
226                let pb = reify(&b)?;
227                return Ok(pa.sub(&pb));
228            }
229            "mul" => {
230                let pa = reify(&a)?;
231                let pb = reify(&b)?;
232                return Ok(pa.mul(&pb));
233            }
234            "div" | "mod" => {
235                return Err(ReifyError::NonPolynomial(format!(
236                    "Operation '{}' is not supported in ring",
237                    op
238                )));
239            }
240            _ => {
241                return Err(ReifyError::NonPolynomial(format!(
242                    "Unknown operation '{}'",
243                    op
244                )));
245            }
246        }
247    }
248
249    // Cannot reify this term
250    Err(ReifyError::NonPolynomial(
251        "Unrecognized term structure".to_string(),
252    ))
253}
254
255/// Extract integer from SLit n
256fn extract_slit(term: &Term) -> Option<i64> {
257    // Pattern: App(Global("SLit"), Lit(Int(n)))
258    if let Term::App(ctor, arg) = term {
259        if let Term::Global(name) = ctor.as_ref() {
260            if name == "SLit" {
261                if let Term::Lit(Literal::Int(n)) = arg.as_ref() {
262                    return Some(*n);
263                }
264            }
265        }
266    }
267    None
268}
269
270/// Extract variable index from SVar i
271fn extract_svar(term: &Term) -> Option<i64> {
272    // Pattern: App(Global("SVar"), Lit(Int(i)))
273    if let Term::App(ctor, arg) = term {
274        if let Term::Global(name) = ctor.as_ref() {
275            if name == "SVar" {
276                if let Term::Lit(Literal::Int(i)) = arg.as_ref() {
277                    return Some(*i);
278                }
279            }
280        }
281    }
282    None
283}
284
285/// Extract name from SName "x"
286fn extract_sname(term: &Term) -> Option<String> {
287    // Pattern: App(Global("SName"), Lit(Text(s)))
288    if let Term::App(ctor, arg) = term {
289        if let Term::Global(name) = ctor.as_ref() {
290            if name == "SName" {
291                if let Term::Lit(Literal::Text(s)) = arg.as_ref() {
292                    return Some(s.clone());
293                }
294            }
295        }
296    }
297    None
298}
299
300/// Extract binary application: SApp (SApp (SName "op") a) b
301fn extract_binary_app(term: &Term) -> Option<(String, Term, Term)> {
302    // Structure: App(App(SApp, App(App(SApp, op_term), a)), b)
303    // Which represents: SApp (SApp op a) b
304    if let Term::App(outer, b) = term {
305        if let Term::App(sapp_outer, inner) = outer.as_ref() {
306            if let Term::Global(ctor) = sapp_outer.as_ref() {
307                if ctor == "SApp" {
308                    // inner should be: App(App(SApp, op), a)
309                    if let Term::App(partial, a) = inner.as_ref() {
310                        if let Term::App(sapp_inner, op_term) = partial.as_ref() {
311                            if let Term::Global(ctor2) = sapp_inner.as_ref() {
312                                if ctor2 == "SApp" {
313                                    if let Some(op) = extract_sname(op_term) {
314                                        return Some((
315                                            op,
316                                            a.as_ref().clone(),
317                                            b.as_ref().clone(),
318                                        ));
319                                    }
320                                }
321                            }
322                        }
323                    }
324                }
325            }
326        }
327    }
328    None
329}
330
331/// Convert a name to a unique negative variable index
332fn name_to_var_index(name: &str) -> i64 {
333    // Use a hash of the name, made negative to distinguish from SVar indices
334    let hash: i64 = name
335        .bytes()
336        .fold(0i64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as i64));
337    -(hash.abs() + 1_000_000) // Ensure it's negative and far from typical SVar indices
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn test_polynomial_constant() {
346        let p = Polynomial::constant(42);
347        assert_eq!(p, Polynomial::constant(42));
348    }
349
350    #[test]
351    fn test_polynomial_add() {
352        let x = Polynomial::var(0);
353        let y = Polynomial::var(1);
354        let sum1 = x.add(&y);
355        let sum2 = y.add(&x);
356        assert!(sum1.canonical_eq(&sum2), "x+y should equal y+x");
357    }
358
359    #[test]
360    fn test_polynomial_mul() {
361        let x = Polynomial::var(0);
362        let y = Polynomial::var(1);
363        let prod1 = x.mul(&y);
364        let prod2 = y.mul(&x);
365        assert!(prod1.canonical_eq(&prod2), "x*y should equal y*x");
366    }
367
368    #[test]
369    fn test_polynomial_distributivity() {
370        let x = Polynomial::var(0);
371        let y = Polynomial::var(1);
372        let z = Polynomial::var(2);
373
374        // x*(y+z) should equal x*y + x*z
375        let lhs = x.mul(&y.add(&z));
376        let rhs = x.mul(&y).add(&x.mul(&z));
377        assert!(lhs.canonical_eq(&rhs));
378    }
379
380    #[test]
381    fn test_polynomial_subtraction() {
382        let x = Polynomial::var(0);
383        let result = x.sub(&x);
384        assert!(result.canonical_eq(&Polynomial::zero()));
385    }
386
387    #[test]
388    fn test_collatz_algebra() {
389        // 3(2k+1) + 1 = 6k + 4
390        let k = Polynomial::var(0);
391        let two = Polynomial::constant(2);
392        let three = Polynomial::constant(3);
393        let one = Polynomial::constant(1);
394        let four = Polynomial::constant(4);
395        let six = Polynomial::constant(6);
396
397        // LHS: 3*(2*k + 1) + 1
398        let two_k = two.mul(&k);
399        let two_k_plus_1 = two_k.add(&one);
400        let three_times = three.mul(&two_k_plus_1);
401        let lhs = three_times.add(&one);
402
403        // RHS: 6*k + 4
404        let six_k = six.mul(&k);
405        let rhs = six_k.add(&four);
406
407        assert!(lhs.canonical_eq(&rhs), "3(2k+1)+1 should equal 6k+4");
408    }
409}