1use crate::computation::{OperationResult, VetoType};
2use crate::evaluation::explanations::Explanation;
3
4use crate::parsing::ast::DateTimeValue;
5use crate::planning::execution_plan::ExecutionPlan;
6use crate::planning::semantics::{
7 DataDefinition, DataPath, LemmaType, LiteralValue, PathSegment, RowInstance, RulePath, Source,
8 SpecInstance, SpecValue, ValueKind,
9};
10use crate::planning::unit_family::FamilyUnitCatalog;
11use crate::result_value::{
12 rule_result_value_failure_message, rule_result_value_from_literal, RowReference, RowResult,
13 RuleResultValue, RuleResultValueFailure, SpecResult, SpecRuleResult,
14};
15use indexmap::IndexMap;
16use std::collections::HashMap;
17use std::sync::Arc;
18
19#[derive(Debug, Clone)]
22pub struct EvaluatedRule {
23 pub name: String,
24 pub path: RulePath,
25 pub source_location: Source,
26 pub rule_type: Arc<LemmaType>,
27}
28
29#[derive(Debug, Clone)]
31pub struct Response {
32 pub spec_name: String,
33 pub effective: String,
34 pub spec_effective_from: Option<DateTimeValue>,
38 pub spec_effective_to: Option<DateTimeValue>,
39 pub results: IndexMap<String, RuleResult>,
40 pub rows: Vec<RowResult>,
42}
43
44#[derive(Debug, Clone)]
46pub struct RuleResult {
47 pub rule: EvaluatedRule,
48 pub veto_detail: Option<VetoType>,
49
50 pub vetoed: bool,
51 pub veto_reason: Option<String>,
52 pub rule_type: String,
53
54 pub result: Option<RuleResultValue>,
56 pub explanation: Option<Explanation>,
57 missing_data: Vec<String>,
62}
63
64impl RuleResult {
65 #[must_use]
67 pub fn result(&self) -> Option<&str> {
68 self.result
69 .as_ref()
70 .and_then(|value| value.result.as_deref())
71 }
72
73 #[must_use]
78 pub fn is_missing_data(&self) -> bool {
79 matches!(
80 self.veto_detail.as_ref(),
81 Some(VetoType::MissingData { .. } | VetoType::MissingRowData { .. })
82 )
83 }
84
85 #[must_use]
88 pub fn missing_data(&self) -> &[String] {
89 &self.missing_data
90 }
91
92 pub(crate) fn from_operation_result(
96 rule: EvaluatedRule,
97 operation_result: &OperationResult,
98 rule_type: &LemmaType,
99 family_units: &FamilyUnitCatalog,
100 explanation: Option<Explanation>,
101 missing_data: Vec<String>,
102 ) -> Self {
103 match operation_result {
104 OperationResult::Veto(VetoType::MissingData { data, .. }) => {
105 let key = data.input_key();
106 if !missing_data.iter().any(|listed| listed == &key) {
107 panic!("BUG: MissingData path {key} not in missing_data {missing_data:?}");
108 }
109 }
110 OperationResult::Veto(VetoType::MissingRowData { key }) => {
111 if !missing_data.contains(key) {
112 panic!("BUG: missing row field {key} not in missing_data {missing_data:?}");
113 }
114 }
115 _ => {
116 if !missing_data.is_empty() {
117 panic!(
118 "BUG: missing_data must be empty when result is not MissingData: {missing_data:?}"
119 );
120 }
121 }
122 }
123 let rule_type_name = rule_type.name().to_string();
124 match operation_result {
125 OperationResult::Veto(veto) => Self::vetoed_with(
126 rule,
127 veto.clone(),
128 rule_type_name,
129 explanation,
130 missing_data,
131 ),
132 OperationResult::Value(bound) => {
133 match rule_result_value_from_literal(&bound.to_literal(), rule_type, family_units) {
134 Ok(value) => Self {
135 rule,
136 veto_detail: None,
137 vetoed: false,
138 veto_reason: None,
139 rule_type: rule_type_name,
140 result: Some(value),
141 explanation,
142 missing_data,
143 },
144 Err(failure) => vetoed_rule_result_for_rule_result_value_failure(
145 rule,
146 rule_type,
147 explanation,
148 failure,
149 missing_data,
150 ),
151 }
152 }
153 }
154 }
155
156 fn vetoed_with(
157 rule: EvaluatedRule,
158 veto: VetoType,
159 rule_type: String,
160 explanation: Option<Explanation>,
161 missing_data: Vec<String>,
162 ) -> Self {
163 Self {
164 rule,
165 veto_reason: match &veto {
166 VetoType::UserDefined { message: None } => None,
167 _ => Some(veto.to_string()),
168 },
169 veto_detail: Some(veto),
170 vetoed: true,
171 rule_type,
172 result: None,
173 explanation,
174 missing_data,
175 }
176 }
177
178 pub fn to_literal(&self) -> LiteralValue {
182 assert!(
183 !self.vetoed,
184 "BUG: to_literal called on vetoed rule '{}'",
185 self.rule.name
186 );
187 let value = self
188 .result
189 .as_ref()
190 .unwrap_or_else(|| panic!("BUG: non-vetoed rule '{}' missing result", self.rule.name));
191 value.to_literal(&self.rule.rule_type)
192 }
193}
194
195fn vetoed_rule_result_for_rule_result_value_failure(
196 rule: EvaluatedRule,
197 rule_type: &LemmaType,
198 explanation: Option<Explanation>,
199 failure: RuleResultValueFailure,
200 missing_data: Vec<String>,
201) -> RuleResult {
202 RuleResult::vetoed_with(
203 rule,
204 VetoType::computation(rule_result_value_failure_message(failure).to_string()),
205 rule_type.name().to_string(),
206 explanation,
207 missing_data,
208 )
209}
210
211impl Response {
212 pub fn get(&self, rule_name: &str) -> Result<&RuleResult, crate::error::Error> {
216 self.results
217 .get(rule_name)
218 .ok_or_else(|| crate::error::Error::rule_not_found(rule_name, None::<String>))
219 }
220
221 pub fn add_result(&mut self, result: RuleResult) {
222 self.results.insert(result.rule.name.clone(), result);
223 }
224}
225
226#[derive(Default)]
230pub(crate) struct RowTable {
231 index: HashMap<Arc<RowInstance>, usize>,
232 rows: Vec<Option<RowResult>>,
233}
234
235impl RowTable {
236 pub(crate) fn into_rows(self) -> Vec<RowResult> {
237 self.rows
238 .into_iter()
239 .enumerate()
240 .map(|(index, row)| row.unwrap_or_else(|| panic!("BUG: row {index} never filled")))
241 .collect()
242 }
243}
244
245pub(crate) fn fill_spec_values(
248 literal: &ValueKind,
249 value: &mut RuleResultValue,
250 plan: &ExecutionPlan,
251 ctx: &mut crate::evaluation::EvaluationContext,
252 table: &mut RowTable,
253 stack: &mut Vec<SpecInstance>,
254) {
255 if matches!(
256 literal,
257 ValueKind::Number(_)
258 | ValueKind::Measure(_)
259 | ValueKind::Ratio(_)
260 | ValueKind::Text(_)
261 | ValueKind::Date(_)
262 | ValueKind::Time(_)
263 | ValueKind::Boolean(_)
264 | ValueKind::Range(_, _)
265 ) {
266 return;
267 }
268 let mut walk = Walk {
269 base: ctx,
270 table,
271 row_contexts: Vec::new(),
272 ancestors: std::mem::take(stack),
273 };
274 let root = Frame::fill(plan, literal.clone(), std::mem::take(value));
275 *value = walk.run(root).into_value();
276 *stack = walk.ancestors;
277}
278
279struct Walk<'b> {
282 base: &'b mut crate::evaluation::EvaluationContext,
283 table: &'b mut RowTable,
284 row_contexts: Vec<crate::evaluation::EvaluationContext>,
285 ancestors: Vec<SpecInstance>,
286}
287
288impl Walk<'_> {
289 fn ctx(&mut self) -> &mut crate::evaluation::EvaluationContext {
290 match self.row_contexts.last_mut() {
291 Some(row_ctx) => row_ctx,
292 None => &mut *self.base,
293 }
294 }
295
296 fn run<'a>(&mut self, root: Frame<'a>) -> Output {
297 let mut frames = vec![root];
298 let mut input = None;
299 loop {
300 let frame = frames.last_mut().expect("BUG: fill walk frame stack empty");
301 match frame.resume(self, input.take()) {
302 Step::Call(child) => frames.push(child),
303 Step::Return(output) => {
304 frames.pop();
305 if frames.is_empty() {
306 return output;
307 }
308 input = Some(output);
309 }
310 }
311 }
312 }
313}
314
315enum Frame<'a> {
316 Fill(Box<FillFrame<'a>>),
317 Spec(SpecFrame<'a>),
318 Row(RowFrame<'a>),
319 Members(Box<MembersFrame<'a>>),
320}
321
322impl<'a> Frame<'a> {
323 fn fill(plan: &'a ExecutionPlan, literal: ValueKind, value: RuleResultValue) -> Self {
324 Frame::Fill(Box::new(FillFrame::new(plan, literal, value)))
325 }
326
327 fn members(
328 plan: &'a ExecutionPlan,
329 prefix: Vec<PathSegment>,
330 row: Option<Arc<RowInstance>>,
331 ) -> Self {
332 Frame::Members(Box::new(MembersFrame::new(plan, prefix, row)))
333 }
334
335 fn resume(&mut self, walk: &mut Walk<'_>, input: Option<Output>) -> Step<'a> {
336 match self {
337 Frame::Fill(frame) => frame.resume(input),
338 Frame::Spec(frame) => frame.resume(walk, input),
339 Frame::Row(frame) => frame.resume(walk, input),
340 Frame::Members(frame) => frame.resume(walk, input),
341 }
342 }
343}
344
345enum Step<'a> {
346 Call(Frame<'a>),
347 Return(Output),
348}
349
350enum Output {
351 Value(Box<RuleResultValue>),
352 Spec(SpecResult),
353 Row(RowReference),
354 Members(FilledMembers),
355}
356
357impl Output {
358 fn into_value(self) -> RuleResultValue {
359 match self {
360 Output::Value(value) => *value,
361 _ => panic!("BUG: fill walk expected a value output"),
362 }
363 }
364
365 fn into_spec(self) -> SpecResult {
366 match self {
367 Output::Spec(spec) => spec,
368 _ => panic!("BUG: fill walk expected a spec output"),
369 }
370 }
371
372 fn into_row(self) -> RowReference {
373 match self {
374 Output::Row(row) => row,
375 _ => panic!("BUG: fill walk expected a row output"),
376 }
377 }
378
379 fn into_members(self) -> FilledMembers {
380 match self {
381 Output::Members(members) => members,
382 _ => panic!("BUG: fill walk expected a members output"),
383 }
384 }
385}
386
387fn child_output(input: Option<Output>) -> Output {
388 input.expect("BUG: fill walk frame resumed without child output")
389}
390
391enum FillState {
392 Start(ValueKind),
393 AwaitSpec,
394 AwaitRow,
395 Items(
396 std::vec::IntoIter<(ValueKind, RuleResultValue)>,
397 Vec<RuleResultValue>,
398 ),
399}
400
401struct FillFrame<'a> {
404 plan: &'a ExecutionPlan,
405 value: RuleResultValue,
406 state: Option<FillState>,
407}
408
409impl<'a> FillFrame<'a> {
410 fn new(plan: &'a ExecutionPlan, literal: ValueKind, value: RuleResultValue) -> Self {
411 Self {
412 plan,
413 value,
414 state: Some(FillState::Start(literal)),
415 }
416 }
417
418 fn resume(&mut self, input: Option<Output>) -> Step<'a> {
419 match self
420 .state
421 .take()
422 .expect("BUG: fill frame resumed after completion")
423 {
424 FillState::Start(literal) => {
425 assert!(
426 input.is_none(),
427 "BUG: fill frame started with a child output"
428 );
429 self.start(literal)
430 }
431 FillState::AwaitSpec => {
432 self.value.spec = Some(Box::new(child_output(input).into_spec()));
433 self.finish()
434 }
435 FillState::AwaitRow => {
436 self.value.row = Some(child_output(input).into_row());
437 self.finish()
438 }
439 FillState::Items(items, mut filled) => {
440 filled.push(child_output(input).into_value());
441 self.next_item(items, filled)
442 }
443 }
444 }
445
446 fn start(&mut self, literal: ValueKind) -> Step<'a> {
447 let plan = self.plan;
448 match literal {
449 ValueKind::Spec(SpecValue::Fixed(instance)) => {
450 assert!(
451 self.value.spec.is_some(),
452 "BUG: fixed spec value '{instance}' without spec arm"
453 );
454 self.state = Some(FillState::AwaitSpec);
455 Step::Call(Frame::Spec(SpecFrame::new(plan, instance)))
456 }
457 ValueKind::Spec(SpecValue::Row(row)) => {
458 assert!(
459 self.value.row.is_none(),
460 "BUG: row '{}' filled twice",
461 row.origin
462 );
463 self.state = Some(FillState::AwaitRow);
464 Step::Call(Frame::Row(RowFrame::new(plan, row)))
465 }
466 ValueKind::List(rows) => {
467 let entries = self
468 .value
469 .list
470 .take()
471 .expect("BUG: list value without list arm");
472 assert_eq!(
473 entries.len(),
474 rows.len(),
475 "BUG: list arm row count differs from the list value"
476 );
477 let filled = Vec::with_capacity(entries.len());
478 let items: Vec<(ValueKind, RuleResultValue)> =
479 rows.into_iter().map(|row| row.value).zip(entries).collect();
480 self.next_item(items.into_iter(), filled)
481 }
482 ValueKind::Number(_)
483 | ValueKind::Measure(_)
484 | ValueKind::Ratio(_)
485 | ValueKind::Text(_)
486 | ValueKind::Date(_)
487 | ValueKind::Time(_)
488 | ValueKind::Boolean(_)
489 | ValueKind::Range(_, _) => self.finish(),
490 }
491 }
492
493 fn next_item(
494 &mut self,
495 mut items: std::vec::IntoIter<(ValueKind, RuleResultValue)>,
496 filled: Vec<RuleResultValue>,
497 ) -> Step<'a> {
498 match items.next() {
499 Some((literal, entry)) => {
500 self.state = Some(FillState::Items(items, filled));
501 Step::Call(Frame::fill(self.plan, literal, entry))
502 }
503 None => {
504 self.value.list = Some(filled);
505 self.finish()
506 }
507 }
508 }
509
510 fn finish(&mut self) -> Step<'a> {
511 Step::Return(Output::Value(Box::new(std::mem::take(&mut self.value))))
512 }
513}
514
515struct SpecFrame<'a> {
518 plan: &'a ExecutionPlan,
519 instance: SpecInstance,
520 identity: Option<(Option<String>, String, String)>,
521}
522
523impl<'a> SpecFrame<'a> {
524 fn new(plan: &'a ExecutionPlan, instance: SpecInstance) -> Self {
525 Self {
526 plan,
527 instance,
528 identity: None,
529 }
530 }
531
532 fn resume(&mut self, walk: &mut Walk<'_>, input: Option<Output>) -> Step<'a> {
533 let plan = self.plan;
534 match input {
535 None => {
536 if walk.ancestors.iter().any(|seen| seen == &self.instance) {
537 panic!("BUG: spec result walked instance '{}' twice", self.instance);
538 }
539 walk.ancestors.push(self.instance.clone());
540 self.identity = Some(instance_identity(plan, &self.instance));
541 Step::Call(Frame::members(plan, self.instance.prefix.clone(), None))
542 }
543 Some(output) => {
544 walk.ancestors.pop();
545 let (data, rules) = output.into_members();
546 let (repository, spec, effective) = self
547 .identity
548 .take()
549 .expect("BUG: spec frame finished before it started");
550 Step::Return(Output::Spec(SpecResult {
551 repository,
552 spec,
553 effective,
554 instance: self.instance.to_string(),
555 path: self
556 .instance
557 .prefix
558 .iter()
559 .map(crate::api::PathSegment::from)
560 .collect(),
561 data,
562 rules,
563 }))
564 }
565 }
566 }
567}
568
569struct RowFrame<'a> {
572 plan: &'a ExecutionPlan,
573 row: Arc<RowInstance>,
574 index: usize,
575 saved_ancestors: Vec<SpecInstance>,
576}
577
578impl<'a> RowFrame<'a> {
579 fn new(plan: &'a ExecutionPlan, row: Arc<RowInstance>) -> Self {
580 Self {
581 plan,
582 row,
583 index: 0,
584 saved_ancestors: Vec::new(),
585 }
586 }
587
588 fn resume(&mut self, walk: &mut Walk<'_>, input: Option<Output>) -> Step<'a> {
589 match input {
590 None => {
591 if let Some(index) = walk.table.index.get(&self.row).copied() {
592 return Step::Return(Output::Row(row_reference(&self.row, index)));
593 }
594 let index = walk.table.rows.len();
595 walk.table.index.insert(Arc::clone(&self.row), index);
596 walk.table.rows.push(None);
597 self.index = index;
598 let plan = self.plan;
599 let template = plan.row_template(&self.row.identity);
600 let row_ctx =
601 crate::evaluation::rows::take_row_context(&self.row, template, walk.ctx());
602 walk.row_contexts.push(row_ctx);
603 self.saved_ancestors = std::mem::take(&mut walk.ancestors);
604 Step::Call(Frame::members(
605 template,
606 Vec::new(),
607 Some(Arc::clone(&self.row)),
608 ))
609 }
610 Some(output) => {
611 let (data, rules) = output.into_members();
612 walk.ancestors = std::mem::take(&mut self.saved_ancestors);
613 let row_ctx = walk
614 .row_contexts
615 .pop()
616 .expect("BUG: row context stack empty when its row ends");
617 walk.ctx()
618 .row_contexts
619 .insert(Arc::clone(&self.row), row_ctx);
620 walk.table.rows[self.index] = Some(RowResult {
621 repository: self.row.identity.repository.clone(),
622 spec: self.row.identity.spec.clone(),
623 effective: self.row.identity.effective.to_string(),
624 data,
625 rules,
626 });
627 Step::Return(Output::Row(row_reference(&self.row, self.index)))
628 }
629 }
630 }
631}
632
633fn row_reference(row: &RowInstance, index: usize) -> RowReference {
634 RowReference {
635 repository: row.identity.repository.clone(),
636 spec: row.identity.spec.clone(),
637 effective: row.identity.effective.to_string(),
638 instance: row.origin.to_string(),
639 index,
640 }
641}
642
643enum Awaiting {
645 Data(String, SpecRuleResult),
646 Rule(RulePath, SpecRuleResult),
647}
648
649struct MembersFrame<'a> {
652 plan: &'a ExecutionPlan,
653 prefix: Vec<PathSegment>,
654 row: Option<Arc<RowInstance>>,
655 data_keys: std::vec::IntoIter<(DataPath, &'a DataDefinition)>,
656 rule_paths: std::vec::IntoIter<RulePath>,
657 data: IndexMap<String, SpecRuleResult>,
658 rules: IndexMap<String, SpecRuleResult>,
659 awaiting: Option<Awaiting>,
660}
661
662impl<'a> MembersFrame<'a> {
663 fn new(
664 plan: &'a ExecutionPlan,
665 prefix: Vec<PathSegment>,
666 row: Option<Arc<RowInstance>>,
667 ) -> Self {
668 let rule_paths: Vec<RulePath> = plan
669 .rules
670 .keys()
671 .filter(|path| path_under_prefix(&path.segments, &prefix))
672 .cloned()
673 .collect();
674 Self {
675 plan,
676 data_keys: plan.nested_input_keys().into_iter(),
678 rule_paths: rule_paths.into_iter(),
679 prefix,
680 row,
681 data: IndexMap::new(),
682 rules: IndexMap::new(),
683 awaiting: None,
684 }
685 }
686
687 fn resume(&mut self, walk: &mut Walk<'_>, input: Option<Output>) -> Step<'a> {
688 match (self.awaiting.take(), input) {
689 (None, None) => {}
690 (Some(Awaiting::Data(key, mut entry)), Some(output)) => {
691 entry.result = Some(output.into_value());
692 self.data.insert(key, entry);
693 }
694 (Some(Awaiting::Rule(path, mut entry)), Some(output)) => {
695 entry.result = Some(output.into_value());
696 self.rules
697 .insert(relative_rule_key(&path, &self.prefix), entry);
698 }
699 _ => panic!("BUG: members frame resumed with an unexpected child output"),
700 }
701 while let Some((path, definition)) = self.data_keys.next() {
702 if let Some(step) = self.data_item(walk, path, definition) {
703 return step;
704 }
705 }
706 while let Some(path) = self.rule_paths.next() {
707 if let Some(step) = self.rule_item(walk, path) {
708 return step;
709 }
710 }
711 Step::Return(Output::Members((
712 std::mem::take(&mut self.data),
713 std::mem::take(&mut self.rules),
714 )))
715 }
716
717 fn data_item(
718 &mut self,
719 walk: &mut Walk<'_>,
720 path: DataPath,
721 definition: &'a DataDefinition,
722 ) -> Option<Step<'a>> {
723 let plan = self.plan;
724 if path.segments.len() != self.prefix.len()
725 || !path_under_prefix(&path.segments, &self.prefix)
726 {
727 return None;
728 }
729 definition.schema_type()?;
730 let relative = relative_data_key(&path, &self.prefix);
731 let slot = match walk.ctx().data_slot(plan, &path).cloned() {
732 Some(slot) => slot,
733 None if !path.segments.is_empty() => {
734 crate::evaluation::imports::eval_import_member(&path, plan, walk.ctx())
735 }
736 None => return None,
737 };
738 if matches!(slot, OperationResult::Veto(VetoType::MissingData { .. })) {
739 return None;
740 }
741 let display_type =
742 crate::evaluation::EvaluationContext::data_display_type(plan, &path, &slot);
743 let mut entry = spec_entry(
744 &slot,
745 path.input_key(),
746 RulePath {
747 segments: path.segments.clone(),
748 rule: path.data.clone(),
749 },
750 definition.source().clone(),
751 display_type.as_ref(),
752 &plan.family_units,
753 Vec::new(),
754 );
755 let OperationResult::Value(bound) = slot else {
756 self.data.insert(relative, entry);
757 return None;
758 };
759 let Some(value) = entry.result.take() else {
760 self.data.insert(relative, entry);
761 return None;
762 };
763 self.awaiting = Some(Awaiting::Data(relative, entry));
764 Some(Step::Call(Frame::fill(plan, bound.value, value)))
765 }
766
767 fn rule_item(&mut self, walk: &mut Walk<'_>, path: RulePath) -> Option<Step<'a>> {
768 let plan = self.plan;
769 let row = self.row.as_deref();
770 let exec = plan
771 .rules
772 .get(&path)
773 .expect("BUG: rule path missing from plan");
774 crate::evaluation::tree::ensure_rule_values(plan.rule_index(&path), plan, walk.ctx());
775 let local = walk.ctx().rule_value(plan, &path).clone();
776 let missing_data = if local.is_missing_data() {
777 let keys = walk.ctx().missing_data_for_rule(plan, exec.normal_form);
778 match row {
779 Some(row) => keys
780 .iter()
781 .map(|key| crate::evaluation::rows::row_key(row, key))
782 .collect(),
783 None => keys,
784 }
785 } else {
786 Vec::new()
787 };
788 let operation = match row {
789 Some(row) => crate::evaluation::rows::row_result(local, row, plan),
790 None => local,
791 };
792 let mut entry = spec_entry(
793 &operation,
794 path.input_key(),
795 path.clone(),
796 exec.source.clone(),
797 exec.rule_type.as_ref(),
798 &plan.family_units,
799 missing_data,
800 );
801 match operation {
802 OperationResult::Value(bound) => match entry.result.take() {
803 Some(value) => {
804 self.awaiting = Some(Awaiting::Rule(path, entry));
805 Some(Step::Call(Frame::fill(plan, bound.value, value)))
806 }
807 None => {
808 self.rules
809 .insert(relative_rule_key(&path, &self.prefix), entry);
810 None
811 }
812 },
813 OperationResult::Veto(_) => {
814 self.rules
815 .insert(relative_rule_key(&path, &self.prefix), entry);
816 None
817 }
818 }
819 }
820}
821
822type FilledMembers = (
823 IndexMap<String, SpecRuleResult>,
824 IndexMap<String, SpecRuleResult>,
825);
826
827fn instance_identity(
828 plan: &ExecutionPlan,
829 instance: &SpecInstance,
830) -> (Option<String>, String, String) {
831 let last = instance
832 .prefix
833 .last()
834 .unwrap_or_else(|| panic!("BUG: spec instance '{instance}' has no uses hop"));
835 let import_path = DataPath {
836 segments: instance.prefix[..instance.prefix.len() - 1].to_vec(),
837 data: last.uses.clone(),
838 };
839 let Some(crate::planning::semantics::DataDefinition::Import { identity, .. }) =
840 crate::planning::normalize::lookup_data_definition(
841 &plan.data,
842 &plan.row_templates,
843 &import_path,
844 )
845 else {
846 panic!(
847 "BUG: spec instance '{instance}' is not an import at '{}'",
848 import_path.input_key()
849 );
850 };
851 (
852 identity.repository.clone(),
853 identity.spec.clone(),
854 identity.effective.to_string(),
855 )
856}
857
858fn spec_entry(
859 operation: &OperationResult,
860 name: String,
861 path: RulePath,
862 source: Source,
863 display_type: &LemmaType,
864 family_units: &FamilyUnitCatalog,
865 missing_data: Vec<String>,
866) -> SpecRuleResult {
867 let built = RuleResult::from_operation_result(
868 EvaluatedRule {
869 name,
870 path,
871 source_location: source,
872 rule_type: Arc::new(display_type.clone()),
873 },
874 operation,
875 display_type,
876 family_units,
877 None,
878 missing_data,
879 );
880 SpecRuleResult {
881 vetoed: built.vetoed,
882 veto_reason: built.veto_reason.clone(),
883 rule_type: built.rule_type.clone(),
884 result: built.result.clone(),
885 }
886}
887
888fn path_under_prefix(segments: &[PathSegment], prefix: &[PathSegment]) -> bool {
889 segments.len() >= prefix.len() && segments[..prefix.len()] == prefix[..]
890}
891
892fn relative_data_key(path: &DataPath, prefix: &[PathSegment]) -> String {
893 assert!(
894 path_under_prefix(&path.segments, prefix),
895 "BUG: data '{}' is under its instance but path_under_prefix is false",
896 path.input_key()
897 );
898 let mut key = String::new();
899 for segment in &path.segments[prefix.len()..] {
900 key.push_str(&segment.uses);
901 key.push('.');
902 }
903 key.push_str(&path.data);
904 key
905}
906
907fn relative_rule_key(path: &RulePath, prefix: &[PathSegment]) -> String {
908 assert!(
909 path_under_prefix(&path.segments, prefix),
910 "BUG: rule '{}' is under its instance but path_under_prefix is false",
911 path.input_key()
912 );
913 let mut key = String::new();
914 for segment in &path.segments[prefix.len()..] {
915 key.push_str(&segment.uses);
916 key.push('.');
917 }
918 key.push_str(&path.rule);
919 key
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925 use crate::literals::DateGranularity;
926 use crate::parsing::ast::Span;
927 use crate::planning::semantics::{
928 primitive_number_arc, BaseMeasureVector, DataPath, LemmaType, LiteralValue, MeasureUnit,
929 MeasureUnits, RatioUnit, RatioUnits, RulePath, TypeExtends, TypeSpecification, ValueKind,
930 };
931 use crate::planning::unit_family::FamilyUnitCatalog;
932 use crate::planning::unit_index::{UnitIndex, UnitOwner};
933 use rust_decimal::Decimal;
934 use std::str::FromStr;
935 use std::sync::Arc;
936
937 fn dec(s: &str) -> Decimal {
938 Decimal::from_str(s).expect("BUG: test decimal literal")
939 }
940
941 fn empty_family_catalog() -> FamilyUnitCatalog {
942 FamilyUnitCatalog::default()
943 }
944
945 fn family_catalog_for_index(index: &UnitIndex) -> FamilyUnitCatalog {
946 FamilyUnitCatalog::build(index)
947 }
948
949 fn family_catalog_for_money(money: &LemmaType) -> FamilyUnitCatalog {
950 family_catalog_for_index(&unit_index_for_money(money))
951 }
952
953 fn family_catalog_for_ratio(ratio_type: &LemmaType) -> FamilyUnitCatalog {
954 family_catalog_for_index(&unit_index_for_ratio(ratio_type))
955 }
956
957 fn unit_index_for_money(money: &LemmaType) -> UnitIndex {
958 let mut index = UnitIndex::new();
959 let arc = Arc::new(money.clone());
960 for unit_name in money.measure_unit_names().into_iter().flatten() {
961 index.insert_owner(
962 unit_name.to_string(),
963 UnitOwner::Named {
964 import_path: Vec::new(),
965 type_name: "money".into(),
966 lemma_type: Arc::clone(&arc),
967 },
968 );
969 }
970 index
971 }
972
973 fn unit_index_for_ratio(ratio_type: &LemmaType) -> UnitIndex {
974 let mut index = UnitIndex::new();
975 let arc = Arc::new(ratio_type.clone());
976 let type_name = ratio_type.name();
977 for unit_name in ratio_type.ratio_unit_names().into_iter().flatten() {
978 index.insert_owner(
979 unit_name.to_string(),
980 UnitOwner::Named {
981 import_path: Vec::new(),
982 type_name: type_name.clone(),
983 lemma_type: Arc::clone(&arc),
984 },
985 );
986 }
987 index
988 }
989
990 fn dummy_source() -> Source {
991 Source::new(
992 crate::parsing::source::SourceType::Volatile,
993 Span {
994 start: 0,
995 end: 0,
996 line: 1,
997 col: 1,
998 },
999 )
1000 }
1001
1002 fn dummy_evaluated_rule(name: &str, rule_type: &LemmaType) -> EvaluatedRule {
1003 EvaluatedRule {
1004 name: name.to_string(),
1005 path: RulePath::new(vec![], name.to_string()),
1006 source_location: dummy_source(),
1007 rule_type: Arc::new(rule_type.clone()),
1008 }
1009 }
1010
1011 #[test]
1012 fn test_response_serialization() {
1013 let mut results = IndexMap::new();
1014 results.insert(
1015 "test_rule".to_string(),
1016 RuleResult::from_operation_result(
1017 dummy_evaluated_rule("test_rule", primitive_number_arc().as_ref()),
1018 &OperationResult::from_literal(LiteralValue::number_from_decimal(Decimal::from(
1019 42,
1020 ))),
1021 primitive_number_arc().as_ref(),
1022 &empty_family_catalog(),
1023 None,
1024 Vec::new(),
1025 ),
1026 );
1027 let response = Response {
1028 spec_name: "test_spec".to_string(),
1029 effective: "2026-01-01".to_string(),
1030 spec_effective_from: None,
1031 spec_effective_to: None,
1032 results,
1033 rows: Vec::new(),
1034 };
1035
1036 let json = serde_json::to_string(&crate::api::Response::from(&response)).unwrap();
1037 assert!(json.contains("test_spec"));
1038 assert!(json.contains("test_rule"));
1039 assert!(json.contains("\"number\":\"42\""));
1040 assert!(!json.contains("lemma_type"));
1041 }
1042
1043 #[test]
1044 fn response_number_json_never_uses_fraction_notation() {
1045 use crate::computation::rational::decimal_to_rational;
1046
1047 let rational = decimal_to_rational(Decimal::new(1, 1) / Decimal::new(3, 1)).unwrap();
1048 let decimal_string = rational.try_to_decimal().unwrap().to_string();
1049 let mut results = IndexMap::new();
1050 results.insert(
1051 "third".to_string(),
1052 RuleResult::from_operation_result(
1053 dummy_evaluated_rule("third", primitive_number_arc().as_ref()),
1054 &OperationResult::from_literal(LiteralValue::number_from_decimal(
1055 rational.try_to_decimal().unwrap(),
1056 )),
1057 primitive_number_arc().as_ref(),
1058 &empty_family_catalog(),
1059 None,
1060 Vec::new(),
1061 ),
1062 );
1063 if let Some(rule) = results.get_mut("third") {
1065 let decimal = rational.try_to_decimal().unwrap();
1066 rule.result = Some(crate::result_value::RuleResultValue {
1067 result: Some(decimal_string.clone()),
1068 number: Some(decimal),
1069 ..Default::default()
1070 });
1071 }
1072
1073 let response = Response {
1074 spec_name: "test".to_string(),
1075 effective: "test".to_string(),
1076 spec_effective_from: None,
1077 spec_effective_to: None,
1078 results,
1079 rows: Vec::new(),
1080 };
1081
1082 let json: serde_json::Value = serde_json::from_str(
1083 &serde_json::to_string(&crate::api::Response::from(&response)).unwrap(),
1084 )
1085 .unwrap();
1086 let number = json["results"]["third"]["number"]
1087 .as_str()
1088 .expect("number must be a JSON string");
1089 assert!(
1090 !number.contains('/'),
1091 "API decimal string must not use fraction notation, got {number}"
1092 );
1093 }
1094
1095 #[test]
1096 fn test_rule_result_veto() {
1097 let missing = RuleResult::from_operation_result(
1098 dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
1099 &OperationResult::Veto(VetoType::missing_data(
1100 DataPath::new(vec![], "data1".to_string()),
1101 None,
1102 )),
1103 &LemmaType::veto_type(),
1104 &empty_family_catalog(),
1105 None,
1106 vec!["data1".to_string()],
1107 );
1108 assert!(missing.vetoed);
1109 assert!(missing.veto_reason.as_ref().unwrap().contains("data1"));
1110
1111 let veto = RuleResult::from_operation_result(
1112 dummy_evaluated_rule("rule4", &LemmaType::veto_type()),
1113 &OperationResult::Veto(VetoType::UserDefined {
1114 message: Some("Vetoed".to_string()),
1115 }),
1116 &LemmaType::veto_type(),
1117 &empty_family_catalog(),
1118 None,
1119 Vec::new(),
1120 );
1121 assert_eq!(veto.veto_reason.as_deref(), Some("Vetoed"));
1122 }
1123
1124 #[test]
1126 #[should_panic(expected = "BUG: MissingData")]
1127 fn missing_data_veto_must_not_attach_empty_missing_data_list() {
1128 RuleResult::from_operation_result(
1129 dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
1130 &OperationResult::Veto(VetoType::missing_data(
1131 DataPath::new(vec![], "data1".to_string()),
1132 None,
1133 )),
1134 &LemmaType::veto_type(),
1135 &empty_family_catalog(),
1136 None,
1137 Vec::new(),
1138 );
1139 }
1140
1141 #[test]
1142 #[should_panic(expected = "BUG: MissingData")]
1143 fn missing_data_veto_must_include_veto_path_in_list() {
1144 RuleResult::from_operation_result(
1145 dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
1146 &OperationResult::Veto(VetoType::missing_data(
1147 DataPath::new(vec![], "data1".to_string()),
1148 None,
1149 )),
1150 &LemmaType::veto_type(),
1151 &empty_family_catalog(),
1152 None,
1153 vec!["other".to_string()],
1154 );
1155 }
1156
1157 #[test]
1158 #[should_panic(expected = "BUG: missing_data must be empty")]
1159 fn non_missing_data_result_must_not_attach_leftover_missing_data() {
1160 RuleResult::from_operation_result(
1161 dummy_evaluated_rule("rule4", &LemmaType::veto_type()),
1162 &OperationResult::Veto(VetoType::UserDefined {
1163 message: Some("Vetoed".to_string()),
1164 }),
1165 &LemmaType::veto_type(),
1166 &empty_family_catalog(),
1167 None,
1168 vec!["leftover".to_string()],
1169 );
1170 }
1171
1172 #[test]
1173 fn rule_result_value_out_of_memory_is_not_decimal_limit_veto() {
1174 let result = vetoed_rule_result_for_rule_result_value_failure(
1175 dummy_evaluated_rule("rule", primitive_number_arc().as_ref()),
1176 primitive_number_arc().as_ref(),
1177 None,
1178 RuleResultValueFailure::OutOfMemory,
1179 Vec::new(),
1180 );
1181 assert_eq!(
1182 result.veto_reason.as_deref(),
1183 Some("The calculation ran out of memory.")
1184 );
1185 assert_ne!(
1186 result.veto_reason.as_deref(),
1187 Some("The calculated result exceeds the decimal value limit.")
1188 );
1189 }
1190
1191 #[test]
1192 fn rule_result_value_decimal_limit_uses_commit_message() {
1193 let result = vetoed_rule_result_for_rule_result_value_failure(
1194 dummy_evaluated_rule("rule", primitive_number_arc().as_ref()),
1195 primitive_number_arc().as_ref(),
1196 None,
1197 RuleResultValueFailure::DecimalLimit,
1198 Vec::new(),
1199 );
1200 assert_eq!(
1201 result.veto_reason.as_deref(),
1202 Some("The calculated result exceeds the decimal value limit.")
1203 );
1204 }
1205
1206 fn test_money_type() -> LemmaType {
1207 LemmaType::new(
1208 "money".to_string(),
1209 TypeSpecification::Measure {
1210 minimum: None,
1211 maximum: None,
1212 decimals: Some(2),
1213 units: MeasureUnits::from(vec![
1214 MeasureUnit {
1215 name: "eur".to_string(),
1216 factor: crate::computation::rational::rational_one(),
1217 derived_measure_factors: Vec::new(),
1218 decomposition: BaseMeasureVector::new(),
1219 minimum: None,
1220 maximum: None,
1221 suggestion_magnitude: None,
1222 },
1223 MeasureUnit {
1224 name: "usd".to_string(),
1225 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
1226 91, 2,
1227 ))
1228 .expect("factor"),
1229 derived_measure_factors: Vec::new(),
1230 decomposition: BaseMeasureVector::new(),
1231 minimum: None,
1232 maximum: None,
1233 suggestion_magnitude: None,
1234 },
1235 ]),
1236 traits: Vec::new(),
1237 decomposition: Some(BaseMeasureVector::new()),
1238 unit: None,
1239 help: String::new(),
1240 },
1241 TypeExtends::Primitive,
1242 )
1243 }
1244
1245 #[test]
1246 fn measure_rule_result_value_uses_rule_type_when_expression_index_empty() {
1247 let money = test_money_type();
1248 let family_units = family_catalog_for_money(&money);
1249 let ten_usd = LiteralValue {
1250 value: ValueKind::Measure(
1251 crate::computation::rational::checked_mul(
1252 &crate::computation::rational::decimal_to_rational(Decimal::from(10))
1253 .expect("ten"),
1254 &crate::computation::rational::decimal_to_rational(Decimal::new(91, 2))
1255 .expect("usd factor"),
1256 )
1257 .expect("canonical usd"),
1258 ),
1259 };
1260 let result = RuleResult::from_operation_result(
1261 dummy_evaluated_rule("total", &money),
1262 &OperationResult::from_literal(ten_usd),
1263 &money,
1264 &family_units,
1265 None,
1266 Vec::new(),
1267 );
1268 let measure = result
1269 .result
1270 .as_ref()
1271 .expect("value")
1272 .measure
1273 .clone()
1274 .expect("measure map");
1275 assert_eq!(measure.get("usd"), Some(&dec("10.00")));
1276 assert!(measure.contains_key("eur"));
1277 }
1278
1279 #[test]
1280 fn test_measure_rule_result_value_multi_unit() {
1281 let money = test_money_type();
1282 let family_units = family_catalog_for_money(&money);
1283 let ten_eur = LiteralValue {
1284 value: ValueKind::Measure(
1285 crate::computation::rational::decimal_to_rational(Decimal::from(10)).expect("ten"),
1286 ),
1287 };
1288 let result = RuleResult::from_operation_result(
1289 dummy_evaluated_rule("total", &money),
1290 &OperationResult::from_literal(ten_eur),
1291 &money,
1292 &family_units,
1293 None,
1294 Vec::new(),
1295 );
1296 let measure = result
1297 .result
1298 .as_ref()
1299 .expect("value")
1300 .measure
1301 .clone()
1302 .expect("measure map");
1303 assert_eq!(measure.get("eur"), Some(&dec("10.00")));
1304 assert_eq!(measure.get("usd"), Some(&dec("10.99")));
1305 }
1306
1307 #[test]
1308 fn measure_rule_result_value_respects_decimals_on_unit_conversion() {
1309 let money = LemmaType::new(
1310 "money".to_string(),
1311 TypeSpecification::Measure {
1312 minimum: None,
1313 maximum: None,
1314 decimals: Some(2),
1315 units: MeasureUnits::from(vec![
1316 MeasureUnit {
1317 name: "eur".to_string(),
1318 factor: crate::computation::rational::rational_one(),
1319 derived_measure_factors: Vec::new(),
1320 decomposition: BaseMeasureVector::new(),
1321 minimum: None,
1322 maximum: None,
1323 suggestion_magnitude: None,
1324 },
1325 MeasureUnit {
1326 name: "usd".to_string(),
1327 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
1328 84, 2,
1329 ))
1330 .expect("usd factor"),
1331 derived_measure_factors: Vec::new(),
1332 decomposition: BaseMeasureVector::new(),
1333 minimum: None,
1334 maximum: None,
1335 suggestion_magnitude: None,
1336 },
1337 ]),
1338 traits: Vec::new(),
1339 decomposition: Some(BaseMeasureVector::new()),
1340 unit: None,
1341 help: String::new(),
1342 },
1343 TypeExtends::Primitive,
1344 );
1345 let family_units = family_catalog_for_money(&money);
1346 let three_twelve_eur = LiteralValue {
1347 value: ValueKind::Measure(
1348 crate::computation::rational::decimal_to_rational(Decimal::new(312, 2))
1349 .expect("3.12 eur canonical"),
1350 ),
1351 };
1352 let result = RuleResult::from_operation_result(
1353 dummy_evaluated_rule("delivery_cost", &money),
1354 &OperationResult::from_literal(three_twelve_eur),
1355 &money,
1356 &family_units,
1357 None,
1358 Vec::new(),
1359 );
1360 let measure = result
1361 .result
1362 .as_ref()
1363 .expect("value")
1364 .measure
1365 .clone()
1366 .expect("measure map");
1367 assert_eq!(measure.get("eur"), Some(&dec("3.12")));
1368 assert_eq!(measure.get("usd"), Some(&dec("3.71")));
1369 }
1370
1371 #[test]
1372 fn test_ratio_rule_result_value_multi_unit() {
1373 let ratio_type = LemmaType::new(
1374 "rate".to_string(),
1375 TypeSpecification::Ratio {
1376 minimum: None,
1377 maximum: None,
1378 decimals: None,
1379 units: RatioUnits::from(vec![
1380 RatioUnit {
1381 name: "percent".to_string(),
1382 value: crate::computation::rational::decimal_to_rational(Decimal::from(
1383 100,
1384 ))
1385 .expect("percent"),
1386 minimum: None,
1387 maximum: None,
1388 suggestion_magnitude: None,
1389 },
1390 RatioUnit {
1391 name: "basis_points".to_string(),
1392 value: crate::computation::rational::decimal_to_rational(Decimal::from(
1393 10_000,
1394 ))
1395 .expect("bp"),
1396 minimum: None,
1397 maximum: None,
1398 suggestion_magnitude: None,
1399 },
1400 ]),
1401 unit: None,
1402 help: String::new(),
1403 },
1404 TypeExtends::Primitive,
1405 );
1406 let family_units = family_catalog_for_ratio(&ratio_type);
1407 let half = crate::computation::rational::rational_new(1, 2);
1408 let lit = LiteralValue {
1409 value: ValueKind::Ratio(half),
1410 };
1411 let result = RuleResult::from_operation_result(
1412 dummy_evaluated_rule("rate_out", &ratio_type),
1413 &OperationResult::from_literal(lit),
1414 &ratio_type,
1415 &family_units,
1416 None,
1417 Vec::new(),
1418 );
1419 let ratio = result
1420 .result
1421 .as_ref()
1422 .expect("value")
1423 .ratio
1424 .clone()
1425 .expect("ratio map");
1426 assert_eq!(ratio.get("percent"), Some(&dec("50")));
1427 assert_eq!(ratio.get("basis_points"), Some(&dec("5000")));
1428 }
1429
1430 #[test]
1431 fn test_measure_rule_result_value_cross_spec_import() {
1432 use crate::parsing::source::SourceType;
1433 use crate::Engine;
1434
1435 let mut engine = Engine::new();
1436 engine
1437 .load([(
1438 SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("t.lemma"))),
1439 r#"
1440spec consumer 2025-01-01
1441uses d: dep 2025-10-01
1442rule out: d.doubled
1443
1444spec dep 2025-01-01
1445uses c: child 2025-06-01
1446data money: c.money
1447data p: 5 usd
1448rule doubled: p * 2
1449
1450spec child 2025-01-01
1451data money: measure
1452 -> unit eur: 1.00
1453 -> decimals 2
1454
1455spec child 2025-06-01
1456data money: measure
1457 -> unit eur: 1.00
1458 -> unit usd: 0.91
1459 -> decimals 2
1460"#
1461 .to_string(),
1462 )])
1463 .expect("load");
1464 let effective = crate::literals::DateTimeValue {
1465 year: 2025,
1466 month: 3,
1467 day: 1,
1468 hour: 0,
1469 minute: 0,
1470 second: 0,
1471 microsecond: 0,
1472 timezone: None,
1473
1474 granularity: DateGranularity::Full,
1475 };
1476 let response = engine
1477 .evaluate(
1478 None,
1479 "consumer",
1480 Some(&effective),
1481 IndexMap::new(),
1482 None,
1483 false,
1484 )
1485 .expect("evaluate");
1486 let out = response.results.get("out").expect("out rule");
1487 assert!(!out.vetoed);
1488 let measure = out
1489 .result
1490 .as_ref()
1491 .expect("value")
1492 .measure
1493 .as_ref()
1494 .expect("measure map");
1495 assert!(measure.contains_key("usd"));
1496 assert!(measure.contains_key("eur"));
1497 }
1498
1499 #[test]
1500 fn to_literal_roundtrips_number() {
1501 let literal = LiteralValue::number_from_decimal(Decimal::from(42));
1502 let rule_result = RuleResult::from_operation_result(
1503 dummy_evaluated_rule("answer", primitive_number_arc().as_ref()),
1504 &OperationResult::from_literal(literal.clone()),
1505 primitive_number_arc().as_ref(),
1506 &empty_family_catalog(),
1507 None,
1508 Vec::new(),
1509 );
1510 assert_eq!(rule_result.to_literal(), literal);
1511 }
1512
1513 #[test]
1514 fn to_literal_roundtrips_measure() {
1515 let money = test_money_type();
1516 let family_units = family_catalog_for_money(&money);
1517 let literal = LiteralValue::measure(crate::computation::rational::rational_new(60, 1));
1518 let rule_result = RuleResult::from_operation_result(
1519 dummy_evaluated_rule("pay", &money),
1520 &OperationResult::from_literal(literal.clone()),
1521 &money,
1522 &family_units,
1523 None,
1524 Vec::new(),
1525 );
1526 assert_eq!(rule_result.to_literal(), literal);
1527 }
1528}