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