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