Skip to main content

vidhi/
lib.rs

1use std::collections::{BTreeMap, HashMap};
2
3/// Represents a concrete value.
4#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
5pub enum Atom {
6    String(String),
7    Number(isize),
8}
9
10impl From<isize> for Atom {
11    fn from(value: isize) -> Self {
12        Atom::Number(value)
13    }
14}
15
16impl From<&str> for Atom {
17    fn from(value: &str) -> Self {
18        Atom::String(value.to_string())
19    }
20}
21
22/// Represents a variable.
23#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24pub struct Variable {
25    pub name: String,
26}
27
28impl From<&str> for Variable {
29    fn from(value: &str) -> Self {
30        Variable {
31            name: value.to_string(),
32        }
33    }
34}
35
36/// Represents either a variable or a concrete value.
37#[derive(Debug, PartialEq, Clone)]
38pub enum Term {
39    Literal(Atom),
40    Variable(Variable),
41}
42
43/// Represents the facts in our database.
44#[derive(Debug, PartialEq, Clone)]
45pub struct Fact {
46    pub entity: Atom,
47    pub attribute: Atom,
48    pub value: Atom,
49}
50
51/// A pattern to create rules.
52#[derive(Debug, PartialEq, Clone)]
53pub struct Pattern {
54    pub entity: Term,
55    pub attribute: Term,
56    pub value: Term,
57}
58
59/// Binds a variable to a concrete value.
60pub type Bindings = HashMap<Variable, Atom>;
61
62/// Tries to unify a pattern to a fact, and returns updated bindings if found.
63pub fn unify(pattern: &Pattern, fact: &Fact, bindings: &Bindings) -> Option<Bindings> {
64    let mut bindings = bindings.clone();
65    unify_slot(&pattern.entity, &fact.entity, &mut bindings)?;
66    unify_slot(&pattern.attribute, &fact.attribute, &mut bindings)?;
67    unify_slot(&pattern.value, &fact.value, &mut bindings)?;
68    Some(bindings)
69}
70
71fn unify_slot(term: &Term, atom: &Atom, bindings: &mut Bindings) -> Option<()> {
72    match term {
73        Term::Literal(l) => (l == atom).then_some(()),
74        Term::Variable(v) => match bindings.get(v) {
75            Some(l) => (l == atom).then_some(()),
76            None => {
77                bindings.insert(v.clone(), atom.clone());
78                Some(())
79            }
80        },
81    }
82}
83
84/// Find's all possible bindings for given database and conditions.
85pub fn match_rule<'a, F>(conditions: &[Pattern], facts: F, bindings: &Bindings) -> Vec<Bindings>
86where
87    F: IntoIterator<Item = &'a Fact> + Clone,
88{
89    match conditions.split_first() {
90        Some((first, rest)) => {
91            let mut results = Vec::new();
92            for fact in facts.clone() {
93                if let Some(new_bindings) = unify(first, fact, bindings) {
94                    results.extend(match_rule(rest, facts.clone(), &new_bindings));
95                }
96            }
97            results
98        }
99        None => vec![bindings.clone()],
100    }
101}
102
103/// Represents the working memory, comprised of facts.
104///
105/// # Invariant
106///
107/// - For a given (entity, attribute), only a single value can exist.
108#[derive(Default)]
109pub struct WorkingMemory {
110    /// Maps a (entity, attribute) -> (entity, attribute, value)
111    memory: BTreeMap<(Atom, Atom), Fact>,
112}
113
114/// Represents the change in working memory after an insert operation.
115#[derive(Debug, PartialEq)]
116pub enum InsertResult {
117    /// A fresh fact was added.
118    Added,
119    /// An existing fact was updated. The old value is returned.
120    Updated(Fact),
121    /// No facts were changed.
122    Unchanged,
123}
124
125impl WorkingMemory {
126    pub fn insert(&mut self, fact: Fact) -> InsertResult {
127        use std::collections::btree_map::Entry;
128
129        match self
130            .memory
131            .entry((fact.entity.clone(), fact.attribute.clone()))
132        {
133            Entry::Vacant(e) => {
134                e.insert(fact);
135                InsertResult::Added
136            }
137            Entry::Occupied(mut e) => {
138                if e.get().value == fact.value {
139                    InsertResult::Unchanged
140                } else {
141                    InsertResult::Updated(e.insert(fact))
142                }
143            }
144        }
145    }
146
147    pub fn retract(&mut self, entity: Atom, attribute: Atom) -> Option<Fact> {
148        self.memory.remove(&(entity, attribute))
149    }
150
151    pub fn facts(&self) -> impl Iterator<Item = &Fact> + Clone {
152        self.memory.values()
153    }
154}
155
156#[macro_export]
157macro_rules! atom {
158    ($lit:literal) => {
159        Atom::from($lit)
160    };
161    ($name:ident) => {
162        Atom::from(stringify!($name))
163    };
164}
165
166#[macro_export]
167macro_rules! variable {
168    ($name:ident) => {
169        Variable::from(stringify!($name))
170    };
171}
172
173#[macro_export]
174macro_rules! fact {
175    ($e: tt, $a: tt, $v: tt) => {
176        Fact {
177            entity: atom!($e),
178            attribute: atom!($a),
179            value: atom!($v),
180        }
181    };
182}
183
184#[macro_export]
185macro_rules! pattern {
186    (? $e:ident, $($rest:tt)*) => {
187        pattern!(@attr Term::Variable(variable!($e)), $($rest)*)
188    };
189    ($e:tt, $($rest:tt)*) => {
190        pattern!(@attr Term::Literal(atom!($e)), $($rest)*)
191    };
192    (@attr $entity:expr, ? $a:ident, $($rest:tt)*) => {
193        pattern!(@val $entity, Term::Variable(variable!($a)), $($rest)*)
194    };
195    (@attr $entity:expr, $a:tt, $($rest:tt)*) => {
196        pattern!(@val $entity, Term::Literal(atom!($a)), $($rest)*)
197    };
198    (@val $entity:expr, $attribute:expr, ? $v:ident) => {
199        Pattern {
200            entity: $entity,
201            attribute: $attribute,
202            value: Term::Variable(variable!($v))
203        }
204    };
205    (@val $entity:expr, $attribute:expr, $v:tt) => {
206        Pattern {
207            entity: $entity,
208            attribute: $attribute,
209            value: Term::Literal(atom!($v))
210        }
211    };
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use test_that::prelude::*;
218
219    #[test_that::test]
220    fn test_atom() {
221        let atom = atom!(10);
222        expect_that!(atom, eq(Atom::Number(10)));
223
224        let atom = atom!(player);
225        expect_that!(atom, eq(Atom::String("player".to_string())));
226    }
227
228    #[test_that::test]
229    fn test_fact() {
230        let fact = fact!(player, health, 10);
231
232        expect_that!(
233            fact,
234            eq(Fact {
235                entity: Atom::String("player".to_string()),
236                attribute: Atom::String("health".to_string()),
237                value: Atom::Number(10),
238            })
239        );
240    }
241
242    #[test_that::test]
243    fn test_variable() {
244        let variable = variable!(e);
245        expect_that!(variable, eq(Variable::from("e")));
246    }
247
248    #[test_that::test]
249    fn test_pattern() {
250        let pattern = pattern!(?e, health, 10);
251        expect_that!(
252            pattern,
253            eq(Pattern {
254                entity: Term::Variable("e".into()),
255                attribute: Term::Literal("health".into()),
256                value: Term::Literal(10.into()),
257            })
258        );
259        let pattern = pattern!(?e, ?a, ?v);
260        expect_that!(
261            pattern,
262            eq(Pattern {
263                entity: Term::Variable("e".into()),
264                attribute: Term::Variable("a".into()),
265                value: Term::Variable("v".into()),
266            })
267        );
268    }
269
270    #[test_that::test]
271    fn test_unify() {
272        // new variables
273        let bindings = Bindings::new();
274        let result = unify(&pattern!(?e, age, ?a), &fact!(alice, age, 30), &bindings);
275        let expected = Bindings::from([
276            (variable!(e), Atom::from("alice")),
277            (variable!(a), Atom::from(30)),
278        ]);
279        expect_that!(result, some(eq(expected)));
280
281        // conflicts
282        let bindings = Bindings::from([(variable!(e), Atom::from("alice"))]);
283        let result = unify(
284            &pattern!(?e, likes, pizza),
285            &fact!(bob, likes, pizza),
286            &bindings,
287        );
288        expect_that!(result, none());
289    }
290
291    #[test_that::test]
292    fn test_match_rule() {
293        let facts = [
294            fact!(alice, likes, pizza),
295            fact!(alice, age, 30),
296            fact!(bob, likes, pasta),
297            fact!(bob, age, 25),
298        ];
299        // single fact found
300        let conditions = [pattern!(?e, age, ?a), pattern!(?e, likes, pizza)];
301        let bindings = Bindings::new();
302
303        let result = match_rule(&conditions, &facts, &bindings);
304
305        let expected = Bindings::from([
306            (variable!(e), Atom::from("alice")),
307            (variable!(a), Atom::from(30)),
308        ]);
309        expect_that!(result, contains_exactly!(eq(expected)));
310
311        // multiple bindings found
312        let conditions = [pattern!(?e, age, ?a)];
313        let bindings = Bindings::new();
314
315        let result = match_rule(&conditions, &facts, &bindings);
316
317        let expected_alice = Bindings::from([
318            (variable!(e), Atom::from("alice")),
319            (variable!(a), Atom::from(30)),
320        ]);
321        let expected_bob = Bindings::from([
322            (variable!(e), Atom::from("bob")),
323            (variable!(a), Atom::from(25)),
324        ]);
325        expect_that!(
326            result,
327            contains_exactly!(eq(expected_alice), eq(expected_bob))
328        );
329
330        // no patterns matched
331        let conditions = [pattern!(?e, likes, dosa)];
332        let bindings = Bindings::new();
333
334        let result = match_rule(&conditions, &facts, &bindings);
335
336        expect_that!(result, empty());
337
338        // existing bindings returned
339        let bindings = Bindings::from([(variable!(e), Atom::from("alice"))]);
340
341        let result = match_rule(&[], &facts, &bindings);
342
343        expect_that!(result, contains_exactly!(eq(bindings)));
344    }
345
346    #[test_that::test]
347    fn test_working_memory() {
348        let mut wm = WorkingMemory::default();
349
350        let result = wm.insert(fact!(player, health, 10));
351        expect_that!(result, eq(InsertResult::Added));
352
353        let result = wm.insert(fact!(player, health, 8));
354        expect_that!(result, eq(InsertResult::Updated(fact!(player, health, 10))));
355        let facts: Vec<Fact> = wm.facts().cloned().collect();
356        expect_that!(facts, contains_exactly!(eq(fact!(player, health, 8))));
357
358        let result = wm.retract("player".into(), "health".into());
359        let facts: Vec<Fact> = wm.facts().cloned().collect();
360        expect_that!(facts.len(), eq(0));
361        expect_that!(result, some(eq(fact!(player, health, 8))));
362    }
363}