WIP
This commit is contained in:
parent
02e2e45848
commit
fc66b8e393
30
README.md
30
README.md
@ -11,13 +11,16 @@ The chase algorithm is a fundamental technique in context of database theory and
|
||||
- Ontology-based data access (OBDA)
|
||||
- Datalog with existential rules
|
||||
|
||||
This implementation provides a **restricted chase** that guarantees termination even with existential rules by tracking applied triggers.
|
||||
This implementation provides a restricted-chase style materialization with active-trigger checks.
|
||||
The default entrypoints use a `10_000` step safeguard and report incomplete results with
|
||||
`terminated == false` if that limit is reached.
|
||||
|
||||
### Features
|
||||
|
||||
- **Core Data Types**: Terms, Atoms, Rules, and Instances
|
||||
- **Existential Quantification**: Automatic generation of labeled nulls
|
||||
- **Restricted Chase**: Termination guarantees via trigger tracking
|
||||
- **Restricted Chase Style**: Active-trigger checks for existential rules
|
||||
- **Configurable Step Limit**: `chase_with_config` exposes the step bound
|
||||
- **Fluent API**: `RuleBuilder` for readable rule construction
|
||||
- **Interactive Frontends**: REPL, script runner, and a local GUI
|
||||
- **Zero Dependencies**: Pure Rust with no external runtime dependencies
|
||||
@ -65,6 +68,29 @@ assert!(result.terminated);
|
||||
println!("Derived {} facts", result.instance.len());
|
||||
```
|
||||
|
||||
Use `chase_with_config` to change the default step bound:
|
||||
|
||||
```rust
|
||||
use chase_rs::{Atom, ChaseConfig, Instance, Term, chase_with_config};
|
||||
use chase_rs::chase::rule::RuleBuilder;
|
||||
|
||||
let instance: Instance = vec![Atom::new("P", vec![Term::constant("a")])]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let rule = RuleBuilder::new()
|
||||
.when("P", vec![Term::var("X")])
|
||||
.then("Q", vec![Term::var("X")])
|
||||
.build();
|
||||
|
||||
let result = chase_with_config(
|
||||
instance,
|
||||
&[rule],
|
||||
ChaseConfig { max_steps: 100 },
|
||||
);
|
||||
|
||||
assert!(result.terminated);
|
||||
```
|
||||
|
||||
#### Existential Rules
|
||||
|
||||
Rules with head-only variables (existential quantification) automatically generate fresh labeled nulls:
|
||||
|
||||
@ -1,55 +1,99 @@
|
||||
//! A database instance is a set of ground atoms (facts).
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
use super::atom::Atom;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum InstanceError {
|
||||
NonGroundFact(Atom),
|
||||
}
|
||||
|
||||
impl fmt::Display for InstanceError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
InstanceError::NonGroundFact(atom) => {
|
||||
write!(f, "facts must be ground atoms: {}", atom)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for InstanceError {}
|
||||
|
||||
/// A database instance containing ground atoms.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Instance {
|
||||
facts: HashSet<Atom>,
|
||||
facts_by_predicate: HashMap<String, HashSet<Atom>>,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl Instance {
|
||||
/// Create an empty instance.
|
||||
pub fn new() -> Self {
|
||||
Instance {
|
||||
facts: HashSet::new(),
|
||||
facts_by_predicate: HashMap::new(),
|
||||
len: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to add a fact to the instance. Returns true if the fact was new.
|
||||
pub fn try_add(&mut self, fact: Atom) -> Result<bool, InstanceError> {
|
||||
if !fact.is_ground() {
|
||||
return Err(InstanceError::NonGroundFact(fact));
|
||||
}
|
||||
|
||||
let bucket = self
|
||||
.facts_by_predicate
|
||||
.entry(fact.predicate.clone())
|
||||
.or_default();
|
||||
let inserted = bucket.insert(fact);
|
||||
if inserted {
|
||||
self.len += 1;
|
||||
}
|
||||
|
||||
Ok(inserted)
|
||||
}
|
||||
|
||||
/// Add a fact to the instance. Returns true if the fact was new.
|
||||
///
|
||||
/// Panics if the atom is not ground.
|
||||
pub fn add(&mut self, fact: Atom) -> bool {
|
||||
debug_assert!(fact.is_ground(), "Facts must be ground atoms");
|
||||
self.facts.insert(fact)
|
||||
self.try_add(fact).expect("facts must be ground atoms")
|
||||
}
|
||||
|
||||
/// Check if the instance contains a fact.
|
||||
pub fn contains(&self, fact: &Atom) -> bool {
|
||||
self.facts.contains(fact)
|
||||
self.facts_by_predicate
|
||||
.get(&fact.predicate)
|
||||
.is_some_and(|facts| facts.contains(fact))
|
||||
}
|
||||
|
||||
/// Get the number of facts.
|
||||
pub fn len(&self) -> usize {
|
||||
self.facts.len()
|
||||
self.len
|
||||
}
|
||||
|
||||
/// Check if the instance is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.facts.is_empty()
|
||||
self.len == 0
|
||||
}
|
||||
|
||||
/// Iterate over all facts.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Atom> {
|
||||
self.facts.iter()
|
||||
self.facts_by_predicate
|
||||
.values()
|
||||
.flat_map(|facts| facts.iter())
|
||||
}
|
||||
|
||||
/// Get all facts with a given predicate.
|
||||
pub fn facts_for_predicate(&self, predicate: &str) -> Vec<&Atom> {
|
||||
self.facts
|
||||
.iter()
|
||||
.filter(|a| a.predicate == predicate)
|
||||
self.facts_by_predicate
|
||||
.get(predicate)
|
||||
.into_iter()
|
||||
.flat_map(|facts| facts.iter())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@ -57,7 +101,7 @@ impl Instance {
|
||||
impl fmt::Display for Instance {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "Instance {{")?;
|
||||
for fact in &self.facts {
|
||||
for fact in self.iter() {
|
||||
writeln!(f, " {}", fact)?;
|
||||
}
|
||||
write!(f, "}}")
|
||||
@ -116,4 +160,15 @@ mod tests {
|
||||
assert_eq!(instance.facts_for_predicate("Person").len(), 1);
|
||||
assert_eq!(instance.facts_for_predicate("Other").len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_add_rejects_non_ground_facts() {
|
||||
let mut instance = Instance::new();
|
||||
let fact = Atom::new("Parent", vec![Term::var("X"), Term::constant("bob")]);
|
||||
|
||||
let error = instance.try_add(fact).unwrap_err();
|
||||
|
||||
assert!(matches!(error, InstanceError::NonGroundFact(_)));
|
||||
assert!(instance.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,8 +9,8 @@ pub mod term;
|
||||
mod engine;
|
||||
|
||||
pub use atom::Atom;
|
||||
pub use engine::{ChaseResult, chase};
|
||||
pub use instance::Instance;
|
||||
pub use engine::{ChaseConfig, ChaseResult, chase, chase_with_config};
|
||||
pub use instance::{Instance, InstanceError};
|
||||
pub use rule::Rule;
|
||||
pub use substitution::Substitution;
|
||||
pub use term::Term;
|
||||
|
||||
@ -53,6 +53,9 @@ pub fn parse_command(input: &str) -> Result<Command, String> {
|
||||
|
||||
if let Some(rest) = strip_keyword(trimmed, "fact") {
|
||||
let atom = parse_atom(trim_suffix(rest, '.')?)?;
|
||||
if !atom.is_ground() {
|
||||
return Err("facts must be ground atoms".to_string());
|
||||
}
|
||||
return Ok(Command::Fact(atom));
|
||||
}
|
||||
|
||||
@ -346,6 +349,12 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_fact_command_rejects_variables() {
|
||||
let error = parse_command("fact Parent(?X, bob).").unwrap_err();
|
||||
assert_eq!(error, "facts must be ground atoms");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rule_command() {
|
||||
let command = parse_command("rule P(?X), Q(?X, a) -> R(?X).").unwrap();
|
||||
|
||||
@ -3,9 +3,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::chase::substitution::unify_atom;
|
||||
use crate::{Atom, ChaseResult, Instance, Rule, Substitution, Term};
|
||||
|
||||
const DEFAULT_MAX_STEPS: usize = 10_000;
|
||||
use crate::{Atom, ChaseConfig, ChaseResult, Instance, Rule, Substitution, Term};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MaterializedState {
|
||||
@ -76,12 +74,13 @@ pub fn materialize(base_instance: Instance, rules: &[Rule]) -> MaterializedState
|
||||
.cloned()
|
||||
.map(|fact| (fact, Derivation::Input))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let max_steps = ChaseConfig::default().max_steps;
|
||||
let mut null_gen = NullGenerator::seeded_from(&instance, rules);
|
||||
let mut applied_triggers = HashSet::new();
|
||||
let mut steps = 0;
|
||||
|
||||
loop {
|
||||
if steps >= DEFAULT_MAX_STEPS {
|
||||
if steps >= max_steps {
|
||||
return MaterializedState {
|
||||
result: ChaseResult {
|
||||
instance,
|
||||
|
||||
@ -41,7 +41,10 @@ impl Session {
|
||||
match command {
|
||||
Command::Fact(atom) => {
|
||||
self.materialized = None;
|
||||
let inserted = self.base_instance.add(atom.clone());
|
||||
let inserted = self
|
||||
.base_instance
|
||||
.try_add(atom.clone())
|
||||
.map_err(|err| err.to_string())?;
|
||||
let action = if inserted {
|
||||
"Added"
|
||||
} else {
|
||||
|
||||
@ -2,4 +2,7 @@ pub mod chase;
|
||||
pub mod frontend;
|
||||
|
||||
// Re-export main types for convenience
|
||||
pub use chase::{Atom, ChaseResult, Instance, Rule, Substitution, Term, chase};
|
||||
pub use chase::{
|
||||
Atom, ChaseConfig, ChaseResult, Instance, InstanceError, Rule, Substitution, Term, chase,
|
||||
chase_with_config,
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user