1use crate::bitset::BitSet;
2use crate::compiled::{path_from_snippet, CompiledRule, MemoSlot, Node, Path as FactPath};
3use crate::facts::{Fact, FactValue};
4use crate::functions::{self, Function};
5use crate::knowledge_base::KnowledgeBase;
6use crate::methods::MethodRegistry;
7use crate::operators;
8use crate::rule::Rule;
9use serde::de::DeserializeOwned;
10use serde::Serialize;
11use std::borrow::Cow;
12use std::cell::Cell;
13use std::collections::HashMap;
14use std::fmt::{self, Write};
15use std::path::Path;
16use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
17use std::time::{Duration, Instant};
18use thiserror::Error;
19
20pub const DEFAULT_MAX_CYCLES: usize = 5000;
22
23#[derive(Error, Debug)]
25#[non_exhaustive]
26pub enum EngineError {
27 #[error("Evaluation error: {0}")]
29 EvaluationError(String),
30 #[error("Unknown variable: {0}")]
32 UnknownVariable(String),
33 #[error("Type error: {0}")]
35 TypeError(String),
36 #[error("Division by zero")]
38 DivisionByZero,
39 #[error("Unknown function: {0}")]
41 UnknownFunction(String),
42 #[error("Function error: {0}")]
44 FunctionError(String),
45 #[error("Unknown rule: {0}")]
47 UnknownRule(String),
48 #[error("Rules fired {0} times without settling; a rule's actions may never make its condition false (consider Retract)")]
50 MaxCyclesExceeded(usize),
51 #[error("Execution was cancelled")]
53 Cancelled,
54 #[error("Execution timed out")]
56 TimedOut,
57 #[error("Rule '{rule}' failed in its {phase} scope: {source}")]
59 InRule {
60 rule: String,
62 phase: RulePhase,
64 #[source]
66 source: Box<EngineError>,
67 },
68}
69
70impl EngineError {
71 pub fn root(&self) -> &EngineError {
73 match self {
74 EngineError::InRule { source, .. } => source.root(),
75 other => other,
76 }
77 }
78
79 fn in_rule(self, rule: &Rule, phase: RulePhase) -> EngineError {
80 EngineError::InRule {
81 rule: rule.name.clone(),
82 phase,
83 source: Box::new(self),
84 }
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum RulePhase {
91 When,
93 Then,
95}
96
97impl fmt::Display for RulePhase {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 f.write_str(match self {
100 RulePhase::When => "when",
101 RulePhase::Then => "then",
102 })
103 }
104}
105
106pub trait EngineListener: Send + Sync {
109 fn begin_cycle(&self, _cycle: u64) {}
111 fn evaluate_rule(&self, _cycle: u64, _rule: &Rule, _matched: bool) {}
113 fn execute_rule(&self, _cycle: u64, _rule: &Rule) {}
115}
116
117#[derive(Debug, Clone)]
119pub struct ExecutionResult {
120 pub rules_fired: Vec<String>,
122 pub execution_time_ms: u128,
124}
125
126impl ExecutionResult {
127 pub fn new() -> Self {
129 Self {
130 rules_fired: Vec::new(),
131 execution_time_ms: 0,
132 }
133 }
134}
135
136impl Default for ExecutionResult {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142#[derive(Default)]
144struct ExecutionState {
145 retracted: Vec<String>,
147 completed: bool,
148 changed: Vec<FactPath>,
150}
151
152struct Agenda {
154 dirty: BitSet,
157 matching: BitSet,
159 retracted: BitSet,
160}
161
162struct Memo {
165 slots: Vec<Cell<u8>>,
166}
167
168impl Memo {
169 const UNKNOWN: u8 = 0;
170
171 fn new(slot_count: usize) -> Self {
172 Memo {
173 slots: vec![Cell::new(Self::UNKNOWN); slot_count],
174 }
175 }
176
177 fn disabled() -> Self {
179 Memo { slots: Vec::new() }
180 }
181
182 fn is_enabled(&self) -> bool {
183 !self.slots.is_empty()
184 }
185
186 fn get(&self, slot: MemoSlot) -> Option<bool> {
187 match self.slots[slot].get() {
188 Self::UNKNOWN => None,
189 value => Some(value == 2),
190 }
191 }
192
193 fn set(&self, slot: MemoSlot, value: bool) {
194 self.slots[slot].set(if value { 2 } else { 1 });
195 }
196
197 fn forget(&self, slot: MemoSlot) {
198 self.slots[slot].set(Self::UNKNOWN);
199 }
200}
201
202enum Key {
204 Field(String),
205 Index(i64),
206}
207
208pub struct RuleEngine {
214 knowledge_base: KnowledgeBase,
215 functions: HashMap<String, Box<Function>>,
216 methods: MethodRegistry,
217 max_cycles: usize,
218 return_err_on_failed_rule_evaluation: bool,
219 listeners: Vec<Box<dyn EngineListener>>,
220 condition_caching: bool,
221}
222
223impl RuleEngine {
224 pub fn new() -> Self {
227 Self {
228 knowledge_base: KnowledgeBase::new(),
229 functions: HashMap::new(),
230 methods: MethodRegistry::default(),
231 max_cycles: DEFAULT_MAX_CYCLES,
232 return_err_on_failed_rule_evaluation: false,
233 listeners: Vec::new(),
234 condition_caching: true,
235 }
236 }
237
238 pub fn add_rule(&mut self, rule: Rule) -> Result<(), String> {
240 self.knowledge_base.add_rule(rule)
241 }
242
243 pub fn add_rules_from_str(&mut self, grl: &str) -> Result<usize, String> {
245 self.knowledge_base.add_rules_from_str(grl)
246 }
247
248 pub fn add_rules_from_file(&mut self, path: impl AsRef<Path>) -> Result<usize, String> {
250 self.knowledge_base.add_rules_from_file(path)
251 }
252
253 pub fn add_rules_from_dir(&mut self, dir: impl AsRef<Path>) -> Result<usize, String> {
255 self.knowledge_base.add_rules_from_dir(dir)
256 }
257
258 pub fn set_max_cycles(&mut self, max_cycles: usize) {
261 self.max_cycles = max_cycles;
262 }
263
264 pub fn set_return_err_on_failed_rule_evaluation(&mut self, enabled: bool) {
268 self.return_err_on_failed_rule_evaluation = enabled;
269 }
270
271 pub fn set_condition_caching(&mut self, enabled: bool) {
274 self.condition_caching = enabled;
275 }
276
277 pub fn add_listener(&mut self, listener: impl EngineListener + 'static) {
280 self.listeners.push(Box::new(listener));
281 }
282
283 pub fn register_function<F>(&mut self, name: &str, function: F) -> Result<(), String>
285 where
286 F: Fn(&[FactValue]) -> Result<FactValue, String> + Send + Sync + 'static,
287 {
288 if functions::is_reserved(name) {
289 return Err(format!("'{}' is a built-in function", name));
290 }
291 if self.functions.contains_key(name) {
292 return Err(format!("Function '{}' is already registered", name));
293 }
294 self.functions.insert(name.to_string(), Box::new(function));
295 Ok(())
296 }
297
298 pub fn register_method<F>(&mut self, name: &str, method: F) -> Result<(), String>
304 where
305 F: Fn(&mut FactValue, &[FactValue]) -> Result<FactValue, String> + Send + Sync + 'static,
306 {
307 self.methods.register(name, Box::new(method))
308 }
309
310 pub fn register_typed_method<T, A, R, F>(&mut self, name: &str, method: F) -> Result<(), String>
339 where
340 T: Serialize + DeserializeOwned + 'static,
341 A: DeserializeOwned + 'static,
342 R: Serialize + 'static,
343 F: Fn(&mut T, A) -> Result<R, String> + Send + Sync + 'static,
344 {
345 self.methods.register_typed(name, method)
346 }
347
348 pub fn execute(
363 &self,
364 facts: &mut HashMap<String, Fact>,
365 ) -> Result<ExecutionResult, EngineError> {
366 self.run(facts, &Cancellation::default())
367 }
368
369 pub fn execute_with_cancel(
372 &self,
373 facts: &mut HashMap<String, Fact>,
374 cancel: &AtomicBool,
375 ) -> Result<ExecutionResult, EngineError> {
376 let cancellation = Cancellation {
377 flag: Some(cancel),
378 deadline: None,
379 };
380 self.run(facts, &cancellation)
381 }
382
383 pub fn execute_with_timeout(
387 &self,
388 facts: &mut HashMap<String, Fact>,
389 timeout: Duration,
390 ) -> Result<ExecutionResult, EngineError> {
391 let cancellation = Cancellation {
392 flag: None,
393 deadline: Some(Instant::now() + timeout),
394 };
395 self.run(facts, &cancellation)
396 }
397
398 fn run(
399 &self,
400 facts: &mut HashMap<String, Fact>,
401 cancel: &Cancellation,
402 ) -> Result<ExecutionResult, EngineError> {
403 let start_time = Instant::now();
404 let mut result = ExecutionResult::new();
405 let mut state = ExecutionState::default();
406 let kb = &self.knowledge_base;
407 let n = kb.len();
408
409 let mut agenda = Agenda {
412 dirty: BitSet::new(n, true),
413 matching: BitSet::new(n, false),
414 retracted: BitSet::new(n, false),
415 };
416 let memo = Memo::new(if self.condition_caching {
417 kb.interner().slot_count()
418 } else {
419 0
420 });
421
422 let full_evaluation =
425 !self.listeners.is_empty() || self.return_err_on_failed_rule_evaluation;
426 let mut always = BitSet::new(n, false);
428 let mut always_positions = Vec::new();
429 if !full_evaluation && !kb.all_conditions_skippable(!self.methods.is_empty()) {
430 for position in 0..n {
431 if !self.is_skippable(kb.entry_at(position).2) {
432 always.insert(position);
433 always_positions.push(position);
434 agenda.dirty.remove(position);
435 }
436 }
437 }
438
439 let mut cycle: u64 = 0;
440 loop {
441 cancel.check()?;
442 self.listeners.iter().for_each(|l| l.begin_cycle(cycle + 1));
443
444 let selected = if full_evaluation {
445 self.select_by_full_evaluation(cycle + 1, &mut agenda, facts, &memo, cancel)?
446 } else {
447 self.select_incrementally(&mut agenda, &always_positions, facts, &memo, cancel)?
448 };
449
450 let Some(position) = selected else { break };
451 cycle += 1;
452 if cycle > self.max_cycles as u64 {
453 return Err(EngineError::MaxCyclesExceeded(self.max_cycles));
454 }
455
456 let (_, rule, compiled) = kb.entry_at(position);
457 self.listeners
458 .iter()
459 .for_each(|l| l.execute_rule(cycle, rule));
460 for action in &compiled.then {
461 self.execute_action(action, facts, &mut state)
462 .map_err(|e| e.in_rule(rule, RulePhase::Then))?;
463 }
464 result.rules_fired.push(rule.name.clone());
465 if state.completed {
466 break;
467 }
468
469 for name in state.retracted.drain(..) {
470 let position = kb.position_of(kb.index_of(&name).expect("checked by Retract"));
471 agenda.retracted.insert(position);
472 agenda.matching.remove(position);
473 agenda.dirty.remove(position);
474 }
475 for written in compiled.writes.iter().chain(&state.changed) {
476 self.invalidate(written, &mut agenda, &always, &memo);
477 }
478 state.changed.clear();
479 }
480
481 result.execution_time_ms = start_time.elapsed().as_millis();
482 Ok(result)
483 }
484
485 fn select_by_full_evaluation(
488 &self,
489 cycle: u64,
490 agenda: &mut Agenda,
491 facts: &HashMap<String, Fact>,
492 memo: &Memo,
493 cancel: &Cancellation,
494 ) -> Result<Option<usize>, EngineError> {
495 let mut selected = None;
496 for position in 0..self.knowledge_base.len() {
497 cancel.check()?;
498 if agenda.retracted.contains(position) {
499 continue;
500 }
501 let (_, rule, compiled) = self.knowledge_base.entry_at(position);
502 let matched = if agenda.dirty.contains(position) || !self.is_cacheable(compiled) {
503 self.evaluate_into(position, rule, compiled, agenda, facts, memo)?
504 } else {
505 agenda.matching.contains(position)
506 };
507 self.listeners
508 .iter()
509 .for_each(|l| l.evaluate_rule(cycle, rule, matched));
510 if matched && selected.is_none() {
511 selected = Some(position);
512 }
513 }
514 Ok(selected)
515 }
516
517 fn select_incrementally(
521 &self,
522 agenda: &mut Agenda,
523 always: &[usize],
524 facts: &HashMap<String, Fact>,
525 memo: &Memo,
526 cancel: &Cancellation,
527 ) -> Result<Option<usize>, EngineError> {
528 let mut best = agenda.matching.next_from(0);
529
530 for &position in always {
531 if agenda.retracted.contains(position) {
532 continue;
533 }
534 cancel.check()?;
535 let (_, rule, compiled) = self.knowledge_base.entry_at(position);
536 if self.test_rule(rule, compiled, facts, memo)? && best.is_none_or(|b| position < b) {
537 best = Some(position);
538 }
539 }
540
541 let mut cursor = 0;
542 while let Some(position) = agenda.dirty.next_from(cursor) {
543 if best.is_some_and(|b| position >= b) {
544 break;
545 }
546 cursor = position + 1;
547 cancel.check()?;
548 let (_, rule, compiled) = self.knowledge_base.entry_at(position);
549 if self.evaluate_into(position, rule, compiled, agenda, facts, memo)? {
550 best = Some(position);
551 break;
552 }
553 }
554 Ok(best)
555 }
556
557 fn evaluate_into(
559 &self,
560 position: usize,
561 rule: &Rule,
562 compiled: &CompiledRule,
563 agenda: &mut Agenda,
564 facts: &HashMap<String, Fact>,
565 memo: &Memo,
566 ) -> Result<bool, EngineError> {
567 let matched = self.test_rule(rule, compiled, facts, memo)?;
568 if self.is_cacheable(compiled) {
569 agenda.dirty.remove(position);
570 if matched {
571 agenda.matching.insert(position);
572 }
573 }
574 Ok(matched)
575 }
576
577 fn is_cacheable(&self, compiled: &CompiledRule) -> bool {
579 self.condition_caching && compiled.deterministic && !self.calls_user_methods(compiled)
580 }
581
582 fn is_skippable(&self, compiled: &CompiledRule) -> bool {
584 !compiled.has_effects && !self.calls_user_methods(compiled)
585 }
586
587 fn calls_user_methods(&self, compiled: &CompiledRule) -> bool {
588 self.any_user_method(&compiled.condition_methods)
589 }
590
591 fn any_user_method(&self, names: &[String]) -> bool {
592 !self.methods.is_empty() && names.iter().any(|name| self.methods.contains(name))
593 }
594
595 fn invalidate(&self, written: &[String], agenda: &mut Agenda, always: &BitSet, memo: &Memo) {
598 let kb = &self.knowledge_base;
599 kb.readers().overlapping(written, |index| {
600 let position = kb.position_of(index);
601 if !agenda.retracted.contains(position) && !always.contains(position) {
602 agenda.matching.remove(position);
603 agenda.dirty.insert(position);
604 }
605 });
606 if memo.is_enabled() {
607 kb.interner()
608 .slot_readers()
609 .overlapping(written, |slot| memo.forget(slot));
610 }
611 }
612
613 pub fn fetch_matching_rules(
616 &self,
617 facts: &HashMap<String, Fact>,
618 ) -> Result<Vec<&Rule>, EngineError> {
619 let kb = &self.knowledge_base;
620 let memo = Memo::new(if self.condition_caching {
621 kb.interner().slot_count()
622 } else {
623 0
624 });
625 let mut matching = Vec::new();
626 for position in 0..kb.len() {
627 let (_, rule, compiled) = kb.entry_at(position);
628 if self.test_rule(rule, compiled, facts, &memo)? {
629 matching.push(rule);
630 }
631 }
632 Ok(matching)
633 }
634
635 fn test_rule(
637 &self,
638 rule: &Rule,
639 compiled: &CompiledRule,
640 facts: &HashMap<String, Fact>,
641 memo: &Memo,
642 ) -> Result<bool, EngineError> {
643 match self.evaluate_condition(&compiled.when, facts, "rule condition", memo) {
644 Ok(matched) => Ok(matched),
645 Err(e) if self.return_err_on_failed_rule_evaluation => {
646 Err(e.in_rule(rule, RulePhase::When))
647 }
648 Err(e) => {
649 log::warn!("Failed testing condition for rule {}: {}", rule.name, e);
650 Ok(false)
651 }
652 }
653 }
654
655 fn evaluate_condition(
656 &self,
657 node: &Node,
658 facts: &HashMap<String, Fact>,
659 operation: &str,
660 memo: &Memo,
661 ) -> Result<bool, EngineError> {
662 operators::boolean(&*self.eval(node, facts, memo)?, operation)
663 }
664
665 fn evaluate_expression(
667 &self,
668 node: &Node,
669 facts: &HashMap<String, Fact>,
670 ) -> std::result::Result<FactValue, EngineError> {
671 self.eval(node, facts, &Memo::disabled())
672 .map(Cow::into_owned)
673 }
674
675 fn memoized<'f>(
677 &self,
678 slot: Option<MemoSlot>,
679 memo: &Memo,
680 compute: impl FnOnce() -> Result<bool, EngineError>,
681 ) -> Result<Cow<'f, FactValue>, EngineError> {
682 let slot = slot.filter(|&slot| {
683 memo.is_enabled()
684 && !self.any_user_method(&self.knowledge_base.interner().slot_info(slot).methods)
685 });
686 if let Some(value) = slot.and_then(|slot| memo.get(slot)) {
687 return Ok(Cow::Owned(FactValue::Boolean(value)));
688 }
689 let value = compute()?;
690 if let Some(slot) = slot {
691 memo.set(slot, value);
692 }
693 Ok(Cow::Owned(FactValue::Boolean(value)))
694 }
695
696 fn eval<'f>(
699 &self,
700 node: &'f Node,
701 facts: &'f HashMap<String, Fact>,
702 memo: &Memo,
703 ) -> Result<Cow<'f, FactValue>, EngineError> {
704 match node {
705 Node::Const(value) => Ok(Cow::Borrowed(value)),
706
707 Node::Variable(name) => facts
708 .get(name)
709 .map(|fact| Cow::Borrowed(&fact.value))
710 .ok_or_else(|| EngineError::UnknownVariable(name.clone())),
711
712 Node::Field(base, field) => project(self.eval(base, facts, memo)?, |obj| match obj {
713 FactValue::Object(obj) => obj.get(field).ok_or_else(|| {
714 EngineError::EvaluationError(format!("Field '{}' not found", field))
715 }),
716 other => Err(EngineError::TypeError(format!(
717 "Cannot access field '{}' on {}",
718 field,
719 other.type_name()
720 ))),
721 }),
722
723 Node::Index(base, index_node) => {
724 let index = self.eval(index_node, facts, memo)?;
725 project(self.eval(base, facts, memo)?, |container| {
726 match (container, &*index) {
727 (FactValue::Array(items), FactValue::Integer(n)) => {
728 Ok(&items[array_index(*n, items.len())?])
729 }
730 (FactValue::Object(obj), FactValue::String(key)) => {
731 obj.get(key).ok_or_else(|| {
732 EngineError::EvaluationError(format!("Key '{}' not found", key))
733 })
734 }
735 (
737 FactValue::Object(obj),
738 key @ (FactValue::Integer(_) | FactValue::Boolean(_)),
739 ) => {
740 let key = operators::to_text(key);
741 obj.get(&key).ok_or_else(|| {
742 EngineError::EvaluationError(format!("Key '{}' not found", key))
743 })
744 }
745 _ => Err(EngineError::TypeError(format!(
746 "Cannot index {} with {}",
747 container.type_name(),
748 index.type_name()
749 ))),
750 }
751 })
752 }
753
754 Node::Builtin {
755 name,
756 function,
757 args,
758 } => function(name, &self.evaluate_args(args, facts, memo)?)
759 .map(Cow::Owned)
760 .map_err(EngineError::FunctionError),
761
762 Node::EngineCall(name, _) => Err(EngineError::EvaluationError(format!(
763 "{}() can only be used as a statement in a rule's then block",
764 name
765 ))),
766
767 Node::UserCall(name, args) => {
768 let args = self.evaluate_args(args, facts, memo)?;
769 let function = self
770 .functions
771 .get(name)
772 .ok_or_else(|| EngineError::UnknownFunction(name.clone()))?;
773 function(&args)
774 .map(Cow::Owned)
775 .map_err(|e| EngineError::FunctionError(format!("{}(): {}", name, e)))
776 }
777
778 Node::Method(target_node, name, args) => {
779 let target = self.eval(target_node, facts, memo)?;
780 let args = self.evaluate_args(args, facts, memo)?;
781 if let Some(result) = functions::call_method(&target, name, &args) {
782 return result.map(Cow::Owned).map_err(EngineError::FunctionError);
783 }
784 let mut receiver = target.into_owned();
786 self.call_method_mut(&mut receiver, name, &args)
787 .map(Cow::Owned)
788 }
789
790 Node::Binary(op, left, right) => Ok(Cow::Owned(op(
791 &*self.eval(left, facts, memo)?,
792 &*self.eval(right, facts, memo)?,
793 )?)),
794 Node::Compare {
795 test,
796 left,
797 right,
798 memo: slot,
799 } => self.memoized(*slot, memo, || {
800 let ordering = operators::compare(
801 &*self.eval(left, facts, memo)?,
802 &*self.eval(right, facts, memo)?,
803 )?;
804 Ok(ordering.is_some_and(test))
805 }),
806 Node::Equal {
807 negate,
808 left,
809 right,
810 memo: slot,
811 } => self.memoized(*slot, memo, || {
812 let equal = operators::equal(
813 &*self.eval(left, facts, memo)?,
814 &*self.eval(right, facts, memo)?,
815 )?;
816 Ok(equal != *negate)
817 }),
818
819 Node::And {
821 left,
822 right,
823 memo: slot,
824 } => self.memoized(*slot, memo, || {
825 Ok(self.evaluate_condition(left, facts, "logical AND", memo)?
826 && self.evaluate_condition(right, facts, "logical AND", memo)?)
827 }),
828 Node::Or {
829 left,
830 right,
831 memo: slot,
832 } => self.memoized(*slot, memo, || {
833 Ok(self.evaluate_condition(left, facts, "logical OR", memo)?
834 || self.evaluate_condition(right, facts, "logical OR", memo)?)
835 }),
836 Node::Not { inner, memo: slot } => self.memoized(*slot, memo, || {
837 Ok(!self.evaluate_condition(inner, facts, "negation", memo)?)
838 }),
839
840 Node::Assign(..) => Err(EngineError::EvaluationError(
841 "Assignment cannot be used as a value".to_string(),
842 )),
843 }
844 }
845
846 fn evaluate_args(
847 &self,
848 args: &[Node],
849 facts: &HashMap<String, Fact>,
850 memo: &Memo,
851 ) -> Result<Vec<FactValue>, EngineError> {
852 args.iter()
853 .map(|arg| self.eval(arg, facts, memo).map(Cow::into_owned))
854 .collect()
855 }
856
857 fn call_method_mut(
860 &self,
861 receiver: &mut FactValue,
862 name: &str,
863 args: &[FactValue],
864 ) -> Result<FactValue, EngineError> {
865 if let Some(result) = functions::call_method_mut(receiver, name, args) {
866 return result.map_err(EngineError::FunctionError);
867 }
868 let type_name = receiver.type_name();
869 match self.methods.call(receiver, name, args) {
870 Some(result) => {
871 result.map_err(|e| EngineError::FunctionError(format!("{}(): {}", name, e)))
872 }
873 None => Err(EngineError::FunctionError(format!(
874 "Unknown method '{}' on {}",
875 name, type_name
876 ))),
877 }
878 }
879
880 fn execute_action(
881 &self,
882 action: &Node,
883 facts: &mut HashMap<String, Fact>,
884 state: &mut ExecutionState,
885 ) -> std::result::Result<(), EngineError> {
886 match action {
887 Node::Assign(target, value_node) => {
888 let value = self.evaluate_expression(value_node, facts)?;
889 self.assign(target, value, facts)
890 }
891
892 Node::EngineCall(name, args) if name == "Retract" => {
893 let [arg] = args.as_slice() else {
894 return Err(EngineError::FunctionError(format!(
895 "Retract() takes 1 argument(s), got {}",
896 args.len()
897 )));
898 };
899 let rule_name = match self.evaluate_expression(arg, facts)? {
900 FactValue::String(s) => s,
901 other => {
902 return Err(EngineError::TypeError(format!(
903 "Retract() expects a rule name, got {}",
904 other.type_name()
905 )))
906 }
907 };
908 if self.knowledge_base.get_rule(&rule_name).is_none() {
909 return Err(EngineError::UnknownRule(rule_name));
910 }
911 state.retracted.push(rule_name);
912 Ok(())
913 }
914
915 Node::EngineCall(_, args) => {
916 if !args.is_empty() {
918 return Err(EngineError::FunctionError(format!(
919 "Complete() takes 0 argument(s), got {}",
920 args.len()
921 )));
922 }
923 state.completed = true;
924 Ok(())
925 }
926
927 Node::Builtin { name, args, .. } if name == "Changed" || name == "Forget" => {
930 self.evaluate_expression(action, facts)?;
931 if let FactValue::String(snippet) = self.evaluate_expression(&args[0], facts)? {
932 state.changed.push(path_from_snippet(&snippet));
933 }
934 Ok(())
935 }
936
937 Node::Method(target, name, args) if target.is_assignable() => {
940 let args = self.evaluate_args(args, facts, &Memo::disabled())?;
941 let current = self.eval(target, facts, &Memo::disabled())?;
942 if let Some(result) = functions::call_method(¤t, name, &args) {
943 return result.map(|_| ()).map_err(EngineError::FunctionError);
945 }
946 let receiver = self.resolve_mut(target, facts)?;
947 self.call_method_mut(receiver, name, &args).map(|_| ())
948 }
949
950 Node::Builtin { .. } | Node::UserCall(..) | Node::Method(..) => {
952 self.evaluate_expression(action, facts)?;
953 Ok(())
954 }
955
956 _ => Err(EngineError::EvaluationError(
957 "Invalid action expression".to_string(),
958 )),
959 }
960 }
961
962 fn assign(
964 &self,
965 target: &Node,
966 value: FactValue,
967 facts: &mut HashMap<String, Fact>,
968 ) -> Result<(), EngineError> {
969 let (root, keys) = self.target_path(target, facts)?;
970
971 let Some((last, parent_keys)) = keys.split_last() else {
972 facts.insert(root.clone(), Fact::new(root, value));
973 return Ok(());
974 };
975
976 let slot = |current: Option<&FactValue>, value| match current {
978 Some(current) => operators::coerce_assignment(current, value).map_err(|e| {
979 EngineError::TypeError(format!("{}: {}", describe_path(&root, &keys), e))
980 }),
981 None => Ok(value),
982 };
983 match (walk_mut(facts, &root, &keys, parent_keys.len())?, last) {
984 (FactValue::Object(obj), key @ (Key::Field(_) | Key::Index(_))) => {
985 let field = object_key(key);
986 let value = slot(obj.get(&field), value)?;
987 obj.insert(field, value);
988 Ok(())
989 }
990 (FactValue::Array(items), Key::Index(n)) => {
991 let i = array_index(*n, items.len())?;
992 items[i] = slot(Some(&items[i]), value)?;
993 Ok(())
994 }
995 (other, key) => Err(key_mismatch(other, key, &describe_path(&root, parent_keys))),
996 }
997 }
998
999 fn resolve_mut<'f>(
1001 &self,
1002 target: &Node,
1003 facts: &'f mut HashMap<String, Fact>,
1004 ) -> Result<&'f mut FactValue, EngineError> {
1005 let (root, keys) = self.target_path(target, facts)?;
1006 walk_mut(facts, &root, &keys, keys.len())
1007 }
1008
1009 fn target_path(
1013 &self,
1014 target: &Node,
1015 facts: &HashMap<String, Fact>,
1016 ) -> Result<(String, Vec<Key>), EngineError> {
1017 match target {
1018 Node::Variable(name) => Ok((name.clone(), Vec::new())),
1019 Node::Field(inner, field) => {
1020 let (root, mut keys) = self.target_path(inner, facts)?;
1021 keys.push(Key::Field(field.clone()));
1022 Ok((root, keys))
1023 }
1024 Node::Index(inner, index) => {
1025 let (root, mut keys) = self.target_path(inner, facts)?;
1026 keys.push(match self.evaluate_expression(index, facts)? {
1027 FactValue::Integer(n) => Key::Index(n),
1028 FactValue::String(s) => Key::Field(s),
1029 FactValue::Boolean(b) => Key::Field(b.to_string()),
1030 other => {
1031 return Err(EngineError::TypeError(format!(
1032 "Cannot index with {}",
1033 other.type_name()
1034 )))
1035 }
1036 });
1037 Ok((root, keys))
1038 }
1039 _ => Err(EngineError::EvaluationError(
1040 "Invalid assignment target".to_string(),
1041 )),
1042 }
1043 }
1044
1045 pub fn get_knowledge_base(&self) -> &KnowledgeBase {
1047 &self.knowledge_base
1048 }
1049}
1050
1051impl Default for RuleEngine {
1052 fn default() -> Self {
1053 Self::new()
1054 }
1055}
1056
1057#[derive(Default)]
1059struct Cancellation<'a> {
1060 flag: Option<&'a AtomicBool>,
1061 deadline: Option<Instant>,
1062}
1063
1064impl Cancellation<'_> {
1065 fn check(&self) -> Result<(), EngineError> {
1066 if self
1067 .flag
1068 .is_some_and(|flag| flag.load(AtomicOrdering::Relaxed))
1069 {
1070 return Err(EngineError::Cancelled);
1071 }
1072 if self
1073 .deadline
1074 .is_some_and(|deadline| Instant::now() >= deadline)
1075 {
1076 return Err(EngineError::TimedOut);
1077 }
1078 Ok(())
1079 }
1080}
1081
1082fn walk_mut<'f>(
1085 facts: &'f mut HashMap<String, Fact>,
1086 root: &str,
1087 keys: &[Key],
1088 depth: usize,
1089) -> Result<&'f mut FactValue, EngineError> {
1090 let fact = facts
1091 .get_mut(root)
1092 .ok_or_else(|| EngineError::UnknownVariable(root.to_string()))?;
1093 let mut current = &mut fact.value;
1094 for (step, key) in keys[..depth].iter().enumerate() {
1095 current = match (current, key) {
1096 (FactValue::Object(obj), key @ (Key::Field(_) | Key::Index(_))) => {
1097 obj.get_mut(&object_key(key)).ok_or_else(|| {
1098 EngineError::EvaluationError(format!(
1099 "Field '{}' not found",
1100 describe_path(root, &keys[..=step])
1101 ))
1102 })?
1103 }
1104 (FactValue::Array(items), Key::Index(n)) => {
1105 let i = array_index(*n, items.len())?;
1106 &mut items[i]
1107 }
1108 (other, key) => {
1109 return Err(key_mismatch(
1110 other,
1111 key,
1112 &describe_path(root, &keys[..step]),
1113 ))
1114 }
1115 };
1116 }
1117 Ok(current)
1118}
1119
1120fn project<'f>(
1123 value: Cow<'f, FactValue>,
1124 part: impl for<'v> FnOnce(&'v FactValue) -> Result<&'v FactValue, EngineError>,
1125) -> Result<Cow<'f, FactValue>, EngineError> {
1126 match value {
1127 Cow::Borrowed(value) => part(value).map(Cow::Borrowed),
1128 Cow::Owned(value) => part(&value).map(|p| Cow::Owned(p.clone())),
1129 }
1130}
1131
1132fn object_key(key: &Key) -> String {
1135 match key {
1136 Key::Field(field) => field.clone(),
1137 Key::Index(n) => n.to_string(),
1138 }
1139}
1140
1141fn array_index(n: i64, len: usize) -> Result<usize, EngineError> {
1142 if n < 0 {
1143 Err(EngineError::EvaluationError(format!(
1144 "Invalid array index: {}",
1145 n
1146 )))
1147 } else if n as usize >= len {
1148 Err(EngineError::EvaluationError(format!(
1149 "Index {} out of bounds for array of length {}",
1150 n, len
1151 )))
1152 } else {
1153 Ok(n as usize)
1154 }
1155}
1156
1157fn describe_path(root: &str, keys: &[Key]) -> String {
1158 let mut path = root.to_string();
1159 for key in keys {
1160 match key {
1161 Key::Field(field) => write!(path, ".{}", field),
1162 Key::Index(n) => write!(path, "[{}]", n),
1163 }
1164 .unwrap();
1165 }
1166 path
1167}
1168
1169fn key_mismatch(value: &FactValue, key: &Key, path: &str) -> EngineError {
1170 EngineError::TypeError(match key {
1171 Key::Field(field) => format!(
1172 "Cannot access field '{}' on {} '{}'",
1173 field,
1174 value.type_name(),
1175 path
1176 ),
1177 Key::Index(n) => format!("Cannot index {} '{}' with [{}]", value.type_name(), path, n),
1178 })
1179}
1180
1181#[cfg(test)]
1182mod tests {
1183 use super::*;
1184 use crate::parser::GrlParser;
1185
1186 fn obj(fields: Vec<(&str, FactValue)>) -> FactValue {
1187 FactValue::Object(
1188 fields
1189 .into_iter()
1190 .map(|(k, v)| (k.to_string(), v))
1191 .collect(),
1192 )
1193 }
1194
1195 fn order_facts() -> HashMap<String, Fact> {
1196 let order = obj(vec![
1197 (
1198 "customer",
1199 obj(vec![
1200 ("age", FactValue::Integer(30)),
1201 (
1202 "address",
1203 obj(vec![("city", FactValue::String("Porto".to_string()))]),
1204 ),
1205 ]),
1206 ),
1207 ("total", FactValue::Integer(100)),
1208 ]);
1209 HashMap::from([("order".to_string(), Fact::new("order".to_string(), order))])
1210 }
1211
1212 fn run(grl: &str, facts: &mut HashMap<String, Fact>) -> Result<ExecutionResult, EngineError> {
1213 let mut engine = RuleEngine::new();
1214 engine
1215 .add_rule(GrlParser::new().parse_rule(grl).unwrap())
1216 .unwrap();
1217 engine.execute(facts)
1218 }
1219
1220 fn customer_field<'a>(facts: &'a HashMap<String, Fact>, field: &str) -> Option<&'a FactValue> {
1221 facts["order"]
1222 .get_field("customer")?
1223 .as_object()?
1224 .get(field)
1225 }
1226
1227 #[test]
1228 fn test_nested_assignment_updates_deep_field() {
1229 let mut facts = order_facts();
1230 run(
1231 r#"rule R { when order.customer.address.city == "Porto"
1232 then order.customer.address.city = "Lisbon"; order.customer.age += 1; }"#,
1233 &mut facts,
1234 )
1235 .unwrap();
1236
1237 assert_eq!(
1238 customer_field(&facts, "address")
1239 .unwrap()
1240 .as_object()
1241 .unwrap()["city"],
1242 FactValue::String("Lisbon".to_string())
1243 );
1244 assert_eq!(customer_field(&facts, "age"), Some(&FactValue::Integer(31)));
1245 }
1246
1247 #[test]
1248 fn test_nested_assignment_creates_leaf_field() {
1249 let mut facts = order_facts();
1250 run(
1251 r#"rule R { when true then order.customer.vip = true; Retract("R"); }"#,
1252 &mut facts,
1253 )
1254 .unwrap();
1255 assert_eq!(
1256 customer_field(&facts, "vip"),
1257 Some(&FactValue::Boolean(true))
1258 );
1259 }
1260
1261 #[test]
1262 fn test_nested_assignment_missing_intermediate_field() {
1263 let mut facts = order_facts();
1264 let err = run(
1265 "rule R { when true then order.shipping.city = \"X\"; }",
1266 &mut facts,
1267 )
1268 .unwrap_err();
1269 assert!(err.to_string().contains("order.shipping"), "{}", err);
1270 }
1271
1272 #[test]
1273 fn test_nested_assignment_through_non_object() {
1274 let mut facts = order_facts();
1275 let err = run(
1276 "rule R { when true then order.total.cents = 5; }",
1277 &mut facts,
1278 )
1279 .unwrap_err();
1280 assert!(matches!(err.root(), EngineError::TypeError(_)), "{}", err);
1281 assert!(err.to_string().contains("order.total"), "{}", err);
1282 }
1283
1284 #[test]
1285 fn test_nested_assignment_unknown_root() {
1286 let mut facts = order_facts();
1287 let err = run("rule R { when true then invoice.total = 1; }", &mut facts).unwrap_err();
1288 assert!(matches!(err.root(), EngineError::UnknownVariable(name) if name == "invoice"));
1289 }
1290}