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}