use crate::dag::builder::DagBuilder;
use crate::dag::node::DagNodeId;
use crate::dag::symbol::SymbolKind;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
pub type RuleFn =
Box<dyn Fn(&mut DagBuilder, SymbolKind, &[DagNodeId]) -> Option<DagNodeId> + Send + Sync>;
struct PrioritizedRule {
func: RuleFn,
priority: i32,
#[allow(dead_code)]
kind_filter: Option<SymbolKind>,
name: String,
}
pub struct RuleRegistry {
rules: Vec<PrioritizedRule>,
kind_index: HashMap<u8, Vec<usize>>,
wildcard_indices: Vec<usize>,
}
impl std::fmt::Debug for RuleRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RuleRegistry")
.field("rules_count", &self.rules.len())
.finish_non_exhaustive()
}
}
impl Default for RuleRegistry {
fn default() -> Self {
Self::new()
}
}
const fn kind_disc(k: &SymbolKind) -> u8 {
match k {
SymbolKind::Variable(_) => 0,
SymbolKind::Constant(_) => 1,
SymbolKind::Operator(op) => 2 + (*op as u8),
SymbolKind::Function(_) => 16,
}
}
impl RuleRegistry {
#[must_use]
pub fn new() -> Self {
Self {
rules: Vec::new(),
kind_index: HashMap::new(),
wildcard_indices: Vec::new(),
}
}
pub fn register<F>(&mut self, rule: F)
where
F: Fn(&mut DagBuilder, SymbolKind, &[DagNodeId]) -> Option<DagNodeId>
+ Send
+ Sync
+ 'static,
{
self.register_named_impl(format!("rule#{}", self.rules.len()), rule, 0, None);
}
pub fn register_with_priority<F>(
&mut self,
rule: F,
priority: i32,
kind_filter: Option<SymbolKind>,
) where
F: Fn(&mut DagBuilder, SymbolKind, &[DagNodeId]) -> Option<DagNodeId>
+ Send
+ Sync
+ 'static,
{
self.register_named_impl(
format!("rule#{}", self.rules.len()),
rule,
priority,
kind_filter,
);
}
pub fn register_named<F>(
&mut self,
name: &str,
rule: F,
priority: i32,
kind_filter: Option<SymbolKind>,
) where
F: Fn(&mut DagBuilder, SymbolKind, &[DagNodeId]) -> Option<DagNodeId>
+ Send
+ Sync
+ 'static,
{
self.register_named_impl(name.to_owned(), rule, priority, kind_filter);
}
fn register_named_impl<F>(
&mut self,
name: String,
rule: F,
priority: i32,
kind_filter: Option<SymbolKind>,
) where
F: Fn(&mut DagBuilder, SymbolKind, &[DagNodeId]) -> Option<DagNodeId>
+ Send
+ Sync
+ 'static,
{
let idx = self.rules.len();
self.rules.push(PrioritizedRule {
func: Box::new(rule),
priority,
kind_filter,
name,
});
if let Some(ref k) = kind_filter {
let disc = kind_disc(k);
let bucket = self.kind_index.entry(disc).or_default();
bucket.push(idx);
bucket.sort_by(|&a, &b| self.rules[b].priority.cmp(&self.rules[a].priority));
} else {
self.wildcard_indices.push(idx);
self.wildcard_indices
.sort_by(|&a, &b| self.rules[b].priority.cmp(&self.rules[a].priority));
}
}
#[must_use]
pub fn rule_set_fingerprint(&self) -> u64 {
if self.rules.is_empty() {
return 0;
}
let mut h = rapidhash::fast::RapidHasher::default();
for rule in &self.rules {
rule.name.hash(&mut h);
}
let fp = h.finish();
if fp == 0 { u64::MAX } else { fp }
}
pub fn named_rules(&self) -> impl Iterator<Item = &str> {
self.rules.iter().map(|r| r.name.as_str())
}
#[must_use]
pub const fn len(&self) -> usize {
self.rules.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.rules.is_empty()
}
pub fn try_apply(
&self,
builder: &mut DagBuilder,
kind: SymbolKind,
children: &[DagNodeId],
) -> Option<DagNodeId> {
let disc = kind_disc(&kind);
if let Some(indices) = self.kind_index.get(&disc) {
for &idx in indices {
if let Some(result) = (self.rules[idx].func)(builder, kind, children) {
return Some(result);
}
}
}
for &idx in &self.wildcard_indices {
if let Some(result) = (self.rules[idx].func)(builder, kind, children) {
return Some(result);
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dag::builder::DagBuilder;
use crate::dag::symbol::{OpKind, SymbolKind};
#[test]
fn custom_rule_fires() {
let mut reg = RuleRegistry::new();
reg.register(|builder, kind, children| {
if kind == SymbolKind::Operator(OpKind::Sub)
&& children.len() == 2
&& children[0] == children[1]
{
Some(builder.constant(0.0))
} else {
None
}
});
let mut b = DagBuilder::new();
let x = b.variable("x");
let result = reg.try_apply(&mut b, SymbolKind::Operator(OpKind::Sub), &[x, x]);
assert!(result.is_some(), "x - x rule should fire");
let node = b.arena().get(result.unwrap()).unwrap();
assert!(matches!(node.kind, SymbolKind::Constant(_)));
}
#[test]
fn multiple_rules_first_wins() {
let mut reg = RuleRegistry::new();
reg.register(|_builder, kind, _children| {
if kind == SymbolKind::Operator(OpKind::Add) {
None } else {
None
}
});
reg.register(|builder, kind, children| {
if kind == SymbolKind::Operator(OpKind::Add)
&& children.len() == 2
&& children[0] == children[1]
{
Some(builder.constant(999.0))
} else {
None
}
});
let mut b = DagBuilder::new();
let x = b.variable("x");
let result = reg.try_apply(&mut b, SymbolKind::Operator(OpKind::Add), &[x, x]);
assert!(result.is_some());
}
#[test]
fn priority_ordering_respected() {
let mut reg = RuleRegistry::new();
reg.register_with_priority(
|builder, kind, _children| {
if matches!(kind, SymbolKind::Operator(OpKind::Add)) {
Some(builder.constant(1.0))
} else {
None
}
},
-10,
None,
);
reg.register_with_priority(
|builder, kind, _children| {
if matches!(kind, SymbolKind::Operator(OpKind::Add)) {
Some(builder.constant(2.0))
} else {
None
}
},
10,
None,
);
let mut b = DagBuilder::new();
let x = b.variable("x");
let result = reg.try_apply(&mut b, SymbolKind::Operator(OpKind::Add), &[x, x]);
let result_id = result.expect("should fire");
let node = b.arena().get(result_id).unwrap();
assert!(matches!(node.kind, SymbolKind::Constant(v) if (v - 2.0).abs() < f64::EPSILON));
}
#[test]
fn kind_filter_skips_non_matching_rules() {
let mut reg = RuleRegistry::new();
let fired = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let fired_clone = std::sync::Arc::clone(&fired);
reg.register_with_priority(
move |_builder, _kind, _children| {
fired_clone.store(true, std::sync::atomic::Ordering::SeqCst);
None
},
0,
Some(SymbolKind::Operator(OpKind::Sub)),
);
let mut b = DagBuilder::new();
let x = b.variable("x");
let result = reg.try_apply(&mut b, SymbolKind::Operator(OpKind::Mul), &[x, x]);
assert!(result.is_none());
assert!(
!fired.load(std::sync::atomic::Ordering::SeqCst),
"kind-filtered rule must not fire"
);
}
#[test]
fn indexed_dispatch_only_consults_matching_kind_rules() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let add_counter = Arc::new(AtomicUsize::new(0));
let mul_counter = Arc::new(AtomicUsize::new(0));
let mut reg = RuleRegistry::new();
for _ in 0..100 {
let c = Arc::clone(&add_counter);
reg.register_with_priority(
move |_b, _k, _ch| {
c.fetch_add(1, Ordering::SeqCst);
None
},
0,
Some(SymbolKind::Operator(OpKind::Add)),
);
}
for _ in 0..100 {
let c = Arc::clone(&mul_counter);
reg.register_with_priority(
move |_b, _k, _ch| {
c.fetch_add(1, Ordering::SeqCst);
None
},
0,
Some(SymbolKind::Operator(OpKind::Mul)),
);
}
let mut b = DagBuilder::new();
let x = b.variable("x");
let _ = reg.try_apply(&mut b, SymbolKind::Operator(OpKind::Mul), &[x, x]);
assert_eq!(
add_counter.load(Ordering::SeqCst),
0,
"Add-filtered rules must NOT run when dispatching for Mul"
);
assert_eq!(
mul_counter.load(Ordering::SeqCst),
100,
"All 100 Mul-filtered rules must run"
);
}
}