use crate::compiled::{CompiledRule, Interner, PathIndex};
use crate::parser::GrlParser;
use crate::rule::Rule;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Default)]
pub struct KnowledgeBase {
rules: Vec<Rule>,
rule_index: HashMap<String, usize>,
salience_order: Vec<usize>,
compiled: Vec<CompiledRule>,
position_of: Vec<usize>,
interner: Interner,
readers: PathIndex,
effectful_conditions: usize,
conditions_with_methods: usize,
}
impl KnowledgeBase {
pub fn new() -> Self {
Self::default()
}
pub fn add_rule(&mut self, rule: Rule) -> Result<(), String> {
if self.rule_index.contains_key(&rule.name) {
return Err(format!("Rule '{}' already exists", rule.name));
}
let index = self.rules.len();
let position = self
.salience_order
.partition_point(|&i| self.rules[i].salience >= rule.salience);
self.salience_order.insert(position, index);
self.position_of.push(position);
for (p, &i) in self.salience_order.iter().enumerate().skip(position) {
self.position_of[i] = p;
}
self.rule_index.insert(rule.name.clone(), index);
let compiled = CompiledRule::new(&rule, &mut self.interner);
self.add_readers(index, &compiled);
self.compiled.push(compiled);
self.rules.push(rule);
Ok(())
}
pub fn add_rules_from_str(&mut self, grl: &str) -> Result<usize, String> {
let rules = GrlParser::new().parse_rules(grl)?;
let mut names = std::collections::HashSet::new();
for rule in &rules {
if self.rule_index.contains_key(&rule.name) || !names.insert(&rule.name) {
return Err(format!("Rule '{}' already exists", rule.name));
}
}
let count = rules.len();
for rule in rules {
self.add_rule(rule)?;
}
Ok(count)
}
pub fn add_rules_from_file(&mut self, path: impl AsRef<Path>) -> Result<usize, String> {
let path = path.as_ref();
let grl =
std::fs::read_to_string(path).map_err(|e| format!("{}: {}", path.display(), e))?;
self.add_rules_from_str(&grl)
.map_err(|e| format!("{}: {}", path.display(), e))
}
pub fn add_rules_from_dir(&mut self, dir: impl AsRef<Path>) -> Result<usize, String> {
let mut files = Vec::new();
collect_grl_files(dir.as_ref(), &mut files)?;
files.sort();
let mut count = 0;
for file in files {
count += self.add_rules_from_file(&file)?;
}
Ok(count)
}
pub fn get_rule(&self, name: &str) -> Option<&Rule> {
self.rule_index
.get(name)
.and_then(|&index| self.rules.get(index))
}
pub fn get_rules(&self) -> &[Rule] {
&self.rules
}
pub fn get_rules_sorted_by_salience(&self) -> Vec<&Rule> {
self.rules_by_salience().collect()
}
pub fn rules_by_salience(&self) -> impl Iterator<Item = &Rule> + '_ {
self.salience_order.iter().map(|&i| &self.rules[i])
}
pub(crate) fn entry_at(&self, position: usize) -> (usize, &Rule, &CompiledRule) {
let i = self.salience_order[position];
(i, &self.rules[i], &self.compiled[i])
}
pub(crate) fn position_of(&self, index: usize) -> usize {
self.position_of[index]
}
pub(crate) fn index_of(&self, name: &str) -> Option<usize> {
self.rule_index.get(name).copied()
}
pub(crate) fn interner(&self) -> &Interner {
&self.interner
}
pub(crate) fn readers(&self) -> &PathIndex {
&self.readers
}
pub(crate) fn all_conditions_skippable(&self, user_methods_registered: bool) -> bool {
self.effectful_conditions == 0
&& (!user_methods_registered || self.conditions_with_methods == 0)
}
fn add_readers(&mut self, index: usize, compiled: &CompiledRule) {
self.effectful_conditions += compiled.has_effects as usize;
self.conditions_with_methods += !compiled.condition_methods.is_empty() as usize;
self.readers.add(index, &compiled.reads);
}
pub fn remove_rule(&mut self, name: &str) -> Option<Rule> {
if let Some(&index) = self.rule_index.get(name) {
let rule = self.rules.remove(index);
self.compiled.remove(index);
self.rule_index.remove(name);
for rule_index in self.rule_index.values_mut() {
if *rule_index > index {
*rule_index -= 1;
}
}
self.salience_order.retain(|&i| i != index);
for i in self.salience_order.iter_mut() {
if *i > index {
*i -= 1;
}
}
self.position_of = vec![0; self.rules.len()];
for (p, &i) in self.salience_order.iter().enumerate() {
self.position_of[i] = p;
}
let compiled = std::mem::take(&mut self.compiled);
self.readers = PathIndex::default();
self.effectful_conditions = 0;
self.conditions_with_methods = 0;
for (i, rule) in compiled.iter().enumerate() {
self.add_readers(i, rule);
}
self.compiled = compiled;
Some(rule)
} else {
None
}
}
pub fn clear(&mut self) {
self.rules.clear();
self.rule_index.clear();
self.salience_order.clear();
self.compiled.clear();
self.position_of.clear();
self.interner = Interner::default();
self.readers = PathIndex::default();
self.effectful_conditions = 0;
self.conditions_with_methods = 0;
}
pub fn len(&self) -> usize {
self.rules.len()
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
}
fn collect_grl_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<(), String> {
let entries = std::fs::read_dir(dir).map_err(|e| format!("{}: {}", dir.display(), e))?;
for entry in entries {
let path = entry
.map_err(|e| format!("{}: {}", dir.display(), e))?
.path();
if path.is_dir() {
collect_grl_files(&path, files)?;
} else if path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("grl"))
{
files.push(path);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Expression;
fn rule(name: &str, salience: i32) -> Rule {
Rule::new(
name.to_string(),
salience,
Expression::Boolean(true),
vec![],
)
}
fn order(kb: &KnowledgeBase) -> Vec<&str> {
kb.rules_by_salience().map(|r| r.name.as_str()).collect()
}
#[test]
fn salience_order_is_stable_and_survives_removal() {
let mut kb = KnowledgeBase::new();
for (name, salience) in [("a", 1), ("b", 5), ("c", 1), ("d", 9), ("e", 5)] {
kb.add_rule(rule(name, salience)).unwrap();
}
assert_eq!(order(&kb), ["d", "b", "e", "a", "c"]);
kb.remove_rule("b").unwrap();
assert_eq!(order(&kb), ["d", "e", "a", "c"]);
assert_eq!(kb.get_rule("c").unwrap().name, "c");
kb.add_rule(rule("f", 5)).unwrap();
assert_eq!(order(&kb), ["d", "e", "f", "a", "c"]);
kb.clear();
assert_eq!(order(&kb), Vec::<&str>::new());
}
}