Skip to main content

logicaffeine_compile/optimize/egraph/
rules.rs

1//! Rewrite rules (EXODIA Groups 1–3) with KERNEL-CHECKED soundness
2//! certificates (D11b): the compiler's e-graph is a sibling of the proof
3//! engine, not a stranger.
4//!
5//! # Soundness discipline
6//!
7//! Three obligations gate every rule, and all three fail closed:
8//!
9//! 1. **Type**: a rule fires only when the operand kinds are PROVEN
10//!    (literal nodes or Oracle facts). Float operands never rewrite —
11//!    `-0.0 + 0.0 == +0.0` already breaks `x + 0 → x` bit-for-bit.
12//! 2. **Error/effect preservation**: a rule that DELETES a subterm (stops
13//!    it being evaluated) requires [`CompilerEGraph::provably_total`] —
14//!    `(a / b) * 0 → 0` would erase the divide-by-zero. Short-circuit
15//!    forms (`false ∧ x → false`) are exempt for the RIGHT operand only,
16//!    because the runtime never evaluates it either.
17//! 3. **Value**: the identity itself carries a certificate checked through
18//!    `logicaffeine_kernel`:
19//!    - [`Certificate::Ring`] — polynomial identity via the kernel `ring`
20//!      tactic. Ring identities with integer coefficients hold in EVERY
21//!      commutative ring, in particular ℤ/2⁶⁴ = wrapping `i64`.
22//!    - [`Certificate::BoolCases`] — exhaustive Bool case analysis: both
23//!      sides are built as CIC terms over Match-defined `and/or/not` and
24//!      normalized BY THE KERNEL for every valuation.
25//!    - [`Certificate::Bitvector`] — wrapping/shift identities outside the
26//!      ring fragment, exhaustively checked on the adversarial i64
27//!      boundary grid. The full Z3 bitvector theorem lands with the Forge
28//!      SMT layer (M15); this grid is the standing mechanical check.
29
30use logicaffeine_kernel::{normalize, ring, Context, Literal as KLiteral, Term};
31
32use super::{CompilerEGraph, CompilerENode, NodeId};
33use crate::optimize::ScalarKind;
34
35pub struct Rewrite {
36    pub name: &'static str,
37    pub apply: fn(&mut CompilerEGraph, NodeId) -> Option<NodeId>,
38    pub certificate: Certificate,
39}
40
41// =====================================================================
42// Certificates
43// =====================================================================
44
45/// A tiny polynomial expression language reified into the kernel's
46/// `ring` tactic syntax.
47#[derive(Debug, Clone)]
48pub enum RingExpr {
49    Var(u8),
50    Const(i64),
51    Add(Box<RingExpr>, Box<RingExpr>),
52    Sub(Box<RingExpr>, Box<RingExpr>),
53    Mul(Box<RingExpr>, Box<RingExpr>),
54}
55
56/// A tiny boolean expression language evaluated through kernel
57/// normalization (Match-defined connectives over the prelude's Bool).
58#[derive(Debug, Clone)]
59pub enum BoolExpr {
60    Var(u8),
61    Const(bool),
62    And(Box<BoolExpr>, Box<BoolExpr>),
63    Or(Box<BoolExpr>, Box<BoolExpr>),
64    Not(Box<BoolExpr>),
65}
66
67pub enum Certificate {
68    /// `lhs = rhs` as polynomials — kernel `ring` tactic.
69    Ring { lhs: RingExpr, rhs: RingExpr },
70    /// `lhs = rhs` for all Bool valuations of `vars` variables — each side
71    /// normalized by the kernel.
72    BoolCases { vars: u8, lhs: BoolExpr, rhs: BoolExpr },
73    /// Wrapping-i64 identity checked exhaustively on the boundary grid by
74    /// the named check function (upgraded to a Z3 proof in M15).
75    Bitvector { check: fn() -> Result<(), String> },
76    /// An executable LOGOS property program (D11b — the language is its
77    /// own test substrate): the program generates a deterministic corpus,
78    /// compares the rule's two sides over it, and Shows its failure
79    /// count. Certification = the tree-walker printing `expected`.
80    LogosProperty { program: &'static str, expected: &'static str },
81}
82
83// ----- Ring reification ------------------------------------------------
84
85fn s_lit(n: i64) -> Term {
86    Term::App(
87        Box::new(Term::Global("SLit".to_string())),
88        Box::new(Term::Lit(KLiteral::Int(n))),
89    )
90}
91
92fn s_var(i: i64) -> Term {
93    Term::App(
94        Box::new(Term::Global("SVar".to_string())),
95        Box::new(Term::Lit(KLiteral::Int(i))),
96    )
97}
98
99fn s_name(name: &str) -> Term {
100    Term::App(
101        Box::new(Term::Global("SName".to_string())),
102        Box::new(Term::Lit(KLiteral::Text(name.to_string()))),
103    )
104}
105
106fn s_app(f: Term, x: Term) -> Term {
107    Term::App(
108        Box::new(Term::App(Box::new(Term::Global("SApp".to_string())), Box::new(f))),
109        Box::new(x),
110    )
111}
112
113fn s_binop(op: &str, a: Term, b: Term) -> Term {
114    s_app(s_app(s_name(op), a), b)
115}
116
117fn ring_to_syntax(e: &RingExpr) -> Term {
118    match e {
119        RingExpr::Var(i) => s_var(*i as i64),
120        RingExpr::Const(k) => s_lit(*k),
121        RingExpr::Add(a, b) => s_binop("add", ring_to_syntax(a), ring_to_syntax(b)),
122        RingExpr::Sub(a, b) => s_binop("sub", ring_to_syntax(a), ring_to_syntax(b)),
123        RingExpr::Mul(a, b) => s_binop("mul", ring_to_syntax(a), ring_to_syntax(b)),
124    }
125}
126
127fn check_ring(lhs: &RingExpr, rhs: &RingExpr) -> Result<(), String> {
128    let mut vars = logicaffeine_kernel::VarInterner::new();
129    let pl = ring::reify(&ring_to_syntax(lhs), &mut vars)
130        .map_err(|e| format!("ring reify (lhs): {e:?}"))?;
131    let pr = ring::reify(&ring_to_syntax(rhs), &mut vars)
132        .map_err(|e| format!("ring reify (rhs): {e:?}"))?;
133    if pl.canonical_eq(&pr) {
134        Ok(())
135    } else {
136        Err("polynomials differ".to_string())
137    }
138}
139
140// ----- Bool case analysis through kernel normalization ------------------
141
142fn k_bool(b: bool) -> Term {
143    Term::Global(if b { "true" } else { "false" }.to_string())
144}
145
146fn match_bool(disc: Term, on_true: Term, on_false: Term) -> Term {
147    Term::Match {
148        discriminant: Box::new(disc),
149        motive: Box::new(Term::Lambda {
150            param: "_b".to_string(),
151            param_type: Box::new(Term::Global("Bool".to_string())),
152            body: Box::new(Term::Global("Bool".to_string())),
153        }),
154        cases: vec![on_true, on_false],
155    }
156}
157
158/// Build the kernel term for `e` under a concrete valuation. The
159/// connectives are the textbook Match definitions:
160/// `and a b = match a with true ⇒ b | false ⇒ false`, etc. — evaluation
161/// is entirely the kernel's iota reduction.
162fn bool_to_term(e: &BoolExpr, valuation: &[bool]) -> Term {
163    match e {
164        BoolExpr::Var(i) => k_bool(valuation[*i as usize]),
165        BoolExpr::Const(b) => k_bool(*b),
166        BoolExpr::And(a, b) => match_bool(
167            bool_to_term(a, valuation),
168            bool_to_term(b, valuation),
169            k_bool(false),
170        ),
171        BoolExpr::Or(a, b) => match_bool(
172            bool_to_term(a, valuation),
173            k_bool(true),
174            bool_to_term(b, valuation),
175        ),
176        BoolExpr::Not(a) => match_bool(bool_to_term(a, valuation), k_bool(false), k_bool(true)),
177    }
178}
179
180fn check_bool_cases(vars: u8, lhs: &BoolExpr, rhs: &BoolExpr) -> Result<(), String> {
181    let mut ctx = Context::new();
182    logicaffeine_kernel::prelude::StandardLibrary::register(&mut ctx);
183    for bits in 0..(1u32 << vars) {
184        let valuation: Vec<bool> = (0..vars).map(|i| bits & (1 << i) != 0).collect();
185        let nl = normalize(&ctx, &bool_to_term(lhs, &valuation));
186        let nr = normalize(&ctx, &bool_to_term(rhs, &valuation));
187        if nl != nr {
188            return Err(format!("valuation {valuation:?}: {nl:?} ≠ {nr:?}"));
189        }
190    }
191    Ok(())
192}
193
194// ----- Bitvector boundary grid ------------------------------------------
195
196/// Adversarial i64 boundary values — overflow rims, sign boundaries,
197/// shift-width rims, small primes.
198const GRID: &[i64] = &[
199    i64::MIN,
200    i64::MIN + 1,
201    i64::MIN / 2,
202    -4_294_967_296,
203    -65_537,
204    -255,
205    -65,
206    -64,
207    -63,
208    -9,
209    -8,
210    -7,
211    -3,
212    -2,
213    -1,
214    0,
215    1,
216    2,
217    3,
218    7,
219    8,
220    9,
221    63,
222    64,
223    65,
224    255,
225    65_537,
226    4_294_967_296,
227    i64::MAX / 2,
228    i64::MAX - 1,
229    i64::MAX,
230];
231
232const POWERS: &[u32] = &[0, 1, 2, 3, 5, 16, 31, 32, 62];
233
234fn bv_mul_pow2_is_shl() -> Result<(), String> {
235    for &x in GRID {
236        for &n in POWERS {
237            let p = 1i64.wrapping_shl(n);
238            let mul = x.wrapping_mul(p);
239            let shl = x.wrapping_shl(n);
240            if mul != shl {
241                return Err(format!("{x} * 2^{n}: mul {mul} ≠ shl {shl}"));
242            }
243        }
244    }
245    Ok(())
246}
247
248fn bv_div_pow2_is_shr_nonneg() -> Result<(), String> {
249    for &x in GRID.iter().filter(|&&x| x >= 0) {
250        for &n in POWERS {
251            let p = 1i64.wrapping_shl(n);
252            if p <= 0 {
253                continue;
254            }
255            let div = x.wrapping_div(p);
256            let shr = x.wrapping_shr(n);
257            if div != shr {
258                return Err(format!("{x} / 2^{n}: div {div} ≠ shr {shr}"));
259            }
260        }
261    }
262    // The guard is REQUIRED: witness that negatives disagree.
263    if (-7i64).wrapping_div(4) == (-7i64).wrapping_shr(2) {
264        return Err("guard witness failed: -7/4 should differ from -7>>2".to_string());
265    }
266    Ok(())
267}
268
269fn bv_mod_pow2_is_and_nonneg() -> Result<(), String> {
270    for &x in GRID.iter().filter(|&&x| x >= 0) {
271        for &n in POWERS {
272            let p = 1i64.wrapping_shl(n);
273            if p <= 0 {
274                continue;
275            }
276            let md = x.wrapping_rem(p);
277            let masked = x & (p - 1);
278            if md != masked {
279                return Err(format!("{x} % 2^{n}: rem {md} ≠ mask {masked}"));
280            }
281        }
282    }
283    if (-7i64).wrapping_rem(4) == (-7i64 & 3) {
284        return Err("guard witness failed: -7%4 should differ from -7&3".to_string());
285    }
286    Ok(())
287}
288
289fn bv_div_one_is_identity() -> Result<(), String> {
290    for &x in GRID {
291        if x.wrapping_div(1) != x {
292            return Err(format!("{x} / 1 ≠ {x}"));
293        }
294    }
295    Ok(())
296}
297
298
299/// The constant-folder's evaluator, differentially checked against an
300/// independent transcription of the kernel's wrapping arithmetic over the
301/// full grid × grid, INCLUDING the error cases (zero divisors must refuse
302/// to fold on both sides).
303fn bv_fold_evaluator_matches_kernel() -> Result<(), String> {
304    for &a in GRID {
305        for &b in GRID {
306            // Exact spec: arithmetic folds ONLY when it fits i64 (overflow → None, so
307            // the promoting runtime computes the BigInt); div/mod also refuse a zero
308            // divisor. Bitwise/shift ops are total.
309            let cases: &[(&str, Option<i64>, Option<i64>)] = &[
310                ("add", fold_binop(FoldOp::Add, a, b), a.checked_add(b)),
311                ("sub", fold_binop(FoldOp::Sub, a, b), a.checked_sub(b)),
312                ("mul", fold_binop(FoldOp::Mul, a, b), a.checked_mul(b)),
313                (
314                    "div",
315                    fold_binop(FoldOp::Div, a, b),
316                    if b == 0 { None } else { a.checked_div(b) },
317                ),
318                (
319                    "mod",
320                    fold_binop(FoldOp::Mod, a, b),
321                    if b == 0 { None } else { a.checked_rem(b) },
322                ),
323                ("shl", fold_binop(FoldOp::Shl, a, b), Some(a.wrapping_shl(b as u32))),
324                ("shr", fold_binop(FoldOp::Shr, a, b), Some(a.wrapping_shr(b as u32))),
325                ("xor", fold_binop(FoldOp::Xor, a, b), Some(a ^ b)),
326                ("and", fold_binop(FoldOp::And, a, b), Some(a & b)),
327                ("or", fold_binop(FoldOp::Or, a, b), Some(a | b)),
328            ];
329            for (name, got, want) in cases {
330                if got != want {
331                    return Err(format!("fold {name}({a}, {b}): {got:?} ≠ {want:?}"));
332                }
333            }
334        }
335    }
336    Ok(())
337}
338
339// =====================================================================
340// The fold evaluator (shared by the const-fold rule and its certificate)
341// =====================================================================
342
343#[derive(Clone, Copy)]
344pub(crate) enum FoldOp {
345    Add,
346    Sub,
347    Mul,
348    Div,
349    Mod,
350    Shl,
351    Shr,
352    Xor,
353    And,
354    Or,
355}
356
357/// Kernel-exact arithmetic for the constant-folder. `None` = refuses to fold —
358/// either because the operation has no value (a zero divisor, whose runtime error
359/// is the program's meaning) OR because the result OVERFLOWS i64, in which case the
360/// exact (promoting) runtime must compute the BigInt rather than have us bake a
361/// wrapped constant. Bitwise/shift ops are total and always fold.
362pub(crate) fn fold_binop(op: FoldOp, a: i64, b: i64) -> Option<i64> {
363    match op {
364        FoldOp::Add => a.checked_add(b),
365        FoldOp::Sub => a.checked_sub(b),
366        FoldOp::Mul => a.checked_mul(b),
367        FoldOp::Div => {
368            if b == 0 {
369                return None;
370            }
371            a.checked_div(b)
372        }
373        FoldOp::Mod => {
374            if b == 0 {
375                return None;
376            }
377            a.checked_rem(b)
378        }
379        FoldOp::Shl => Some(a.wrapping_shl(b as u32)),
380        FoldOp::Shr => Some(a.wrapping_shr(b as u32)),
381        FoldOp::Xor => Some(a ^ b),
382        FoldOp::And => Some(a & b),
383        FoldOp::Or => Some(a | b),
384    }
385}
386
387// =====================================================================
388// Rule guards
389// =====================================================================
390
391fn is_int(eg: &mut CompilerEGraph, id: NodeId) -> bool {
392    eg.scalar_of(id) == Some(ScalarKind::Int)
393}
394
395fn is_bool(eg: &mut CompilerEGraph, id: NodeId) -> bool {
396    eg.scalar_of(id) == Some(ScalarKind::Bool)
397}
398
399/// May this operand be DELETED from the residual?
400fn removable(eg: &mut CompilerEGraph, id: NodeId) -> bool {
401    eg.provably_total(id)
402}
403
404/// The class is a proven zero / one (point interval — literals seed these,
405/// and the Oracle can prove them for variables too).
406fn proven(eg: &mut CompilerEGraph, id: NodeId, k: i64) -> bool {
407    eg.int_value(id) == Some(k)
408}
409
410// =====================================================================
411// Group 1 — algebraic identities (int-guarded)
412// =====================================================================
413
414fn r_add_zero(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
415    if let CompilerENode::Add(l, r) = eg.canonical_node(id) {
416        if proven(eg, r, 0) && is_int(eg, l) && removable(eg, r) {
417            return Some(l);
418        }
419        if proven(eg, l, 0) && is_int(eg, r) && removable(eg, l) {
420            return Some(r);
421        }
422    }
423    None
424}
425
426fn r_mul_one(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
427    if let CompilerENode::Mul(l, r) = eg.canonical_node(id) {
428        if proven(eg, r, 1) && is_int(eg, l) && removable(eg, r) {
429            return Some(l);
430        }
431        if proven(eg, l, 1) && is_int(eg, r) && removable(eg, l) {
432            return Some(r);
433        }
434    }
435    None
436}
437
438fn r_mul_zero(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
439    if let CompilerENode::Mul(l, r) = eg.canonical_node(id) {
440        let zero_side = if proven(eg, r, 0) {
441            Some((r, l))
442        } else if proven(eg, l, 0) {
443            Some((l, r))
444        } else {
445            None
446        };
447        if let Some((zero, other)) = zero_side {
448            // BOTH operands stop being evaluated.
449            if is_int(eg, other) && removable(eg, other) && removable(eg, zero) {
450                let z = eg.add(CompilerENode::Int(0));
451                return Some(z);
452            }
453        }
454    }
455    None
456}
457
458fn r_sub_zero(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
459    if let CompilerENode::Sub(l, r) = eg.canonical_node(id) {
460        if proven(eg, r, 0) && is_int(eg, l) && removable(eg, r) {
461            return Some(l);
462        }
463    }
464    None
465}
466
467fn r_sub_self(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
468    if let CompilerENode::Sub(l, r) = eg.canonical_node(id) {
469        if eg.find(l) == eg.find(r) && is_int(eg, l) && removable(eg, l) {
470            let z = eg.add(CompilerENode::Int(0));
471            return Some(z);
472        }
473    }
474    None
475}
476
477fn r_div_one(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
478    if let CompilerENode::Div(l, r) = eg.canonical_node(id) {
479        if proven(eg, r, 1) && is_int(eg, l) && removable(eg, r) {
480            return Some(l);
481        }
482    }
483    None
484}
485
486fn r_not_not_bool(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
487    if let CompilerENode::Not(inner) = eg.canonical_node(id) {
488        for m in eg.class_members(inner) {
489            if let CompilerENode::Not(x) = eg.canonical_node(m) {
490                if is_bool(eg, x) {
491                    return Some(x);
492                }
493            }
494        }
495    }
496    None
497}
498
499// (`not` is logical over truthiness: `Not(Not(x))` on an Int is `x != 0` as a
500// Bool, NOT `x` — so the only sound double-negation elimination is the
501// Bool-guarded rule above.)
502
503// =====================================================================
504// Group 2 — boolean simplification (bool-guarded; `And`/`Or` are
505// short-circuit in the language, so left-constant forms delete only the
506// never-evaluated RIGHT operand)
507// =====================================================================
508
509fn r_true_and(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
510    if let CompilerENode::And(l, r) = eg.canonical_node(id) {
511        if eg.class_has_bool(l, true) && is_bool(eg, r) && removable(eg, l) {
512            return Some(r);
513        }
514    }
515    None
516}
517
518fn r_false_and(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
519    if let CompilerENode::And(l, r) = eg.canonical_node(id) {
520        // Short-circuit: the runtime never evaluates r either, so only
521        // the deleted LEFT operand needs the totality proof.
522        if eg.class_has_bool(l, false) && is_bool(eg, r) && removable(eg, l) {
523            let f = eg.add(CompilerENode::Bool(false));
524            return Some(f);
525        }
526    }
527    None
528}
529
530fn r_true_or(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
531    if let CompilerENode::Or(l, r) = eg.canonical_node(id) {
532        if eg.class_has_bool(l, true) && is_bool(eg, r) && removable(eg, l) {
533            let t = eg.add(CompilerENode::Bool(true));
534            return Some(t);
535        }
536    }
537    None
538}
539
540fn r_false_or(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
541    if let CompilerENode::Or(l, r) = eg.canonical_node(id) {
542        if eg.class_has_bool(l, false) && is_bool(eg, r) && removable(eg, l) {
543            return Some(r);
544        }
545    }
546    None
547}
548
549fn r_and_self(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
550    if let CompilerENode::And(l, r) = eg.canonical_node(id) {
551        // The kept copy evaluates identically (same class, same first
552        // error) — no totality requirement.
553        if eg.find(l) == eg.find(r) && is_bool(eg, l) {
554            return Some(l);
555        }
556    }
557    None
558}
559
560fn r_or_self(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
561    if let CompilerENode::Or(l, r) = eg.canonical_node(id) {
562        if eg.find(l) == eg.find(r) && is_bool(eg, l) {
563            return Some(l);
564        }
565    }
566    None
567}
568
569/// Is some member of `maybe_not`'s class `Not(x)` with x ≡ `base`?
570fn class_negates(eg: &mut CompilerEGraph, maybe_not: NodeId, base: NodeId) -> bool {
571    let broot = eg.find(base);
572    for m in eg.class_members(maybe_not) {
573        if let CompilerENode::Not(x) = eg.canonical_node(m) {
574            if eg.find(x) == broot {
575                return true;
576            }
577        }
578    }
579    false
580}
581
582fn r_and_not_self(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
583    if let CompilerENode::And(l, r) = eg.canonical_node(id) {
584        if is_bool(eg, l)
585            && (class_negates(eg, r, l) || class_negates(eg, l, r))
586            && removable(eg, l)
587            && removable(eg, r)
588        {
589            let f = eg.add(CompilerENode::Bool(false));
590            return Some(f);
591        }
592    }
593    None
594}
595
596fn r_or_not_self(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
597    if let CompilerENode::Or(l, r) = eg.canonical_node(id) {
598        if is_bool(eg, l)
599            && (class_negates(eg, r, l) || class_negates(eg, l, r))
600            && removable(eg, l)
601            && removable(eg, r)
602        {
603            let t = eg.add(CompilerENode::Bool(true));
604            return Some(t);
605        }
606    }
607    None
608}
609
610// =====================================================================
611// Group 3 — strength reduction (Oracle-conditional)
612// =====================================================================
613
614fn pow2_log(k: i64) -> Option<u32> {
615    if k > 0 && (k as u64).is_power_of_two() {
616        Some(k.trailing_zeros())
617    } else {
618        None
619    }
620}
621
622fn r_mul_two_add(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
623    if let CompilerENode::Mul(l, r) = eg.canonical_node(id) {
624        let x = if proven(eg, r, 2) && removable(eg, r) {
625            Some(l)
626        } else if proven(eg, l, 2) && removable(eg, l) {
627            Some(r)
628        } else {
629            None
630        };
631        if let Some(x) = x {
632            if is_int(eg, x) {
633                let sum = eg.add(CompilerENode::Add(x, x));
634                return Some(sum);
635            }
636        }
637    }
638    None
639}
640
641/// `x * 2^n` provably stays within i64 for every `x` in its proven interval —
642/// the soundness precondition for replacing the (EXACT, BigInt-promoting)
643/// multiply with a (wrapping) left shift. `2^n` fits a positive i64 for every
644/// `n` `pow2_log` returns (`n ≤ 62`); the product's extremes sit at the
645/// interval ends, so checking both is sufficient.
646fn mul_pow2_fits_i64(range: Option<(i64, i64)>, n: u32) -> bool {
647    let Some((lo, hi)) = range else { return false };
648    if n >= 63 {
649        return false;
650    }
651    let m = 1i64 << n;
652    lo.checked_mul(m).is_some() && hi.checked_mul(m).is_some()
653}
654
655fn r_mul_pow2_shl(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
656    if let CompilerENode::Mul(l, r) = eg.canonical_node(id) {
657        let hit = if let Some(n) = eg.int_value(r).and_then(pow2_log) {
658            if removable(eg, r) { Some((l, n)) } else { None }
659        } else if let Some(n) = eg.int_value(l).and_then(pow2_log) {
660            if removable(eg, l) { Some((r, n)) } else { None }
661        } else {
662            None
663        };
664        if let Some((x, n)) = hit {
665            // ORACLE GATE: integer `*` is EXACT (promotes to BigInt on overflow)
666            // but `<<` WRAPS, so the rewrite is valid only where the Oracle's
667            // interval for `x` proves `x * 2^n` cannot escape i64. Mirrors the
668            // gate on `r_div_pow2_shr`; unproven cases keep the exact multiply
669            // (the backend still lowers it to a checked native `imul`).
670            if is_int(eg, x) && mul_pow2_fits_i64(eg.int_range(x), n) {
671                let shift = eg.add(CompilerENode::Int(n as i64));
672                let shl = eg.add(CompilerENode::Shl(x, shift));
673                return Some(shl);
674            }
675        }
676    }
677    None
678}
679
680fn r_div_pow2_shr(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
681    if let CompilerENode::Div(l, r) = eg.canonical_node(id) {
682        if let Some(n) = eg.int_value(r).and_then(pow2_log) {
683            // ORACLE GATE: truncating ÷ and arithmetic shift agree only
684            // for proven non-negative dividends.
685            if is_int(eg, l) && eg.proven_nonneg(l) && removable(eg, r) {
686                let shift = eg.add(CompilerENode::Int(n as i64));
687                let shr = eg.add(CompilerENode::Shr(l, shift));
688                return Some(shr);
689            }
690        }
691    }
692    None
693}
694
695fn r_mod_pow2_and(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
696    if let CompilerENode::Mod(l, r) = eg.canonical_node(id) {
697        if let Some(k) = eg.int_value(r) {
698            if pow2_log(k).is_some() && is_int(eg, l) && eg.proven_nonneg(l) && removable(eg, r) {
699                let mask = eg.add(CompilerENode::Int(k - 1));
700                let masked = eg.add(CompilerENode::BitAnd(l, mask));
701                return Some(masked);
702            }
703        }
704    }
705    None
706}
707
708// =====================================================================
709// Group 5: deforestation / fusion — the Len/Slice/Copy algebra
710// (Wadler 1988 at expression grain: intermediate collections that exist
711// only to be measured or read once never materialize)
712// =====================================================================
713
714/// A member of `class` matching `pred`, canonicalized.
715fn member_matching(
716    eg: &mut CompilerEGraph,
717    class: NodeId,
718    pred: fn(&CompilerENode) -> bool,
719) -> Option<CompilerENode> {
720    let members = eg.class_members(class);
721    members.into_iter().map(|m| eg.canonical_node(m)).find(pred)
722}
723
724/// `len(copy(xs))` → `len(xs)`: over a PROVEN collection the copy cannot
725/// raise and preserves length — the O(n) materialization disappears.
726fn r_len_copy(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
727    if let CompilerENode::Len(c) = eg.canonical_node(id) {
728        if let Some(CompilerENode::Copy(inner)) =
729            member_matching(eg, c, |n| matches!(n, CompilerENode::Copy(_)))
730        {
731            if eg.proven_collection(inner) {
732                return Some(eg.add(CompilerENode::Len(inner)));
733            }
734        }
735    }
736    None
737}
738
739/// `index(copy(xs), i)` → `index(xs, i)`: same contents, same bounds,
740/// same error — one read through a fresh copy is unobservable.
741fn r_index_copy(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
742    if let CompilerENode::Index(c, i) = eg.canonical_node(id) {
743        if let Some(CompilerENode::Copy(inner)) =
744            member_matching(eg, c, |n| matches!(n, CompilerENode::Copy(_)))
745        {
746            if eg.proven_collection(inner) {
747                return Some(eg.add(CompilerENode::Index(inner, i)));
748            }
749        }
750    }
751    None
752}
753
754/// `copy(copy(x))` ≡ `copy(x)` extensionally: both are fresh values with
755/// identical contents, and an erroring operand raises the SAME first
756/// error on both sides — unconditional.
757fn r_copy_copy(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
758    if let CompilerENode::Copy(c) = eg.canonical_node(id) {
759        if member_matching(eg, c, |n| matches!(n, CompilerENode::Copy(_))).is_some() {
760            return Some(eg.find(c));
761        }
762    }
763    None
764}
765
766/// `slice(xs, 1, len(xs))` ≡ `copy(xs)` for a PROVEN LIST: the full
767/// slice's clamps are no-ops and both sides are fresh. Gated on listness
768/// because slicing is list-shaped while copy accepts any collection.
769fn r_slice_full_copy(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
770    if let CompilerENode::Slice(xs, a, b) = eg.canonical_node(id) {
771        if eg.int_value(a) == Some(1) && eg.proven_list(xs) {
772            let len_match = member_matching(eg, b, |n| matches!(n, CompilerENode::Len(_)));
773            if let Some(CompilerENode::Len(c2)) = len_match {
774                if eg.find(c2) == eg.find(xs) {
775                    return Some(eg.add(CompilerENode::Copy(xs)));
776                }
777            }
778        }
779    }
780    None
781}
782
783/// `len(slice(xs, a, b))` → `b − a + 1` under PROOFS: constant bounds
784/// with 1 ≤ a, a ≤ b + 1 and b ≤ len(xs) guaranteed — the clamps cannot
785/// engage, so the length is exact and the whole slice (xs included)
786/// disappears. Deleting xs requires its tree total.
787fn r_len_slice_bounds(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
788    if let CompilerENode::Len(s) = eg.canonical_node(id) {
789        let slice = member_matching(eg, s, |n| matches!(n, CompilerENode::Slice(..)));
790        if let Some(CompilerENode::Slice(xs, a, b)) = slice {
791            let (Some(av), Some(bv)) = (eg.int_value(a), eg.int_value(b)) else {
792                return None;
793            };
794            if av < 1 || av > bv.saturating_add(1) || !eg.proven_list(xs) {
795                return None;
796            }
797            let len_class = eg.add(CompilerENode::Len(xs));
798            let Some((len_lo, _)) = eg.int_range(len_class) else {
799                return None;
800            };
801            if bv <= len_lo && removable(eg, xs) {
802                return Some(eg.add(CompilerENode::Int(bv - av + 1)));
803            }
804        }
805    }
806    None
807}
808
809// =====================================================================
810// Commutativity / associativity (int-only — float reassociation is
811// unsound, and `And`/`Or` short-circuit so they are NOT commutative
812// with respect to errors)
813// =====================================================================
814
815fn r_add_comm(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
816    if let CompilerENode::Add(l, r) = eg.canonical_node(id) {
817        // Swapping evaluation order is observable through error precedence
818        // unless both operands are total.
819        if is_int(eg, l) && is_int(eg, r) && removable(eg, l) && removable(eg, r) {
820            let flipped = eg.add(CompilerENode::Add(r, l));
821            return Some(flipped);
822        }
823    }
824    None
825}
826
827fn r_mul_comm(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
828    if let CompilerENode::Mul(l, r) = eg.canonical_node(id) {
829        if is_int(eg, l) && is_int(eg, r) && removable(eg, l) && removable(eg, r) {
830            let flipped = eg.add(CompilerENode::Mul(r, l));
831            return Some(flipped);
832        }
833    }
834    None
835}
836
837fn r_add_assoc(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
838    if let CompilerENode::Add(l, c) = eg.canonical_node(id) {
839        if !is_int(eg, c) {
840            return None;
841        }
842        for m in eg.class_members(l) {
843            if let CompilerENode::Add(a, b) = eg.canonical_node(m) {
844                if is_int(eg, a) && is_int(eg, b) {
845                    let bc = eg.add(CompilerENode::Add(b, c));
846                    let abc = eg.add(CompilerENode::Add(a, bc));
847                    return Some(abc);
848                }
849            }
850        }
851    }
852    None
853}
854
855fn r_mul_assoc(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
856    if let CompilerENode::Mul(l, c) = eg.canonical_node(id) {
857        if !is_int(eg, c) {
858            return None;
859        }
860        for m in eg.class_members(l) {
861            if let CompilerENode::Mul(a, b) = eg.canonical_node(m) {
862                if is_int(eg, a) && is_int(eg, b) {
863                    let bc = eg.add(CompilerENode::Mul(b, c));
864                    let abc = eg.add(CompilerENode::Mul(a, bc));
865                    return Some(abc);
866                }
867            }
868        }
869    }
870    None
871}
872
873/// Gauss / Karatsuba butterfly: `la·lb + ra·rb → (la+ra)(lb+rb) − la·rb − ra·lb`. The naive
874/// sum is two multiplies; the rewritten form spends one NEW multiply `(la+ra)(lb+rb)` plus the
875/// cross products `la·rb`, `ra·lb`. It is a *conditional* speedup — it pays only when those
876/// cross products are SHARED with a sibling (the real part `la·rb − ra·lb` of a complex / NTT
877/// butterfly), where the pair then costs 3 multiplies instead of 4. Adding the alternative
878/// here lets the cost extractor pick it exactly when the sharing makes it cheaper; the value is
879/// unchanged either way, because the rule's `Certificate::Ring` is kernel-proven (the NTT can
880/// never be miscompiled by it).
881fn r_gauss_butterfly(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
882    if let CompilerENode::Add(l, r) = eg.canonical_node(id) {
883        for lm in eg.class_members(l) {
884            if let CompilerENode::Mul(la, lb) = eg.canonical_node(lm) {
885                for rm in eg.class_members(r) {
886                    if let CompilerENode::Mul(ra, rb) = eg.canonical_node(rm) {
887                        if [la, lb, ra, rb].iter().all(|&x| is_int(eg, x) && removable(eg, x)) {
888                            let sum_a = eg.add(CompilerENode::Add(la, ra));
889                            let sum_b = eg.add(CompilerENode::Add(lb, rb));
890                            let prod = eg.add(CompilerENode::Mul(sum_a, sum_b));
891                            let cross1 = eg.add(CompilerENode::Mul(la, rb));
892                            let cross2 = eg.add(CompilerENode::Mul(ra, lb));
893                            let s1 = eg.add(CompilerENode::Sub(prod, cross1));
894                            let res = eg.add(CompilerENode::Sub(s1, cross2));
895                            return Some(res);
896                        }
897                    }
898                }
899            }
900        }
901    }
902    None
903}
904
905// =====================================================================
906// Constant folding (point intervals — subsumes literal folding and
907// extends to Oracle-proven single values)
908// =====================================================================
909
910fn r_const_fold(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
911    let node = eg.canonical_node(id);
912    let op = match node {
913        CompilerENode::Add(..) => FoldOp::Add,
914        CompilerENode::Sub(..) => FoldOp::Sub,
915        CompilerENode::Mul(..) => FoldOp::Mul,
916        CompilerENode::Div(..) => FoldOp::Div,
917        CompilerENode::Mod(..) => FoldOp::Mod,
918        CompilerENode::Shl(..) => FoldOp::Shl,
919        CompilerENode::Shr(..) => FoldOp::Shr,
920        CompilerENode::BitXor(..) => FoldOp::Xor,
921        CompilerENode::BitAnd(..) => FoldOp::And,
922        CompilerENode::BitOr(..) => FoldOp::Or,
923        _ => return None,
924    };
925    let (l, r) = match node {
926        CompilerENode::Add(l, r)
927        | CompilerENode::Sub(l, r)
928        | CompilerENode::Mul(l, r)
929        | CompilerENode::Div(l, r)
930        | CompilerENode::Mod(l, r)
931        | CompilerENode::Shl(l, r)
932        | CompilerENode::Shr(l, r)
933        | CompilerENode::BitXor(l, r)
934        | CompilerENode::BitAnd(l, r)
935        | CompilerENode::BitOr(l, r) => (l, r),
936        _ => unreachable!(),
937    };
938    if eg.find(id) == eg.find(l) || eg.find(id) == eg.find(r) {
939        // Cyclic class (id ≡ one of its own children) — folding would
940        // self-justify; leave it to extraction.
941        return None;
942    }
943    let a = eg.int_value(l)?;
944    let b = eg.int_value(r)?;
945    if !(removable(eg, l) && removable(eg, r)) {
946        return None;
947    }
948    let v = fold_binop(op, a, b)?;
949    let lit = eg.add(CompilerENode::Int(v));
950    Some(lit)
951}
952
953// =====================================================================
954// The registry
955// =====================================================================
956
957fn rx(e: RingExpr) -> Box<RingExpr> {
958    Box::new(e)
959}
960
961fn bx(e: BoolExpr) -> Box<BoolExpr> {
962    Box::new(e)
963}
964
965pub fn all() -> Vec<Rewrite> {
966    use BoolExpr as B;
967    use RingExpr as R;
968    vec![
969        Rewrite {
970            name: "add-zero",
971            apply: r_add_zero,
972            certificate: Certificate::Ring {
973                lhs: R::Add(rx(R::Var(0)), rx(R::Const(0))),
974                rhs: R::Var(0),
975            },
976        },
977        Rewrite {
978            name: "mul-one",
979            apply: r_mul_one,
980            certificate: Certificate::Ring {
981                lhs: R::Mul(rx(R::Var(0)), rx(R::Const(1))),
982                rhs: R::Var(0),
983            },
984        },
985        Rewrite {
986            name: "mul-zero",
987            apply: r_mul_zero,
988            certificate: Certificate::Ring {
989                lhs: R::Mul(rx(R::Var(0)), rx(R::Const(0))),
990                rhs: R::Const(0),
991            },
992        },
993        Rewrite {
994            name: "sub-zero",
995            apply: r_sub_zero,
996            certificate: Certificate::Ring {
997                lhs: R::Sub(rx(R::Var(0)), rx(R::Const(0))),
998                rhs: R::Var(0),
999            },
1000        },
1001        Rewrite {
1002            name: "sub-self",
1003            apply: r_sub_self,
1004            certificate: Certificate::Ring {
1005                lhs: R::Sub(rx(R::Var(0)), rx(R::Var(0))),
1006                rhs: R::Const(0),
1007            },
1008        },
1009        Rewrite {
1010            name: "gauss-butterfly",
1011            apply: r_gauss_butterfly,
1012            certificate: Certificate::Ring {
1013                // la·lb + ra·rb  =  (la+ra)(lb+rb) − la·rb − ra·lb   (la=V0, lb=V1, ra=V2, rb=V3)
1014                lhs: R::Add(
1015                    rx(R::Mul(rx(R::Var(0)), rx(R::Var(1)))),
1016                    rx(R::Mul(rx(R::Var(2)), rx(R::Var(3)))),
1017                ),
1018                rhs: R::Sub(
1019                    rx(R::Sub(
1020                        rx(R::Mul(
1021                            rx(R::Add(rx(R::Var(0)), rx(R::Var(2)))),
1022                            rx(R::Add(rx(R::Var(1)), rx(R::Var(3)))),
1023                        )),
1024                        rx(R::Mul(rx(R::Var(0)), rx(R::Var(3)))),
1025                    )),
1026                    rx(R::Mul(rx(R::Var(2)), rx(R::Var(1)))),
1027                ),
1028            },
1029        },
1030        Rewrite {
1031            name: "div-one",
1032            apply: r_div_one,
1033            certificate: Certificate::Bitvector { check: bv_div_one_is_identity },
1034        },
1035        Rewrite {
1036            name: "not-not-bool",
1037            apply: r_not_not_bool,
1038            certificate: Certificate::BoolCases {
1039                vars: 1,
1040                lhs: B::Not(bx(B::Not(bx(B::Var(0))))),
1041                rhs: B::Var(0),
1042            },
1043        },
1044        Rewrite {
1045            name: "true-and",
1046            apply: r_true_and,
1047            certificate: Certificate::BoolCases {
1048                vars: 1,
1049                lhs: B::And(bx(B::Const(true)), bx(B::Var(0))),
1050                rhs: B::Var(0),
1051            },
1052        },
1053        Rewrite {
1054            name: "false-and",
1055            apply: r_false_and,
1056            certificate: Certificate::BoolCases {
1057                vars: 1,
1058                lhs: B::And(bx(B::Const(false)), bx(B::Var(0))),
1059                rhs: B::Const(false),
1060            },
1061        },
1062        Rewrite {
1063            name: "true-or",
1064            apply: r_true_or,
1065            certificate: Certificate::BoolCases {
1066                vars: 1,
1067                lhs: B::Or(bx(B::Const(true)), bx(B::Var(0))),
1068                rhs: B::Const(true),
1069            },
1070        },
1071        Rewrite {
1072            name: "false-or",
1073            apply: r_false_or,
1074            certificate: Certificate::BoolCases {
1075                vars: 1,
1076                lhs: B::Or(bx(B::Const(false)), bx(B::Var(0))),
1077                rhs: B::Var(0),
1078            },
1079        },
1080        Rewrite {
1081            name: "and-self",
1082            apply: r_and_self,
1083            certificate: Certificate::BoolCases {
1084                vars: 1,
1085                lhs: B::And(bx(B::Var(0)), bx(B::Var(0))),
1086                rhs: B::Var(0),
1087            },
1088        },
1089        Rewrite {
1090            name: "or-self",
1091            apply: r_or_self,
1092            certificate: Certificate::BoolCases {
1093                vars: 1,
1094                lhs: B::Or(bx(B::Var(0)), bx(B::Var(0))),
1095                rhs: B::Var(0),
1096            },
1097        },
1098        Rewrite {
1099            name: "and-not-self",
1100            apply: r_and_not_self,
1101            certificate: Certificate::BoolCases {
1102                vars: 1,
1103                lhs: B::And(bx(B::Var(0)), bx(B::Not(bx(B::Var(0))))),
1104                rhs: B::Const(false),
1105            },
1106        },
1107        Rewrite {
1108            name: "or-not-self",
1109            apply: r_or_not_self,
1110            certificate: Certificate::BoolCases {
1111                vars: 1,
1112                lhs: B::Or(bx(B::Var(0)), bx(B::Not(bx(B::Var(0))))),
1113                rhs: B::Const(true),
1114            },
1115        },
1116        Rewrite {
1117            name: "mul-two-add",
1118            apply: r_mul_two_add,
1119            certificate: Certificate::Ring {
1120                lhs: R::Mul(rx(R::Var(0)), rx(R::Const(2))),
1121                rhs: R::Add(rx(R::Var(0)), rx(R::Var(0))),
1122            },
1123        },
1124        Rewrite {
1125            name: "mul-pow2-shl",
1126            apply: r_mul_pow2_shl,
1127            certificate: Certificate::Bitvector { check: bv_mul_pow2_is_shl },
1128        },
1129        Rewrite {
1130            name: "div-pow2-shr",
1131            apply: r_div_pow2_shr,
1132            certificate: Certificate::Bitvector { check: bv_div_pow2_is_shr_nonneg },
1133        },
1134        Rewrite {
1135            name: "mod-pow2-and",
1136            apply: r_mod_pow2_and,
1137            certificate: Certificate::Bitvector { check: bv_mod_pow2_is_and_nonneg },
1138        },
1139        Rewrite {
1140            name: "add-comm",
1141            apply: r_add_comm,
1142            certificate: Certificate::Ring {
1143                lhs: R::Add(rx(R::Var(0)), rx(R::Var(1))),
1144                rhs: R::Add(rx(R::Var(1)), rx(R::Var(0))),
1145            },
1146        },
1147        Rewrite {
1148            name: "add-assoc",
1149            apply: r_add_assoc,
1150            certificate: Certificate::Ring {
1151                lhs: R::Add(rx(R::Add(rx(R::Var(0)), rx(R::Var(1)))), rx(R::Var(2))),
1152                rhs: R::Add(rx(R::Var(0)), rx(R::Add(rx(R::Var(1)), rx(R::Var(2))))),
1153            },
1154        },
1155        Rewrite {
1156            name: "mul-comm",
1157            apply: r_mul_comm,
1158            certificate: Certificate::Ring {
1159                lhs: R::Mul(rx(R::Var(0)), rx(R::Var(1))),
1160                rhs: R::Mul(rx(R::Var(1)), rx(R::Var(0))),
1161            },
1162        },
1163        Rewrite {
1164            name: "mul-assoc",
1165            apply: r_mul_assoc,
1166            certificate: Certificate::Ring {
1167                lhs: R::Mul(rx(R::Mul(rx(R::Var(0)), rx(R::Var(1)))), rx(R::Var(2))),
1168                rhs: R::Mul(rx(R::Var(0)), rx(R::Mul(rx(R::Var(1)), rx(R::Var(2))))),
1169            },
1170        },
1171        Rewrite {
1172            name: "const-fold",
1173            apply: r_const_fold,
1174            certificate: Certificate::Bitvector { check: bv_fold_evaluator_matches_kernel },
1175        },
1176        Rewrite {
1177            name: "len-copy",
1178            apply: r_len_copy,
1179            certificate: Certificate::LogosProperty {
1180                program: PROP_LEN_COPY,
1181                expected: "0",
1182            },
1183        },
1184        Rewrite {
1185            name: "index-copy",
1186            apply: r_index_copy,
1187            certificate: Certificate::LogosProperty {
1188                program: PROP_INDEX_COPY,
1189                expected: "0",
1190            },
1191        },
1192        Rewrite {
1193            name: "copy-copy",
1194            apply: r_copy_copy,
1195            certificate: Certificate::LogosProperty {
1196                program: PROP_COPY_COPY,
1197                expected: "0",
1198            },
1199        },
1200        Rewrite {
1201            name: "slice-full-copy",
1202            apply: r_slice_full_copy,
1203            certificate: Certificate::LogosProperty {
1204                program: PROP_SLICE_FULL_COPY,
1205                expected: "0",
1206            },
1207        },
1208        Rewrite {
1209            name: "len-slice-bounds",
1210            apply: r_len_slice_bounds,
1211            certificate: Certificate::LogosProperty {
1212                program: PROP_LEN_SLICE_BOUNDS,
1213                expected: "0",
1214            },
1215        },
1216    ]
1217}
1218
1219// =====================================================================
1220// Group 5 property certificates: deterministic LCG corpora (lists of
1221// length 0..12 — the empty list and the a = b + 1 empty slice are in
1222// range by construction), each Showing its failure count.
1223// =====================================================================
1224
1225const PROP_LEN_COPY: &str = "## Main\n\
1226Let mutable seed be 42.\n\
1227Let mutable failures be 0.\n\
1228Let mutable t be 0.\n\
1229While t is less than 40:\n\
1230\x20   Let mutable xs be a new Seq of Int.\n\
1231\x20   Set seed to (seed * 1103515245 + 12345) % 2147483648.\n\
1232\x20   Let n be seed % 13.\n\
1233\x20   Let mutable i be 0.\n\
1234\x20   While i is less than n:\n\
1235\x20       Set seed to (seed * 1103515245 + 12345) % 2147483648.\n\
1236\x20       Push seed % 100 to xs.\n\
1237\x20       Set i to i + 1.\n\
1238\x20   If length of (copy of xs) is not length of xs:\n\
1239\x20       Set failures to failures + 1.\n\
1240\x20   Set t to t + 1.\n\
1241Show failures.\n";
1242
1243const PROP_INDEX_COPY: &str = "## Main\n\
1244Let mutable seed be 99.\n\
1245Let mutable failures be 0.\n\
1246Let mutable t be 0.\n\
1247While t is less than 40:\n\
1248\x20   Let mutable xs be a new Seq of Int.\n\
1249\x20   Set seed to (seed * 1103515245 + 12345) % 2147483648.\n\
1250\x20   Let n be seed % 13.\n\
1251\x20   Let mutable i be 0.\n\
1252\x20   While i is less than n:\n\
1253\x20       Set seed to (seed * 1103515245 + 12345) % 2147483648.\n\
1254\x20       Push seed % 100 to xs.\n\
1255\x20       Set i to i + 1.\n\
1256\x20   If n is at least 1:\n\
1257\x20       Set seed to (seed * 1103515245 + 12345) % 2147483648.\n\
1258\x20       Let k be seed % n + 1.\n\
1259\x20       If item k of (copy of xs) is not item k of xs:\n\
1260\x20           Set failures to failures + 1.\n\
1261\x20   Set t to t + 1.\n\
1262Show failures.\n";
1263
1264const PROP_COPY_COPY: &str = "## Main\n\
1265Let mutable seed be 7.\n\
1266Let mutable failures be 0.\n\
1267Let mutable t be 0.\n\
1268While t is less than 40:\n\
1269\x20   Let mutable xs be a new Seq of Int.\n\
1270\x20   Set seed to (seed * 1103515245 + 12345) % 2147483648.\n\
1271\x20   Let n be seed % 13.\n\
1272\x20   Let mutable i be 0.\n\
1273\x20   While i is less than n:\n\
1274\x20       Set seed to (seed * 1103515245 + 12345) % 2147483648.\n\
1275\x20       Push seed % 100 to xs.\n\
1276\x20       Set i to i + 1.\n\
1277\x20   Let a be copy of (copy of xs).\n\
1278\x20   Let b be copy of xs.\n\
1279\x20   If length of a is not length of b:\n\
1280\x20       Set failures to failures + 1.\n\
1281\x20   Let mutable j be 1.\n\
1282\x20   While j is at most length of a:\n\
1283\x20       If item j of a is not item j of b:\n\
1284\x20           Set failures to failures + 1.\n\
1285\x20       Set j to j + 1.\n\
1286\x20   Set t to t + 1.\n\
1287Show failures.\n";
1288
1289const PROP_SLICE_FULL_COPY: &str = "## Main\n\
1290Let mutable seed be 1234.\n\
1291Let mutable failures be 0.\n\
1292Let mutable t be 0.\n\
1293While t is less than 40:\n\
1294\x20   Let mutable xs be a new Seq of Int.\n\
1295\x20   Set seed to (seed * 1103515245 + 12345) % 2147483648.\n\
1296\x20   Let n be seed % 13.\n\
1297\x20   Let mutable i be 0.\n\
1298\x20   While i is less than n:\n\
1299\x20       Set seed to (seed * 1103515245 + 12345) % 2147483648.\n\
1300\x20       Push seed % 100 to xs.\n\
1301\x20       Set i to i + 1.\n\
1302\x20   Let s be items 1 through length of xs of xs.\n\
1303\x20   Let c be copy of xs.\n\
1304\x20   If length of s is not length of c:\n\
1305\x20       Set failures to failures + 1.\n\
1306\x20   Let mutable j be 1.\n\
1307\x20   While j is at most length of s:\n\
1308\x20       If item j of s is not item j of c:\n\
1309\x20           Set failures to failures + 1.\n\
1310\x20       Set j to j + 1.\n\
1311\x20   Set t to t + 1.\n\
1312Show failures.\n";
1313
1314const PROP_LEN_SLICE_BOUNDS: &str = "## Main\n\
1315Let mutable seed be 5150.\n\
1316Let mutable failures be 0.\n\
1317Let mutable t be 0.\n\
1318While t is less than 60:\n\
1319\x20   Let mutable xs be a new Seq of Int.\n\
1320\x20   Set seed to (seed * 1103515245 + 12345) % 2147483648.\n\
1321\x20   Let n be seed % 12 + 1.\n\
1322\x20   Let mutable i be 0.\n\
1323\x20   While i is less than n:\n\
1324\x20       Set seed to (seed * 1103515245 + 12345) % 2147483648.\n\
1325\x20       Push seed % 100 to xs.\n\
1326\x20       Set i to i + 1.\n\
1327\x20   Set seed to (seed * 1103515245 + 12345) % 2147483648.\n\
1328\x20   Let lo be seed % n + 1.\n\
1329\x20   Set seed to (seed * 1103515245 + 12345) % 2147483648.\n\
1330\x20   Let hi be lo - 1 + (seed % (n - lo + 2)).\n\
1331\x20   Let s be items (lo) through (hi) of xs.\n\
1332\x20   If length of s is not hi - lo + 1:\n\
1333\x20       Set failures to failures + 1.\n\
1334\x20   Set t to t + 1.\n\
1335Show failures.\n";
1336
1337/// Check every rule's certificate through the kernel. Returns the number
1338/// of verified rules; the first failure aborts with the rule's name.
1339pub fn verify_all_with_kernel() -> Result<usize, String> {
1340    let rules = all();
1341    for rule in &rules {
1342        let outcome = match &rule.certificate {
1343            Certificate::Ring { lhs, rhs } => check_ring(lhs, rhs),
1344            Certificate::BoolCases { vars, lhs, rhs } => check_bool_cases(*vars, lhs, rhs),
1345            Certificate::Bitvector { check } => check(),
1346            Certificate::LogosProperty { program, expected } => {
1347                match crate::compile::interpret_program(program) {
1348                    Ok(out) if out.trim() == *expected => Ok(()),
1349                    Ok(out) => Err(format!(
1350                        "property program printed {:?}, expected {:?}",
1351                        out.trim(),
1352                        expected
1353                    )),
1354                    Err(e) => Err(format!("property program failed: {e:?}")),
1355                }
1356            }
1357        };
1358        outcome.map_err(|e| format!("rule '{}': {e}", rule.name))?;
1359    }
1360    Ok(rules.len())
1361}
1362
1363#[cfg(test)]
1364mod tests {
1365    use super::*;
1366
1367    #[test]
1368    fn mul_pow2_fits_i64_is_the_exact_overflow_boundary() {
1369        // 2^3 = 8: bounded multiplicands fit; unbounded / unproven do not.
1370        assert!(mul_pow2_fits_i64(Some((0, 255)), 3));
1371        assert!(mul_pow2_fits_i64(Some((-100, 100)), 3));
1372        assert!(!mul_pow2_fits_i64(None, 3));
1373        assert!(!mul_pow2_fits_i64(Some((0, i64::MAX)), 3));
1374        assert!(!mul_pow2_fits_i64(Some((i64::MIN, 0)), 1));
1375        // The exact rim: ⌊i64::MAX / 8⌋ * 8 fits, one more overflows.
1376        assert!(mul_pow2_fits_i64(Some((0, i64::MAX / 8)), 3));
1377        assert!(!mul_pow2_fits_i64(Some((0, i64::MAX / 8 + 1)), 3));
1378        // n ≥ 63 can never be safe (2^63 escapes i64).
1379        assert!(!mul_pow2_fits_i64(Some((1, 1)), 63));
1380    }
1381
1382    fn mul_by_eight(lo: i64, hi: i64) -> bool {
1383        let mut eg = CompilerEGraph::new();
1384        let x = eg.add(CompilerENode::Var(0, 0));
1385        eg.set_scalar(x, ScalarKind::Int);
1386        eg.set_int_range(x, lo, hi);
1387        let eight = eg.add(CompilerENode::Int(8));
1388        let mul = eg.add(CompilerENode::Mul(x, eight));
1389        matches!(
1390            r_mul_pow2_shl(&mut eg, mul),
1391            Some(n) if matches!(eg.canonical_node(n), CompilerENode::Shl(..))
1392        )
1393    }
1394
1395    #[test]
1396    fn mul_pow2_shl_fires_only_when_product_proven_to_fit() {
1397        // Proven-bounded x ∈ [0, 1000]: 1000 * 8 = 8000 fits → strength-reduce.
1398        assert!(mul_by_eight(0, 1000), "proven-bounded x*8 must become a shift");
1399        // Unbounded x: x*8 may overflow — exact `*` promotes, `<<` would WRAP, so
1400        // the rewrite must be refused (the backend keeps the checked native imul).
1401        assert!(
1402            !mul_by_eight(1, i64::MAX),
1403            "unbounded x*8 must NOT become a wrapping shift under exact arithmetic"
1404        );
1405    }
1406
1407    fn mk_int_var(eg: &mut CompilerEGraph, i: u32) -> NodeId {
1408        let v = eg.add(CompilerENode::Var(i, 0));
1409        eg.set_scalar(v, ScalarKind::Int);
1410        eg.set_int_range(v, 0, 1000);
1411        v
1412    }
1413
1414    #[test]
1415    fn gauss_butterfly_offers_the_three_multiply_form() {
1416        // a*b + c*d  must gain the Gauss alternative (a+c)(b+d) − a*d − c*b (a Sub at the root),
1417        // the form that costs 3 multiplies instead of 4 when the cross products are shared.
1418        let mut eg = CompilerEGraph::new();
1419        let a = mk_int_var(&mut eg, 0);
1420        let b = mk_int_var(&mut eg, 1);
1421        let c = mk_int_var(&mut eg, 2);
1422        let d = mk_int_var(&mut eg, 3);
1423        let ab = eg.add(CompilerENode::Mul(a, b));
1424        let cd = eg.add(CompilerENode::Mul(c, d));
1425        let sum = eg.add(CompilerENode::Add(ab, cd));
1426        let rewritten = r_gauss_butterfly(&mut eg, sum);
1427        assert!(
1428            matches!(rewritten, Some(n) if matches!(eg.canonical_node(n), CompilerENode::Sub(..))),
1429            "the Gauss butterfly rewrite must fire and offer the (a+c)(b+d) − a*d − c*b form"
1430        );
1431    }
1432
1433    #[test]
1434    fn every_rewrite_rule_including_gauss_is_kernel_certified() {
1435        // The soundness gate ("the symmetry won't break the algorithm"): EVERY rule's
1436        // certificate — including gauss-butterfly's `Ring` identity — is discharged by the
1437        // kernel, so a rewrite can never change the value of the node it fires on.
1438        let n = verify_all_with_kernel().expect("all rewrite rules must be kernel-certified");
1439        assert!(n >= 1, "at least one certified rule");
1440    }
1441
1442    /// Count the distinct multiply e-classes reachable from `roots` — the multiply op-count of
1443    /// the shared DAG (e-graph hash-consing makes equal products one class, so a product used
1444    /// by two outputs is counted once).
1445    fn count_mul_classes(eg: &mut CompilerEGraph, roots: &[NodeId]) -> usize {
1446        let mut seen = std::collections::HashSet::new();
1447        let mut muls = std::collections::HashSet::new();
1448        let mut stack: Vec<NodeId> = roots.to_vec();
1449        while let Some(id) = stack.pop() {
1450            let cid = eg.find(id);
1451            if !seen.insert(cid) {
1452                continue;
1453            }
1454            let node = eg.canonical_node(cid);
1455            if matches!(node, CompilerENode::Mul(..)) {
1456                muls.insert(cid);
1457            }
1458            for child in node.children() {
1459                stack.push(child);
1460            }
1461        }
1462        muls.len()
1463    }
1464
1465    #[test]
1466    fn gauss_cuts_ntt_base_multiply_from_five_to_four_multiplies() {
1467        // ML-KEM's NTT base case multiplies two degree-1 polynomials mod (X² − ζ):
1468        //   result0 = a0·b0 + ζ·(a1·b1)        result1 = a0·b1 + a1·b0
1469        // The cross products a0·b0 and a1·b1 are SHARED with result0, so Gauss turns the pair
1470        // from 5 multiplies to 4 — a measured 20% cut, certified sound by the kernel.
1471        let mut eg = CompilerEGraph::new();
1472        let a0 = mk_int_var(&mut eg, 0);
1473        let a1 = mk_int_var(&mut eg, 1);
1474        let b0 = mk_int_var(&mut eg, 2);
1475        let b1 = mk_int_var(&mut eg, 3);
1476        let zeta = mk_int_var(&mut eg, 4);
1477
1478        let a0b0 = eg.add(CompilerENode::Mul(a0, b0));
1479        let a1b1 = eg.add(CompilerENode::Mul(a1, b1));
1480        let zeta_a1b1 = eg.add(CompilerENode::Mul(zeta, a1b1));
1481        let result0 = eg.add(CompilerENode::Add(a0b0, zeta_a1b1));
1482
1483        // Naive cross term: a0·b1 + a1·b0 (two fresh multiplies).
1484        let a0b1 = eg.add(CompilerENode::Mul(a0, b1));
1485        let a1b0 = eg.add(CompilerENode::Mul(a1, b0));
1486        let result1_naive = eg.add(CompilerENode::Add(a0b1, a1b0));
1487
1488        let naive = count_mul_classes(&mut eg, &[result0, result1_naive]);
1489        assert_eq!(naive, 5, "the naive NTT base multiply uses 5 multiplies");
1490
1491        // Gauss the cross term → (a0+a1)(b1+b0) − a0·b0 − a1·b1, reusing a0·b0 and a1·b1.
1492        let result1_gauss = r_gauss_butterfly(&mut eg, result1_naive).expect("gauss must fire");
1493        let gauss = count_mul_classes(&mut eg, &[result0, result1_gauss]);
1494        assert_eq!(gauss, 4, "Gauss shares a0·b0 and a1·b1 → 4 multiplies");
1495        assert!(gauss < naive, "the certified symmetry strictly cuts the NTT base-multiply op-count");
1496    }
1497}