1pub 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#[derive(Default)]
26pub struct Evaluator;
27
28impl Evaluator {
29 pub fn new() -> Self {
30 Self
31 }
32
33 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 let graph = crate::analysis::build_dependency_graph(doc, documents)?;
54 let execution_order = topological_sort(&graph)?;
55
56 let facts = build_fact_map(doc, &doc.facts, &fact_overrides, documents)?;
58
59 let mut context =
61 EvaluationContext::new(doc, documents, sources, facts, &timeout_tracker, limits);
62
63 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 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 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 context.operations.clear();
105
106 let path_prefix: Vec<String> = if target_doc_name != doc_name {
108 rule_path.segments.iter().map(|s| s.fact.clone()).collect()
112 } else {
113 Vec::new()
115 };
116 let eval_result = rules::evaluate_rule(rule, &mut context, &path_prefix);
117
118 match eval_result {
119 Ok(result) => {
120 context
122 .rule_results
123 .insert(rule_path.clone(), result.clone());
124
125 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 if let Some(rule_names) = requested_rules {
157 response.filter_rules(&rule_names);
158 }
159
160 Ok(response)
161 }
162}
163
164pub(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 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 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 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 while let Some(node) = queue.pop_front() {
208 result.push(node.clone());
209
210 if let Some(dependents) = reverse_graph.get(&node) {
212 for dependent in dependents {
213 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 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}