use std::collections::HashSet;
use std::sync::Arc;
use std::time::Instant;
use super::knobs::HeuristicConfig;
use super::patterns;
use super::rule_registry::RuleRegistry;
use super::simplifier::approximate_simplify;
use super::strategy::SearchStrategy;
use crate::dag::builder::DagBuilder;
use crate::dag::node::DagNodeId;
use crate::dag::symbol::SymbolKind;
use crate::egraph::EGraph;
struct Frame {
node_id: DagNodeId,
depth: usize,
cursor: usize,
arity: usize,
kind: SymbolKind,
}
#[derive(Debug, Clone)]
pub struct HeuristicEngine {
pub config: HeuristicConfig,
pub strategy: SearchStrategy,
canonical_cache: HashSet<DagNodeId>,
rule_registry: Arc<RuleRegistry>,
}
impl HeuristicEngine {
#[must_use]
pub fn new(config: HeuristicConfig, strategy: SearchStrategy) -> Self {
Self {
config,
strategy,
canonical_cache: HashSet::new(),
rule_registry: Arc::new(RuleRegistry::new()),
}
}
#[must_use]
pub fn with_rule_registry(mut self, registry: Arc<RuleRegistry>) -> Self {
self.rule_registry = registry;
self
}
#[must_use]
pub fn simplify(&mut self, builder: &mut DagBuilder, root: DagNodeId) -> DagNodeId {
let start_time = Instant::now();
let pre = if self.config.simplification_aggressiveness > 0.5 {
approximate_simplify(builder, root, self.config.simplification_aggressiveness)
} else {
root
};
let after_heuristic = self.rewrite_iterative(builder, pre, start_time);
if self.config.use_egraph {
self.run_egraph_pass(builder, after_heuristic)
} else {
after_heuristic
}
}
fn run_egraph_pass(&self, builder: &mut DagBuilder, root: DagNodeId) -> DagNodeId {
let mut eg = EGraph::new(builder, self.config.egraph_config);
eg.saturate(root);
eg.extract(root)
}
fn rewrite_iterative(
&mut self,
builder: &mut DagBuilder,
root: DagNodeId,
start_time: Instant,
) -> DagNodeId {
if root.is_none() {
return root;
}
if start_time.elapsed() >= self.config.timeout {
return root;
}
let mut stack: Vec<Frame> = Vec::with_capacity(64);
let mut values: Vec<DagNodeId> = Vec::with_capacity(64);
let Some(root_node) = builder.arena().get(root) else {
return root;
};
stack.push(Frame {
node_id: root,
depth: 0,
cursor: 0,
arity: root_node.children.len(),
kind: root_node.kind,
});
while let Some(top) = stack.last_mut() {
if top.depth >= self.config.max_depth || start_time.elapsed() >= self.config.timeout {
let Some(frame) = stack.pop() else { break };
values.truncate(values.len().saturating_sub(frame.cursor));
values.push(frame.node_id);
continue;
}
if top.cursor < top.arity {
let parent_id = top.node_id;
let cursor = top.cursor;
top.cursor += 1;
let parent_depth = top.depth;
let child_id = builder
.arena()
.get(parent_id)
.and_then(|n| n.children.as_slice().get(cursor).copied())
.unwrap_or(DagNodeId::NONE);
if child_id.is_none() {
values.push(DagNodeId::NONE);
continue;
}
let Some(child_node) = builder.arena().get(child_id) else {
values.push(child_id);
continue;
};
let child_kind = child_node.kind;
let child_arity = child_node.children.len();
if child_arity == 0 {
values.push(child_id);
continue;
}
let is_canonical = self.canonical_cache.contains(&child_id)
|| builder
.arena()
.get(child_id)
.is_some_and(|n| n.meta.flags.is_canonical());
if is_canonical {
values.push(child_id);
continue;
}
stack.push(Frame {
node_id: child_id,
depth: parent_depth + 1,
cursor: 0,
arity: child_arity,
kind: child_kind,
});
} else {
let Some(frame) = stack.pop() else { break };
let split_at = values.len().saturating_sub(frame.arity);
let rebuilt = rebuild_or_match(
&self.rule_registry,
builder,
frame.kind,
&values[split_at..],
frame.node_id,
);
values.truncate(split_at);
self.canonical_cache.insert(rebuilt);
if let Some(node) = builder.arena_mut().get_mut(rebuilt) {
node.meta.flags = node.meta.flags.with_canonical();
}
values.push(rebuilt);
}
}
values.pop().unwrap_or(root)
}
}
fn rebuild_or_match(
registry: &RuleRegistry,
builder: &mut DagBuilder,
kind: SymbolKind,
new_children: &[DagNodeId],
original: DagNodeId,
) -> DagNodeId {
if let Some(replacement) = registry.try_apply(builder, kind, new_children) {
return replacement;
}
if let Some(replacement) = patterns::try_apply(builder, kind, new_children) {
return replacement;
}
let original_same = builder
.arena()
.get(original)
.is_some_and(|n| n.children.as_slice() == new_children);
if original_same {
return original;
}
match kind {
SymbolKind::Operator(crate::dag::symbol::OpKind::Add) if new_children.len() == 2 => {
builder.add(new_children[0], new_children[1])
}
SymbolKind::Operator(crate::dag::symbol::OpKind::Sub) if new_children.len() == 2 => {
builder.sub(new_children[0], new_children[1])
}
SymbolKind::Operator(crate::dag::symbol::OpKind::Mul) if new_children.len() == 2 => {
builder.mul(new_children[0], new_children[1])
}
SymbolKind::Operator(crate::dag::symbol::OpKind::Div) if new_children.len() == 2 => {
builder.div(new_children[0], new_children[1])
}
SymbolKind::Operator(crate::dag::symbol::OpKind::Pow) if new_children.len() == 2 => {
builder.pow(new_children[0], new_children[1])
}
SymbolKind::Operator(crate::dag::symbol::OpKind::Mod) if new_children.len() == 2 => {
builder.modulo(new_children[0], new_children[1])
}
SymbolKind::Operator(crate::dag::symbol::OpKind::Neg) if new_children.len() == 1 => {
builder.neg(new_children[0])
}
_ => builder.operator(kind, new_children, crate::dag::metadata::NodeFlags::EMPTY),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dag::symbol::{OpKind, SymbolKind};
#[test]
fn simplify_preserves_dedup_on_deep_chain() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let mut acc = x;
for _ in 0..2000 {
acc = b.add(acc, x);
}
let pre_size = b.arena().len();
let mut engine = HeuristicEngine::new(HeuristicConfig::default(), SearchStrategy::Greedy);
let out = engine.simplify(&mut b, acc);
let post_size = b.arena().len();
let baseline_after_rebuild = post_size;
let mut acc2 = x;
for _ in 0..2000 {
acc2 = b.add(acc2, x);
}
assert_eq!(
b.arena().len(),
baseline_after_rebuild,
"rebuilding the same expression must not allocate any new nodes \
— dedup is broken"
);
assert!(!out.is_none());
let _ = pre_size;
}
#[test]
fn simplify_folds_x_plus_zero() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let zero = b.constant(0.0);
let expr = b.add(x, zero);
let mut engine = HeuristicEngine::new(HeuristicConfig::default(), SearchStrategy::Greedy);
let result = engine.simplify(&mut b, expr);
assert_eq!(result, x);
}
#[test]
fn simplify_handles_nested_identities() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let zero = b.constant(0.0);
let one = b.constant(1.0);
let inner = b.add(x, zero);
let outer = b.mul(inner, one);
let mut engine = HeuristicEngine::new(HeuristicConfig::default(), SearchStrategy::Greedy);
let result = engine.simplify(&mut b, outer);
assert_eq!(
result, x,
"nested identity rewrites should collapse to the variable"
);
let node = b.arena().get(result).expect("result");
assert!(matches!(node.kind, SymbolKind::Variable(_)));
let _ = OpKind::Add;
}
}