Skip to main content

lemma/evaluation/
response.rs

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