1mod egraph;
27mod extract;
28mod pattern;
29#[cfg(test)]
30mod tests;
31
32use std::sync::{Arc, OnceLock};
33
34use egraph::{ClassId, EGraph, ENode, EOp, Halt};
35use pattern::Identity;
36
37use crate::engine::budget::Meter;
38use crate::engine::{Admission, Allowance, Budget, BuildError, Cap, Deadline, Exhausted};
39use crate::error::Error;
40use crate::expr::{Context, Expr, FnEnv, OpCode};
41use crate::hash::{IdMap, combine};
42use crate::rules::{Ledger, RuleProgram};
43use crate::{BitVec, SymbolKey, Width};
44
45#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
47#[non_exhaustive]
48pub struct Fragment {
49 pub max_width: u16,
51}
52
53impl Fragment {
54 pub fn conservative() -> Fragment {
57 Fragment { max_width: 128 }
58 }
59}
60
61setters!(Fragment {
62 with_max_width: max_width: u16,
63});
64
65#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
67#[non_exhaustive]
68pub enum UnsupportedPolicy {
69 Decline,
71 Atomize,
73}
74
75#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
77#[non_exhaustive]
78pub struct SaturateConfig {
79 pub fragment: Fragment,
81 pub iterations: u8,
83 pub matches_per_rule_iter: u32,
85 pub admission: Admission,
87 pub unsupported: UnsupportedPolicy,
89 pub max_root_nodes: u32,
91}
92
93impl Default for SaturateConfig {
94 fn default() -> Self {
97 SaturateConfig {
98 fragment: Fragment::conservative(),
99 iterations: 8,
100 matches_per_rule_iter: 16,
101 admission: Admission::default(),
102 unsupported: UnsupportedPolicy::Decline,
103 max_root_nodes: 4096,
104 }
105 }
106}
107
108setters!(SaturateConfig {
109 with_fragment: fragment: Fragment,
110 with_iterations: iterations: u8,
111 with_matches_per_rule_iter: matches_per_rule_iter: u32,
112 with_admission: admission: Admission,
113 with_unsupported: unsupported: UnsupportedPolicy,
114 with_max_root_nodes: max_root_nodes: u32,
115});
116
117impl SaturateConfig {
118 pub fn exploratory() -> SaturateConfig {
124 SaturateConfig {
125 iterations: 16,
126 matches_per_rule_iter: 64,
127 ..SaturateConfig::default()
128 }
129 }
130}
131
132#[derive(Clone, Debug, Default, PartialEq, Eq)]
134#[non_exhaustive]
135pub struct AdmissionReport {
136 pub admitted: Vec<String>,
138 pub refused: Vec<(String, String)>,
140}
141
142#[derive(Default)]
144#[non_exhaustive]
145pub struct SearchRun<'a> {
146 pub allowance: Option<&'a mut Allowance>,
148 pub per_call: Option<Budget>,
150 pub deadline: Option<Deadline<'a>>,
152}
153
154setters!(SearchRun<'a> {
155 with_allowance: allowance ? &'a mut Allowance,
156 with_per_call: per_call ? Budget,
157 with_deadline: deadline ? Deadline<'a>,
158});
159
160impl SearchRun<'_> {
161 pub const DEFAULT_BUDGET: Budget = {
163 let mut b = Budget::UNLIMITED;
164 b.eqsat_nodes = 2048;
165 b.eqsat_work = 100_000;
166 b
167 };
168}
169
170impl core::fmt::Debug for SearchRun<'_> {
171 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
172 f.debug_struct("SearchRun")
173 .field("per_call", &self.per_call)
174 .finish_non_exhaustive()
175 }
176}
177
178#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
180#[non_exhaustive]
181pub enum RootEnd {
182 Saturated,
184 IterationCap,
186 Stopped(Exhausted),
188 DeclinedUnsupported,
190 DeclinedAdmission(Cap),
192 Memo,
194 Skipped,
196}
197
198#[derive(Copy, Clone, Debug, PartialEq, Eq)]
200#[non_exhaustive]
201pub struct SearchRoot {
202 pub input: Expr,
204 pub candidate: Option<Expr>,
206 pub end: RootEnd,
208}
209
210#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
212#[non_exhaustive]
213pub enum Publication {
214 Published,
216 Withheld {
218 cause: RootEnd,
221 },
222}
223
224#[derive(Clone, Debug, Default, PartialEq, Eq)]
226#[non_exhaustive]
227pub struct EqsatStats {
228 pub declined_unsupported: u64,
230 pub declined_admission: u64,
232 pub saturated: u64,
234 pub iteration_capped: u64,
236 pub budget_terminated: u64,
238 pub memo_hits: u64,
240 pub changed: u64,
242 pub enodes: u64,
244 pub work: u64,
246 pub unions: u64,
248 pub iterations: u64,
250 pub withheld: u64,
252}
253
254#[derive(Clone, Debug, PartialEq, Eq)]
256#[non_exhaustive]
257pub struct SearchReport {
258 pub roots: Vec<SearchRoot>,
260 pub publication: Publication,
262 pub stats: EqsatStats,
264}
265
266struct Inner {
267 rules: Vec<Identity>,
268 cfg: SaturateConfig,
269 epoch: u64,
270}
271
272#[derive(Clone)]
274pub struct Saturator {
275 inner: Arc<Inner>,
276}
277
278impl core::fmt::Debug for Saturator {
279 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
280 f.debug_struct("Saturator")
281 .field("identities", &self.inner.rules.len())
282 .field("config", &self.inner.cfg)
283 .finish()
284 }
285}
286
287#[derive(Clone, Debug, Default)]
289pub(crate) struct Memo {
290 epoch: u64,
291 entries: IdMap<u32, (Option<u32>, RootEnd)>,
292}
293
294impl Memo {
295 pub(crate) fn clear(&mut self) {
296 *self = Memo::default();
297 }
298}
299
300fn builtin_program() -> &'static (RuleProgram, Ledger) {
301 static B: OnceLock<(RuleProgram, Ledger)> = OnceLock::new();
302 B.get_or_init(|| {
303 let p = RuleProgram::compile_trusted(crate::rules::corpus::EQSAT)
304 .unwrap_or_else(|e| panic!("the built-in identities do not compile: {e}"));
305 let l = Ledger::parse(crate::rules::corpus::EQSAT_LEDGER)
306 .unwrap_or_else(|e| panic!("the built-in identity ledger does not parse: {e}"));
307 (p, l)
308 })
309}
310
311impl Saturator {
312 pub fn new(
315 programs: &[(&RuleProgram, &Ledger)],
316 groups: &[&str],
317 cfg: SaturateConfig,
318 ) -> Result<(Saturator, AdmissionReport), BuildError> {
319 let mut report = AdmissionReport::default();
320 let mut rules = Vec::new();
321 let mut epoch = combine(0x65_7173_6174, u64::from(cfg.iterations));
322 for (p, ledger) in programs {
323 for g in p.groups() {
324 if !groups.is_empty() && !groups.contains(&g.name.as_str()) {
325 continue;
326 }
327 for &ri in &g.rules {
328 let rule = &p.rules()[ri];
329 match pattern::admit(rule, ledger.vouches_for(&rule.name, rule.id)) {
330 Ok(id) => {
331 epoch = combine(combine(epoch, rule.id.0[0]), rule.id.0[1]);
332 report.admitted.push(rule.name.clone());
333 rules.push(id);
334 }
335 Err(why) => report.refused.push((rule.name.clone(), why)),
336 }
337 }
338 }
339 }
340 for g in groups {
341 if !programs
342 .iter()
343 .any(|(p, _)| p.groups().iter().any(|x| x.name == *g))
344 {
345 return Err(BuildError::UnknownGroup((*g).to_string()));
346 }
347 }
348 {
349 use core::hash::{Hash, Hasher};
350 let mut h = std::collections::hash_map::DefaultHasher::new();
351 cfg.hash(&mut h);
352 epoch = combine(epoch, h.finish());
353 }
354 Ok((
355 Saturator {
356 inner: Arc::new(Inner { rules, cfg, epoch }),
357 },
358 report,
359 ))
360 }
361
362 pub fn builtin(cfg: SaturateConfig) -> (Saturator, AdmissionReport) {
366 Saturator::builtin_groups(&[], cfg)
367 }
368
369 pub fn builtin_groups(groups: &[&str], cfg: SaturateConfig) -> (Saturator, AdmissionReport) {
373 let (p, l) = builtin_program();
374 Saturator::new(&[(p, l)], groups, cfg)
375 .unwrap_or_else(|e| panic!("the built-in identities do not link: {e}"))
376 }
377
378 pub fn search(
381 &self,
382 cx: &mut Context,
383 batch: &[Expr],
384 run: SearchRun<'_>,
385 ) -> Result<SearchReport, Error> {
386 let ids = cx.ids(batch)?;
387 let inner = &*self.inner;
388 let SearchRun {
389 allowance,
390 per_call,
391 deadline,
392 } = run;
393 let per_call = per_call.unwrap_or(SearchRun::DEFAULT_BUDGET);
394 let limit = match &allowance {
395 Some(a) => per_call.min(&a.remaining()),
396 None => per_call,
397 };
398 let mut meter = Meter::new(limit, deadline);
399 let mut stats = EqsatStats::default();
400 if cx.eqsat_memo.epoch != inner.epoch {
401 cx.eqsat_memo.clear();
402 cx.eqsat_memo.epoch = inner.epoch;
403 }
404 let mut done: IdMap<u32, (Option<u32>, RootEnd)> = IdMap::default();
405 let mut cause: Option<RootEnd> = meter.deadline_now().err().map(RootEnd::Stopped);
407 let mut failure: Option<Error> = None;
408 for &root in &ids {
409 if done.contains_key(&root) {
410 continue;
411 }
412 if let Some(&(c, _)) = cx.eqsat_memo.entries.get(&root) {
413 stats.memo_hits += 1;
414 done.insert(root, (c, RootEnd::Memo));
415 continue;
416 }
417 match cause {
418 Some(RootEnd::Stopped(e)) => {
420 done.insert(root, (None, RootEnd::Stopped(e)));
421 continue;
422 }
423 Some(_) => {
425 done.insert(root, (None, RootEnd::Skipped));
426 continue;
427 }
428 None => {}
429 }
430 let (cand, end) = match self.search_root(cx, root, &mut meter, &mut stats) {
431 Ok(x) => x,
432 Err(e) => {
433 failure = Some(e);
434 break;
435 }
436 };
437 match end {
438 RootEnd::Stopped(_) => cause = Some(end),
439 RootEnd::IterationCap => {
440 cause.get_or_insert(end);
441 }
442 _ => {}
443 }
444 let stable = matches!(
447 end,
448 RootEnd::Saturated | RootEnd::DeclinedUnsupported | RootEnd::DeclinedAdmission(_)
449 );
450 if stable {
451 cx.eqsat_memo.entries.insert(root, (cand, end));
452 }
453 done.insert(root, (cand, end));
454 }
455 let spent = meter.spent;
456 if let Some(a) = allowance {
457 a.spend(&spent);
458 }
459 if let Some(e) = failure {
460 return Err(e);
461 }
462 stats.enodes = spent.eqsat_nodes;
463 stats.work = spent.eqsat_work;
464 let publication = match cause {
465 None => Publication::Published,
466 Some(c) => {
467 stats.withheld += 1;
468 Publication::Withheld { cause: c }
469 }
470 };
471 let published = publication == Publication::Published;
472 Ok(SearchReport {
473 roots: ids
474 .iter()
475 .map(|&i| {
476 let (c, end) = done[&i];
477 SearchRoot {
478 input: cx.handle(i),
479 candidate: c.filter(|_| published).map(|c| cx.handle(c)),
480 end,
481 }
482 })
483 .collect(),
484 publication,
485 stats,
486 })
487 }
488
489 fn search_root(
490 &self,
491 cx: &mut Context,
492 root: u32,
493 meter: &mut Meter<'_>,
494 stats: &mut EqsatStats,
495 ) -> Result<(Option<u32>, RootEnd), Error> {
496 let cfg = &self.inner.cfg;
497 let meta = cx.meta[root as usize];
499 if meta.height > cfg.admission.max_root_height {
500 stats.declined_admission += 1;
501 return Ok((None, RootEnd::DeclinedAdmission(Cap::Height)));
502 }
503 if meta.tree > cfg.admission.max_root_tree_size {
504 stats.declined_admission += 1;
505 return Ok((None, RootEnd::DeclinedAdmission(Cap::TreeSize)));
506 }
507 let w = cx.width_of(root);
508 let order = match self.fragment(cx, root, w) {
510 Ok(o) => o,
511 Err(end) => {
512 match end {
513 RootEnd::DeclinedAdmission(_) => stats.declined_admission += 1,
514 _ => stats.declined_unsupported += 1,
515 }
516 return Ok((None, end));
517 }
518 };
519 let mut eg = EGraph::new();
520 let rootc = match self.import(cx, &mut eg, &order, meter) {
521 Ok(c) => c,
522 Err(Halt::Budget(e)) => {
523 stats.budget_terminated += 1;
524 return Ok((None, RootEnd::Stopped(e)));
525 }
526 Err(Halt::Contract) => {
527 return Err(Error::Contract(
528 "constant folding disagreed on import".into(),
529 ));
530 }
531 };
532 let end = match self.saturate(&mut eg, w, meter, stats) {
533 Ok(end) => end,
534 Err(Halt::Budget(e)) => {
535 stats.budget_terminated += 1;
536 stats.unions += eg.unions;
537 return Ok((None, RootEnd::Stopped(e)));
538 }
539 Err(Halt::Contract) => {
540 return Err(Error::Contract(
541 "an admitted identity united two different constants".into(),
542 ));
543 }
544 };
545 stats.unions += eg.unions;
546 let best = match extract::costs(&mut eg, meter) {
547 Ok(b) => b,
548 Err(e) => {
549 stats.budget_terminated += 1;
550 return Ok((None, RootEnd::Stopped(e)));
551 }
552 };
553 match end {
554 RootEnd::Saturated => stats.saturated += 1,
555 RootEnd::IterationCap => stats.iteration_capped += 1,
556 _ => {}
557 }
558 let Some(e) = cx.as_engine(|cx| extract::build(&mut eg, &best, rootc, cx, w))? else {
559 return Ok((None, end));
560 };
561 if e == root {
562 return Ok((None, end));
563 }
564 let (te, tr) = (cx.meta[e as usize].tree, cx.meta[root as usize].tree);
566 let dag = |cx: &mut Context, i: u32| match cx
567 .dag_size(&[cx.handle(i)], cfg.max_root_nodes.saturating_mul(4))
568 {
569 Ok(crate::Bounded::Exact(n)) => Some(n),
570 _ => None,
571 };
572 let (de, dr) = (dag(cx, e), dag(cx, root));
573 let cheaper = te < tr && matches!((de, dr), (Some(a), Some(b)) if a <= b);
574 if !cheaper {
575 return Ok((None, end));
576 }
577 if !agrees(cx, root, e)? {
578 return Err(Error::Contract(
579 "an extracted candidate differs from its input at a sampled point".into(),
580 ));
581 }
582 stats.changed += 1;
583 Ok((Some(e), end))
584 }
585
586 fn fragment(&self, cx: &Context, root: u32, w: Width) -> Result<Vec<u32>, RootEnd> {
589 let cfg = &self.inner.cfg;
590 if w.bits() > cfg.fragment.max_width {
591 return Err(RootEnd::DeclinedUnsupported);
592 }
593 let max_nodes = cfg
595 .admission
596 .max_root_dag_size
597 .map_or(cfg.max_root_nodes, |d| d.min(cfg.max_root_nodes));
598 let mut order = Vec::new();
599 let mut seen: IdMap<u32, ()> = IdMap::default();
600 let mut stack: Vec<(u32, bool)> = vec![(root, false)];
601 while let Some((i, expanded)) = stack.pop() {
602 if expanded {
603 order.push(i);
604 continue;
605 }
606 if seen.insert(i, ()).is_some() {
607 continue;
608 }
609 if seen.len() as u32 > max_nodes {
610 return Err(RootEnd::DeclinedAdmission(Cap::DagSize));
611 }
612 let n = cx.node(i);
613 if n.width != w.bits() {
614 if cfg.unsupported == UnsupportedPolicy::Atomize && i != root {
615 order.push(i);
616 continue;
617 }
618 return Err(RootEnd::DeclinedUnsupported);
619 }
620 let inside = match n.op {
621 OpCode::Const | OpCode::Sym => true,
622 op => {
623 op.as_bin().is_some_and(pattern::admitted_bin)
624 || op.as_un().is_some_and(pattern::admitted_un)
625 }
626 };
627 if !inside {
628 if cfg.unsupported == UnsupportedPolicy::Atomize && i != root {
629 order.push(i);
630 continue;
631 }
632 return Err(RootEnd::DeclinedUnsupported);
633 }
634 stack.push((i, true));
635 for c in n.children() {
636 stack.push((c, false));
637 }
638 }
639 Ok(order)
640 }
641
642 fn import(
643 &self,
644 cx: &Context,
645 eg: &mut EGraph,
646 order: &[u32],
647 m: &mut Meter<'_>,
648 ) -> Result<ClassId, Halt> {
649 let mut at: IdMap<u32, ClassId> = IdMap::default();
650 for &i in order {
651 let n = cx.node(i);
652 let inside = n.op == OpCode::Const
653 || n.op.as_bin().is_some_and(pattern::admitted_bin)
654 || n.op.as_un().is_some_and(pattern::admitted_un);
655 let e = if let Some(v) = cx.const_val(i) {
656 ENode {
657 op: EOp::Const(v),
658 kids: [0, 0],
659 }
660 } else if !inside || n.op == OpCode::Sym || !n.children().all(|c| at.contains_key(&c)) {
661 ENode {
662 op: EOp::Leaf(i),
663 kids: [0, 0],
664 }
665 } else if let Some(u) = n.op.as_un() {
666 ENode {
667 op: EOp::Un(u),
668 kids: [at[&n.a], 0],
669 }
670 } else {
671 let b = n.op.as_bin().unwrap_or(crate::BinOp::Add);
672 ENode {
673 op: EOp::Bin(b),
674 kids: [at[&n.a], at[&n.b]],
675 }
676 };
677 let c = eg.add(e, true, m)?;
678 at.insert(i, c);
679 }
680 order
681 .last()
682 .and_then(|r| at.get(r).copied())
683 .ok_or(Halt::Contract)
684 }
685
686 fn saturate(
687 &self,
688 eg: &mut EGraph,
689 w: Width,
690 m: &mut Meter<'_>,
691 stats: &mut EqsatStats,
692 ) -> Result<RootEnd, Halt> {
693 let cfg = &self.inner.cfg;
694 let mut applied: std::collections::HashSet<
697 (usize, usize, ClassId, Vec<ClassId>),
698 crate::hash::IdBuild,
699 > = std::collections::HashSet::default();
700 for _ in 0..cfg.iterations {
701 stats.iterations += 1;
702 let (n0, u0) = (eg.nodes.len(), eg.unions);
703 let mut apps: Vec<(usize, usize, ClassId, Vec<ClassId>)> = Vec::new();
706 let classes = eg.canonical_classes();
707 let mut capped = false;
709 for (ri, rule) in self.inner.rules.iter().enumerate() {
710 for dir in 0..if rule.bidirectional { 2 } else { 1 } {
711 let mut left = cfg.matches_per_rule_iter as usize;
712 for &c in &classes {
713 if left == 0 && capped {
714 break;
716 }
717 let mut found: Vec<Vec<ClassId>> = Vec::new();
718 let window = left + 1;
721 let mut skip = |eg: &mut EGraph, b: &[ClassId]| {
722 let key = (
723 ri,
724 dir,
725 eg.find(c),
726 b.iter().map(|&x| eg.find(x)).collect::<Vec<_>>(),
727 );
728 applied.contains(&key)
729 };
730 pattern::ematch(
731 eg,
732 &rule.sides[dir],
733 rule.vars,
734 c,
735 w,
736 &mut found,
737 window,
738 &mut skip,
739 m,
740 )?;
741 if found.len() >= window {
742 capped = true;
743 }
744 for b in found {
745 let key = (
746 ri,
747 dir,
748 eg.find(c),
749 b.iter().map(|&x| eg.find(x)).collect::<Vec<_>>(),
750 );
751 if applied.contains(&key) {
752 continue;
753 }
754 if left == 0 {
755 capped = true;
756 break;
757 }
758 left -= 1;
759 applied.insert(key);
760 apps.push((ri, dir, c, b));
761 }
762 }
763 }
764 }
765 for (ri, dir, c, b) in apps {
766 let rule = &self.inner.rules[ri];
767 if let Some(r) = pattern::instantiate(eg, &rule.sides[1 - dir], &b, w, m)? {
768 eg.union(c, r, m)?;
769 }
770 }
771 eg.rebuild(m)?;
772 if eg.nodes.len() == n0 && eg.unions == u0 && !capped {
773 return Ok(RootEnd::Saturated);
774 }
775 }
776 Ok(RootEnd::IterationCap)
777 }
778}
779
780fn agrees(cx: &mut Context, a: u32, b: u32) -> Result<bool, Error> {
782 let (ea, eb) = (cx.handle(a), cx.handle(b));
783 let syms = cx.symbols_in(&[ea, eb])?;
784 let keys: Vec<(SymbolKey, Width, Option<crate::KnownBits>)> = syms
786 .iter()
787 .filter_map(|&s| {
788 let key = cx.symbol_key(s)?.clone();
789 let known = cx
790 .find_symbol(&key)
791 .and_then(|e| cx.declared_known(e).ok().flatten());
792 Some((key, cx.symbol_width(s)?, known))
793 })
794 .collect();
795 let mut x = cx.meta[a as usize].shash;
796 for k in 0..32u32 {
797 let vals: Vec<(SymbolKey, BitVec)> = keys
798 .iter()
799 .map(|(key, w, known)| {
800 let v = match k {
801 0 => BitVec::zero(*w),
802 1 => BitVec::ones(*w),
803 _ => {
804 let limbs: Vec<u64> = (0..8)
805 .map(|_| {
806 x = x.wrapping_add(0x9e37_79b9_7f4a_7c15);
807 let mut z = x;
808 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
809 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
810 z ^ (z >> 31)
811 })
812 .collect();
813 BitVec::wrapping_from_limbs(*w, &limbs)
814 }
815 };
816 (key.clone(), known.map_or(v, |d| d.fit(&v)))
817 })
818 .collect();
819 let env =
820 FnEnv(|key: &SymbolKey, _| vals.iter().find(|(k2, _)| k2 == key).map(|(_, v)| *v));
821 let r = cx.eval(&[ea, eb], &env)?;
822 if r[0] != r[1] {
823 return Ok(false);
824 }
825 }
826 Ok(true)
827}