use std::collections::{BTreeMap, HashMap};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub enum Atom {
String(String),
Number(isize),
}
impl From<isize> for Atom {
fn from(value: isize) -> Self {
Atom::Number(value)
}
}
impl From<&str> for Atom {
fn from(value: &str) -> Self {
Atom::String(value.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Variable {
pub name: String,
}
impl From<&str> for Variable {
fn from(value: &str) -> Self {
Variable {
name: value.to_string(),
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum Term {
Literal(Atom),
Variable(Variable),
}
#[derive(Debug, PartialEq, Clone)]
pub struct Fact {
pub entity: Atom,
pub attribute: Atom,
pub value: Atom,
}
#[derive(Debug, PartialEq, Clone)]
pub struct Pattern {
pub entity: Term,
pub attribute: Term,
pub value: Term,
}
pub type Bindings = HashMap<Variable, Atom>;
pub fn unify(pattern: &Pattern, fact: &Fact, bindings: &Bindings) -> Option<Bindings> {
let mut bindings = bindings.clone();
unify_slot(&pattern.entity, &fact.entity, &mut bindings)?;
unify_slot(&pattern.attribute, &fact.attribute, &mut bindings)?;
unify_slot(&pattern.value, &fact.value, &mut bindings)?;
Some(bindings)
}
fn unify_slot(term: &Term, atom: &Atom, bindings: &mut Bindings) -> Option<()> {
match term {
Term::Literal(l) => (l == atom).then_some(()),
Term::Variable(v) => match bindings.get(v) {
Some(l) => (l == atom).then_some(()),
None => {
bindings.insert(v.clone(), atom.clone());
Some(())
}
},
}
}
pub fn match_rule<'a, F>(conditions: &[Pattern], facts: F, bindings: &Bindings) -> Vec<Bindings>
where
F: IntoIterator<Item = &'a Fact> + Clone,
{
match conditions.split_first() {
Some((first, rest)) => {
let mut results = Vec::new();
for fact in facts.clone() {
if let Some(new_bindings) = unify(first, fact, bindings) {
results.extend(match_rule(rest, facts.clone(), &new_bindings));
}
}
results
}
None => vec![bindings.clone()],
}
}
#[derive(Default)]
pub struct WorkingMemory {
memory: BTreeMap<(Atom, Atom), Fact>,
}
#[derive(Debug, PartialEq)]
pub enum InsertResult {
Added,
Updated(Fact),
Unchanged,
}
impl WorkingMemory {
pub fn insert(&mut self, fact: Fact) -> InsertResult {
use std::collections::btree_map::Entry;
match self
.memory
.entry((fact.entity.clone(), fact.attribute.clone()))
{
Entry::Vacant(e) => {
e.insert(fact);
InsertResult::Added
}
Entry::Occupied(mut e) => {
if e.get().value == fact.value {
InsertResult::Unchanged
} else {
InsertResult::Updated(e.insert(fact))
}
}
}
}
pub fn retract(&mut self, entity: Atom, attribute: Atom) -> Option<Fact> {
self.memory.remove(&(entity, attribute))
}
pub fn facts(&self) -> impl Iterator<Item = &Fact> + Clone {
self.memory.values()
}
}
#[macro_export]
macro_rules! atom {
($lit:literal) => {
Atom::from($lit)
};
($name:ident) => {
Atom::from(stringify!($name))
};
}
#[macro_export]
macro_rules! variable {
($name:ident) => {
Variable::from(stringify!($name))
};
}
#[macro_export]
macro_rules! fact {
($e: tt, $a: tt, $v: tt) => {
Fact {
entity: atom!($e),
attribute: atom!($a),
value: atom!($v),
}
};
}
#[macro_export]
macro_rules! pattern {
(? $e:ident, $($rest:tt)*) => {
pattern!(@attr Term::Variable(variable!($e)), $($rest)*)
};
($e:tt, $($rest:tt)*) => {
pattern!(@attr Term::Literal(atom!($e)), $($rest)*)
};
(@attr $entity:expr, ? $a:ident, $($rest:tt)*) => {
pattern!(@val $entity, Term::Variable(variable!($a)), $($rest)*)
};
(@attr $entity:expr, $a:tt, $($rest:tt)*) => {
pattern!(@val $entity, Term::Literal(atom!($a)), $($rest)*)
};
(@val $entity:expr, $attribute:expr, ? $v:ident) => {
Pattern {
entity: $entity,
attribute: $attribute,
value: Term::Variable(variable!($v))
}
};
(@val $entity:expr, $attribute:expr, $v:tt) => {
Pattern {
entity: $entity,
attribute: $attribute,
value: Term::Literal(atom!($v))
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use test_that::prelude::*;
#[test_that::test]
fn test_atom() {
let atom = atom!(10);
expect_that!(atom, eq(Atom::Number(10)));
let atom = atom!(player);
expect_that!(atom, eq(Atom::String("player".to_string())));
}
#[test_that::test]
fn test_fact() {
let fact = fact!(player, health, 10);
expect_that!(
fact,
eq(Fact {
entity: Atom::String("player".to_string()),
attribute: Atom::String("health".to_string()),
value: Atom::Number(10),
})
);
}
#[test_that::test]
fn test_variable() {
let variable = variable!(e);
expect_that!(variable, eq(Variable::from("e")));
}
#[test_that::test]
fn test_pattern() {
let pattern = pattern!(?e, health, 10);
expect_that!(
pattern,
eq(Pattern {
entity: Term::Variable("e".into()),
attribute: Term::Literal("health".into()),
value: Term::Literal(10.into()),
})
);
let pattern = pattern!(?e, ?a, ?v);
expect_that!(
pattern,
eq(Pattern {
entity: Term::Variable("e".into()),
attribute: Term::Variable("a".into()),
value: Term::Variable("v".into()),
})
);
}
#[test_that::test]
fn test_unify() {
let bindings = Bindings::new();
let result = unify(&pattern!(?e, age, ?a), &fact!(alice, age, 30), &bindings);
let expected = Bindings::from([
(variable!(e), Atom::from("alice")),
(variable!(a), Atom::from(30)),
]);
expect_that!(result, some(eq(expected)));
let bindings = Bindings::from([(variable!(e), Atom::from("alice"))]);
let result = unify(
&pattern!(?e, likes, pizza),
&fact!(bob, likes, pizza),
&bindings,
);
expect_that!(result, none());
}
#[test_that::test]
fn test_match_rule() {
let facts = [
fact!(alice, likes, pizza),
fact!(alice, age, 30),
fact!(bob, likes, pasta),
fact!(bob, age, 25),
];
let conditions = [pattern!(?e, age, ?a), pattern!(?e, likes, pizza)];
let bindings = Bindings::new();
let result = match_rule(&conditions, &facts, &bindings);
let expected = Bindings::from([
(variable!(e), Atom::from("alice")),
(variable!(a), Atom::from(30)),
]);
expect_that!(result, contains_exactly!(eq(expected)));
let conditions = [pattern!(?e, age, ?a)];
let bindings = Bindings::new();
let result = match_rule(&conditions, &facts, &bindings);
let expected_alice = Bindings::from([
(variable!(e), Atom::from("alice")),
(variable!(a), Atom::from(30)),
]);
let expected_bob = Bindings::from([
(variable!(e), Atom::from("bob")),
(variable!(a), Atom::from(25)),
]);
expect_that!(
result,
contains_exactly!(eq(expected_alice), eq(expected_bob))
);
let conditions = [pattern!(?e, likes, dosa)];
let bindings = Bindings::new();
let result = match_rule(&conditions, &facts, &bindings);
expect_that!(result, empty());
let bindings = Bindings::from([(variable!(e), Atom::from("alice"))]);
let result = match_rule(&[], &facts, &bindings);
expect_that!(result, contains_exactly!(eq(bindings)));
}
#[test_that::test]
fn test_working_memory() {
let mut wm = WorkingMemory::default();
let result = wm.insert(fact!(player, health, 10));
expect_that!(result, eq(InsertResult::Added));
let result = wm.insert(fact!(player, health, 8));
expect_that!(result, eq(InsertResult::Updated(fact!(player, health, 10))));
let facts: Vec<Fact> = wm.facts().cloned().collect();
expect_that!(facts, contains_exactly!(eq(fact!(player, health, 8))));
let result = wm.retract("player".into(), "health".into());
let facts: Vec<Fact> = wm.facts().cloned().collect();
expect_that!(facts.len(), eq(0));
expect_that!(result, some(eq(fact!(player, health, 8))));
}
}