use crate::bitset::BitSet;
use crate::compiled::{path_from_snippet, CompiledRule, MemoSlot, Node, Path as FactPath};
use crate::facts::{Fact, FactValue};
use crate::functions::{self, Function};
use crate::knowledge_base::KnowledgeBase;
use crate::methods::MethodRegistry;
use crate::operators;
use crate::rule::Rule;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::borrow::Cow;
use std::cell::Cell;
use std::collections::HashMap;
use std::fmt::{self, Write};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
use std::time::{Duration, Instant};
use thiserror::Error;
pub const DEFAULT_MAX_CYCLES: usize = 5000;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum EngineError {
#[error("Evaluation error: {0}")]
EvaluationError(String),
#[error("Unknown variable: {0}")]
UnknownVariable(String),
#[error("Type error: {0}")]
TypeError(String),
#[error("Division by zero")]
DivisionByZero,
#[error("Unknown function: {0}")]
UnknownFunction(String),
#[error("Function error: {0}")]
FunctionError(String),
#[error("Unknown rule: {0}")]
UnknownRule(String),
#[error("Rules fired {0} times without settling; a rule's actions may never make its condition false (consider Retract)")]
MaxCyclesExceeded(usize),
#[error("Execution was cancelled")]
Cancelled,
#[error("Execution timed out")]
TimedOut,
#[error("Rule '{rule}' failed in its {phase} scope: {source}")]
InRule {
rule: String,
phase: RulePhase,
#[source]
source: Box<EngineError>,
},
}
impl EngineError {
pub fn root(&self) -> &EngineError {
match self {
EngineError::InRule { source, .. } => source.root(),
other => other,
}
}
fn in_rule(self, rule: &Rule, phase: RulePhase) -> EngineError {
EngineError::InRule {
rule: rule.name.clone(),
phase,
source: Box::new(self),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RulePhase {
When,
Then,
}
impl fmt::Display for RulePhase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
RulePhase::When => "when",
RulePhase::Then => "then",
})
}
}
pub trait EngineListener: Send + Sync {
fn begin_cycle(&self, _cycle: u64) {}
fn evaluate_rule(&self, _cycle: u64, _rule: &Rule, _matched: bool) {}
fn execute_rule(&self, _cycle: u64, _rule: &Rule) {}
}
#[derive(Debug, Clone)]
pub struct ExecutionResult {
pub rules_fired: Vec<String>,
pub execution_time_ms: u128,
}
impl ExecutionResult {
pub fn new() -> Self {
Self {
rules_fired: Vec::new(),
execution_time_ms: 0,
}
}
}
impl Default for ExecutionResult {
fn default() -> Self {
Self::new()
}
}
#[derive(Default)]
struct ExecutionState {
retracted: Vec<String>,
completed: bool,
changed: Vec<FactPath>,
}
struct Agenda {
dirty: BitSet,
matching: BitSet,
retracted: BitSet,
}
struct Memo {
slots: Vec<Cell<u8>>,
}
impl Memo {
const UNKNOWN: u8 = 0;
fn new(slot_count: usize) -> Self {
Memo {
slots: vec![Cell::new(Self::UNKNOWN); slot_count],
}
}
fn disabled() -> Self {
Memo { slots: Vec::new() }
}
fn is_enabled(&self) -> bool {
!self.slots.is_empty()
}
fn get(&self, slot: MemoSlot) -> Option<bool> {
match self.slots[slot].get() {
Self::UNKNOWN => None,
value => Some(value == 2),
}
}
fn set(&self, slot: MemoSlot, value: bool) {
self.slots[slot].set(if value { 2 } else { 1 });
}
fn forget(&self, slot: MemoSlot) {
self.slots[slot].set(Self::UNKNOWN);
}
}
enum Key {
Field(String),
Index(i64),
}
pub struct RuleEngine {
knowledge_base: KnowledgeBase,
functions: HashMap<String, Box<Function>>,
methods: MethodRegistry,
max_cycles: usize,
return_err_on_failed_rule_evaluation: bool,
listeners: Vec<Box<dyn EngineListener>>,
condition_caching: bool,
}
impl RuleEngine {
pub fn new() -> Self {
Self {
knowledge_base: KnowledgeBase::new(),
functions: HashMap::new(),
methods: MethodRegistry::default(),
max_cycles: DEFAULT_MAX_CYCLES,
return_err_on_failed_rule_evaluation: false,
listeners: Vec::new(),
condition_caching: true,
}
}
pub fn add_rule(&mut self, rule: Rule) -> Result<(), String> {
self.knowledge_base.add_rule(rule)
}
pub fn add_rules_from_str(&mut self, grl: &str) -> Result<usize, String> {
self.knowledge_base.add_rules_from_str(grl)
}
pub fn add_rules_from_file(&mut self, path: impl AsRef<Path>) -> Result<usize, String> {
self.knowledge_base.add_rules_from_file(path)
}
pub fn add_rules_from_dir(&mut self, dir: impl AsRef<Path>) -> Result<usize, String> {
self.knowledge_base.add_rules_from_dir(dir)
}
pub fn set_max_cycles(&mut self, max_cycles: usize) {
self.max_cycles = max_cycles;
}
pub fn set_return_err_on_failed_rule_evaluation(&mut self, enabled: bool) {
self.return_err_on_failed_rule_evaluation = enabled;
}
pub fn set_condition_caching(&mut self, enabled: bool) {
self.condition_caching = enabled;
}
pub fn add_listener(&mut self, listener: impl EngineListener + 'static) {
self.listeners.push(Box::new(listener));
}
pub fn register_function<F>(&mut self, name: &str, function: F) -> Result<(), String>
where
F: Fn(&[FactValue]) -> Result<FactValue, String> + Send + Sync + 'static,
{
if functions::is_reserved(name) {
return Err(format!("'{}' is a built-in function", name));
}
if self.functions.contains_key(name) {
return Err(format!("Function '{}' is already registered", name));
}
self.functions.insert(name.to_string(), Box::new(function));
Ok(())
}
pub fn register_method<F>(&mut self, name: &str, method: F) -> Result<(), String>
where
F: Fn(&mut FactValue, &[FactValue]) -> Result<FactValue, String> + Send + Sync + 'static,
{
self.methods.register(name, Box::new(method))
}
pub fn register_typed_method<T, A, R, F>(&mut self, name: &str, method: F) -> Result<(), String>
where
T: Serialize + DeserializeOwned + 'static,
A: DeserializeOwned + 'static,
R: Serialize + 'static,
F: Fn(&mut T, A) -> Result<R, String> + Send + Sync + 'static,
{
self.methods.register_typed(name, method)
}
pub fn execute(
&self,
facts: &mut HashMap<String, Fact>,
) -> Result<ExecutionResult, EngineError> {
self.run(facts, &Cancellation::default())
}
pub fn execute_with_cancel(
&self,
facts: &mut HashMap<String, Fact>,
cancel: &AtomicBool,
) -> Result<ExecutionResult, EngineError> {
let cancellation = Cancellation {
flag: Some(cancel),
deadline: None,
};
self.run(facts, &cancellation)
}
pub fn execute_with_timeout(
&self,
facts: &mut HashMap<String, Fact>,
timeout: Duration,
) -> Result<ExecutionResult, EngineError> {
let cancellation = Cancellation {
flag: None,
deadline: Some(Instant::now() + timeout),
};
self.run(facts, &cancellation)
}
fn run(
&self,
facts: &mut HashMap<String, Fact>,
cancel: &Cancellation,
) -> Result<ExecutionResult, EngineError> {
let start_time = Instant::now();
let mut result = ExecutionResult::new();
let mut state = ExecutionState::default();
let kb = &self.knowledge_base;
let n = kb.len();
let mut agenda = Agenda {
dirty: BitSet::new(n, true),
matching: BitSet::new(n, false),
retracted: BitSet::new(n, false),
};
let memo = Memo::new(if self.condition_caching {
kb.interner().slot_count()
} else {
0
});
let full_evaluation =
!self.listeners.is_empty() || self.return_err_on_failed_rule_evaluation;
let mut always = BitSet::new(n, false);
let mut always_positions = Vec::new();
if !full_evaluation && !kb.all_conditions_skippable(!self.methods.is_empty()) {
for position in 0..n {
if !self.is_skippable(kb.entry_at(position).2) {
always.insert(position);
always_positions.push(position);
agenda.dirty.remove(position);
}
}
}
let mut cycle: u64 = 0;
loop {
cancel.check()?;
self.listeners.iter().for_each(|l| l.begin_cycle(cycle + 1));
let selected = if full_evaluation {
self.select_by_full_evaluation(cycle + 1, &mut agenda, facts, &memo, cancel)?
} else {
self.select_incrementally(&mut agenda, &always_positions, facts, &memo, cancel)?
};
let Some(position) = selected else { break };
cycle += 1;
if cycle > self.max_cycles as u64 {
return Err(EngineError::MaxCyclesExceeded(self.max_cycles));
}
let (_, rule, compiled) = kb.entry_at(position);
self.listeners
.iter()
.for_each(|l| l.execute_rule(cycle, rule));
for action in &compiled.then {
self.execute_action(action, facts, &mut state)
.map_err(|e| e.in_rule(rule, RulePhase::Then))?;
}
result.rules_fired.push(rule.name.clone());
if state.completed {
break;
}
for name in state.retracted.drain(..) {
let position = kb.position_of(kb.index_of(&name).expect("checked by Retract"));
agenda.retracted.insert(position);
agenda.matching.remove(position);
agenda.dirty.remove(position);
}
for written in compiled.writes.iter().chain(&state.changed) {
self.invalidate(written, &mut agenda, &always, &memo);
}
state.changed.clear();
}
result.execution_time_ms = start_time.elapsed().as_millis();
Ok(result)
}
fn select_by_full_evaluation(
&self,
cycle: u64,
agenda: &mut Agenda,
facts: &HashMap<String, Fact>,
memo: &Memo,
cancel: &Cancellation,
) -> Result<Option<usize>, EngineError> {
let mut selected = None;
for position in 0..self.knowledge_base.len() {
cancel.check()?;
if agenda.retracted.contains(position) {
continue;
}
let (_, rule, compiled) = self.knowledge_base.entry_at(position);
let matched = if agenda.dirty.contains(position) || !self.is_cacheable(compiled) {
self.evaluate_into(position, rule, compiled, agenda, facts, memo)?
} else {
agenda.matching.contains(position)
};
self.listeners
.iter()
.for_each(|l| l.evaluate_rule(cycle, rule, matched));
if matched && selected.is_none() {
selected = Some(position);
}
}
Ok(selected)
}
fn select_incrementally(
&self,
agenda: &mut Agenda,
always: &[usize],
facts: &HashMap<String, Fact>,
memo: &Memo,
cancel: &Cancellation,
) -> Result<Option<usize>, EngineError> {
let mut best = agenda.matching.next_from(0);
for &position in always {
if agenda.retracted.contains(position) {
continue;
}
cancel.check()?;
let (_, rule, compiled) = self.knowledge_base.entry_at(position);
if self.test_rule(rule, compiled, facts, memo)? && best.is_none_or(|b| position < b) {
best = Some(position);
}
}
let mut cursor = 0;
while let Some(position) = agenda.dirty.next_from(cursor) {
if best.is_some_and(|b| position >= b) {
break;
}
cursor = position + 1;
cancel.check()?;
let (_, rule, compiled) = self.knowledge_base.entry_at(position);
if self.evaluate_into(position, rule, compiled, agenda, facts, memo)? {
best = Some(position);
break;
}
}
Ok(best)
}
fn evaluate_into(
&self,
position: usize,
rule: &Rule,
compiled: &CompiledRule,
agenda: &mut Agenda,
facts: &HashMap<String, Fact>,
memo: &Memo,
) -> Result<bool, EngineError> {
let matched = self.test_rule(rule, compiled, facts, memo)?;
if self.is_cacheable(compiled) {
agenda.dirty.remove(position);
if matched {
agenda.matching.insert(position);
}
}
Ok(matched)
}
fn is_cacheable(&self, compiled: &CompiledRule) -> bool {
self.condition_caching && compiled.deterministic && !self.calls_user_methods(compiled)
}
fn is_skippable(&self, compiled: &CompiledRule) -> bool {
!compiled.has_effects && !self.calls_user_methods(compiled)
}
fn calls_user_methods(&self, compiled: &CompiledRule) -> bool {
self.any_user_method(&compiled.condition_methods)
}
fn any_user_method(&self, names: &[String]) -> bool {
!self.methods.is_empty() && names.iter().any(|name| self.methods.contains(name))
}
fn invalidate(&self, written: &[String], agenda: &mut Agenda, always: &BitSet, memo: &Memo) {
let kb = &self.knowledge_base;
kb.readers().overlapping(written, |index| {
let position = kb.position_of(index);
if !agenda.retracted.contains(position) && !always.contains(position) {
agenda.matching.remove(position);
agenda.dirty.insert(position);
}
});
if memo.is_enabled() {
kb.interner()
.slot_readers()
.overlapping(written, |slot| memo.forget(slot));
}
}
pub fn fetch_matching_rules(
&self,
facts: &HashMap<String, Fact>,
) -> Result<Vec<&Rule>, EngineError> {
let kb = &self.knowledge_base;
let memo = Memo::new(if self.condition_caching {
kb.interner().slot_count()
} else {
0
});
let mut matching = Vec::new();
for position in 0..kb.len() {
let (_, rule, compiled) = kb.entry_at(position);
if self.test_rule(rule, compiled, facts, &memo)? {
matching.push(rule);
}
}
Ok(matching)
}
fn test_rule(
&self,
rule: &Rule,
compiled: &CompiledRule,
facts: &HashMap<String, Fact>,
memo: &Memo,
) -> Result<bool, EngineError> {
match self.evaluate_condition(&compiled.when, facts, "rule condition", memo) {
Ok(matched) => Ok(matched),
Err(e) if self.return_err_on_failed_rule_evaluation => {
Err(e.in_rule(rule, RulePhase::When))
}
Err(e) => {
log::warn!("Failed testing condition for rule {}: {}", rule.name, e);
Ok(false)
}
}
}
fn evaluate_condition(
&self,
node: &Node,
facts: &HashMap<String, Fact>,
operation: &str,
memo: &Memo,
) -> Result<bool, EngineError> {
operators::boolean(&*self.eval(node, facts, memo)?, operation)
}
fn evaluate_expression(
&self,
node: &Node,
facts: &HashMap<String, Fact>,
) -> std::result::Result<FactValue, EngineError> {
self.eval(node, facts, &Memo::disabled())
.map(Cow::into_owned)
}
fn memoized<'f>(
&self,
slot: Option<MemoSlot>,
memo: &Memo,
compute: impl FnOnce() -> Result<bool, EngineError>,
) -> Result<Cow<'f, FactValue>, EngineError> {
let slot = slot.filter(|&slot| {
memo.is_enabled()
&& !self.any_user_method(&self.knowledge_base.interner().slot_info(slot).methods)
});
if let Some(value) = slot.and_then(|slot| memo.get(slot)) {
return Ok(Cow::Owned(FactValue::Boolean(value)));
}
let value = compute()?;
if let Some(slot) = slot {
memo.set(slot, value);
}
Ok(Cow::Owned(FactValue::Boolean(value)))
}
fn eval<'f>(
&self,
node: &'f Node,
facts: &'f HashMap<String, Fact>,
memo: &Memo,
) -> Result<Cow<'f, FactValue>, EngineError> {
match node {
Node::Const(value) => Ok(Cow::Borrowed(value)),
Node::Variable(name) => facts
.get(name)
.map(|fact| Cow::Borrowed(&fact.value))
.ok_or_else(|| EngineError::UnknownVariable(name.clone())),
Node::Field(base, field) => project(self.eval(base, facts, memo)?, |obj| match obj {
FactValue::Object(obj) => obj.get(field).ok_or_else(|| {
EngineError::EvaluationError(format!("Field '{}' not found", field))
}),
other => Err(EngineError::TypeError(format!(
"Cannot access field '{}' on {}",
field,
other.type_name()
))),
}),
Node::Index(base, index_node) => {
let index = self.eval(index_node, facts, memo)?;
project(self.eval(base, facts, memo)?, |container| {
match (container, &*index) {
(FactValue::Array(items), FactValue::Integer(n)) => {
Ok(&items[array_index(*n, items.len())?])
}
(FactValue::Object(obj), FactValue::String(key)) => {
obj.get(key).ok_or_else(|| {
EngineError::EvaluationError(format!("Key '{}' not found", key))
})
}
(
FactValue::Object(obj),
key @ (FactValue::Integer(_) | FactValue::Boolean(_)),
) => {
let key = operators::to_text(key);
obj.get(&key).ok_or_else(|| {
EngineError::EvaluationError(format!("Key '{}' not found", key))
})
}
_ => Err(EngineError::TypeError(format!(
"Cannot index {} with {}",
container.type_name(),
index.type_name()
))),
}
})
}
Node::Builtin {
name,
function,
args,
} => function(name, &self.evaluate_args(args, facts, memo)?)
.map(Cow::Owned)
.map_err(EngineError::FunctionError),
Node::EngineCall(name, _) => Err(EngineError::EvaluationError(format!(
"{}() can only be used as a statement in a rule's then block",
name
))),
Node::UserCall(name, args) => {
let args = self.evaluate_args(args, facts, memo)?;
let function = self
.functions
.get(name)
.ok_or_else(|| EngineError::UnknownFunction(name.clone()))?;
function(&args)
.map(Cow::Owned)
.map_err(|e| EngineError::FunctionError(format!("{}(): {}", name, e)))
}
Node::Method(target_node, name, args) => {
let target = self.eval(target_node, facts, memo)?;
let args = self.evaluate_args(args, facts, memo)?;
if let Some(result) = functions::call_method(&target, name, &args) {
return result.map(Cow::Owned).map_err(EngineError::FunctionError);
}
let mut receiver = target.into_owned();
self.call_method_mut(&mut receiver, name, &args)
.map(Cow::Owned)
}
Node::Binary(op, left, right) => Ok(Cow::Owned(op(
&*self.eval(left, facts, memo)?,
&*self.eval(right, facts, memo)?,
)?)),
Node::Compare {
test,
left,
right,
memo: slot,
} => self.memoized(*slot, memo, || {
let ordering = operators::compare(
&*self.eval(left, facts, memo)?,
&*self.eval(right, facts, memo)?,
)?;
Ok(ordering.is_some_and(test))
}),
Node::Equal {
negate,
left,
right,
memo: slot,
} => self.memoized(*slot, memo, || {
let equal = operators::equal(
&*self.eval(left, facts, memo)?,
&*self.eval(right, facts, memo)?,
)?;
Ok(equal != *negate)
}),
Node::And {
left,
right,
memo: slot,
} => self.memoized(*slot, memo, || {
Ok(self.evaluate_condition(left, facts, "logical AND", memo)?
&& self.evaluate_condition(right, facts, "logical AND", memo)?)
}),
Node::Or {
left,
right,
memo: slot,
} => self.memoized(*slot, memo, || {
Ok(self.evaluate_condition(left, facts, "logical OR", memo)?
|| self.evaluate_condition(right, facts, "logical OR", memo)?)
}),
Node::Not { inner, memo: slot } => self.memoized(*slot, memo, || {
Ok(!self.evaluate_condition(inner, facts, "negation", memo)?)
}),
Node::Assign(..) => Err(EngineError::EvaluationError(
"Assignment cannot be used as a value".to_string(),
)),
}
}
fn evaluate_args(
&self,
args: &[Node],
facts: &HashMap<String, Fact>,
memo: &Memo,
) -> Result<Vec<FactValue>, EngineError> {
args.iter()
.map(|arg| self.eval(arg, facts, memo).map(Cow::into_owned))
.collect()
}
fn call_method_mut(
&self,
receiver: &mut FactValue,
name: &str,
args: &[FactValue],
) -> Result<FactValue, EngineError> {
if let Some(result) = functions::call_method_mut(receiver, name, args) {
return result.map_err(EngineError::FunctionError);
}
let type_name = receiver.type_name();
match self.methods.call(receiver, name, args) {
Some(result) => {
result.map_err(|e| EngineError::FunctionError(format!("{}(): {}", name, e)))
}
None => Err(EngineError::FunctionError(format!(
"Unknown method '{}' on {}",
name, type_name
))),
}
}
fn execute_action(
&self,
action: &Node,
facts: &mut HashMap<String, Fact>,
state: &mut ExecutionState,
) -> std::result::Result<(), EngineError> {
match action {
Node::Assign(target, value_node) => {
let value = self.evaluate_expression(value_node, facts)?;
self.assign(target, value, facts)
}
Node::EngineCall(name, args) if name == "Retract" => {
let [arg] = args.as_slice() else {
return Err(EngineError::FunctionError(format!(
"Retract() takes 1 argument(s), got {}",
args.len()
)));
};
let rule_name = match self.evaluate_expression(arg, facts)? {
FactValue::String(s) => s,
other => {
return Err(EngineError::TypeError(format!(
"Retract() expects a rule name, got {}",
other.type_name()
)))
}
};
if self.knowledge_base.get_rule(&rule_name).is_none() {
return Err(EngineError::UnknownRule(rule_name));
}
state.retracted.push(rule_name);
Ok(())
}
Node::EngineCall(_, args) => {
if !args.is_empty() {
return Err(EngineError::FunctionError(format!(
"Complete() takes 0 argument(s), got {}",
args.len()
)));
}
state.completed = true;
Ok(())
}
Node::Builtin { name, args, .. } if name == "Changed" || name == "Forget" => {
self.evaluate_expression(action, facts)?;
if let FactValue::String(snippet) = self.evaluate_expression(&args[0], facts)? {
state.changed.push(path_from_snippet(&snippet));
}
Ok(())
}
Node::Method(target, name, args) if target.is_assignable() => {
let args = self.evaluate_args(args, facts, &Memo::disabled())?;
let current = self.eval(target, facts, &Memo::disabled())?;
if let Some(result) = functions::call_method(¤t, name, &args) {
return result.map(|_| ()).map_err(EngineError::FunctionError);
}
let receiver = self.resolve_mut(target, facts)?;
self.call_method_mut(receiver, name, &args).map(|_| ())
}
Node::Builtin { .. } | Node::UserCall(..) | Node::Method(..) => {
self.evaluate_expression(action, facts)?;
Ok(())
}
_ => Err(EngineError::EvaluationError(
"Invalid action expression".to_string(),
)),
}
}
fn assign(
&self,
target: &Node,
value: FactValue,
facts: &mut HashMap<String, Fact>,
) -> Result<(), EngineError> {
let (root, keys) = self.target_path(target, facts)?;
let Some((last, parent_keys)) = keys.split_last() else {
facts.insert(root.clone(), Fact::new(root, value));
return Ok(());
};
let slot = |current: Option<&FactValue>, value| match current {
Some(current) => operators::coerce_assignment(current, value).map_err(|e| {
EngineError::TypeError(format!("{}: {}", describe_path(&root, &keys), e))
}),
None => Ok(value),
};
match (walk_mut(facts, &root, &keys, parent_keys.len())?, last) {
(FactValue::Object(obj), key @ (Key::Field(_) | Key::Index(_))) => {
let field = object_key(key);
let value = slot(obj.get(&field), value)?;
obj.insert(field, value);
Ok(())
}
(FactValue::Array(items), Key::Index(n)) => {
let i = array_index(*n, items.len())?;
items[i] = slot(Some(&items[i]), value)?;
Ok(())
}
(other, key) => Err(key_mismatch(other, key, &describe_path(&root, parent_keys))),
}
}
fn resolve_mut<'f>(
&self,
target: &Node,
facts: &'f mut HashMap<String, Fact>,
) -> Result<&'f mut FactValue, EngineError> {
let (root, keys) = self.target_path(target, facts)?;
walk_mut(facts, &root, &keys, keys.len())
}
fn target_path(
&self,
target: &Node,
facts: &HashMap<String, Fact>,
) -> Result<(String, Vec<Key>), EngineError> {
match target {
Node::Variable(name) => Ok((name.clone(), Vec::new())),
Node::Field(inner, field) => {
let (root, mut keys) = self.target_path(inner, facts)?;
keys.push(Key::Field(field.clone()));
Ok((root, keys))
}
Node::Index(inner, index) => {
let (root, mut keys) = self.target_path(inner, facts)?;
keys.push(match self.evaluate_expression(index, facts)? {
FactValue::Integer(n) => Key::Index(n),
FactValue::String(s) => Key::Field(s),
FactValue::Boolean(b) => Key::Field(b.to_string()),
other => {
return Err(EngineError::TypeError(format!(
"Cannot index with {}",
other.type_name()
)))
}
});
Ok((root, keys))
}
_ => Err(EngineError::EvaluationError(
"Invalid assignment target".to_string(),
)),
}
}
pub fn get_knowledge_base(&self) -> &KnowledgeBase {
&self.knowledge_base
}
}
impl Default for RuleEngine {
fn default() -> Self {
Self::new()
}
}
#[derive(Default)]
struct Cancellation<'a> {
flag: Option<&'a AtomicBool>,
deadline: Option<Instant>,
}
impl Cancellation<'_> {
fn check(&self) -> Result<(), EngineError> {
if self
.flag
.is_some_and(|flag| flag.load(AtomicOrdering::Relaxed))
{
return Err(EngineError::Cancelled);
}
if self
.deadline
.is_some_and(|deadline| Instant::now() >= deadline)
{
return Err(EngineError::TimedOut);
}
Ok(())
}
}
fn walk_mut<'f>(
facts: &'f mut HashMap<String, Fact>,
root: &str,
keys: &[Key],
depth: usize,
) -> Result<&'f mut FactValue, EngineError> {
let fact = facts
.get_mut(root)
.ok_or_else(|| EngineError::UnknownVariable(root.to_string()))?;
let mut current = &mut fact.value;
for (step, key) in keys[..depth].iter().enumerate() {
current = match (current, key) {
(FactValue::Object(obj), key @ (Key::Field(_) | Key::Index(_))) => {
obj.get_mut(&object_key(key)).ok_or_else(|| {
EngineError::EvaluationError(format!(
"Field '{}' not found",
describe_path(root, &keys[..=step])
))
})?
}
(FactValue::Array(items), Key::Index(n)) => {
let i = array_index(*n, items.len())?;
&mut items[i]
}
(other, key) => {
return Err(key_mismatch(
other,
key,
&describe_path(root, &keys[..step]),
))
}
};
}
Ok(current)
}
fn project<'f>(
value: Cow<'f, FactValue>,
part: impl for<'v> FnOnce(&'v FactValue) -> Result<&'v FactValue, EngineError>,
) -> Result<Cow<'f, FactValue>, EngineError> {
match value {
Cow::Borrowed(value) => part(value).map(Cow::Borrowed),
Cow::Owned(value) => part(&value).map(|p| Cow::Owned(p.clone())),
}
}
fn object_key(key: &Key) -> String {
match key {
Key::Field(field) => field.clone(),
Key::Index(n) => n.to_string(),
}
}
fn array_index(n: i64, len: usize) -> Result<usize, EngineError> {
if n < 0 {
Err(EngineError::EvaluationError(format!(
"Invalid array index: {}",
n
)))
} else if n as usize >= len {
Err(EngineError::EvaluationError(format!(
"Index {} out of bounds for array of length {}",
n, len
)))
} else {
Ok(n as usize)
}
}
fn describe_path(root: &str, keys: &[Key]) -> String {
let mut path = root.to_string();
for key in keys {
match key {
Key::Field(field) => write!(path, ".{}", field),
Key::Index(n) => write!(path, "[{}]", n),
}
.unwrap();
}
path
}
fn key_mismatch(value: &FactValue, key: &Key, path: &str) -> EngineError {
EngineError::TypeError(match key {
Key::Field(field) => format!(
"Cannot access field '{}' on {} '{}'",
field,
value.type_name(),
path
),
Key::Index(n) => format!("Cannot index {} '{}' with [{}]", value.type_name(), path, n),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::GrlParser;
fn obj(fields: Vec<(&str, FactValue)>) -> FactValue {
FactValue::Object(
fields
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect(),
)
}
fn order_facts() -> HashMap<String, Fact> {
let order = obj(vec![
(
"customer",
obj(vec![
("age", FactValue::Integer(30)),
(
"address",
obj(vec![("city", FactValue::String("Porto".to_string()))]),
),
]),
),
("total", FactValue::Integer(100)),
]);
HashMap::from([("order".to_string(), Fact::new("order".to_string(), order))])
}
fn run(grl: &str, facts: &mut HashMap<String, Fact>) -> Result<ExecutionResult, EngineError> {
let mut engine = RuleEngine::new();
engine
.add_rule(GrlParser::new().parse_rule(grl).unwrap())
.unwrap();
engine.execute(facts)
}
fn customer_field<'a>(facts: &'a HashMap<String, Fact>, field: &str) -> Option<&'a FactValue> {
facts["order"]
.get_field("customer")?
.as_object()?
.get(field)
}
#[test]
fn test_nested_assignment_updates_deep_field() {
let mut facts = order_facts();
run(
r#"rule R { when order.customer.address.city == "Porto"
then order.customer.address.city = "Lisbon"; order.customer.age += 1; }"#,
&mut facts,
)
.unwrap();
assert_eq!(
customer_field(&facts, "address")
.unwrap()
.as_object()
.unwrap()["city"],
FactValue::String("Lisbon".to_string())
);
assert_eq!(customer_field(&facts, "age"), Some(&FactValue::Integer(31)));
}
#[test]
fn test_nested_assignment_creates_leaf_field() {
let mut facts = order_facts();
run(
r#"rule R { when true then order.customer.vip = true; Retract("R"); }"#,
&mut facts,
)
.unwrap();
assert_eq!(
customer_field(&facts, "vip"),
Some(&FactValue::Boolean(true))
);
}
#[test]
fn test_nested_assignment_missing_intermediate_field() {
let mut facts = order_facts();
let err = run(
"rule R { when true then order.shipping.city = \"X\"; }",
&mut facts,
)
.unwrap_err();
assert!(err.to_string().contains("order.shipping"), "{}", err);
}
#[test]
fn test_nested_assignment_through_non_object() {
let mut facts = order_facts();
let err = run(
"rule R { when true then order.total.cents = 5; }",
&mut facts,
)
.unwrap_err();
assert!(matches!(err.root(), EngineError::TypeError(_)), "{}", err);
assert!(err.to_string().contains("order.total"), "{}", err);
}
#[test]
fn test_nested_assignment_unknown_root() {
let mut facts = order_facts();
let err = run("rule R { when true then invoice.total = 1; }", &mut facts).unwrap_err();
assert!(matches!(err.root(), EngineError::UnknownVariable(name) if name == "invoice"));
}
}