Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Equality saturation (feature eqsat)

The simplifier only ever makes expressions smaller, one step at a time, so it cannot find a result that needs a detour: factoring x·y + x·z into x·(y + z) means applying distributivity backwards, and x·(y + 1) − x·y only cancels after it is expanded. The equality-saturation service searches for such alternatives in an e-graph, where every equation is applied without losing the original.

It is a search service, not a phase: you decide when to run it (at an output or checkpoint boundary, not in routine maintenance), on which roots, and with which allowance. What it finds is a candidate that your own profitability and verification decide to use.

#![allow(unused)]
fn main() {
use bitwright::eqsat::{Publication, SaturateConfig, Saturator, SearchRun};
use bitwright::{Context, ParseOptions, Width};

let (sat, report) = Saturator::builtin_groups(
    &["eqsat.distrib", "eqsat.cancel"],
    SaturateConfig::default(),
);
assert!(report.admitted.len() > 1);
let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
let factor = cx.parse("x * y + x * z", &o)?;
let cancel = cx.parse("x * (y + 1) - x * y", &o)?;
let out = sat.search(&mut cx, &[factor, cancel], SearchRun::default())?;
assert_eq!(out.publication, Publication::Published);
assert_eq!(out.roots[0].candidate, Some(cx.parse("x * (y + z)", &o)?));
assert_eq!(out.roots[1].candidate, Some(cx.parse("x", &o)?));
Ok::<(), bitwright::Error>(())
}

Equations

Admitted equations are proven (by their ledger), unconditional, of one width, and in a conservative fragment: + − * & | ^, negation and complement, constants and symbols, widths up to 128. Identities are used in both directions; guard-free rules (cancellations like (x + y) − y → x) in their authored direction only. Other inputs are declined, never approximated.

The built-in equations come in groups: eqsat.assoc, eqsat.distrib (* over +), eqsat.distrib_and (& over | and ^), eqsat.distrib_or (| over &), eqsat.negation and eqsat.cancel. Choose the groups a search needs: some combinations never saturate (distrib_and with distrib_or rewrite each other’s results without end), and a search that does not saturate publishes nothing.

Publication

Each root is searched in its own e-graph, sharing only the allowance. A batch is published only if every root it searched saturated within its bounds: reaching the iteration cap, a budget or a deadline withholds the whole batch (Publication::Withheld), and the rest of the batch is not searched (those roots are reported as stopped, or as Skipped). A deadline already passed when the search starts withholds the batch even where the memo has the answer. Spent allowance stays spent. A candidate must be strictly smaller as a tree, no larger as a DAG, and agree with its input at sampled points.

Stable outcomes (saturated searches, admission declines) are memoized in the context, so asking again is free; incomplete searches are not, so more budget can succeed later.

#![allow(unused)]
fn main() {
use bitwright::eqsat::{Publication, RootEnd, SaturateConfig, Saturator, SearchRun};
use bitwright::{Context, ParseOptions, Width};

let (sat, _) = Saturator::builtin_groups(
    &["eqsat.distrib_and", "eqsat.distrib_or"],
    SaturateConfig::default(),
);
let mut cx = Context::new();
let e = cx.parse("a & b | a & c", &ParseOptions::width(Width::W32))?;
let out = sat.search(&mut cx, &[e], SearchRun::default())?;
assert_eq!(out.roots[0].end, RootEnd::IterationCap);
assert!(matches!(out.publication, Publication::Withheld { .. }));
assert_eq!(out.roots[0].candidate, None);
Ok::<(), bitwright::Error>(())
}