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!(result.veto_reason.as_deref(), Some("out of memory"));
1182 assert_ne!(
1183 result.veto_reason.as_deref(),
1184 Some("Calculated result exceeds decimal value limit")
1185 );
1186 }
1187
1188 #[test]
1189 fn rule_result_value_decimal_limit_uses_commit_message() {
1190 let result = vetoed_rule_result_for_rule_result_value_failure(
1191 dummy_evaluated_rule("rule", primitive_number_arc().as_ref()),
1192 primitive_number_arc().as_ref(),
1193 None,
1194 RuleResultValueFailure::DecimalLimit,
1195 Vec::new(),
1196 );
1197 assert_eq!(
1198 result.veto_reason.as_deref(),
1199 Some("Calculated result exceeds decimal value limit")
1200 );
1201 }
1202
1203 fn test_money_type() -> LemmaType {
1204 LemmaType::new(
1205 "money".to_string(),
1206 TypeSpecification::Measure {
1207 minimum: None,
1208 maximum: None,
1209 decimals: Some(2),
1210 units: MeasureUnits::from(vec![
1211 MeasureUnit {
1212 name: "eur".to_string(),
1213 factor: crate::computation::rational::rational_one(),
1214 derived_measure_factors: Vec::new(),
1215 decomposition: BaseMeasureVector::new(),
1216 minimum: None,
1217 maximum: None,
1218 suggestion_magnitude: None,
1219 },
1220 MeasureUnit {
1221 name: "usd".to_string(),
1222 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
1223 91, 2,
1224 ))
1225 .expect("factor"),
1226 derived_measure_factors: Vec::new(),
1227 decomposition: BaseMeasureVector::new(),
1228 minimum: None,
1229 maximum: None,
1230 suggestion_magnitude: None,
1231 },
1232 ]),
1233 traits: Vec::new(),
1234 decomposition: Some(BaseMeasureVector::new()),
1235 unit: None,
1236 help: String::new(),
1237 },
1238 TypeExtends::Primitive,
1239 )
1240 }
1241
1242 #[test]
1243 fn measure_rule_result_value_uses_rule_type_when_expression_index_empty() {
1244 let money = test_money_type();
1245 let family_units = family_catalog_for_money(&money);
1246 let ten_usd = LiteralValue {
1247 value: ValueKind::Measure(
1248 crate::computation::rational::checked_mul(
1249 &crate::computation::rational::decimal_to_rational(Decimal::from(10))
1250 .expect("ten"),
1251 &crate::computation::rational::decimal_to_rational(Decimal::new(91, 2))
1252 .expect("usd factor"),
1253 )
1254 .expect("canonical usd"),
1255 ),
1256 };
1257 let result = RuleResult::from_operation_result(
1258 dummy_evaluated_rule("total", &money),
1259 &OperationResult::from_literal(ten_usd),
1260 &money,
1261 &family_units,
1262 None,
1263 Vec::new(),
1264 );
1265 let measure = result
1266 .result
1267 .as_ref()
1268 .expect("value")
1269 .measure
1270 .clone()
1271 .expect("measure map");
1272 assert_eq!(measure.get("usd"), Some(&dec("10.00")));
1273 assert!(measure.contains_key("eur"));
1274 }
1275
1276 #[test]
1277 fn test_measure_rule_result_value_multi_unit() {
1278 let money = test_money_type();
1279 let family_units = family_catalog_for_money(&money);
1280 let ten_eur = LiteralValue {
1281 value: ValueKind::Measure(
1282 crate::computation::rational::decimal_to_rational(Decimal::from(10)).expect("ten"),
1283 ),
1284 };
1285 let result = RuleResult::from_operation_result(
1286 dummy_evaluated_rule("total", &money),
1287 &OperationResult::from_literal(ten_eur),
1288 &money,
1289 &family_units,
1290 None,
1291 Vec::new(),
1292 );
1293 let measure = result
1294 .result
1295 .as_ref()
1296 .expect("value")
1297 .measure
1298 .clone()
1299 .expect("measure map");
1300 assert_eq!(measure.get("eur"), Some(&dec("10.00")));
1301 assert_eq!(measure.get("usd"), Some(&dec("10.99")));
1302 }
1303
1304 #[test]
1305 fn measure_rule_result_value_respects_decimals_on_unit_conversion() {
1306 let money = LemmaType::new(
1307 "money".to_string(),
1308 TypeSpecification::Measure {
1309 minimum: None,
1310 maximum: None,
1311 decimals: Some(2),
1312 units: MeasureUnits::from(vec![
1313 MeasureUnit {
1314 name: "eur".to_string(),
1315 factor: crate::computation::rational::rational_one(),
1316 derived_measure_factors: Vec::new(),
1317 decomposition: BaseMeasureVector::new(),
1318 minimum: None,
1319 maximum: None,
1320 suggestion_magnitude: None,
1321 },
1322 MeasureUnit {
1323 name: "usd".to_string(),
1324 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
1325 84, 2,
1326 ))
1327 .expect("usd factor"),
1328 derived_measure_factors: Vec::new(),
1329 decomposition: BaseMeasureVector::new(),
1330 minimum: None,
1331 maximum: None,
1332 suggestion_magnitude: None,
1333 },
1334 ]),
1335 traits: Vec::new(),
1336 decomposition: Some(BaseMeasureVector::new()),
1337 unit: None,
1338 help: String::new(),
1339 },
1340 TypeExtends::Primitive,
1341 );
1342 let family_units = family_catalog_for_money(&money);
1343 let three_twelve_eur = LiteralValue {
1344 value: ValueKind::Measure(
1345 crate::computation::rational::decimal_to_rational(Decimal::new(312, 2))
1346 .expect("3.12 eur canonical"),
1347 ),
1348 };
1349 let result = RuleResult::from_operation_result(
1350 dummy_evaluated_rule("delivery_cost", &money),
1351 &OperationResult::from_literal(three_twelve_eur),
1352 &money,
1353 &family_units,
1354 None,
1355 Vec::new(),
1356 );
1357 let measure = result
1358 .result
1359 .as_ref()
1360 .expect("value")
1361 .measure
1362 .clone()
1363 .expect("measure map");
1364 assert_eq!(measure.get("eur"), Some(&dec("3.12")));
1365 assert_eq!(measure.get("usd"), Some(&dec("3.71")));
1366 }
1367
1368 #[test]
1369 fn test_ratio_rule_result_value_multi_unit() {
1370 let ratio_type = LemmaType::new(
1371 "rate".to_string(),
1372 TypeSpecification::Ratio {
1373 minimum: None,
1374 maximum: None,
1375 decimals: None,
1376 units: RatioUnits::from(vec![
1377 RatioUnit {
1378 name: "percent".to_string(),
1379 value: crate::computation::rational::decimal_to_rational(Decimal::from(
1380 100,
1381 ))
1382 .expect("percent"),
1383 minimum: None,
1384 maximum: None,
1385 suggestion_magnitude: None,
1386 },
1387 RatioUnit {
1388 name: "basis_points".to_string(),
1389 value: crate::computation::rational::decimal_to_rational(Decimal::from(
1390 10_000,
1391 ))
1392 .expect("bp"),
1393 minimum: None,
1394 maximum: None,
1395 suggestion_magnitude: None,
1396 },
1397 ]),
1398 unit: None,
1399 help: String::new(),
1400 },
1401 TypeExtends::Primitive,
1402 );
1403 let family_units = family_catalog_for_ratio(&ratio_type);
1404 let half = crate::computation::rational::rational_new(1, 2);
1405 let lit = LiteralValue {
1406 value: ValueKind::Ratio(half),
1407 };
1408 let result = RuleResult::from_operation_result(
1409 dummy_evaluated_rule("rate_out", &ratio_type),
1410 &OperationResult::from_literal(lit),
1411 &ratio_type,
1412 &family_units,
1413 None,
1414 Vec::new(),
1415 );
1416 let ratio = result
1417 .result
1418 .as_ref()
1419 .expect("value")
1420 .ratio
1421 .clone()
1422 .expect("ratio map");
1423 assert_eq!(ratio.get("percent"), Some(&dec("50")));
1424 assert_eq!(ratio.get("basis_points"), Some(&dec("5000")));
1425 }
1426
1427 #[test]
1428 fn test_measure_rule_result_value_cross_spec_import() {
1429 use crate::parsing::source::SourceType;
1430 use crate::Engine;
1431
1432 let mut engine = Engine::new();
1433 engine
1434 .load([(
1435 SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("t.lemma"))),
1436 r#"
1437spec consumer 2025-01-01
1438uses d: dep 2025-10-01
1439rule out: d.doubled
1440
1441spec dep 2025-01-01
1442uses c: child 2025-06-01
1443data money: c.money
1444data p: 5 usd
1445rule doubled: p * 2
1446
1447spec child 2025-01-01
1448data money: measure
1449 -> unit eur: 1.00
1450 -> decimals 2
1451
1452spec child 2025-06-01
1453data money: measure
1454 -> unit eur: 1.00
1455 -> unit usd: 0.91
1456 -> decimals 2
1457"#
1458 .to_string(),
1459 )])
1460 .expect("load");
1461 let effective = crate::literals::DateTimeValue {
1462 year: 2025,
1463 month: 3,
1464 day: 1,
1465 hour: 0,
1466 minute: 0,
1467 second: 0,
1468 microsecond: 0,
1469 timezone: None,
1470
1471 granularity: DateGranularity::Full,
1472 };
1473 let response = engine
1474 .evaluate(
1475 None,
1476 "consumer",
1477 Some(&effective),
1478 IndexMap::new(),
1479 None,
1480 false,
1481 )
1482 .expect("evaluate");
1483 let out = response.results.get("out").expect("out rule");
1484 assert!(!out.vetoed);
1485 let measure = out
1486 .result
1487 .as_ref()
1488 .expect("value")
1489 .measure
1490 .as_ref()
1491 .expect("measure map");
1492 assert!(measure.contains_key("usd"));
1493 assert!(measure.contains_key("eur"));
1494 }
1495
1496 #[test]
1497 fn to_literal_roundtrips_number() {
1498 let literal = LiteralValue::number_from_decimal(Decimal::from(42));
1499 let rule_result = RuleResult::from_operation_result(
1500 dummy_evaluated_rule("answer", primitive_number_arc().as_ref()),
1501 &OperationResult::from_literal(literal.clone()),
1502 primitive_number_arc().as_ref(),
1503 &empty_family_catalog(),
1504 None,
1505 Vec::new(),
1506 );
1507 assert_eq!(rule_result.to_literal(), literal);
1508 }
1509
1510 #[test]
1511 fn to_literal_roundtrips_measure() {
1512 let money = test_money_type();
1513 let family_units = family_catalog_for_money(&money);
1514 let literal = LiteralValue::measure(crate::computation::rational::rational_new(60, 1));
1515 let rule_result = RuleResult::from_operation_result(
1516 dummy_evaluated_rule("pay", &money),
1517 &OperationResult::from_literal(literal.clone()),
1518 &money,
1519 &family_units,
1520 None,
1521 Vec::new(),
1522 );
1523 assert_eq!(rule_result.to_literal(), literal);
1524 }
1525}