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}