use ocas_atom::{Atom, AtomArena};
use crate::matcher::{Bindings, MatchError, match_pattern};
use crate::pattern::{Pattern, PatternAlloc};
pub struct Rule<'a> {
pattern: Pattern<'a>,
replacement: Replacement<'a>,
condition: Option<Condition<'a>>,
}
type Replacement<'a> = Box<dyn Fn(&Bindings<'a>, &AtomArena<'a>) -> Atom<'a> + 'a>;
type Condition<'a> = Box<dyn Fn(&Bindings<'a>) -> bool + 'a>;
impl<'a> Rule<'a> {
pub fn new<F>(pattern: Pattern<'a>, replacement: F) -> Self
where
F: Fn(&Bindings<'a>, &AtomArena<'a>) -> Atom<'a> + 'a,
{
Self {
pattern,
replacement: Box::new(replacement),
condition: None,
}
}
pub fn with_condition<F>(mut self, condition: F) -> Self
where
F: Fn(&Bindings<'a>) -> bool + 'a,
{
self.condition = Some(Box::new(condition));
self
}
pub fn apply(&self, ctx: &AtomArena<'a>, atom: Atom<'a>) -> Option<Atom<'a>> {
match match_pattern(self.pattern.clone(), atom) {
Ok(bindings) => {
if let Some(cond) = &self.condition
&& !cond(&bindings)
{
return None;
}
Some((self.replacement)(&bindings, ctx))
}
Err(MatchError::NoMatch) | Err(MatchError::InconsistentBinding) => None,
}
}
}
fn pattern_from_str<'a>(
ctx: &'a AtomArena<'a>,
alloc: &'a impl PatternAlloc<'a>,
s: &'a str,
) -> Pattern<'a> {
let atom = ocas_parse::parse(ctx, s).expect("built-in rule pattern is valid");
Pattern::from_atom(alloc, atom)
}
macro_rules! binding_single {
($bindings:expr, $name:expr) => {
match $bindings.get(ocas_atom::Symbol::new($name)) {
Some(crate::matcher::MatchValue::Single(atom)) => *atom,
_ => panic!(concat!("expected single binding for '", $name, "'")),
}
};
}
pub fn add_zero<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a> {
Rule::new(pattern_from_str(ctx, alloc, "x_ + 0"), |bindings, _ctx| {
binding_single!(bindings, "x")
})
}
pub fn add_zero_left<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a> {
Rule::new(pattern_from_str(ctx, alloc, "0 + x_"), |bindings, _ctx| {
binding_single!(bindings, "x")
})
}
pub fn mul_zero<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a> {
Rule::new(pattern_from_str(ctx, alloc, "x_ * 0"), |_bindings, ctx| {
ctx.num(0)
})
}
pub fn mul_zero_left<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a> {
Rule::new(pattern_from_str(ctx, alloc, "0 * x_"), |_bindings, ctx| {
ctx.num(0)
})
}
pub fn mul_one<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a> {
Rule::new(pattern_from_str(ctx, alloc, "x_ * 1"), |bindings, _ctx| {
binding_single!(bindings, "x")
})
}
pub fn mul_one_left<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a> {
Rule::new(pattern_from_str(ctx, alloc, "1 * x_"), |bindings, _ctx| {
binding_single!(bindings, "x")
})
}
pub fn add_same<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a> {
Rule::new(pattern_from_str(ctx, alloc, "x_ + x_"), |bindings, ctx| {
let x = binding_single!(bindings, "x");
ctx.mul(&[ctx.num(2), x])
})
}
pub fn pow_zero<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a> {
Rule::new(pattern_from_str(ctx, alloc, "x_ ^ 0"), |_bindings, ctx| {
ctx.num(1)
})
}
pub fn pow_one<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a> {
Rule::new(pattern_from_str(ctx, alloc, "x_ ^ 1"), |bindings, _ctx| {
binding_single!(bindings, "x")
})
}
pub fn default_rules<'a>(
ctx: &'a AtomArena<'a>,
alloc: &'a impl PatternAlloc<'a>,
) -> Vec<Rule<'a>> {
vec![
add_zero(ctx, alloc),
add_zero_left(ctx, alloc),
mul_zero(ctx, alloc),
mul_zero_left(ctx, alloc),
mul_one(ctx, alloc),
mul_one_left(ctx, alloc),
add_same(ctx, alloc),
pow_zero(ctx, alloc),
pow_one(ctx, alloc),
]
}
#[cfg(test)]
mod tests {
use super::*;
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;
struct VecAlloc;
impl<'a> PatternAlloc<'a> for VecAlloc {
fn alloc_slice(&self, _items: &[Pattern<'a>]) -> &'a [Pattern<'a>] {
unreachable!()
}
}
fn pat_atom<'a>(ctx: &'a AtomArena<'a>, alloc: &'a VecAlloc, s: &'a str) -> Pattern<'a> {
let atom = ocas_parse::parse(ctx, s).unwrap();
Pattern::from_atom(alloc, atom)
}
#[test]
fn add_zero_applies() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let alloc = VecAlloc;
let rule = add_zero(&ctx, &alloc);
let x = ctx.var("x");
let atom = ctx.add(&[x, ctx.num(0)]);
let result = rule.apply(&ctx, atom).unwrap();
assert_eq!(result, x);
}
#[test]
fn mul_zero_applies() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let alloc = VecAlloc;
let rule = mul_zero(&ctx, &alloc);
let x = ctx.var("x");
let atom = ctx.mul(&[x, ctx.num(0)]);
let result = rule.apply(&ctx, atom).unwrap();
assert_eq!(result, ctx.num(0));
}
#[test]
fn add_same_applies() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let alloc = VecAlloc;
let rule = add_same(&ctx, &alloc);
let x = ctx.var("x");
let atom = ctx.add(&[x, x]);
let result = rule.apply(&ctx, atom).unwrap();
assert_eq!(result.to_string(), "2*x");
}
#[test]
fn rule_with_condition_can_block() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let alloc = VecAlloc;
let pat = pat_atom(&ctx, &alloc, "x_");
let rule = Rule::new(pat, |bindings, _ctx| binding_single!(bindings, "x"))
.with_condition(|bindings| {
matches!(
bindings.get(ocas_atom::Symbol::new("x")),
Some(crate::matcher::MatchValue::Single(a)) if matches!(a.node(), ocas_atom::AtomNode::Num(_))
)
});
let x = ctx.var("x");
assert!(rule.apply(&ctx, x).is_none());
assert!(rule.apply(&ctx, ctx.num(5)).is_some());
}
}