lemma/evaluator/
mod.rs

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