1pub 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
26pub(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::OperationRecord>>,
32 explanation_nodes: HashMap<usize, crate::evaluation::explanation::ExplanationNode>,
33 now: LiteralValue,
34 rule_references: HashMap<DataPath, RulePath>,
42 reference_vetoes: HashMap<DataPath, VetoType>,
49 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 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> = data_values
105 .get(target_path)
106 .map(|v| v.value.clone())
107 .or_else(|| local_default.clone());
108 if let Some(value_kind) = copied_kind {
109 let value = LiteralValue {
110 value: value_kind,
111 lemma_type: resolved_type.clone(),
112 };
113 match validate_value_against_type(resolved_type, &value) {
114 Ok(()) => {
115 data_values.insert(reference_path.clone(), value);
116 }
117 Err(msg) => {
118 reference_vetoes.insert(
119 reference_path.clone(),
120 VetoType::computation(format!(
121 "Reference '{}' violates declared constraint: {}",
122 reference_path, msg
123 )),
124 );
125 }
126 }
127 }
128 }
129 Some(DataDefinition::Reference {
130 target: ReferenceTarget::Rule(_),
131 ..
132 }) => {
133 }
138 Some(_) => {
139 }
142 None => unreachable!(
143 "BUG: reference_evaluation_order references missing data path '{}'",
144 reference_path
145 ),
146 }
147 }
148
149 Self {
150 data_values,
151 rule_results: HashMap::new(),
152 rule_explanations: HashMap::new(),
153 operations: if record_operations {
154 Some(Vec::new())
155 } else {
156 None
157 },
158 explanation_nodes: HashMap::new(),
159 now,
160 rule_references,
161 reference_vetoes,
162 reference_types,
163 }
164 }
165
166 pub(crate) fn lazy_rule_reference_resolve(
173 &mut self,
174 data_path: &DataPath,
175 ) -> Option<Result<LiteralValue, crate::evaluation::operations::VetoType>> {
176 let rule_path = self.rule_references.get(data_path)?.clone();
177 let result = self
178 .rule_results
179 .get(&rule_path)
180 .cloned()
181 .unwrap_or_else(|| {
182 unreachable!(
183 "BUG: rule-target reference '{}' read before target rule '{}' evaluated; \
184 planning must have injected the dependency edge",
185 data_path, rule_path
186 );
187 });
188 match result {
189 OperationResult::Value(v) => {
190 let v = *v;
191 let v = match self.reference_types.get(data_path) {
192 Some(ref_type) => {
193 let retyped = LiteralValue {
194 value: v.value,
195 lemma_type: ref_type.clone(),
196 };
197 if let Err(msg) = validate_value_against_type(ref_type, &retyped) {
198 return Some(Err(VetoType::computation(format!(
199 "Reference '{}' violates declared constraint: {}",
200 data_path, msg
201 ))));
202 }
203 retyped
204 }
205 None => v,
206 };
207 self.data_values.insert(data_path.clone(), v.clone());
208 Some(Ok(v))
209 }
210 OperationResult::Veto(veto) => Some(Err(veto)),
211 }
212 }
213
214 pub(crate) fn get_reference_veto(&self, data_path: &DataPath) -> Option<&VetoType> {
218 self.reference_vetoes.get(data_path)
219 }
220
221 pub(crate) fn now(&self) -> &LiteralValue {
222 &self.now
223 }
224
225 fn get_data(&self, data_path: &DataPath) -> Option<&LiteralValue> {
226 self.data_values.get(data_path)
227 }
228
229 fn push_operation(&mut self, kind: OperationKind) {
230 if let Some(ref mut ops) = self.operations {
231 ops.push(OperationRecord { kind });
232 }
233 }
234
235 fn set_explanation_node(
236 &mut self,
237 expression: &Expression,
238 node: crate::evaluation::explanation::ExplanationNode,
239 ) {
240 self.explanation_nodes
241 .insert(expression as *const Expression as usize, node);
242 }
243
244 fn get_explanation_node(
245 &self,
246 expression: &Expression,
247 ) -> Option<&crate::evaluation::explanation::ExplanationNode> {
248 self.explanation_nodes
249 .get(&(expression as *const Expression as usize))
250 }
251
252 fn get_rule_explanation(
253 &self,
254 rule_path: &RulePath,
255 ) -> Option<&crate::evaluation::explanation::Explanation> {
256 self.rule_explanations.get(rule_path)
257 }
258
259 fn set_rule_explanation(
260 &mut self,
261 rule_path: RulePath,
262 explanation: crate::evaluation::explanation::Explanation,
263 ) {
264 self.rule_explanations.insert(rule_path, explanation);
265 }
266}
267
268fn build_transitive_rule_references(
273 data: &IndexMap<DataPath, DataDefinition>,
274) -> HashMap<DataPath, RulePath> {
275 let mut out: HashMap<DataPath, RulePath> = HashMap::new();
276 for (path, def) in data {
277 if !matches!(def, DataDefinition::Reference { .. }) {
278 continue;
279 }
280 let mut visited: HashSet<DataPath> = HashSet::new();
281 let mut cursor: DataPath = path.clone();
282 loop {
283 if !visited.insert(cursor.clone()) {
284 break;
285 }
286 let Some(DataDefinition::Reference { target, .. }) = data.get(&cursor) else {
287 break;
288 };
289 match target {
290 ReferenceTarget::Data(next) => cursor = next.clone(),
291 ReferenceTarget::Rule(rule_path) => {
292 out.insert(path.clone(), rule_path.clone());
293 break;
294 }
295 }
296 }
297 }
298 out
299}
300
301fn collect_used_data_from_explanation(
302 node: &ExplanationNode,
303 out: &mut HashMap<DataPath, LiteralValue>,
304) {
305 match node {
306 ExplanationNode::Value {
307 value,
308 source: ValueSource::Data { data_ref },
309 ..
310 } => {
311 out.entry(data_ref.clone()).or_insert_with(|| value.clone());
312 }
313 ExplanationNode::Value { .. } => {}
314 ExplanationNode::RuleReference { expansion, .. } => {
315 collect_used_data_from_explanation(expansion.as_ref(), out);
316 }
317 ExplanationNode::Computation { operands, .. } => {
318 for op in operands {
319 collect_used_data_from_explanation(op, out);
320 }
321 }
322 ExplanationNode::Branches {
323 matched,
324 non_matched,
325 ..
326 } => {
327 if let Some(ref cond) = matched.condition {
328 collect_used_data_from_explanation(cond, out);
329 }
330 collect_used_data_from_explanation(&matched.result, out);
331 for nm in non_matched {
332 collect_used_data_from_explanation(&nm.condition, out);
333 if let Some(ref res) = nm.result {
334 collect_used_data_from_explanation(res, out);
335 }
336 }
337 }
338 ExplanationNode::Condition { operands, .. } => {
339 for op in operands {
340 collect_used_data_from_explanation(op, out);
341 }
342 }
343 ExplanationNode::Veto { .. } => {}
344 }
345}
346
347#[cfg(test)]
348mod runtime_invariant_tests {
349 use super::*;
350 use crate::parsing::ast::DateTimeValue;
351 use crate::Engine;
352
353 #[test]
361 fn reference_runtime_value_carries_resolved_type_not_target_type() {
362 let code = r#"
363spec inner
364data slot: number -> minimum 0 -> maximum 100
365
366spec source_spec
367data v: number -> default 5
368
369spec outer
370with i: inner
371with src: source_spec
372data i.slot: src.v
373rule r: i.slot
374"#;
375 let mut engine = Engine::new();
376 engine
377 .load(code, crate::SourceType::Labeled("ref_invariant.lemma"))
378 .expect("must load");
379
380 let now = DateTimeValue::now();
381 let plan = engine
382 .get_plan("outer", Some(&now))
383 .expect("must plan")
384 .clone();
385
386 let now_lit = LiteralValue {
387 value: crate::planning::semantics::ValueKind::Date(
388 crate::planning::semantics::date_time_to_semantic(&now),
389 ),
390 lemma_type: crate::planning::semantics::primitive_date().clone(),
391 };
392 let context = EvaluationContext::new(&plan, now_lit, false);
393
394 let reference_path = plan
395 .data
396 .iter()
397 .find_map(|(path, def)| match def {
398 DataDefinition::Reference { .. } => Some(path.clone()),
399 _ => None,
400 })
401 .expect("plan must contain the reference for `i.slot`");
402
403 let resolved_type = match plan.data.get(&reference_path).expect("entry exists") {
404 DataDefinition::Reference { resolved_type, .. } => resolved_type.clone(),
405 _ => unreachable!("filter above kept only Reference entries"),
406 };
407
408 let stored = context
409 .data_values
410 .get(&reference_path)
411 .expect("EvaluationContext must populate reference path with the copied value");
412
413 assert_eq!(
414 stored.lemma_type, resolved_type,
415 "stored LiteralValue must carry the reference's resolved_type \
416 (LHS-merged), not the target's loose type. \
417 stored = {:?}, resolved = {:?}",
418 stored.lemma_type, resolved_type
419 );
420 }
421}
422
423#[derive(Default)]
425pub(crate) struct Evaluator;
426
427impl Evaluator {
428 pub(crate) fn evaluate(
441 &self,
442 plan: &ExecutionPlan,
443 now: LiteralValue,
444 record_operations: bool,
445 ) -> Response {
446 let mut context = EvaluationContext::new(plan, now, record_operations);
447
448 let mut response = Response {
449 spec_name: plan.spec_name.clone(),
450 spec_hash: None,
451 spec_effective_from: None,
452 spec_effective_to: None,
453 data: Vec::new(),
454 results: IndexMap::new(),
455 };
456
457 for exec_rule in &plan.rules {
459 if let Some(ref mut ops) = context.operations {
460 ops.clear();
461 }
462 context.explanation_nodes.clear();
463
464 let (result, explanation) = expression::evaluate_rule(exec_rule, &mut context);
465
466 context
467 .rule_results
468 .insert(exec_rule.path.clone(), result.clone());
469 context.set_rule_explanation(exec_rule.path.clone(), explanation.clone());
470
471 let rule_operations = context.operations.clone().unwrap_or_default();
472
473 if !exec_rule.path.segments.is_empty() {
474 continue;
475 }
476
477 let unless_branches: Vec<(Option<Expression>, Expression)> = exec_rule.branches[1..]
478 .iter()
479 .map(|b| (b.condition.clone(), b.result.clone()))
480 .collect();
481
482 response.add_result(RuleResult {
483 rule: EvaluatedRule {
484 name: exec_rule.name.clone(),
485 path: exec_rule.path.clone(),
486 default_expression: exec_rule.branches[0].result.clone(),
487 unless_branches,
488 source_location: exec_rule.source.clone(),
489 rule_type: exec_rule.rule_type.clone(),
490 },
491 result,
492 data: vec![],
493 operations: rule_operations,
494 explanation: Some(explanation),
495 rule_type: exec_rule.rule_type.clone(),
496 });
497 }
498
499 let mut used_data: HashMap<DataPath, LiteralValue> = HashMap::new();
500 for rule_result in response.results.values() {
501 if let Some(ref explanation) = rule_result.explanation {
502 collect_used_data_from_explanation(explanation.tree.as_ref(), &mut used_data);
503 }
504 }
505
506 let data_list: Vec<Data> = plan
508 .data
509 .keys()
510 .filter_map(|path| {
511 used_data.remove(path).map(|value| Data {
512 path: path.clone(),
513 value: DataValue::Literal(value),
514 source: None,
515 })
516 })
517 .collect();
518
519 if !data_list.is_empty() {
520 response.data = vec![DataGroup {
521 data_path: String::new(),
522 referencing_data_name: String::new(),
523 data: data_list,
524 }];
525 }
526
527 response
528 }
529}