Skip to main content

logicaffeine_proof/
arith.rs

1//! Proof-PRODUCING arithmetic oracle (untrusted search, kernel-checked proof).
2//!
3//! Given an `Int` equality goal `Eq Int lhs rhs`, [`prove_int_eq`] searches for a
4//! genuine kernel proof term and returns it — or `None`. Nothing here is trusted:
5//! whatever it returns is re-checked by the kernel's `infer_type`, so a wrong
6//! proof is rejected, never believed. This is the Coq-`lia`/`nia` model — the fast
7//! search lives outside the trusted base; a bug here can only cause a *failed*
8//! proof, never a false one.
9//!
10//! Trust boundary: closed/literal goals are proven by `add`/`mul` **computation**
11//! plus `refl` (zero axioms). Ring identities are proven from the seven registered
12//! commutative-ring axioms (`add_comm`/`add_assoc`/`add_zero`/`mul_comm`/
13//! `mul_assoc`/`mul_one`/`mul_distrib_add`) — the entire trusted arithmetic base.
14
15use logicaffeine_kernel::{normalize, Context, Term};
16
17fn global(name: &str) -> Term {
18    Term::Global(name.to_string())
19}
20fn app(f: Term, x: Term) -> Term {
21    Term::App(Box::new(f), Box::new(x))
22}
23fn app2(f: Term, x: Term, y: Term) -> Term {
24    app(app(f, x), y)
25}
26fn app3(f: Term, x: Term, y: Term, z: Term) -> Term {
27    app(app2(f, x, y), z)
28}
29fn int() -> Term {
30    global("Int")
31}
32
33/// `refl Int t`
34fn refl(t: Term) -> Term {
35    app2(global("refl"), int(), t)
36}
37
38/// `Eq_sym Int x y proof` : turns a proof of `Eq Int x y` into `Eq Int y x`.
39fn eq_sym(x: Term, y: Term, proof: Term) -> Term {
40    app(
41        app(app(app(global("Eq_sym"), int()), x), y),
42        proof,
43    )
44}
45
46/// Match `op a b` (i.e. `App(App(Global op, a), b)`), returning `(a, b)`.
47fn match_bin(t: &Term, op: &str) -> Option<(Term, Term)> {
48    if let Term::App(f, b) = t {
49        if let Term::App(g, a) = f.as_ref() {
50            if let Term::Global(name) = g.as_ref() {
51                if name == op {
52                    return Some(((**a).clone(), (**b).clone()));
53                }
54            }
55        }
56    }
57    None
58}
59
60/// Definitional equality check: do `a` and `b` share a normal form?
61fn conv(ctx: &Context, a: &Term, b: &Term) -> bool {
62    normalize(ctx, a) == normalize(ctx, b)
63}
64
65/// Prove an `Int` equality `Eq Int lhs rhs`, or return `None`.
66///
67/// The returned term, when it exists, has type `Eq Int lhs rhs` (the kernel will
68/// confirm). `None` means "this oracle found no proof" — never "it is false."
69pub fn prove_int_eq(ctx: &Context, lhs: &Term, rhs: &Term) -> Option<Term> {
70    // Complete negative decision: two terms are a *formal* ring identity iff they
71    // have the same canonical polynomial. If they differ, no proof exists — bail
72    // fast (and never waste the search on a non-identity).
73    let mut polys = Polynomials { atoms: Vec::new() };
74    let pl = to_poly(&mut polys, ctx, lhs);
75    let pr = to_poly(&mut polys, ctx, rhs);
76    if pl != pr {
77        return None;
78    }
79
80    // Positive proof: bounded rewrite search first…
81    if let Some(p) = prove_eq(ctx, lhs, rhs, MAX_REWRITE_DEPTH) {
82        return Some(p);
83    }
84    // …then the proof-producing normalizer (handles coefficient collection / FOIL
85    // that the bounded search can't reach). Additive & sound: returns None if it
86    // can't build a proof, and every proof it returns is kernel-checked.
87    prove_by_normalization(ctx, lhs, rhs)
88}
89
90// =============================================================================
91// Polynomial decision layer — canonical multivariate polynomials over opaque
92// atoms (any non-add/mul/sub/literal subterm). Used for the fast, complete
93// negative decision and as the target of the proof-producing normalizer.
94// =============================================================================
95
96/// Atom interner: distinct non-arithmetic subterms get stable ids.
97struct Polynomials {
98    atoms: Vec<Term>,
99}
100impl Polynomials {
101    fn atom_id(&mut self, t: &Term) -> usize {
102        if let Some(i) = self.atoms.iter().position(|a| a == t) {
103            i
104        } else {
105            self.atoms.push(t.clone());
106            self.atoms.len() - 1
107        }
108    }
109}
110
111/// A monomial: a sorted multiset of atom ids (`[]` = the constant monomial).
112type Mono = Vec<usize>;
113/// A polynomial: monomials with nonzero coefficients, sorted, like terms combined.
114type Poly = Vec<(Mono, i64)>;
115
116fn poly_canon(mut terms: Vec<(Mono, i64)>) -> Poly {
117    for (m, _) in terms.iter_mut() {
118        m.sort_unstable();
119    }
120    terms.sort_by(|a, b| a.0.cmp(&b.0));
121    let mut out: Poly = Vec::new();
122    for (m, c) in terms {
123        if c == 0 {
124            continue;
125        }
126        if let Some(last) = out.last_mut() {
127            if last.0 == m {
128                last.1 += c;
129                if last.1 == 0 {
130                    out.pop();
131                }
132                continue;
133            }
134        }
135        out.push((m, c));
136    }
137    out
138}
139
140fn poly_add(a: &Poly, b: &Poly) -> Poly {
141    let mut t = a.clone();
142    t.extend(b.iter().cloned());
143    poly_canon(t)
144}
145fn poly_mul(a: &Poly, b: &Poly) -> Poly {
146    let mut t = Vec::new();
147    for (m1, c1) in a {
148        for (m2, c2) in b {
149            let mut m = m1.clone();
150            m.extend(m2.iter().cloned());
151            t.push((m, c1 * c2));
152        }
153    }
154    poly_canon(t)
155}
156fn poly_scale(k: i64, a: &Poly) -> Poly {
157    poly_canon(a.iter().map(|(m, c)| (m.clone(), c * k)).collect())
158}
159
160/// Compute the canonical polynomial of an arithmetic term.
161fn to_poly(p: &mut Polynomials, ctx: &Context, t: &Term) -> Poly {
162    let t = normalize(ctx, t);
163    if let Term::Lit(logicaffeine_kernel::Literal::Int(n)) = t {
164        return if n == 0 { vec![] } else { vec![(vec![], n)] };
165    }
166    if let Some((a, b)) = match_bin(&t, "add") {
167        return poly_add(&to_poly(p, ctx, &a), &to_poly(p, ctx, &b));
168    }
169    if let Some((a, b)) = match_bin(&t, "mul") {
170        return poly_mul(&to_poly(p, ctx, &a), &to_poly(p, ctx, &b));
171    }
172    if let Some((a, b)) = match_bin(&t, "sub") {
173        return poly_add(&to_poly(p, ctx, &a), &poly_scale(-1, &to_poly(p, ctx, &b)));
174    }
175    let id = p.atom_id(&t);
176    vec![(vec![id], 1)]
177}
178
179// =============================================================================
180// Proof-producing canonical normalizer.
181//
182// `norm(t)` returns `(c, proof : Eq Int t c)` where `c = reify(to_poly t)` is the
183// deterministic canonical term. Because the negative guard already established
184// `to_poly(lhs) == to_poly(rhs)`, the two canonical terms are identical, so the
185// goal follows by transitivity. Every proof is built from the ring axioms and is
186// re-checked by the kernel — the normalizer is untrusted.
187// =============================================================================
188
189fn lit_t(n: i64) -> Term {
190    Term::Lit(logicaffeine_kernel::Literal::Int(n))
191}
192fn ax1(name: &str, a: Term) -> Term {
193    app(global(name), a)
194}
195fn ax2(name: &str, a: Term, b: Term) -> Term {
196    app2(global(name), a, b)
197}
198fn ax3(name: &str, a: Term, b: Term, c: Term) -> Term {
199    app3(global(name), a, b, c)
200}
201
202/// The product term for a monomial (left-assoc), or `None` for the empty monomial.
203fn mono_to_term(mono: &[usize], atoms: &[Term]) -> Option<Term> {
204    let mut iter = mono.iter();
205    let first = *iter.next()?;
206    let mut t = atoms[first].clone();
207    for &id in iter {
208        t = ax2("mul", t, atoms[id].clone());
209    }
210    Some(t)
211}
212/// The canonical term for one `(monomial, coeff)`.
213fn scaled_term(mono: &[usize], coeff: i64, atoms: &[Term]) -> Term {
214    match mono_to_term(mono, atoms) {
215        None => lit_t(coeff),
216        Some(m) if coeff == 1 => m,
217        Some(m) => ax2("mul", lit_t(coeff), m),
218    }
219}
220/// The canonical term for a whole polynomial (left-assoc sum, sorted).
221fn reify(poly: &[(Mono, i64)], atoms: &[Term]) -> Term {
222    // Drop zero-coefficient monomials so the canonical form is unique (a cancelled
223    // term must not linger as `add 0 …` / `mul 0 …`, which would make two equal
224    // polynomials reify to syntactically different terms).
225    let mut iter = poly.iter().filter(|(_, c)| *c != 0);
226    let Some((m0, c0)) = iter.next() else {
227        return lit_t(0);
228    };
229    let mut t = scaled_term(m0, *c0, atoms);
230    for (m, c) in iter {
231        t = ax2("add", t, scaled_term(m, *c, atoms));
232    }
233    t
234}
235
236/// Proof `term = mul (lit c) M`, where `term = scaled_term(m, c)` and `M = mono_to_term(m)`.
237/// For `c == 1` the term is the bare monomial `M`, coerced via `mul_one`/`mul_comm`.
238fn as_scaled_mul(c: i64, m_term: &Term) -> Term {
239    if c == 1 {
240        // M = mul 1 M  via  sym (mul 1 M = mul M 1 = M)
241        let mul1m = ax2("mul", lit_t(1), m_term.clone());
242        let chain = eq_trans(
243            mul1m.clone(),
244            ax2("mul", m_term.clone(), lit_t(1)),
245            m_term.clone(),
246            ax2("mul_comm", lit_t(1), m_term.clone()),
247            ax1("mul_one", m_term.clone()),
248        );
249        eq_sym(mul1m, m_term.clone(), chain)
250    } else {
251        refl(ax2("mul", lit_t(c), m_term.clone()))
252    }
253}
254
255/// Proof `add (mul c1 M) (mul c2 M) = mul (add c1 c2) M`  (right reverse-distribution).
256fn rev_distrib(c1: i64, c2: i64, m_term: &Term) -> Term {
257    let big_c = ax2("add", lit_t(c1), lit_t(c2));
258    let mul_c1 = ax2("mul", lit_t(c1), m_term.clone());
259    let mul_c2 = ax2("mul", lit_t(c2), m_term.clone());
260    // mul C M = mul M C
261    let s1 = ax2("mul_comm", big_c.clone(), m_term.clone());
262    // mul M C = add (mul M c1) (mul M c2)
263    let s2 = ax3("mul_distrib_add", m_term.clone(), lit_t(c1), lit_t(c2));
264    // add (mul M c1)(mul M c2) = add (mul c1 M)(mul c2 M)
265    let s3 = cong2(
266        "add",
267        &ax2("mul", m_term.clone(), lit_t(c1)),
268        &mul_c1,
269        &ax2("mul", m_term.clone(), lit_t(c2)),
270        &mul_c2,
271        ax2("mul_comm", m_term.clone(), lit_t(c1)),
272        ax2("mul_comm", m_term.clone(), lit_t(c2)),
273    );
274    // mul C M = add (mul c1 M)(mul c2 M)
275    let forward = eq_trans(
276        ax2("mul", big_c.clone(), m_term.clone()),
277        ax2("mul", m_term.clone(), big_c.clone()),
278        ax2("add", mul_c1.clone(), mul_c2.clone()),
279        s1,
280        eq_trans(
281            ax2("mul", m_term.clone(), big_c.clone()),
282            ax2("add", ax2("mul", m_term.clone(), lit_t(c1)), ax2("mul", m_term.clone(), lit_t(c2))),
283            ax2("add", mul_c1.clone(), mul_c2.clone()),
284            s2,
285            s3,
286        ),
287    );
288    eq_sym(ax2("mul", big_c, m_term.clone()), ax2("add", mul_c1, mul_c2), forward)
289}
290
291/// Proof `add (scaled m c1) (scaled m c2) = scaled m (c1+c2)` for the SAME monomial `m`.
292fn combine_coeff(m: &[usize], c1: i64, c2: i64, atoms: &[Term]) -> Option<Term> {
293    let t1 = scaled_term(m, c1, atoms);
294    let t2 = scaled_term(m, c2, atoms);
295    let sum = c1 + c2;
296    let result = scaled_term(m, sum, atoms);
297    let _ = (&t1, &t2, &result);
298    match mono_to_term(m, atoms) {
299        // constant terms: `add (lit c1)(lit c2)` ≡ `lit (c1+c2)` by computation.
300        None => Some(refl(lit_t(sum))),
301        Some(m_term) => {
302            // add t1 t2 = add (mul c1 M)(mul c2 M)  [coerce]  = mul (c1+c2) M  [rev_distrib]
303            let coerce = cong2("add", &t1, &ax2("mul", lit_t(c1), m_term.clone()),
304                &t2, &ax2("mul", lit_t(c2), m_term.clone()),
305                as_scaled_mul(c1, &m_term), as_scaled_mul(c2, &m_term));
306            let rd = rev_distrib(c1, c2, &m_term);
307            let coerced = ax2("add", ax2("mul", lit_t(c1), m_term.clone()), ax2("mul", lit_t(c2), m_term.clone()));
308            if sum != 1 {
309                Some(eq_trans(ax2("add", t1, t2), coerced, result, coerce, rd))
310            } else {
311                // c1+c2 == 1: the result is the bare monomial, so extend the chain
312                // past `mul 1 M` (what rev_distrib's RHS reduces to) with
313                // `mul 1 M = mul M 1 = M`.
314                let mul1m = ax2("mul", lit_t(1), m_term.clone());
315                let to_bare = eq_trans(
316                    mul1m.clone(),
317                    ax2("mul", m_term.clone(), lit_t(1)),
318                    m_term.clone(),
319                    ax2("mul_comm", lit_t(1), m_term.clone()),
320                    ax1("mul_one", m_term.clone()),
321                );
322                let inner = eq_trans(coerced.clone(), mul1m, result.clone(), rd, to_bare);
323                Some(eq_trans(ax2("add", t1, t2), coerced, result, coerce, inner))
324            }
325        }
326    }
327}
328
329/// Proof `add (add X Y) Z = add (add X Z) Y` (move `Z` past `Y`).
330fn swap_top(x: Term, y: Term, z: Term) -> Term {
331    // add(add X Y)Z = add X (add Y Z) = add X (add Z Y) = add(add X Z)Y
332    let s1 = ax3("add_assoc", x.clone(), y.clone(), z.clone());
333    let s2 = cong2(
334        "add",
335        &x,
336        &x,
337        &ax2("add", y.clone(), z.clone()),
338        &ax2("add", z.clone(), y.clone()),
339        refl(x.clone()),
340        ax2("add_comm", y.clone(), z.clone()),
341    );
342    let s3 = eq_sym(
343        ax2("add", ax2("add", x.clone(), z.clone()), y.clone()),
344        ax2("add", x.clone(), ax2("add", z.clone(), y.clone())),
345        ax3("add_assoc", x.clone(), z.clone(), y.clone()),
346    );
347    eq_trans(
348        ax2("add", ax2("add", x.clone(), y.clone()), z.clone()),
349        ax2("add", x.clone(), ax2("add", y.clone(), z.clone())),
350        ax2("add", ax2("add", x.clone(), z.clone()), y.clone()),
351        s1,
352        eq_trans(
353            ax2("add", x.clone(), ax2("add", y.clone(), z.clone())),
354            ax2("add", x.clone(), ax2("add", z.clone(), y.clone())),
355            ax2("add", ax2("add", x.clone(), z.clone()), y),
356            s2,
357            s3,
358        ),
359    )
360}
361
362/// Insert one `(mono, coeff)` term into a canonical poly `p`, returning
363/// `(result_poly, proof : add (reify p) (scaled term) = reify(result))`.
364fn merge_term(atoms: &[Term], p: &[(Mono, i64)], m: &[usize], c: i64) -> Option<(Poly, Term)> {
365    let st = scaled_term(m, c, atoms);
366    if p.is_empty() {
367        // add (lit 0) st = add st 0 = st
368        let proof = eq_trans(
369            ax2("add", lit_t(0), st.clone()),
370            ax2("add", st.clone(), lit_t(0)),
371            st.clone(),
372            ax2("add_comm", lit_t(0), st.clone()),
373            ax1("add_zero", st.clone()),
374        );
375        return Some((vec![(m.to_vec(), c)], proof));
376    }
377    let (ml, cl) = p.last().unwrap().clone();
378    let init = &p[..p.len() - 1];
379    let last_t = scaled_term(&ml, cl, atoms);
380    let reify_p = reify(p, atoms);
381
382    use std::cmp::Ordering;
383    match m.to_vec().cmp(&ml) {
384        Ordering::Greater => {
385            // already sorted: structurally reify(p ++ [term])
386            let mut res = p.to_vec();
387            res.push((m.to_vec(), c));
388            Some((res, refl(ax2("add", reify_p, st))))
389        }
390        Ordering::Equal => {
391            if cl + c == 0 {
392                // The monomial cancels (`cl·M + c·M = 0`), so it drops from the
393                // canonical form. `combine_coeff` gives `add last_t st = mul 0 M`;
394                // chain `mul 0 M = mul M 0 (mul_comm) = 0 (mul_zero)`.
395                let cc = combine_coeff(&ml, cl, c, atoms)?;
396                let cancel = match mono_to_term(&ml, atoms) {
397                    None => cc, // constant monomial: `scaled(ml, 0)` is already `lit 0`
398                    Some(m_term) => eq_trans(
399                        ax2("add", last_t.clone(), st.clone()),
400                        ax2("mul", lit_t(0), m_term.clone()),
401                        lit_t(0),
402                        cc,
403                        eq_trans(
404                            ax2("mul", lit_t(0), m_term.clone()),
405                            ax2("mul", m_term.clone(), lit_t(0)),
406                            lit_t(0),
407                            ax2("mul_comm", lit_t(0), m_term.clone()),
408                            ax1("mul_zero", m_term),
409                        ),
410                    ),
411                };
412                // cancel : add last_t st = 0
413                if init.is_empty() {
414                    return Some((vec![], cancel));
415                }
416                let ri = reify(init, atoms);
417                let assoc = ax3("add_assoc", ri.clone(), last_t.clone(), st.clone());
418                let cong = cong2(
419                    "add",
420                    &ri,
421                    &ri,
422                    &ax2("add", last_t.clone(), st.clone()),
423                    &lit_t(0),
424                    refl(ri.clone()),
425                    cancel,
426                );
427                let azero = ax1("add_zero", ri.clone());
428                let proof = eq_trans(
429                    ax2("add", ax2("add", ri.clone(), last_t.clone()), st.clone()),
430                    ax2("add", ri.clone(), ax2("add", last_t.clone(), st.clone())),
431                    ri.clone(),
432                    assoc,
433                    eq_trans(
434                        ax2("add", ri.clone(), ax2("add", last_t.clone(), st.clone())),
435                        ax2("add", ri.clone(), lit_t(0)),
436                        ri.clone(),
437                        cong,
438                        azero,
439                    ),
440                );
441                return Some((init.to_vec(), proof));
442            }
443            let cc = combine_coeff(&ml, cl, c, atoms)?; // add last_t st = scaled(ml, cl+c)
444            let combined = scaled_term(&ml, cl + c, atoms);
445            if init.is_empty() {
446                Some((vec![(ml, cl + c)], cc))
447            } else {
448                let ri = reify(init, atoms);
449                let assoc = ax3("add_assoc", ri.clone(), last_t.clone(), st.clone());
450                let cong = cong2(
451                    "add",
452                    &ri,
453                    &ri,
454                    &ax2("add", last_t.clone(), st.clone()),
455                    &combined,
456                    refl(ri.clone()),
457                    cc,
458                );
459                let proof = eq_trans(
460                    ax2("add", ax2("add", ri.clone(), last_t), st),
461                    ax2("add", ri.clone(), ax2("add", scaled_term(&ml, cl, atoms), scaled_term(m, c, atoms))),
462                    ax2("add", ri.clone(), combined),
463                    assoc,
464                    cong,
465                );
466                let mut res = init.to_vec();
467                res.push((ml, cl + c));
468                Some((res, proof))
469            }
470        }
471        Ordering::Less => {
472            if init.is_empty() {
473                // add last_t st = add st last_t
474                let mut res = vec![(m.to_vec(), c)];
475                res.push((ml, cl));
476                Some((res, ax2("add_comm", last_t, st)))
477            } else {
478                let ri = reify(init, atoms);
479                let swap = swap_top(ri.clone(), last_t.clone(), st.clone());
480                let (init2, inner) = merge_term(atoms, init, m, c)?; // add ri st = reify(init2)
481                let ri2 = reify(&init2, atoms);
482                let cong = cong2(
483                    "add",
484                    &ax2("add", ri.clone(), st.clone()),
485                    &ri2,
486                    &last_t,
487                    &last_t,
488                    inner,
489                    refl(last_t.clone()),
490                );
491                let proof = eq_trans(
492                    ax2("add", ax2("add", ri.clone(), last_t.clone()), st.clone()),
493                    ax2("add", ax2("add", ri, st.clone()), last_t.clone()),
494                    ax2("add", ri2.clone(), last_t.clone()),
495                    swap,
496                    cong,
497                );
498                // If the merge cancelled all of `init`, the result reifies to the
499                // bare `last_t` — eliminate the `add 0 last_t` residue so the
500                // proof's conclusion IS the canonical form.
501                let proof = if init2.is_empty() {
502                    let zfix = eq_trans(
503                        ax2("add", lit_t(0), last_t.clone()),
504                        ax2("add", last_t.clone(), lit_t(0)),
505                        last_t.clone(),
506                        ax2("add_comm", lit_t(0), last_t.clone()),
507                        ax1("add_zero", last_t.clone()),
508                    );
509                    eq_trans(
510                        ax2("add", ax2("add", reify(init, atoms), last_t.clone()), st.clone()),
511                        ax2("add", ri2, last_t.clone()),
512                        last_t.clone(),
513                        proof,
514                        zfix,
515                    )
516                } else {
517                    proof
518                };
519                let mut res = init2;
520                res.push((ml, cl));
521                Some((res, proof))
522            }
523        }
524    }
525}
526
527/// Merge two canonical polynomials, returning
528/// `(merged, proof : add (reify pa)(reify pb) = reify(merged))`.
529fn merge_canonical(atoms: &[Term], pa: &[(Mono, i64)], pb: &[(Mono, i64)]) -> Option<(Poly, Term)> {
530    let ra = reify(pa, atoms);
531    if pb.is_empty() {
532        // add (reify pa) 0 = reify pa
533        return Some((pa.to_vec(), ax1("add_zero", ra)));
534    }
535    if pb.len() == 1 {
536        let (m, c) = &pb[0];
537        return merge_term(atoms, pa, m, *c);
538    }
539    let (ml, cl) = pb.last().unwrap().clone();
540    let pb_init = &pb[..pb.len() - 1];
541    let rbi = reify(pb_init, atoms);
542    let slast = scaled_term(&ml, cl, atoms);
543    // add ra (add rbi slast) = add (add ra rbi) slast
544    let assoc_sym = eq_sym(
545        ax2("add", ax2("add", ra.clone(), rbi.clone()), slast.clone()),
546        ax2("add", ra.clone(), ax2("add", rbi.clone(), slast.clone())),
547        ax3("add_assoc", ra.clone(), rbi.clone(), slast.clone()),
548    );
549    let (m1, p1) = merge_canonical(atoms, pa, pb_init)?; // add ra rbi = reify(m1)
550    let rm1 = reify(&m1, atoms);
551    let cong = cong2(
552        "add",
553        &ax2("add", ra.clone(), rbi.clone()),
554        &rm1,
555        &slast,
556        &slast,
557        p1,
558        refl(slast.clone()),
559    );
560    let (m2, p2) = merge_term(atoms, &m1, &ml, cl)?; // add rm1 slast = reify(m2)
561    let rm2 = reify(&m2, atoms);
562    let proof = eq_trans(
563        ax2("add", ra.clone(), ax2("add", rbi.clone(), slast.clone())),
564        ax2("add", ax2("add", ra, rbi), slast.clone()),
565        rm2,
566        assoc_sym,
567        eq_trans(
568            ax2("add", ax2("add", reify(pa, atoms), reify(pb_init, atoms)), slast.clone()),
569            ax2("add", rm1, slast),
570            reify(&m2, atoms),
571            cong,
572            p2,
573        ),
574    );
575    Some((m2, proof))
576}
577
578/// Distribute `mul ca cb` (canonical terms), returning
579/// `(product_poly, proof : mul ca cb = reify(product))`.
580fn dist_mul(ctx: &Context, polys: &mut Polynomials, ca: &Term, cb: &Term) -> Option<(Poly, Term)> {
581    if let Some((cb1, cb2)) = match_bin(cb, "add") {
582        // mul ca (add cb1 cb2) = add (mul ca cb1)(mul ca cb2)
583        let distrib = ax3("mul_distrib_add", ca.clone(), cb1.clone(), cb2.clone());
584        let (pp1, d1) = dist_mul(ctx, polys, ca, &cb1)?;
585        let (pp2, d2) = dist_mul(ctx, polys, ca, &cb2)?;
586        let rp1 = reify(&pp1, &polys.atoms);
587        let rp2 = reify(&pp2, &polys.atoms);
588        let cong = cong2(
589            "add",
590            &ax2("mul", ca.clone(), cb1.clone()),
591            &rp1,
592            &ax2("mul", ca.clone(), cb2.clone()),
593            &rp2,
594            d1,
595            d2,
596        );
597        let (pm, mproof) = merge_canonical(&polys.atoms, &pp1, &pp2)?;
598        let rpm = reify(&pm, &polys.atoms);
599        let proof = eq_trans(
600            ax2("mul", ca.clone(), cb.clone()),
601            ax2("add", ax2("mul", ca.clone(), cb1.clone()), ax2("mul", ca.clone(), cb2.clone())),
602            rpm,
603            distrib,
604            eq_trans(
605                ax2("add", ax2("mul", ca.clone(), cb1), ax2("mul", ca.clone(), cb2)),
606                ax2("add", rp1, rp2),
607                reify(&pm, &polys.atoms),
608                cong,
609                mproof,
610            ),
611        );
612        return Some((pm, proof));
613    }
614    if let Some((_ca1, _ca2)) = match_bin(ca, "add") {
615        // mul (sum) cb = mul cb (sum) then distribute
616        let comm = ax2("mul_comm", ca.clone(), cb.clone());
617        let (pm, inner) = dist_mul(ctx, polys, cb, ca)?; // mul cb ca = reify(pm)
618        let rpm = reify(&pm, &polys.atoms);
619        return Some((
620            pm,
621            eq_trans(ax2("mul", ca.clone(), cb.clone()), ax2("mul", cb.clone(), ca.clone()), rpm, comm, inner),
622        ));
623    }
624    // both monomials: a single product; let the bounded search canonicalize it.
625    let prod = ax2("mul", ca.clone(), cb.clone());
626    let pp = to_poly(polys, ctx, &prod);
627    let c = reify(&pp, &polys.atoms);
628    let proof = prove_eq(ctx, &prod, &c, MAX_REWRITE_DEPTH)?;
629    Some((pp, proof))
630}
631
632/// `norm(t)` → `(canonical_term, proof : Eq Int t canonical_term)`, or `None`.
633fn norm(ctx: &Context, polys: &mut Polynomials, t: &Term) -> Option<(Term, Term)> {
634    if let Some((a, b)) = match_bin(t, "add") {
635        let (ca, pa) = norm(ctx, polys, &a)?;
636        let (cb, pb) = norm(ctx, polys, &b)?;
637        let pa_poly = to_poly(polys, ctx, &a);
638        let pb_poly = to_poly(polys, ctx, &b);
639        let cong = cong2("add", &a, &ca, &b, &cb, pa, pb); // add a b = add ca cb
640        let (merged, merge) = merge_canonical(&polys.atoms, &pa_poly, &pb_poly)?;
641        let c = reify(&merged, &polys.atoms);
642        return Some((c.clone(), eq_trans(t.clone(), ax2("add", ca, cb), c, cong, merge)));
643    }
644    if let Some((a, b)) = match_bin(t, "mul") {
645        let (ca, pa) = norm(ctx, polys, &a)?;
646        let (cb, pb) = norm(ctx, polys, &b)?;
647        let cong = cong2("mul", &a, &ca, &b, &cb, pa, pb); // mul a b = mul ca cb
648        let (pm, dproof) = dist_mul(ctx, polys, &ca, &cb)?;
649        let c = reify(&pm, &polys.atoms);
650        return Some((c.clone(), eq_trans(t.clone(), ax2("mul", ca, cb), c, cong, dproof)));
651    }
652    // atoms, literals: canonical form via the bounded search (handles nothing
653    // for a bare atom — refl — and is here for robustness).
654    let c = reify(&to_poly(polys, ctx, t), &polys.atoms);
655    let proof = prove_eq(ctx, t, &c, MAX_REWRITE_DEPTH)?;
656    Some((c, proof))
657}
658
659/// Prove `Eq Int lhs rhs` by normalizing both sides to the shared canonical form.
660fn prove_by_normalization(ctx: &Context, lhs: &Term, rhs: &Term) -> Option<Term> {
661    let mut polys = Polynomials { atoms: Vec::new() };
662    let (cl, pl) = norm(ctx, &mut polys, lhs)?; // lhs = cl
663    let (cr, pr) = norm(ctx, &mut polys, rhs)?; // rhs = cr
664    // The guard guarantees the polynomials match; their canonical terms are equal.
665    if cl != cr {
666        return None;
667    }
668    // lhs = cl = cr = rhs  ⇒  lhs = rhs
669    Some(eq_trans(lhs.clone(), cl, rhs.clone(), pl, eq_sym(rhs.clone(), cr, pr)))
670}
671
672/// Bound on the multi-step (Eq_trans) rewrite search. Congruence does not consume
673/// it (it recurses on strictly-smaller subterms), so this only limits same-size
674/// axiom-chaining — enough for the ring identities that arise, and total.
675const MAX_REWRITE_DEPTH: u32 = 6;
676
677fn prove_eq(ctx: &Context, lhs: &Term, rhs: &Term, depth: u32) -> Option<Term> {
678    // 1. Computation: if both sides reduce to the same term, `refl` closes it.
679    //    Covers all closed/literal arithmetic — zero axioms.
680    let nlhs = normalize(ctx, lhs);
681    let nrhs = normalize(ctx, rhs);
682    if nlhs == nrhs {
683        return Some(refl(nlhs));
684    }
685
686    // 2. A single oriented ring-axiom step (try both orientations).
687    if let Some(p) = match_axiom(ctx, lhs, rhs) {
688        return Some(p);
689    }
690    if let Some(p) = match_axiom(ctx, rhs, lhs) {
691        // proof : Eq Int rhs lhs  ⇒  Eq_sym … : Eq Int lhs rhs
692        return Some(eq_sym(rhs.clone(), lhs.clone(), p));
693    }
694
695    // 3. Congruence: `op a b = op a' b'` when `a=a'` and `b=b'` are each provable.
696    //    Recurses on strictly-smaller subterms, so it terminates.
697    for op in ["add", "mul", "sub"] {
698        if let (Some((la, lb)), Some((ra, rb))) = (match_bin(lhs, op), match_bin(rhs, op)) {
699            if let (Some(pa), Some(pb)) =
700                (prove_eq(ctx, &la, &ra, depth), prove_eq(ctx, &lb, &rb, depth))
701            {
702                return Some(cong2(op, &la, &ra, &lb, &rb, pa, pb));
703            }
704        }
705    }
706
707    // 4. Multi-step: rewrite lhs → mid by one forward axiom, prove `mid = rhs`,
708    //    and compose with `Eq_trans`. Handles identities needing a rewrite plus a
709    //    congruence (e.g. (x+y)+z = z+(y+x)). Depth-bounded ⇒ total.
710    if depth > 0 {
711        for (mid, p_lhs_mid) in forward_rewrites(lhs) {
712            if let Some(p_mid_rhs) = prove_eq(ctx, &mid, rhs, depth - 1) {
713                return Some(eq_trans(lhs.clone(), mid, rhs.clone(), p_lhs_mid, p_mid_rhs));
714            }
715        }
716    }
717
718    None
719}
720
721/// `Eq_trans Int x y z p1 p2` : from `p1 : x=y` and `p2 : y=z`, prove `x=z`.
722fn eq_trans(x: Term, y: Term, z: Term, p1: Term, p2: Term) -> Term {
723    app(
724        app(app(app(app(app(global("Eq_trans"), int()), x), y), z), p1),
725        p2,
726    )
727}
728
729/// Single-step forward ring rewrites of `l`: each `(l', proof : Eq Int l l')`.
730/// Only top-level rewrites; sub-term rewriting is covered by congruence.
731fn forward_rewrites(l: &Term) -> Vec<(Term, Term)> {
732    let g = global;
733    let mut out = Vec::new();
734    // add_comm : add a b → add b a
735    if let Some((a, b)) = match_bin(l, "add") {
736        out.push((
737            app2(g("add"), b.clone(), a.clone()),
738            app2(g("add_comm"), a.clone(), b.clone()),
739        ));
740        // add_assoc fwd : add (add a b) c → add a (add b c)
741        if let Some((a2, b2)) = match_bin(&a, "add") {
742            let c = b.clone();
743            out.push((
744                app2(g("add"), a2.clone(), app2(g("add"), b2.clone(), c.clone())),
745                app3(g("add_assoc"), a2.clone(), b2.clone(), c.clone()),
746            ));
747        }
748        // add_assoc rev : add a (add b c) → add (add a b) c
749        if let Some((b2, c2)) = match_bin(&b, "add") {
750            let lhs_a = app2(g("add"), app2(g("add"), a.clone(), b2.clone()), c2.clone());
751            let rhs_a = app2(g("add"), a.clone(), app2(g("add"), b2.clone(), c2.clone()));
752            out.push((
753                lhs_a.clone(),
754                eq_sym(lhs_a, rhs_a, app3(g("add_assoc"), a.clone(), b2.clone(), c2.clone())),
755            ));
756        }
757    }
758    // mul_comm : mul a b → mul b a
759    if let Some((a, b)) = match_bin(l, "mul") {
760        out.push((
761            app2(g("mul"), b.clone(), a.clone()),
762            app2(g("mul_comm"), a.clone(), b.clone()),
763        ));
764        // mul_assoc fwd : mul (mul a b) c → mul a (mul b c)
765        if let Some((a2, b2)) = match_bin(&a, "mul") {
766            let c = b.clone();
767            out.push((
768                app2(g("mul"), a2.clone(), app2(g("mul"), b2.clone(), c.clone())),
769                app3(g("mul_assoc"), a2.clone(), b2.clone(), c.clone()),
770            ));
771        }
772        // mul_distrib_add fwd : mul a (add b c) → add (mul a b) (mul a c)
773        if let Some((b2, c2)) = match_bin(&b, "add") {
774            out.push((
775                app2(g("add"), app2(g("mul"), a.clone(), b2.clone()), app2(g("mul"), a.clone(), c2.clone())),
776                app3(g("mul_distrib_add"), a.clone(), b2.clone(), c2.clone()),
777            ));
778        }
779    }
780    out
781}
782
783/// `Eq Int l r` as a term.
784fn eq_int_term(l: Term, r: Term) -> Term {
785    app(app(app(global("Eq"), int()), l), r)
786}
787
788/// `Eq_rec Int x P base y eqp` : rewrites `x` to `y` in `P` using `eqp : Eq Int x y`.
789fn eq_rec(x: Term, motive: Term, base: Term, y: Term, eqp: Term) -> Term {
790    app(
791        app(app(app(app(app(global("Eq_rec"), int()), x), motive), base), y),
792        eqp,
793    )
794}
795
796/// `λ(__w : Int). body`
797fn lam_int(body: Term) -> Term {
798    Term::Lambda {
799        param: "__w".to_string(),
800        param_type: Box::new(int()),
801        body: Box::new(body),
802    }
803}
804
805/// Congruence for a binary op: from `pa : a = a'` and `pb : b = b'`, build a
806/// proof of `Eq Int (op a b) (op a' b')` by two `Eq_rec` rewrites.
807fn cong2(op: &str, a: &Term, a2: &Term, b: &Term, b2: &Term, pa: Term, pb: Term) -> Term {
808    let opab = app2(global(op), a.clone(), b.clone());
809    let w = Term::Var("__w".to_string());
810
811    // step1 : Eq Int (op a b) (op a' b)   — rewrite a → a'
812    //   motive P1 = λw. Eq Int (op a b) (op w b)
813    let p1 = lam_int(eq_int_term(opab.clone(), app2(global(op), w.clone(), b.clone())));
814    let step1 = eq_rec(a.clone(), p1, refl(opab.clone()), a2.clone(), pa);
815
816    // step2 : Eq Int (op a b) (op a' b')  — rewrite b → b'
817    //   motive P2 = λw. Eq Int (op a b) (op a' w)
818    let p2 = lam_int(eq_int_term(opab.clone(), app2(global(op), a2.clone(), w)));
819    eq_rec(b.clone(), p2, step1, b2.clone(), pb)
820}
821
822/// One oriented ring-axiom application proving `Eq Int l r`, if `(l, r)` matches.
823fn match_axiom(ctx: &Context, l: &Term, r: &Term) -> Option<Term> {
824    // add_comm : l = add a b,  r = add b a
825    if let (Some((la, lb)), Some((ra, rb))) = (match_bin(l, "add"), match_bin(r, "add")) {
826        if conv(ctx, &la, &rb) && conv(ctx, &lb, &ra) {
827            return Some(app2(global("add_comm"), la, lb));
828        }
829    }
830    // mul_comm : l = mul a b,  r = mul b a
831    if let (Some((la, lb)), Some((ra, rb))) = (match_bin(l, "mul"), match_bin(r, "mul")) {
832        if conv(ctx, &la, &rb) && conv(ctx, &lb, &ra) {
833            return Some(app2(global("mul_comm"), la, lb));
834        }
835    }
836    // add_assoc : l = add (add a b) c,  r = add a (add b c)
837    if let Some((lab, lc)) = match_bin(l, "add") {
838        if let Some((la, lb)) = match_bin(&lab, "add") {
839            if let Some((ra, rbc)) = match_bin(r, "add") {
840                if let Some((rb, rc)) = match_bin(&rbc, "add") {
841                    if conv(ctx, &la, &ra) && conv(ctx, &lb, &rb) && conv(ctx, &lc, &rc) {
842                        return Some(app3(global("add_assoc"), la, lb, lc));
843                    }
844                }
845            }
846        }
847    }
848    // mul_assoc : l = mul (mul a b) c,  r = mul a (mul b c)
849    if let Some((lab, lc)) = match_bin(l, "mul") {
850        if let Some((la, lb)) = match_bin(&lab, "mul") {
851            if let Some((ra, rbc)) = match_bin(r, "mul") {
852                if let Some((rb, rc)) = match_bin(&rbc, "mul") {
853                    if conv(ctx, &la, &ra) && conv(ctx, &lb, &rb) && conv(ctx, &lc, &rc) {
854                        return Some(app3(global("mul_assoc"), la, lb, lc));
855                    }
856                }
857            }
858        }
859    }
860    // add_zero : l = add a 0,  r = a
861    if let Some((la, lb)) = match_bin(l, "add") {
862        if conv(ctx, &lb, &Term::Lit(logicaffeine_kernel::Literal::Int(0))) && conv(ctx, &la, r) {
863            return Some(app(global("add_zero"), la));
864        }
865    }
866    // mul_one : l = mul a 1,  r = a
867    if let Some((la, lb)) = match_bin(l, "mul") {
868        if conv(ctx, &lb, &Term::Lit(logicaffeine_kernel::Literal::Int(1))) && conv(ctx, &la, r) {
869            return Some(app(global("mul_one"), la));
870        }
871    }
872    // mul_distrib_add : l = mul a (add b c),  r = add (mul a b) (mul a c)
873    if let Some((a, bc)) = match_bin(l, "mul") {
874        if let Some((b, c)) = match_bin(&bc, "add") {
875            if let Some((rab, rac)) = match_bin(r, "add") {
876                if let (Some((ra1, rb1)), Some((ra2, rc1))) =
877                    (match_bin(&rab, "mul"), match_bin(&rac, "mul"))
878                {
879                    if conv(ctx, &a, &ra1)
880                        && conv(ctx, &a, &ra2)
881                        && conv(ctx, &b, &rb1)
882                        && conv(ctx, &c, &rc1)
883                    {
884                        return Some(app3(global("mul_distrib_add"), a, b, c));
885                    }
886                }
887            }
888        }
889    }
890    None
891}
892
893#[cfg(test)]
894mod tests {
895    use super::*;
896    use logicaffeine_kernel::{infer_type, prelude::StandardLibrary};
897
898    fn ctx() -> Context {
899        let mut c = Context::new();
900        StandardLibrary::register(&mut c);
901        c.add_declaration("x", int());
902        c.add_declaration("y", int());
903        c
904    }
905
906    /// The oracle must find a proof AND the kernel must accept it as `Eq Int lhs rhs`.
907    fn assert_certifies(ctx: &Context, lhs: &Term, rhs: &Term) {
908        let proof = prove_int_eq(ctx, lhs, rhs)
909            .unwrap_or_else(|| panic!("oracle found no proof for {lhs:?} = {rhs:?}"));
910        let ty = infer_type(ctx, &proof)
911            .unwrap_or_else(|e| panic!("kernel rejected the proof for {lhs:?} = {rhs:?}: {e:?}"));
912        let want = eq_int_term(lhs.clone(), rhs.clone());
913        assert!(
914            conv(ctx, &ty, &want),
915            "proof types as {ty:?}, wanted Eq Int {lhs:?} {rhs:?}"
916        );
917    }
918
919    fn add_t(a: Term, b: Term) -> Term {
920        ax2("add", a, b)
921    }
922    fn mul_t(a: Term, b: Term) -> Term {
923        ax2("mul", a, b)
924    }
925
926    #[test]
927    fn coefficients_summing_to_one_recombine() {
928        // The merge gap: like monomials whose coefficients sum to exactly 1 must
929        // recombine to the bare monomial (2x + (-1)x = x), not fail the proof.
930        let ctx = ctx();
931        let x = global("x");
932        assert_certifies(&ctx, &add_t(mul_t(lit_t(2), x.clone()), mul_t(lit_t(-1), x.clone())), &x);
933        assert_certifies(&ctx, &add_t(mul_t(lit_t(-1), x.clone()), mul_t(lit_t(2), x.clone())), &x);
934        assert_certifies(
935            &ctx,
936            &add_t(mul_t(lit_t(3), x.clone()), mul_t(lit_t(-2), x.clone())),
937            &x,
938        );
939    }
940
941    #[test]
942    fn farkas_shape_big_l_certifies() {
943        // cert_farkas's summed left side: Σ λᵢ·0 must certify equal to 0.
944        let ctx = ctx();
945        let big_l = add_t(mul_t(lit_t(1), lit_t(0)), mul_t(lit_t(1), lit_t(0)));
946        assert_certifies(&ctx, &big_l, &lit_t(0));
947    }
948
949    #[test]
950    fn farkas_shape_double_constant_big_r_certifies() {
951        // The exact BigR cert_farkas builds for the double-constant system
952        // x+1 ≤ y ∧ y+1 ≤ x+1 with multipliers λ = (1, 1):
953        //   1·(y − (x+1)) + 1·((x+1) − (y+1))  =  −1
954        // encoded sub-free as add(r, mul(−1, l)) per hypothesis.
955        let ctx = ctx();
956        let x = global("x");
957        let y = global("y");
958        let l1 = add_t(x.clone(), lit_t(1));
959        let r1 = y.clone();
960        let l2 = add_t(y.clone(), lit_t(1));
961        let r2 = add_t(x.clone(), lit_t(1));
962        let diff1 = add_t(r1, mul_t(lit_t(-1), l1));
963        let diff2 = add_t(r2, mul_t(lit_t(-1), l2));
964        let big_r = add_t(mul_t(lit_t(1), diff1), mul_t(lit_t(1), diff2));
965        assert_certifies(&ctx, &big_r, &lit_t(-1));
966    }
967}