Skip to main content

rust_runes/
engine.rs

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
20/// Maximum number of rule firings per execution, matching Grule's default.
21pub const DEFAULT_MAX_CYCLES: usize = 5000;
22
23/// Errors from executing rules.
24#[derive(Error, Debug)]
25#[non_exhaustive]
26pub enum EngineError {
27    /// An expression couldn't be evaluated, e.g. a missing field.
28    #[error("Evaluation error: {0}")]
29    EvaluationError(String),
30    /// A rule referred to a fact that doesn't exist.
31    #[error("Unknown variable: {0}")]
32    UnknownVariable(String),
33    /// An operation was applied to values of the wrong type.
34    #[error("Type error: {0}")]
35    TypeError(String),
36    /// Integer `%` by zero.
37    #[error("Division by zero")]
38    DivisionByZero,
39    /// A rule called a function that is neither built in nor registered.
40    #[error("Unknown function: {0}")]
41    UnknownFunction(String),
42    /// A function or method call failed (wrong arguments, or an error it returned).
43    #[error("Function error: {0}")]
44    FunctionError(String),
45    /// `Retract` named a rule that doesn't exist.
46    #[error("Unknown rule: {0}")]
47    UnknownRule(String),
48    /// Rules kept firing past the cycle limit (see `RuleEngine::set_max_cycles`).
49    #[error("Rules fired {0} times without settling; a rule's actions may never make its condition false (consider Retract)")]
50    MaxCyclesExceeded(usize),
51    /// The execution's cancellation flag was set.
52    #[error("Execution was cancelled")]
53    Cancelled,
54    /// The execution's timeout elapsed.
55    #[error("Execution timed out")]
56    TimedOut,
57    /// An error raised while evaluating or executing a specific rule.
58    #[error("Rule '{rule}' failed in its {phase} scope: {source}")]
59    InRule {
60        /// The rule's name.
61        rule: String,
62        /// Whether the error came from the rule's condition or its actions.
63        phase: RulePhase,
64        /// The underlying error.
65        #[source]
66        source: Box<EngineError>,
67    },
68}
69
70impl EngineError {
71    /// The underlying error, without the rule it occurred in.
72    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/// Which part of a rule an error came from.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum RulePhase {
91    /// The rule's condition.
92    When,
93    /// The rule's actions.
94    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
106/// Receives notifications as the engine runs, like Grule's `GruleEngineListener`.
107/// All methods do nothing by default.
108pub trait EngineListener: Send + Sync {
109    /// A new cycle is starting (cycles are numbered from 1).
110    fn begin_cycle(&self, _cycle: u64) {}
111    /// A rule's condition was evaluated; `matched` is whether it held.
112    fn evaluate_rule(&self, _cycle: u64, _rule: &Rule, _matched: bool) {}
113    /// A rule was chosen and its actions are about to run.
114    fn execute_rule(&self, _cycle: u64, _rule: &Rule) {}
115}
116
117/// What an execution did.
118#[derive(Debug, Clone)]
119pub struct ExecutionResult {
120    /// The rules that fired, in order (a rule may appear more than once).
121    pub rules_fired: Vec<String>,
122    /// How long the execution took, in milliseconds.
123    pub execution_time_ms: u128,
124}
125
126impl ExecutionResult {
127    /// An empty result.
128    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/// Per-execution state changed by engine functions like `Retract` and `Complete`.
143#[derive(Default)]
144struct ExecutionState {
145    /// Rules retracted by the current rule's actions
146    retracted: Vec<String>,
147    completed: bool,
148    /// Paths named by `Changed` / `Forget` in the current rule's actions
149    changed: Vec<FactPath>,
150}
151
152/// Per-execution rule state, by salience position.
153struct Agenda {
154    /// Rules whose condition result is unknown (always includes rules whose results
155    /// can't be reused)
156    dirty: BitSet,
157    /// Rules known to match
158    matching: BitSet,
159    retracted: BitSet,
160}
161
162/// Memoized results of shared boolean sub-expressions for one execution (see
163/// `compiled::Interner`). Uses `Cell`s so evaluation can fill it through `&self`.
164struct 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    /// A memo that never stores anything.
178    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
202/// One step of a path into a fact, e.g. `.customer` or `[0]`.
203enum Key {
204    Field(String),
205    Index(i64),
206}
207
208/// Runs rules against facts, as Grule's engine does.
209///
210/// An engine holds a set of rules (its [`KnowledgeBase`]) plus registered functions,
211/// methods and listeners. [`execute`](Self::execute) takes `&self`, so one engine can be
212/// shared between threads and run concurrently.
213pub 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    /// An engine with no rules, Grule's default cycle limit (5000), and failed
225    /// conditions counting as false.
226    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    /// Adds a rule. Fails if a rule with the same name exists.
239    pub fn add_rule(&mut self, rule: Rule) -> Result<(), String> {
240        self.knowledge_base.add_rule(rule)
241    }
242
243    /// Parses GRL text and adds all of its rules. Returns how many were added.
244    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    /// Loads all rules from a `.grl` file. Returns how many were added.
249    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    /// Loads all `.grl` files under `dir`, recursively. Returns how many rules were added.
254    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    /// Sets the maximum number of rule firings per execution before it fails
259    /// with `EngineError::MaxCyclesExceeded`.
260    pub fn set_max_cycles(&mut self, max_cycles: usize) {
261        self.max_cycles = max_cycles;
262    }
263
264    /// By default, as in Grule, a condition that fails to evaluate (a missing field,
265    /// a type error, ...) is logged and treated as false. When enabled, execution
266    /// stops with the error instead.
267    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    /// Reusing condition results between cycles is on by default and never changes
272    /// which rules fire; turning it off is only useful for comparison and debugging.
273    pub fn set_condition_caching(&mut self, enabled: bool) {
274        self.condition_caching = enabled;
275    }
276
277    /// Adds a listener notified as executions run. With any listener attached, every
278    /// rule is evaluated every cycle (as in Grule) so that listeners see each evaluation.
279    pub fn add_listener(&mut self, listener: impl EngineListener + 'static) {
280        self.listeners.push(Box::new(listener));
281    }
282
283    /// Makes `function` callable from rules as `name(args...)`.
284    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    /// Makes `method` callable from rules as `receiver.name(args...)` on any value
299    /// that has no built-in method of that name. This is the equivalent of calling a
300    /// method on a Go fact in Grule: when called as a statement in a rule's `then`
301    /// scope on a fact path (`Order.AddItem(x);`), the method receives the stored
302    /// value and its changes persist. Anywhere else it receives a copy.
303    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    /// Like `register_method`, but the method works on your own type `T`, the
311    /// counterpart of a method on a Go fact struct. Around each call the receiver is
312    /// converted into `T` and back with serde (so `T` is typically the type the fact was
313    /// created from with `Fact::from_serialize`), the arguments are decoded into `A`,
314    /// and the result `R` is converted into a fact value.
315    ///
316    /// `A` is a tuple for fixed arguments (`(f64,)`, `(String, i64)`), `()` for none,
317    /// or a `Vec` for any number. Return `Ok(())` for no result (it becomes `nil`).
318    ///
319    /// The same name can be registered for several types; a call goes to the first
320    /// whose type the receiver converts into, then to an untyped method of that name.
321    /// Fields the receiver has that `T` doesn't are kept.
322    ///
323    /// ```
324    /// # use rust_runes::RuleEngine;
325    /// # #[derive(serde::Serialize, serde::Deserialize)]
326    /// # struct Loan { amount: f64, escrow: f64 }
327    /// let mut engine = RuleEngine::new();
328    /// engine.register_typed_method("CalculateEscrow", |loan: &mut Loan, (): ()| {
329    ///     loan.escrow = loan.amount * 0.01;
330    ///     Ok(())
331    /// })?;
332    /// engine.register_typed_method("Payment", |loan: &mut Loan, (rate,): (f64,)| {
333    ///     Ok(loan.amount * rate / 12.0)
334    /// })?;
335    /// // GRL: then Loan.CalculateEscrow(); Loan.Monthly = Loan.Payment(0.06);
336    /// # Ok::<(), String>(())
337    /// ```
338    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    /// Runs rules until no condition holds, as Grule does. Each cycle evaluates every
349    /// rule's condition, then fires the highest-salience rule among those that matched
350    /// (ties go to the rule added first). Rules removed with `Retract` stay out for the
351    /// rest of this execution, and `Complete()` ends it once the current rule's actions
352    /// have run.
353    ///
354    /// Two optimizations keep this from re-evaluating everything every cycle, without
355    /// changing which rules fire:
356    /// - A condition's result is reused until a rule's actions (or `Changed`/`Forget`)
357    ///   touch a fact path it reads. Conditions using `Now()`, logging or user
358    ///   functions/methods are always re-evaluated.
359    /// - Once the cycle's highest-salience match is found, lower rules are skipped
360    ///   unless evaluating them has observable effects: a listener is attached,
361    ///   evaluation errors are returned, or the condition logs or calls user code.
362    pub fn execute(
363        &self,
364        facts: &mut HashMap<String, Fact>,
365    ) -> Result<ExecutionResult, EngineError> {
366        self.run(facts, &Cancellation::default())
367    }
368
369    /// Like `execute`, but stops with `EngineError::Cancelled` once `cancel` is set,
370    /// checked before each rule evaluation. Set it from another thread.
371    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    /// Like `execute`, but stops with `EngineError::TimedOut` once `timeout` has
384    /// elapsed, checked before each rule evaluation (like a Go context deadline).
385    /// A single slow function call can still overrun it.
386    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        // Rule state by salience position: `dirty` rules have no known result;
410        // `agenda` holds rules known to match
411        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        // Every evaluation is observable with a listener or when errors are returned,
423        // so evaluate every rule each cycle as Grule does
424        let full_evaluation =
425            !self.listeners.is_empty() || self.return_err_on_failed_rule_evaluation;
426        // Otherwise only conditions with effects must run every cycle
427        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    /// Finds the highest-salience matching rule by evaluating every rule (using known
486    /// results where still valid) and notifying listeners of each.
487    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    /// Finds the highest-salience matching rule while evaluating as little as possible:
518    /// the best known match is the first rule on the agenda, so only rules above it
519    /// whose results are unknown need evaluating (plus conditions with effects).
520    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    /// Evaluates a rule's condition and, if its result can be reused, records it.
558    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    /// Whether a rule's condition result can be reused until its inputs change.
578    fn is_cacheable(&self, compiled: &CompiledRule) -> bool {
579        self.condition_caching && compiled.deterministic && !self.calls_user_methods(compiled)
580    }
581
582    /// Whether a rule's condition can go unevaluated without anyone noticing.
583    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    /// Forgets results that depend on anything under `written`: the conditions that
596    /// read it go back to unknown, and so do memoized sub-expressions that read it.
597    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    /// Returns the rules whose conditions currently hold, highest salience first,
614    /// without running any of them (Grule's `FetchMatchingRules`).
615    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    /// Evaluates a rule's condition, applying the failed-evaluation policy.
636    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    /// Evaluates without memoization, for actions: facts change between statements.
666    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    /// Returns a memoized boolean, or computes and memoizes it.
676    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    /// Evaluates `node`, borrowing from `facts` (and from the rule's constants) where
697    /// possible so that reading `order.customer.age` doesn't copy `order`.
698    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                        // Maps with integer or boolean keys store them as strings
736                        (
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                // Methods that may change their receiver run on a copy here
785                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            // && and || short-circuit, so the right side is only evaluated when needed
820            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    /// Calls a method that may change its receiver: a built-in like `Append`,
858    /// or a user-registered method.
859    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                // Complete()
917                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            // Changed("Fact.X") / Forget("Fact.X"): something changed that the engine
928            // couldn't see, so conditions reading it must be re-evaluated
929            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            // A method called on a fact path operates on the stored value, so
938            // `Order.Items.Append(x);` changes the fact
939            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(&current, name, &args) {
943                    // A built-in that never changes its receiver
944                    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            // Called for their effect; the result is discarded
951            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    /// Assigns `value` to a variable, or to a field/element reached through it.
963    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        // Existing fields and elements keep their type, as Go struct fields do
977        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    /// Finds the stored value a path like `order.items[0]` refers to.
1000    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    /// Splits a path like `order.items[i].price` into its root variable and the keys
1010    /// leading from it. Index expressions are evaluated here, before facts are
1011    /// borrowed mutably.
1012    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    /// The engine's rules.
1046    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/// When to stop an execution early: a flag set by the caller, a deadline, or both.
1058#[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
1082/// Follows the first `depth` keys from the fact `root`, returning the value reached.
1083/// Every step must already exist.
1084fn 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
1120/// Narrows a value to a part of it (a field or element), borrowing when the value
1121/// itself is borrowed and cloning just the part when it was computed.
1122fn 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
1132/// The key an object is accessed with: fields by name, and numeric indexes by their
1133/// decimal form, which is how serde stores maps with integer keys.
1134fn 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}