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::evaluation::operations::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> =
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 }
142 Some(_) => {
143 }
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 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 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
272fn 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 #[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#[derive(Default)]
436pub(crate) struct Evaluator;
437
438impl Evaluator {
439 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 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 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}