1use 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#[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#[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 Ring { lhs: RingExpr, rhs: RingExpr },
70 BoolCases { vars: u8, lhs: BoolExpr, rhs: BoolExpr },
73 Bitvector { check: fn() -> Result<(), String> },
76 LogosProperty { program: &'static str, expected: &'static str },
81}
82
83fn 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
140fn 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
158fn 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
194const 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 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
299fn bv_fold_evaluator_matches_kernel() -> Result<(), String> {
304 for &a in GRID {
305 for &b in GRID {
306 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#[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
357pub(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
387fn 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
399fn removable(eg: &mut CompilerEGraph, id: NodeId) -> bool {
401 eg.provably_total(id)
402}
403
404fn proven(eg: &mut CompilerEGraph, id: NodeId, k: i64) -> bool {
407 eg.int_value(id) == Some(k)
408}
409
410fn 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 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
499fn 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 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 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
569fn 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
610fn 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
641fn 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 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 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
708fn 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
724fn 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
739fn 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
754fn 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
766fn 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
783fn 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
809fn r_add_comm(eg: &mut CompilerEGraph, id: NodeId) -> Option<NodeId> {
816 if let CompilerENode::Add(l, r) = eg.canonical_node(id) {
817 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
873fn 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
905fn 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 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
953fn 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 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
1219const 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
1337pub 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 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 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 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 assert!(mul_by_eight(0, 1000), "proven-bounded x*8 must become a shift");
1399 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 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 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 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 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 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 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}