#![allow(clippy::float_cmp)]
use std::collections::HashMap;
use crate::dag::builder::DagBuilder;
use crate::dag::node::DagNodeId;
use crate::dag::symbol::{OpKind, SymbolKind};
use crate::egraph::cost::{CostWeights, node_cost};
use crate::egraph::union_find::UnionFind;
pub type RewriteRule =
Box<dyn Fn(&mut DagBuilder, &SymbolKind, &[DagNodeId]) -> Option<DagNodeId> + Send + Sync>;
#[derive(Debug, Clone, Copy)]
pub struct EGraphConfig {
pub max_rounds: usize,
pub max_merges: usize,
pub max_new_nodes: usize,
pub strict_ieee754_signed_zero: bool,
pub cost_weights: Option<CostWeights>,
}
impl Default for EGraphConfig {
fn default() -> Self {
Self {
max_rounds: 8,
max_merges: 512,
max_new_nodes: 1024,
strict_ieee754_signed_zero: false,
cost_weights: None,
}
}
}
pub struct EGraph<'b> {
builder: &'b mut DagBuilder,
uf: UnionFind,
cfg: EGraphConfig,
user_rules: Vec<RewriteRule>,
user_rules_late: Vec<RewriteRule>,
pub merges_performed: usize,
pub rounds_completed: usize,
pub converged: bool,
}
impl<'b> EGraph<'b> {
#[must_use]
pub fn new(builder: &'b mut DagBuilder, cfg: EGraphConfig) -> Self {
let n = builder.node_count();
let capacity = n.saturating_add(cfg.max_new_nodes);
let mut uf = UnionFind::new(n);
uf.reserve(capacity.saturating_sub(n));
Self {
builder,
uf,
cfg,
user_rules: Vec::new(),
user_rules_late: Vec::new(),
merges_performed: 0,
rounds_completed: 0,
converged: false,
}
}
pub fn add_rule<F>(&mut self, rule: F)
where
F: Fn(&mut DagBuilder, &SymbolKind, &[DagNodeId]) -> Option<DagNodeId>
+ Send
+ Sync
+ 'static,
{
self.user_rules.push(Box::new(rule));
}
pub fn add_rule_after_builtins<F>(&mut self, rule: F)
where
F: Fn(&mut DagBuilder, &SymbolKind, &[DagNodeId]) -> Option<DagNodeId>
+ Send
+ Sync
+ 'static,
{
self.user_rules_late.push(Box::new(rule));
}
pub fn find(&mut self, id: DagNodeId) -> DagNodeId {
let canon = self.uf.find(id.0);
DagNodeId(canon)
}
pub fn merge(&mut self, a: DagNodeId, b: DagNodeId) -> bool {
self.uf.grow_to((a.0.max(b.0) as usize).saturating_add(1));
let ra = self.uf.find(a.0);
let rb = self.uf.find(b.0);
if ra == rb {
return false;
}
self.uf.union(ra, rb);
true
}
pub fn equivalent(&mut self, a: DagNodeId, b: DagNodeId) -> bool {
self.uf.grow_to((a.0.max(b.0) as usize).saturating_add(1));
self.uf.same(a.0, b.0)
}
pub fn saturate(&mut self, root: DagNodeId) {
self.merges_performed = 0;
self.rounds_completed = 0;
self.converged = false;
let nodes_at_start = self.builder.node_count();
let mut new_nodes_created: usize = 0;
self.uf.grow_to(nodes_at_start);
for round in 0..self.cfg.max_rounds {
let merges_before = self.merges_performed;
let node_count = self.builder.node_count();
let reachable = self.collect_reachable(root, node_count);
self.uf.grow_to(node_count);
for &id in &reachable {
if new_nodes_created >= self.cfg.max_new_nodes {
break;
}
if self.merges_performed >= self.cfg.max_merges {
break;
}
let before = self.builder.node_count();
self.apply_rules(id);
let after = self.builder.node_count();
let added = after.saturating_sub(before);
new_nodes_created = new_nodes_created.saturating_add(added);
if added > 0 {
self.uf.grow_to(after);
}
}
if self.merges_performed > merges_before {
self.rebuild(&reachable);
}
self.rounds_completed = round + 1;
if self.merges_performed == merges_before {
self.converged = true;
break;
}
}
}
#[must_use]
pub fn extract(&mut self, id: DagNodeId) -> DagNodeId {
let weights = self.cfg.cost_weights.unwrap_or_default();
let reachable = self.collect_reachable(id, self.builder.node_count());
let mut best: HashMap<u32, (DagNodeId, f64)> = HashMap::new();
let mut child_costs: HashMap<u32, f64> = HashMap::new();
let mut sorted = reachable;
sorted.sort_unstable_by_key(|n| n.0);
for node_id in sorted {
let cost = node_cost(self.builder, node_id, &child_costs, &weights);
let canon = self.uf.find(node_id.0);
let entry = best.entry(canon).or_insert((node_id, f64::INFINITY));
if cost < entry.1 {
*entry = (node_id, cost);
}
let current = child_costs.entry(canon).or_insert(f64::INFINITY);
if cost < *current {
*current = cost;
}
}
let canon = self.uf.find(id.0);
best.get(&canon).map_or(id, |&(best_id, _)| best_id)
}
#[inline]
fn is_identity_zero(&self, v: f64) -> bool {
if self.cfg.strict_ieee754_signed_zero {
v.to_bits() == 0 } else {
v == 0.0 }
}
fn collect_reachable(&self, root: DagNodeId, node_count: usize) -> Vec<DagNodeId> {
let mut visited = vec![false; node_count];
let mut stack = Vec::new();
let mut result = Vec::new();
if (root.0 as usize) < node_count {
visited[root.0 as usize] = true;
stack.push(root);
}
while let Some(id) = stack.pop() {
result.push(id);
let Some(node) = self.builder.arena().get(id) else {
continue;
};
for &child in node.children.as_slice() {
if (child.0 as usize) < node_count && !visited[child.0 as usize] {
visited[child.0 as usize] = true;
stack.push(child);
}
}
}
result
}
fn apply_rules(&mut self, id: DagNodeId) {
let Some(node) = self.builder.arena().get(id) else {
return;
};
let kind = node.kind;
let children: Vec<DagNodeId> = node.children.as_slice().to_vec();
let canon_children: Vec<DagNodeId> = children
.iter()
.map(|&c| DagNodeId(self.uf.find(c.0)))
.collect();
let const_vals: Vec<Option<f64>> = canon_children
.iter()
.map(|&c| {
self.builder.arena().get(c).and_then(|n| {
if let SymbolKind::Constant(v) = n.kind {
Some(v)
} else {
None
}
})
})
.collect();
let user_rules = std::mem::take(&mut self.user_rules);
for rule in &user_rules {
if let Some(result) = rule(self.builder, &kind, &canon_children) {
self.uf.grow_to((result.0 as usize).saturating_add(1));
if self.merge(id, result) {
self.merges_performed = self.merges_performed.saturating_add(1);
}
}
}
self.user_rules = user_rules;
match &kind {
SymbolKind::Operator(op) => match op {
OpKind::Add => self.rules_add(id, &canon_children, &const_vals),
OpKind::Sub => self.rules_sub(id, &canon_children, &const_vals),
OpKind::Mul => self.rules_mul(id, &canon_children, &const_vals),
OpKind::Div => self.rules_div(id, &canon_children, &const_vals),
OpKind::Pow => self.rules_pow(id, &canon_children, &const_vals),
OpKind::Neg => self.rules_neg(id, &canon_children),
OpKind::Mod => {}
},
SymbolKind::Constant(_)
| SymbolKind::Variable(_)
| SymbolKind::Function(_)
| SymbolKind::ControlFlow(_) => {}
}
let user_rules_late = std::mem::take(&mut self.user_rules_late);
for rule in &user_rules_late {
if let Some(result) = rule(self.builder, &kind, &canon_children) {
self.uf.grow_to((result.0 as usize).saturating_add(1));
if self.merge(id, result) {
self.merges_performed = self.merges_performed.saturating_add(1);
}
}
}
self.user_rules_late = user_rules_late;
}
fn do_merge(&mut self, a: DagNodeId, b: DagNodeId) {
if self.merge(a, b) {
self.merges_performed = self.merges_performed.saturating_add(1);
}
}
fn rules_add(&mut self, id: DagNodeId, ch: &[DagNodeId], cv: &[Option<f64>]) {
if ch.len() != 2 {
return;
}
let (lhs, rhs) = (ch[0], ch[1]);
if cv
.first()
.copied()
.flatten()
.is_some_and(|v| self.is_identity_zero(v))
{
self.do_merge(id, rhs);
}
if cv
.get(1)
.copied()
.flatten()
.is_some_and(|v| self.is_identity_zero(v))
{
self.do_merge(id, lhs);
}
if let (Some(a), Some(b)) = (cv.first().copied().flatten(), cv.get(1).copied().flatten()) {
let folded = self.builder.constant(a + b);
self.do_merge(id, folded);
}
if lhs != rhs {
let swapped = self.builder.add(rhs, lhs);
self.do_merge(id, swapped);
}
}
fn rules_sub(&mut self, id: DagNodeId, ch: &[DagNodeId], cv: &[Option<f64>]) {
if ch.len() != 2 {
return;
}
let (lhs, rhs) = (ch[0], ch[1]);
if cv
.get(1)
.copied()
.flatten()
.is_some_and(|v| self.is_identity_zero(v))
{
self.do_merge(id, lhs);
}
if lhs == rhs || self.uf.same(lhs.0, rhs.0) {
let zero = self.builder.constant(0.0);
self.do_merge(id, zero);
}
if cv
.first()
.copied()
.flatten()
.is_some_and(|v| self.is_identity_zero(v))
{
let negated = self.builder.neg(rhs);
self.do_merge(id, negated);
}
if let (Some(a), Some(b)) = (cv.first().copied().flatten(), cv.get(1).copied().flatten()) {
let folded = self.builder.constant(a - b);
self.do_merge(id, folded);
}
}
fn rules_mul(&mut self, id: DagNodeId, ch: &[DagNodeId], cv: &[Option<f64>]) {
if ch.len() != 2 {
return;
}
let (lhs, rhs) = (ch[0], ch[1]);
if cv.first() == Some(&Some(1.0)) {
self.do_merge(id, rhs);
}
if cv.get(1) == Some(&Some(1.0)) {
self.do_merge(id, lhs);
}
if cv
.first()
.copied()
.flatten()
.is_some_and(|v| self.is_identity_zero(v))
&& cv.get(1).copied().flatten().is_some_and(|v| !v.is_nan())
{
let zero = self.builder.constant(0.0);
self.do_merge(id, zero);
}
if cv
.get(1)
.copied()
.flatten()
.is_some_and(|v| self.is_identity_zero(v))
&& cv.first().copied().flatten().is_some_and(|v| !v.is_nan())
{
let zero = self.builder.constant(0.0);
self.do_merge(id, zero);
}
if let (Some(a), Some(b)) = (cv.first().copied().flatten(), cv.get(1).copied().flatten()) {
let folded = self.builder.constant(a * b);
self.do_merge(id, folded);
}
if lhs != rhs {
let swapped = self.builder.mul(rhs, lhs);
self.do_merge(id, swapped);
}
if lhs == rhs || self.uf.same(lhs.0, rhs.0) {
let two = self.builder.constant(2.0);
let sq = self.builder.pow(lhs, two);
self.do_merge(id, sq);
}
if let Some(lhs_node) = self.builder.arena().get(lhs)
&& let SymbolKind::Operator(op) = lhs_node.kind
{
let inner_ch = lhs_node.children.as_slice();
if inner_ch.len() == 2 {
let (a, b) = (inner_ch[0], inner_ch[1]);
let ac = self.builder.mul(a, rhs);
let bc = self.builder.mul(b, rhs);
match op {
OpKind::Add => {
let distributed = self.builder.add(ac, bc);
self.do_merge(id, distributed);
}
OpKind::Sub => {
let distributed = self.builder.sub(ac, bc);
self.do_merge(id, distributed);
}
_ => {}
}
}
}
if let Some(lhs_node) = self.builder.arena().get(lhs)
&& lhs_node.kind == SymbolKind::Operator(OpKind::Neg)
{
let inner_ch = lhs_node.children.as_slice();
if inner_ch.len() == 1 {
let a = inner_ch[0];
let ab = self.builder.mul(a, rhs);
let negated = self.builder.neg(ab);
self.do_merge(id, negated);
}
}
}
fn rules_div(&mut self, id: DagNodeId, ch: &[DagNodeId], cv: &[Option<f64>]) {
if ch.len() != 2 {
return;
}
let (lhs, rhs) = (ch[0], ch[1]);
if cv.get(1) == Some(&Some(1.0)) {
self.do_merge(id, lhs);
}
if cv
.first()
.copied()
.flatten()
.is_some_and(|v| self.is_identity_zero(v))
&& let Some(Some(r)) = cv.get(1)
&& *r != 0.0
&& !r.is_nan()
{
let zero = self.builder.constant(0.0);
self.do_merge(id, zero);
}
if (lhs == rhs || self.uf.same(lhs.0, rhs.0))
&& let Some(Some(r)) = cv.get(1)
&& *r != 0.0
&& !r.is_nan()
{
let one = self.builder.constant(1.0);
self.do_merge(id, one);
}
if let Some(Some(c)) = cv.get(1)
&& *c != 0.0
&& !c.is_nan()
&& !c.is_infinite()
{
let recip = self.builder.constant(1.0 / c);
let alt = self.builder.mul(lhs, recip);
self.do_merge(id, alt);
}
if let (Some(a), Some(b)) = (cv.first().copied().flatten(), cv.get(1).copied().flatten()) {
let folded = self.builder.constant(a / b);
self.do_merge(id, folded);
}
}
fn rules_pow(&mut self, id: DagNodeId, ch: &[DagNodeId], cv: &[Option<f64>]) {
if ch.len() != 2 {
return;
}
let base = ch[0];
if cv.get(1).copied().flatten().is_some_and(|v| v == 0.0) {
let one = self.builder.constant(1.0);
self.do_merge(id, one);
}
if cv.get(1) == Some(&Some(1.0)) {
self.do_merge(id, base);
}
if cv.first() == Some(&Some(1.0)) {
let one = self.builder.constant(1.0);
self.do_merge(id, one);
}
if cv
.first()
.copied()
.flatten()
.is_some_and(|v| self.is_identity_zero(v))
&& let Some(Some(e)) = cv.get(1)
&& *e > 0.0
{
let zero = self.builder.constant(0.0);
self.do_merge(id, zero);
}
if cv.get(1) == Some(&Some(2.0)) {
let sq = self.builder.mul(base, base);
self.do_merge(id, sq);
}
if let Some(exp_val) = cv.get(1).copied().flatten()
&& let Some(base_node) = self.builder.arena().get(base)
{
if exp_val == 2.0 {
if let SymbolKind::Operator(op) = base_node.kind {
let inner_ch = base_node.children.as_slice();
if inner_ch.len() == 2 {
let (a, b) = (inner_ch[0], inner_ch[1]);
let two = self.builder.constant(2.0);
let a2 = self.builder.pow(a, two);
let b2 = self.builder.pow(b, two);
let ab = self.builder.mul(a, b);
let two_ab = self.builder.mul(two, ab);
match op {
OpKind::Add => {
let sum1 = self.builder.add(a2, two_ab);
let expanded = self.builder.add(sum1, b2);
self.do_merge(id, expanded);
}
OpKind::Sub => {
let diff1 = self.builder.sub(a2, two_ab);
let expanded = self.builder.add(diff1, b2);
self.do_merge(id, expanded);
}
_ => {}
}
}
}
} else if exp_val == 3.0
&& let SymbolKind::Operator(op) = base_node.kind
{
let inner_ch = base_node.children.as_slice();
if inner_ch.len() == 2 {
let (a, b) = (inner_ch[0], inner_ch[1]);
let two = self.builder.constant(2.0);
let c3 = self.builder.constant(3.0);
let a3 = self.builder.pow(a, c3);
let b3 = self.builder.pow(b, c3);
let a2 = self.builder.pow(a, two);
let b2 = self.builder.pow(b, two);
let a2b = self.builder.mul(a2, b);
let three_a2b = self.builder.mul(c3, a2b);
let ab2 = self.builder.mul(a, b2);
let three_ab2 = self.builder.mul(c3, ab2);
match op {
OpKind::Add => {
let sum1 = self.builder.add(a3, three_a2b);
let sum2 = self.builder.add(sum1, three_ab2);
let expanded = self.builder.add(sum2, b3);
self.do_merge(id, expanded);
}
OpKind::Sub => {
let diff1 = self.builder.sub(a3, three_a2b);
let sum1 = self.builder.add(diff1, three_ab2);
let expanded = self.builder.sub(sum1, b3);
self.do_merge(id, expanded);
}
_ => {}
}
}
}
}
if let Some(base_node) = self.builder.arena().get(base)
&& base_node.kind == SymbolKind::Operator(OpKind::Pow)
{
let inner_ch = base_node.children.as_slice();
if inner_ch.len() == 2 {
let (x, a) = (inner_ch[0], inner_ch[1]);
let b = ch[1];
let ab = self.builder.mul(a, b);
let folded_pow = self.builder.pow(x, ab);
self.do_merge(id, folded_pow);
}
}
if let Some(base_node) = self.builder.arena().get(base)
&& base_node.kind == SymbolKind::Operator(OpKind::Mul)
{
let inner_ch = base_node.children.as_slice();
if inner_ch.len() == 2 {
let (a, b) = (inner_ch[0], inner_ch[1]);
let e = ch[1];
let ae = self.builder.pow(a, e);
let be = self.builder.pow(b, e);
let distributed = self.builder.mul(ae, be);
self.do_merge(id, distributed);
}
}
if let Some(base_node) = self.builder.arena().get(base)
&& base_node.kind == SymbolKind::Operator(OpKind::Div)
{
let inner_ch = base_node.children.as_slice();
if inner_ch.len() == 2 {
let (a, b) = (inner_ch[0], inner_ch[1]);
let e = ch[1];
let ae = self.builder.pow(a, e);
let be = self.builder.pow(b, e);
let distributed = self.builder.div(ae, be);
self.do_merge(id, distributed);
}
}
if let (Some(b_val), Some(e_val)) =
(cv.first().copied().flatten(), cv.get(1).copied().flatten())
{
let result = b_val.powf(e_val);
if result.is_finite() {
let folded = self.builder.constant(result);
self.do_merge(id, folded);
}
}
}
fn rules_neg(&mut self, id: DagNodeId, ch: &[DagNodeId]) {
if ch.len() != 1 {
return;
}
let inner = ch[0];
let (is_neg, double_inner, const_val) = {
let Some(inner_node) = self.builder.arena().get(inner) else {
return;
};
let is_neg = matches!(inner_node.kind, SymbolKind::Operator(OpKind::Neg));
let double_inner = if is_neg {
inner_node.children.as_slice().first().copied()
} else {
None
};
let const_val = if let SymbolKind::Constant(v) = inner_node.kind {
Some(v)
} else {
None
};
(is_neg, double_inner, const_val)
};
if is_neg && let Some(di) = double_inner {
self.do_merge(id, di);
}
if let Some(v) = const_val {
let folded = self.builder.constant(-v);
self.do_merge(id, folded);
}
}
fn rebuild(&mut self, reachable: &[DagNodeId]) {
let mut congruence_map: HashMap<(u8, Vec<u32>), DagNodeId> = HashMap::new();
for &id in reachable {
let Some(node) = self.builder.arena().get(id) else {
continue;
};
let op_key: u8 = match &node.kind {
SymbolKind::Operator(op) => match op {
OpKind::Add => 10,
OpKind::Sub => 11,
OpKind::Mul => 12,
OpKind::Div => 13,
OpKind::Pow => 14,
OpKind::Neg => 15,
OpKind::Mod => 16,
},
_ => continue, };
let canon_ch: Vec<u32> = node
.children
.as_slice()
.iter()
.map(|&c| self.uf.find(c.0))
.collect();
let key = (op_key, canon_ch);
match congruence_map.get(&key) {
None => {
congruence_map.insert(key, id);
}
Some(&existing) => {
if self.merge(existing, id) {
self.merges_performed = self.merges_performed.saturating_add(1);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_zero_merges_to_identity() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let zero = b.constant(0.0);
let xpz = b.add(x, zero);
let mut eg = EGraph::new(&mut b, EGraphConfig::default());
eg.saturate(xpz);
let best = eg.extract(xpz);
assert!(
eg.equivalent(xpz, x) || best == x,
"x+0 should extract to x"
);
}
#[test]
fn constant_folding_collapses_add() {
let mut b = DagBuilder::new();
let c3 = b.constant(3.0);
let c4 = b.constant(4.0);
let s = b.add(c3, c4);
let c7 = b.constant(7.0);
let mut eg = EGraph::new(&mut b, EGraphConfig::default());
eg.saturate(s);
assert!(eg.equivalent(s, c7), "3+4 should be in same e-class as 7");
}
#[test]
fn commutativity_merges_add() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let y = b.variable("y");
let xy = b.add(x, y);
let yx = b.add(y, x);
let mut eg = EGraph::new(&mut b, EGraphConfig::default());
eg.saturate(xy);
assert!(eg.equivalent(xy, yx), "x+y ≅ y+x");
}
#[test]
fn mul_one_extracts_to_variable() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let one = b.constant(1.0);
let xm1 = b.mul(x, one);
let mut eg = EGraph::new(&mut b, EGraphConfig::default());
eg.saturate(xm1);
let best = eg.extract(xm1);
assert!(
eg.equivalent(xm1, x) || best == x,
"x*1 should extract to x"
);
}
#[test]
fn double_neg_simplifies() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let neg_x = b.neg(x);
let neg_neg_x = b.neg(neg_x);
let mut eg = EGraph::new(&mut b, EGraphConfig::default());
eg.saturate(neg_neg_x);
assert!(eg.equivalent(neg_neg_x, x), "--x ≅ x");
}
#[test]
fn pow2_equals_square() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let two = b.constant(2.0);
let xpow2 = b.pow(x, two);
let xsq = b.mul(x, x);
let mut eg = EGraph::new(&mut b, EGraphConfig::default());
eg.saturate(xpow2);
assert!(eg.equivalent(xpow2, xsq), "x^2 ≅ x*x");
}
#[test]
fn saturate_sets_converged_on_fixpoint() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let one = b.constant(1.0);
let expr = b.mul(x, one); let mut eg = EGraph::new(&mut b, EGraphConfig::default());
eg.saturate(expr);
assert!(eg.converged, "simple x*1 should converge before budget");
assert!(
eg.rounds_completed < EGraphConfig::default().max_rounds,
"converged early, should not need all rounds"
);
}
#[test]
fn strict_signed_zero_prevents_neg_zero_identity() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let neg_zero = b.constant(-0.0_f64);
let expr = b.add(x, neg_zero); let cfg = EGraphConfig {
strict_ieee754_signed_zero: true,
..EGraphConfig::default()
};
let mut eg = EGraph::new(&mut b, cfg);
eg.saturate(expr);
let best = eg.extract(expr);
let _ = best;
}
#[test]
fn late_rules_run_after_builtins() {
let mut b = DagBuilder::new();
let c7 = b.constant(7.0);
let c8 = b.constant(8.0);
let mut eg = EGraph::new(&mut b, EGraphConfig::default());
eg.add_rule_after_builtins(|builder, kind, _children| {
if let crate::dag::symbol::SymbolKind::Constant(v) = kind {
if (v - 7.0).abs() < 1e-9 {
return Some(builder.constant(8.0));
}
}
None
});
eg.saturate(c7); assert!(eg.equivalent(c7, c8), "late rule should merge 7.0 ≅ 8.0");
assert!(
eg.merges_performed > 0,
"at least one merge must have occurred"
);
}
}