Skip to main content

lemma/evaluation/
response.rs

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/// Rule info with resolved expressions for use in evaluation response.
20/// Evaluation uses only semantics types; no parsing types.
21#[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/// Response from evaluating a Lemma spec
30#[derive(Debug, Clone)]
31pub struct Response {
32    pub spec_name: String,
33    pub effective: String,
34    /// Declared temporal window `[spec_effective_from, spec_effective_to)` of the
35    /// resolved spec version. Set by [`crate::Engine::evaluate`] after evaluation;
36    /// `None` here (evaluation-internal construction) until that assignment.
37    pub spec_effective_from: Option<DateTimeValue>,
38    pub spec_effective_to: Option<DateTimeValue>,
39    pub results: IndexMap<String, RuleResult>,
40    /// Rows reached by `results`, indexed by [`RowReference::index`].
41    pub rows: Vec<RowResult>,
42}
43
44/// Result of evaluating a single rule. Struct fields match the API JSON shape.
45#[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    /// Flattened value fields, including `result` when the rule is not vetoed.
55    pub result: Option<RuleResultValue>,
56    pub explanation: Option<Explanation>,
57    /// Unbound caller data paths still live for this rule under the current run data
58    /// (`DataPath::input_key` strings; subset of `Show.data` keys with non-empty
59    /// `needed_by_rules`). Ordered by evaluation / decision-tree preorder (first key =
60    /// next fact the live tree needs); `Show.data` stays declaration order.
61    missing_data: Vec<String>,
62}
63
64impl RuleResult {
65    /// Engine-rendered one-liner from the flattened [`RuleResultValue`].
66    #[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    /// True when this result is a [`VetoType::MissingData`] or [`VetoType::MissingRowData`] veto.
74    ///
75    /// Value and other vetoes are settled answers; leftover live keys in
76    /// [`Self::missing_data`] must not drive prompts or human "Missing data" display.
77    #[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    /// Unbound caller data paths still live for this rule (`DataPath::input_key`),
86    /// in evaluation / decision-tree order (contrast `Show.data` declaration order).
87    #[must_use]
88    pub fn missing_data(&self) -> &[String] {
89        &self.missing_data
90    }
91
92    /// Build a [`RuleResult`] for API output from a rule evaluation result.
93    ///
94    /// Measure and ratio payloads expand into every unit in the result type's family.
95    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    /// Reconstruct the evaluated [`LiteralValue`] from committed [`RuleResultValue`] fields.
179    ///
180    /// Panics if the rule is vetoed or fields cannot be reconstructed.
181    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    /// Looks up a rule result by name.
213    ///
214    /// Returns an error if the rule is not found.
215    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/// Distinct rows reached by one response, in first-reached order. Each row is filled
227/// once and referenced by index everywhere else, so output grows with the number of
228/// rows, not with the number of paths that reach them.
229#[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
245/// Fill every spec value inside `literal`'s API `value`: a fixed instance gets its data
246/// and rules inline, a list row a reference into `table`.
247pub(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
279/// Shared state of one fill walk. `base` is the caller's context; a row being walked
280/// runs in its own context on `row_contexts`, innermost last.
281struct 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
401/// Fills one value: a fixed instance gets its data and rules inline, a list row a
402/// reference into the table, a list fills each entry in order.
403struct 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
515/// Data and rules of one spec instance: its ancestors entry is held for the walk of its
516/// members.
517struct 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
569/// Reference to `row` in the table, filling the row through its template plan when
570/// first reached. The row's members walk in their own context and ancestor scope.
571struct 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
643/// An entry whose value is being filled, with the key it is inserted under once filled.
644enum Awaiting {
645    Data(String, SpecRuleResult),
646    Rule(RulePath, SpecRuleResult),
647}
648
649/// Data then rules of `plan` under `prefix`, keyed relative to it. For a row, `plan` is
650/// its template and missing data is reported under the row's number.
651struct 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            // Named-`uses` fields live on templates; enumerate via nested_input_keys.
677            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        // Override committed decimal number field to match serialization path under test
1064        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    /// Attach hole: MissingData + empty missing_data is a BUG, not a silent stall.
1125    #[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}