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::operations::VetoType;
14use crate::evaluation::response::EvaluatedRule;
15use crate::planning::execution_plan::validate_value_against_type;
16use crate::planning::semantics::{
17    Data, DataDefinition, DataPath, DataValue, Expression, LemmaType, LiteralValue,
18    ReferenceTarget, RulePath, ValueKind,
19};
20use crate::planning::ExecutionPlan;
21use indexmap::IndexMap;
22pub use operations::{ComputationKind, OperationKind, OperationRecord, OperationResult};
23pub use response::{DataGroup, Response, RuleResult};
24use std::collections::{HashMap, HashSet};
25
26/// Evaluation context for storing intermediate results
27pub(crate) struct EvaluationContext {
28    data_values: HashMap<DataPath, LiteralValue>,
29    pub(crate) rule_results: HashMap<RulePath, OperationResult>,
30    rule_explanations: HashMap<RulePath, crate::evaluation::explanation::Explanation>,
31    operations: Option<Vec<crate::evaluation::operations::OperationRecord>>,
32    explanation_nodes: HashMap<usize, crate::evaluation::explanation::ExplanationNode>,
33    now: LiteralValue,
34    /// Map of rule-target reference data paths to their target rule path.
35    /// Used by [`Self::lazy_rule_reference_resolve`] at first read of the
36    /// reference data path: if the target rule produced a `Value`, we copy
37    /// it into `data_values` (memoizing further reads); if it produced a
38    /// `Veto`, we propagate that exact veto reason. The target rule is
39    /// guaranteed to have been evaluated already because planning injected
40    /// a `depends_on_rules` edge from every consumer rule to the target.
41    rule_references: HashMap<DataPath, RulePath>,
42    /// Constraint-violation vetoes on reference data paths discovered at
43    /// run-time. Populated for data-target references when the copied
44    /// value fails validation against the merged `resolved_type`; used by
45    /// the data-path read in [`expression`] so consumers see a precise
46    /// `Computation` veto naming the violated constraint instead of a
47    /// generic missing-data veto.
48    reference_vetoes: HashMap<DataPath, VetoType>,
49    /// Merged `resolved_type` per reference data path, used to validate
50    /// rule-target reference values lazily in
51    /// [`Self::lazy_rule_reference_resolve`].
52    reference_types: HashMap<DataPath, LemmaType>,
53}
54
55impl EvaluationContext {
56    fn new(plan: &ExecutionPlan, now: LiteralValue, record_operations: bool) -> Self {
57        let mut data_values: HashMap<DataPath, LiteralValue> = plan
58            .data
59            .iter()
60            .filter_map(|(path, d)| d.value().map(|v| (path.clone(), v.clone())))
61            .collect();
62
63        let rule_references: HashMap<DataPath, RulePath> =
64            build_transitive_rule_references(&plan.data);
65
66        let reference_types: HashMap<DataPath, LemmaType> = plan
67            .data
68            .iter()
69            .filter_map(|(path, def)| match def {
70                DataDefinition::Reference { resolved_type, .. } => {
71                    Some((path.clone(), resolved_type.clone()))
72                }
73                _ => None,
74            })
75            .collect();
76        let mut reference_vetoes: HashMap<DataPath, VetoType> = HashMap::new();
77
78        // Resolve data-target references: copy the target's value into the
79        // reference path. Must happen in dependency order so reference-of-
80        // reference chains see their target already populated. A reference
81        // whose target value is missing simply stays missing here; any
82        // rule/expression that later reads the reference will produce a
83        // `MissingData` veto, matching the existing missing-data semantics
84        // for type-declaration data with no value.
85        //
86        // A caller-supplied value (via `set_data_values`) replaces the
87        // `DataDefinition::Reference` entry with `DataDefinition::Value`
88        // before evaluation, so any path that is no longer a `Reference` is
89        // skipped here — the user-provided value has already been placed in
90        // `data_values` and wins over the reference copy.
91        //
92        // Rule-target references are intentionally absent from
93        // `reference_evaluation_order` (filtered in planning); they are
94        // resolved lazily at the consumer's read site once the target rule
95        // has been evaluated.
96        for reference_path in &plan.reference_evaluation_order {
97            match plan.data.get(reference_path) {
98                Some(DataDefinition::Reference {
99                    target: ReferenceTarget::Data(target_path),
100                    resolved_type,
101                    local_default,
102                    ..
103                }) => {
104                    let copied_kind: Option<ValueKind> =
105                        data_values.get(target_path).map(|v| v.value.clone());
106                    if let Some(value_kind) = copied_kind {
107                        let value = LiteralValue {
108                            value: value_kind,
109                            lemma_type: resolved_type.clone(),
110                        };
111                        match validate_value_against_type(resolved_type, &value) {
112                            Ok(()) => {
113                                data_values.insert(reference_path.clone(), value);
114                            }
115                            Err(msg) => {
116                                reference_vetoes.insert(
117                                    reference_path.clone(),
118                                    VetoType::computation(format!(
119                                        "Reference '{}' violates declared constraint: {}",
120                                        reference_path, msg
121                                    )),
122                                );
123                            }
124                        }
125                    } else if let Some(dv) = local_default {
126                        let value = LiteralValue {
127                            value: dv.clone(),
128                            lemma_type: resolved_type.clone(),
129                        };
130                        data_values.insert(reference_path.clone(), value);
131                    }
132                }
133                Some(DataDefinition::Reference {
134                    target: ReferenceTarget::Rule(_),
135                    ..
136                }) => {
137                    // Rule-target references are resolved lazily on first
138                    // read. They should never appear in
139                    // `reference_evaluation_order` (planning filters them
140                    // out), but skip defensively.
141                }
142                Some(_) => {
143                    // User-provided value has replaced the reference;
144                    // nothing to copy.
145                }
146                None => unreachable!(
147                    "BUG: reference_evaluation_order references missing data path '{}'",
148                    reference_path
149                ),
150            }
151        }
152
153        Self {
154            data_values,
155            rule_results: HashMap::new(),
156            rule_explanations: HashMap::new(),
157            operations: if record_operations {
158                Some(Vec::new())
159            } else {
160                None
161            },
162            explanation_nodes: HashMap::new(),
163            now,
164            rule_references,
165            reference_vetoes,
166            reference_types,
167        }
168    }
169
170    /// Resolve a rule-target reference data path lazily from the already-
171    /// evaluated target rule's result. Returns:
172    /// - `Some(Ok(value))` — target rule produced a value; memoized into `data_values`.
173    /// - `Some(Err(veto))` — target rule produced a veto, or the rule's
174    ///   value violates the reference's merged `resolved_type` constraints.
175    /// - `None` — the path is not a rule-target reference.
176    pub(crate) fn lazy_rule_reference_resolve(
177        &mut self,
178        data_path: &DataPath,
179    ) -> Option<Result<LiteralValue, crate::evaluation::operations::VetoType>> {
180        let rule_path = self.rule_references.get(data_path)?.clone();
181        let result = self
182            .rule_results
183            .get(&rule_path)
184            .cloned()
185            .unwrap_or_else(|| {
186                unreachable!(
187                    "BUG: rule-target reference '{}' read before target rule '{}' evaluated; \
188                 planning must have injected the dependency edge",
189                    data_path, rule_path
190                );
191            });
192        match result {
193            OperationResult::Value(v) => {
194                let v = *v;
195                let v = match self.reference_types.get(data_path) {
196                    Some(ref_type) => {
197                        let retyped = LiteralValue {
198                            value: v.value,
199                            lemma_type: ref_type.clone(),
200                        };
201                        if let Err(msg) = validate_value_against_type(ref_type, &retyped) {
202                            return Some(Err(VetoType::computation(format!(
203                                "Reference '{}' violates declared constraint: {}",
204                                data_path, msg
205                            ))));
206                        }
207                        retyped
208                    }
209                    None => v,
210                };
211                self.data_values.insert(data_path.clone(), v.clone());
212                Some(Ok(v))
213            }
214            OperationResult::Veto(veto) => Some(Err(veto)),
215        }
216    }
217
218    /// Returns a recorded constraint-violation veto for a reference data
219    /// path, if any. Populated in [`Self::new`] for data-target references
220    /// whose copied value failed `validate_value_against_type`.
221    pub(crate) fn get_reference_veto(&self, data_path: &DataPath) -> Option<&VetoType> {
222        self.reference_vetoes.get(data_path)
223    }
224
225    pub(crate) fn now(&self) -> &LiteralValue {
226        &self.now
227    }
228
229    fn get_data(&self, data_path: &DataPath) -> Option<&LiteralValue> {
230        self.data_values.get(data_path)
231    }
232
233    fn push_operation(&mut self, kind: OperationKind) {
234        if let Some(ref mut ops) = self.operations {
235            ops.push(OperationRecord { kind });
236        }
237    }
238
239    fn set_explanation_node(
240        &mut self,
241        expression: &Expression,
242        node: crate::evaluation::explanation::ExplanationNode,
243    ) {
244        self.explanation_nodes
245            .insert(expression as *const Expression as usize, node);
246    }
247
248    fn get_explanation_node(
249        &self,
250        expression: &Expression,
251    ) -> Option<&crate::evaluation::explanation::ExplanationNode> {
252        self.explanation_nodes
253            .get(&(expression as *const Expression as usize))
254    }
255
256    fn get_rule_explanation(
257        &self,
258        rule_path: &RulePath,
259    ) -> Option<&crate::evaluation::explanation::Explanation> {
260        self.rule_explanations.get(rule_path)
261    }
262
263    fn set_rule_explanation(
264        &mut self,
265        rule_path: RulePath,
266        explanation: crate::evaluation::explanation::Explanation,
267    ) {
268        self.rule_explanations.insert(rule_path, explanation);
269    }
270}
271
272/// For every reference data path in `data`, follow the data-target reference
273/// chain and record the eventual rule-target (if any). Includes direct
274/// rule-target references. A visited set guards against pathological cycles
275/// that planning should already have rejected.
276fn build_transitive_rule_references(
277    data: &IndexMap<DataPath, DataDefinition>,
278) -> HashMap<DataPath, RulePath> {
279    let mut out: HashMap<DataPath, RulePath> = HashMap::new();
280    for (path, def) in data {
281        if !matches!(def, DataDefinition::Reference { .. }) {
282            continue;
283        }
284        let mut visited: HashSet<DataPath> = HashSet::new();
285        let mut cursor: DataPath = path.clone();
286        loop {
287            if !visited.insert(cursor.clone()) {
288                break;
289            }
290            let Some(DataDefinition::Reference { target, .. }) = data.get(&cursor) else {
291                break;
292            };
293            match target {
294                ReferenceTarget::Data(next) => cursor = next.clone(),
295                ReferenceTarget::Rule(rule_path) => {
296                    out.insert(path.clone(), rule_path.clone());
297                    break;
298                }
299            }
300        }
301    }
302    out
303}
304
305fn collect_used_data_from_explanation(
306    node: &ExplanationNode,
307    out: &mut HashMap<DataPath, LiteralValue>,
308) {
309    match node {
310        ExplanationNode::Value {
311            value,
312            source: ValueSource::Data { data_ref },
313            ..
314        } => {
315            out.entry(data_ref.clone()).or_insert_with(|| value.clone());
316        }
317        ExplanationNode::Value { .. } => {}
318        ExplanationNode::RuleReference { expansion, .. } => {
319            collect_used_data_from_explanation(expansion.as_ref(), out);
320        }
321        ExplanationNode::Computation { operands, .. } => {
322            for op in operands {
323                collect_used_data_from_explanation(op, out);
324            }
325        }
326        ExplanationNode::Branches {
327            matched,
328            non_matched,
329            ..
330        } => {
331            if let Some(ref cond) = matched.condition {
332                collect_used_data_from_explanation(cond, out);
333            }
334            collect_used_data_from_explanation(&matched.result, out);
335            for nm in non_matched {
336                collect_used_data_from_explanation(&nm.condition, out);
337                if let Some(ref res) = nm.result {
338                    collect_used_data_from_explanation(res, out);
339                }
340            }
341        }
342        ExplanationNode::Condition { operands, .. } => {
343            for op in operands {
344                collect_used_data_from_explanation(op, out);
345            }
346        }
347        ExplanationNode::Veto { .. } => {}
348    }
349}
350
351#[cfg(test)]
352mod runtime_invariant_tests {
353    use super::*;
354    use crate::parsing::ast::DateTimeValue;
355    use crate::Engine;
356
357    /// At evaluation time the LiteralValue stored under a reference data path
358    /// must carry the reference's own `resolved_type`, not the (possibly looser)
359    /// type embedded in the target's LiteralValue. The merge pass folds in the
360    /// LHS-declared constraints (e.g. a binding's parent-spec type chain) so
361    /// the reference's `resolved_type` is the contract the evaluator promises;
362    /// any consumer that reads `data_values[ref].lemma_type` directly must see
363    /// that contract, not the target's loose shape.
364    #[test]
365    fn reference_runtime_value_carries_resolved_type_not_target_type() {
366        let code = r#"
367spec inner
368data slot: number -> minimum 0 -> maximum 100
369
370spec source_spec
371data v: number -> default 5
372
373spec outer
374uses i: inner
375uses src: source_spec
376data i.slot: src.v
377rule r: i.slot
378"#;
379        let mut engine = Engine::new();
380        engine
381            .load(
382                code,
383                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
384                    "ref_invariant.lemma",
385                ))),
386            )
387            .expect("must load");
388
389        let now = DateTimeValue::now();
390        let plan_basis = engine
391            .get_plan(None, "outer", Some(&now))
392            .expect("must plan")
393            .clone();
394
395        let reference_path = plan_basis
396            .data
397            .iter()
398            .find_map(|(path, def)| match def {
399                DataDefinition::Reference { .. } => Some(path.clone()),
400                _ => None,
401            })
402            .expect("plan must contain the reference for `i.slot`");
403
404        let resolved_type = match plan_basis.data.get(&reference_path).expect("entry exists") {
405            DataDefinition::Reference { resolved_type, .. } => resolved_type.clone(),
406            _ => unreachable!("filter above kept only Reference entries"),
407        };
408
409        let plan = plan_basis.with_defaults();
410
411        let now_lit = LiteralValue {
412            value: crate::planning::semantics::ValueKind::Date(
413                crate::planning::semantics::date_time_to_semantic(&now),
414            ),
415            lemma_type: crate::planning::semantics::primitive_date().clone(),
416        };
417        let context = EvaluationContext::new(&plan, now_lit, false);
418
419        let stored = context
420            .data_values
421            .get(&reference_path)
422            .expect("EvaluationContext must populate reference path with the copied value");
423
424        assert_eq!(
425            stored.lemma_type, resolved_type,
426            "stored LiteralValue must carry the reference's resolved_type \
427             (LHS-merged), not the target's loose type. \
428             stored = {:?}, resolved = {:?}",
429            stored.lemma_type, resolved_type
430        );
431    }
432}
433
434/// Evaluates Lemma rules within their spec context
435#[derive(Default)]
436pub(crate) struct Evaluator;
437
438impl Evaluator {
439    /// Evaluate an execution plan.
440    ///
441    /// Executes rules in pre-computed dependency order with all data pre-loaded.
442    /// Rules are already flattened into executable branches with data prefixes resolved.
443    ///
444    /// After planning, evaluation is guaranteed to complete. This function never returns
445    /// a Error — runtime issues (division by zero, missing data, user-defined veto)
446    /// produce Vetoes, which are valid evaluation outcomes.
447    ///
448    /// When `record_operations` is true, each rule's evaluation records a trace of
449    /// operations (data used, rules used, computations, branch evaluations) into
450    /// `RuleResult::operations`. When false, no trace is recorded.
451    pub(crate) fn evaluate(
452        &self,
453        plan: &ExecutionPlan,
454        now: LiteralValue,
455        record_operations: bool,
456    ) -> Response {
457        let mut context = EvaluationContext::new(plan, now, record_operations);
458
459        let mut response = Response {
460            spec_name: plan.spec_name.clone(),
461            spec_hash: None,
462            spec_effective_from: None,
463            spec_effective_to: None,
464            data: Vec::new(),
465            results: IndexMap::new(),
466        };
467
468        // Execute each rule in topological order (already sorted by ExecutionPlan)
469        for exec_rule in &plan.rules {
470            if let Some(ref mut ops) = context.operations {
471                ops.clear();
472            }
473            context.explanation_nodes.clear();
474
475            let (result, explanation) = expression::evaluate_rule(exec_rule, &mut context);
476
477            context
478                .rule_results
479                .insert(exec_rule.path.clone(), result.clone());
480            context.set_rule_explanation(exec_rule.path.clone(), explanation.clone());
481
482            let rule_operations = context.operations.clone().unwrap_or_default();
483
484            if !exec_rule.path.segments.is_empty() {
485                continue;
486            }
487
488            let unless_branches: Vec<(Option<Expression>, Expression)> = exec_rule.branches[1..]
489                .iter()
490                .map(|b| (b.condition.clone(), b.result.clone()))
491                .collect();
492
493            response.add_result(RuleResult {
494                rule: EvaluatedRule {
495                    name: exec_rule.name.clone(),
496                    path: exec_rule.path.clone(),
497                    default_expression: exec_rule.branches[0].result.clone(),
498                    unless_branches,
499                    source_location: exec_rule.source.clone(),
500                    rule_type: exec_rule.rule_type.clone(),
501                },
502                result,
503                data: vec![],
504                operations: rule_operations,
505                explanation: Some(explanation),
506                rule_type: exec_rule.rule_type.clone(),
507            });
508        }
509
510        let mut used_data: HashMap<DataPath, LiteralValue> = HashMap::new();
511        for rule_result in response.results.values() {
512            if let Some(ref explanation) = rule_result.explanation {
513                collect_used_data_from_explanation(explanation.tree.as_ref(), &mut used_data);
514            }
515        }
516
517        // Build data list in definition order (plan.data is an IndexMap)
518        let data_list: Vec<Data> = plan
519            .data
520            .keys()
521            .filter_map(|path| {
522                used_data.remove(path).map(|value| Data {
523                    path: path.clone(),
524                    value: DataValue::from_bound_literal(value),
525                    source: None,
526                })
527            })
528            .collect();
529
530        if !data_list.is_empty() {
531            response.data = vec![DataGroup {
532                data_path: String::new(),
533                referencing_data_name: String::new(),
534                data: data_list,
535            }];
536        }
537
538        response
539    }
540}