Skip to main content

lemma/
result_value.rs

1//! API value for a rule result, `ShowData.value`, or `ShowData.suggestion`.
2//!
3//! This is the single API-facing value representation shared by all three sites, expanded
4//! into every declared unit for measure/ratio on data inputs, and into every unit in the
5//! result type's family for rule results. It sits at the crate root (not under
6//! `evaluation/`) because `planning::execution_plan::ShowData` needs it too, and planning
7//! must not import evaluation. The plan/eval-internal representation is the canonical
8//! `planning::semantics::LiteralValue`; this module is the boundary between the two.
9//!
10//! Magnitudes are [`Decimal`] (28-scale API boundary). JSON still emits decimal strings via
11//! `Decimal`'s default serde.
12
13use crate::computation::rational::{
14    checked_div, checked_mul, decimal_to_display_str, NumericFailure, RationalInteger,
15};
16use crate::literals::rational_from_parsed_decimal;
17use crate::planning::semantics::{
18    format_decimal_for_api, range_element_type_specification, ratio_element_type_for_api,
19    semantic_calendar_unit_from_measure_type, LemmaType, LiteralUnitMapFailure, LiteralValue,
20    SemanticDateTime, SemanticTime, SpecInstance, SpecValue, TypeSpecification, TypedLiteral,
21    UnitFactorSource, ValueKind,
22};
23use crate::planning::unit_family::{declared_bare_names_only, FamilyUnitCatalog, FamilyUnitEntry};
24use indexmap::IndexMap;
25use rust_decimal::Decimal;
26use serde::{Deserialize, Serialize};
27use std::borrow::Cow;
28use std::collections::{BTreeMap, HashMap};
29use std::fmt;
30use std::sync::Arc;
31
32/// Calendar value (a measure whose unit is a calendar unit, e.g. `3 months`) on a result.
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub struct CalendarResult {
35    pub value: Decimal,
36    pub unit: String,
37}
38
39/// Both endpoints of a range result.
40///
41/// Each endpoint is itself a [`RuleResultValue`], but an endpoint's own `range` field is
42/// always `None` — a range endpoint must never itself be a range. Building a
43/// [`RuleResultValue`] and reconstructing a literal both panic if this invariant is
44/// violated.
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
46pub struct RangeResult {
47    pub from: RuleResultValue,
48    pub to: RuleResultValue,
49}
50
51/// API value shared by flattened [`crate::evaluation::response::RuleResult`],
52/// `ShowData.value` / `suggestion`, and explanation `Data` / `Rule` nodes.
53///
54/// When present: `result` (from [`LiteralValue::display_value`]), plus exactly
55/// one typed field for a non-range value; `range` is set instead for a range value, and
56/// every other typed field stays `None`. A list row carries `row` in rule results and
57/// in [`RowResult`] members; in explanation nodes it carries `result` only. The one
58/// value without `result` is an instance list in Show: the list carries `list` only and
59/// each source row `fields` only, in row order.
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
61pub struct RuleResultValue {
62    /// Engine-rendered string for UI (`LiteralValue::display_value`). Present whenever
63    /// this value is present, including range endpoints.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub result: Option<String>,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub measure: Option<BTreeMap<String, Decimal>>,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub ratio: Option<BTreeMap<String, Decimal>>,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub number: Option<Decimal>,
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub boolean: Option<bool>,
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub text: Option<String>,
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub date: Option<SemanticDateTime>,
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub time: Option<SemanticTime>,
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub calendar: Option<CalendarResult>,
82    /// Unit `result` is expressed in, for a measure or ratio value written in a unit
83    /// (including range endpoints). A written dynamic unit without a factor in the
84    /// presenting spec is reported as the unit used instead.
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub unit: Option<String>,
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub range: Option<Box<RangeResult>>,
89    /// Set when the value is a spec instance. `data` holds one evaluated entry per
90    /// data slot of that instance, keyed relative to that spec.
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub spec: Option<Box<SpecResult>>,
93    /// Set when the value is an instance-list row or a `uses` instance projected from one.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub row: Option<RowReference>,
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub list: Option<Vec<RuleResultValue>>,
98    /// Set on a source row of an instance list in Show: the fields the row sets,
99    /// keyed relative to the row spec, in the order the row writes them.
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub fields: Option<IndexMap<String, RuleResultValue>>,
102    /// Show only: on a source-row field whose value is a reference; the input key
103    /// of the referenced data in the spec declaring the list.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub reference: Option<String>,
106}
107
108/// A spec instance returned by a rule, including the values needed to run it again.
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110pub struct SpecResult {
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub repository: Option<String>,
113    pub spec: String,
114    /// Resolved row's effective date. Empty when the row is the origin row.
115    pub effective: String,
116    /// `uses` path of this instance in the consumer (`basic`, `basic.finance`).
117    pub instance: String,
118    /// One segment per `uses` hop of `instance`.
119    pub path: Vec<crate::api::PathSegment>,
120    /// Evaluated data slots, keyed relative to `spec`, declaration order. A slot
121    /// with a value carries that value; a vetoed slot carries `vetoed` and
122    /// `veto_reason`. Unbound slots are omitted.
123    pub data: IndexMap<String, SpecRuleResult>,
124    /// Rule results relative to `spec` (`tax`, `finance.fee`), plan rule order.
125    pub rules: IndexMap<String, SpecRuleResult>,
126}
127
128/// An instance-list row or a `uses` instance projected from one.
129/// Its data and rules are `Response.rows[index]`.
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131pub struct RowReference {
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub repository: Option<String>,
134    pub spec: String,
135    /// Resolved row's effective date. Empty when the row is the origin row.
136    pub effective: String,
137    /// Row path relative to the spec holding this value (`lines.2`, `lines.2.item`).
138    /// Present for a row with a data path (evaluation-data rows). An appended row
139    /// (`-> with`) has no path; its identity is its place in the list, and a value
140    /// bound inside it carries only its row-relative path (`item`).
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub instance: Option<String>,
143    pub index: usize,
144}
145
146/// Data and rules of one distinct row reached by a response, filled once.
147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
148pub struct RowResult {
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub repository: Option<String>,
151    pub spec: String,
152    /// Resolved row's effective date. Empty when the row is the origin row.
153    pub effective: String,
154    /// Evaluated data slots, keyed relative to `spec`, declaration order.
155    /// Unbound slots are omitted.
156    pub data: IndexMap<String, SpecRuleResult>,
157    /// Rule results relative to `spec`, plan rule order.
158    pub rules: IndexMap<String, SpecRuleResult>,
159}
160
161/// One rule of a [`SpecResult`], or one evaluated data slot of a [`SpecResult`].
162/// A spec-valued rule nests another [`SpecResult`]
163/// in [`RuleResultValue::spec`].
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
165pub struct SpecRuleResult {
166    pub vetoed: bool,
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub veto_reason: Option<String>,
169    pub rule_type: String,
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub result: Option<RuleResultValue>,
172}
173
174/// Why building a [`RuleResultValue`] from a canonical [`LiteralValue`] failed.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum RuleResultValueFailure {
177    DecimalLimit,
178    NumericOverflow,
179    OutOfMemory,
180}
181
182/// Which units to expand when building a [`RuleResultValue`] from a measure or ratio literal.
183pub(crate) enum UnitExpansion<'a> {
184    /// Show data fill/suggestion: units declared on `lemma_type` only.
185    Declared,
186    /// Rule results: precomputed family entry from the plan catalog.
187    Family(&'a FamilyUnitEntry),
188}
189
190/// Human-readable veto message for a [`RuleResultValueFailure`].
191pub fn rule_result_value_failure_message(failure: RuleResultValueFailure) -> &'static str {
192    match failure {
193        RuleResultValueFailure::DecimalLimit => {
194            "The calculated result exceeds the decimal value limit."
195        }
196        RuleResultValueFailure::NumericOverflow => "The numeric result overflowed.",
197        RuleResultValueFailure::OutOfMemory => "The calculation ran out of memory.",
198    }
199}
200
201fn map_numeric_to_rule_result_value_failure(failure: NumericFailure) -> RuleResultValueFailure {
202    match failure {
203        NumericFailure::Overflow => RuleResultValueFailure::DecimalLimit,
204        NumericFailure::OutOfMemory => RuleResultValueFailure::OutOfMemory,
205        NumericFailure::DivisionByZero => {
206            panic!(
207                "BUG: decimal commit encountered division by zero while building RuleResultValue"
208            )
209        }
210        NumericFailure::Irrational => {
211            panic!(
212                "BUG: decimal commit encountered irrational result while building RuleResultValue"
213            )
214        }
215        NumericFailure::MagnitudeLimit => {
216            panic!("BUG: decimal commit does not apply the arithmetic magnitude limit")
217        }
218    }
219}
220
221fn map_unit_conversion_failure(failure: NumericFailure) -> RuleResultValueFailure {
222    match failure {
223        NumericFailure::Overflow => RuleResultValueFailure::NumericOverflow,
224        NumericFailure::OutOfMemory => RuleResultValueFailure::OutOfMemory,
225        NumericFailure::DivisionByZero => {
226            panic!("BUG: unit conversion division by zero while building RuleResultValue")
227        }
228        NumericFailure::Irrational => {
229            panic!("BUG: unit conversion irrational while building RuleResultValue")
230        }
231        NumericFailure::MagnitudeLimit => {
232            panic!("BUG: unit conversion does not apply the arithmetic magnitude limit")
233        }
234    }
235}
236
237fn map_literal_unit_map_failure(failure: LiteralUnitMapFailure) -> RuleResultValueFailure {
238    match failure {
239        LiteralUnitMapFailure::Commit(failure) => map_numeric_to_rule_result_value_failure(failure),
240        LiteralUnitMapFailure::UnitConversion(failure) => map_unit_conversion_failure(failure),
241    }
242}
243
244fn expansion_for_type<'a>(
245    rule_type: &LemmaType,
246    catalog: Option<&'a FamilyUnitCatalog>,
247) -> UnitExpansion<'a> {
248    let Some(catalog) = catalog else {
249        return UnitExpansion::Declared;
250    };
251    match catalog.entry_for_type(rule_type) {
252        Some(entry) => UnitExpansion::Family(entry),
253        None => UnitExpansion::Declared,
254    }
255}
256
257fn unit_names_for_expansion(
258    result_type: &LemmaType,
259    expansion: &UnitExpansion<'_>,
260    catalog: Option<&FamilyUnitCatalog>,
261) -> Arc<[String]> {
262    match expansion {
263        UnitExpansion::Declared => Arc::from(declared_bare_names_only(result_type)),
264        UnitExpansion::Family(_) => catalog
265            .expect("BUG: family expansion requires FamilyUnitCatalog")
266            .ordered_bare_names_for_type(result_type),
267    }
268}
269
270fn measure_factor_source<'a>(
271    result_type: &'a LemmaType,
272    expansion: &'a UnitExpansion<'a>,
273) -> UnitFactorSource<'a> {
274    match expansion {
275        UnitExpansion::Declared => UnitFactorSource::DeclaredOn(result_type),
276        UnitExpansion::Family(entry) => UnitFactorSource::Merged {
277            measure: entry.merged_measure_units.as_ref(),
278            ratio: entry.merged_ratio_units.as_ref(),
279        },
280    }
281}
282
283fn ratio_factor_source<'a>(
284    result_type: &'a LemmaType,
285    expansion: &'a UnitExpansion<'a>,
286) -> UnitFactorSource<'a> {
287    measure_factor_source(result_type, expansion)
288}
289
290/// Type of a node in a value tree walked with an explicit stack: the root type is borrowed,
291/// a list row's type is shared with the row's [`TypedLiteral`].
292enum NodeType<'a> {
293    Borrowed(&'a LemmaType),
294    Shared(Arc<LemmaType>),
295}
296
297impl NodeType<'_> {
298    fn get(&self) -> &LemmaType {
299        match self {
300            Self::Borrowed(lemma_type) => lemma_type,
301            Self::Shared(lemma_type) => lemma_type.as_ref(),
302        }
303    }
304}
305
306enum ResultValueTask<'a> {
307    Visit {
308        literal: Cow<'a, LiteralValue>,
309        lemma_type: NodeType<'a>,
310    },
311    AssembleList {
312        display: String,
313        rows: usize,
314    },
315}
316
317fn result_value_from_literal(
318    literal: &LiteralValue,
319    lemma_type: &LemmaType,
320    expansion: &UnitExpansion<'_>,
321    catalog: Option<&FamilyUnitCatalog>,
322    runtime_scales: Option<&HashMap<String, RationalInteger>>,
323) -> Result<RuleResultValue, RuleResultValueFailure> {
324    let mut tasks = vec![ResultValueTask::Visit {
325        literal: Cow::Borrowed(literal),
326        lemma_type: NodeType::Borrowed(lemma_type),
327    }];
328    let mut converted: Vec<RuleResultValue> = Vec::new();
329    while let Some(task) = tasks.pop() {
330        match task {
331            ResultValueTask::Visit {
332                literal,
333                lemma_type,
334            } => {
335                let lemma_type = lemma_type.get();
336                match &literal.value {
337                    ValueKind::Range(from, to) => {
338                        let from_value = result_value_from_range_endpoint(
339                            &from.to_literal(),
340                            from.lemma_type.as_ref(),
341                            expansion,
342                            catalog,
343                            runtime_scales,
344                        )?;
345                        let to_value = result_value_from_range_endpoint(
346                            &to.to_literal(),
347                            to.lemma_type.as_ref(),
348                            expansion,
349                            catalog,
350                            runtime_scales,
351                        )?;
352                        converted.push(RuleResultValue {
353                            result: Some(literal.display_value_with_type(lemma_type)),
354                            range: Some(Box::new(RangeResult {
355                                from: from_value,
356                                to: to_value,
357                            })),
358                            ..RuleResultValue::default()
359                        });
360                    }
361                    ValueKind::List(rows) => {
362                        tasks.push(ResultValueTask::AssembleList {
363                            display: literal.display_value_with_type(lemma_type),
364                            rows: rows.len(),
365                        });
366                        for row in rows.iter().rev() {
367                            tasks.push(ResultValueTask::Visit {
368                                literal: Cow::Owned(row.to_literal()),
369                                lemma_type: NodeType::Shared(Arc::clone(&row.lemma_type)),
370                            });
371                        }
372                    }
373                    _ => converted.push(result_value_from_non_range_literal(
374                        &literal,
375                        lemma_type,
376                        expansion,
377                        catalog,
378                        runtime_scales,
379                    )?),
380                }
381            }
382            ResultValueTask::AssembleList { display, rows } => {
383                let list = converted.split_off(converted.len() - rows);
384                converted.push(RuleResultValue {
385                    result: Some(display),
386                    list: Some(list),
387                    ..RuleResultValue::default()
388                });
389            }
390        }
391    }
392    Ok(converted
393        .pop()
394        .expect("BUG: literal conversion yields exactly one RuleResultValue"))
395}
396
397/// Build a [`RuleResultValue`] from a canonical [`LiteralValue`] for a rule result.
398///
399/// Measure and ratio expand into every unit in the result type's family.
400pub(crate) fn rule_result_value_from_literal(
401    literal: &LiteralValue,
402    rule_type: &LemmaType,
403    catalog: &FamilyUnitCatalog,
404    runtime_scales: Option<&HashMap<String, RationalInteger>>,
405) -> Result<RuleResultValue, RuleResultValueFailure> {
406    let expansion = expansion_for_type(rule_type, Some(catalog));
407    result_value_from_literal(
408        literal,
409        rule_type,
410        &expansion,
411        Some(catalog),
412        runtime_scales,
413    )
414}
415
416/// Build a [`RuleResultValue`] using only units declared on `lemma_type` (Show data fill/suggestion).
417pub fn type_scoped_result_value_from_literal(
418    literal: &LiteralValue,
419    lemma_type: &LemmaType,
420) -> Result<RuleResultValue, RuleResultValueFailure> {
421    let expansion = UnitExpansion::Declared;
422    result_value_from_literal(literal, lemma_type, &expansion, None, None)
423}
424
425fn result_value_from_range_endpoint(
426    endpoint: &LiteralValue,
427    endpoint_type: &LemmaType,
428    expansion: &UnitExpansion<'_>,
429    catalog: Option<&FamilyUnitCatalog>,
430    runtime_scales: Option<&HashMap<String, RationalInteger>>,
431) -> Result<RuleResultValue, RuleResultValueFailure> {
432    if matches!(&endpoint.value, ValueKind::Range(_, _)) {
433        panic!("BUG: range endpoint must not itself be a range");
434    }
435    result_value_from_non_range_literal(endpoint, endpoint_type, expansion, catalog, runtime_scales)
436}
437
438fn result_value_from_non_range_literal(
439    literal: &LiteralValue,
440    result_type: &LemmaType,
441    expansion: &UnitExpansion<'_>,
442    catalog: Option<&FamilyUnitCatalog>,
443    runtime_scales: Option<&HashMap<String, RationalInteger>>,
444) -> Result<RuleResultValue, RuleResultValueFailure> {
445    match &literal.value {
446        ValueKind::Measure(rational) if result_type.is_calendar_like() => {
447            let unit = semantic_calendar_unit_from_measure_type(result_type);
448            let value = result_type
449                .try_rational_as_decimal(rational)
450                .map_err(map_numeric_to_rule_result_value_failure)?;
451            let result = Some(literal.display_value_with_type(result_type));
452            Ok(RuleResultValue {
453                result,
454                calendar: Some(CalendarResult {
455                    value,
456                    unit: unit.to_string(),
457                }),
458                ..RuleResultValue::default()
459            })
460        }
461        ValueKind::UnscaledMeasure { magnitude, unit } => {
462            let decimal = magnitude
463                .try_to_decimal()
464                .map_err(map_numeric_to_rule_result_value_failure)?;
465            let api_decimal = format_decimal_for_api(decimal);
466            let display = format!("{} {unit}", decimal_to_display_str(&decimal));
467            let mut measure = BTreeMap::new();
468            measure.insert(unit.to_string(), api_decimal);
469            Ok(RuleResultValue {
470                result: Some(display),
471                measure: Some(measure),
472                unit: Some(unit.to_string()),
473                ..RuleResultValue::default()
474            })
475        }
476        ValueKind::Measure(_) => {
477            let unit_names = unit_names_for_expansion(result_type, expansion, catalog);
478            let unit_name_refs: Vec<&str> = unit_names.iter().map(String::as_str).collect();
479            let fallback = measure_factor_source(result_type, expansion);
480            let source = match runtime_scales {
481                Some(scales) => UnitFactorSource::Runtime {
482                    scales,
483                    fallback: &fallback,
484                },
485                None => fallback,
486            };
487            let measure = result_type
488                .measure_literal_unit_map(literal, &unit_name_refs, source)
489                .map_err(map_literal_unit_map_failure)?;
490            let result = Some(literal.display_value_with_scales(result_type, runtime_scales));
491            // Wire `unit` is the unit the result string actually used, not a binding
492            // whose factor was dropped from runtime_scales.
493            let unit = result_type.unit().and_then(|_| {
494                result_type
495                    .measure_display_unit_name(runtime_scales)
496                    .map(str::to_string)
497            });
498            Ok(RuleResultValue {
499                result,
500                measure: Some(measure),
501                unit,
502                ..RuleResultValue::default()
503            })
504        }
505        ValueKind::Ratio(_) => {
506            let result = Some(literal.display_value_with_type(result_type));
507            let unit_names = unit_names_for_expansion(result_type, expansion, catalog);
508            let unit_name_refs: Vec<&str> = unit_names.iter().map(String::as_str).collect();
509            Ok(RuleResultValue {
510                result,
511                ratio: Some(
512                    result_type
513                        .ratio_literal_unit_map(
514                            literal,
515                            &unit_name_refs,
516                            ratio_factor_source(result_type, expansion),
517                        )
518                        .map_err(map_literal_unit_map_failure)?,
519                ),
520                unit: result_type.unit().map(str::to_string),
521                ..RuleResultValue::default()
522            })
523        }
524        other => scalar_result_value(result_type, other),
525    }
526}
527
528fn scalar_result_value(
529    result_type: &LemmaType,
530    value: &ValueKind,
531) -> Result<RuleResultValue, RuleResultValueFailure> {
532    match value {
533        ValueKind::Number(rational) => {
534            let decimal = rational
535                .try_to_decimal()
536                .map_err(map_numeric_to_rule_result_value_failure)?;
537            let api_decimal = format_decimal_for_api(decimal);
538            let display_string = decimal_to_display_str(&decimal);
539            Ok(RuleResultValue {
540                result: Some(display_string),
541                number: Some(api_decimal),
542                ..RuleResultValue::default()
543            })
544        }
545        ValueKind::Boolean(b) => {
546            let result = Some(value.to_string());
547            Ok(RuleResultValue {
548                result,
549                boolean: Some(*b),
550                ..RuleResultValue::default()
551            })
552        }
553        ValueKind::Text(_) | ValueKind::Date(_) | ValueKind::Time(_) => {
554            let result = Some(value.to_string());
555            Ok(scalar_result_value_non_numeric(result, value))
556        }
557        ValueKind::Measure(_) | ValueKind::Ratio(_) | ValueKind::UnscaledMeasure { .. } => {
558            unreachable!("BUG: measure, ratio and unscaled measure must be handled by caller")
559        }
560        ValueKind::Range(_, _) => {
561            unreachable!("BUG: range must be handled by result_value_from_literal")
562        }
563        ValueKind::List(_) => {
564            unreachable!("BUG: list must be handled by result_value_from_literal")
565        }
566        ValueKind::Spec(SpecValue::Row(key)) => match key.segments() {
567            Some(path) => Ok(RuleResultValue {
568                result: Some(key.to_string()),
569                spec: Some(Box::new(SpecResult {
570                    repository: key.row.identity.repository.clone(),
571                    spec: key.row.identity.spec.clone(),
572                    effective: key.row.identity.effective.to_string(),
573                    instance: key.to_string(),
574                    path: path.iter().map(crate::api::PathSegment::from).collect(),
575                    data: IndexMap::new(),
576                    rules: IndexMap::new(),
577                })),
578                ..RuleResultValue::default()
579            }),
580            None => Ok(RuleResultValue {
581                result: Some(value.to_string()),
582                ..RuleResultValue::default()
583            }),
584        },
585        ValueKind::Spec(SpecValue::Fixed(instance)) => {
586            let identity = result_type
587                .spec_identity()
588                .expect("BUG: spec value whose type is not a spec");
589            Ok(RuleResultValue {
590                result: Some(instance.to_string()),
591                spec: Some(Box::new(SpecResult {
592                    repository: identity.repository.clone(),
593                    spec: identity.spec.clone(),
594                    effective: identity.effective.to_string(),
595                    instance: instance.to_string(),
596                    path: instance
597                        .prefix
598                        .iter()
599                        .map(crate::api::PathSegment::from)
600                        .collect(),
601                    data: IndexMap::new(),
602                    rules: IndexMap::new(),
603                })),
604                ..RuleResultValue::default()
605            })
606        }
607    }
608}
609
610fn scalar_result_value_non_numeric(result: Option<String>, value: &ValueKind) -> RuleResultValue {
611    match value {
612        ValueKind::Text(s) => RuleResultValue {
613            result,
614            text: Some(s.to_string()),
615            ..RuleResultValue::default()
616        },
617        ValueKind::Date(d) => RuleResultValue {
618            result,
619            date: Some(d.clone()),
620            ..RuleResultValue::default()
621        },
622        ValueKind::Time(t) => RuleResultValue {
623            result,
624            time: Some(t.clone()),
625            ..RuleResultValue::default()
626        },
627        _ => unreachable!("BUG: scalar_result_value_non_numeric called with non-scalar type"),
628    }
629}
630
631fn literal_from_measure_map(
632    measure: &BTreeMap<String, Decimal>,
633    rule_type: &LemmaType,
634) -> LiteralValue {
635    let unit = rule_type
636        .unit()
637        .and_then(|name| rule_type.measure_units_table()?.get(name).ok())
638        .or_else(|| rule_type.measure_fallback_unit())
639        .expect(
640            "BUG: reconstructing a measure from a unit map requires a declared unit on the type",
641        );
642    let magnitude = measure
643        .get(&unit.name)
644        .unwrap_or_else(|| panic!("BUG: measure map missing unit '{}'", unit.name));
645    let rational = rational_from_parsed_decimal(*magnitude)
646        .expect("BUG: measure rule result value must lift to rational");
647    match unit.literal_factor() {
648        Some(factor) => {
649            let canonical = checked_mul(&rational, factor).unwrap_or_else(|failure| {
650                panic!(
651                    "BUG: measure canonicalization from RuleResultValue fields failed: {failure}"
652                )
653            });
654            LiteralValue::measure(canonical)
655        }
656        None => LiteralValue::new(ValueKind::UnscaledMeasure {
657            magnitude: rational,
658            unit: Arc::from(unit.name.as_str()),
659        }),
660    }
661}
662
663fn literal_from_ratio_map(
664    ratio: &BTreeMap<String, Decimal>,
665    rule_type: &LemmaType,
666) -> LiteralValue {
667    let ratio_type = ratio_element_type_for_api(rule_type);
668    let units = match &ratio_type.specifications {
669        TypeSpecification::Ratio { units, .. } => units,
670        _ => panic!(
671            "BUG: ratio rule result type must be Ratio, got {}",
672            rule_type.name()
673        ),
674    };
675    let unit = rule_type
676        .unit()
677        .and_then(|name| units.get(name).ok())
678        .or_else(|| units.iter().next())
679        .expect("BUG: ratio rule result type must declare at least one unit");
680    let magnitude = ratio
681        .get(&unit.name)
682        .unwrap_or_else(|| panic!("BUG: ratio map missing unit '{}'", unit.name));
683    let display_rational = rational_from_parsed_decimal(*magnitude)
684        .expect("BUG: ratio rule result value must lift to rational");
685    let canonical = checked_div(&display_rational, &unit.value).unwrap_or_else(|failure| {
686        panic!("BUG: ratio canonicalization from RuleResultValue fields failed: {failure}")
687    });
688    LiteralValue::ratio(canonical)
689}
690
691impl RuleResultValue {
692    /// Reconstruct the [`LiteralValue`] from this API value's fields.
693    ///
694    /// Panics if the fields cannot reconstruct a literal, or if a range endpoint is
695    /// itself a range (ranges do not nest; enforced here, not by convention).
696    /// A list row is not rebuilt: its value is its bindings, which the API value does not
697    /// carry. Read a row through its fields instead.
698    pub fn to_literal(&self, rule_type: &LemmaType) -> LiteralValue {
699        if let Some(row) = &self.row {
700            panic!(
701                "BUG: list row '{}' cannot be rebuilt from its API value",
702                row.instance.as_deref().unwrap_or("<appended>")
703            );
704        }
705        if let Some(spec) = &self.spec {
706            assert!(
707                !spec.path.is_empty(),
708                "BUG: spec result '{}' has no uses hop",
709                spec.instance
710            );
711            return LiteralValue::new(ValueKind::Spec(SpecValue::Fixed(SpecInstance {
712                prefix: spec
713                    .path
714                    .iter()
715                    .map(|segment| crate::planning::semantics::PathSegment {
716                        uses: Arc::from(segment.uses.as_str()),
717                        row: segment.row,
718                        repository: segment.repository.as_deref().map(Arc::from),
719                        spec: Arc::from(segment.spec.as_str()),
720                    })
721                    .collect(),
722            })));
723        }
724        if let Some(rows) = &self.list {
725            let TypeSpecification::List { element, .. } = &rule_type.specifications else {
726                panic!(
727                    "BUG: list result whose rule type is not a list ({})",
728                    rule_type.name()
729                );
730            };
731            return LiteralValue::new(ValueKind::List(
732                rows.iter()
733                    .map(|row| {
734                        let row_type =
735                            endpoint_type_with_written_unit(element, row.unit.as_deref());
736                        TypedLiteral {
737                            value: row.to_literal(&row_type).value,
738                            lemma_type: Arc::new(row_type),
739                        }
740                    })
741                    .collect(),
742            ));
743        }
744        if let Some(range) = &self.range {
745            if range.from.range.is_some() || range.to.range.is_some() {
746                panic!("BUG: range endpoint must not itself be a range");
747            }
748            let endpoint_schema =
749                element_type_from_range_rule(rule_type).unwrap_or_else(|| rule_type.clone());
750            let left_type =
751                endpoint_type_with_written_unit(&endpoint_schema, range.from.unit.as_deref());
752            let right_type =
753                endpoint_type_with_written_unit(&endpoint_schema, range.to.unit.as_deref());
754            let left = range.from.to_literal(&left_type);
755            let right = range.to.to_literal(&right_type);
756            return LiteralValue::range(
757                TypedLiteral {
758                    value: left.value,
759                    lemma_type: Arc::new(left_type),
760                },
761                TypedLiteral {
762                    value: right.value,
763                    lemma_type: Arc::new(right_type),
764                },
765            );
766        }
767
768        if let Some(b) = self.boolean {
769            return LiteralValue::from_bool(b);
770        }
771        let owned_rule_type = match self.unit.as_deref() {
772            Some(unit)
773                if matches!(
774                    &rule_type.specifications,
775                    TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. }
776                ) && rule_type.unit() != Some(unit) =>
777            {
778                Arc::new(rule_type.clone().with_unit(unit))
779            }
780            _ => Arc::new(rule_type.clone()),
781        };
782        if let Some(number) = self.number {
783            return LiteralValue::number_from_decimal(number);
784        }
785        if let Some(calendar) = &self.calendar {
786            let rational = rational_from_parsed_decimal(calendar.value)
787                .expect("BUG: calendar rule result value must lift to rational");
788            return LiteralValue::measure(rational);
789        }
790        if let Some(measure) = &self.measure {
791            return literal_from_measure_map(measure, owned_rule_type.as_ref());
792        }
793        if let Some(ratio) = &self.ratio {
794            return literal_from_ratio_map(ratio, owned_rule_type.as_ref());
795        }
796        if let Some(date) = &self.date {
797            return LiteralValue::date(date.clone());
798        }
799        if let Some(time) = &self.time {
800            return LiteralValue::time(time.clone());
801        }
802        if let Some(text) = &self.text {
803            return LiteralValue::text(text.clone());
804        }
805        panic!("BUG: rule result value fields cannot reconstruct literal");
806    }
807}
808
809fn endpoint_type_with_written_unit(endpoint_schema: &LemmaType, unit: Option<&str>) -> LemmaType {
810    match unit {
811        Some(unit)
812            if matches!(
813                &endpoint_schema.specifications,
814                TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. }
815            ) && endpoint_schema.unit() != Some(unit) =>
816        {
817            endpoint_schema.clone().with_unit(unit)
818        }
819        _ => endpoint_schema.clone(),
820    }
821}
822
823fn element_type_from_range_rule(rule_type: &LemmaType) -> Option<LemmaType> {
824    range_element_type_specification(&rule_type.specifications).map(LemmaType::primitive)
825}
826
827fn format_unit_map(map: &BTreeMap<String, Decimal>) -> String {
828    map.iter()
829        .map(|(unit, value)| format!("{value} {unit}"))
830        .collect::<Vec<_>>()
831        .join(", ")
832}
833
834impl fmt::Display for RuleResultValue {
835    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
836        if let Some(range) = &self.range {
837            return write!(f, "{}...{}", range.from, range.to);
838        }
839        if let Some(measure) = &self.measure {
840            return write!(f, "{}", format_unit_map(measure));
841        }
842        if let Some(ratio) = &self.ratio {
843            return write!(f, "{}", format_unit_map(ratio));
844        }
845        if let Some(number) = &self.number {
846            return write!(f, "{number}");
847        }
848        if let Some(b) = self.boolean {
849            return write!(f, "{b}");
850        }
851        if let Some(text) = &self.text {
852            return write!(f, "{text}");
853        }
854        if let Some(date) = &self.date {
855            return write!(f, "{date}");
856        }
857        if let Some(time) = &self.time {
858            return write!(f, "{time}");
859        }
860        if let Some(calendar) = &self.calendar {
861            return write!(f, "{} {}", calendar.value, calendar.unit);
862        }
863        if let Some(display) = &self.result {
864            return write!(f, "{display}");
865        }
866        if let Some(reference) = &self.reference {
867            return write!(f, "{reference}");
868        }
869        write!(f, "")
870    }
871}