lemma/evaluator/
mod.rs

1//! Pure Rust evaluation engine for Lemma
2//!
3//! Evaluates Lemma documents by:
4//! 1. Building a fact map (inputs)
5//! 2. Topologically sorting rules (execution plan)
6//! 3. Executing rules in dependency order
7//! 4. Building response with operation records
8
9pub mod context;
10pub mod datetime;
11pub mod expression;
12pub mod operations;
13pub mod rules;
14pub mod units;
15
16use crate::{LemmaDoc, LemmaError, LemmaFact, LemmaResult, Response, RuleResult};
17use context::{build_fact_map, EvaluationContext};
18use std::collections::HashMap;
19
20/// Stateless evaluator for Lemma documents
21pub struct Evaluator;
22
23impl Evaluator {
24    pub fn new() -> Self {
25        Self
26    }
27
28    /// Evaluate a Lemma document
29    ///
30    /// Executes all rules in the document in topological order,
31    /// applying fact overrides if provided.
32    pub fn evaluate_document(
33        &self,
34        doc_name: &str,
35        documents: &HashMap<String, LemmaDoc>,
36        sources: &HashMap<String, String>,
37        fact_overrides: Vec<LemmaFact>,
38        requested_rules: Option<Vec<String>>,
39    ) -> LemmaResult<Response> {
40        let doc = documents
41            .get(doc_name)
42            .ok_or_else(|| LemmaError::Engine(format!("Document '{}' not found", doc_name)))?;
43
44        // Phase 1: Build fact map (resolving document references)
45        let facts = build_fact_map(&doc.facts, &fact_overrides, documents);
46
47        // Phase 2: Build execution plan (topological sort of rules)
48        let execution_order = build_execution_plan(&doc.rules)?;
49
50        // Phase 3: Build evaluation context
51        let mut context = EvaluationContext::new(doc, documents, sources, facts);
52
53        // Phase 4: Execute rules in dependency order
54        let mut response = Response::new(doc_name.to_string());
55        let mut failed_rules: std::collections::HashSet<String> = std::collections::HashSet::new();
56
57        for rule_name in execution_order {
58            let rule = doc
59                .rules
60                .iter()
61                .find(|r| r.name == rule_name)
62                .ok_or_else(|| LemmaError::Engine(format!("Rule {} not found", rule_name)))?;
63
64            // Check if any dependencies have failed
65            let mut all_rule_deps = std::collections::HashSet::new();
66
67            // Extract from main expression
68            let refs = crate::analysis::extract_references(&rule.expression);
69            for rule_ref in refs.rules {
70                all_rule_deps.insert(rule_ref.join("."));
71            }
72
73            // Extract from unless clauses
74            for uc in &rule.unless_clauses {
75                let cond_refs = crate::analysis::extract_references(&uc.condition);
76                let res_refs = crate::analysis::extract_references(&uc.result);
77                for rule_ref in cond_refs.rules.into_iter().chain(res_refs.rules) {
78                    all_rule_deps.insert(rule_ref.join("."));
79                }
80            }
81
82            let mut missing_deps = Vec::new();
83            for dep_name in all_rule_deps {
84                if failed_rules.contains(&dep_name) {
85                    missing_deps.push(dep_name);
86                }
87            }
88
89            if !missing_deps.is_empty() {
90                // This rule depends on failed rules - mark it as missing dependencies
91                failed_rules.insert(rule.name.clone());
92                response.add_result(RuleResult::missing_facts(rule.name.clone(), missing_deps));
93                continue;
94            }
95
96            // Clear operation records for this rule
97            context.operations.clear();
98
99            // Evaluate the rule
100            match rules::evaluate_rule(rule, &mut context) {
101                Ok(result) => {
102                    // Store result in context for subsequent rules
103                    context
104                        .rule_results
105                        .insert(rule.name.clone(), result.clone());
106
107                    // Add to response based on result type
108                    match result {
109                        crate::OperationResult::Value(value) => {
110                            response.add_result(RuleResult::success_with_operations(
111                                rule.name.clone(),
112                                value.clone(),
113                                HashMap::new(),
114                                context.operations.clone(),
115                            ));
116                        }
117                        crate::OperationResult::Veto(msg) => {
118                            response.add_result(RuleResult::veto(rule.name.clone(), msg));
119                        }
120                    }
121                }
122                Err(LemmaError::Engine(msg)) if msg.starts_with("Missing fact:") => {
123                    failed_rules.insert(rule.name.clone());
124                    let missing = vec![msg.replace("Missing fact: ", "")];
125                    response.add_result(RuleResult::missing_facts(rule.name.clone(), missing));
126                }
127                Err(e) => {
128                    return Err(e);
129                }
130            }
131        }
132
133        // Filter response to only requested rules if specified
134        if let Some(rule_names) = requested_rules {
135            response.filter_rules(&rule_names);
136        }
137
138        Ok(response)
139    }
140}
141
142impl Default for Evaluator {
143    fn default() -> Self {
144        Self::new()
145    }
146}
147
148/// Build an execution plan for rules using topological sort
149///
150/// Returns rules in dependency order (dependencies before dependents)
151fn build_execution_plan(rules: &[crate::LemmaRule]) -> LemmaResult<Vec<String>> {
152    // Build dependency graph (rule -> rules it depends on)
153    let graph = crate::analysis::build_dependency_graph(rules);
154
155    // Topological sort to get execution order
156    topological_sort(&graph)
157}
158
159/// Topological sort of rules to get execution order.
160///
161/// Returns rules in an order such that dependencies come before dependents.
162/// Graph format: node -> set of rules that node depends on.
163pub(crate) fn topological_sort(
164    graph: &HashMap<String, std::collections::HashSet<String>>,
165) -> LemmaResult<Vec<String>> {
166    use std::collections::{HashSet, VecDeque};
167
168    // Build reverse graph: node -> set of rules that depend on node
169    let mut reverse_graph: HashMap<String, HashSet<String>> = HashMap::new();
170    let mut all_nodes: HashSet<String> = HashSet::new();
171
172    for (node, dependencies) in graph {
173        all_nodes.insert(node.clone());
174        reverse_graph.entry(node.clone()).or_default();
175
176        for dep in dependencies {
177            all_nodes.insert(dep.clone());
178            reverse_graph
179                .entry(dep.clone())
180                .or_default()
181                .insert(node.clone());
182        }
183    }
184
185    // Count how many dependencies each node has
186    let mut dependency_count: HashMap<String, usize> = HashMap::new();
187    for node in &all_nodes {
188        let count = graph.get(node).map(|deps| deps.len()).unwrap_or(0);
189        dependency_count.insert(node.clone(), count);
190    }
191
192    // Start with nodes that have no dependencies
193    let mut queue: VecDeque<String> = dependency_count
194        .iter()
195        .filter(|(_, &count)| count == 0)
196        .map(|(node, _)| node.clone())
197        .collect();
198
199    let mut result = Vec::new();
200
201    // Process nodes in order
202    while let Some(node) = queue.pop_front() {
203        result.push(node.clone());
204
205        // For each node that depends on this one
206        if let Some(dependents) = reverse_graph.get(&node) {
207            for dependent in dependents {
208                // Decrease its dependency count
209                if let Some(count) = dependency_count.get_mut(dependent) {
210                    *count -= 1;
211                    if *count == 0 {
212                        queue.push_back(dependent.clone());
213                    }
214                }
215            }
216        }
217    }
218
219    // If we haven't processed all nodes, there's a cycle
220    if result.len() != all_nodes.len() {
221        return Err(LemmaError::Engine(
222            "Circular dependency detected in rules (validator should have caught this)".to_string(),
223        ));
224    }
225
226    Ok(result)
227}