Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions crates/logic-eval/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,45 @@
[codecov-badge]: https://codecov.io/gh/ecoricemon/logic-eval/graph/badge.svg?flag=logic-eval
[codecov-url]: https://app.codecov.io/gh/ecoricemon/logic-eval?flags%5B0%5D=logic-eval

A prolog-like logic evaluator.
```text
+------------------+
| Feel(fresh) :- |
| Sleep(well), |
| Sun(shine), |
| Air(cool). |
+------------------+
```

`logic-eval` is a Prolog-like logic evaluation library for Rust.

## Features

- `SLG resolution`: handles recursive queries with tabling.
- `Custom type support`: use `&str`, interned strings, or your own `Atom` type.
- `Parsing`: parse facts, rules, and queries from a Prolog-like text syntax with `parse_str`.
- `Basic logical operators`: supports NOT, AND, and OR in rule bodies.

## Examples

## Example
### Parse text and query a database

```rust
use logic_eval::{Database, StrInterner, parse_str};

// Creates a DB.
let mut db = Database::new();
let interner = StrInterner::new();

// Initializes the DB with a little bit of logic.
let dataset = "
child(a, b).
child(b, c).
child(c, d).
descend($X, $Y) :- child($X, $Y).
descend($X, $Z) :- child($X, $Y), descend($Y, $Z).
";
db.insert_dataset(parse_str(dataset, &interner).unwrap());
db.commit();

// Queries the DB.
let query = "descend($X, $Y).";
let mut cx = db.query(parse_str(query, &interner).unwrap());
let mut cx = db.query(parse_str("descend($X, $Y).", &interner).unwrap());

let mut answer = Vec::new();
while let Some(eval) = cx.prove_next() {
Expand All @@ -45,6 +60,9 @@ while let Some(eval) = cx.prove_next() {
assert_eq!(answer, [
"$X = a, $Y = b",
"$X = b, $Y = c",
"$X = c, $Y = d",
"$X = a, $Y = c",
"$X = b, $Y = d",
"$X = a, $Y = d",
]);
```
9 changes: 5 additions & 4 deletions crates/logic-eval/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod prove;
// === Re-exports ===

pub use parse::{
common::{Intern, InternedStr, StrCanonicalizer, StrInterner},
common::{Intern, InternedStr, StrInterner},
inner::VAR_PREFIX,
inner::{parse_str, Parse},
repr::{Clause, ClauseDataset, Expr, Predicate, Term},
Expand Down Expand Up @@ -37,16 +37,17 @@ mod intern_alias {
pub(crate) type ClauseDatasetIn<'int, Int> = parse::repr::ClauseDataset<NameIn<'int, Int>>;
}

// === Hash map and set used within this crate ===
pub(crate) type Map<K, V> = fxhash::FxHashMap<K, V>;
pub(crate) type IndexMap<K, V> = indexmap::IndexMap<K, V, fxhash::FxBuildHasher>;
pub(crate) type IndexSet<T> = indexmap::IndexSet<T, fxhash::FxBuildHasher>;
pub(crate) type PassThroughIndexMap<K, V> = indexmap::IndexMap<K, V, PassThroughState>;

use std::{
error::Error as StdError,
hash::{BuildHasherDefault, Hasher},
result::Result as StdResult,
};

pub(crate) type Map<K, V> = fxhash::FxHashMap<K, V>;

#[derive(Default, Clone, Copy)]
struct PassThroughHasher {
hash: u64,
Expand Down
68 changes: 1 addition & 67 deletions crates/logic-eval/src/parse/common.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use crate::{Atom, Clause, Expr, Term};
use crate::Atom;
use core::fmt::{self, Display};
use smallvec::SmallVec;

pub trait Intern {
type Interned<'a>: Atom
Expand Down Expand Up @@ -56,68 +55,3 @@ impl Intern for any_intern::Interner {

pub type StrInterner = any_intern::DroplessInterner;
pub type InternedStr<'int> = any_intern::Interned<'int, str>;

pub struct StrCanonicalizer<'int> {
interner: &'int StrInterner,
}

impl<'int> StrCanonicalizer<'int> {
pub fn new(interner: &'int StrInterner) -> Self {
Self { interner }
}

pub fn canonicalize(&self, clause: Clause<InternedStr<'int>>) -> Clause<InternedStr<'int>> {
let mut vars = SmallVec::new();
find_var_in_clause(&clause, &mut vars);

let mut clause = clause;
clause.replace_term(&mut |term| {
if !term.args.is_empty() {
return None;
}
vars.iter().enumerate().find_map(|(i, var)| {
if &term.functor == var {
Some(Term {
functor: self.interner.intern_formatted_str(&i, i % 10 + 1).unwrap(),
args: Vec::new(),
})
} else {
None
}
})
});

return clause;

// === Internal helper functions ===

fn find_var_in_clause<T: Atom>(clause: &Clause<T>, vars: &mut SmallVec<[T; 4]>) {
find_var_in_term(&clause.head, vars);
if let Some(body) = &clause.body {
find_var_in_expr(body, vars);
}
}

fn find_var_in_expr<T: Atom>(expr: &Expr<T>, vars: &mut SmallVec<[T; 4]>) {
match expr {
Expr::Term(term) => find_var_in_term(term, vars),
Expr::Not(expr) => find_var_in_expr(expr, vars),
Expr::And(expr) | Expr::Or(expr) => {
for inner_expr in expr.iter() {
find_var_in_expr(inner_expr, vars);
}
}
}
}

fn find_var_in_term<T: Atom>(term: &Term<T>, vars: &mut SmallVec<[T; 4]>) {
if term.functor.is_variable() {
vars.push(term.functor.clone());
} else {
for arg in &term.args {
find_var_in_term(arg, vars);
}
}
}
}
}
158 changes: 142 additions & 16 deletions crates/logic-eval/src/parse/repr.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::text::Name;
use crate::Atom;
use crate::{
prove::{canonical as canon, prover::Integer},
Atom,
};
use std::{
fmt::{self, Debug, Display, Write},
ops,
Expand Down Expand Up @@ -62,6 +64,44 @@ impl<T> Clause<T> {
}
}

impl Clause<Integer> {
/// Returns true if the clause needs SLG resolution (tabling).
///
/// If a clause has left or mid recursion, it must be handled by tabling.
///
/// # Examples
/// foo(X, Y) :- foo(A, B) ... // left recursion
/// foo(X, Y) :- ... foo(A, B) ... // mid recursion
pub fn needs_tabling(&self) -> bool {
return if let Some(body) = &self.body {
let mut head = self.head.clone();
let mut body = body.clone();
canon::canonicalize_term(&mut head);
canon::canonicalize_expr_on_term(&mut body);
helper(&body.distribute_not(), &head)
} else {
false
};

// === Internal helper functions ===

fn helper(expr: &Expr<Integer>, head: &Term<Integer>) -> bool {
match expr {
Expr::Term(term) => term == head,
Expr::Not(arg) => helper(arg, head),
Expr::And(args) => {
if let Some((last, first)) = args.split_last() {
first.iter().any(|arg| helper(arg, head)) || helper(last, head)
} else {
false
}
}
Expr::Or(args) => args.iter().any(|arg| helper(arg, head)),
}
}
}
}

impl<T: Display> Display for Clause<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.head.fmt(f)?;
Expand Down Expand Up @@ -127,7 +167,7 @@ impl<T: Clone> Term<T> {
}
}

impl<T: Atom> Term<Name<T>> {
impl<T: Atom> Term<T> {
pub fn is_variable(&self) -> bool {
let is_variable = self.functor.is_variable();

Expand All @@ -146,6 +186,23 @@ impl<T: Atom> Term<Name<T>> {

self.args.iter().any(|arg| arg.contains_variable())
}

pub fn replace_variables<F: FnMut(&mut T)>(&mut self, mut f: F) {
fn helper<T, F>(term: &mut Term<T>, f: &mut F)
where
T: Atom,
F: FnMut(&mut T),
{
if term.is_variable() {
f(&mut term.functor);
} else {
for arg in &mut term.args {
helper(arg, f);
}
}
}
helper(self, &mut f)
}
}

impl<T: Display> Display for Term<T> {
Expand Down Expand Up @@ -190,20 +247,20 @@ impl<T> Expr<T> {
Self::Not(Box::new(expr))
}

pub fn expr_and<I: IntoIterator<Item = Expr<T>>>(elems: I) -> Self {
Self::And(elems.into_iter().collect())
pub fn expr_and<I: IntoIterator<Item = Expr<T>>>(args: I) -> Self {
Self::And(args.into_iter().collect())
}

pub fn expr_or<I: IntoIterator<Item = Expr<T>>>(elems: I) -> Self {
Self::Or(elems.into_iter().collect())
pub fn expr_or<I: IntoIterator<Item = Expr<T>>>(args: I) -> Self {
Self::Or(args.into_iter().collect())
}

pub fn map<U, F: FnMut(T) -> U>(self, f: &mut F) -> Expr<U> {
match self {
Self::Term(v) => Expr::Term(v.map(f)),
Self::Not(v) => Expr::Not(Box::new(v.map(f))),
Self::And(v) => Expr::And(v.into_iter().map(|expr| expr.map(f)).collect()),
Self::Or(v) => Expr::Or(v.into_iter().map(|expr| expr.map(f)).collect()),
Self::Term(term) => Expr::Term(term.map(f)),
Self::Not(arg) => Expr::Not(Box::new(arg.map(f))),
Self::And(args) => Expr::And(args.into_iter().map(|arg| arg.map(f)).collect()),
Self::Or(args) => Expr::Or(args.into_iter().map(|arg| arg.map(f)).collect()),
}
}

Expand All @@ -225,18 +282,51 @@ impl<T> Expr<T> {
}
}

impl<T: PartialEq> Expr<T> {
pub fn contains_term(&self, term: &Term<T>) -> bool {
match self {
Self::Term(t) => t == term,
Self::Not(arg) => arg.contains_term(term),
Self::And(args) | Self::Or(args) => args.iter().any(|arg| arg.contains_term(term)),
}
}

/// e.g. ¬(A ∧ (B ∨ C)) -> ¬A ∨ (¬B ∧ ¬C)
pub fn distribute_not(self) -> Self {
match self {
Self::Term(term) => Self::Term(term),
Self::Not(expr) => match *expr {
Self::Term(term) => Self::Not(Box::new(Self::Term(term))),
Self::Not(inner) => inner.distribute_not(),
Self::And(args) => Self::Or(
args.into_iter()
.map(|arg| Self::Not(Box::new(arg)).distribute_not())
.collect(),
),
Self::Or(args) => Self::And(
args.into_iter()
.map(|arg| Self::Not(Box::new(arg)).distribute_not())
.collect(),
),
},
Self::And(args) => Self::And(args.into_iter().map(Self::distribute_not).collect()),
Self::Or(args) => Self::Or(args.into_iter().map(Self::distribute_not).collect()),
}
}
}

impl<T: Display> Display for Expr<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Term(term) => term.fmt(f)?,
Self::Not(inner) => {
Self::Not(arg) => {
f.write_str("\\+ ")?;
if matches!(**inner, Self::And(_) | Self::Or(_)) {
if matches!(**arg, Self::And(_) | Self::Or(_)) {
f.write_char('(')?;
inner.fmt(f)?;
arg.fmt(f)?;
f.write_char(')')?;
} else {
inner.fmt(f)?;
arg.fmt(f)?;
}
}
Self::And(args) => {
Expand Down Expand Up @@ -266,8 +356,44 @@ impl<T: Display> Display for Expr<T> {
}
}

#[derive(Debug, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Predicate<T> {
pub functor: T,
pub arity: u32,
}

#[cfg(test)]
mod tests {
use super::{Expr, Term};

#[test]
fn distribute_not_applies_de_morgan() {
let expr = Expr::expr_not(Expr::expr_and([
Expr::term_atom("a"),
Expr::expr_or([Expr::term_atom("b"), Expr::term_atom("c")]),
]));

let expected = Expr::expr_or([
Expr::expr_not(Expr::term_atom("a")),
Expr::expr_and([
Expr::expr_not(Expr::term_atom("b")),
Expr::expr_not(Expr::term_atom("c")),
]),
]);

assert_eq!(expr.distribute_not(), expected);
}

#[test]
fn distribute_not_removes_double_negation() {
let expr = Expr::expr_not(Expr::expr_not(Expr::term(Term::compound(
"f",
[Term::atom("x")],
))));

assert_eq!(
expr.distribute_not(),
Expr::term(Term::compound("f", [Term::atom("x")]))
);
}
}
Loading
Loading