Skip to main content

lemma/evaluation/
mod.rs

1//! Pure Rust evaluation engine for Lemma
2//!
3//! Executes pre-validated execution plans in dependency order.
4//! The execution plan is self-contained with all rules flattened into branches.
5//! The evaluator executes rules linearly without recursion or tree traversal.
6
7pub mod explanation;
8pub mod expression;
9pub mod operations;
10pub mod response;
11
12use crate::evaluation::explanation::{ExplanationNode, ValueSource};
13use crate::evaluation::response::EvaluatedRule;
14use crate::planning::semantics::{Expression, Fact, FactPath, FactValue, LiteralValue, RulePath};
15use crate::planning::ExecutionPlan;
16use indexmap::IndexMap;
17pub use operations::{ComputationKind, OperationKind, OperationRecord, OperationResult};
18pub use response::{Facts, Response, RuleResult};
19use std::collections::HashMap;
20
21/// Evaluation context for storing intermediate results
22pub(crate) struct EvaluationContext {
23    fact_values: HashMap<FactPath, LiteralValue>,
24    pub(crate) rule_results: HashMap<RulePath, OperationResult>,
25    rule_explanations: HashMap<RulePath, crate::evaluation::explanation::Explanation>,
26    operations: Option<Vec<crate::OperationRecord>>,
27    explanation_nodes: HashMap<usize, crate::evaluation::explanation::ExplanationNode>,
28    now: LiteralValue,
29}
30
31impl EvaluationContext {
32    fn new(plan: &ExecutionPlan, now: LiteralValue, record_operations: bool) -> Self {
33        let fact_values: HashMap<FactPath, LiteralValue> = plan
34            .facts
35            .iter()
36            .filter_map(|(path, d)| d.value().map(|v| (path.clone(), v.clone())))
37            .collect();
38        Self {
39            fact_values,
40            rule_results: HashMap::new(),
41            rule_explanations: HashMap::new(),
42            operations: if record_operations {
43                Some(Vec::new())
44            } else {
45                None
46            },
47            explanation_nodes: HashMap::new(),
48            now,
49        }
50    }
51
52    pub(crate) fn now(&self) -> &LiteralValue {
53        &self.now
54    }
55
56    fn get_fact(&self, fact_path: &FactPath) -> Option<&LiteralValue> {
57        self.fact_values.get(fact_path)
58    }
59
60    fn push_operation(&mut self, kind: OperationKind) {
61        if let Some(ref mut ops) = self.operations {
62            ops.push(OperationRecord { kind });
63        }
64    }
65
66    fn set_explanation_node(
67        &mut self,
68        expression: &Expression,
69        node: crate::evaluation::explanation::ExplanationNode,
70    ) {
71        self.explanation_nodes
72            .insert(expression as *const Expression as usize, node);
73    }
74
75    fn get_explanation_node(
76        &self,
77        expression: &Expression,
78    ) -> Option<&crate::evaluation::explanation::ExplanationNode> {
79        self.explanation_nodes
80            .get(&(expression as *const Expression as usize))
81    }
82
83    fn get_rule_explanation(
84        &self,
85        rule_path: &RulePath,
86    ) -> Option<&crate::evaluation::explanation::Explanation> {
87        self.rule_explanations.get(rule_path)
88    }
89
90    fn set_rule_explanation(
91        &mut self,
92        rule_path: RulePath,
93        explanation: crate::evaluation::explanation::Explanation,
94    ) {
95        self.rule_explanations.insert(rule_path, explanation);
96    }
97}
98
99fn collect_used_facts_from_explanation(
100    node: &ExplanationNode,
101    out: &mut HashMap<FactPath, LiteralValue>,
102) {
103    match node {
104        ExplanationNode::Value {
105            value,
106            source: ValueSource::Fact { fact_ref },
107            ..
108        } => {
109            out.entry(fact_ref.clone()).or_insert_with(|| value.clone());
110        }
111        ExplanationNode::Value { .. } => {}
112        ExplanationNode::RuleReference { expansion, .. } => {
113            collect_used_facts_from_explanation(expansion.as_ref(), out);
114        }
115        ExplanationNode::Computation { operands, .. } => {
116            for op in operands {
117                collect_used_facts_from_explanation(op, out);
118            }
119        }
120        ExplanationNode::Branches {
121            matched,
122            non_matched,
123            ..
124        } => {
125            if let Some(ref cond) = matched.condition {
126                collect_used_facts_from_explanation(cond, out);
127            }
128            collect_used_facts_from_explanation(&matched.result, out);
129            for nm in non_matched {
130                collect_used_facts_from_explanation(&nm.condition, out);
131                if let Some(ref res) = nm.result {
132                    collect_used_facts_from_explanation(res, out);
133                }
134            }
135        }
136        ExplanationNode::Condition { operands, .. } => {
137            for op in operands {
138                collect_used_facts_from_explanation(op, out);
139            }
140        }
141        ExplanationNode::Veto { .. } => {}
142    }
143}
144
145/// Evaluates Lemma rules within their spec context
146#[derive(Default)]
147pub(crate) struct Evaluator;
148
149impl Evaluator {
150    /// Evaluate an execution plan.
151    ///
152    /// Executes rules in pre-computed dependency order with all facts pre-loaded.
153    /// Rules are already flattened into executable branches with fact prefixes resolved.
154    ///
155    /// After planning, evaluation is guaranteed to complete. This function never returns
156    /// a Error — runtime issues (division by zero, missing facts, user-defined veto)
157    /// produce Vetoes, which are valid evaluation outcomes.
158    ///
159    /// When `record_operations` is true, each rule's evaluation records a trace of
160    /// operations (facts used, rules used, computations, branch evaluations) into
161    /// `RuleResult::operations`. When false, no trace is recorded.
162    pub(crate) fn evaluate(
163        &self,
164        plan: &ExecutionPlan,
165        now: LiteralValue,
166        record_operations: bool,
167    ) -> Response {
168        let mut context = EvaluationContext::new(plan, now, record_operations);
169
170        let mut response = Response {
171            spec_name: plan.spec_name.clone(),
172            spec_hash: None,
173            spec_effective_from: None,
174            spec_effective_to: None,
175            facts: Vec::new(),
176            results: IndexMap::new(),
177        };
178
179        // Execute each rule in topological order (already sorted by ExecutionPlan)
180        for exec_rule in &plan.rules {
181            if let Some(ref mut ops) = context.operations {
182                ops.clear();
183            }
184            context.explanation_nodes.clear();
185
186            let (result, explanation) = expression::evaluate_rule(exec_rule, &mut context);
187
188            context
189                .rule_results
190                .insert(exec_rule.path.clone(), result.clone());
191            context.set_rule_explanation(exec_rule.path.clone(), explanation.clone());
192
193            let rule_operations = context.operations.clone().unwrap_or_default();
194
195            if !exec_rule.path.segments.is_empty() {
196                continue;
197            }
198
199            let unless_branches: Vec<(Option<Expression>, Expression)> = exec_rule.branches[1..]
200                .iter()
201                .map(|b| (b.condition.clone(), b.result.clone()))
202                .collect();
203
204            response.add_result(RuleResult {
205                rule: EvaluatedRule {
206                    name: exec_rule.name.clone(),
207                    path: exec_rule.path.clone(),
208                    default_expression: exec_rule.branches[0].result.clone(),
209                    unless_branches,
210                    source_location: exec_rule.source.clone(),
211                    rule_type: exec_rule.rule_type.clone(),
212                },
213                result,
214                facts: vec![],
215                operations: rule_operations,
216                explanation: Some(explanation),
217                rule_type: exec_rule.rule_type.clone(),
218            });
219        }
220
221        let mut used_facts: HashMap<FactPath, LiteralValue> = HashMap::new();
222        for rule_result in response.results.values() {
223            if let Some(ref explanation) = rule_result.explanation {
224                collect_used_facts_from_explanation(explanation.tree.as_ref(), &mut used_facts);
225            }
226        }
227
228        // Build fact list in definition order (plan.facts is an IndexMap)
229        let fact_list: Vec<Fact> = plan
230            .facts
231            .keys()
232            .filter_map(|path| {
233                used_facts.remove(path).map(|value| Fact {
234                    path: path.clone(),
235                    value: FactValue::Literal(value),
236                    source: None,
237                })
238            })
239            .collect();
240
241        if !fact_list.is_empty() {
242            response.facts = vec![Facts {
243                fact_path: String::new(),
244                referencing_fact_name: String::new(),
245                facts: fact_list,
246            }];
247        }
248
249        response
250    }
251}