summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLLLL Colonq <llll@colonq>2026-10-08 06:02:58 -0400
committerLLLL Colonq <llll@colonq>2026-10-08 06:02:58 -0400
commit742fcf6793e1a38d4cf65bf72832847c0fd679b4 (patch)
tree5ae1d7a91b570a4fa4acde8f2cf6f4bd45ce8920
parent09ff5f929853cc2ba1a1ed7ddb20566cd8fe021d (diff)
Add semi-naive Datalog evaluation
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml9
-rw-r--r--crates/elfmind/Cargo.toml1
-rw-r--r--crates/elfmind/src/knowledge.rs154
-rw-r--r--crates/elfmind/src/knowledge/relation.rs80
-rw-r--r--crates/elfmind/src/main.rs24
-rw-r--r--flake.lock14
7 files changed, 251 insertions, 38 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 5894d4f..e1b26e9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -790,6 +790,7 @@ dependencies = [
"log",
"rand",
"serde",
+ "smallbox",
"strum",
"teleia",
"teleia_macros",
@@ -2428,6 +2429,12 @@ dependencies = [
]
[[package]]
+name = "smallbox"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aca054fd9f8c2ebe8557a2433f307e038c0716124efd045daa0388afa5172189"
+
+[[package]]
name = "smallvec"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 694bb91..83d51da 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -35,14 +35,15 @@ bitflags = { version = "2.6.0", features = ["serde"] } # bitwise flags
rand = { version = "=0.8.5", features = ["small_rng"] } # rng
log = "0.4.21" # logging
lazy_static = "1.5.0" # global constants
-bimap = {version = "0.6.3", features = ["serde"]} # bijective maps
-strum = {version = "0.27.1", features = ["derive"]} # utility macros for enums
-enum-map = {version = "2.7.3", features = ["serde"]} # enum map
+bimap = { version = "0.6.3", features = ["serde"] } # bijective maps
+strum = { version = "0.27.1", features = ["derive"] } # utility macros for enums
+enum-map = { version = "2.7.3", features = ["serde"] } # enum map
serde = { version = "1.0.228", features = ["derive"] } # serialization
-bincode = {version = "2.0.1", features = ["serde"]} # binary serialization
+bincode = { version = "2.0.1", features = ["serde"] } # binary serialization
itertools = "0.15.0" # iterator tools
syn = "3.0.5" # parser for proc macros
quote = "1.0.47" # quasiquotation for proc macros
+smallbox = { version = "0.8.8", features = ["coerce"] } # store small objects inline, large ones on the heap
# cfg(target_arch = "wasm32")
wasm-bindgen = "=0.2.100" # wasm bindings
diff --git a/crates/elfmind/Cargo.toml b/crates/elfmind/Cargo.toml
index fc8d9e0..982623d 100644
--- a/crates/elfmind/Cargo.toml
+++ b/crates/elfmind/Cargo.toml
@@ -24,6 +24,7 @@ enum-map = { workspace = true }
serde = { workspace = true }
bincode = { workspace = true }
itertools = { workspace = true }
+smallbox = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { workspace = true }
diff --git a/crates/elfmind/src/knowledge.rs b/crates/elfmind/src/knowledge.rs
index cbcce28..a610328 100644
--- a/crates/elfmind/src/knowledge.rs
+++ b/crates/elfmind/src/knowledge.rs
@@ -112,8 +112,8 @@ impl Rule {
pub fn unify_body(&self, facts: &[&Relation]) -> Option<Unification> {
if self.body.len() != facts.len() { return None; }
let mut u = Unification::default();
- for (r1, r2) in self.body.iter().zip(facts) {
- u = r1.unify(r2, u)?;
+ for (r, f) in self.body.iter().zip(facts) {
+ u = r.unify(f, u)?;
}
Some(u)
}
@@ -130,6 +130,17 @@ impl Facts {
pub fn with_sort(&self, s: RelationDiscriminants) -> &HashSet<Relation> {
&self.facts[s]
}
+ pub fn with_sort_mut(&mut self, s: RelationDiscriminants) -> &mut HashSet<Relation> {
+ &mut self.facts[s]
+ }
+ pub fn contains(&self, f: &Relation) -> bool {
+ self.facts[f.discriminant()].contains(f)
+ }
+ pub fn absorb(&mut self, other: &mut Facts) {
+ for (rd, fs) in self.facts.iter_mut() {
+ fs.extend(other.with_sort_mut(rd).drain());
+ }
+ }
}
#[derive(Debug, Default, Clone)]
@@ -138,28 +149,141 @@ pub struct Database {
rules: Vec<Rule>,
}
impl Database {
- pub fn add_fact(&mut self, r: Relation) { self.facts.add(r); }
- pub fn add_rule(&mut self, r: Rule) { self.rules.push(r) }
- fn apply_rule(r: &Rule, facts: &mut Facts) -> bool {
- let found: Vec<Unification> = r.body.iter()
- .map(|r| facts.with_sort(r.discriminant())).multi_cartesian_product()
- .filter_map(|fs| r.unify_body(&fs))
- .collect();
+ pub fn is_known(&self, r: &Relation) -> bool { self.facts.contains(r) }
+ pub fn query<'a>(&'a mut self) -> Query<'a> {
+ Query {
+ db: self,
+ facts: Facts::default(),
+ rules: Vec::new(),
+ }
+ }
+}
+
+#[must_use]
+#[derive(Debug)]
+pub struct Query<'a> {
+ db: &'a mut Database,
+ facts: Facts,
+ rules: Vec<Rule>,
+}
+#[derive(Debug, Clone, Copy)]
+enum QueryFactsToConsider<'a> {
+ LastIteration(&'a Facts),
+ Query,
+ DB,
+}
+impl<'a> Query<'a> {
+ fn apply_rule(&self, r: &Rule, new_facts: &mut Facts, required_facts: QueryFactsToConsider) -> bool {
+ // we don't need to consider every possible fact against each body term (naive evaluation)
+ // instead, we can limit ourselves to the subset where at least one of the facts is new (semi-naive)
+ // let found: Vec<Unification> = r.body.iter()
+ // .map(|r| {
+ // let d = r.discriminant();
+ // new_facts.with_sort(d).iter()
+ // .chain(self.facts.with_sort(d).iter())
+ // .chain(self.db.facts.with_sort(d).iter())
+ // })
+ // .multi_cartesian_product()
+ // .filter_map(|fs| r.unify_body(&fs))
+ // .collect();
+ let mut found = Vec::new();
+ // identify which slots of this rule body possibly have new facts to consider
+ let changed_slots = r.body.iter()
+ .enumerate()
+ .filter_map(|(i, r)| {
+ match required_facts {
+ // if we are only considering facts from the provided set
+ QueryFactsToConsider::LastIteration(rf) =>
+ if rf.with_sort(r.discriminant()).is_empty() { None } else { Some(i) },
+ // if we are only considering facts added by this query
+ QueryFactsToConsider::Query =>
+ if self.facts.with_sort(r.discriminant()).is_empty() { None } else { Some(i) },
+ // if we are considering all facts, simply consider every index
+ QueryFactsToConsider::DB => Some(i),
+ }
+ });
+ let empty = HashSet::new();
+ for cs in changed_slots {
+ let attempts = r.body.iter()
+ .enumerate()
+ .map(|(i, r)| {
+ let d = r.discriminant();
+ match required_facts {
+ // if this is an index that changed, limit it to only the provided set of facts
+ QueryFactsToConsider::LastIteration(rf) if i == cs =>
+ rf.with_sort(d).iter().chain(empty.iter()),
+ // (or the set of facts from this query)
+ QueryFactsToConsider::Query if i == cs =>
+ self.facts.with_sort(d).iter().chain(empty.iter()),
+ // otherwise, consider all facts - a new fact might be used alongside an unchanged fact!
+ _ =>
+ self.facts.with_sort(d).iter().chain(self.db.facts.with_sort(d).iter()),
+ }
+ })
+ .multi_cartesian_product();
+ for fs in attempts {
+ if let Some(u) = r.unify_body(&fs) {
+ found.push(u);
+ }
+ }
+ }
let mut changed = false;
for u in found {
- if facts.add(r.head.substitute(&u)) { changed = true; }
+ let new = r.head.substitute(&u);
+ if new.is_concrete() && !self.facts.contains(&new) && !self.db.facts.contains(&new) {
+ if new_facts.add(new) { changed = true; }
+ }
}
changed
}
- pub fn apply_rules_once(&mut self) -> bool {
+ fn apply_new_once(&mut self, new_facts: &mut Facts, required_facts: QueryFactsToConsider) -> bool {
let mut changed = false;
- for r in self.rules.iter() {
- if Self::apply_rule(r, &mut self.facts) { changed = true }
+ match &required_facts {
+ QueryFactsToConsider::DB => {
+ // if we are considering only DB facts, we only need to consider new rules
+ for r in self.rules.iter() {
+ if self.apply_rule(r, new_facts, required_facts) { changed = true }
+ }
+ },
+ _ => { // otherwise, we should consider all rules
+ for r in self.rules.iter().chain(self.db.rules.iter()) {
+ if self.apply_rule(r, new_facts, required_facts) { changed = true }
+ }
+ }
}
changed
}
- pub fn apply_rules(&mut self) {
- while self.apply_rules_once() {}
+ pub fn fact(mut self, r: Relation) -> Self {
+ if !self.db.is_known(&r) {
+ self.facts.add(r);
+ }
+ self
+ }
+ pub fn rule(mut self, r: Rule) -> Self {
+ self.rules.push(r);
+ self
+ }
+ fn run(&mut self) {
+ let mut new_facts = Facts::default();
+ // compare the new rules with the old facts
+ self.apply_new_once(&mut new_facts, QueryFactsToConsider::DB);
+ // compare the new+old rules with the new facts
+ self.apply_new_once(&mut new_facts, QueryFactsToConsider::Query);
+ let mut prev_facts = new_facts.clone();
+ self.facts.absorb(&mut new_facts);
+ while self.apply_new_once(&mut new_facts, QueryFactsToConsider::LastIteration(&prev_facts)) {
+ prev_facts = new_facts.clone();
+ self.facts.absorb(&mut new_facts);
+ }
+ }
+ pub fn ask(mut self) -> Facts {
+ self.run();
+ self.facts
+ }
+ pub fn learn(mut self) {
+ self.run();
+ self.db.facts.absorb(&mut self.facts);
+ self.db.rules.extend_from_slice(&mut self.rules);
}
}
diff --git a/crates/elfmind/src/knowledge/relation.rs b/crates/elfmind/src/knowledge/relation.rs
index 754bf0f..b738aa1 100644
--- a/crates/elfmind/src/knowledge/relation.rs
+++ b/crates/elfmind/src/knowledge/relation.rs
@@ -7,6 +7,76 @@ use crate::character::*;
use enum_map::Enum;
use strum::EnumDiscriminants;
+pub trait Concretizable {
+ type Concrete;
+ fn concretize(&self) -> Option<Self::Concrete>;
+}
+impl<A: Clone> Concretizable for Hole<A> {
+ type Concrete = A;
+ fn concretize(&self) -> Option<Self::Concrete> {
+ match self {
+ Self::Val(x) => Some(x.clone()),
+ _ => None,
+ }
+ }
+}
+impl<
+ A: Clone,
+ B: Clone,
+ > Concretizable for (Hole<A>, Hole<B>) {
+ type Concrete = (A, B);
+ fn concretize(&self) -> Option<Self::Concrete> {
+ let a = self.0.concretize()?;
+ let b = self.1.concretize()?;
+ Some((a, b))
+ }
+}
+impl<
+ A: Clone,
+ B: Clone,
+ C: Clone,
+ > Concretizable for (Hole<A>, Hole<B>, Hole<C>) {
+ type Concrete = (A, B, C);
+ fn concretize(&self) -> Option<Self::Concrete> {
+ let a = self.0.concretize()?;
+ let b = self.1.concretize()?;
+ let c = self.2.concretize()?;
+ Some((a, b, c))
+ }
+}
+impl<
+ A: Clone,
+ B: Clone,
+ C: Clone,
+ D: Clone,
+ > Concretizable for (Hole<A>, Hole<B>, Hole<C>, Hole<D>) {
+ type Concrete = (A, B, C, D);
+ fn concretize(&self) -> Option<Self::Concrete> {
+ let a = self.0.concretize()?;
+ let b = self.1.concretize()?;
+ let c = self.2.concretize()?;
+ let d = self.3.concretize()?;
+ Some((a, b, c, d))
+ }
+}
+impl<
+ A: Clone,
+ B: Clone,
+ C: Clone,
+ D: Clone,
+ E: Clone,
+ > Concretizable for (Hole<A>, Hole<B>, Hole<C>, Hole<D>, Hole<E>) {
+ type Concrete = (A, B, C, D, E);
+ fn concretize(&self) -> Option<Self::Concrete> {
+ let a = self.0.concretize()?;
+ let b = self.1.concretize()?;
+ let c = self.2.concretize()?;
+ let d = self.3.concretize()?;
+ let e = self.4.concretize()?;
+ Some((a, b, c, d, e))
+ }
+}
+
pub trait Substitutable {
fn substitute(&self, u: &Unification) -> Self;
}
@@ -140,6 +210,16 @@ pub enum Relation {
TimeIs(Hole<Time>),
FriendOf((Hole<Character>, Hole<Character>)),
}
+impl Relation {
+ pub fn is_concrete(&self) -> bool {
+ match self {
+ Self::HasItem(x) => x.concretize().is_some(),
+ Self::UsedItemAt(x) => x.concretize().is_some(),
+ Self::TimeIs(x) => x.concretize().is_some(),
+ Self::FriendOf(x) => x.concretize().is_some(),
+ }
+ }
+}
impl Substitutable for Relation {
fn substitute(&self, u: &Unification) -> Self {
match self {
diff --git a/crates/elfmind/src/main.rs b/crates/elfmind/src/main.rs
index cd811f7..19b8a03 100644
--- a/crates/elfmind/src/main.rs
+++ b/crates/elfmind/src/main.rs
@@ -4,13 +4,13 @@ use knowledge::*;
fn main() {
let mut ctx = Context::default();
let mut db = Database::default();
- db.add_fact(Relation::TimeIs(Hole::Val(6)));
- db.add_fact(Relation::TimeIs(Hole::Val(7)));
- db.add_fact(Relation::HasItem((Hole::Val(character::Character::Tyumici), Hole::Val(item::Item::Apple))));
- db.add_fact(Relation::FriendOf((Hole::Val(character::Character::Bobo), Hole::Val(character::Character::Naesten))));
+ db.query()
+ .fact(Relation::HasItem((Hole::Val(character::Character::Tyumici), Hole::Val(item::Item::Apple))))
+ .fact(Relation::FriendOf((Hole::Val(character::Character::Bobo), Hole::Val(character::Character::Naesten))))
+ .learn();
let vt = ctx.var("time");
let vi = ctx.var("item");
- db.add_rule(Rule {
+ db.query().rule(Rule {
head: Relation::UsedItemAt((
Hole::Val(character::Character::Tyumici),
Hole::Var(vi),
@@ -20,10 +20,10 @@ fn main() {
Relation::TimeIs(Hole::Var(vt)),
Relation::HasItem((Hole::Val(character::Character::Tyumici), Hole::Var(vi))),
],
- });
+ }).learn();
let vt = ctx.var("time");
let vc = ctx.var("char");
- db.add_rule(Rule {
+ db.query().rule(Rule {
head: Relation::FriendOf((
Hole::Val(character::Character::Naesten),
Hole::Var(vc),
@@ -32,11 +32,11 @@ fn main() {
Relation::TimeIs(Hole::Var(vt)),
Relation::UsedItemAt((Hole::Var(vc), Hole::Val(item::Item::Apple), Hole::Var(vt))),
],
- });
+ }).learn();
let vc1 = ctx.var("char");
let vc2 = ctx.var("char");
let vc3 = ctx.var("char");
- db.add_rule(Rule {
+ db.query().rule(Rule {
head: Relation::FriendOf((
Hole::Var(vc1),
Hole::Var(vc3),
@@ -45,7 +45,7 @@ fn main() {
Relation::FriendOf((Hole::Var(vc1), Hole::Var(vc2))),
Relation::FriendOf((Hole::Var(vc2), Hole::Var(vc3))),
],
- });
- db.apply_rules();
- println!("{:#?}", db);
+ }).learn();
+ println!("{:#?}", db.query().fact(Relation::TimeIs(Hole::Val(8))).ask());
+
}
diff --git a/flake.lock b/flake.lock
index 1debd6f..a241c91 100644
--- a/flake.lock
+++ b/flake.lock
@@ -60,11 +60,11 @@
]
},
"locked": {
- "lastModified": 1788735623,
- "narHash": "sha256-hMRMaytIKgMZyxDYe0M6z1eSr//TSNswgqro4NRSiNc=",
+ "lastModified": 1790722782,
+ "narHash": "sha256-/QJdpk2P/23c6kAL7rd8INfms0DuFV6owL2iS3HkVCc=",
"ref": "refs/heads/master",
- "rev": "3a8eb9491bda8c9dac9bc9a88f67298991054a13",
- "revCount": 21,
+ "rev": "fe4c9bd487cb9ae59d86dc7680855c5797ae04e3",
+ "revCount": 30,
"type": "git",
"url": "ssh://git@git.colonq.computer/~/libcolonq"
},
@@ -158,11 +158,11 @@
"rust-overlay": "rust-overlay"
},
"locked": {
- "lastModified": 1788735648,
- "narHash": "sha256-WLHQCRaqUwzmWmmDdlmoXQk1rLctueAwMDZ2IerU1aA=",
+ "lastModified": 1791336679,
+ "narHash": "sha256-HTnhjZTFuxWXBMTCdQFzsaI47/1tE8p/j4zrzrJfbFY=",
"owner": "lcolonq",
"repo": "teleia",
- "rev": "e73cea3b6df3b1a9ec2597a881cc35ec6d8c6462",
+ "rev": "b71e024a8ecd93a5ff1db38db68010af9775c679",
"type": "github"
},
"original": {