Skip to main content

lemma/planning/
semantics.rs

1//! Resolved semantic types for Lemma
2//!
3//! This module contains all types that represent resolved semantics after planning.
4//! These types are created during the planning phase and used by evaluation, inversion, etc.
5
6// Re-exported parsing types: downstream modules (evaluation, inversion, computation,
7// serialization) import these from `planning::semantics`, never from `parsing` directly.
8use crate::parsing::arena::{ExprId, ExprKind, ExprTree};
9pub use crate::parsing::ast::{
10    ArithmeticComputation, ComparisonComputation, MathematicalComputation, NegationType,
11    RangeBound, VetoExpression,
12};
13pub use crate::parsing::source::Source;
14
15/// Logical computation operators (defined in semantics, not used by the parser).
16/// Returns the logical negation of a comparison (used by De Morgan / NOT-normal-form rewriting).
17#[must_use]
18pub fn negated_comparison(op: ComparisonComputation) -> ComparisonComputation {
19    match op {
20        ComparisonComputation::LessThan => ComparisonComputation::GreaterThanOrEqual,
21        ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThan,
22        ComparisonComputation::GreaterThan => ComparisonComputation::LessThanOrEqual,
23        ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThan,
24        ComparisonComputation::Is => ComparisonComputation::IsNot,
25        ComparisonComputation::IsNot => ComparisonComputation::Is,
26    }
27}
28
29/// Returns the operator that states the same fact with the operands swapped:
30/// `k < x` and `x > k` hold on exactly the same values.
31#[must_use]
32pub fn mirrored_comparison(op: ComparisonComputation) -> ComparisonComputation {
33    match op {
34        ComparisonComputation::LessThan => ComparisonComputation::GreaterThan,
35        ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThanOrEqual,
36        ComparisonComputation::GreaterThan => ComparisonComputation::LessThan,
37        ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThanOrEqual,
38        ComparisonComputation::Is => ComparisonComputation::Is,
39        ComparisonComputation::IsNot => ComparisonComputation::IsNot,
40    }
41}
42
43// Internal-only parsing imports (used only within this module for value/type resolution).
44use crate::computation::rational::{checked_div, rational_new, RationalInteger};
45use crate::parsing::ast::Constraint;
46use crate::parsing::ast::{
47    BooleanValue, CalendarPeriodUnit, ChainStep, CommandArg, ConversionTarget, DateCalendarKind,
48    DateRelativeKind, DateTimeValue, PrimitiveKind, TimeValue, TimezoneValue,
49    TypeConstraintCommand,
50};
51use crate::planning::unit_index::UnitIndex;
52use crate::Error;
53use indexmap::IndexMap;
54use rust_decimal::Decimal;
55use serde::{Deserialize, Deserializer, Serialize, Serializer};
56use std::collections::BTreeMap;
57use std::fmt;
58use std::hash::Hash;
59use std::str::FromStr;
60use std::sync::{Arc, OnceLock};
61
62// -----------------------------------------------------------------------------
63// Type specification and units (resolved type shape; apply constraints is planning)
64// -----------------------------------------------------------------------------
65
66// Unit tables live in `crate::literals` (no dependency on parsing/ast). Re-exported
67// here so downstream modules importing from `planning::semantics` keep working.
68pub use crate::literals::{BaseMeasureVector, MeasureUnit, MeasureUnits, RatioUnit, RatioUnits};
69
70/// Combine two `BaseMeasureVector`s by adding (for multiply) or subtracting (for divide) exponents.
71/// Entries that reach zero exponent are removed (they cancel out). `None` when a resulting
72/// exponent exceeds [`crate::limits::MAX_UNIT_EXPONENT`] in magnitude.
73pub fn combine_decompositions(
74    left: &BaseMeasureVector,
75    right: &BaseMeasureVector,
76    is_multiply: bool,
77) -> Option<BaseMeasureVector> {
78    let mut result = left.clone();
79    for (dim, &exp) in right {
80        let delta = if is_multiply { exp } else { exp.checked_neg()? };
81        let entry = result.entry(dim.clone()).or_insert(0);
82        *entry = entry.checked_add(delta)?;
83        if entry.unsigned_abs() > crate::limits::MAX_UNIT_EXPONENT.unsigned_abs() {
84            return None;
85        }
86        if *entry == 0 {
87            result.remove(dim);
88        }
89    }
90    Some(result)
91}
92
93/// Combine two symbolic unit signatures (sorted-by-unit-name, no-zero-exponent vectors)
94/// under multiplication or division. The result is in canonical form: sorted by unit name
95/// ascending, no zero exponents.
96///
97/// Planning resolves measure products via decompositions; this remains the pinned algebra
98/// for signature-index tests.
99#[cfg(test)]
100pub fn combine_signatures(
101    left: &[(String, i32)],
102    right: &[(String, i32)],
103    is_multiply: bool,
104) -> Vec<(String, i32)> {
105    use std::collections::BTreeMap;
106    let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
107    for (name, exponent) in left {
108        *accumulator.entry(name.clone()).or_insert(0) += exponent;
109    }
110    for (name, exponent) in right {
111        let delta = if is_multiply { *exponent } else { -*exponent };
112        *accumulator.entry(name.clone()).or_insert(0) += delta;
113    }
114    accumulator
115        .into_iter()
116        .filter(|(_, exponent)| *exponent != 0)
117        .collect()
118}
119
120/// Canonicalize a unit signature: sum duplicate entries by name, drop zero exponents,
121/// sort ascending by unit name. Idempotent.
122/// Format a canonical unit signature into human-readable operator style.
123///
124/// Rules:
125/// - Numerator units (positive exponents) sorted alphabetically, joined by `*`.
126/// - Denominator units (negative exponents) sorted alphabetically, joined by `*`, exponents shown
127///   as positive values.
128/// - Separated by `/`. Empty numerator: `1/<denominator>`. No denominator: numerator only.
129/// - Exponents > 1 suffixed as `^n`.
130///
131/// Examples: `eur/hour`, `kilogram*meter^2/second^2`, `1/meter`.
132pub fn format_signature_operator_style(signature: &[(String, i32)]) -> String {
133    let canonical = canonicalize_signature(signature);
134    let mut numerator: Vec<(String, i32)> = Vec::new();
135    let mut denominator: Vec<(String, i32)> = Vec::new();
136    for (name, exponent) in canonical {
137        if exponent > 0 {
138            numerator.push((name, exponent));
139        } else if exponent < 0 {
140            denominator.push((name, -exponent));
141        }
142    }
143    let render = |terms: &[(String, i32)]| -> String {
144        terms
145            .iter()
146            .map(|(name, exp)| {
147                if *exp == 1 {
148                    name.clone()
149                } else {
150                    format!("{name}^{exp}")
151                }
152            })
153            .collect::<Vec<_>>()
154            .join("*")
155    };
156    match (numerator.is_empty(), denominator.is_empty()) {
157        (true, true) => String::new(),
158        (false, true) => render(&numerator),
159        (true, false) => format!("1/{}", render(&denominator)),
160        (false, false) => format!("{}/{}", render(&numerator), render(&denominator)),
161    }
162}
163
164/// Returns the intra-calendar-dimension factor for `name`, if it is a known calendar unit.
165///
166/// Month is canonical (factor 1). Year = 12.
167/// Keys are **singular** (`"month"`, `"year"`).
168///
169/// Returns `None` for names that are not calendar units.
170pub fn calendar_unit_factor(name: &str) -> Option<crate::computation::rational::RationalInteger> {
171    use crate::computation::rational::rational_one;
172    match name {
173        "month" => Some(rational_one()),
174        "year" => Some(rational_new(12, 1)),
175        _ => None,
176    }
177}
178
179fn reject_negative_width_magnitude(magnitude: &RationalInteger, cmd: &str) -> Result<(), String> {
180    use crate::computation::rational::rational_zero;
181    use std::cmp::Ordering;
182    match magnitude.try_cmp(&rational_zero()) {
183        Ok(Ordering::Less) => Err(format!("{cmd} width must not be negative")),
184        Ok(Ordering::Equal | Ordering::Greater) => Ok(()),
185        Err(failure) => Err(format!("{cmd} width compare failed: {failure}")),
186    }
187}
188
189/// Store a width bound as declared `(magnitude, unit)`. Family and factors are resolved
190/// later against `unit_index` during planning validation.
191fn parse_unresolved_width_bound(
192    args: &[CommandArg],
193    cmd: &str,
194) -> Result<(RationalInteger, String), String> {
195    use crate::computation::rational::decimal_to_rational;
196    let lit = require_literal(args, cmd)?;
197    let (magnitude, unit_name) = match lit {
198        crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
199        other => {
200            return Err(format!(
201                "{cmd} requires a measure literal with a unit, got {}",
202                value_kind_name(other)
203            ));
204        }
205    };
206    let magnitude_rational = decimal_to_rational(magnitude)
207        .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
208    reject_negative_width_magnitude(&magnitude_rational, cmd)?;
209    Ok((magnitude_rational, unit_name))
210}
211
212/// Planning consistency for range endpoint and width bound declarations.
213///
214/// `unit_index` resolves date/time width units (duration vs calendar). Pass empty for
215/// callers that only check number/ratio/measure ranges.
216pub(crate) fn check_range_bound_consistency(
217    spec: &TypeSpecification,
218    unit_index: &crate::planning::unit_index::UnitIndex,
219) -> Result<(), String> {
220    use std::cmp::Ordering;
221
222    fn endpoint_order_ok_dates(lo: &DateTimeValue, hi: &DateTimeValue) -> bool {
223        compare_semantic_dates(&date_time_to_semantic(lo), &date_time_to_semantic(hi))
224            != Ordering::Greater
225    }
226    fn endpoint_order_ok_times(lo: &TimeValue, hi: &TimeValue) -> bool {
227        compare_semantic_times(&time_to_semantic(lo), &time_to_semantic(hi)) != Ordering::Greater
228    }
229
230    match spec {
231        TypeSpecification::NumberRange {
232            lower,
233            upper,
234            minimum,
235            maximum,
236            ..
237        }
238        | TypeSpecification::RatioRange {
239            lower,
240            upper,
241            minimum,
242            maximum,
243            ..
244        } => {
245            if let (Some(lo), Some(hi)) = (lower, upper) {
246                if lo
247                    .try_cmp(hi)
248                    .map_err(|failure| format!("range endpoint compare failed: {failure}"))?
249                    == Ordering::Greater
250                {
251                    return Err(format!(
252                        "invalid range: lower {} is greater than upper {}",
253                        lo.display_str(),
254                        hi.display_str()
255                    ));
256                }
257            }
258            if let (Some(min_w), Some(max_w)) = (minimum, maximum) {
259                if min_w
260                    .try_cmp(max_w)
261                    .map_err(|failure| format!("range width compare failed: {failure}"))?
262                    == Ordering::Greater
263                {
264                    return Err(format!(
265                        "invalid range: minimum width {} is greater than maximum width {}",
266                        min_w.display_str(),
267                        max_w.display_str()
268                    ));
269                }
270            }
271            Ok(())
272        }
273        TypeSpecification::MeasureRange {
274            lower,
275            upper,
276            minimum,
277            maximum,
278            units,
279            ..
280        } => {
281            if let (Some(lo), Some(hi)) = (lower, upper) {
282                let lo_c =
283                    measure_declared_bound_to_canonical(&lo.0, &lo.1, units, "range", "lower")?;
284                let hi_c =
285                    measure_declared_bound_to_canonical(&hi.0, &hi.1, units, "range", "upper")?;
286                if lo_c
287                    .try_cmp(&hi_c)
288                    .map_err(|failure| format!("range endpoint compare failed: {failure}"))?
289                    == Ordering::Greater
290                {
291                    return Err(format!(
292                        "invalid range: lower {} {} is greater than upper {} {}",
293                        lo.0.display_str(),
294                        lo.1,
295                        hi.0.display_str(),
296                        hi.1
297                    ));
298                }
299            }
300            if let (Some(min_w), Some(max_w)) = (minimum, maximum) {
301                let min_c = measure_declared_bound_to_canonical(
302                    &min_w.0, &min_w.1, units, "range", "minimum",
303                )?;
304                let max_c = measure_declared_bound_to_canonical(
305                    &max_w.0, &max_w.1, units, "range", "maximum",
306                )?;
307                if min_c
308                    .try_cmp(&max_c)
309                    .map_err(|failure| format!("range width compare failed: {failure}"))?
310                    == Ordering::Greater
311                {
312                    return Err(format!(
313                        "invalid range: minimum width {} {} is greater than maximum width {} {}",
314                        min_w.0.display_str(),
315                        min_w.1,
316                        max_w.0.display_str(),
317                        max_w.1
318                    ));
319                }
320            }
321            Ok(())
322        }
323        TypeSpecification::DateRange {
324            lower,
325            upper,
326            minimum,
327            maximum,
328            ..
329        } => {
330            if let (Some(lo), Some(hi)) = (lower, upper) {
331                if !endpoint_order_ok_dates(lo, hi) {
332                    return Err(format!(
333                        "invalid range: lower {lo} is greater than upper {hi}"
334                    ));
335                }
336            }
337            check_temporal_width_pair_consistency(minimum, maximum, unit_index, true)
338        }
339        TypeSpecification::TimeRange {
340            lower,
341            upper,
342            minimum,
343            maximum,
344            ..
345        } => {
346            if let (Some(lo), Some(hi)) = (lower, upper) {
347                if !endpoint_order_ok_times(lo, hi) {
348                    return Err(format!(
349                        "invalid range: lower {lo} is greater than upper {hi}"
350                    ));
351                }
352            }
353            check_temporal_width_pair_consistency(minimum, maximum, unit_index, false)
354        }
355        _ => Ok(()),
356    }
357}
358
359fn check_temporal_width_pair_consistency(
360    minimum: &Option<(RationalInteger, String)>,
361    maximum: &Option<(RationalInteger, String)>,
362    unit_index: &crate::planning::unit_index::UnitIndex,
363    allow_calendar: bool,
364) -> Result<(), String> {
365    use std::cmp::Ordering;
366    let resolve = |bound: &(RationalInteger, String),
367                   command: &str|
368     -> Result<(RationalInteger, Arc<LemmaType>), String> {
369        let (bare, owner) = unit_index.resolve(bound.1.as_str()).map_err(|err| {
370            format!(
371                "{command} width unit '{}': {err} (add `uses lemma units` or declare the unit)",
372                bound.1
373            )
374        })?;
375        if allow_calendar {
376            if !owner.is_duration_like() && !owner.is_calendar_like() {
377                return Err(format!(
378                    "{command} width unit '{bare}' must be a duration or calendar unit",
379                ));
380            }
381        } else if !owner.is_duration_like() {
382            return Err(format!(
383                "{command} width unit '{bare}' must be a duration unit",
384            ));
385        }
386        let TypeSpecification::Measure { units, .. } = &owner.specifications else {
387            return Err(format!(
388                "{command} width unit '{bare}' must resolve to a measure type",
389            ));
390        };
391        let canonical = measure_declared_bound_to_canonical(
392            &bound.0,
393            &bare,
394            units,
395            owner.name().as_str(),
396            command,
397        )?;
398        Ok((canonical, Arc::clone(&owner)))
399    };
400
401    match (minimum, maximum) {
402        (None, None) => Ok(()),
403        (Some(min_w), None) => {
404            resolve(min_w, "minimum")?;
405            Ok(())
406        }
407        (None, Some(max_w)) => {
408            resolve(max_w, "maximum")?;
409            Ok(())
410        }
411        (Some(min_w), Some(max_w)) => {
412            let (min_c, min_owner) = resolve(min_w, "minimum")?;
413            let (max_c, max_owner) = resolve(max_w, "maximum")?;
414            if min_owner.is_calendar_like() != max_owner.is_calendar_like() {
415                return Err(
416                    "invalid range: minimum and maximum width must not mix calendar and duration units"
417                        .to_string(),
418                );
419            }
420            if min_c
421                .try_cmp(&max_c)
422                .map_err(|failure| format!("range width compare failed: {failure}"))?
423                == Ordering::Greater
424            {
425                return Err(format!(
426                    "invalid range: minimum width {} {} is greater than maximum width {} {}",
427                    min_w.0.display_str(),
428                    min_w.1,
429                    max_w.0.display_str(),
430                    max_w.1
431                ));
432            }
433            Ok(())
434        }
435    }
436}
437
438fn owner_declares_measure_unit(owner: &LemmaType, unit_name: &str) -> bool {
439    owner
440        .measure_unit_names()
441        .is_some_and(|names| names.contains(&unit_name))
442}
443
444/// Compute the numeric factor of a symbolic unit signature relative to canonical bases.
445///
446/// For each `(unit_name, exponent)` in `signature`:
447/// 1. When `owner` declares the unit, use `owner.measure_unit_factor(unit_name)`.
448/// 2. Fall back to `expression_units[unit_name].measure_unit_factor(unit_name)`.
449/// 3. Unknown names panic with `"BUG: signature_factor called with unresolved unit name"`.
450///    Ambiguous multi-owner names panic asking the caller to pass a declaring owner.
451///
452/// Returns the product of `factor^exponent` over all pairs, or `NumericFailure` on overflow.
453pub fn signature_factor(
454    signature: &[(String, i32)],
455    expression_units: &crate::planning::unit_index::UnitIndex,
456    owner: Option<&LemmaType>,
457) -> Result<
458    crate::computation::rational::RationalInteger,
459    crate::computation::rational::NumericFailure,
460> {
461    use crate::computation::rational::{checked_div, checked_mul, rational_one};
462    let mut acc = rational_one();
463    for (name, exponent) in signature {
464        let factor =
465            if let Some(owner) = owner.filter(|owner| owner_declares_measure_unit(owner, name)) {
466                owner.measure_unit_factor(name).clone()
467            } else if let Some(lemma_type) = expression_units.unique_owner(name) {
468                lemma_type.measure_unit_factor(name).clone()
469            } else if !expression_units.owners_for(name).is_empty() {
470                panic!(
471                "BUG: signature_factor called with ambiguous unit name '{}' (pass declaring owner)",
472                name
473            );
474            } else {
475                panic!(
476                    "BUG: signature_factor called with unresolved unit name '{}'",
477                    name
478                );
479            };
480        let mut term = rational_one();
481        let abs_exp = exponent.unsigned_abs();
482        for _ in 0..abs_exp {
483            term = checked_mul(&term, &factor)?;
484        }
485        if *exponent >= 0 {
486            acc = checked_mul(&acc, &term)?;
487        } else {
488            acc = checked_div(&acc, &term)?;
489        }
490    }
491    Ok(acc)
492}
493
494pub fn canonicalize_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
495    use std::collections::BTreeMap;
496    let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
497    for (name, exponent) in signature {
498        let entry = accumulator.entry(name.clone()).or_insert(0);
499        *entry = entry
500            .checked_add(*exponent)
501            .expect("BUG: planning bounds signature unit exponents");
502    }
503    accumulator
504        .into_iter()
505        .filter(|(_, exponent)| *exponent != 0)
506        .collect()
507}
508
509/// Convert a `BaseMeasureVector` decomposition into canonical signature form.
510pub fn base_measure_vector_as_signature(decomposition: &BaseMeasureVector) -> Vec<(String, i32)> {
511    decomposition
512        .iter()
513        .filter(|(_, exponent)| **exponent != 0)
514        .map(|(dimension, exponent)| (dimension.name().to_string(), *exponent))
515        .collect()
516}
517
518/// One base axis of a measure decomposition.
519#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
520pub enum Dimension {
521    Duration,
522    Calendar,
523    Family(TypeFamily),
524}
525
526impl Dimension {
527    /// Bare name: builtin axis or family root type name.
528    #[must_use]
529    pub fn name(&self) -> &str {
530        match self {
531            Dimension::Duration => "duration",
532            Dimension::Calendar => "calendar",
533            Dimension::Family(family) => &family.name,
534        }
535    }
536
537    #[must_use]
538    fn with_families_without_effective(&self) -> Self {
539        match self {
540            Dimension::Duration => Dimension::Duration,
541            Dimension::Calendar => Dimension::Calendar,
542            Dimension::Family(family) => Dimension::Family(family.without_effective()),
543        }
544    }
545}
546
547fn base_measure_vector_without_effective(domain: &BaseMeasureVector) -> BaseMeasureVector {
548    domain
549        .iter()
550        .map(|(dimension, exponent)| (dimension.with_families_without_effective(), *exponent))
551        .collect()
552}
553
554fn measure_units_without_effective(units: &MeasureUnits) -> MeasureUnits {
555    MeasureUnits(
556        units
557            .iter()
558            .map(|unit| MeasureUnit {
559                name: unit.name.clone(),
560                factor: unit.factor.clone(),
561                derived_measure_factors: unit.derived_measure_factors.clone(),
562                decomposition: base_measure_vector_without_effective(&unit.decomposition),
563                minimum: unit.minimum.clone(),
564                maximum: unit.maximum.clone(),
565                suggestion_magnitude: unit.suggestion_magnitude.clone(),
566            })
567            .collect(),
568    )
569}
570
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
572#[serde(rename_all = "snake_case")]
573pub enum MeasureTrait {
574    Duration,
575    Calendar,
576}
577
578pub fn duration_decomposition() -> BaseMeasureVector {
579    [(Dimension::Duration, 1i32)].into_iter().collect()
580}
581
582pub fn calendar_decomposition() -> BaseMeasureVector {
583    [(Dimension::Calendar, 1i32)].into_iter().collect()
584}
585
586/// Marker `LemmaType` for a Measure value whose signature has not been resolved to
587/// a named measure type. Carries an empty decomposition; runtime signature is
588/// derived via [`LemmaType::measure_runtime_signature`].
589pub fn anonymous_measure_type() -> LemmaType {
590    LemmaType::anonymous_for_decomposition(BaseMeasureVector::new())
591}
592
593/// Return a copy of `signature` with every exponent negated. Used by Number/Measure
594/// reciprocal construction (`1 / Q`).
595pub fn negate_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
596    signature.iter().map(|(n, e)| (n.clone(), -*e)).collect()
597}
598
599#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
600pub enum TypeSpecification {
601    Boolean {
602        help: String,
603    },
604    Measure {
605        minimum: Option<(RationalInteger, String)>,
606        maximum: Option<(RationalInteger, String)>,
607        decimals: Option<u8>,
608        units: MeasureUnits,
609        traits: Vec<MeasureTrait>,
610        /// Common dimensional decomposition vector shared by all units in this measure.
611        /// `None` until the decomposition pass runs. Base measures (no compound unit expression)
612        /// are assigned `Some({measure_name: 1})` by the pass. `Some(empty_map)` means resolved
613        /// to dimensionless (e.g. `kg/kg`).
614        decomposition: Option<BaseMeasureVector>,
615        /// Written unit for this measure value (`100 eur`, `as usd`). Schema types use `None`.
616        unit: Option<String>,
617        help: String,
618    },
619    Number {
620        minimum: Option<RationalInteger>,
621        maximum: Option<RationalInteger>,
622        decimals: Option<u8>,
623        help: String,
624    },
625    NumberRange {
626        lower: Option<RationalInteger>,
627        upper: Option<RationalInteger>,
628        minimum: Option<RationalInteger>,
629        maximum: Option<RationalInteger>,
630        help: String,
631    },
632    Ratio {
633        minimum: Option<RationalInteger>,
634        maximum: Option<RationalInteger>,
635        decimals: Option<u8>,
636        units: RatioUnits,
637        /// Written unit for this ratio value (`25%`, `as percent`). Schema types use `None`.
638        unit: Option<String>,
639        help: String,
640    },
641    RatioRange {
642        lower: Option<RationalInteger>,
643        upper: Option<RationalInteger>,
644        minimum: Option<RationalInteger>,
645        maximum: Option<RationalInteger>,
646        units: RatioUnits,
647        help: String,
648    },
649    Text {
650        length: Option<usize>,
651        options: Vec<String>,
652        help: String,
653    },
654    Date {
655        minimum: Option<DateTimeValue>,
656        maximum: Option<DateTimeValue>,
657        help: String,
658    },
659    DateRange {
660        lower: Option<DateTimeValue>,
661        upper: Option<DateTimeValue>,
662        minimum: Option<(RationalInteger, String)>,
663        maximum: Option<(RationalInteger, String)>,
664        help: String,
665    },
666    Time {
667        minimum: Option<TimeValue>,
668        maximum: Option<TimeValue>,
669        help: String,
670    },
671    TimeRange {
672        lower: Option<TimeValue>,
673        upper: Option<TimeValue>,
674        minimum: Option<(RationalInteger, String)>,
675        maximum: Option<(RationalInteger, String)>,
676        help: String,
677    },
678    MeasureRange {
679        lower: Option<(RationalInteger, String)>,
680        upper: Option<(RationalInteger, String)>,
681        minimum: Option<(RationalInteger, String)>,
682        maximum: Option<(RationalInteger, String)>,
683        units: MeasureUnits,
684        decomposition: Option<BaseMeasureVector>,
685        help: String,
686    },
687    Veto {
688        message: Option<String>,
689    },
690    /// Sentinel used during type inference when the type could not be determined.
691    /// Propagates through expressions without generating cascading errors.
692    /// Must never appear in a successfully validated graph or execution plan.
693    Undetermined,
694    /// A spec instance. Identity is repository, spec name, and the resolved
695    /// temporal row (`effective`). Appended so postcard discriminants of earlier
696    /// variants stay stable.
697    Spec {
698        spec: SpecIdentity,
699    },
700    /// A list of rows of `element`, keyed by row number or named fields.
701    List {
702        #[serde(with = "crate::snapshot::shared::arc")]
703        element: Arc<LemmaType>,
704        key: ListKey,
705    },
706}
707
708/// How a list addresses its rows.
709#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
710pub enum ListKey {
711    Numbered,
712    Fields(Vec<String>),
713}
714
715/// Resolved spec a value or rule result refers to.
716///
717/// `effective` is the resolved row's `effective_from`, not the run instant.
718#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
719pub struct SpecIdentity {
720    pub repository: Option<String>,
721    pub spec: String,
722    pub effective: crate::parsing::ast::EffectiveDate,
723}
724
725impl SpecIdentity {
726    #[must_use]
727    pub fn of(
728        repository: &crate::parsing::ast::LemmaRepository,
729        spec: &crate::parsing::ast::LemmaSpec,
730    ) -> Self {
731        Self {
732            repository: repository.name.clone(),
733            spec: spec.name.clone(),
734            effective: spec.effective_from.clone(),
735        }
736    }
737}
738
739impl fmt::Display for SpecIdentity {
740    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
741        if let Some(repository) = &self.repository {
742            write!(f, "{repository} ")?;
743        }
744        write!(f, "{}", self.spec)?;
745        if let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.effective {
746            write!(f, " {at}")?;
747        }
748        Ok(())
749    }
750}
751
752impl std::fmt::Display for TypeSpecification {
753    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
754        match self {
755            Self::Boolean { .. } => f.write_str("boolean"),
756            Self::Measure { .. } => f.write_str("measure"),
757            Self::MeasureRange { .. } => f.write_str("measure range"),
758            Self::Number { .. } => f.write_str("number"),
759            Self::NumberRange { .. } => f.write_str("number range"),
760            Self::Text { .. } => f.write_str("text"),
761            Self::Date { .. } => f.write_str("date"),
762            Self::DateRange { .. } => f.write_str("date range"),
763            Self::Time { .. } => f.write_str("time"),
764            Self::TimeRange { .. } => f.write_str("time range"),
765            Self::Ratio { .. } => f.write_str("ratio"),
766            Self::RatioRange { .. } => f.write_str("ratio range"),
767            Self::Veto { .. } => f.write_str("veto"),
768            Self::Undetermined => f.write_str("undetermined"),
769            Self::Spec { spec } => write!(f, "{spec}"),
770            Self::List {
771                element,
772                key: ListKey::Numbered,
773            } => write!(f, "{} list", element.specifications),
774            Self::List {
775                element,
776                key: ListKey::Fields(fields),
777            } => write!(
778                f,
779                "{} list key {}",
780                element.specifications,
781                fields.join(", ")
782            ),
783        }
784    }
785}
786
787impl TypeSpecification {
788    /// Returns the help text associated with this type, or an empty string if none.
789    pub fn help(&self) -> &str {
790        match self {
791            Self::Boolean { help, .. }
792            | Self::Measure { help, .. }
793            | Self::Number { help, .. }
794            | Self::NumberRange { help, .. }
795            | Self::Text { help, .. }
796            | Self::Date { help, .. }
797            | Self::DateRange { help, .. }
798            | Self::Time { help, .. }
799            | Self::TimeRange { help, .. }
800            | Self::Ratio { help, .. }
801            | Self::RatioRange { help, .. }
802            | Self::MeasureRange { help, .. } => help.as_str(),
803            Self::Veto { .. } | Self::Undetermined | Self::Spec { .. } => "",
804            Self::List { element, .. } => element.specifications.help(),
805        }
806    }
807
808    #[must_use]
809    fn with_families_without_effective(&self) -> Self {
810        match self {
811            Self::Measure {
812                minimum,
813                maximum,
814                decimals,
815                units,
816                traits,
817                decomposition,
818                unit,
819                help,
820            } => Self::Measure {
821                minimum: minimum.clone(),
822                maximum: maximum.clone(),
823                decimals: *decimals,
824                units: measure_units_without_effective(units),
825                traits: traits.clone(),
826                decomposition: decomposition
827                    .as_ref()
828                    .map(base_measure_vector_without_effective),
829                unit: unit.clone(),
830                help: help.clone(),
831            },
832            Self::MeasureRange {
833                lower,
834                upper,
835                minimum,
836                maximum,
837                units,
838                decomposition,
839                help,
840            } => Self::MeasureRange {
841                lower: lower.clone(),
842                upper: upper.clone(),
843                minimum: minimum.clone(),
844                maximum: maximum.clone(),
845                units: measure_units_without_effective(units),
846                decomposition: decomposition
847                    .as_ref()
848                    .map(base_measure_vector_without_effective),
849                help: help.clone(),
850            },
851            Self::List { element, key } => Self::List {
852                element: Arc::new(element.with_families_without_effective()),
853                key: key.clone(),
854            },
855            other => other.clone(),
856        }
857    }
858
859    /// Schema equality that ignores written [`unit`](Self::Measure::unit) /
860    /// [`unit`](Self::Ratio::unit). Used when comparing declared types that may
861    /// carry a value-specific written unit.
862    #[must_use]
863    pub fn equal_ignoring_unit(&self, other: &Self) -> bool {
864        match (self, other) {
865            (
866                Self::Measure {
867                    minimum: a_min,
868                    maximum: a_max,
869                    decimals: a_dec,
870                    units: a_units,
871                    traits: a_traits,
872                    decomposition: a_decomp,
873                    help: a_help,
874                    unit: _,
875                },
876                Self::Measure {
877                    minimum: b_min,
878                    maximum: b_max,
879                    decimals: b_dec,
880                    units: b_units,
881                    traits: b_traits,
882                    decomposition: b_decomp,
883                    help: b_help,
884                    unit: _,
885                },
886            ) => {
887                a_min == b_min
888                    && a_max == b_max
889                    && a_dec == b_dec
890                    && a_units == b_units
891                    && a_traits == b_traits
892                    && a_decomp == b_decomp
893                    && a_help == b_help
894            }
895            (
896                Self::Ratio {
897                    minimum: a_min,
898                    maximum: a_max,
899                    decimals: a_dec,
900                    units: a_units,
901                    help: a_help,
902                    unit: _,
903                },
904                Self::Ratio {
905                    minimum: b_min,
906                    maximum: b_max,
907                    decimals: b_dec,
908                    units: b_units,
909                    help: b_help,
910                    unit: _,
911                },
912            ) => {
913                a_min == b_min
914                    && a_max == b_max
915                    && a_dec == b_dec
916                    && a_units == b_units
917                    && a_help == b_help
918            }
919            _ => self == other,
920        }
921    }
922}
923
924/// Extract a typed [`Value`] from the first `CommandArg`, requiring `Literal` shape.
925///
926/// `Label` args carry identifiers (unit names, option keywords) and never satisfy a
927/// command position that wants a literal value. Returning a typed `Value` keeps the
928/// caller's match exhaustive over [`Value`] variants — no string coercion path.
929fn require_literal<'a>(
930    args: &'a [CommandArg],
931    cmd: &str,
932) -> Result<&'a crate::literals::Value, String> {
933    if args.len() > 1 {
934        return Err(format!("{cmd} takes exactly one value"));
935    }
936    let arg = args
937        .first()
938        .ok_or_else(|| format!("{} requires an argument", cmd))?;
939    match arg {
940        CommandArg::Literal(v) => Ok(v),
941        CommandArg::Label(name) => Err(format!(
942            "{} requires a literal value, got identifier '{}'",
943            cmd, name
944        )),
945        CommandArg::UnitExpr(_) => Err(format!(
946            "{} requires a literal value, got a unit expression (only valid for 'unit' command)",
947            cmd
948        )),
949    }
950}
951
952fn apply_type_help_command(help: &mut String, args: &[CommandArg]) -> Result<(), String> {
953    match require_literal(args, "help")? {
954        crate::literals::Value::Text(s) => {
955            *help = s.clone();
956            Ok(())
957        }
958        other => Err(format!(
959            "help requires a text literal (quoted string), got {}",
960            value_kind_name(other)
961        )),
962    }
963}
964
965fn format_measure_units_list(units: &MeasureUnits) -> String {
966    units
967        .iter()
968        .map(|u| u.name.as_str())
969        .collect::<Vec<_>>()
970        .join(", ")
971}
972
973/// What kind of value `-> suggest` expects when rejecting a calendar literal.
974#[derive(Debug, Clone, Copy, PartialEq, Eq)]
975pub(crate) enum SuggestionExpectation {
976    MeasureUnits,
977    Text,
978    Number,
979    Boolean,
980    Date,
981    Time,
982    Ratio,
983    NumberRange,
984    DateRange,
985    TimeRange,
986    MeasureRange,
987    RatioRange,
988}
989
990pub(crate) fn suggestion_value_mismatch_error(
991    calendar_unit: &str,
992    type_name: &str,
993    expectation: SuggestionExpectation,
994    measure_units: Option<&MeasureUnits>,
995) -> String {
996    let unit_label = calendar_unit;
997    let first = format!("Unit '{unit_label}' is for calendar data.");
998    match expectation {
999        SuggestionExpectation::MeasureUnits => {
1000            let list = measure_units
1001                .map(format_measure_units_list)
1002                .unwrap_or_default();
1003            format!("{first} Valid '{type_name}' units are: {list}.")
1004        }
1005        SuggestionExpectation::Text => format!(
1006            "{first} Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
1007        ),
1008        SuggestionExpectation::Number => format!(
1009            "{first} Please provide a number, for example `-> suggest 42`."
1010        ),
1011        SuggestionExpectation::Boolean => format!(
1012            "{first} Please provide true or false, for example `-> suggest true`."
1013        ),
1014        SuggestionExpectation::Date => format!(
1015            "{first} Please provide a date, for example `-> suggest 2024-06-15`."
1016        ),
1017        SuggestionExpectation::Time => format!(
1018            "{first} Please provide a time, for example `-> suggest 09:00:00`."
1019        ),
1020        SuggestionExpectation::Ratio | SuggestionExpectation::RatioRange => format!(
1021            "{first} Please provide a ratio, for example `-> suggest 25%`."
1022        ),
1023        SuggestionExpectation::NumberRange => format!(
1024            "{first} Please provide a number range, for example `-> suggest 10...100`."
1025        ),
1026        SuggestionExpectation::DateRange => format!(
1027            "{first} Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
1028        ),
1029        SuggestionExpectation::TimeRange => format!(
1030            "{first} Please provide a time range, for example `-> suggest 09:00...17:00`."
1031        ),
1032        SuggestionExpectation::MeasureRange => format!(
1033            "{first} Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
1034        ),
1035    }
1036}
1037
1038fn measure_suggestion_wrong_shape_error(type_name: &str, traits: &[MeasureTrait]) -> String {
1039    let example = if traits.contains(&MeasureTrait::Duration) {
1040        "4 week"
1041    } else if traits.contains(&MeasureTrait::Calendar) {
1042        "3 month"
1043    } else {
1044        "30 kilogram"
1045    };
1046    format!(
1047        "Please provide a value with a unit valid for '{type_name}', for example `-> suggest {example}`."
1048    )
1049}
1050
1051fn reject_calendar_for_suggestion(
1052    value: &crate::literals::Value,
1053    type_name: &str,
1054    expectation: SuggestionExpectation,
1055    measure_units: Option<&MeasureUnits>,
1056) -> Result<(), String> {
1057    if let crate::literals::Value::NumberWithUnit(_, unit) = value {
1058        if calendar_unit_factor(unit).is_some() {
1059            return Err(suggestion_value_mismatch_error(
1060                unit,
1061                type_name,
1062                expectation,
1063                measure_units,
1064            ));
1065        }
1066    }
1067    Ok(())
1068}
1069
1070/// Human-readable name for a [`Value`] variant — used in mismatch error messages.
1071fn value_kind_name(v: &crate::literals::Value) -> &'static str {
1072    use crate::literals::Value;
1073    match v {
1074        Value::Number(_) => "number",
1075        Value::NumberWithUnit(_, _) => "number_with_unit",
1076        Value::Text(_) => "text",
1077        Value::Date(_) => "date",
1078        Value::Time(_) => "time",
1079        Value::Boolean(_) => "boolean",
1080        Value::Range(_, _) => "range",
1081    }
1082}
1083
1084fn require_suggestion_range_endpoints<'a>(
1085    args: &'a [CommandArg],
1086    type_name: &str,
1087    expectation: SuggestionExpectation,
1088    measure_units: Option<&MeasureUnits>,
1089) -> Result<(&'a crate::literals::Value, &'a crate::literals::Value), String> {
1090    match require_literal(args, "suggest")? {
1091        crate::literals::Value::NumberWithUnit(_, unit)
1092            if calendar_unit_factor(unit).is_some() =>
1093        {
1094            Err(suggestion_value_mismatch_error(
1095                unit,
1096                type_name,
1097                expectation,
1098                measure_units,
1099            ))
1100        }
1101        crate::literals::Value::Range(left, right) => Ok((left.as_ref(), right.as_ref())),
1102        _ => Err(match expectation {
1103            SuggestionExpectation::NumberRange => {
1104                "Please provide a number range, for example `-> suggest 10...100`.".to_string()
1105            }
1106            SuggestionExpectation::DateRange => {
1107                "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
1108                    .to_string()
1109            }
1110            SuggestionExpectation::RatioRange => {
1111                "Please provide a ratio range, for example `-> suggest 10%...50%`.".to_string()
1112            }
1113            SuggestionExpectation::MeasureRange => format!(
1114                "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
1115            ),
1116            _ => unreachable!("BUG: require_suggestion_range_endpoints called with non-range expectation"),
1117        }),
1118    }
1119}
1120
1121fn lift_parser_decimal(decimal: rust_decimal::Decimal) -> Result<RationalInteger, String> {
1122    crate::computation::rational::decimal_to_rational(decimal)
1123        .map_err(|failure| format!("literal failed rational lift: {failure}"))
1124}
1125
1126/// Element spec for a range type, used for parsing endpoints and lifting literal endpoints.
1127pub fn range_element_type_specification(
1128    range_spec: &TypeSpecification,
1129) -> Option<TypeSpecification> {
1130    range_spec.element_from_range()
1131}
1132
1133fn range_endpoints_compatible(left: &LemmaType, right: &LemmaType) -> bool {
1134    match (&left.specifications, &right.specifications) {
1135        (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
1136        (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
1137        (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
1138        (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
1139            left.same_measure_family(right)
1140                || left.compatible_with_anonymous_measure(right)
1141                || right.compatible_with_anonymous_measure(left)
1142        }
1143        (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
1144        _ => false,
1145    }
1146}
1147
1148/// Infer the range type specification from two compatible endpoint types.
1149pub fn range_type_specification_from_endpoints(
1150    left: &LemmaType,
1151    right: &LemmaType,
1152) -> Option<TypeSpecification> {
1153    if !range_endpoints_compatible(left, right) {
1154        return None;
1155    }
1156    left.specifications.range_from_element()
1157}
1158
1159/// Resolves a written unit token to a bare declared unit name against the named types
1160/// visible at one planning site.
1161pub enum UnitTokenResolver<'a> {
1162    /// The spec's types are resolved: tokens resolve through its unit index.
1163    Declared {
1164        unit_index: &'a UnitIndex,
1165        named_types: &'a IndexMap<String, Arc<LemmaType>>,
1166    },
1167    /// The spec has no resolved types: every token is unknown.
1168    Undeclared,
1169    /// Tokens resolve through the planning unit index of the spec being planned.
1170    Planning {
1171        unit_index: &'a crate::planning::unit_index::PlanningUnitIndex,
1172        named_types: &'a IndexMap<String, Arc<LemmaType>>,
1173    },
1174    /// Tokens resolve through the first scope that knows them.
1175    Search(Vec<(&'a UnitIndex, &'a IndexMap<String, Arc<LemmaType>>)>),
1176    /// Tokens resolve through the unit index and must be declared on `lemma_type`
1177    /// or on its range element.
1178    Typed {
1179        unit_index: &'a UnitIndex,
1180        named_types: &'a IndexMap<String, Arc<LemmaType>>,
1181        lemma_type: &'a LemmaType,
1182    },
1183}
1184
1185impl UnitTokenResolver<'_> {
1186    pub fn resolve(&self, unit: &str) -> Result<String, String> {
1187        match self {
1188            UnitTokenResolver::Declared {
1189                unit_index,
1190                named_types,
1191            } => unit_index
1192                .resolve_with_named_types(unit, named_types)
1193                .map(|(bare, _)| bare),
1194            UnitTokenResolver::Undeclared => Err(unknown_unit_message(unit)),
1195            UnitTokenResolver::Planning {
1196                unit_index,
1197                named_types,
1198            } => unit_index
1199                .resolve_with_named_types(unit, named_types)
1200                .map(|(bare, _)| bare),
1201            UnitTokenResolver::Search(scopes) => scopes
1202                .iter()
1203                .find_map(|(unit_index, named_types)| {
1204                    unit_index
1205                        .resolve_with_named_types(unit, named_types)
1206                        .ok()
1207                        .map(|(bare, _)| bare)
1208                })
1209                .ok_or_else(|| unknown_unit_message(unit)),
1210            UnitTokenResolver::Typed {
1211                unit_index,
1212                named_types,
1213                lemma_type,
1214            } => resolve_declared_on_type(unit, lemma_type, unit_index, named_types),
1215        }
1216    }
1217}
1218
1219fn unknown_unit_message(unit: &str) -> String {
1220    format!("Unknown unit '{unit}'. Declare it on a measure or ratio type, or import it with uses.")
1221}
1222
1223fn resolve_declared_on_type(
1224    written: &str,
1225    lemma_type: &LemmaType,
1226    unit_index: &UnitIndex,
1227    named_types: &IndexMap<String, Arc<LemmaType>>,
1228) -> Result<String, String> {
1229    match unit_index.resolve_with_named_types(written, named_types) {
1230        Ok((bare, _)) => {
1231            if unit_name_declared_on_type(lemma_type, &bare)
1232                || range_element_declares_unit(lemma_type, &bare)
1233            {
1234                Ok(bare)
1235            } else {
1236                Err(format!(
1237                    "Unit '{written}' resolves to '{bare}', which is not declared on type {}",
1238                    lemma_type.name()
1239                ))
1240            }
1241        }
1242        Err(index_err) => {
1243            if unit_name_declared_on_type(lemma_type, written)
1244                || range_element_declares_unit(lemma_type, written)
1245            {
1246                Ok(written.to_string())
1247            } else {
1248                Err(index_err)
1249            }
1250        }
1251    }
1252}
1253
1254fn range_element_declares_unit(lemma_type: &LemmaType, unit_name: &str) -> bool {
1255    let Some(element) = lemma_type.specifications.element_from_range() else {
1256        return false;
1257    };
1258    unit_name_declared_on_type(&LemmaType::primitive(element), unit_name)
1259}
1260
1261fn unit_name_declared_on_type(lemma_type: &LemmaType, unit_name: &str) -> bool {
1262    match &lemma_type.specifications {
1263        TypeSpecification::Measure { units, .. } => units.get(unit_name).is_ok(),
1264        TypeSpecification::Ratio { units, .. } => units.get(unit_name).is_ok(),
1265        _ => false,
1266    }
1267}
1268
1269/// Declared bare unit name on a measure or ratio element type for `token`.
1270fn bare_declared_unit_on_element(
1271    element_spec: &TypeSpecification,
1272    token: &str,
1273) -> Result<String, String> {
1274    match element_spec {
1275        TypeSpecification::Measure { units, .. } => Ok(units.get(token)?.name.clone()),
1276        TypeSpecification::Ratio { units, .. } => Ok(units.get(token)?.name.clone()),
1277        other => panic!("BUG: bare_declared_unit_on_element on non-measure/ratio element {other}"),
1278    }
1279}
1280
1281/// Resolve a written unit token to a bare declared name on `element_spec`.
1282///
1283/// Tries an exact declared name first. When that fails and `resolve_unit` is
1284/// provided, resolves through the unit index and checks the bare name is declared
1285/// on the element.
1286pub fn resolve_endpoint_unit_token(
1287    token: &str,
1288    element_spec: &TypeSpecification,
1289    resolve_unit: Option<&UnitTokenResolver<'_>>,
1290) -> Result<String, String> {
1291    match bare_declared_unit_on_element(element_spec, token) {
1292        Ok(bare) => Ok(bare),
1293        Err(declared_err) => {
1294            let Some(resolve) = resolve_unit else {
1295                return Err(declared_err);
1296            };
1297            let bare = resolve.resolve(token)?;
1298            bare_declared_unit_on_element(element_spec, &bare).map_err(|_| {
1299                format!("Unit '{token}' resolves to '{bare}', which is not declared on this type")
1300            })
1301        }
1302    }
1303}
1304
1305/// Lift a parser literal range endpoint to a [`TypedLiteral`] with the element's primitive type.
1306/// Routes [`Value::NumberWithUnit`] through [`parser_value_to_value_kind`] so ratio endpoints
1307/// (e.g. `10%` in a `ratio range`) canonicalize to ratios, not anonymous quantities.
1308///
1309/// When `resolve_unit` is set, qualified tokens are resolved to bare declared names before bind.
1310pub(crate) fn lift_range_endpoint(
1311    value: &crate::parsing::ast::Value,
1312    element_spec: &TypeSpecification,
1313    resolve_unit: Option<&UnitTokenResolver<'_>>,
1314) -> Result<TypedLiteral, String> {
1315    use crate::parsing::ast::Value;
1316    match value {
1317        Value::NumberWithUnit(magnitude, unit_token) => {
1318            let bare = resolve_endpoint_unit_token(unit_token, element_spec, resolve_unit)?;
1319            let kind = number_with_unit_value_kind(*magnitude, &bare, element_spec)?;
1320            let lemma_type = match &kind {
1321                ValueKind::Measure(_) | ValueKind::Ratio(_) => {
1322                    Arc::new(LemmaType::primitive(element_spec.clone()).with_unit(bare))
1323                }
1324                _ => Arc::new(LemmaType::primitive(element_spec.clone())),
1325            };
1326            Ok(TypedLiteral {
1327                value: kind,
1328                lemma_type,
1329            })
1330        }
1331        _ => {
1332            let kind = value_to_semantic(value)?;
1333            Ok(TypedLiteral {
1334                value: kind,
1335                lemma_type: Arc::new(LemmaType::primitive(element_spec.clone())),
1336            })
1337        }
1338    }
1339}
1340
1341fn literal_value_from_parser_value(
1342    value: &crate::parsing::ast::Value,
1343) -> Result<TypedLiteral, String> {
1344    use crate::parsing::ast::Value;
1345
1346    enum Step<'a> {
1347        Visit(&'a Value),
1348        CombineRange,
1349    }
1350
1351    let mut steps = vec![Step::Visit(value)];
1352    let mut literals: Vec<TypedLiteral> = Vec::new();
1353    while let Some(step) = steps.pop() {
1354        match step {
1355            Step::Visit(Value::Range(left, right)) => {
1356                steps.push(Step::CombineRange);
1357                steps.push(Step::Visit(right));
1358                steps.push(Step::Visit(left));
1359            }
1360            Step::Visit(scalar) => literals.push(scalar_literal_from_parser_value(scalar)?),
1361            Step::CombineRange => {
1362                let right = literals
1363                    .pop()
1364                    .expect("BUG: range right endpoint missing from literal stack");
1365                let left = literals
1366                    .pop()
1367                    .expect("BUG: range left endpoint missing from literal stack");
1368                let compatible = match (
1369                    &left.lemma_type.specifications,
1370                    &right.lemma_type.specifications,
1371                ) {
1372                    (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
1373                    (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
1374                    (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
1375                    (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
1376                        left.lemma_type.same_measure_family(&right.lemma_type)
1377                            || left
1378                                .lemma_type
1379                                .compatible_with_anonymous_measure(&right.lemma_type)
1380                            || right
1381                                .lemma_type
1382                                .compatible_with_anonymous_measure(&left.lemma_type)
1383                    }
1384                    (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
1385                    _ => false,
1386                };
1387                if !compatible {
1388                    return Err(format!(
1389                        "range endpoints must have the same supported base type, got {} and {}",
1390                        left.lemma_type.name(),
1391                        right.lemma_type.name()
1392                    ));
1393                }
1394                literals.push(TypedLiteral::range(left, right));
1395            }
1396        }
1397    }
1398    Ok(literals.pop().expect("BUG: literal walk left no result"))
1399}
1400
1401fn scalar_literal_from_parser_value(
1402    value: &crate::parsing::ast::Value,
1403) -> Result<TypedLiteral, String> {
1404    use crate::parsing::ast::Value;
1405
1406    match value {
1407        Value::Number(n) => Ok(TypedLiteral::number(lift_parser_decimal(*n)?)),
1408        Value::Text(s) => Ok(TypedLiteral::text(s.clone())),
1409        Value::Date(dt) => Ok(TypedLiteral::date(date_time_to_semantic(dt))),
1410        Value::Time(t) => Ok(TypedLiteral::time(time_to_semantic(t))),
1411        Value::Boolean(b) => Ok(TypedLiteral::from_bool(bool::from(*b))),
1412        Value::NumberWithUnit(n, _) => Ok(TypedLiteral::number_interpreted_as_measure(
1413            lift_parser_decimal(*n)?,
1414        )),
1415        Value::Range(_, _) => {
1416            unreachable!("BUG: range values are walked by literal_value_from_parser_value")
1417        }
1418    }
1419}
1420
1421/// Cast a [`RationalInteger`] to `u8`, requiring it to be a non-negative whole number that fits.
1422fn decimal_to_u8(d: RationalInteger, ctx: &str) -> Result<u8, String> {
1423    if !d.is_integer() {
1424        return Err(format!(
1425            "{} requires a whole number, got fractional value",
1426            ctx
1427        ));
1428    }
1429    d.numer_to_u8()
1430        .ok_or_else(|| format!("{} value out of range for u8", ctx))
1431}
1432
1433/// Cast a [`RationalInteger`] to `usize`, requiring it to be a non-negative whole number that fits.
1434fn decimal_to_usize(d: RationalInteger, ctx: &str) -> Result<usize, String> {
1435    if !d.is_integer() {
1436        return Err(format!(
1437            "{} requires a whole number, got fractional value",
1438            ctx
1439        ));
1440    }
1441    d.numer_to_usize()
1442        .ok_or_else(|| format!("{} value out of range for usize", ctx))
1443}
1444
1445/// Extract a number literal from a [`Value::Number`] arg and lift it to [`RationalInteger`].
1446///
1447/// Numeric meta-constraints (`decimals`, `length`, `minimum`/`maximum`
1448/// on `Number` and `Measure`) take a bare number literal — not a ratio, not a measure. Reject
1449/// any other variant to honour the no-coercion contract.
1450fn ratio_bound_to_canonical_rational(
1451    args: &[CommandArg],
1452    cmd: &str,
1453    units: &RatioUnits,
1454) -> Result<RationalInteger, String> {
1455    use crate::computation::rational::{checked_div, decimal_to_rational};
1456    let lit = require_literal(args, cmd)?;
1457    match lit {
1458        crate::literals::Value::NumberWithUnit(magnitude, unit_name) => {
1459            let unit = units.get(unit_name.as_str())?;
1460            let magnitude_rational = decimal_to_rational(*magnitude)
1461                .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1462            checked_div(&magnitude_rational, &unit.value)
1463                .map_err(|failure| format!("{cmd}: unit conversion failed: {failure}"))
1464        }
1465        other => Err(format!(
1466            "{cmd} requires a ratio literal with a unit, got {}",
1467            value_kind_name(other)
1468        )),
1469    }
1470}
1471
1472fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<RationalInteger, String> {
1473    use crate::computation::rational::decimal_to_rational;
1474    match require_literal(args, cmd)? {
1475        crate::literals::Value::Number(d) => decimal_to_rational(*d)
1476            .map_err(|failure| format!("{} literal failed rational lift: {}", cmd, failure)),
1477        other => Err(format!(
1478            "{} requires a number literal, got {}",
1479            cmd,
1480            value_kind_name(other)
1481        )),
1482    }
1483}
1484
1485enum UnitConstraintField {
1486    Minimum,
1487    Maximum,
1488    SuggestionMagnitude,
1489}
1490
1491pub(crate) fn measure_declared_bound_to_canonical(
1492    magnitude: &RationalInteger,
1493    unit_name: &str,
1494    units: &MeasureUnits,
1495    type_name: &str,
1496    command: &str,
1497) -> Result<RationalInteger, String> {
1498    use crate::computation::rational::checked_mul;
1499    let unit = units.get(unit_name).map_err(|_| {
1500        format!(
1501            "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1502            format_measure_units_list(units)
1503        )
1504    })?;
1505    checked_mul(magnitude, &unit.factor)
1506        .map_err(|failure| format!("{command}: unit conversion overflow: {failure}"))
1507}
1508
1509fn parse_measure_declared_bound(
1510    args: &[CommandArg],
1511    cmd: &str,
1512    units: &MeasureUnits,
1513    type_name: &str,
1514) -> Result<(RationalInteger, String), String> {
1515    use crate::computation::rational::decimal_to_rational;
1516    let lit = require_literal(args, cmd)?;
1517    let (magnitude, unit_name) = match lit {
1518        crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
1519        other => {
1520            return Err(format!(
1521                "{cmd} requires a measure literal with a unit, got {}",
1522                value_kind_name(other)
1523            ));
1524        }
1525    };
1526    units.get(unit_name.as_str()).map_err(|_| {
1527        format!(
1528            "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1529            format_measure_units_list(units)
1530        )
1531    })?;
1532    let magnitude_rational = decimal_to_rational(magnitude)
1533        .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1534    Ok((magnitude_rational, unit_name))
1535}
1536
1537fn sync_measure_units_from_canonical(
1538    units: &mut MeasureUnits,
1539    canonical: &RationalInteger,
1540    field: UnitConstraintField,
1541) -> Result<(), String> {
1542    use crate::computation::rational::checked_div;
1543    for unit in &mut units.0 {
1544        let magnitude = checked_div(canonical, &unit.factor).map_err(|failure| {
1545            format!(
1546                "cannot derive per-unit constraint for unit '{}': {failure}",
1547                unit.name
1548            )
1549        })?;
1550        match field {
1551            UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1552            UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1553            UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1554        }
1555    }
1556    Ok(())
1557}
1558
1559fn sync_ratio_units_from_canonical(
1560    units: &mut RatioUnits,
1561    canonical: &RationalInteger,
1562    field: UnitConstraintField,
1563) -> Result<(), String> {
1564    use crate::computation::rational::checked_mul;
1565    for unit in &mut units.0 {
1566        let magnitude = checked_mul(canonical, &unit.value).map_err(|failure| {
1567            format!(
1568                "cannot derive per-unit constraint for ratio unit '{}': {failure}",
1569                unit.name
1570            )
1571        })?;
1572        match field {
1573            UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1574            UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1575            UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1576        }
1577    }
1578    Ok(())
1579}
1580
1581fn sync_measure_suggestion_units(
1582    units: &mut MeasureUnits,
1583    default: &ValueKind,
1584    type_name: &str,
1585) -> Result<(), String> {
1586    let ValueKind::Measure(magnitude) = default else {
1587        return Ok(());
1588    };
1589    let unit_name = units
1590        .iter()
1591        .find(|unit| unit.is_canonical_factor())
1592        .or_else(|| units.iter().next())
1593        .map(|unit| unit.name.as_str())
1594        .expect(
1595            "BUG: Measure suggestion value requires at least one declared unit on the measure type",
1596        );
1597    units.get(unit_name).map_err(|_| {
1598        format!("Suggestion unit '{unit_name}' is not defined on measure type '{type_name}'.")
1599    })?;
1600    sync_measure_units_from_canonical(units, magnitude, UnitConstraintField::SuggestionMagnitude)
1601}
1602
1603pub(crate) fn finalize_measure_unit_constraint_magnitudes(
1604    specification: &mut TypeSpecification,
1605    declared_suggestion: Option<&ValueKind>,
1606    type_name: &str,
1607) -> Result<(), String> {
1608    let TypeSpecification::Measure {
1609        minimum,
1610        maximum,
1611        units,
1612        ..
1613    } = specification
1614    else {
1615        return Ok(());
1616    };
1617
1618    if let Some(bound) = minimum.as_ref() {
1619        let canonical =
1620            measure_declared_bound_to_canonical(&bound.0, &bound.1, units, type_name, "minimum")?;
1621        sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Minimum)?;
1622    }
1623    if let Some(bound) = maximum.as_ref() {
1624        let canonical =
1625            measure_declared_bound_to_canonical(&bound.0, &bound.1, units, type_name, "maximum")?;
1626        sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Maximum)?;
1627    }
1628    if let Some(default) = declared_suggestion {
1629        sync_measure_suggestion_units(units, default, type_name)?;
1630    }
1631
1632    if minimum.is_some() {
1633        for unit in units.iter() {
1634            assert!(
1635                unit.minimum.is_some(),
1636                "BUG: type '{type_name}' has minimum but unit '{}' missing per-unit minimum after finalize",
1637                unit.name
1638            );
1639        }
1640    }
1641    if maximum.is_some() {
1642        for unit in units.iter() {
1643            assert!(
1644                unit.maximum.is_some(),
1645                "BUG: type '{type_name}' has maximum but unit '{}' missing per-unit maximum after finalize",
1646                unit.name
1647            );
1648        }
1649    }
1650    if declared_suggestion.is_some() {
1651        for unit in units.iter() {
1652            assert!(
1653                unit.suggestion_magnitude.is_some(),
1654                "BUG: type '{type_name}' has default but unit '{}' missing per-unit default after finalize",
1655                unit.name
1656            );
1657        }
1658    }
1659
1660    Ok(())
1661}
1662
1663fn sync_ratio_suggestion_units(units: &mut RatioUnits, default: &ValueKind) -> Result<(), String> {
1664    let ValueKind::Ratio(canonical) = default else {
1665        return Ok(());
1666    };
1667    sync_ratio_units_from_canonical(units, canonical, UnitConstraintField::SuggestionMagnitude)
1668}
1669
1670/// Extract an option name from a single arg.
1671///
1672/// Both `option red` (bare identifier, parsed as `Label`) and `option "red"`
1673/// (quoted text literal) are valid lemma syntax for option enumeration; the
1674/// grammar accepts either form. All other variants are rejected.
1675fn option_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
1676    match arg {
1677        CommandArg::Literal(crate::literals::Value::Text(s)) => Ok(s.clone()),
1678        CommandArg::Label(name) => Ok(name.clone()),
1679        CommandArg::Literal(other) => Err(format!(
1680            "{} requires a text literal or identifier, got {}",
1681            cmd,
1682            value_kind_name(other)
1683        )),
1684        CommandArg::UnitExpr(_) => Err(format!(
1685            "{} requires a text literal or identifier, got a unit expression",
1686            cmd
1687        )),
1688    }
1689}
1690
1691fn label_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
1692    match arg {
1693        CommandArg::Label(name) => Ok(name.clone()),
1694        CommandArg::Literal(other) => Err(format!(
1695            "{} requires an identifier, got {}",
1696            cmd,
1697            value_kind_name(other)
1698        )),
1699        CommandArg::UnitExpr(_) => Err(format!(
1700            "{} requires an identifier, got a unit expression",
1701            cmd
1702        )),
1703    }
1704}
1705
1706fn measure_trait_name(measure_trait: MeasureTrait) -> &'static str {
1707    match measure_trait {
1708        MeasureTrait::Duration => "duration",
1709        MeasureTrait::Calendar => "calendar",
1710    }
1711}
1712
1713fn parse_measure_trait(args: &[CommandArg]) -> Result<MeasureTrait, String> {
1714    if args.len() != 1 {
1715        return Err("trait requires exactly one identifier argument".to_string());
1716    }
1717    match label_name(&args[0], "trait")?
1718        .trim()
1719        .to_lowercase()
1720        .as_str()
1721    {
1722        "duration" => Ok(MeasureTrait::Duration),
1723        "calendar" => Ok(MeasureTrait::Calendar),
1724        other => Err(format!("Unknown measure trait '{}'", other)),
1725    }
1726}
1727
1728fn validate_calendar_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
1729    let month_unit = units
1730        .iter()
1731        .find(|unit| unit.name == "month")
1732        .ok_or_else(|| {
1733            "trait calendar requires a canonical 'month' unit declared before 'trait calendar'"
1734                .to_string()
1735        })?;
1736    if !month_unit.is_canonical_factor() {
1737        return Err("trait calendar requires unit month 1".to_string());
1738    }
1739    Ok(())
1740}
1741
1742fn validate_duration_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
1743    let second_unit = units
1744        .iter()
1745        .find(|unit| unit.name == "second")
1746        .ok_or_else(|| {
1747            "trait duration requires a canonical 'second' unit declared before 'trait duration'"
1748                .to_string()
1749        })?;
1750    if !second_unit.is_canonical_factor() {
1751        return Err("trait duration requires unit second 1".to_string());
1752    }
1753    Ok(())
1754}
1755
1756/// Extract a [`DateTimeValue`] from a [`Value::Date`] literal arg.
1757fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
1758    match require_literal(args, cmd)? {
1759        crate::literals::Value::Date(dt) => Ok(dt.clone()),
1760        other => Err(format!(
1761            "{} requires a date literal (e.g. 2024-01-01), got {}",
1762            cmd,
1763            value_kind_name(other)
1764        )),
1765    }
1766}
1767
1768/// Extract a [`TimeValue`] from a [`Value::Time`] literal arg.
1769fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
1770    match require_literal(args, cmd)? {
1771        crate::literals::Value::Time(t) => Ok(t.clone()),
1772        other => Err(format!(
1773            "{} requires a time literal (e.g. 12:30:00), got {}",
1774            cmd,
1775            value_kind_name(other)
1776        )),
1777    }
1778}
1779
1780/// Default `help` for a built-in primitive (goal-oriented; syntax lives in [`LemmaType::example_value`]).
1781#[must_use]
1782pub fn default_help_for_primitive(kind: PrimitiveKind) -> &'static str {
1783    use PrimitiveKind::*;
1784    match kind {
1785        Boolean => "Whether this holds (true or false).",
1786        Number => "A dimensionless number.",
1787        NumberRange => "The lower and upper bound of the number range.",
1788        Text => "A text value.",
1789        Measure => "A numeric amount in one of this type's units.",
1790        MeasureRange => "The lower and upper bound of the measure range.",
1791        Ratio => "A ratio in one of this type's units (e.g. percent).",
1792        RatioRange => "The lower and upper bound of the ratio range.",
1793        Date => "A date, or a date and time with optional timezone.",
1794        DateRange => "The start date and end date of the date range.",
1795        Time => "A time of day, with optional timezone.",
1796        TimeRange => "The start time and end time of the time range.",
1797    }
1798}
1799
1800impl TypeSpecification {
1801    pub fn boolean() -> Self {
1802        TypeSpecification::Boolean {
1803            help: default_help_for_primitive(PrimitiveKind::Boolean).to_string(),
1804        }
1805    }
1806    pub fn measure() -> Self {
1807        TypeSpecification::Measure {
1808            minimum: None,
1809            maximum: None,
1810            decimals: None,
1811            units: MeasureUnits::new(),
1812            traits: Vec::new(),
1813            decomposition: None,
1814            unit: None,
1815            help: default_help_for_primitive(PrimitiveKind::Measure).to_string(),
1816        }
1817    }
1818    pub fn number() -> Self {
1819        TypeSpecification::Number {
1820            minimum: None,
1821            maximum: None,
1822            decimals: None,
1823            help: default_help_for_primitive(PrimitiveKind::Number).to_string(),
1824        }
1825    }
1826    pub fn number_range() -> Self {
1827        TypeSpecification::NumberRange {
1828            lower: None,
1829            upper: None,
1830            minimum: None,
1831            maximum: None,
1832            help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
1833        }
1834    }
1835    pub fn ratio() -> Self {
1836        TypeSpecification::Ratio {
1837            minimum: None,
1838            maximum: None,
1839            decimals: None,
1840            units: RatioUnits(vec![
1841                RatioUnit {
1842                    name: "percent".to_string(),
1843                    value: crate::computation::rational::rational_new(100, 1),
1844                    minimum: None,
1845                    maximum: None,
1846                    suggestion_magnitude: None,
1847                },
1848                RatioUnit {
1849                    name: "permille".to_string(),
1850                    value: crate::computation::rational::rational_new(1000, 1),
1851                    minimum: None,
1852                    maximum: None,
1853                    suggestion_magnitude: None,
1854                },
1855            ]),
1856            unit: None,
1857            help: default_help_for_primitive(PrimitiveKind::Ratio).to_string(),
1858        }
1859    }
1860    pub fn ratio_range() -> Self {
1861        TypeSpecification::RatioRange {
1862            lower: None,
1863            upper: None,
1864            minimum: None,
1865            maximum: None,
1866            units: match TypeSpecification::ratio() {
1867                TypeSpecification::Ratio { units, .. } => units,
1868                _ => unreachable!("BUG: ratio constructor must return a ratio type"),
1869            },
1870            help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
1871        }
1872    }
1873    pub fn text() -> Self {
1874        TypeSpecification::Text {
1875            length: None,
1876            options: vec![],
1877            help: default_help_for_primitive(PrimitiveKind::Text).to_string(),
1878        }
1879    }
1880    pub fn date() -> Self {
1881        TypeSpecification::Date {
1882            minimum: None,
1883            maximum: None,
1884            help: default_help_for_primitive(PrimitiveKind::Date).to_string(),
1885        }
1886    }
1887    pub fn date_range() -> Self {
1888        TypeSpecification::DateRange {
1889            lower: None,
1890            upper: None,
1891            minimum: None,
1892            maximum: None,
1893            help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
1894        }
1895    }
1896    pub fn time() -> Self {
1897        TypeSpecification::Time {
1898            minimum: None,
1899            maximum: None,
1900            help: default_help_for_primitive(PrimitiveKind::Time).to_string(),
1901        }
1902    }
1903    pub fn time_range() -> Self {
1904        TypeSpecification::TimeRange {
1905            lower: None,
1906            upper: None,
1907            minimum: None,
1908            maximum: None,
1909            help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
1910        }
1911    }
1912    pub fn measure_range() -> Self {
1913        TypeSpecification::MeasureRange {
1914            lower: None,
1915            upper: None,
1916            minimum: None,
1917            maximum: None,
1918            units: MeasureUnits::new(),
1919            decomposition: None,
1920            help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
1921        }
1922    }
1923
1924    /// Element spec for a range type (e.g. `MeasureRange` → `Measure`).
1925    #[must_use]
1926    pub fn element_from_range(&self) -> Option<Self> {
1927        match self {
1928            TypeSpecification::NumberRange { lower, upper, .. } => {
1929                Some(TypeSpecification::Number {
1930                    minimum: lower.clone(),
1931                    maximum: upper.clone(),
1932                    decimals: None,
1933                    help: String::new(),
1934                })
1935            }
1936            TypeSpecification::MeasureRange {
1937                lower,
1938                upper,
1939                units,
1940                decomposition,
1941                ..
1942            } => Some(TypeSpecification::Measure {
1943                minimum: lower.clone(),
1944                maximum: upper.clone(),
1945                decimals: None,
1946                units: units.clone(),
1947                traits: Vec::new(),
1948                decomposition: decomposition.clone(),
1949                unit: None,
1950                help: String::new(),
1951            }),
1952            TypeSpecification::DateRange { lower, upper, .. } => Some(TypeSpecification::Date {
1953                minimum: lower.clone(),
1954                maximum: upper.clone(),
1955                help: String::new(),
1956            }),
1957            TypeSpecification::TimeRange { lower, upper, .. } => Some(TypeSpecification::Time {
1958                minimum: lower.clone(),
1959                maximum: upper.clone(),
1960                help: String::new(),
1961            }),
1962            TypeSpecification::RatioRange {
1963                lower,
1964                upper,
1965                units,
1966                ..
1967            } => Some(TypeSpecification::Ratio {
1968                minimum: lower.clone(),
1969                maximum: upper.clone(),
1970                decimals: None,
1971                units: units.clone(),
1972                unit: None,
1973                help: String::new(),
1974            }),
1975            _ => None,
1976        }
1977    }
1978
1979    /// Range spec for an element type (e.g. `Measure` → `MeasureRange`).
1980    #[must_use]
1981    pub fn range_from_element(&self) -> Option<Self> {
1982        match self {
1983            TypeSpecification::Number {
1984                minimum, maximum, ..
1985            } => Some(TypeSpecification::NumberRange {
1986                lower: minimum.clone(),
1987                upper: maximum.clone(),
1988                minimum: None,
1989                maximum: None,
1990                help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
1991            }),
1992            TypeSpecification::Measure {
1993                minimum,
1994                maximum,
1995                units,
1996                decomposition,
1997                ..
1998            } => Some(TypeSpecification::MeasureRange {
1999                lower: minimum.clone(),
2000                upper: maximum.clone(),
2001                minimum: None,
2002                maximum: None,
2003                units: units.clone(),
2004                decomposition: decomposition.clone(),
2005                help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
2006            }),
2007            TypeSpecification::Date {
2008                minimum, maximum, ..
2009            } => Some(TypeSpecification::DateRange {
2010                lower: minimum.clone(),
2011                upper: maximum.clone(),
2012                minimum: None,
2013                maximum: None,
2014                help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
2015            }),
2016            TypeSpecification::Time {
2017                minimum, maximum, ..
2018            } => Some(TypeSpecification::TimeRange {
2019                lower: minimum.clone(),
2020                upper: maximum.clone(),
2021                minimum: None,
2022                maximum: None,
2023                help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
2024            }),
2025            TypeSpecification::Ratio {
2026                minimum,
2027                maximum,
2028                units,
2029                ..
2030            } => Some(TypeSpecification::RatioRange {
2031                lower: minimum.clone(),
2032                upper: maximum.clone(),
2033                minimum: None,
2034                maximum: None,
2035                units: units.clone(),
2036                help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
2037            }),
2038            _ => None,
2039        }
2040    }
2041
2042    /// Minimum bound as decimal for interactive numeric prompts (number, measure, ratio).
2043    #[must_use]
2044    pub fn minimum_decimal(&self) -> Option<Decimal> {
2045        match self {
2046            TypeSpecification::Number { minimum, .. }
2047            | TypeSpecification::Ratio { minimum, .. } => minimum.as_ref().map(|bound| {
2048                bound
2049                    .try_to_decimal()
2050                    .expect("BUG: planned minimum must convert to decimal")
2051            }),
2052            TypeSpecification::Measure { minimum, .. } => minimum.as_ref().map(|(bound, _)| {
2053                bound
2054                    .try_to_decimal()
2055                    .expect("BUG: planned minimum must convert to decimal")
2056            }),
2057            _ => None,
2058        }
2059    }
2060
2061    /// Maximum bound as decimal for interactive numeric prompts (number, measure, ratio).
2062    #[must_use]
2063    pub fn maximum_decimal(&self) -> Option<Decimal> {
2064        match self {
2065            TypeSpecification::Number { maximum, .. }
2066            | TypeSpecification::Ratio { maximum, .. } => maximum.as_ref().map(|bound| {
2067                bound
2068                    .try_to_decimal()
2069                    .expect("BUG: planned maximum must convert to decimal")
2070            }),
2071            TypeSpecification::Measure { maximum, .. } => maximum.as_ref().map(|(bound, _)| {
2072                bound
2073                    .try_to_decimal()
2074                    .expect("BUG: planned maximum must convert to decimal")
2075            }),
2076            _ => None,
2077        }
2078    }
2079
2080    pub fn veto() -> Self {
2081        TypeSpecification::Veto { message: None }
2082    }
2083
2084    /// Apply a single constraint command to this spec.
2085    ///
2086    /// The `declared_suggestion` and `declared_value` out-parameters receive default values
2087    /// (if the command is `Suggest` or `With`), encoded as [`RawSuggestion`]. Defaults are
2088    /// owned by the data binding or typedef entry, not by the type specification itself;
2089    /// callers thread `&mut Option<RawSuggestion>` for each across constraint applications
2090    /// for one declaration. Duplicate `-> suggest` / `-> with` / `minimum` / `maximum` /
2091    /// `decimals` on the same declaration are rejected by the caller seen-set before this
2092    /// runs. A child typedef may override an inherited suggest, with, or bound with one
2093    /// command of that kind. Measure scalars stay raw until unit factors are resolved;
2094    /// callers convert via [`bound_value_kind_from_raw_suggestion`].
2095    pub fn apply_constraint(
2096        &mut self,
2097        type_name: &str,
2098        command: TypeConstraintCommand,
2099        args: &[CommandArg],
2100        declared_suggestion: &mut Option<RawSuggestion>,
2101        declared_value: &mut Option<RawSuggestion>,
2102        resolve_unit: Option<&UnitTokenResolver<'_>>,
2103    ) -> Result<(), String> {
2104        if command == TypeConstraintCommand::With
2105            && args.len() > 1
2106            && !matches!(self, TypeSpecification::List { .. })
2107        {
2108            return Err(format!("`-> with` on {self} takes exactly one value"));
2109        }
2110        if command == TypeConstraintCommand::Trait
2111            && !matches!(&self, TypeSpecification::Measure { .. })
2112        {
2113            return Err("trait command is only valid on measure types".to_string());
2114        }
2115        match self {
2116            TypeSpecification::Boolean { help } => match command {
2117                TypeConstraintCommand::Help => {
2118                    apply_type_help_command(help, args)?;
2119                }
2120                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2121                    let (target, cmd) = match command {
2122                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2123                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
2124                        _ => unreachable!("BUG: only Suggest or With in this arm"),
2125                    };
2126                    let lit = require_literal(args, cmd)?;
2127                    reject_calendar_for_suggestion(
2128                        lit,
2129                        type_name,
2130                        SuggestionExpectation::Boolean,
2131                        None,
2132                    )?;
2133                    match lit {
2134                        crate::literals::Value::Boolean(bv) => {
2135                            *target =
2136                                Some(RawSuggestion::Value(ValueKind::Boolean(bool::from(bv))));
2137                        }
2138                        _ => {
2139                            return Err(
2140                                "Please provide true or false, for example `-> suggest true`."
2141                                    .to_string(),
2142                            );
2143                        }
2144                    }
2145                }
2146                other => {
2147                    return Err(format!(
2148                        "Invalid command '{}' for boolean type. Valid commands: help, suggest, with",
2149                        other
2150                    ));
2151                }
2152            },
2153            TypeSpecification::Measure {
2154                decimals,
2155                minimum,
2156                maximum,
2157                units,
2158                traits,
2159                help,
2160                ..
2161            } => match command {
2162                TypeConstraintCommand::Decimals => {
2163                    let d = require_decimal_literal(args, "decimals")?;
2164                    *decimals = Some(decimal_to_u8(d, "decimals")?);
2165                }
2166                TypeConstraintCommand::Unit => {
2167                    let (unit_name, value, derived_measure_factors) = match args {
2168                        [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
2169                            (name.clone(), *v, Vec::new())
2170                        }
2171                        [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
2172                            prefix,
2173                            factors,
2174                        ))] => {
2175                            let raw: Vec<(String, i32)> = factors
2176                                .iter()
2177                                .map(|f| (f.measure_ref.clone(), f.exp))
2178                                .collect();
2179                            (name.clone(), *prefix, raw)
2180                        }
2181                        _ => {
2182                            return Err(
2183                                "unit requires a unit name followed by a conversion factor or compound unit expression (e.g., 'unit eur 1.00' or 'unit mps meter/second')"
2184                                    .to_string(),
2185                            );
2186                        }
2187                    };
2188                    if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2189                        let new_factor = crate::computation::rational::decimal_to_rational(value)
2190                            .map_err(|failure| failure.to_string())?;
2191                        if existing.factor != new_factor
2192                            || existing.derived_measure_factors != derived_measure_factors
2193                        {
2194                            return Err(format!(
2195                                "Unit '{unit_name}' is already defined in this type's inherited units; \
2196                                 cannot change factor or decomposition. Add a new unit name instead."
2197                            ));
2198                        }
2199                    } else {
2200                        units.0.push(MeasureUnit::from_decimal_factor(
2201                            unit_name,
2202                            value,
2203                            derived_measure_factors,
2204                        )?);
2205                    }
2206                }
2207                TypeConstraintCommand::Trait => {
2208                    let measure_trait = parse_measure_trait(args)?;
2209                    if traits.contains(&measure_trait) {
2210                        return Err(format!(
2211                            "Duplicate trait '{}' for measure type.",
2212                            measure_trait_name(measure_trait)
2213                        ));
2214                    }
2215                    if measure_trait == MeasureTrait::Duration {
2216                        validate_duration_trait_requirements(units)?;
2217                    }
2218                    if measure_trait == MeasureTrait::Calendar {
2219                        validate_calendar_trait_requirements(units)?;
2220                    }
2221                    traits.push(measure_trait);
2222                }
2223                TypeConstraintCommand::Minimum => {
2224                    *minimum = Some(parse_measure_declared_bound(
2225                        args, "minimum", units, type_name,
2226                    )?);
2227                }
2228                TypeConstraintCommand::Maximum => {
2229                    *maximum = Some(parse_measure_declared_bound(
2230                        args, "maximum", units, type_name,
2231                    )?);
2232                }
2233                TypeConstraintCommand::Help => {
2234                    apply_type_help_command(help, args)?;
2235                }
2236                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2237                    let (target, cmd) = match command {
2238                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2239                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
2240                        _ => unreachable!("BUG: only Suggest or With in this arm"),
2241                    };
2242                    let lit = require_literal(args, cmd)?;
2243                    if !traits.contains(&MeasureTrait::Calendar) {
2244                        reject_calendar_for_suggestion(
2245                            lit,
2246                            type_name,
2247                            SuggestionExpectation::MeasureUnits,
2248                            Some(units),
2249                        )?;
2250                    }
2251                    match lit {
2252                        crate::literals::Value::NumberWithUnit(_, _) => {
2253                            let (magnitude, unit_name) =
2254                                parse_measure_declared_bound(args, cmd, units, type_name)?;
2255                            *target = Some(RawSuggestion::Measure {
2256                                magnitude,
2257                                unit_name,
2258                            });
2259                        }
2260                        _ => {
2261                            return Err(measure_suggestion_wrong_shape_error(type_name, traits));
2262                        }
2263                    }
2264                }
2265                _ => {
2266                    return Err(format!(
2267                        "Invalid command '{}' for measure type. Valid commands: unit, trait, minimum, maximum, decimals, help, suggest, with",
2268                        command
2269                    ));
2270                }
2271            },
2272            TypeSpecification::Number {
2273                decimals,
2274                minimum,
2275                maximum,
2276                help,
2277            } => match command {
2278                TypeConstraintCommand::Decimals => {
2279                    let d = require_decimal_literal(args, "decimals")?;
2280                    *decimals = Some(decimal_to_u8(d, "decimals")?);
2281                }
2282                TypeConstraintCommand::Unit => {
2283                    return Err(
2284                        "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'measure' type instead.".to_string()
2285                    );
2286                }
2287                TypeConstraintCommand::Minimum => {
2288                    *minimum = Some(require_decimal_literal(args, "minimum")?);
2289                }
2290                TypeConstraintCommand::Maximum => {
2291                    *maximum = Some(require_decimal_literal(args, "maximum")?);
2292                }
2293                TypeConstraintCommand::Help => {
2294                    apply_type_help_command(help, args)?;
2295                }
2296                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2297                    let (target, cmd) = match command {
2298                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2299                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
2300                        _ => unreachable!("BUG: only Suggest or With in this arm"),
2301                    };
2302                    let lit = require_literal(args, cmd)?;
2303                    reject_calendar_for_suggestion(
2304                        lit,
2305                        type_name,
2306                        SuggestionExpectation::Number,
2307                        None,
2308                    )?;
2309                    match lit {
2310                        crate::literals::Value::Number(d) => {
2311                            *target = Some(RawSuggestion::Value(ValueKind::Number(
2312                                lift_parser_decimal(*d)?,
2313                            )));
2314                        }
2315                        _ => {
2316                            return Err(
2317                                "Please provide a number, for example `-> suggest 42`.".to_string()
2318                            );
2319                        }
2320                    }
2321                }
2322                _ => {
2323                    return Err(format!(
2324                        "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, suggest, with",
2325                        command
2326                    ));
2327                }
2328            },
2329            TypeSpecification::NumberRange {
2330                lower,
2331                upper,
2332                minimum,
2333                maximum,
2334                help,
2335            } => match command {
2336                TypeConstraintCommand::Lower => {
2337                    *lower = Some(require_decimal_literal(args, "lower")?);
2338                }
2339                TypeConstraintCommand::Upper => {
2340                    *upper = Some(require_decimal_literal(args, "upper")?);
2341                }
2342                TypeConstraintCommand::Minimum => {
2343                    let width = require_decimal_literal(args, "minimum")?;
2344                    reject_negative_width_magnitude(&width, "minimum")?;
2345                    *minimum = Some(width);
2346                }
2347                TypeConstraintCommand::Maximum => {
2348                    let width = require_decimal_literal(args, "maximum")?;
2349                    reject_negative_width_magnitude(&width, "maximum")?;
2350                    *maximum = Some(width);
2351                }
2352                TypeConstraintCommand::Help => {
2353                    apply_type_help_command(help, args)?;
2354                }
2355                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2356                    let target = match command {
2357                        TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2358                        TypeConstraintCommand::With => &mut *declared_value,
2359                        _ => unreachable!("BUG: only Suggest or With in this arm"),
2360                    };
2361                    let (left, right) = require_suggestion_range_endpoints(
2362                        args,
2363                        type_name,
2364                        SuggestionExpectation::NumberRange,
2365                        None,
2366                    )?;
2367                    let left = literal_value_from_parser_value(left)?;
2368                    let right = literal_value_from_parser_value(right)?;
2369                    if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
2370                        return Err(
2371                            "Please provide a number range, for example `-> suggest 10...100`."
2372                                .to_string(),
2373                        );
2374                    }
2375                    *target = Some(RawSuggestion::Value(ValueKind::Range(
2376                        Box::new(left),
2377                        Box::new(right),
2378                    )));
2379                }
2380                _ => {
2381                    return Err(format!(
2382                        "Invalid command '{}' for number range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2383                        command
2384                    ));
2385                }
2386            },
2387            TypeSpecification::Ratio {
2388                decimals,
2389                minimum,
2390                maximum,
2391                units,
2392                unit: _,
2393                help,
2394            } => match command {
2395                TypeConstraintCommand::Decimals => {
2396                    let d = require_decimal_literal(args, "decimals")?;
2397                    *decimals = Some(decimal_to_u8(d, "decimals")?);
2398                }
2399                TypeConstraintCommand::Unit => {
2400                    let (unit_name, value_dec) = match args {
2401                        [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
2402                            (name.clone(), *v)
2403                        }
2404                        _ => {
2405                            return Err(
2406                                "unit requires a unit name followed by a numeric conversion factor (e.g., 'unit percent 100'). Compound unit expressions are not supported for ratio types."
2407                                    .to_string(),
2408                            );
2409                        }
2410                    };
2411                    let value = crate::computation::rational::decimal_to_rational(value_dec)
2412                        .map_err(|failure| {
2413                            format!(
2414                                "ratio unit value is not exactly representable as a rational: {}",
2415                                failure
2416                            )
2417                        })?;
2418                    if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2419                        if existing.value != value {
2420                            return Err(format!(
2421                                "Unit '{unit_name}' is already defined in this type's inherited units; \
2422                                 cannot change factor. Add a new unit name instead."
2423                            ));
2424                        }
2425                    } else {
2426                        units.0.push(RatioUnit {
2427                            name: unit_name,
2428                            value,
2429                            minimum: None,
2430                            maximum: None,
2431                            suggestion_magnitude: None,
2432                        });
2433                    }
2434                }
2435                TypeConstraintCommand::Minimum => {
2436                    let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
2437                    sync_ratio_units_from_canonical(
2438                        units,
2439                        &canonical,
2440                        UnitConstraintField::Minimum,
2441                    )?;
2442                    *minimum = Some(canonical);
2443                }
2444                TypeConstraintCommand::Maximum => {
2445                    let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
2446                    sync_ratio_units_from_canonical(
2447                        units,
2448                        &canonical,
2449                        UnitConstraintField::Maximum,
2450                    )?;
2451                    *maximum = Some(canonical);
2452                }
2453                TypeConstraintCommand::Help => {
2454                    apply_type_help_command(help, args)?;
2455                }
2456                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2457                    let (target, cmd) = match command {
2458                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2459                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
2460                        _ => unreachable!("BUG: only Suggest or With in this arm"),
2461                    };
2462                    let lit = require_literal(args, cmd)?;
2463                    reject_calendar_for_suggestion(
2464                        lit,
2465                        type_name,
2466                        SuggestionExpectation::Ratio,
2467                        None,
2468                    )?;
2469                    match lit {
2470                        crate::literals::Value::NumberWithUnit(_, unit_name) => {
2471                            let element_spec = TypeSpecification::Ratio {
2472                                decimals: *decimals,
2473                                minimum: minimum.clone(),
2474                                maximum: maximum.clone(),
2475                                units: units.clone(),
2476                                unit: None,
2477                                help: help.clone(),
2478                            };
2479                            let value = parser_value_to_value_kind(lit, &element_spec, None)?;
2480                            sync_ratio_suggestion_units(units, &value)?;
2481                            *target = Some(RawSuggestion::UnitBound {
2482                                value,
2483                                unit_name: unit_name.clone(),
2484                            });
2485                        }
2486                        other => {
2487                            return Err(format!(
2488                                "suggest requires a ratio literal with a unit, got {}. Please provide a ratio value with a unit, for example `-> suggest 25%`.",
2489                                value_kind_name(other)
2490                            ));
2491                        }
2492                    }
2493                }
2494                _ => {
2495                    return Err(format!(
2496                        "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, suggest, with",
2497                        command
2498                    ));
2499                }
2500            },
2501            TypeSpecification::RatioRange {
2502                lower,
2503                upper,
2504                minimum,
2505                maximum,
2506                units,
2507                help,
2508            } => match command {
2509                TypeConstraintCommand::Unit => {
2510                    return Err(format!(
2511                        "'unit' is not allowed on 'ratio range' type '{type_name}'. Declare units on a ratio type and write 'data x: <that type> range'."
2512                    ));
2513                }
2514                TypeConstraintCommand::Lower => {
2515                    *lower = Some(ratio_bound_to_canonical_rational(args, "lower", units)?);
2516                }
2517                TypeConstraintCommand::Upper => {
2518                    *upper = Some(ratio_bound_to_canonical_rational(args, "upper", units)?);
2519                }
2520                TypeConstraintCommand::Minimum => {
2521                    let width = ratio_bound_to_canonical_rational(args, "minimum", units)?;
2522                    reject_negative_width_magnitude(&width, "minimum")?;
2523                    *minimum = Some(width);
2524                }
2525                TypeConstraintCommand::Maximum => {
2526                    let width = ratio_bound_to_canonical_rational(args, "maximum", units)?;
2527                    reject_negative_width_magnitude(&width, "maximum")?;
2528                    *maximum = Some(width);
2529                }
2530                TypeConstraintCommand::Help => {
2531                    apply_type_help_command(help, args)?;
2532                }
2533                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2534                    let target = match command {
2535                        TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2536                        TypeConstraintCommand::With => &mut *declared_value,
2537                        _ => unreachable!("BUG: only Suggest or With in this arm"),
2538                    };
2539                    let (left, right) = require_suggestion_range_endpoints(
2540                        args,
2541                        type_name,
2542                        SuggestionExpectation::RatioRange,
2543                        None,
2544                    )?;
2545                    let element_spec = TypeSpecification::RatioRange {
2546                        lower: lower.clone(),
2547                        upper: upper.clone(),
2548                        minimum: minimum.clone(),
2549                        maximum: maximum.clone(),
2550                        units: units.clone(),
2551                        help: help.clone(),
2552                    }
2553                    .element_from_range()
2554                    .expect("BUG: RatioRange must define element_from_range");
2555                    let left = lift_range_endpoint(left, &element_spec, resolve_unit)?;
2556                    let right = lift_range_endpoint(right, &element_spec, resolve_unit)?;
2557                    if !left.lemma_type.is_ratio() || !right.lemma_type.is_ratio() {
2558                        return Err(
2559                            "Please provide a ratio range, for example `-> suggest 10%...50%`."
2560                                .to_string(),
2561                        );
2562                    }
2563                    *target = Some(RawSuggestion::Value(ValueKind::Range(
2564                        Box::new(left),
2565                        Box::new(right),
2566                    )));
2567                }
2568                _ => {
2569                    return Err(format!(
2570                        "Invalid command '{}' for ratio range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2571                        command
2572                    ));
2573                }
2574            },
2575            TypeSpecification::Text {
2576                length,
2577                options,
2578                help,
2579            } => match command {
2580                TypeConstraintCommand::Option => {
2581                    if args.len() != 1 {
2582                        return Err("option takes exactly one argument".to_string());
2583                    }
2584                    options.push(option_name(&args[0], "option")?);
2585                }
2586                TypeConstraintCommand::Options => {
2587                    let mut collected = Vec::with_capacity(args.len());
2588                    for arg in args {
2589                        collected.push(option_name(arg, "options")?);
2590                    }
2591                    *options = collected;
2592                }
2593                TypeConstraintCommand::Length => {
2594                    let d = require_decimal_literal(args, "length")?;
2595                    *length = Some(decimal_to_usize(d, "length")?);
2596                }
2597                TypeConstraintCommand::Help => {
2598                    apply_type_help_command(help, args)?;
2599                }
2600                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2601                    let (target, cmd) = match command {
2602                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2603                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
2604                        _ => unreachable!("BUG: only Suggest or With in this arm"),
2605                    };
2606                    let lit = require_literal(args, cmd)?;
2607                    reject_calendar_for_suggestion(
2608                        lit,
2609                        type_name,
2610                        SuggestionExpectation::Text,
2611                        None,
2612                    )?;
2613                    match lit {
2614                        crate::literals::Value::Text(s) => {
2615                            *target = Some(RawSuggestion::Value(ValueKind::Text(s.clone())));
2616                        }
2617                        _ => {
2618                            return Err(
2619                                "Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
2620                                    .to_string(),
2621                            );
2622                        }
2623                    }
2624                }
2625                _ => {
2626                    return Err(format!(
2627                        "Invalid command '{}' for text type. Valid commands: options, length, help, suggest, with",
2628                        command
2629                    ));
2630                }
2631            },
2632            TypeSpecification::Date {
2633                minimum,
2634                maximum,
2635                help,
2636            } => match command {
2637                TypeConstraintCommand::Minimum => {
2638                    let dt = require_date_literal(args, "minimum")?;
2639                    *minimum = Some(dt);
2640                }
2641                TypeConstraintCommand::Maximum => {
2642                    let dt = require_date_literal(args, "maximum")?;
2643                    *maximum = Some(dt);
2644                }
2645                TypeConstraintCommand::Help => {
2646                    apply_type_help_command(help, args)?;
2647                }
2648                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2649                    let (target, cmd) = match command {
2650                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2651                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
2652                        _ => unreachable!("BUG: only Suggest or With in this arm"),
2653                    };
2654                    let lit = require_literal(args, cmd)?;
2655                    reject_calendar_for_suggestion(
2656                        lit,
2657                        type_name,
2658                        SuggestionExpectation::Date,
2659                        None,
2660                    )?;
2661                    match lit {
2662                        crate::literals::Value::Date(dt) => {
2663                            *target = Some(RawSuggestion::Value(ValueKind::Date(
2664                                date_time_to_semantic(dt),
2665                            )));
2666                        }
2667                        _ => {
2668                            return Err(
2669                                "Please provide a date, for example `-> suggest 2024-06-15`."
2670                                    .to_string(),
2671                            );
2672                        }
2673                    }
2674                }
2675                _ => {
2676                    return Err(format!(
2677                        "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, suggest, with",
2678                        command
2679                    ));
2680                }
2681            },
2682            TypeSpecification::DateRange {
2683                lower,
2684                upper,
2685                minimum,
2686                maximum,
2687                help,
2688            } => match command {
2689                TypeConstraintCommand::Lower => {
2690                    *lower = Some(require_date_literal(args, "lower")?);
2691                }
2692                TypeConstraintCommand::Upper => {
2693                    *upper = Some(require_date_literal(args, "upper")?);
2694                }
2695                TypeConstraintCommand::Minimum => {
2696                    *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2697                }
2698                TypeConstraintCommand::Maximum => {
2699                    *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2700                }
2701                TypeConstraintCommand::Help => {
2702                    apply_type_help_command(help, args)?;
2703                }
2704                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2705                    let target = match command {
2706                        TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2707                        TypeConstraintCommand::With => &mut *declared_value,
2708                        _ => unreachable!("BUG: only Suggest or With in this arm"),
2709                    };
2710                    let (left, right) = require_suggestion_range_endpoints(
2711                        args,
2712                        type_name,
2713                        SuggestionExpectation::DateRange,
2714                        None,
2715                    )?;
2716                    let left = literal_value_from_parser_value(left)?;
2717                    let right = literal_value_from_parser_value(right)?;
2718                    if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
2719                        return Err(
2720                            "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
2721                                .to_string(),
2722                        );
2723                    }
2724                    *target = Some(RawSuggestion::Value(ValueKind::Range(
2725                        Box::new(left),
2726                        Box::new(right),
2727                    )));
2728                }
2729                _ => {
2730                    return Err(format!(
2731                        "Invalid command '{}' for date range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2732                        command
2733                    ));
2734                }
2735            },
2736            TypeSpecification::Time {
2737                minimum,
2738                maximum,
2739                help,
2740            } => match command {
2741                TypeConstraintCommand::Minimum => {
2742                    let t = require_time_literal(args, "minimum")?;
2743                    *minimum = Some(t);
2744                }
2745                TypeConstraintCommand::Maximum => {
2746                    let t = require_time_literal(args, "maximum")?;
2747                    *maximum = Some(t);
2748                }
2749                TypeConstraintCommand::Help => {
2750                    apply_type_help_command(help, args)?;
2751                }
2752                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2753                    let (target, cmd) = match command {
2754                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2755                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
2756                        _ => unreachable!("BUG: only Suggest or With in this arm"),
2757                    };
2758                    let lit = require_literal(args, cmd)?;
2759                    reject_calendar_for_suggestion(
2760                        lit,
2761                        type_name,
2762                        SuggestionExpectation::Time,
2763                        None,
2764                    )?;
2765                    match lit {
2766                        crate::literals::Value::Time(t) => {
2767                            *target =
2768                                Some(RawSuggestion::Value(ValueKind::Time(time_to_semantic(t))));
2769                        }
2770                        _ => {
2771                            return Err(
2772                                "Please provide a time, for example `-> suggest 09:00:00`."
2773                                    .to_string(),
2774                            );
2775                        }
2776                    }
2777                }
2778                _ => {
2779                    return Err(format!(
2780                        "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, suggest, with",
2781                        command
2782                    ));
2783                }
2784            },
2785            TypeSpecification::TimeRange {
2786                lower,
2787                upper,
2788                minimum,
2789                maximum,
2790                help,
2791            } => match command {
2792                TypeConstraintCommand::Lower => {
2793                    *lower = Some(require_time_literal(args, "lower")?);
2794                }
2795                TypeConstraintCommand::Upper => {
2796                    *upper = Some(require_time_literal(args, "upper")?);
2797                }
2798                TypeConstraintCommand::Minimum => {
2799                    *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2800                }
2801                TypeConstraintCommand::Maximum => {
2802                    *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2803                }
2804                TypeConstraintCommand::Help => {
2805                    apply_type_help_command(help, args)?;
2806                }
2807                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2808                    let target = match command {
2809                        TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2810                        TypeConstraintCommand::With => &mut *declared_value,
2811                        _ => unreachable!("BUG: only Suggest or With in this arm"),
2812                    };
2813                    let (left, right) = require_suggestion_range_endpoints(
2814                        args,
2815                        type_name,
2816                        SuggestionExpectation::TimeRange,
2817                        None,
2818                    )?;
2819                    let left = literal_value_from_parser_value(left)?;
2820                    let right = literal_value_from_parser_value(right)?;
2821                    if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
2822                        return Err(
2823                            "Please provide a time range, for example `-> suggest 09:00...17:00`."
2824                                .to_string(),
2825                        );
2826                    }
2827                    *target = Some(RawSuggestion::Value(ValueKind::Range(
2828                        Box::new(left),
2829                        Box::new(right),
2830                    )));
2831                }
2832                _ => {
2833                    return Err(format!(
2834                        "Invalid command '{}' for time range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2835                        command
2836                    ));
2837                }
2838            },
2839            TypeSpecification::MeasureRange {
2840                lower,
2841                upper,
2842                minimum,
2843                maximum,
2844                units,
2845                decomposition,
2846                help,
2847            } => match command {
2848                TypeConstraintCommand::Unit => {
2849                    return Err(format!(
2850                        "'unit' is not allowed on 'measure range' type '{type_name}'. Declare units on a measure type and write 'data x: <that type> range'."
2851                    ));
2852                }
2853                TypeConstraintCommand::Lower => {
2854                    *lower = Some(parse_measure_declared_bound(
2855                        args, "lower", units, type_name,
2856                    )?);
2857                }
2858                TypeConstraintCommand::Upper => {
2859                    *upper = Some(parse_measure_declared_bound(
2860                        args, "upper", units, type_name,
2861                    )?);
2862                }
2863                TypeConstraintCommand::Minimum => {
2864                    let width = parse_measure_declared_bound(args, "minimum", units, type_name)?;
2865                    reject_negative_width_magnitude(&width.0, "minimum")?;
2866                    *minimum = Some(width);
2867                }
2868                TypeConstraintCommand::Maximum => {
2869                    let width = parse_measure_declared_bound(args, "maximum", units, type_name)?;
2870                    reject_negative_width_magnitude(&width.0, "maximum")?;
2871                    *maximum = Some(width);
2872                }
2873                TypeConstraintCommand::Help => {
2874                    apply_type_help_command(help, args)?;
2875                }
2876                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2877                    let target = match command {
2878                        TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2879                        TypeConstraintCommand::With => &mut *declared_value,
2880                        _ => unreachable!("BUG: only Suggest or With in this arm"),
2881                    };
2882                    let (left, right) = require_suggestion_range_endpoints(
2883                        args,
2884                        type_name,
2885                        SuggestionExpectation::MeasureRange,
2886                        Some(units),
2887                    )?;
2888                    let element_spec = TypeSpecification::MeasureRange {
2889                        lower: lower.clone(),
2890                        upper: upper.clone(),
2891                        minimum: minimum.clone(),
2892                        maximum: maximum.clone(),
2893                        units: units.clone(),
2894                        decomposition: decomposition.clone(),
2895                        help: help.clone(),
2896                    }
2897                    .element_from_range()
2898                    .expect("BUG: MeasureRange must define element_from_range");
2899                    let left = lift_range_endpoint(left, &element_spec, resolve_unit)?;
2900                    let right = lift_range_endpoint(right, &element_spec, resolve_unit)?;
2901                    if !left.lemma_type.is_measure() || !right.lemma_type.is_measure() {
2902                        return Err(format!(
2903                            "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
2904                        ));
2905                    }
2906                    *target = Some(RawSuggestion::Value(ValueKind::Range(
2907                        Box::new(left),
2908                        Box::new(right),
2909                    )));
2910                }
2911                _ => {
2912                    return Err(format!(
2913                        "Invalid command '{}' for measure range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
2914                        command
2915                    ));
2916                }
2917            },
2918            TypeSpecification::Veto { .. } => {
2919                return Err(format!(
2920                    "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
2921                    command
2922                ));
2923            }
2924            TypeSpecification::Undetermined => {
2925                return Err(format!(
2926                    "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
2927                    command
2928                ));
2929            }
2930            TypeSpecification::Spec { .. } => {
2931                return Err(format!(
2932                    "Invalid command '{command}' for spec type. A spec instance has no data constraints"
2933                ));
2934            }
2935            TypeSpecification::List { element, .. } => match command {
2936                TypeConstraintCommand::Help => {
2937                    let mut unused_suggestion = None;
2938                    let mut unused_value = None;
2939                    Arc::make_mut(element).specifications.apply_constraint(
2940                        type_name,
2941                        command,
2942                        args,
2943                        &mut unused_suggestion,
2944                        &mut unused_value,
2945                        resolve_unit,
2946                    )?;
2947                }
2948                TypeConstraintCommand::With => {
2949                    let mut rows = match declared_value.take() {
2950                        Some(RawSuggestion::List(existing)) => existing,
2951                        None => Vec::new(),
2952                        Some(other) => unreachable!(
2953                            "BUG: list declared value must be RawSuggestion::List, got {other:?}"
2954                        ),
2955                    };
2956                    for arg in args {
2957                        let mut row_value = None;
2958                        let mut unused_suggestion = None;
2959                        Arc::make_mut(element).specifications.apply_constraint(
2960                            type_name,
2961                            TypeConstraintCommand::With,
2962                            std::slice::from_ref(arg),
2963                            &mut unused_suggestion,
2964                            &mut row_value,
2965                            resolve_unit,
2966                        )?;
2967                        rows.push(
2968                            row_value.expect("BUG: -> with on a list row must produce a value"),
2969                        );
2970                    }
2971                    *declared_value = Some(RawSuggestion::List(rows));
2972                }
2973                other => {
2974                    return Err(format!(
2975                        "Invalid command '{other}' for list type. Valid commands: with, help"
2976                    ));
2977                }
2978            },
2979        }
2980        Ok(())
2981    }
2982}
2983
2984/// Parse a "number unit" string into a Measure or Ratio value according to the type.
2985/// Caller must have obtained the TypeSpecification via unit_index from the unit in the string.
2986pub fn parse_number_unit(
2987    value_str: &str,
2988    type_spec: &TypeSpecification,
2989) -> Result<crate::parsing::ast::Value, String> {
2990    use crate::literals::{NumberWithUnit, RatioLiteral};
2991    use crate::parsing::ast::Value;
2992
2993    let trimmed = value_str.trim();
2994    match type_spec {
2995        TypeSpecification::Measure { units, .. } => {
2996            if units.is_empty() {
2997                unreachable!(
2998                    "BUG: Measure type has no units; should have been validated during planning"
2999                );
3000            }
3001            match trimmed.parse::<NumberWithUnit>() {
3002                Ok(n) => {
3003                    let unit = units.get(&n.1).map_err(|e| e.to_string())?;
3004                    Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
3005                }
3006                Err(e) => {
3007                    if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
3008                        let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
3009                        let example_unit = units
3010                            .iter()
3011                            .next()
3012                            .expect("BUG: units non-empty after guard")
3013                            .name
3014                            .as_str();
3015                        Err(format!(
3016                            "Measure value must include a unit, for example: '{} {}'. Valid units: {}.",
3017                            trimmed,
3018                            example_unit,
3019                            valid.join(", ")
3020                        ))
3021                    } else {
3022                        Err(e)
3023                    }
3024                }
3025            }
3026        }
3027        TypeSpecification::Ratio { units, .. } => {
3028            if units.is_empty() {
3029                unreachable!(
3030                    "BUG: Ratio type has no units; should have been validated during planning"
3031                );
3032            }
3033            match trimmed.parse::<RatioLiteral>()? {
3034                RatioLiteral::Bare(_) => {
3035                    Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
3036                }
3037                RatioLiteral::Percent(n) => {
3038                    let unit = units.get("percent").map_err(|e| e.to_string())?;
3039                    Ok(Value::NumberWithUnit(n, unit.name.clone()))
3040                }
3041                RatioLiteral::Permille(n) => {
3042                    let unit = units.get("permille").map_err(|e| e.to_string())?;
3043                    Ok(Value::NumberWithUnit(n, unit.name.clone()))
3044                }
3045                RatioLiteral::Named { value, unit } => {
3046                    let resolved = units.get(&unit).map_err(|e| e.to_string())?;
3047                    Ok(Value::NumberWithUnit(value, resolved.name.clone()))
3048                }
3049            }
3050        }
3051        _ => Err("parse_number_unit only accepts Measure or Ratio type".to_string()),
3052    }
3053}
3054
3055/// Parse a string value according to a TypeSpecification.
3056/// Used to parse runtime user input into typed values.
3057pub fn parse_value_from_string(
3058    value_str: &str,
3059    type_spec: &TypeSpecification,
3060    source: &Source,
3061) -> Result<crate::parsing::ast::Value, Error> {
3062    match type_spec {
3063        TypeSpecification::NumberRange { .. }
3064        | TypeSpecification::MeasureRange { .. }
3065        | TypeSpecification::DateRange { .. }
3066        | TypeSpecification::TimeRange { .. }
3067        | TypeSpecification::RatioRange { .. } => {
3068            let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
3069                unreachable!(
3070                    "BUG: range_element_type_specification missing arm for known range type"
3071                )
3072            });
3073            parse_range_value_from_string(value_str, &element_spec, source)
3074        }
3075        _ => parse_scalar_value_from_string(value_str, type_spec, source),
3076    }
3077}
3078
3079fn parse_range_value_from_string(
3080    value_str: &str,
3081    element_spec: &TypeSpecification,
3082    source: &Source,
3083) -> Result<crate::parsing::ast::Value, Error> {
3084    use crate::parsing::ast::Value;
3085
3086    let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
3087
3088    let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
3089        to_err("Range value must use '...' between the two endpoints".to_string())
3090    })?;
3091    if left_str.trim().is_empty() || right_str.trim().is_empty() {
3092        return Err(to_err(
3093            "Range value must contain a non-empty left and right endpoint".to_string(),
3094        ));
3095    }
3096    let left = parse_scalar_value_from_string(left_str.trim(), element_spec, source)?;
3097    let right = parse_scalar_value_from_string(right_str.trim(), element_spec, source)?;
3098    Ok(Value::Range(Box::new(left), Box::new(right)))
3099}
3100
3101fn parse_scalar_value_from_string(
3102    value_str: &str,
3103    type_spec: &TypeSpecification,
3104    source: &Source,
3105) -> Result<crate::parsing::ast::Value, Error> {
3106    use crate::parsing::ast::Value;
3107
3108    let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
3109
3110    match type_spec {
3111        TypeSpecification::Text { .. } => value_str
3112            .parse::<crate::literals::TextLiteral>()
3113            .map(|t| Value::Text(t.0))
3114            .map_err(to_err),
3115        TypeSpecification::Number { .. } => value_str
3116            .parse::<crate::literals::NumberLiteral>()
3117            .map(|n| Value::Number(n.0))
3118            .map_err(to_err),
3119        TypeSpecification::Measure { .. } => {
3120            parse_number_unit(value_str, type_spec).map_err(to_err)
3121        }
3122        TypeSpecification::Boolean { .. } => value_str
3123            .parse::<BooleanValue>()
3124            .map(Value::Boolean)
3125            .map_err(to_err),
3126        TypeSpecification::Date { .. } => {
3127            let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
3128            Ok(Value::Date(date))
3129        }
3130        TypeSpecification::Time { .. } => {
3131            let time = value_str.parse::<TimeValue>().map_err(to_err)?;
3132            Ok(Value::Time(time))
3133        }
3134        TypeSpecification::Ratio { .. } => {
3135            parse_number_unit(value_str, type_spec).map_err(to_err)
3136        }
3137        TypeSpecification::NumberRange { .. }
3138        | TypeSpecification::MeasureRange { .. }
3139        | TypeSpecification::DateRange { .. }
3140        | TypeSpecification::TimeRange { .. }
3141        | TypeSpecification::RatioRange { .. } => {
3142            unreachable!("BUG: range types are parsed by parse_range_value_from_string")
3143        }
3144        TypeSpecification::Veto { .. } => Err(to_err(
3145            "Veto type cannot be parsed from string".to_string(),
3146        )),
3147        TypeSpecification::Undetermined => unreachable!(
3148            "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
3149        ),
3150        TypeSpecification::Spec { .. } => Err(to_err(
3151            "spec values are not evaluation-data scalars".to_string(),
3152        )),
3153        TypeSpecification::List { .. } => Err(to_err(
3154            "list values are not evaluation-data scalars".to_string(),
3155        )),
3156    }
3157}
3158
3159// -----------------------------------------------------------------------------
3160// Semantic value types (no parser dependency - used by evaluation, inversion, etc.)
3161// -----------------------------------------------------------------------------
3162
3163#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3164#[serde(rename_all = "snake_case")]
3165pub enum SemanticCalendarUnit {
3166    Month,
3167    Year,
3168}
3169
3170impl fmt::Display for SemanticCalendarUnit {
3171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3172        let s = match self {
3173            SemanticCalendarUnit::Month => "month",
3174            SemanticCalendarUnit::Year => "year",
3175        };
3176        write!(f, "{}", s)
3177    }
3178}
3179
3180pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
3181    match unit_name {
3182        "month" => SemanticCalendarUnit::Month,
3183        "year" => SemanticCalendarUnit::Year,
3184        other => unreachable!(
3185            "BUG: calendar measure signature unit must be month or year, got '{other}'"
3186        ),
3187    }
3188}
3189
3190pub fn semantic_calendar_unit_from_measure_signature(
3191    signature: &[(String, i32)],
3192) -> SemanticCalendarUnit {
3193    let unit_name = signature
3194        .first()
3195        .map(|(name, _)| name.as_str())
3196        .expect("BUG: calendar measure must carry a unit signature");
3197    semantic_calendar_unit_from_unit_name(unit_name)
3198}
3199
3200pub fn semantic_calendar_unit_from_measure_type(lemma_type: &LemmaType) -> SemanticCalendarUnit {
3201    if !lemma_type.is_calendar_like() {
3202        unreachable!(
3203            "BUG: semantic_calendar_unit_from_measure_type called on non-calendar type {}",
3204            lemma_type.name()
3205        );
3206    }
3207    let signature = lemma_type.measure_runtime_signature();
3208    if signature.is_empty() {
3209        return SemanticCalendarUnit::Month;
3210    }
3211    semantic_calendar_unit_from_measure_signature(&signature)
3212}
3213
3214/// Target type for `as` casts (semantic; used by evaluation/computation).
3215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3216pub enum SemanticConversionTarget {
3217    Type(PrimitiveKind),
3218    /// `number as eur` — construct, convert, relabel, or range-span into `unit_name`.
3219    Unit {
3220        unit_name: String,
3221        /// Measure/ratio type resolved in the spec where the conversion was written.
3222        #[serde(with = "crate::snapshot::shared::arc")]
3223        owning_type: Arc<LemmaType>,
3224    },
3225}
3226
3227impl std::hash::Hash for SemanticConversionTarget {
3228    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3229        match self {
3230            Self::Type(kind) => {
3231                0u8.hash(state);
3232                kind.hash(state);
3233            }
3234            Self::Unit {
3235                unit_name,
3236                owning_type,
3237            } => {
3238                1u8.hash(state);
3239                unit_name.hash(state);
3240                owning_type.hash(state);
3241            }
3242        }
3243    }
3244}
3245
3246impl SemanticConversionTarget {}
3247
3248impl fmt::Display for SemanticConversionTarget {
3249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3250        match self {
3251            SemanticConversionTarget::Type(kind) => write!(f, "{kind}"),
3252            SemanticConversionTarget::Unit { unit_name, .. } => write!(f, "{unit_name}"),
3253        }
3254    }
3255}
3256
3257/// Timezone for semantic date/time values: UTC offset in signed whole minutes, same
3258/// representation and bounds as [`TimezoneValue`].
3259#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3260pub struct SemanticTimezone {
3261    pub offset_minutes: i16,
3262}
3263
3264impl SemanticTimezone {
3265    /// `None` when `seconds` is not a whole number of minutes within ±23:59.
3266    pub fn from_offset_seconds(seconds: i32) -> Option<Self> {
3267        TimezoneValue::from_offset_seconds(seconds).map(|tz| Self::from(&tz))
3268    }
3269
3270    pub fn offset_seconds(&self) -> i32 {
3271        i32::from(self.offset_minutes) * 60
3272    }
3273}
3274
3275impl From<&TimezoneValue> for SemanticTimezone {
3276    fn from(tz: &TimezoneValue) -> Self {
3277        Self {
3278            offset_minutes: tz.offset_minutes,
3279        }
3280    }
3281}
3282
3283impl From<&SemanticTimezone> for TimezoneValue {
3284    fn from(tz: &SemanticTimezone) -> Self {
3285        Self {
3286            offset_minutes: tz.offset_minutes,
3287        }
3288    }
3289}
3290
3291impl fmt::Display for SemanticTimezone {
3292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3293        TimezoneValue::from(self).fmt(f)
3294    }
3295}
3296
3297impl Serialize for SemanticTimezone {
3298    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3299        serializer.serialize_str(&self.to_string())
3300    }
3301}
3302
3303impl<'de> Deserialize<'de> for SemanticTimezone {
3304    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3305        let s = String::deserialize(deserializer)?;
3306        Self::from_str(&s).map_err(serde::de::Error::custom)
3307    }
3308}
3309
3310impl FromStr for SemanticTimezone {
3311    type Err = String;
3312
3313    fn from_str(s: &str) -> Result<Self, Self::Err> {
3314        Ok(Self::from(&TimezoneValue::from_str(s)?))
3315    }
3316}
3317
3318/// Time-of-day for semantic values
3319#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3320pub struct SemanticTime {
3321    pub hour: u32,
3322    pub minute: u32,
3323    pub second: u32,
3324    pub microsecond: u32,
3325    pub timezone: Option<SemanticTimezone>,
3326}
3327
3328impl fmt::Display for SemanticTime {
3329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3330        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
3331        if self.microsecond != 0 {
3332            write!(f, ".{:06}", self.microsecond)?;
3333        }
3334        if let Some(timezone) = &self.timezone {
3335            write!(f, "{}", timezone)?;
3336        }
3337        Ok(())
3338    }
3339}
3340
3341impl Serialize for SemanticTime {
3342    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3343        serializer.serialize_str(&self.to_string())
3344    }
3345}
3346
3347impl<'de> Deserialize<'de> for SemanticTime {
3348    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3349        let s = String::deserialize(deserializer)?;
3350        Self::from_str(&s).map_err(serde::de::Error::custom)
3351    }
3352}
3353
3354impl FromStr for SemanticTime {
3355    type Err = String;
3356
3357    fn from_str(s: &str) -> Result<Self, Self::Err> {
3358        Ok(time_to_semantic(&TimeValue::from_str(s)?))
3359    }
3360}
3361
3362/// Date-time for semantic values
3363#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3364pub struct SemanticDateTime {
3365    pub year: i32,
3366    pub month: u32,
3367    pub day: u32,
3368    pub hour: u32,
3369    pub minute: u32,
3370    pub second: u32,
3371    pub microsecond: u32,
3372    pub timezone: Option<SemanticTimezone>,
3373}
3374
3375impl fmt::Display for SemanticDateTime {
3376    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3377        let has_time = self.hour != 0
3378            || self.minute != 0
3379            || self.second != 0
3380            || self.microsecond != 0
3381            || self.timezone.is_some();
3382        if !has_time {
3383            write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
3384        } else {
3385            write!(
3386                f,
3387                "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
3388                self.year, self.month, self.day, self.hour, self.minute, self.second
3389            )?;
3390            if self.microsecond != 0 {
3391                write!(f, ".{:06}", self.microsecond)?;
3392            }
3393            if let Some(tz) = &self.timezone {
3394                write!(f, "{}", tz)?;
3395            }
3396            Ok(())
3397        }
3398    }
3399}
3400
3401impl Serialize for SemanticDateTime {
3402    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3403        serializer.serialize_str(&self.to_string())
3404    }
3405}
3406
3407impl<'de> Deserialize<'de> for SemanticDateTime {
3408    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3409        let s = String::deserialize(deserializer)?;
3410        Self::from_str(&s).map_err(serde::de::Error::custom)
3411    }
3412}
3413
3414impl FromStr for SemanticDateTime {
3415    type Err = String;
3416
3417    fn from_str(s: &str) -> Result<Self, Self::Err> {
3418        Ok(date_time_to_semantic(&DateTimeValue::from_str(s)?))
3419    }
3420}
3421
3422/// Default captured during type constraint application, before measure unit factors are final.
3423/// Converted into [`ValueKind`] after `resolve_measure_decompositions` (or immediately for
3424/// reference-local defaults, which run after that pass).
3425#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3426pub enum RawSuggestion {
3427    Value(ValueKind),
3428    /// Measure suggest/fill: magnitude in the written unit (canonicalized later).
3429    Measure {
3430        magnitude: RationalInteger,
3431        unit_name: String,
3432    },
3433    /// Ratio (or other) suggest/fill that already holds a canonical [`ValueKind`]
3434    /// plus the written unit name for display binding.
3435    UnitBound {
3436        value: ValueKind,
3437        unit_name: String,
3438    },
3439    List(Vec<RawSuggestion>),
3440}
3441
3442/// Canonical suggestion/fill value plus the written measure unit when one was declared.
3443///
3444/// `unit` is the unit from `-> suggest 100 inr` / `-> with 5 eur`; Show
3445/// display stamps it so the one-liner matches the written unit, not the type's first unit.
3446/// Shared via [`Arc`] so value-table clones bump a refcount instead of allocating.
3447/// For ranges this stays `None`; each endpoint carries its own unit on its type.
3448#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3449pub struct BoundValueKind {
3450    pub value: ValueKind,
3451    pub unit: Option<Arc<str>>,
3452}
3453
3454impl BoundValueKind {
3455    pub fn unbound(value: ValueKind) -> Self {
3456        Self { value, unit: None }
3457    }
3458
3459    pub fn with_binding(value: ValueKind, unit: Option<Arc<str>>) -> Self {
3460        Self { value, unit }
3461    }
3462
3463    pub fn to_literal(&self) -> LiteralValue {
3464        LiteralValue {
3465            value: self.value.clone(),
3466        }
3467    }
3468
3469    /// Written-unit agreement for ± on same-family measure/ratio: equal keep; one Some keep that;
3470    /// conflict → left wins.
3471    #[must_use]
3472    pub fn agree_or_left_binding(
3473        left: Option<&Arc<str>>,
3474        right: Option<&Arc<str>>,
3475    ) -> Option<Arc<str>> {
3476        match (left, right) {
3477            (Some(a), Some(b)) if a.as_ref() == b.as_ref() => Some(Arc::clone(a)),
3478            (Some(a), Some(_)) => Some(Arc::clone(a)),
3479            (Some(a), None) => Some(Arc::clone(a)),
3480            (None, Some(b)) => Some(Arc::clone(b)),
3481            (None, None) => None,
3482        }
3483    }
3484}
3485
3486pub fn bound_value_kind_from_raw_suggestion(
3487    raw: RawSuggestion,
3488    specifications: &TypeSpecification,
3489    type_name: &str,
3490) -> Result<BoundValueKind, String> {
3491    enum Step<'a> {
3492        Visit(RawSuggestion, &'a TypeSpecification),
3493        CollectList {
3494            element: &'a Arc<LemmaType>,
3495            rows: usize,
3496        },
3497    }
3498
3499    let mut steps = vec![Step::Visit(raw, specifications)];
3500    let mut bounds: Vec<BoundValueKind> = Vec::new();
3501    while let Some(step) = steps.pop() {
3502        match step {
3503            Step::Visit(raw, specifications) => match raw {
3504                RawSuggestion::Value(vk) => bounds.push(BoundValueKind::unbound(vk)),
3505                RawSuggestion::UnitBound { value, unit_name } => bounds.push(BoundValueKind {
3506                    value,
3507                    unit: Some(Arc::from(unit_name)),
3508                }),
3509                RawSuggestion::Measure {
3510                    magnitude,
3511                    unit_name,
3512                } => {
3513                    let TypeSpecification::Measure { units, .. } = specifications else {
3514                        unreachable!(
3515                            "BUG: RawSuggestion::Measure for non-measure type '{type_name}'"
3516                        );
3517                    };
3518                    let canonical = measure_declared_bound_to_canonical(
3519                        &magnitude, &unit_name, units, type_name, "suggest",
3520                    )?;
3521                    bounds.push(BoundValueKind {
3522                        value: ValueKind::Measure(canonical),
3523                        unit: Some(Arc::from(unit_name)),
3524                    });
3525                }
3526                RawSuggestion::List(rows) => {
3527                    let TypeSpecification::List { element, .. } = specifications else {
3528                        unreachable!("BUG: RawSuggestion::List for non-list type '{type_name}'");
3529                    };
3530                    steps.push(Step::CollectList {
3531                        element,
3532                        rows: rows.len(),
3533                    });
3534                    for row in rows.into_iter().rev() {
3535                        steps.push(Step::Visit(row, &element.specifications));
3536                    }
3537                }
3538            },
3539            Step::CollectList { element, rows } => {
3540                let start = bounds
3541                    .len()
3542                    .checked_sub(rows)
3543                    .expect("BUG: list rows missing from bound stack");
3544                let converted: Vec<TypedLiteral> = bounds
3545                    .split_off(start)
3546                    .iter()
3547                    .map(|bound| typed_literal_from_bound(bound, element))
3548                    .collect();
3549                bounds.push(BoundValueKind::unbound(ValueKind::List(converted)));
3550            }
3551        }
3552    }
3553    Ok(bounds.pop().expect("BUG: bound walk left no result"))
3554}
3555
3556/// Display one-liner unit: `as` / schema > settled > fill > suggest > none (first declared).
3557///
3558/// Range schemas are skipped: endpoint units live on each endpoint type.
3559#[must_use]
3560pub fn display_binding_unit(
3561    schema: &LemmaType,
3562    settled: Option<&BoundValueKind>,
3563    fill: Option<&BoundValueKind>,
3564    suggestion: Option<&BoundValueKind>,
3565) -> Option<String> {
3566    if schema.is_measure_range() || schema.is_ratio_range() {
3567        return None;
3568    }
3569    if let Some(unit) = schema.unit() {
3570        return Some(unit.to_string());
3571    }
3572    if let Some(unit) = settled.and_then(|bound| bound.unit.as_ref()) {
3573        return Some(unit.to_string());
3574    }
3575    if let Some(unit) = fill.and_then(|bound| bound.unit.as_ref()) {
3576        return Some(unit.to_string());
3577    }
3578    suggestion
3579        .and_then(|bound| bound.unit.as_ref())
3580        .map(|unit| unit.to_string())
3581}
3582
3583/// Schema type stamped with [`display_binding_unit`] when a binding wins.
3584///
3585/// Clones only when the winning unit differs from what `schema` already carries.
3586/// Range types are returned unchanged.
3587#[must_use]
3588pub fn lemma_type_with_display_binding(
3589    schema: &LemmaType,
3590    settled: Option<&BoundValueKind>,
3591    fill: Option<&BoundValueKind>,
3592    suggestion: Option<&BoundValueKind>,
3593) -> LemmaType {
3594    if schema.is_measure_range() || schema.is_ratio_range() {
3595        return schema.clone();
3596    }
3597    match display_binding_unit(schema, settled, fill, suggestion) {
3598        Some(unit) if schema.unit() != Some(unit.as_str()) => schema.clone().with_unit(unit),
3599        Some(_) | None => schema.clone(),
3600    }
3601}
3602
3603/// Like [`lemma_type_with_display_binding`] but reuses `schema` when no override.
3604#[must_use]
3605pub fn lemma_type_arc_with_display_binding(
3606    schema: &Arc<LemmaType>,
3607    settled: Option<&BoundValueKind>,
3608    fill: Option<&BoundValueKind>,
3609    suggestion: Option<&BoundValueKind>,
3610) -> Arc<LemmaType> {
3611    if schema.is_measure_range() || schema.is_ratio_range() {
3612        return Arc::clone(schema);
3613    }
3614    match display_binding_unit(schema.as_ref(), settled, fill, suggestion) {
3615        Some(unit) if schema.unit() != Some(unit.as_str()) => {
3616            Arc::new(schema.as_ref().clone().with_unit(unit))
3617        }
3618        Some(_) | None => Arc::clone(schema),
3619    }
3620}
3621
3622/// Build a [`TypedLiteral`] from an eval bound and the plan type for that cell.
3623///
3624/// Stamps `bound.unit` onto measure/ratio types. Range values already carry
3625/// typed endpoints inside [`ValueKind::Range`].
3626#[must_use]
3627pub fn typed_literal_from_bound(
3628    bound: &BoundValueKind,
3629    lemma_type: &Arc<LemmaType>,
3630) -> TypedLiteral {
3631    let lemma_type = match (&bound.unit, &lemma_type.specifications) {
3632        (Some(unit), TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. })
3633            if lemma_type.unit() != Some(unit.as_ref()) =>
3634        {
3635            Arc::new(lemma_type.as_ref().clone().with_unit(unit.as_ref()))
3636        }
3637        _ => Arc::clone(lemma_type),
3638    };
3639    TypedLiteral {
3640        value: bound.value.clone(),
3641        lemma_type,
3642    }
3643}
3644
3645/// Value payload (shape of a literal).
3646///
3647/// Scalar variants are type-free: type lives on `NormalForm.result_type` /
3648/// `DataDefinition`. The [`ValueKind::Range`] arm is the exception — each
3649/// endpoint keeps a [`TypedLiteral`] so a mixed-unit range (`10 eur...20 usd`)
3650/// retains both written units.
3651#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3652pub enum ValueKind {
3653    Number(RationalInteger),
3654    /// Measure: magnitude in canonical (base-unit) space.
3655    ///
3656    /// At bind time the user-facing value is multiplied by `unit.factor` to produce
3657    /// the stored magnitude; API unit maps divide back. Unit/signature identity
3658    /// comes from the node/`DataDefinition` type via [`LemmaType::measure_runtime_signature`].
3659    Measure(RationalInteger),
3660    Text(String),
3661    Date(SemanticDateTime),
3662    Time(SemanticTime),
3663    Boolean(bool),
3664    /// Ratio: canonical magnitude. Display unit comes from the node/`DataDefinition` type.
3665    Ratio(RationalInteger),
3666    /// Range endpoints carry their own type (and written unit for measure/ratio).
3667    Range(Box<TypedLiteral>, Box<TypedLiteral>),
3668    /// A spec instance: a `uses` path in the consumer plan, or one row of an instance list.
3669    Spec(SpecValue),
3670    /// Ordered rows of one type. Each row keeps its own type (written unit for measures).
3671    List(Vec<TypedLiteral>),
3672}
3673
3674impl ValueKind {
3675    /// Decimal magnitude for numeric variants (number, measure, ratio).
3676    pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
3677        match self {
3678            ValueKind::Number(n) | ValueKind::Measure(n) | ValueKind::Ratio(n) => {
3679                n.try_to_decimal().map_err(|failure| failure.to_string())
3680            }
3681            other => Err(format!("expected numeric value kind, got {other}")),
3682        }
3683    }
3684
3685    /// Cheap structural byte-size estimate (no formatting, no rational→decimal).
3686    ///
3687    /// Used by resource-limit checks where the exact string length is not
3688    /// required — only an upper-bound within a constant factor.
3689    pub fn structural_byte_size(&self) -> usize {
3690        fn rational_byte_estimate(r: &RationalInteger) -> usize {
3691            let numer_bytes = (r.numer_magnitude_bits() as usize).div_ceil(8);
3692            let denom_bytes = (r.denom_magnitude_bits() as usize).div_ceil(8);
3693            numer_bytes.max(1) + denom_bytes.max(1)
3694        }
3695        match self {
3696            ValueKind::Number(r) => rational_byte_estimate(r),
3697            ValueKind::Measure(r) => rational_byte_estimate(r),
3698            ValueKind::Ratio(r) => rational_byte_estimate(r),
3699            ValueKind::Text(s) => s.len(),
3700            ValueKind::Date(_) => 30, // "2026-01-01T00:00:00+02:00" upper bound
3701            ValueKind::Time(_) => 12, // "23:59:59" upper bound
3702            ValueKind::Boolean(_) => 5, // "false"
3703            ValueKind::Range(left, right) => {
3704                left.value.structural_byte_size() + 3 + right.value.structural_byte_size()
3705            }
3706            ValueKind::Spec(SpecValue::Fixed(instance)) => instance.structural_byte_size(),
3707            ValueKind::Spec(SpecValue::Row(row)) => row
3708                .bindings
3709                .values()
3710                .map(|binding| match binding {
3711                    crate::computation::OperationResult::Value(bound) => {
3712                        bound.value.structural_byte_size()
3713                    }
3714                    crate::computation::OperationResult::Veto(_) => 0,
3715                })
3716                .sum(),
3717            ValueKind::List(rows) => rows
3718                .iter()
3719                .map(|row| row.value.structural_byte_size())
3720                .sum(),
3721        }
3722    }
3723}
3724
3725fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
3726    rational.display_str()
3727}
3728
3729fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
3730    use crate::parsing::ast::Value;
3731    match rational.try_to_decimal() {
3732        Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
3733        Err(_) => format!("{} {}", rational.display_str(), unit),
3734    }
3735}
3736
3737impl fmt::Display for ValueKind {
3738    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3739        match self {
3740            ValueKind::Number(rational) => {
3741                write!(f, "{}", format_rational_magnitude_for_display(rational))
3742            }
3743            ValueKind::Measure(rational) => {
3744                write!(f, "{}", format_rational_magnitude_for_display(rational))
3745            }
3746            ValueKind::Text(s) => write!(f, "{}", crate::parsing::ast::Value::Text(s.clone())),
3747            ValueKind::Ratio(rational) => {
3748                write!(f, "{}", format_rational_magnitude_for_display(rational))
3749            }
3750            ValueKind::Date(dt) => write!(f, "{}", dt),
3751            ValueKind::Time(t) => write!(
3752                f,
3753                "{}",
3754                crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
3755                    hour: t.hour as u8,
3756                    minute: t.minute as u8,
3757                    second: t.second as u8,
3758                    microsecond: t.microsecond,
3759                    timezone: t
3760                        .timezone
3761                        .as_ref()
3762                        .map(crate::parsing::ast::TimezoneValue::from),
3763                })
3764            ),
3765            ValueKind::Boolean(b) => write!(f, "{}", b),
3766            ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
3767            ValueKind::Spec(value) => write!(f, "{value}"),
3768            ValueKind::List(rows) => {
3769                write!(f, "list[")?;
3770                for (i, row) in rows.iter().enumerate() {
3771                    if i > 0 {
3772                        write!(f, ", ")?;
3773                    }
3774                    write!(f, "{row}")?;
3775                }
3776                write!(f, "]")
3777            }
3778        }
3779    }
3780}
3781
3782// -----------------------------------------------------------------------------
3783// Resolved path types (moved from parsing::ast)
3784// -----------------------------------------------------------------------------
3785
3786/// A single segment in a resolved path traversal
3787///
3788/// Used in both DataPath and RulePath for cross-spec traversal.
3789/// Each segment contains a `uses` alias, the resolved target repository name
3790/// (`repository`; `None` = unnamed workspace), and the resolved target spec
3791/// name (`spec`).
3792#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3793pub struct PathSegment {
3794    /// The `uses` alias for this hop (binding key component).
3795    pub uses: String,
3796    /// Target repository name from `Arc<LemmaRepository>.name` at resolve.
3797    /// `None` is the unnamed workspace. Always encoded for postcard (no skip).
3798    pub repository: Option<String>,
3799    /// The spec this hop resolves to (resolved during planning)
3800    pub spec: String,
3801}
3802
3803/// One spec instance in a consumer plan: the `uses` hops that name it.
3804///
3805/// `basic` is one segment. A nested instance is the outer hops plus its own.
3806#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3807pub struct SpecInstance {
3808    pub prefix: Vec<PathSegment>,
3809}
3810
3811impl SpecInstance {
3812    pub fn structural_byte_size(&self) -> usize {
3813        self.prefix
3814            .iter()
3815            .map(|segment| {
3816                segment.uses.len()
3817                    + segment.spec.len()
3818                    + segment.repository.as_ref().map_or(0, String::len)
3819            })
3820            .sum()
3821    }
3822}
3823
3824impl fmt::Display for SpecInstance {
3825    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3826        for (index, segment) in self.prefix.iter().enumerate() {
3827            if index > 0 {
3828                write!(f, ".")?;
3829            }
3830            write!(f, "{}", segment.uses)?;
3831        }
3832        Ok(())
3833    }
3834}
3835
3836/// Value of a spec-typed expression.
3837///
3838/// `Fixed` is a `uses` instance in the consumer plan. `Row` is one row of an
3839/// instance list; it never appears in a plan, only in a request's value table.
3840#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3841pub enum SpecValue {
3842    Fixed(SpecInstance),
3843    #[serde(skip)]
3844    Row(Arc<RowInstance>),
3845}
3846
3847impl fmt::Display for SpecValue {
3848    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3849        match self {
3850            SpecValue::Fixed(instance) => write!(f, "{instance}"),
3851            SpecValue::Row(row) => write!(f, "{}", row.origin),
3852        }
3853    }
3854}
3855
3856/// One row of an instance list: its row spec, where it came from, and its bound fields.
3857///
3858/// `bindings` are keyed by data paths of the row spec's template plan. A field
3859/// without a binding reads the template's own default or is missing.
3860///
3861/// Hashed by value (including nested self-list rows in bindings). Lookup and
3862/// Dropping a key walks its nested bindings to their depth, which
3863/// [`crate::limits::ResourceLimits::max_list_depth`] bounds before evaluation (default 32). Keying by `Arc` pointer identity would
3864/// be cheaper but only safe while every read shares the same `Arc`.
3865#[derive(Debug, Clone, PartialEq, Eq)]
3866pub struct RowInstance {
3867    pub identity: SpecIdentity,
3868    pub origin: RowOrigin,
3869    /// Declaration / caller / `-> with` field order (not `DataPath` sort).
3870    pub bindings: IndexMap<DataPath, crate::computation::OperationResult>,
3871}
3872
3873impl std::hash::Hash for RowInstance {
3874    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3875        self.identity.hash(state);
3876        self.origin.hash(state);
3877        self.bindings.len().hash(state);
3878        let mut entries: Vec<_> = self.bindings.iter().collect();
3879        entries.sort_by(|(a, _), (b, _)| a.cmp(b));
3880        for (path, result) in entries {
3881            path.hash(state);
3882            result.hash(state);
3883        }
3884    }
3885}
3886
3887/// Where a row of an instance list came from. Names missing fields and explanations.
3888#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3889pub enum RowOrigin {
3890    /// Row `number` (1-based) of list `list`: a source row or a run-data row.
3891    Numbered { list: String, number: usize },
3892    /// Row appended by an expression `-> with` as row `number` (1-based) of the result.
3893    Appended { number: usize },
3894}
3895
3896/// 1-based numbered origin of a list row (`items.2`).
3897#[must_use]
3898pub fn numbered_row_origin(list_key: &str, number: usize) -> String {
3899    format!("{list_key}.{number}")
3900}
3901
3902/// Input key of a field of a numbered list row (`items.2.weight`).
3903#[must_use]
3904pub fn row_field_input_key(list_key: &str, number: usize, field_key: &str) -> String {
3905    format!("{}.{field_key}", numbered_row_origin(list_key, number))
3906}
3907
3908impl fmt::Display for RowOrigin {
3909    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3910        match self {
3911            RowOrigin::Numbered { list, number } => {
3912                write!(f, "{}", numbered_row_origin(list, *number))
3913            }
3914            RowOrigin::Appended { number } => write!(f, "appended row {number}"),
3915        }
3916    }
3917}
3918
3919/// One evaluated instance-list row as a typed literal.
3920#[must_use]
3921pub fn row_literal(
3922    identity: SpecIdentity,
3923    origin: RowOrigin,
3924    bindings: IndexMap<DataPath, crate::computation::OperationResult>,
3925    lemma_type: Arc<LemmaType>,
3926) -> TypedLiteral {
3927    TypedLiteral {
3928        value: ValueKind::Spec(SpecValue::Row(Arc::new(RowInstance {
3929            identity,
3930            origin,
3931            bindings,
3932        }))),
3933        lemma_type,
3934    }
3935}
3936
3937/// Field of a row read through the row spec's template plan.
3938#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3939pub enum RowEnd {
3940    Data(DataPath),
3941    Rule(RulePath),
3942}
3943
3944impl fmt::Display for RowEnd {
3945    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3946        match self {
3947            RowEnd::Data(path) => write!(f, "{}", path.input_key()),
3948            RowEnd::Rule(path) => write!(f, "{}", path.input_key()),
3949        }
3950    }
3951}
3952
3953/// Where a field read on one candidate instance lands.
3954#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3955pub enum SpecMemberEnd {
3956    Rule(RulePath),
3957    Data(DataPath),
3958    /// The field is a nested `uses`. The value is that instance.
3959    Instance {
3960        instance: SpecInstance,
3961        identity: SpecIdentity,
3962    },
3963}
3964
3965/// One candidate instance of a [`ExpressionKind::SpecMember`], with the field it reads.
3966#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3967pub struct SpecMemberTarget {
3968    pub instance: SpecInstance,
3969    pub end: SpecMemberEnd,
3970}
3971
3972/// Resolved path to a data (created during planning from AST DataReference)
3973///
3974/// Represents a fully resolved path through specs to reach a datum.
3975#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3976pub struct DataPath {
3977    /// Path segments (each is a cross-spec step)
3978    pub segments: Vec<PathSegment>,
3979    /// Final data name
3980    pub data: String,
3981}
3982
3983impl DataPath {
3984    /// Create a data path from segments and data name (matches AST DataReference shape)
3985    pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
3986        Self { segments, data }
3987    }
3988
3989    /// Create a local data path (no cross-spec steps)
3990    pub fn local(data: String) -> Self {
3991        Self {
3992            segments: vec![],
3993            data,
3994        }
3995    }
3996
3997    /// Alias hops then the data name: the segments of [`Self::input_key`].
3998    pub fn input_key_segments(&self) -> impl Iterator<Item = &str> {
3999        self.segments
4000            .iter()
4001            .map(|segment| segment.uses.as_str())
4002            .chain(std::iter::once(self.data.as_str()))
4003    }
4004
4005    /// Dot-separated key used for matching user-provided data values (e.g. `"order.payment_method"`).
4006    /// Same string as [`Display`]: alias hops only, no repository or spec names.
4007    pub fn input_key(&self) -> String {
4008        let mut segments = self.input_key_segments();
4009        let mut s = segments
4010            .next()
4011            .expect("BUG: DataPath always has a data name segment")
4012            .to_string();
4013        for segment in segments {
4014            s.push('.');
4015            s.push_str(segment);
4016        }
4017        s
4018    }
4019}
4020
4021/// Resolved path to a rule (created during planning from a rule reference).
4022///
4023/// Represents a fully resolved path through specs to reach a rule.
4024#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
4025pub struct RulePath {
4026    /// Path segments (each is a cross-spec step)
4027    pub segments: Vec<PathSegment>,
4028    /// Final rule name
4029    pub rule: String,
4030}
4031
4032impl RulePath {
4033    /// Create a rule path from segments and rule name.
4034    pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
4035        Self { segments, rule }
4036    }
4037
4038    /// Dot-separated key for Show/run (`alias.rule`), twin of [`DataPath::input_key`].
4039    /// Same string as [`Display`]: alias hops only, no repository or spec names.
4040    pub fn input_key(&self) -> String {
4041        if self.segments.is_empty() {
4042            return self.rule.clone();
4043        }
4044        let mut s = String::new();
4045        for segment in &self.segments {
4046            s.push_str(&segment.uses);
4047            s.push('.');
4048        }
4049        s.push_str(&self.rule);
4050        s
4051    }
4052}
4053
4054// -----------------------------------------------------------------------------
4055// Resolved expression types (created during planning)
4056// -----------------------------------------------------------------------------
4057
4058/// Resolved expression (all references resolved to paths, all literals typed)
4059///
4060/// Created during planning from AST Expression. All unresolved references
4061/// are converted to DataPath/RulePath, and all literals are typed.
4062/// Resolved expression tree (all references resolved to paths, all literals typed).
4063///
4064/// Nodes live in a flat table and reference children by [`ExprId`]. Created during
4065/// planning from the AST; all unresolved references are converted to DataPath/RulePath.
4066pub type Expression = ExprTree<ExpressionKind>;
4067
4068/// Resolved expression kind (only resolved variants, no unresolved references)
4069#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4070#[serde(rename_all = "snake_case")]
4071pub enum ExpressionKind {
4072    /// Resolved literal with type (boxed to keep enum small)
4073    Literal(Box<TypedLiteral>),
4074    /// Resolved data path
4075    DataPath(DataPath),
4076    /// Resolved rule path
4077    RulePath(RulePath),
4078    /// `first and steps[0].operand and ...`, operands in source order.
4079    LogicalAnd {
4080        first: ExprId,
4081        steps: Vec<ChainStep<()>>,
4082    },
4083    /// Left fold of `first` over `steps`; every step's operator has one precedence level.
4084    Arithmetic {
4085        first: ExprId,
4086        steps: Vec<ChainStep<ArithmeticComputation>>,
4087    },
4088    Comparison(ExprId, ComparisonComputation, ExprId),
4089    UnitConversion(ExprId, SemanticConversionTarget),
4090    LogicalNegation(ExprId, NegationType),
4091    MathematicalComputation(MathematicalComputation, ExprId),
4092    Veto(VetoExpression),
4093    /// The `now` keyword — resolved at evaluation to the effective datetime.
4094    Now,
4095    /// Date-relative sugar: `<date_expr> in past` / `in future`
4096    DateRelative(DateRelativeKind, ExprId),
4097    /// Calendar-period sugar: `<date_expr> in [past|future] calendar year|month|week`
4098    DateCalendar(DateCalendarKind, CalendarPeriodUnit, ExprId),
4099    RangeLiteral(ExprId, ExprId),
4100    PastFutureRange(DateRelativeKind, ExprId),
4101    RangeContainment(ExprId, ExprId),
4102    /// Ordered endpoint of a range (`lower` included, `upper` excluded).
4103    RangeBound(RangeBound, ExprId),
4104    /// Whether evaluating the operand produced a veto (no value). Parses as `is veto` syntax.
4105    ResultIsVeto(ExprId),
4106    /// Field of a spec value: `top_bracket.tax`, `top_bracket.finance.fee`, or
4107    /// `(staff -> first).bonus`.
4108    ///
4109    /// `targets` is filled after candidate `uses` instances of `base` are known. Each
4110    /// target is one fixed instance the base can yield. A base that yields list rows
4111    /// reads the field through the row spec's template plan instead.
4112    SpecMember {
4113        base: ExprId,
4114        path: Vec<String>,
4115        name: String,
4116        targets: Vec<SpecMemberTarget>,
4117    },
4118    /// Field of the current row of the list command at `level` (0 = outermost).
4119    RowField {
4120        level: u32,
4121        identity: SpecIdentity,
4122        end: RowEnd,
4123    },
4124    /// Unless structure: (condition, result) pairs in source order; last true condition wins.
4125    /// First arm is the default (condition is always-true literal).
4126    Piecewise(Vec<(ExprId, ExprId)>),
4127    /// `row_level` is set when the list holds instance rows: arguments read the
4128    /// current row through [`Self::RowField`] at that level.
4129    ListCommand {
4130        list: ExprId,
4131        op: ListOp,
4132        row_level: Option<u32>,
4133    },
4134}
4135
4136/// Planned postfix list command.
4137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4138pub enum ListOp {
4139    Find {
4140        keys: Vec<ExprId>,
4141    },
4142    First,
4143    Last,
4144    Count,
4145    Reduce {
4146        kind: ListReduce,
4147        argument: Option<ExprId>,
4148    },
4149    Predicate {
4150        kind: ListPredKind,
4151        op: crate::parsing::ast::RowCondition,
4152        right: ExprId,
4153    },
4154    InstancePredicate {
4155        kind: ListPredKind,
4156        expr: ExprId,
4157    },
4158    Sort {
4159        descending: bool,
4160        argument: Option<ExprId>,
4161    },
4162    Top {
4163        n: ExprId,
4164    },
4165    With {
4166        rows: Vec<ExprId>,
4167    },
4168    /// Append one instance row; each field is a row-spec data path and its value.
4169    WithRow {
4170        fields: Vec<(DataPath, ExprId)>,
4171    },
4172}
4173
4174#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4175pub enum ListReduce {
4176    Sum,
4177    Avg,
4178    Product,
4179    Min,
4180    Max,
4181}
4182
4183impl ListReduce {
4184    pub fn keyword(self) -> &'static str {
4185        match self {
4186            ListReduce::Sum => "sum",
4187            ListReduce::Avg => "avg",
4188            ListReduce::Product => "product",
4189            ListReduce::Min => "min",
4190            ListReduce::Max => "max",
4191        }
4192    }
4193}
4194
4195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4196pub enum ListPredKind {
4197    Any,
4198    All,
4199    Where,
4200}
4201
4202impl ListPredKind {
4203    pub fn keyword(self) -> &'static str {
4204        match self {
4205            ListPredKind::Any => "any",
4206            ListPredKind::All => "all",
4207            ListPredKind::Where => "where",
4208        }
4209    }
4210}
4211
4212impl ExprKind for ExpressionKind {
4213    fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
4214        match self {
4215            ExpressionKind::Literal(_)
4216            | ExpressionKind::DataPath(_)
4217            | ExpressionKind::RulePath(_)
4218            | ExpressionKind::Veto(_)
4219            | ExpressionKind::Now
4220            | ExpressionKind::RowField { .. } => {}
4221            ExpressionKind::LogicalAnd { first, steps } => {
4222                f(*first);
4223                steps.iter().for_each(|step| f(step.operand));
4224            }
4225            ExpressionKind::Arithmetic { first, steps } => {
4226                f(*first);
4227                steps.iter().for_each(|step| f(step.operand));
4228            }
4229            ExpressionKind::Comparison(left, _, right)
4230            | ExpressionKind::RangeLiteral(left, right)
4231            | ExpressionKind::RangeContainment(left, right) => {
4232                f(*left);
4233                f(*right);
4234            }
4235            ExpressionKind::UnitConversion(inner, _)
4236            | ExpressionKind::LogicalNegation(inner, _)
4237            | ExpressionKind::MathematicalComputation(_, inner)
4238            | ExpressionKind::DateRelative(_, inner)
4239            | ExpressionKind::DateCalendar(_, _, inner)
4240            | ExpressionKind::PastFutureRange(_, inner)
4241            | ExpressionKind::RangeBound(_, inner)
4242            | ExpressionKind::ResultIsVeto(inner)
4243            | ExpressionKind::SpecMember { base: inner, .. } => f(*inner),
4244            ExpressionKind::Piecewise(arms) => arms.iter().for_each(|(condition, result)| {
4245                f(*condition);
4246                f(*result);
4247            }),
4248            ExpressionKind::ListCommand { list, op, .. } => {
4249                f(*list);
4250                op.for_each_child(f);
4251            }
4252        }
4253    }
4254
4255    fn children_mut(&mut self) -> Vec<&mut ExprId> {
4256        match self {
4257            ExpressionKind::Literal(_)
4258            | ExpressionKind::DataPath(_)
4259            | ExpressionKind::RulePath(_)
4260            | ExpressionKind::Veto(_)
4261            | ExpressionKind::Now
4262            | ExpressionKind::RowField { .. } => Vec::new(),
4263            ExpressionKind::LogicalAnd { first, steps } => std::iter::once(first)
4264                .chain(steps.iter_mut().map(|step| &mut step.operand))
4265                .collect(),
4266            ExpressionKind::Arithmetic { first, steps } => std::iter::once(first)
4267                .chain(steps.iter_mut().map(|step| &mut step.operand))
4268                .collect(),
4269            ExpressionKind::Comparison(left, _, right)
4270            | ExpressionKind::RangeLiteral(left, right)
4271            | ExpressionKind::RangeContainment(left, right) => vec![left, right],
4272            ExpressionKind::UnitConversion(inner, _)
4273            | ExpressionKind::LogicalNegation(inner, _)
4274            | ExpressionKind::MathematicalComputation(_, inner)
4275            | ExpressionKind::DateRelative(_, inner)
4276            | ExpressionKind::DateCalendar(_, _, inner)
4277            | ExpressionKind::PastFutureRange(_, inner)
4278            | ExpressionKind::RangeBound(_, inner)
4279            | ExpressionKind::ResultIsVeto(inner) => vec![inner],
4280            ExpressionKind::SpecMember { base, .. } => vec![base],
4281            ExpressionKind::Piecewise(arms) => arms
4282                .iter_mut()
4283                .flat_map(|(condition, result)| [condition, result])
4284                .collect(),
4285            ExpressionKind::ListCommand { list, op, .. } => {
4286                let mut children = vec![list];
4287                children.extend(op.children_mut());
4288                children
4289            }
4290        }
4291    }
4292}
4293
4294impl ListOp {
4295    fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
4296        match self {
4297            ListOp::First | ListOp::Last | ListOp::Count => {}
4298            ListOp::Find { keys } => keys.iter().copied().for_each(f),
4299            ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
4300                argument.iter().copied().for_each(f)
4301            }
4302            ListOp::Predicate { right, .. } => f(*right),
4303            ListOp::InstancePredicate { expr, .. } => f(*expr),
4304            ListOp::Top { n } => f(*n),
4305            ListOp::With { rows } => rows.iter().copied().for_each(f),
4306            ListOp::WithRow { fields } => fields.iter().for_each(|(_, value)| f(*value)),
4307        }
4308    }
4309
4310    fn children_mut(&mut self) -> Vec<&mut ExprId> {
4311        match self {
4312            ListOp::First | ListOp::Last | ListOp::Count => Vec::new(),
4313            ListOp::Find { keys } => keys.iter_mut().collect(),
4314            ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
4315                argument.iter_mut().collect()
4316            }
4317            ListOp::Predicate { right, .. } => vec![right],
4318            ListOp::InstancePredicate { expr, .. } => vec![expr],
4319            ListOp::Top { n } => vec![n],
4320            ListOp::With { rows } => rows.iter_mut().collect(),
4321            ListOp::WithRow { fields } => fields.iter_mut().map(|(_, value)| value).collect(),
4322        }
4323    }
4324}
4325
4326// -----------------------------------------------------------------------------
4327// Resolved types and values
4328// -----------------------------------------------------------------------------
4329
4330/// Where the custom extension chain is rooted: same spec as this type, or imported from another resolved spec.
4331#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
4332pub enum TypeDefiningSpec {
4333    /// Parent type is defined in the same spec as this type.
4334    Local,
4335    /// Parent type was resolved from types loaded from another spec.
4336    Import,
4337}
4338
4339/// Root of a custom type's extension chain: the spec version that declares the root type,
4340/// and its name. Types share a unit family only when they share this root declaration;
4341/// a local `money` and an imported `pricing.money` are different families. Two versions
4342/// of `pricing` declare different `pricing <date>.money` families; values from different
4343/// versions do not mix. Interface drift across unpinned versions compares structure with
4344/// [`LemmaType::same_interface`], which ignores the family's effective date.
4345#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
4346pub struct TypeFamily {
4347    pub repository: Option<String>,
4348    pub spec: String,
4349    pub effective: crate::parsing::ast::EffectiveDate,
4350    pub name: String,
4351}
4352
4353impl TypeFamily {
4354    #[must_use]
4355    pub fn declared_in(spec: &SpecIdentity, name: &str) -> Self {
4356        Self {
4357            repository: spec.repository.clone(),
4358            spec: spec.spec.clone(),
4359            effective: spec.effective.clone(),
4360            name: name.to_string(),
4361        }
4362    }
4363
4364    /// Whether `spec` (this exact version) declares this family's root.
4365    #[must_use]
4366    pub fn is_declared_in(&self, spec: &SpecIdentity) -> bool {
4367        self.repository == spec.repository
4368            && self.spec == spec.spec
4369            && self.effective == spec.effective
4370    }
4371
4372    /// Family identity with the version erased (interface comparisons only).
4373    #[must_use]
4374    pub fn without_effective(&self) -> Self {
4375        Self {
4376            repository: self.repository.clone(),
4377            spec: self.spec.clone(),
4378            effective: crate::parsing::ast::EffectiveDate::Origin,
4379            name: self.name.clone(),
4380        }
4381    }
4382}
4383
4384/// `[<repo> ]<spec>[ <date>].<type>`: the root type named with its declaring spec version.
4385impl fmt::Display for TypeFamily {
4386    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4387        if let Some(repository) = &self.repository {
4388            write!(f, "{repository} ")?;
4389        }
4390        write!(f, "{}", self.spec)?;
4391        if let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.effective {
4392            write!(f, " {at}")?;
4393        }
4394        write!(f, ".{}", self.name)
4395    }
4396}
4397
4398/// What this type extends (primitive built-in or custom type by name).
4399#[derive(Clone, Debug, Serialize, Deserialize)]
4400pub enum TypeExtends {
4401    /// Extends a primitive built-in type (number, boolean, text, etc.)
4402    Primitive,
4403    /// Extends a custom type: parent is the immediate parent type name; family is the root of the extension chain.
4404    /// `defining_spec` records whether the parent chain is local or imported from another spec.
4405    Custom {
4406        parent: String,
4407        family: TypeFamily,
4408        defining_spec: TypeDefiningSpec,
4409    },
4410}
4411
4412impl PartialEq for TypeExtends {
4413    fn eq(&self, other: &Self) -> bool {
4414        match (self, other) {
4415            (TypeExtends::Primitive, TypeExtends::Primitive) => true,
4416            (
4417                TypeExtends::Custom {
4418                    parent: lp,
4419                    family: lf,
4420                    defining_spec: ld,
4421                },
4422                TypeExtends::Custom {
4423                    parent: rp,
4424                    family: rf,
4425                    defining_spec: rd,
4426                },
4427            ) => lp == rp && lf == rf && ld == rd,
4428            _ => false,
4429        }
4430    }
4431}
4432
4433impl Eq for TypeExtends {}
4434
4435impl std::hash::Hash for TypeExtends {
4436    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
4437        match self {
4438            TypeExtends::Primitive => {
4439                0u8.hash(state);
4440            }
4441            TypeExtends::Custom {
4442                parent,
4443                family,
4444                defining_spec,
4445            } => {
4446                1u8.hash(state);
4447                parent.hash(state);
4448                family.hash(state);
4449                defining_spec.hash(state);
4450            }
4451        }
4452    }
4453}
4454
4455impl TypeExtends {
4456    /// Custom extension in the same spec as the defining type (no cross-spec import for the parent chain).
4457    #[must_use]
4458    pub fn custom_local(parent: String, family: TypeFamily) -> Self {
4459        TypeExtends::Custom {
4460            parent,
4461            family,
4462            defining_spec: TypeDefiningSpec::Local,
4463        }
4464    }
4465
4466    /// Returns the parent type name if this type extends a custom type.
4467    #[must_use]
4468    pub fn parent_name(&self) -> Option<&str> {
4469        match self {
4470            TypeExtends::Primitive => None,
4471            TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
4472        }
4473    }
4474
4475    #[must_use]
4476    fn with_families_without_effective(&self) -> Self {
4477        match self {
4478            TypeExtends::Primitive => TypeExtends::Primitive,
4479            TypeExtends::Custom {
4480                parent,
4481                family,
4482                defining_spec,
4483            } => TypeExtends::Custom {
4484                parent: parent.clone(),
4485                family: family.without_effective(),
4486                defining_spec: defining_spec.clone(),
4487            },
4488        }
4489    }
4490}
4491
4492/// Resolved type after planning
4493///
4494/// Contains a type specification and optional name. Created during planning
4495/// from TypeSpecification in the AST.
4496#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
4497pub struct LemmaType {
4498    /// Optional type name (e.g., "age", "temperature")
4499    pub name: Option<String>,
4500    /// The type specification (Boolean, Number, Measure, etc.).
4501    pub specifications: TypeSpecification,
4502    /// What this type extends (primitive or custom from a spec)
4503    pub extends: TypeExtends,
4504}
4505
4506impl LemmaType {
4507    /// Functional update of the `Measure` payload (units + decomposition).
4508    /// Non-Measure variants pass through unchanged. The transform receives the owned
4509    /// units and decomposition and returns the replacements.
4510    pub fn map_measure<F>(self, f: F) -> Self
4511    where
4512        F: FnOnce(
4513            MeasureUnits,
4514            Option<BaseMeasureVector>,
4515        ) -> (MeasureUnits, Option<BaseMeasureVector>),
4516    {
4517        let LemmaType {
4518            name,
4519            specifications,
4520            extends,
4521        } = self;
4522        let specifications = match specifications {
4523            TypeSpecification::Measure {
4524                minimum,
4525                maximum,
4526                decimals,
4527                units,
4528                traits,
4529                decomposition,
4530                unit,
4531                help,
4532            } => {
4533                let (units, decomposition) = f(units, decomposition);
4534                TypeSpecification::Measure {
4535                    minimum,
4536                    maximum,
4537                    decimals,
4538                    units,
4539                    traits,
4540                    decomposition,
4541                    unit,
4542                    help,
4543                }
4544            }
4545            other => other,
4546        };
4547        LemmaType {
4548            name,
4549            specifications,
4550            extends,
4551        }
4552    }
4553
4554    /// Create a new type with a name
4555    pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
4556        Self {
4557            name: Some(name),
4558            specifications,
4559            extends,
4560        }
4561    }
4562
4563    /// Create a type without a name (anonymous/inline type)
4564    pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
4565        Self {
4566            name: None,
4567            specifications,
4568            extends,
4569        }
4570    }
4571
4572    /// Create a primitive type (no name, extends Primitive)
4573    pub fn primitive(specifications: TypeSpecification) -> Self {
4574        Self {
4575            name: None,
4576            specifications,
4577            extends: TypeExtends::Primitive,
4578        }
4579    }
4580
4581    /// Prefer `unit_name` for [`measure_runtime_signature`] (bind / signature-index hit).
4582    ///
4583    /// Writes `unit` onto `Measure` or `Ratio` specifications. Any other variant panics.
4584    #[must_use]
4585    pub fn with_unit(mut self, unit_name: impl Into<String>) -> Self {
4586        let unit_name = unit_name.into();
4587        match &mut self.specifications {
4588            TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
4589                *unit = Some(unit_name);
4590            }
4591            other => panic!(
4592                "BUG: with_unit called on non-measure/non-ratio type {}",
4593                other
4594            ),
4595        }
4596        self
4597    }
4598
4599    /// Written unit on a measure or ratio specification, if any.
4600    #[must_use]
4601    pub fn unit(&self) -> Option<&str> {
4602        match &self.specifications {
4603            TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
4604                unit.as_deref()
4605            }
4606            _ => None,
4607        }
4608    }
4609
4610    /// Get the type name, or a default based on the type specification
4611    pub fn name(&self) -> String {
4612        self.name
4613            .clone()
4614            .unwrap_or_else(|| self.specifications.to_string())
4615    }
4616
4617    /// Check if this type is boolean
4618    pub fn is_boolean(&self) -> bool {
4619        matches!(&self.specifications, TypeSpecification::Boolean { .. })
4620    }
4621
4622    pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
4623        matches!(
4624            (kind, &self.specifications),
4625            (PrimitiveKind::Number, TypeSpecification::Number { .. })
4626                | (PrimitiveKind::Text, TypeSpecification::Text { .. })
4627                | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
4628                | (PrimitiveKind::Date, TypeSpecification::Date { .. })
4629                | (PrimitiveKind::Time, TypeSpecification::Time { .. })
4630                | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
4631                | (PrimitiveKind::Measure, TypeSpecification::Measure { .. })
4632        )
4633    }
4634
4635    /// Check if this type is measure
4636    pub fn is_measure(&self) -> bool {
4637        matches!(&self.specifications, TypeSpecification::Measure { .. })
4638    }
4639
4640    pub fn is_measure_range(&self) -> bool {
4641        matches!(&self.specifications, TypeSpecification::MeasureRange { .. })
4642    }
4643
4644    /// Check if this type is number (dimensionless)
4645    pub fn is_number(&self) -> bool {
4646        matches!(&self.specifications, TypeSpecification::Number { .. })
4647    }
4648
4649    pub fn is_number_range(&self) -> bool {
4650        matches!(&self.specifications, TypeSpecification::NumberRange { .. })
4651    }
4652
4653    /// Check if this type is numeric (either measure or number)
4654    pub fn is_numeric(&self) -> bool {
4655        matches!(
4656            &self.specifications,
4657            TypeSpecification::Measure { .. } | TypeSpecification::Number { .. }
4658        )
4659    }
4660
4661    /// Check if this type is text
4662    pub fn is_text(&self) -> bool {
4663        matches!(&self.specifications, TypeSpecification::Text { .. })
4664    }
4665
4666    /// Check if this type is date
4667    pub fn is_date(&self) -> bool {
4668        matches!(&self.specifications, TypeSpecification::Date { .. })
4669    }
4670
4671    pub fn is_date_range(&self) -> bool {
4672        matches!(&self.specifications, TypeSpecification::DateRange { .. })
4673    }
4674
4675    pub fn is_time_range(&self) -> bool {
4676        matches!(&self.specifications, TypeSpecification::TimeRange { .. })
4677    }
4678
4679    /// Check if this type is time
4680    pub fn is_time(&self) -> bool {
4681        matches!(&self.specifications, TypeSpecification::Time { .. })
4682    }
4683
4684    pub fn has_trait_duration(&self) -> bool {
4685        match &self.specifications {
4686            TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Duration),
4687            _ => false,
4688        }
4689    }
4690
4691    pub fn is_duration_like_measure(&self) -> bool {
4692        if !self.is_measure() {
4693            return false;
4694        }
4695        if self.has_trait_duration() {
4696            return true;
4697        }
4698        self.is_anonymous_measure()
4699            && self
4700                .measure_type_decomposition()
4701                .is_some_and(|d| *d == duration_decomposition())
4702    }
4703
4704    pub fn is_duration_like(&self) -> bool {
4705        self.is_duration_like_measure()
4706    }
4707
4708    pub fn has_trait_calendar(&self) -> bool {
4709        match &self.specifications {
4710            TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Calendar),
4711            _ => false,
4712        }
4713    }
4714
4715    pub fn is_calendar_like_measure(&self) -> bool {
4716        if !self.is_measure() {
4717            return false;
4718        }
4719        if self.has_trait_calendar() {
4720            return true;
4721        }
4722        self.is_anonymous_measure()
4723            && self
4724                .measure_type_decomposition()
4725                .is_some_and(|d| *d == calendar_decomposition())
4726    }
4727
4728    pub fn is_calendar_like(&self) -> bool {
4729        self.is_calendar_like_measure()
4730    }
4731
4732    /// Check if this type is ratio
4733    pub fn is_ratio(&self) -> bool {
4734        matches!(&self.specifications, TypeSpecification::Ratio { .. })
4735    }
4736
4737    pub fn is_ratio_range(&self) -> bool {
4738        matches!(&self.specifications, TypeSpecification::RatioRange { .. })
4739    }
4740
4741    pub fn is_calendar_measure_range(&self) -> bool {
4742        matches!(
4743            &self.specifications,
4744            TypeSpecification::MeasureRange { decomposition: Some(decomposition), .. }
4745                if *decomposition == calendar_decomposition()
4746        )
4747    }
4748
4749    pub fn is_calendar_like_range(&self) -> bool {
4750        self.is_calendar_measure_range()
4751    }
4752
4753    pub fn is_range(&self) -> bool {
4754        matches!(
4755            &self.specifications,
4756            TypeSpecification::DateRange { .. }
4757                | TypeSpecification::TimeRange { .. }
4758                | TypeSpecification::NumberRange { .. }
4759                | TypeSpecification::MeasureRange { .. }
4760                | TypeSpecification::RatioRange { .. }
4761        )
4762    }
4763
4764    /// Check if this type is a spec instance.
4765    pub fn is_spec(&self) -> bool {
4766        matches!(&self.specifications, TypeSpecification::Spec { .. })
4767    }
4768
4769    pub fn is_list(&self) -> bool {
4770        matches!(&self.specifications, TypeSpecification::List { .. })
4771    }
4772
4773    pub fn list_element_spec(&self) -> Option<&TypeSpecification> {
4774        match &self.specifications {
4775            TypeSpecification::List { element, .. } => Some(&element.specifications),
4776            _ => None,
4777        }
4778    }
4779
4780    pub fn list_of(element: Arc<LemmaType>) -> Self {
4781        Self {
4782            name: Some(format!("{} list", element.name())),
4783            specifications: TypeSpecification::List {
4784                element,
4785                key: ListKey::Numbered,
4786            },
4787            extends: TypeExtends::Primitive,
4788        }
4789    }
4790
4791    /// Element type of a list type (`money` for `money list`).
4792    pub fn list_element_lemma_type(&self) -> &Arc<LemmaType> {
4793        let TypeSpecification::List { element, .. } = &self.specifications else {
4794            panic!(
4795                "BUG: list_element_lemma_type on non-list type {}",
4796                self.name()
4797            );
4798        };
4799        element
4800    }
4801
4802    /// Spec identity when this type is a spec instance.
4803    #[must_use]
4804    pub fn spec_identity(&self) -> Option<&SpecIdentity> {
4805        match &self.specifications {
4806            TypeSpecification::Spec { spec } => Some(spec),
4807            _ => None,
4808        }
4809    }
4810
4811    /// Check if this type is veto
4812    pub fn vetoed(&self) -> bool {
4813        matches!(&self.specifications, TypeSpecification::Veto { .. })
4814    }
4815
4816    /// True if this type is the undetermined sentinel (type could not be inferred).
4817    pub fn is_undetermined(&self) -> bool {
4818        matches!(&self.specifications, TypeSpecification::Undetermined)
4819    }
4820
4821    /// Check if two types have the same base type specification (ignoring constraints)
4822    pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
4823        use TypeSpecification::*;
4824        matches!(
4825            (&self.specifications, &other.specifications),
4826            (Boolean { .. }, Boolean { .. })
4827                | (Number { .. }, Number { .. })
4828                | (NumberRange { .. }, NumberRange { .. })
4829                | (Measure { .. }, Measure { .. })
4830                | (MeasureRange { .. }, MeasureRange { .. })
4831                | (Text { .. }, Text { .. })
4832                | (Date { .. }, Date { .. })
4833                | (DateRange { .. }, DateRange { .. })
4834                | (Time { .. }, Time { .. })
4835                | (TimeRange { .. }, TimeRange { .. })
4836                | (Ratio { .. }, Ratio { .. })
4837                | (RatioRange { .. }, RatioRange { .. })
4838                | (Veto { .. }, Veto { .. })
4839                | (Undetermined, Undetermined)
4840                | (Spec { .. }, Spec { .. })
4841                | (List { .. }, List { .. })
4842        )
4843    }
4844
4845    /// Root of the custom extension chain; `None` for primitive and anonymous types.
4846    #[must_use]
4847    pub fn family(&self) -> Option<&TypeFamily> {
4848        match &self.extends {
4849            TypeExtends::Custom { family, .. } => Some(family),
4850            TypeExtends::Primitive => None,
4851        }
4852    }
4853
4854    /// Unit family of a measure type.
4855    #[must_use]
4856    pub fn measure_family(&self) -> Option<&TypeFamily> {
4857        self.is_measure().then(|| self.family()).flatten()
4858    }
4859
4860    /// Unit family of a ratio type.
4861    #[must_use]
4862    pub fn ratio_family(&self) -> Option<&TypeFamily> {
4863        self.is_ratio().then(|| self.family()).flatten()
4864    }
4865
4866    /// Unit family of a measure or ratio type.
4867    #[must_use]
4868    pub(crate) fn unit_family(&self) -> Option<&TypeFamily> {
4869        self.measure_family().or_else(|| self.ratio_family())
4870    }
4871
4872    /// Root type name of [`Self::measure_family`], for display.
4873    #[must_use]
4874    pub fn measure_family_name(&self) -> Option<&str> {
4875        self.measure_family().map(|family| family.name.as_str())
4876    }
4877
4878    /// Root type name of [`Self::ratio_family`], for display.
4879    #[must_use]
4880    pub fn ratio_family_name(&self) -> Option<&str> {
4881        self.ratio_family().map(|family| family.name.as_str())
4882    }
4883
4884    /// Same base kind and, for measures / measure ranges, the same dimensions:
4885    /// the relation under which two types describe the same values. Names and
4886    /// constraints may differ. RatioRange stays base-kind only (endpoint policy).
4887    #[must_use]
4888    pub(crate) fn same_value_type(&self, other: &LemmaType) -> bool {
4889        if !self.has_same_base_type(other) {
4890            return false;
4891        }
4892        if self.is_measure() {
4893            return self.measure_type_decomposition() == other.measure_type_decomposition();
4894        }
4895        if let (
4896            TypeSpecification::Spec { spec: self_spec },
4897            TypeSpecification::Spec { spec: other_spec },
4898        ) = (&self.specifications, &other.specifications)
4899        {
4900            return self_spec == other_spec;
4901        }
4902        if let (
4903            TypeSpecification::List {
4904                element: self_element,
4905                key: self_key,
4906            },
4907            TypeSpecification::List {
4908                element: other_element,
4909                key: other_key,
4910            },
4911        ) = (&self.specifications, &other.specifications)
4912        {
4913            return self_key == other_key && self_element.same_value_type(other_element);
4914        }
4915        if self.is_measure_range() {
4916            let self_element = self
4917                .specifications
4918                .element_from_range()
4919                .expect("BUG: MeasureRange always defines element_from_range");
4920            let other_element = other
4921                .specifications
4922                .element_from_range()
4923                .expect("BUG: MeasureRange always defines element_from_range");
4924            match (&self_element, &other_element) {
4925                (
4926                    TypeSpecification::Measure {
4927                        decomposition: self_decomp,
4928                        ..
4929                    },
4930                    TypeSpecification::Measure {
4931                        decomposition: other_decomp,
4932                        ..
4933                    },
4934                ) => self_decomp == other_decomp,
4935                _ => unreachable!("BUG: element_from_range of MeasureRange yields Measure"),
4936            }
4937        } else {
4938            true
4939        }
4940    }
4941
4942    /// Returns true if both types are measure and belong to the same named measure family.
4943    #[must_use]
4944    pub fn same_measure_family(&self, other: &LemmaType) -> bool {
4945        if !self.is_measure() || !other.is_measure() {
4946            return false;
4947        }
4948        match (self.measure_family(), other.measure_family()) {
4949            (Some(self_family), Some(other_family)) => self_family == other_family,
4950            _ => false,
4951        }
4952    }
4953
4954    /// Structural equality for interface drift: same as [`PartialEq`] but family
4955    /// versions (`TypeFamily::effective`) are ignored. Unpinned imports spanning
4956    /// unchanged versions of a dependency compare equal; real changes still differ.
4957    #[must_use]
4958    pub fn same_interface(&self, other: &Self) -> bool {
4959        self.with_families_without_effective() == other.with_families_without_effective()
4960    }
4961
4962    /// Clone with every [`TypeFamily::effective`] set to [`EffectiveDate::Origin`].
4963    #[must_use]
4964    pub fn with_families_without_effective(&self) -> Self {
4965        Self {
4966            name: self.name.clone(),
4967            specifications: self.specifications.with_families_without_effective(),
4968            extends: self.extends.with_families_without_effective(),
4969        }
4970    }
4971
4972    #[must_use]
4973    pub fn compatible_with_anonymous_measure(&self, other: &LemmaType) -> bool {
4974        if !self.is_measure() || !other.is_measure() {
4975            return false;
4976        }
4977        if !self.is_anonymous_measure() && !other.is_anonymous_measure() {
4978            return false;
4979        }
4980        match (
4981            self.measure_type_decomposition(),
4982            other.measure_type_decomposition(),
4983        ) {
4984            (Some(a), Some(b)) => a == b,
4985            _ => false,
4986        }
4987    }
4988
4989    /// Create a Veto LemmaType
4990    pub fn veto_type() -> Self {
4991        Self::primitive(TypeSpecification::veto())
4992    }
4993
4994    /// LemmaType sentinel for undetermined type (used during inference when a type cannot be determined).
4995    /// Propagates through expressions and is never present in a validated graph.
4996    pub fn undetermined_type() -> Self {
4997        Self::primitive(TypeSpecification::Undetermined)
4998    }
4999
5000    /// Decimal places for display (Number, Measure, and Ratio). Used by formatters.
5001    /// Ratio: optional, no default; when None display is normalized (no trailing zeros).
5002    pub fn decimal_places(&self) -> Option<u8> {
5003        match &self.specifications {
5004            TypeSpecification::Number { decimals, .. } => *decimals,
5005            TypeSpecification::Measure { decimals, .. } => *decimals,
5006            TypeSpecification::Ratio { decimals, .. } => *decimals,
5007            _ => None,
5008        }
5009    }
5010
5011    /// Convert a rational magnitude to an API [`Decimal`] (28-scale boundary).
5012    ///
5013    /// Applies this type's `decimal_places` when set. Returns [`NumericFailure::Overflow`]
5014    /// when |magnitude| > Decimal::MAX (callers map this to a decimal-limit Veto).
5015    pub fn try_rational_as_decimal(
5016        &self,
5017        magnitude: &crate::computation::rational::RationalInteger,
5018    ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
5019        let decimal = magnitude.try_to_decimal()?;
5020        Ok(format_decimal_for_api(decimal, self.decimal_places()))
5021    }
5022
5023    /// Convert a canonical measure magnitude in the named declared unit to an API [`Decimal`].
5024    pub fn try_measure_canonical_as_decimal_in_unit(
5025        &self,
5026        canonical_magnitude: &crate::computation::rational::RationalInteger,
5027        unit_name: &str,
5028    ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
5029        use crate::computation::rational::checked_div;
5030        let unit_factor = self.measure_unit_factor(unit_name);
5031        let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
5032        self.try_rational_as_decimal(&magnitude_in_unit)
5033    }
5034
5035    /// Convert a canonical ratio magnitude in the named declared unit to an API [`Decimal`].
5036    pub fn try_ratio_canonical_as_decimal_in_unit(
5037        &self,
5038        canonical_magnitude: &crate::computation::rational::RationalInteger,
5039        unit_name: &str,
5040    ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
5041        use crate::computation::rational::checked_mul;
5042        let units = match &self.specifications {
5043            TypeSpecification::Ratio { units, .. } => units,
5044            _ => unreachable!(
5045                "BUG: try_ratio_canonical_as_decimal_in_unit called on non-ratio type {}",
5046                self.name()
5047            ),
5048        };
5049        let ratio_unit = units
5050            .iter()
5051            .find(|unit| unit.name == unit_name)
5052            .unwrap_or_else(|| {
5053                let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
5054                unreachable!(
5055                    "BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
5056                    unit_name,
5057                    self.name(),
5058                    valid.join(", ")
5059                )
5060            });
5061        let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
5062        self.try_rational_as_decimal(&magnitude_in_unit)
5063    }
5064
5065    /// Get an example value string for this type, suitable for UI help text
5066    pub fn example_value(&self) -> &'static str {
5067        match &self.specifications {
5068            TypeSpecification::Text { .. } => "\"hello world\"",
5069            TypeSpecification::Measure { .. } => "12.50 eur",
5070            TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
5071            TypeSpecification::Number { .. } => "3.14",
5072            TypeSpecification::NumberRange { .. } => "0...100",
5073            TypeSpecification::Boolean { .. } => "true",
5074            TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
5075            TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
5076            TypeSpecification::TimeRange { .. } => "09:00...17:00",
5077            TypeSpecification::Veto { .. } => "veto",
5078            TypeSpecification::Time { .. } => "14:30:00",
5079            TypeSpecification::Ratio { .. } => "50%",
5080            TypeSpecification::RatioRange { .. } => "10%...50%",
5081            TypeSpecification::Undetermined => unreachable!(
5082                "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
5083            ),
5084            TypeSpecification::Spec { .. } => unreachable!(
5085                "BUG: example_value called on a spec type; spec instances are not prompted data"
5086            ),
5087            TypeSpecification::List { element, .. } => element.example_value(),
5088        }
5089    }
5090
5091    /// Factor for a unit of this measure type (for unit conversion during evaluation only).
5092    /// Planning must validate conversions first and return Error for invalid units.
5093    /// If called with a non-measure type or unknown unit name, panics (invariant violation).
5094    #[must_use]
5095    /// Returns the resolved `BaseMeasureVector` for Measure types, or `None` if
5096    /// the decomposition pass has not yet resolved this type.
5097    /// Panics if called on non-Measure types.
5098    pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
5099        match &self.specifications {
5100            TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
5101            _ => unreachable!(
5102                "BUG: measure_type_decomposition called on non-measure type {}",
5103                self.name()
5104            ),
5105        }
5106    }
5107
5108    /// Runtime unit signature for measure arithmetic and display.
5109    ///
5110    /// - Named measure with units: `[(canonical_or_first_unit_name, 1)]`. Arithmetic
5111    ///   expands this via `expand_signature_to_base_units` using the unit table.
5112    /// - Anonymous / no units: decomposition converted to signature form.
5113    #[must_use]
5114    pub fn measure_runtime_signature(&self) -> Vec<(String, i32)> {
5115        if let Some(binding) = self.unit() {
5116            return vec![(binding.to_string(), 1)];
5117        }
5118        match &self.specifications {
5119            TypeSpecification::Measure {
5120                units,
5121                decomposition,
5122                ..
5123            } => {
5124                if let Some(canonical) = units.iter().find(|unit| unit.is_canonical_factor()) {
5125                    return vec![(canonical.name.clone(), 1)];
5126                }
5127                if let Some(first) = units.iter().next() {
5128                    return vec![(first.name.clone(), 1)];
5129                }
5130                base_measure_vector_as_signature(
5131                    decomposition
5132                        .as_ref()
5133                        .expect("BUG: measure type without units must carry a decomposition"),
5134                )
5135            }
5136            TypeSpecification::MeasureRange {
5137                units,
5138                decomposition,
5139                ..
5140            } => {
5141                if let Some(canonical) = units.iter().find(|unit| unit.is_canonical_factor()) {
5142                    return vec![(canonical.name.clone(), 1)];
5143                }
5144                if let Some(first) = units.iter().next() {
5145                    return vec![(first.name.clone(), 1)];
5146                }
5147                base_measure_vector_as_signature(
5148                    decomposition
5149                        .as_ref()
5150                        .expect("BUG: measure type without units must carry a decomposition"),
5151                )
5152            }
5153            _ => unreachable!(
5154                "BUG: measure_runtime_signature called on non-measure type {}",
5155                self.name()
5156            ),
5157        }
5158    }
5159
5160    /// Primary declared ratio unit name when the type carries a non-empty unit table.
5161    #[must_use]
5162    pub fn ratio_primary_unit(&self) -> Option<&str> {
5163        match &self.specifications {
5164            TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
5165                units.iter().next().map(|unit| unit.name.as_str())
5166            }
5167            TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
5168                units.iter().next().map(|unit| unit.name.as_str())
5169            }
5170            _ => None,
5171        }
5172    }
5173
5174    /// Returns true if this is an anonymous (no-name) Measure — i.e. an anonymous
5175    /// intermediate produced by cross-axis arithmetic.
5176    pub fn is_anonymous_measure(&self) -> bool {
5177        self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
5178    }
5179
5180    /// Build an anonymous `LemmaType` for a given dimensional decomposition.
5181    /// Used at plan time to represent the inferred type of cross-axis intermediates.
5182    /// Runtime unit signature is derived from this decomposition via
5183    /// [`LemmaType::measure_runtime_signature`].
5184    pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
5185        Self {
5186            name: None,
5187            specifications: TypeSpecification::Measure {
5188                minimum: None,
5189                maximum: None,
5190                decimals: None,
5191                units: crate::literals::MeasureUnits::new(),
5192                traits: Vec::new(),
5193                decomposition: Some(decomposition),
5194                unit: None,
5195                help: String::new(),
5196            },
5197            extends: TypeExtends::Primitive,
5198        }
5199    }
5200
5201    /// Declared ratio unit names when the type carries a non-empty unit table (`None` otherwise).
5202    #[must_use]
5203    pub fn ratio_unit_names(&self) -> Option<Vec<&str>> {
5204        match &self.specifications {
5205            TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
5206                Some(units.iter().map(|unit| unit.name.as_str()).collect())
5207            }
5208            TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
5209                Some(units.iter().map(|unit| unit.name.as_str()).collect())
5210            }
5211            _ => None,
5212        }
5213    }
5214
5215    /// Declared unit names when the type carries a non-empty unit table (`None` otherwise).
5216    #[must_use]
5217    pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
5218        match &self.specifications {
5219            TypeSpecification::Measure { units, .. } if !units.is_empty() => {
5220                Some(units.iter().map(|unit| unit.name.as_str()).collect())
5221            }
5222            TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
5223                Some(units.iter().map(|unit| unit.name.as_str()).collect())
5224            }
5225            _ => None,
5226        }
5227    }
5228
5229    /// `age [number]` or `gender [gender_code]` — brackets omitted when type adds nothing.
5230    #[must_use]
5231    pub fn label_for_data_input(&self, input_key: &str) -> String {
5232        let type_label = if let Some(parent) = self.extends.parent_name() {
5233            parent.to_string()
5234        } else {
5235            let type_name = self.name();
5236            if type_name == input_key {
5237                self.specifications.to_string()
5238            } else {
5239                type_name
5240            }
5241        };
5242        if type_label == input_key {
5243            input_key.to_string()
5244        } else {
5245            format!("{input_key} [{type_label}]")
5246        }
5247    }
5248
5249    /// `Data age [number]: {detail}` for runtime data override vetoes.
5250    #[must_use]
5251    pub fn data_veto_message(&self, input_key: &str, detail: &str) -> String {
5252        format!("Data {}: {}", self.label_for_data_input(input_key), detail)
5253    }
5254
5255    /// Whether an empty [`EvaluateDataValue`] should veto before parse (text may accept `""`).
5256    #[must_use]
5257    pub fn empty_runtime_input_vetoes(&self) -> bool {
5258        !matches!(self.specifications, TypeSpecification::Text { .. })
5259    }
5260
5261    /// Return the conversion factor for a declared unit name on this measure type.
5262    pub fn measure_unit_factor(
5263        &self,
5264        unit_name: &str,
5265    ) -> &crate::computation::rational::RationalInteger {
5266        let units = match &self.specifications {
5267            TypeSpecification::Measure { units, .. } => units,
5268            TypeSpecification::MeasureRange { units, .. } => units,
5269            _ => unreachable!(
5270                "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
5271                self.name()
5272            ),
5273        };
5274        match units.get(unit_name) {
5275            Ok(MeasureUnit { factor, .. }) => factor,
5276            Err(_) => {
5277                let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
5278                unreachable!(
5279                    "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
5280                    unit_name,
5281                    self.name(),
5282                    valid.join(", ")
5283                );
5284            }
5285        }
5286    }
5287
5288    pub fn ratio_unit_factor(
5289        &self,
5290        unit_name: &str,
5291    ) -> &crate::computation::rational::RationalInteger {
5292        let units = match &self.specifications {
5293            TypeSpecification::Ratio { units, .. } => units,
5294            _ => unreachable!(
5295                "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
5296                self.name()
5297            ),
5298        };
5299        match units.get(unit_name) {
5300            Ok(RatioUnit { value, .. }) => value,
5301            Err(_) => {
5302                let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
5303                unreachable!(
5304                    "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
5305                    unit_name,
5306                    self.name(),
5307                    valid.join(", ")
5308                );
5309            }
5310        }
5311    }
5312
5313    /// Convert a measure literal to API [`Decimal`]s in the given unit names.
5314    pub(crate) fn measure_literal_unit_map(
5315        &self,
5316        literal: &LiteralValue,
5317        unit_names: &[&str],
5318        factor_source: UnitFactorSource<'_>,
5319    ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
5320        use crate::computation::rational::checked_div;
5321
5322        let ValueKind::Measure(magnitude) = &literal.value else {
5323            panic!("BUG: measure_literal_unit_map called with non-measure value");
5324        };
5325        let mut map = BTreeMap::new();
5326        for &unit_name in unit_names {
5327            let unit_factor = factor_source.measure_unit_factor(unit_name);
5328            let magnitude_in_unit = checked_div(magnitude, unit_factor)
5329                .map_err(LiteralUnitMapFailure::UnitConversion)?;
5330            let decimal = self
5331                .try_rational_as_decimal(&magnitude_in_unit)
5332                .map_err(LiteralUnitMapFailure::Commit)?;
5333            map.insert(unit_name.to_string(), decimal);
5334        }
5335        Ok(map)
5336    }
5337
5338    /// Convert a ratio literal to API [`Decimal`]s in the given unit names.
5339    pub(crate) fn ratio_literal_unit_map(
5340        &self,
5341        literal: &LiteralValue,
5342        unit_names: &[&str],
5343        factor_source: UnitFactorSource<'_>,
5344    ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
5345        use crate::computation::rational::checked_mul;
5346
5347        let ratio_api_type = match &self.specifications {
5348            TypeSpecification::Ratio { .. } => self,
5349            TypeSpecification::RatioRange { .. } => {
5350                return ratio_element_type_for_api(self).ratio_literal_unit_map(
5351                    literal,
5352                    unit_names,
5353                    factor_source,
5354                );
5355            }
5356            _ => {
5357                panic!(
5358                    "BUG: ratio_literal_unit_map called with non-ratio type {}",
5359                    self.name()
5360                );
5361            }
5362        };
5363        let ValueKind::Ratio(canonical) = &literal.value else {
5364            panic!("BUG: ratio_literal_unit_map called with non-ratio value");
5365        };
5366        if unit_names.is_empty() {
5367            panic!(
5368                "BUG: ratio literal type '{}' must have at least one unit name",
5369                self.name()
5370            );
5371        }
5372        let mut map = BTreeMap::new();
5373        for &unit_name in unit_names {
5374            let unit_factor = factor_source.ratio_unit_factor(unit_name);
5375            let magnitude_in_unit = checked_mul(canonical, unit_factor)
5376                .map_err(LiteralUnitMapFailure::UnitConversion)?;
5377            let decimal = ratio_api_type
5378                .try_rational_as_decimal(&magnitude_in_unit)
5379                .map_err(LiteralUnitMapFailure::Commit)?;
5380            map.insert(unit_name.to_string(), decimal);
5381        }
5382        Ok(map)
5383    }
5384}
5385
5386/// Where to read measure/ratio unit factors when building a per-unit decimal map.
5387pub(crate) enum UnitFactorSource<'a> {
5388    DeclaredOn(&'a LemmaType),
5389    Merged {
5390        measure: Option<&'a MeasureUnits>,
5391        ratio: Option<&'a RatioUnits>,
5392    },
5393}
5394
5395impl UnitFactorSource<'_> {
5396    fn measure_unit_factor(
5397        &self,
5398        unit_name: &str,
5399    ) -> &crate::computation::rational::RationalInteger {
5400        match self {
5401            UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.measure_unit_factor(unit_name),
5402            UnitFactorSource::Merged { measure, .. } => {
5403                let units = measure.unwrap_or_else(|| {
5404                    panic!(
5405                        "BUG: family measure expansion missing merged table for unit '{unit_name}'"
5406                    )
5407                });
5408                &units
5409                    .get(unit_name)
5410                    .unwrap_or_else(|_| {
5411                        panic!("BUG: family unit '{unit_name}' missing from merged measure table")
5412                    })
5413                    .factor
5414            }
5415        }
5416    }
5417
5418    fn ratio_unit_factor(&self, unit_name: &str) -> &crate::computation::rational::RationalInteger {
5419        match self {
5420            UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.ratio_unit_factor(unit_name),
5421            UnitFactorSource::Merged { ratio, .. } => {
5422                let units = ratio.unwrap_or_else(|| {
5423                    panic!(
5424                        "BUG: family ratio expansion missing merged table for unit '{unit_name}'"
5425                    )
5426                });
5427                &units
5428                    .get(unit_name)
5429                    .unwrap_or_else(|_| {
5430                        panic!("BUG: family unit '{unit_name}' missing from merged ratio table")
5431                    })
5432                    .value
5433            }
5434        }
5435    }
5436}
5437
5438/// Primitive ratio type for API unit-map conversion on ratio range wrappers.
5439pub(crate) fn ratio_element_type_for_api(lemma_type: &LemmaType) -> LemmaType {
5440    match &lemma_type.specifications {
5441        TypeSpecification::Ratio { .. } => lemma_type.clone(),
5442        TypeSpecification::RatioRange { .. } => {
5443            let element = range_element_type_specification(&lemma_type.specifications)
5444                .expect("BUG: ratio range type must have ratio element specification");
5445            let TypeSpecification::Ratio {
5446                units, decimals, ..
5447            } = element
5448            else {
5449                panic!("BUG: ratio range element spec must be Ratio");
5450            };
5451            LemmaType::primitive(TypeSpecification::Ratio {
5452                minimum: None,
5453                maximum: None,
5454                decimals,
5455                units,
5456                unit: None,
5457                help: String::new(),
5458            })
5459        }
5460        _ => panic!(
5461            "BUG: ratio_element_type_for_api called with non-ratio type {}",
5462            lemma_type.name()
5463        ),
5464    }
5465}
5466
5467/// Failure while converting a literal to decimal strings across declared units.
5468#[derive(Debug, Clone, PartialEq, Eq)]
5469pub(crate) enum LiteralUnitMapFailure {
5470    Commit(crate::computation::rational::NumericFailure),
5471    UnitConversion(crate::computation::rational::NumericFailure),
5472}
5473
5474/// Runtime literal payload. Type lives on `NormalForm.result_type` / `DataDefinition` /
5475/// explicit computation parameters — not on the value.
5476#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
5477pub struct LiteralValue {
5478    pub value: ValueKind,
5479}
5480
5481impl LiteralValue {
5482    #[inline]
5483    pub fn new(value: ValueKind) -> Self {
5484        Self { value }
5485    }
5486
5487    fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
5488        match signature {
5489            [(unit_name, 1)] => Some(unit_name.as_str()),
5490            _ => None,
5491        }
5492    }
5493}
5494
5495/// Planning-time literal: value plus resolved type.
5496///
5497/// Used by `ExpressionKind::Literal` and coerce/typing paths. Runtime
5498/// `OperationResult` / value-table slots store bare [`LiteralValue`]; the type
5499/// is on the node (`NormalForm.result_type`) or data definition.
5500#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
5501pub struct TypedLiteral {
5502    pub value: ValueKind,
5503    #[serde(with = "crate::snapshot::shared::arc")]
5504    pub lemma_type: Arc<LemmaType>,
5505}
5506
5507impl TypedLiteral {
5508    #[inline]
5509    pub fn to_literal(&self) -> LiteralValue {
5510        LiteralValue {
5511            value: self.value.clone(),
5512        }
5513    }
5514
5515    #[inline]
5516    pub fn into_literal(self) -> LiteralValue {
5517        LiteralValue { value: self.value }
5518    }
5519}
5520
5521impl From<TypedLiteral> for LiteralValue {
5522    fn from(typed: TypedLiteral) -> Self {
5523        typed.into_literal()
5524    }
5525}
5526
5527impl From<&TypedLiteral> for LiteralValue {
5528    fn from(typed: &TypedLiteral) -> Self {
5529        typed.to_literal()
5530    }
5531}
5532
5533impl LiteralValue {
5534    pub fn text(s: String) -> Self {
5535        Self {
5536            value: ValueKind::Text(s),
5537        }
5538    }
5539
5540    pub fn number(n: RationalInteger) -> Self {
5541        Self {
5542            value: ValueKind::Number(n),
5543        }
5544    }
5545
5546    pub fn number_from_decimal(decimal: Decimal) -> Self {
5547        Self::number(
5548            crate::literals::rational_from_parsed_decimal(decimal)
5549                .expect("BUG: literal number from decimal must lift at boundary"),
5550        )
5551    }
5552
5553    pub fn measure(n: RationalInteger) -> Self {
5554        Self {
5555            value: ValueKind::Measure(n),
5556        }
5557    }
5558
5559    pub fn from_bool(b: bool) -> Self {
5560        Self {
5561            value: ValueKind::Boolean(b),
5562        }
5563    }
5564
5565    pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
5566        Self::date(date_time_to_semantic(dt))
5567    }
5568
5569    pub fn date(dt: SemanticDateTime) -> Self {
5570        Self {
5571            value: ValueKind::Date(dt),
5572        }
5573    }
5574
5575    pub fn time(t: SemanticTime) -> Self {
5576        Self {
5577            value: ValueKind::Time(t),
5578        }
5579    }
5580
5581    pub fn ratio(r: RationalInteger) -> Self {
5582        Self {
5583            value: ValueKind::Ratio(r),
5584        }
5585    }
5586
5587    pub fn ratio_from_decimal(r: Decimal) -> Self {
5588        Self::ratio(
5589            crate::literals::rational_from_parsed_decimal(r)
5590                .expect("BUG: ratio literal from decimal must lift at boundary"),
5591        )
5592    }
5593
5594    pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
5595        Self {
5596            value: ValueKind::Range(Box::new(left), Box::new(right)),
5597        }
5598    }
5599
5600    /// Display without an explicit type. Measure/ratio fall back to magnitude-only.
5601    pub fn display_value(&self) -> String {
5602        match &self.value {
5603            ValueKind::Measure(_) | ValueKind::Ratio(_) => format!("{}", self.value),
5604            ValueKind::Range(left, right) => {
5605                format!("{}...{}", left.display_value(), right.display_value())
5606            }
5607            ValueKind::List(rows) => rows
5608                .iter()
5609                .map(|row| row.display_value())
5610                .collect::<Vec<_>>()
5611                .join(", "),
5612            _ => format!("{}", self.value),
5613        }
5614    }
5615
5616    /// Display string given an explicit type (measure/ratio need unit identity).
5617    pub fn display_value_with_type(&self, lemma_type: &LemmaType) -> String {
5618        match &self.value {
5619            ValueKind::Measure(n) => {
5620                let signature = lemma_type.measure_runtime_signature();
5621                format_measure_canonical_for_display(n, lemma_type, &signature)
5622            }
5623            ValueKind::Ratio(n) => format_ratio_canonical_for_display(n, lemma_type),
5624            ValueKind::Range(left, right) => {
5625                format!("{}...{}", left.display_value(), right.display_value())
5626            }
5627            ValueKind::List(rows) => rows
5628                .iter()
5629                .map(|row| row.display_value())
5630                .collect::<Vec<_>>()
5631                .join(", "),
5632            _ => format!("{}", self.value),
5633        }
5634    }
5635
5636    /// Structural byte-size estimate for resource limit checks.
5637    pub fn byte_size(&self) -> usize {
5638        self.value.structural_byte_size()
5639    }
5640
5641    /// Magnitude for decimal input prompts (API [`Decimal`]; format at call site).
5642    #[must_use]
5643    pub fn magnitude_suggestion_for_decimal_prompt(
5644        &self,
5645        lemma_type: &LemmaType,
5646    ) -> Option<rust_decimal::Decimal> {
5647        match &self.value {
5648            ValueKind::Number(n) => Some(
5649                lemma_type
5650                    .try_rational_as_decimal(n)
5651                    .expect("BUG: stored number literal must convert to decimal for prompt"),
5652            ),
5653            ValueKind::Measure(n) => {
5654                let signature = lemma_type.measure_runtime_signature();
5655                let unit_name = Self::single_measure_signature_unit_name(&signature).expect(
5656                    "BUG: measure prompt requires exactly one signature unit with exponent 1",
5657                );
5658                Some(
5659                    lemma_type
5660                        .try_measure_canonical_as_decimal_in_unit(n, unit_name)
5661                        .expect("BUG: stored measure literal must convert to decimal for prompt"),
5662                )
5663            }
5664            ValueKind::Ratio(n) => {
5665                if let Some(unit_name) = lemma_type.unit() {
5666                    Some(
5667                        lemma_type
5668                            .try_ratio_canonical_as_decimal_in_unit(n, unit_name)
5669                            .expect("BUG: stored ratio literal must convert to decimal for prompt"),
5670                    )
5671                } else {
5672                    Some(lemma_type.try_rational_as_decimal(n).expect(
5673                        "BUG: stored bare ratio literal must convert to decimal for prompt",
5674                    ))
5675                }
5676            }
5677            _ => None,
5678        }
5679    }
5680
5681    /// Per-unit magnitudes when this literal is a measure with declared units.
5682    #[must_use]
5683    pub fn measure_units(
5684        &self,
5685        lemma_type: &LemmaType,
5686    ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
5687        if !matches!(self.value, ValueKind::Measure(_)) {
5688            return None;
5689        }
5690        lemma_type.measure_unit_names()?;
5691        let declared: Vec<&str> = lemma_type
5692            .measure_unit_names()
5693            .expect("BUG: measure_unit_names checked above");
5694        lemma_type
5695            .measure_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
5696            .ok()
5697    }
5698
5699    /// Per-unit magnitudes when this literal is a ratio with declared units.
5700    #[must_use]
5701    pub fn ratio_units(
5702        &self,
5703        lemma_type: &LemmaType,
5704    ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
5705        if !matches!(self.value, ValueKind::Ratio(_)) {
5706            return None;
5707        }
5708        let has_declared_units = match &lemma_type.specifications {
5709            TypeSpecification::Ratio { units, .. } => !units.is_empty(),
5710            TypeSpecification::RatioRange { .. } => true,
5711            _ => return None,
5712        };
5713        if !has_declared_units {
5714            return None;
5715        }
5716        let declared: Vec<&str> = lemma_type
5717            .ratio_unit_names()
5718            .expect("BUG: ratio units checked above");
5719        lemma_type
5720            .ratio_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
5721            .ok()
5722    }
5723
5724    /// Magnitude in a declared unit when this literal is measure or ratio.
5725    #[must_use]
5726    pub fn magnitude_in_unit(
5727        &self,
5728        lemma_type: &LemmaType,
5729        unit: &str,
5730    ) -> Option<rust_decimal::Decimal> {
5731        self.measure_units(lemma_type)
5732            .and_then(|map| map.get(unit).copied())
5733            .or_else(|| {
5734                self.ratio_units(lemma_type)
5735                    .and_then(|map| map.get(unit).copied())
5736            })
5737    }
5738
5739    /// Derive second from a duration measure's canonical magnitude.
5740    pub fn duration_canonical_seconds(&self, lemma_type: &LemmaType) -> RationalInteger {
5741        let ValueKind::Measure(magnitude) = &self.value else {
5742            unreachable!(
5743                "BUG: duration_canonical_seconds called with {:?}",
5744                self.value
5745            );
5746        };
5747        if !lemma_type.is_duration_like_measure() {
5748            unreachable!(
5749                "BUG: duration_canonical_seconds called with type {}",
5750                lemma_type.name()
5751            );
5752        }
5753        let factor = lemma_type.measure_unit_factor("second");
5754        checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
5755    }
5756
5757    /// Derive month from a calendar measure's canonical magnitude.
5758    pub fn calendar_canonical_months(&self, lemma_type: &LemmaType) -> RationalInteger {
5759        let ValueKind::Measure(magnitude) = &self.value else {
5760            unreachable!(
5761                "BUG: calendar_canonical_months called with {:?}",
5762                self.value
5763            );
5764        };
5765        if !lemma_type.is_calendar_like() {
5766            unreachable!(
5767                "BUG: calendar_canonical_months called with type {}",
5768                lemma_type.name()
5769            );
5770        }
5771        let factor = lemma_type.measure_unit_factor("month");
5772        checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
5773    }
5774}
5775
5776impl TypedLiteral {
5777    pub fn text(s: String) -> Self {
5778        Self {
5779            value: ValueKind::Text(s),
5780            lemma_type: primitive_text_arc().clone(),
5781        }
5782    }
5783
5784    pub fn text_with_type(s: String, lemma_type: Arc<LemmaType>) -> Self {
5785        Self {
5786            value: ValueKind::Text(s),
5787            lemma_type,
5788        }
5789    }
5790
5791    pub fn number(n: RationalInteger) -> Self {
5792        Self {
5793            value: ValueKind::Number(n),
5794            lemma_type: primitive_number_arc().clone(),
5795        }
5796    }
5797
5798    pub fn number_from_decimal(decimal: Decimal) -> Self {
5799        Self::number(
5800            crate::literals::rational_from_parsed_decimal(decimal)
5801                .expect("BUG: literal number from decimal must lift at boundary"),
5802        )
5803    }
5804
5805    pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5806        Self {
5807            value: ValueKind::Number(n),
5808            lemma_type,
5809        }
5810    }
5811
5812    pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
5813        Self::number_with_type(
5814            crate::literals::rational_from_parsed_decimal(decimal)
5815                .expect("BUG: literal number from decimal must lift at boundary"),
5816            lemma_type,
5817        )
5818    }
5819
5820    /// Build a Measure literal bound to a specific declared unit.
5821    pub fn measure_with_bound_unit(
5822        n: RationalInteger,
5823        unit_name: impl Into<String>,
5824        lemma_type: Arc<LemmaType>,
5825    ) -> Self {
5826        Self::measure_with_type(
5827            n,
5828            Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
5829        )
5830    }
5831
5832    pub fn measure_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5833        Self {
5834            value: ValueKind::Measure(n),
5835            lemma_type,
5836        }
5837    }
5838
5839    pub fn measure_with_signature(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5840        Self {
5841            value: ValueKind::Measure(n),
5842            lemma_type,
5843        }
5844    }
5845
5846    /// Unit literal outside a measure type context: no family, so no dimensions.
5847    pub fn number_interpreted_as_measure(value: RationalInteger) -> Self {
5848        Self {
5849            value: ValueKind::Measure(value),
5850            lemma_type: Arc::new(anonymous_measure_type()),
5851        }
5852    }
5853
5854    pub fn from_bool(b: bool) -> Self {
5855        Self {
5856            value: ValueKind::Boolean(b),
5857            lemma_type: primitive_boolean_arc().clone(),
5858        }
5859    }
5860
5861    pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
5862        Self::date(date_time_to_semantic(dt))
5863    }
5864
5865    pub fn date(dt: SemanticDateTime) -> Self {
5866        Self {
5867            value: ValueKind::Date(dt),
5868            lemma_type: primitive_date_arc().clone(),
5869        }
5870    }
5871
5872    pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
5873        Self {
5874            value: ValueKind::Date(dt),
5875            lemma_type,
5876        }
5877    }
5878
5879    pub fn time(t: SemanticTime) -> Self {
5880        Self {
5881            value: ValueKind::Time(t),
5882            lemma_type: primitive_time_arc().clone(),
5883        }
5884    }
5885
5886    pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
5887        Self {
5888            value: ValueKind::Time(t),
5889            lemma_type,
5890        }
5891    }
5892
5893    pub fn calendar(
5894        value: RationalInteger,
5895        unit: SemanticCalendarUnit,
5896        lemma_type: Arc<LemmaType>,
5897    ) -> Self {
5898        let unit_name = unit.to_string();
5899        debug_assert_eq!(
5900            semantic_calendar_unit_from_unit_name(&unit_name),
5901            unit,
5902            "BUG: calendar unit name must round-trip"
5903        );
5904        Self::measure_with_bound_unit(value, unit_name, lemma_type)
5905    }
5906
5907    pub fn calendar_from_decimal(
5908        value: Decimal,
5909        unit: SemanticCalendarUnit,
5910        lemma_type: Arc<LemmaType>,
5911    ) -> Self {
5912        Self::calendar(
5913            crate::literals::rational_from_parsed_decimal(value)
5914                .expect("BUG: calendar literal from decimal must lift at boundary"),
5915            unit,
5916            lemma_type,
5917        )
5918    }
5919
5920    pub fn calendar_with_type(
5921        value: RationalInteger,
5922        unit: SemanticCalendarUnit,
5923        lemma_type: Arc<LemmaType>,
5924    ) -> Self {
5925        Self::calendar(value, unit, lemma_type)
5926    }
5927
5928    pub fn ratio(r: RationalInteger) -> Self {
5929        Self {
5930            value: ValueKind::Ratio(r),
5931            lemma_type: primitive_ratio_arc().clone(),
5932        }
5933    }
5934
5935    pub fn ratio_from_decimal(r: Decimal) -> Self {
5936        Self::ratio(
5937            crate::literals::rational_from_parsed_decimal(r)
5938                .expect("BUG: ratio literal from decimal must lift at boundary"),
5939        )
5940    }
5941
5942    pub fn ratio_with_type(r: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5943        Self {
5944            value: ValueKind::Ratio(r),
5945            lemma_type,
5946        }
5947    }
5948
5949    pub fn ratio_with_bound_unit(
5950        r: RationalInteger,
5951        unit_name: impl Into<String>,
5952        lemma_type: Arc<LemmaType>,
5953    ) -> Self {
5954        Self::ratio_with_type(
5955            r,
5956            Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
5957        )
5958    }
5959
5960    pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
5961        let specifications =
5962            range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
5963                .unwrap_or_else(|| {
5964                    unreachable!(
5965                "BUG: attempted to construct a range literal from incompatible endpoint types"
5966            )
5967                });
5968
5969        Self {
5970            value: ValueKind::Range(Box::new(left), Box::new(right)),
5971            lemma_type: Arc::new(LemmaType::primitive(specifications)),
5972        }
5973    }
5974
5975    pub fn display_value(&self) -> String {
5976        self.to_literal().display_value_with_type(&self.lemma_type)
5977    }
5978
5979    pub fn get_type(&self) -> &LemmaType {
5980        &self.lemma_type
5981    }
5982
5983    pub fn byte_size(&self) -> usize {
5984        self.value.structural_byte_size()
5985    }
5986}
5987
5988impl fmt::Display for TypedLiteral {
5989    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5990        write!(f, "{}", self.display_value())
5991    }
5992}
5993
5994/// What a [`DataDefinition::Reference`] copies its value from: either another data path
5995/// or a rule whose result becomes this data's value.
5996#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
5997pub enum ReferenceTarget {
5998    Data(DataPath),
5999    Rule(RulePath),
6000}
6001
6002/// One site binding on a named `uses` import (`-> with` or path-fill).
6003///
6004/// Keys live on [`DataDefinition::Import::overlay`] as paths local to the imported
6005/// spec. Literals are defaults (run data may override); references are wiring.
6006#[derive(Clone, Debug, Serialize, Deserialize)]
6007pub enum ImportOverlay {
6008    Literal(BoundValueKind),
6009    Reference {
6010        target: ReferenceTarget,
6011        constraints: Option<Vec<Constraint>>,
6012    },
6013}
6014
6015/// Where a [`DataDefinition::Reference`] chain ends. Computed once per slice in
6016/// [`crate::planning::graph::Graph::validate`] after `compute_data_reference_order`
6017/// rejected cycles.
6018#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
6019pub(crate) enum ReferenceEnd {
6020    Promptable(DataPath),
6021    Rule(RulePath),
6022    Import,
6023}
6024
6025/// Resolved data value for the execution plan: aligned with [`DataValue`] but with source per variant.
6026#[derive(Clone, Debug, Serialize, Deserialize)]
6027pub enum DataDefinition {
6028    /// Value-holding data: current literal (spec or `with` binding).
6029    Value {
6030        value: LiteralValue,
6031        #[serde(with = "crate::snapshot::shared::arc")]
6032        resolved_type: Arc<LemmaType>,
6033        source: Source,
6034    },
6035    /// Type-only data: type known, value to be supplied (e.g. via run data).
6036    /// `declared_suggestion` carries the `-> suggest ...` payload for this binding or
6037    /// the suggestion inherited from the parent type chain, if any; value-promoting code
6038    /// uses it instead of re-deriving suggestions from [`TypeSpecification`].
6039    /// The evaluator never commits a suggestion — unbound stays MissingData.
6040    TypeDeclaration {
6041        #[serde(with = "crate::snapshot::shared::arc")]
6042        resolved_type: Arc<LemmaType>,
6043        declared_suggestion: Option<BoundValueKind>,
6044        declared_value: Option<BoundValueKind>,
6045        source: Source,
6046    },
6047    /// Import (`uses`): alias for another spec.
6048    ///
6049    /// The imported body is the [`crate::planning::execution_plan::ExecutionPlan`]
6050    /// template keyed by [`Self::Import::identity`] (same store as instance-list
6051    /// row templates). Site `-> with` / path-fill bindings live in [`Self::Import::overlay`]
6052    /// (keys local to the imported spec); they never rewrite the shared template.
6053    Import {
6054        identity: SpecIdentity,
6055        /// Local data paths on the imported spec → site binding.
6056        overlay: IndexMap<DataPath, ImportOverlay>,
6057        source: Source,
6058    },
6059    /// Instance list (`uses x: spec list`). `resolved_type` is the list type.
6060    /// Rows live in [`crate::planning::execution_plan::ExecutionPlan::instance_lists`];
6061    /// evaluation builds the list value from them unless run data replaces it.
6062    InstanceList {
6063        #[serde(with = "crate::snapshot::shared::arc")]
6064        resolved_type: Arc<LemmaType>,
6065        source: Source,
6066    },
6067    /// Value-copy reference to another data or a rule result.
6068    ///
6069    /// `resolved_type` is the merged type that the copied value must satisfy at
6070    /// evaluation time. Merging folds together: (1) the LHS's own declared type,
6071    /// if any; (2) the target's type (data declared type or rule return type);
6072    /// (3) any `local_constraints` written after the `->` on the reference itself.
6073    /// Merging happens in a dedicated pass once all data and rule types are
6074    /// known; before that pass, `resolved_type` holds a provisional value and
6075    /// must not be consumed for type checking.
6076    ///
6077    /// `local_constraints` preserves the raw constraint list from the reference's
6078    /// `-> ...` tail (e.g. `minimum 5` in `data license2: law.other -> minimum 5`)
6079    /// for that merging pass. It is `None` when the reference has no trailing
6080    /// constraints.
6081    ///
6082    /// `local_suggestion` carries any `suggest <value>` constraint from the
6083    /// reference's `-> ...` tail. The reference-merge pass extracts it from the
6084    /// constraint list during type resolution. It is a UI hint only
6085    /// ([`Self::suggestion`] / show); the evaluator never commits it — unbound
6086    /// stays MissingData.
6087    ///
6088    /// The reference itself is evaluated by copying the target's value (data path)
6089    /// or the target rule's result in topological order. It is wiring, not input:
6090    /// [`crate::evaluation::evaluate_data::EvaluateData`] naming it is a request Error.
6091    Reference {
6092        target: ReferenceTarget,
6093        #[serde(with = "crate::snapshot::shared::arc")]
6094        resolved_type: Arc<LemmaType>,
6095        local_constraints: Option<Vec<Constraint>>,
6096        local_suggestion: Option<BoundValueKind>,
6097        local_value: Option<BoundValueKind>,
6098        source: Source,
6099    },
6100}
6101
6102impl DataDefinition {
6103    /// Declared lemma type for value, type-declaration, and reference data; `None` for imports.
6104    pub fn lemma_type(&self) -> Option<&LemmaType> {
6105        self.resolved_type_arc().map(|arc| arc.as_ref())
6106    }
6107
6108    /// Shared [`Arc`] for the declared type; `None` for imports.
6109    pub fn resolved_type_arc(&self) -> Option<&Arc<LemmaType>> {
6110        match self {
6111            DataDefinition::Value { resolved_type, .. } => Some(resolved_type),
6112            DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
6113            DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
6114            DataDefinition::InstanceList { resolved_type, .. } => Some(resolved_type),
6115            DataDefinition::Import { .. } => None,
6116        }
6117    }
6118
6119    /// Alias for [`Self::lemma_type`] (historical name).
6120    #[inline]
6121    pub fn schema_type(&self) -> Option<&LemmaType> {
6122        self.lemma_type()
6123    }
6124
6125    /// Returns the literal value when the data already holds one, including `-> with`
6126    /// on a type declaration or reference. A data-target `Reference`'s copied value
6127    /// is produced by the evaluator at runtime, so at plan-time it has no value yet.
6128    pub fn value(&self) -> Option<LiteralValue> {
6129        match self {
6130            DataDefinition::Value { value, .. } => Some(value.clone()),
6131            DataDefinition::TypeDeclaration {
6132                declared_value: Some(dv),
6133                ..
6134            } => Some(dv.to_literal()),
6135            DataDefinition::Reference {
6136                local_value: Some(dv),
6137                resolved_type: _,
6138                ..
6139            } => Some(dv.to_literal()),
6140            DataDefinition::TypeDeclaration { .. }
6141            | DataDefinition::Import { .. }
6142            | DataDefinition::InstanceList { .. }
6143            | DataDefinition::Reference { .. } => None,
6144        }
6145    }
6146
6147    /// Fill value with optional written measure unit binding (Show display).
6148    pub(crate) fn bound_value(&self) -> Option<&BoundValueKind> {
6149        match self {
6150            DataDefinition::TypeDeclaration {
6151                declared_value: Some(dv),
6152                ..
6153            }
6154            | DataDefinition::Reference {
6155                local_value: Some(dv),
6156                ..
6157            } => Some(dv),
6158            DataDefinition::Value { .. }
6159            | DataDefinition::TypeDeclaration { .. }
6160            | DataDefinition::Reference { .. }
6161            | DataDefinition::InstanceList { .. }
6162            | DataDefinition::Import { .. } => None,
6163        }
6164    }
6165
6166    /// Suggestion with optional written measure unit binding (Show display).
6167    /// Surfaces in [`crate::planning::execution_plan::ShowData::suggestion`] for
6168    /// show/response/UI; the evaluator never commits it — unbound stays MissingData.
6169    pub(crate) fn bound_suggestion(&self) -> Option<&BoundValueKind> {
6170        match self {
6171            DataDefinition::TypeDeclaration {
6172                declared_suggestion: Some(dv),
6173                ..
6174            }
6175            | DataDefinition::Reference {
6176                local_suggestion: Some(dv),
6177                ..
6178            } => Some(dv),
6179            DataDefinition::Value { .. }
6180            | DataDefinition::TypeDeclaration { .. }
6181            | DataDefinition::Reference { .. }
6182            | DataDefinition::InstanceList { .. }
6183            | DataDefinition::Import { .. } => None,
6184        }
6185    }
6186
6187    /// Returns the source location for this data.
6188    pub fn source(&self) -> &Source {
6189        match self {
6190            DataDefinition::Value { source, .. } => source,
6191            DataDefinition::TypeDeclaration { source, .. } => source,
6192            DataDefinition::Import { source, .. } => source,
6193            DataDefinition::InstanceList { source, .. } => source,
6194            DataDefinition::Reference { source, .. } => source,
6195        }
6196    }
6197}
6198
6199/// Bind a type-agnostic [`Value::NumberWithUnit`] using the unit index entry for `unit_name`.
6200pub fn number_with_unit_to_value_kind(
6201    magnitude: rust_decimal::Decimal,
6202    unit_name: &str,
6203    lemma_type: &LemmaType,
6204) -> Result<ValueKind, String> {
6205    match &lemma_type.specifications {
6206        TypeSpecification::Ratio { units, .. } => {
6207            use crate::computation::rational::{checked_div, decimal_to_rational};
6208            let unit = units.get(unit_name)?;
6209            let magnitude_rational = decimal_to_rational(magnitude)
6210                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
6211            let canonical_rational = checked_div(&magnitude_rational, &unit.value)
6212                .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
6213            Ok(ValueKind::Ratio(canonical_rational))
6214        }
6215        TypeSpecification::Measure { units, .. } => {
6216            use crate::computation::rational::checked_mul;
6217            let rational = lift_parser_decimal(magnitude)?;
6218            let unit = units.get(unit_name)?;
6219            let canonical = checked_mul(&rational, &unit.factor)
6220                .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
6221            Ok(ValueKind::Measure(canonical))
6222        }
6223        _ => Err(format!(
6224            "Unit '{}' is defined on type '{}' which is not measure or ratio",
6225            unit_name,
6226            lemma_type.name()
6227        )),
6228    }
6229}
6230
6231/// Whether a [`ValueKind`] is structurally compatible with a [`TypeSpecification`].
6232/// Bound validation (min/max/decimals) is separate; this only checks shape.
6233pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
6234    matches!(
6235        (type_spec, value),
6236        (TypeSpecification::Number { .. }, ValueKind::Number(_))
6237            | (TypeSpecification::Text { .. }, ValueKind::Text(_))
6238            | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
6239            | (TypeSpecification::Date { .. }, ValueKind::Date(_))
6240            | (TypeSpecification::Time { .. }, ValueKind::Time(_))
6241            | (TypeSpecification::Measure { .. }, ValueKind::Measure(_))
6242            | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_))
6243            | (TypeSpecification::Ratio { .. }, ValueKind::Number(_))
6244            | (
6245                TypeSpecification::NumberRange { .. },
6246                ValueKind::Range(_, _)
6247            )
6248            | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
6249            | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
6250            | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
6251            | (
6252                TypeSpecification::MeasureRange { .. },
6253                ValueKind::Range(_, _)
6254            )
6255            | (TypeSpecification::Veto { .. }, _)
6256            | (TypeSpecification::Undetermined, _)
6257            | (TypeSpecification::Spec { .. }, ValueKind::Spec(_))
6258            | (TypeSpecification::List { .. }, ValueKind::List(_))
6259    )
6260}
6261
6262fn parser_value_type_mismatch(
6263    value: &crate::literals::Value,
6264    type_spec: &TypeSpecification,
6265) -> String {
6266    use crate::parsing::ast::AsLemmaSource;
6267    let value_str = format!("{}", AsLemmaSource(value));
6268    match type_spec {
6269        TypeSpecification::Measure { units, .. } => {
6270            let unit_hint = units
6271                .iter()
6272                .find(|u| u.factor == crate::computation::rational::rational_one())
6273                .map(|u| u.name.as_str())
6274                .or_else(|| units.iter().next().map(|u| u.name.as_str()))
6275                .unwrap_or("unit");
6276            format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
6277        }
6278        TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
6279            let unit_hint = units
6280                .iter()
6281                .next()
6282                .map(|u| u.name.as_str())
6283                .unwrap_or("unit");
6284            format!(
6285                "cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}` or bare ratio"
6286            )
6287        }
6288        _ => format!("cannot use {value_str} as {type_spec}"),
6289    }
6290}
6291
6292/// Re-canonicalize a measure literal after compound unit factors were resolved.
6293///
6294/// Literals parsed before derived unit resolution were canonicalized with prefix-only
6295/// factors; multiply by `resolved_factor / stored_factor` to align with final factors.
6296pub fn refresh_measure_literal_canonical_magnitude(
6297    lit: &mut LiteralValue,
6298    previous_type: &LemmaType,
6299    resolved_type: &LemmaType,
6300) -> Result<(), crate::computation::rational::NumericFailure> {
6301    use crate::computation::rational::{checked_div, checked_mul};
6302    let ValueKind::Measure(magnitude) = &mut lit.value else {
6303        return Ok(());
6304    };
6305    // Binding unit is no longer on ValueKind; magnitude was canonicalized at bind time
6306    // against the then-current unit table. When a single-term runtime signature unit
6307    // exists on both tables, rescale if that unit's factor changed.
6308    let signature = previous_type.measure_runtime_signature();
6309    let Some((unit_name, 1)) = signature.first().map(|(n, e)| (n.as_str(), *e)) else {
6310        return Ok(());
6311    };
6312    if signature.len() != 1 {
6313        return Ok(());
6314    }
6315    let stored_factor = previous_type.measure_unit_factor(unit_name);
6316    let resolved_factor = resolved_type.measure_unit_factor(unit_name);
6317    if stored_factor == resolved_factor {
6318        return Ok(());
6319    }
6320    let scaled = checked_mul(magnitude, resolved_factor)?;
6321    *magnitude = checked_div(&scaled, stored_factor)?;
6322    Ok(())
6323}
6324
6325fn number_with_unit_value_kind(
6326    magnitude: rust_decimal::Decimal,
6327    unit_name: &str,
6328    type_spec: &TypeSpecification,
6329) -> Result<ValueKind, String> {
6330    use crate::computation::rational::{checked_div, checked_mul, decimal_to_rational};
6331    match type_spec {
6332        TypeSpecification::Ratio { units, .. } => {
6333            let unit = units.get(unit_name)?;
6334            let magnitude_rational = decimal_to_rational(magnitude)
6335                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
6336            let canonical_rational = checked_div(&magnitude_rational, &unit.value)
6337                .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
6338            Ok(ValueKind::Ratio(canonical_rational))
6339        }
6340        TypeSpecification::Measure { units, .. } => {
6341            let rational = lift_parser_decimal(magnitude)?;
6342            let unit = units.get(unit_name)?;
6343            let canonical = checked_mul(&rational, &unit.factor)
6344                .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
6345            Ok(ValueKind::Measure(canonical))
6346        }
6347        _ => Err("number_with_unit literal requires a measure or ratio type".to_string()),
6348    }
6349}
6350
6351/// Convert parser [`Value`] to [`ValueKind`] using the target type (canonicalizes ratio at bind).
6352///
6353/// When `resolve_unit` is set, qualified range-endpoint unit tokens are resolved to bare
6354/// declared names before bind.
6355pub fn parser_value_to_value_kind(
6356    value: &crate::literals::Value,
6357    type_spec: &TypeSpecification,
6358    resolve_unit: Option<&UnitTokenResolver<'_>>,
6359) -> Result<ValueKind, String> {
6360    use crate::computation::rational::decimal_to_rational;
6361    use crate::literals::Value;
6362    match (value, type_spec) {
6363        (Value::NumberWithUnit(magnitude, unit_name), _) => {
6364            number_with_unit_value_kind(*magnitude, unit_name, type_spec)
6365        }
6366        (Value::Number(n), TypeSpecification::Number { .. }) => {
6367            Ok(ValueKind::Number(lift_parser_decimal(*n)?))
6368        }
6369        (Value::Number(n), TypeSpecification::Ratio { .. }) => {
6370            let r = decimal_to_rational(*n)
6371                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
6372            Ok(ValueKind::Ratio(r))
6373        }
6374        (Value::Text(s), TypeSpecification::Text { .. }) => Ok(ValueKind::Text(s.clone())),
6375        (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
6376        (Value::Date(dt), TypeSpecification::Date { .. }) => {
6377            Ok(ValueKind::Date(date_time_to_semantic(dt)))
6378        }
6379        (Value::Time(t), TypeSpecification::Time { .. }) => {
6380            Ok(ValueKind::Time(time_to_semantic(t)))
6381        }
6382        (
6383            Value::Range(left, right),
6384            range_spec @ (TypeSpecification::NumberRange { .. }
6385            | TypeSpecification::DateRange { .. }
6386            | TypeSpecification::TimeRange { .. }
6387            | TypeSpecification::RatioRange { .. }
6388            | TypeSpecification::MeasureRange { .. }),
6389        ) => {
6390            let endpoint = range_element_type_specification(range_spec).ok_or_else(|| {
6391                "BUG: range_element_type_specification missing arm for range type".to_string()
6392            })?;
6393            let left_lit = lift_range_endpoint(left, &endpoint, resolve_unit)?;
6394            let right_lit = lift_range_endpoint(right, &endpoint, resolve_unit)?;
6395            Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
6396        }
6397        (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
6398    }
6399}
6400
6401/// Convert parser Value to ValueKind for primitives and ranges only.
6402///
6403/// [`Value::NumberWithUnit`] requires [`parser_value_to_value_kind`] with a measure or ratio type.
6404pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
6405    use crate::parsing::ast::Value;
6406    Ok(match value {
6407        Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
6408        Value::Text(s) => ValueKind::Text(s.clone()),
6409        Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
6410        Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
6411        Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
6412        Value::NumberWithUnit(_, _) => {
6413            return Err(
6414                "number_with_unit literal requires type context (measure or ratio)".to_string(),
6415            );
6416        }
6417        Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
6418    })
6419}
6420
6421/// Convert AST date-time to semantic (for tests and planning).
6422pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
6423    SemanticDateTime {
6424        year: dt.year,
6425        month: dt.month,
6426        day: dt.day,
6427        hour: dt.hour,
6428        minute: dt.minute,
6429        second: dt.second,
6430        microsecond: dt.microsecond,
6431        timezone: dt.timezone.as_ref().map(SemanticTimezone::from),
6432    }
6433}
6434
6435/// Convert AST time to semantic (for tests and planning).
6436pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
6437    SemanticTime {
6438        hour: t.hour.into(),
6439        minute: t.minute.into(),
6440        second: t.second.into(),
6441        microsecond: t.microsecond,
6442        timezone: t.timezone.as_ref().map(SemanticTimezone::from),
6443    }
6444}
6445
6446/// Compare two semantic date-time values by year, month, day, hour, minute,
6447/// second, then microsecond. Timezone normalisation is a separate concern
6448/// handled at evaluation time.
6449pub(crate) fn compare_semantic_dates(
6450    left: &SemanticDateTime,
6451    right: &SemanticDateTime,
6452) -> std::cmp::Ordering {
6453    left.year
6454        .cmp(&right.year)
6455        .then_with(|| left.month.cmp(&right.month))
6456        .then_with(|| left.day.cmp(&right.day))
6457        .then_with(|| left.hour.cmp(&right.hour))
6458        .then_with(|| left.minute.cmp(&right.minute))
6459        .then_with(|| left.second.cmp(&right.second))
6460        .then_with(|| left.microsecond.cmp(&right.microsecond))
6461}
6462
6463/// Compare two semantic time values by hour, minute, second, then microsecond.
6464/// Timezone is excluded for the same reason as [`compare_semantic_dates`].
6465pub(crate) fn compare_semantic_times(
6466    left: &SemanticTime,
6467    right: &SemanticTime,
6468) -> std::cmp::Ordering {
6469    left.hour
6470        .cmp(&right.hour)
6471        .then_with(|| left.minute.cmp(&right.minute))
6472        .then_with(|| left.second.cmp(&right.second))
6473        .then_with(|| left.microsecond.cmp(&right.microsecond))
6474}
6475
6476/// Convert AST conversion target to semantic (planning boundary; evaluation/computation use only semantic).
6477pub fn conversion_target_to_semantic(
6478    ct: &ConversionTarget,
6479    unit_index: Option<&crate::planning::unit_index::UnitIndex>,
6480    resolved_types: Option<&IndexMap<String, Arc<LemmaType>>>,
6481) -> Result<SemanticConversionTarget, String> {
6482    match ct {
6483        ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
6484        ConversionTarget::Unit { unit_name } => {
6485            let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
6486            let (bare, owning_type) = match resolved_types {
6487                Some(resolved) => index.resolve_with_named_types(unit_name, resolved)?,
6488                None => index.resolve(unit_name)?,
6489            };
6490            Ok(SemanticConversionTarget::Unit {
6491                unit_name: bare,
6492                owning_type,
6493            })
6494        }
6495    }
6496}
6497
6498// -----------------------------------------------------------------------------
6499// Primitive type constructors (moved from parsing::ast)
6500// -----------------------------------------------------------------------------
6501
6502// Statics for lazy initialization of production-used primitive types.
6503static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
6504static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
6505static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
6506static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6507static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6508static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
6509static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
6510static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6511static PRIMITIVE_MEASURE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6512static PRIMITIVE_NUMBER_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6513static PRIMITIVE_RATIO_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6514static PRIMITIVE_TIME_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
6515
6516#[must_use]
6517pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
6518    lemma_type_for_primitive(PrimitiveKind::Boolean)
6519}
6520
6521#[must_use]
6522pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
6523    lemma_type_for_primitive(PrimitiveKind::Number)
6524}
6525
6526#[must_use]
6527pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
6528    lemma_type_for_primitive(PrimitiveKind::Text)
6529}
6530
6531#[must_use]
6532pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
6533    lemma_type_for_primitive(PrimitiveKind::Date)
6534}
6535
6536#[must_use]
6537pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
6538    lemma_type_for_primitive(PrimitiveKind::DateRange)
6539}
6540
6541#[must_use]
6542pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
6543    lemma_type_for_primitive(PrimitiveKind::Time)
6544}
6545
6546#[must_use]
6547pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
6548    lemma_type_for_primitive(PrimitiveKind::Ratio)
6549}
6550
6551/// Singleton primitive `LemmaType` for `kind`. Same `Arc` for the same primitive for the process.
6552#[must_use]
6553pub fn lemma_type_for_primitive(kind: PrimitiveKind) -> &'static Arc<LemmaType> {
6554    let cell = match kind {
6555        PrimitiveKind::Boolean => &PRIMITIVE_BOOLEAN,
6556        PrimitiveKind::Number => &PRIMITIVE_NUMBER,
6557        PrimitiveKind::Text => &PRIMITIVE_TEXT,
6558        PrimitiveKind::Date => &PRIMITIVE_DATE,
6559        PrimitiveKind::DateRange => &PRIMITIVE_DATE_RANGE,
6560        PrimitiveKind::Time => &PRIMITIVE_TIME,
6561        PrimitiveKind::Ratio => &PRIMITIVE_RATIO,
6562        PrimitiveKind::Measure => &PRIMITIVE_MEASURE,
6563        PrimitiveKind::MeasureRange => &PRIMITIVE_MEASURE_RANGE,
6564        PrimitiveKind::NumberRange => &PRIMITIVE_NUMBER_RANGE,
6565        PrimitiveKind::RatioRange => &PRIMITIVE_RATIO_RANGE,
6566        PrimitiveKind::TimeRange => &PRIMITIVE_TIME_RANGE,
6567    };
6568    cell.get_or_init(|| Arc::new(LemmaType::primitive(type_spec_for_primitive(kind))))
6569}
6570
6571/// Map PrimitiveKind to TypeSpecification. Single source of truth for primitive type resolution.
6572#[must_use]
6573pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
6574    match kind {
6575        PrimitiveKind::Boolean => TypeSpecification::boolean(),
6576        PrimitiveKind::Measure => TypeSpecification::measure(),
6577        PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
6578        PrimitiveKind::Number => TypeSpecification::number(),
6579        PrimitiveKind::NumberRange => TypeSpecification::number_range(),
6580        PrimitiveKind::Ratio => TypeSpecification::ratio(),
6581        PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
6582        PrimitiveKind::Text => TypeSpecification::text(),
6583        PrimitiveKind::Date => TypeSpecification::date(),
6584        PrimitiveKind::DateRange => TypeSpecification::date_range(),
6585        PrimitiveKind::Time => TypeSpecification::time(),
6586        PrimitiveKind::TimeRange => TypeSpecification::time_range(),
6587    }
6588}
6589
6590// -----------------------------------------------------------------------------
6591// Display implementations
6592// -----------------------------------------------------------------------------
6593
6594impl fmt::Display for PathSegment {
6595    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6596        match &self.repository {
6597            Some(repository) => write!(f, "{} → {} {}", self.uses, repository, self.spec),
6598            None => write!(f, "{} → {}", self.uses, self.spec),
6599        }
6600    }
6601}
6602
6603impl fmt::Display for DataPath {
6604    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6605        write!(f, "{}", self.input_key())
6606    }
6607}
6608
6609impl fmt::Display for RulePath {
6610    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6611        write!(f, "{}", self.input_key())
6612    }
6613}
6614
6615impl fmt::Display for LemmaType {
6616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6617        write!(f, "{}", self.name())
6618    }
6619}
6620
6621pub(crate) fn format_decimal_for_api(
6622    decimal: rust_decimal::Decimal,
6623    decimal_places: Option<u8>,
6624) -> rust_decimal::Decimal {
6625    match decimal_places {
6626        Some(decimal_places) => {
6627            let places = u32::from(decimal_places);
6628            let mut rounded = decimal.round_dp(places);
6629            rounded.rescale(places);
6630            rounded
6631        }
6632        None => {
6633            let normalized = decimal.normalize();
6634            if normalized.fract().is_zero() {
6635                normalized.trunc()
6636            } else {
6637                normalized
6638            }
6639        }
6640    }
6641}
6642
6643fn format_decimal_for_human_display(
6644    decimal: rust_decimal::Decimal,
6645    decimal_places: Option<u8>,
6646) -> String {
6647    match decimal_places {
6648        Some(decimal_places) => {
6649            let rounded = decimal.round_dp(u32::from(decimal_places));
6650            format!("{:.prec$}", rounded, prec = decimal_places as usize)
6651        }
6652        None => decimal.normalize().to_string(),
6653    }
6654}
6655
6656fn format_rational_for_human_display(
6657    magnitude: &crate::computation::rational::RationalInteger,
6658    decimal_places: Option<u8>,
6659) -> String {
6660    match magnitude.try_to_decimal() {
6661        Ok(decimal) => format_decimal_for_human_display(decimal, decimal_places),
6662        Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
6663        Err(_) => magnitude.display_str(),
6664    }
6665}
6666
6667fn format_measure_canonical_for_display(
6668    canonical: &crate::computation::rational::RationalInteger,
6669    lemma_type: &LemmaType,
6670    signature: &[(String, i32)],
6671) -> String {
6672    use crate::computation::rational::checked_div;
6673
6674    let decimals = lemma_type.decimal_places();
6675
6676    if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
6677        if !units.is_empty() {
6678            // Binding (`as` / suggest / fill) must name a declared unit; otherwise first declared.
6679            let unit = match lemma_type.unit() {
6680                Some(binding) => units
6681                    .iter()
6682                    .find(|unit| unit.name == binding)
6683                    .unwrap_or_else(|| {
6684                        panic!(
6685                            "BUG: measure display binding '{binding}' is not a declared unit on type {}",
6686                            lemma_type.name()
6687                        )
6688                    }),
6689                None => units
6690                    .iter()
6691                    .next()
6692                    .expect("BUG: measure type with non-empty units must have a first unit"),
6693            };
6694            let in_unit = checked_div(canonical, &unit.factor)
6695                .expect("BUG: de-canonicalization for measure display must not fail");
6696            let formatted = format_rational_for_human_display(&in_unit, decimals);
6697            return format!("{} {}", formatted, unit.name);
6698        }
6699    }
6700
6701    let unit_label = match signature {
6702        [] => String::new(),
6703        [(name, 1)] => name.clone(),
6704        _ => format_signature_operator_style(signature),
6705    };
6706    let formatted = format_rational_for_human_display(canonical, decimals);
6707    if unit_label.is_empty() {
6708        formatted
6709    } else {
6710        format!("{formatted} {unit_label}")
6711    }
6712}
6713
6714fn format_ratio_canonical_for_display(
6715    canonical: &crate::computation::rational::RationalInteger,
6716    lemma_type: &LemmaType,
6717) -> String {
6718    use crate::computation::rational::checked_mul;
6719
6720    let units = match &lemma_type.specifications {
6721        TypeSpecification::Ratio { units, .. } | TypeSpecification::RatioRange { units, .. } => {
6722            units
6723        }
6724        _ => unreachable!(
6725            "BUG: ratio display with non-ratio type {}",
6726            lemma_type.name()
6727        ),
6728    };
6729    let display_unit = lemma_type
6730        .unit()
6731        .or_else(|| lemma_type.ratio_primary_unit());
6732    let Some(unit_name) = display_unit else {
6733        return format_rational_magnitude_for_display(canonical);
6734    };
6735    let unit = units.get(unit_name).unwrap_or_else(|_| {
6736        panic!(
6737            "BUG: ratio display unit '{unit_name}' is not declared on type {}",
6738            lemma_type.name()
6739        )
6740    });
6741    let in_unit = checked_mul(canonical, &unit.value)
6742        .expect("BUG: de-canonicalization for ratio display must not fail");
6743    format_number_with_unit_for_display(&in_unit, unit_name)
6744}
6745
6746impl fmt::Display for LiteralValue {
6747    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6748        // Measure/ratio display needs an explicit type; callers must use
6749        // `display_value_with_type`. Bare Display only covers type-free kinds and
6750        // ranges of those kinds.
6751        match &self.value {
6752            ValueKind::Measure(_) | ValueKind::Ratio(_) => {
6753                write!(f, "{}", self.value)
6754            }
6755            ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
6756            _ => write!(f, "{}", self.value),
6757        }
6758    }
6759}
6760
6761// -----------------------------------------------------------------------------
6762// Tests
6763// -----------------------------------------------------------------------------
6764
6765#[cfg(test)]
6766pub(crate) mod tests {
6767    use super::*;
6768    use crate::computation::rational::decimal_to_rational;
6769    use crate::literals::DateGranularity;
6770    use crate::literals::Value;
6771    use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
6772    use rust_decimal::Decimal;
6773    use std::str::FromStr;
6774
6775    #[must_use]
6776    pub(crate) fn test_type_family(name: &str) -> TypeFamily {
6777        TypeFamily {
6778            repository: None,
6779            spec: "test".to_string(),
6780            effective: crate::parsing::ast::EffectiveDate::Origin,
6781            name: name.to_string(),
6782        }
6783    }
6784
6785    #[must_use]
6786    pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
6787        crate::planning::semantics::lemma_type_for_primitive(PrimitiveKind::Measure)
6788    }
6789
6790    #[must_use]
6791    pub(crate) fn primitive_measure() -> &'static LemmaType {
6792        primitive_measure_arc().as_ref()
6793    }
6794
6795    #[test]
6796    fn default_primitive_help_is_goal_oriented() {
6797        let kinds = [
6798            PrimitiveKind::Boolean,
6799            PrimitiveKind::Measure,
6800            PrimitiveKind::MeasureRange,
6801            PrimitiveKind::Number,
6802            PrimitiveKind::NumberRange,
6803            PrimitiveKind::Ratio,
6804            PrimitiveKind::RatioRange,
6805            PrimitiveKind::Text,
6806            PrimitiveKind::Date,
6807            PrimitiveKind::DateRange,
6808            PrimitiveKind::Time,
6809            PrimitiveKind::TimeRange,
6810        ];
6811        for kind in kinds {
6812            let spec = type_spec_for_primitive(kind);
6813            let help = match &spec {
6814                TypeSpecification::Boolean { help, .. }
6815                | TypeSpecification::Number { help, .. }
6816                | TypeSpecification::NumberRange { help, .. }
6817                | TypeSpecification::Text { help, .. }
6818                | TypeSpecification::Measure { help, .. }
6819                | TypeSpecification::MeasureRange { help, .. }
6820                | TypeSpecification::Ratio { help, .. }
6821                | TypeSpecification::RatioRange { help, .. }
6822                | TypeSpecification::Date { help, .. }
6823                | TypeSpecification::DateRange { help, .. }
6824                | TypeSpecification::TimeRange { help, .. }
6825                | TypeSpecification::Time { help, .. } => help,
6826                TypeSpecification::Veto { .. }
6827                | TypeSpecification::Undetermined
6828                | TypeSpecification::Spec { .. }
6829                | TypeSpecification::List { .. } => {
6830                    unreachable!(
6831                        "BUG: primitive kind {:?} mapped to non-primitive spec",
6832                        kind
6833                    )
6834                }
6835            };
6836            assert!(!help.is_empty(), "help for {:?}", kind);
6837            assert!(
6838                !help.to_ascii_lowercase().contains("format:"),
6839                "help for {:?} must not describe syntax: {:?}",
6840                kind,
6841                help
6842            );
6843            assert_eq!(help, default_help_for_primitive(kind));
6844        }
6845    }
6846
6847    #[test]
6848    fn data_path_and_rule_path_display_are_input_key() {
6849        let implicit = PathSegment {
6850            uses: "bag".to_string(),
6851            repository: None,
6852            spec: "bag".to_string(),
6853        };
6854        let explicit = PathSegment {
6855            uses: "bag".to_string(),
6856            repository: None,
6857            spec: "nut_bag".to_string(),
6858        };
6859        let named_repo = PathSegment {
6860            uses: "calc".to_string(),
6861            repository: Some("alpha".to_string()),
6862            spec: "pricing".to_string(),
6863        };
6864
6865        assert_eq!(
6866            DataPath::new(vec![implicit.clone()], "weight".to_string()).to_string(),
6867            "bag.weight"
6868        );
6869        assert_eq!(
6870            DataPath::new(vec![explicit.clone()], "weight".to_string()).to_string(),
6871            "bag.weight"
6872        );
6873        assert_eq!(
6874            DataPath::new(vec![named_repo.clone()], "price".to_string()).to_string(),
6875            "calc.price"
6876        );
6877        assert_eq!(
6878            RulePath::new(vec![implicit], "cost".to_string()).to_string(),
6879            "bag.cost"
6880        );
6881        assert_eq!(
6882            RulePath::new(vec![explicit], "cost".to_string()).to_string(),
6883            "bag.cost"
6884        );
6885        assert_eq!(
6886            RulePath::new(vec![named_repo], "price".to_string()).to_string(),
6887            "calc.price"
6888        );
6889
6890        assert_eq!(
6891            PathSegment {
6892                uses: "bag".to_string(),
6893                repository: None,
6894                spec: "nut_bag".to_string(),
6895            }
6896            .to_string(),
6897            "bag → nut_bag"
6898        );
6899        assert_eq!(
6900            PathSegment {
6901                uses: "calc".to_string(),
6902                repository: Some("alpha".to_string()),
6903                spec: "pricing".to_string(),
6904            }
6905            .to_string(),
6906            "calc → alpha pricing"
6907        );
6908    }
6909
6910    #[test]
6911    fn test_negated_comparison() {
6912        assert_eq!(
6913            negated_comparison(ComparisonComputation::LessThan),
6914            ComparisonComputation::GreaterThanOrEqual
6915        );
6916        assert_eq!(
6917            negated_comparison(ComparisonComputation::GreaterThanOrEqual),
6918            ComparisonComputation::LessThan
6919        );
6920        assert_eq!(
6921            negated_comparison(ComparisonComputation::Is),
6922            ComparisonComputation::IsNot
6923        );
6924        assert_eq!(
6925            negated_comparison(ComparisonComputation::IsNot),
6926            ComparisonComputation::Is
6927        );
6928    }
6929
6930    #[test]
6931    fn value_to_semantic_number_is_decimal() {
6932        let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
6933        assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
6934    }
6935
6936    #[test]
6937    fn value_kind_measure_serializes_magnitude_only() {
6938        let kind =
6939            ValueKind::Measure(decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap());
6940        let json = serde_json::to_value(crate::api::ValueKind::from(&kind)).unwrap();
6941        assert_eq!(json["measure"]["value"], "99.5");
6942        assert!(json["measure"].get("signature").is_none());
6943    }
6944
6945    #[test]
6946    fn value_kind_measure_roundtrips() {
6947        let original =
6948            ValueKind::Measure(decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap());
6949        let api = crate::api::ValueKind::from(&original);
6950        let json = serde_json::to_string(&api).unwrap();
6951        let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
6952        assert_eq!(api, parsed);
6953    }
6954
6955    #[test]
6956    fn value_kind_measure_empty_roundtrips() {
6957        let original =
6958            ValueKind::Measure(decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap());
6959        let api = crate::api::ValueKind::from(&original);
6960        let json = serde_json::to_string(&api).unwrap();
6961        let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
6962        assert_eq!(api, parsed);
6963    }
6964
6965    #[test]
6966    fn literal_value_number_serde_not_rational_array() {
6967        let lit = LiteralValue::number_from_decimal(Decimal::from(20));
6968        let json = serde_json::to_value(crate::api::LiteralValue::from(&lit)).unwrap();
6969        let number = json
6970            .get("value")
6971            .and_then(|v| v.get("number"))
6972            .expect("number field");
6973        assert!(number.is_string());
6974        assert_eq!(number.as_str(), Some("20"));
6975        assert!(
6976            !number.is_array(),
6977            "stored number must not serialize as [n,d]"
6978        );
6979    }
6980
6981    #[test]
6982    fn test_literal_value_to_primitive_type() {
6983        let one = rational_new(1, 1);
6984
6985        assert_eq!(TypedLiteral::text("".to_string()).lemma_type.name(), "text");
6986        assert_eq!(
6987            TypedLiteral::number(one.clone()).lemma_type.name(),
6988            "number"
6989        );
6990        assert_eq!(
6991            TypedLiteral::from_bool(bool::from(BooleanValue::True))
6992                .lemma_type
6993                .name(),
6994            "boolean"
6995        );
6996
6997        let dt = DateTimeValue {
6998            year: 2024,
6999            month: 1,
7000            day: 1,
7001            hour: 0,
7002            minute: 0,
7003            second: 0,
7004            microsecond: 0,
7005            timezone: None,
7006
7007            granularity: DateGranularity::Full,
7008        };
7009        assert_eq!(
7010            TypedLiteral::date(date_time_to_semantic(&dt))
7011                .lemma_type
7012                .name(),
7013            "date"
7014        );
7015        assert_eq!(
7016            TypedLiteral::ratio_from_decimal(Decimal::new(1, 2))
7017                .lemma_type
7018                .name(),
7019            "ratio"
7020        );
7021        let dur_type = LemmaType::new(
7022            "duration".to_string(),
7023            TypeSpecification::Measure {
7024                minimum: None,
7025                maximum: None,
7026                decimals: None,
7027                units: MeasureUnits::from(vec![MeasureUnit {
7028                    name: "second".to_string(),
7029                    factor: crate::computation::rational::rational_one(),
7030                    derived_measure_factors: Vec::new(),
7031                    decomposition: BaseMeasureVector::new(),
7032                    minimum: None,
7033                    maximum: None,
7034                    suggestion_magnitude: None,
7035                }]),
7036                traits: vec![MeasureTrait::Duration],
7037                decomposition: None,
7038                unit: None,
7039                help: String::new(),
7040            },
7041            TypeExtends::Primitive,
7042        );
7043        assert_eq!(
7044            TypedLiteral::measure_with_type(one.clone(), Arc::new(dur_type))
7045                .lemma_type
7046                .name(),
7047            "duration"
7048        );
7049    }
7050
7051    #[test]
7052    fn test_type_display() {
7053        let specs = TypeSpecification::text();
7054        let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
7055        assert_eq!(format!("{}", lemma_type), "name");
7056    }
7057
7058    #[test]
7059    fn test_type_serialization() {
7060        let specs = TypeSpecification::number();
7061        let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
7062        let api = crate::api::LemmaType::from(&lemma_type);
7063        let serialized = serde_json::to_string(&api).unwrap();
7064        let deserialized: crate::api::LemmaType = serde_json::from_str(&serialized).unwrap();
7065        assert_eq!(api, deserialized);
7066    }
7067
7068    #[test]
7069    fn test_literal_value_display_value() {
7070        let ten = rational_new(10, 1);
7071
7072        assert_eq!(
7073            LiteralValue::text("hello".to_string()).display_value(),
7074            "hello"
7075        );
7076        assert_eq!(LiteralValue::number(ten).display_value(), "10");
7077        assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
7078        assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
7079
7080        // 0.10 ratio with "percent" binding displays as 10% (unit conversion applied)
7081        let ratio_ty = Arc::new(primitive_ratio_arc().as_ref().clone().with_unit("percent"));
7082        let ten_percent_ratio = LiteralValue::ratio(
7083            crate::literals::rational_from_parsed_decimal(Decimal::new(1, 1))
7084                .expect("ratio decimal"),
7085        );
7086        assert_eq!(
7087            ten_percent_ratio.display_value_with_type(ratio_ty.as_ref()),
7088            "10%"
7089        );
7090
7091        let time = TimeValue {
7092            hour: 14,
7093            minute: 30,
7094            second: 0,
7095            microsecond: 0,
7096            timezone: None,
7097        };
7098        let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
7099        assert!(time_display.contains("14"));
7100        assert!(time_display.contains("30"));
7101    }
7102
7103    #[test]
7104    fn test_measure_display_respects_type_decimals() {
7105        let money_type = LemmaType {
7106            name: Some("money".to_string()),
7107            specifications: TypeSpecification::Measure {
7108                minimum: None,
7109                maximum: None,
7110                decimals: Some(2),
7111                units: MeasureUnits::from(vec![MeasureUnit {
7112                    name: "eur".to_string(),
7113                    factor: crate::computation::rational::rational_one(),
7114                    derived_measure_factors: Vec::new(),
7115                    decomposition: BaseMeasureVector::new(),
7116                    minimum: None,
7117                    maximum: None,
7118                    suggestion_magnitude: None,
7119                }]),
7120                traits: Vec::new(),
7121                decomposition: None,
7122                unit: None,
7123                help: String::new(),
7124            },
7125            extends: TypeExtends::Primitive,
7126        };
7127        let money_type = Arc::new(money_type);
7128        let val =
7129            LiteralValue::measure(decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap());
7130        assert_eq!(val.display_value_with_type(money_type.as_ref()), "1.80 eur");
7131        let more_precision = LiteralValue::measure(
7132            decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
7133        );
7134        assert_eq!(
7135            more_precision.display_value_with_type(money_type.as_ref()),
7136            "1.80 eur"
7137        );
7138        let measure_no_decimals = Arc::new(LemmaType {
7139            name: Some("count".to_string()),
7140            specifications: TypeSpecification::Measure {
7141                minimum: None,
7142                maximum: None,
7143                decimals: None,
7144                units: MeasureUnits::from(vec![MeasureUnit {
7145                    name: "items".to_string(),
7146                    factor: crate::computation::rational::rational_one(),
7147                    derived_measure_factors: Vec::new(),
7148                    decomposition: BaseMeasureVector::new(),
7149                    minimum: None,
7150                    maximum: None,
7151                    suggestion_magnitude: None,
7152                }]),
7153                traits: Vec::new(),
7154                decomposition: None,
7155                unit: None,
7156                help: String::new(),
7157            },
7158            extends: TypeExtends::Primitive,
7159        });
7160        let val_any = LiteralValue::measure(
7161            decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
7162        );
7163        assert_eq!(
7164            val_any.display_value_with_type(measure_no_decimals.as_ref()),
7165            "42.5 items"
7166        );
7167    }
7168
7169    #[test]
7170    fn test_literal_value_time_type() {
7171        let time = TimeValue {
7172            hour: 14,
7173            minute: 30,
7174            second: 0,
7175            microsecond: 0,
7176            timezone: None,
7177        };
7178        let lit = TypedLiteral::time(time_to_semantic(&time));
7179        assert_eq!(lit.lemma_type.name(), "time");
7180    }
7181
7182    #[test]
7183    fn test_measure_family_name_none_without_declaring_spec() {
7184        let money_primitive = LemmaType::new(
7185            "money".to_string(),
7186            TypeSpecification::measure(),
7187            TypeExtends::Primitive,
7188        );
7189        assert_eq!(money_primitive.measure_family_name(), None);
7190    }
7191
7192    #[test]
7193    fn test_measure_family_name_custom() {
7194        let measure_spec = TypeSpecification::measure();
7195        let money_custom = LemmaType::new(
7196            "money".to_string(),
7197            measure_spec,
7198            TypeExtends::custom_local("money".to_string(), test_type_family("money")),
7199        );
7200        assert_eq!(money_custom.measure_family_name(), Some("money"));
7201    }
7202
7203    #[test]
7204    fn test_same_named_roots_in_different_specs_are_different_families() {
7205        let declared_in = |spec: &str| TypeFamily {
7206            repository: None,
7207            spec: spec.to_string(),
7208            effective: crate::parsing::ast::EffectiveDate::Origin,
7209            name: "money".to_string(),
7210        };
7211        let pricing_money = LemmaType::new(
7212            "money".to_string(),
7213            TypeSpecification::measure(),
7214            TypeExtends::custom_local("measure".to_string(), declared_in("pricing")),
7215        );
7216        let shop_money = LemmaType::new(
7217            "money".to_string(),
7218            TypeSpecification::measure(),
7219            TypeExtends::custom_local("measure".to_string(), declared_in("shop")),
7220        );
7221        assert!(!pricing_money.same_measure_family(&shop_money));
7222        assert!(!shop_money.same_measure_family(&pricing_money));
7223    }
7224
7225    #[test]
7226    fn test_same_measure_family_parent_and_child() {
7227        let measure_spec = TypeSpecification::measure();
7228        let type_x = LemmaType::new(
7229            "x".to_string(),
7230            measure_spec.clone(),
7231            TypeExtends::custom_local("measure".to_string(), test_type_family("x")),
7232        );
7233        let type_x2 = LemmaType::new(
7234            "x2".to_string(),
7235            measure_spec,
7236            TypeExtends::custom_local("x".to_string(), test_type_family("x")),
7237        );
7238        assert_eq!(type_x.measure_family_name(), Some("x"));
7239        assert_eq!(type_x2.measure_family_name(), Some("x"));
7240        assert!(type_x.same_measure_family(&type_x2));
7241        assert!(type_x2.same_measure_family(&type_x));
7242    }
7243
7244    #[test]
7245    fn test_same_measure_family_siblings() {
7246        let measure_spec = TypeSpecification::measure();
7247        let type_x2_a = LemmaType::new(
7248            "x2a".to_string(),
7249            measure_spec.clone(),
7250            TypeExtends::custom_local("x".to_string(), test_type_family("x")),
7251        );
7252        let type_x2_b = LemmaType::new(
7253            "x2b".to_string(),
7254            measure_spec,
7255            TypeExtends::custom_local("x".to_string(), test_type_family("x")),
7256        );
7257        assert!(type_x2_a.same_measure_family(&type_x2_b));
7258    }
7259
7260    #[test]
7261    fn test_same_measure_family_different_families() {
7262        let measure_spec = TypeSpecification::measure();
7263        let money = LemmaType::new(
7264            "money".to_string(),
7265            measure_spec.clone(),
7266            TypeExtends::Primitive,
7267        );
7268        let temperature = LemmaType::new(
7269            "temperature".to_string(),
7270            measure_spec,
7271            TypeExtends::Primitive,
7272        );
7273        assert!(!money.same_measure_family(&temperature));
7274        assert!(!temperature.same_measure_family(&money));
7275    }
7276
7277    #[test]
7278    fn test_same_measure_family_measure_vs_non_measure() {
7279        let measure_spec = TypeSpecification::measure();
7280        let number_spec = TypeSpecification::number();
7281        let measure_type =
7282            LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
7283        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
7284        assert!(!measure_type.same_measure_family(&number_type));
7285        assert!(!number_type.same_measure_family(&measure_type));
7286    }
7287
7288    #[test]
7289    fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
7290        let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
7291        let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
7292
7293        assert!(!left.same_measure_family(&right));
7294        assert!(left.compatible_with_anonymous_measure(&right));
7295    }
7296
7297    #[test]
7298    fn test_measure_family_name_non_measure_returns_none() {
7299        let number_spec = TypeSpecification::number();
7300        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
7301        assert_eq!(number_type.measure_family_name(), None);
7302    }
7303
7304    #[test]
7305    fn test_lemma_type_inequality_local_vs_import_same_shape() {
7306        let measure_spec = TypeSpecification::measure();
7307        let local = LemmaType::new(
7308            "t".to_string(),
7309            measure_spec.clone(),
7310            TypeExtends::custom_local("money".to_string(), test_type_family("money")),
7311        );
7312        let imported = LemmaType::new(
7313            "t".to_string(),
7314            measure_spec,
7315            TypeExtends::Custom {
7316                parent: "money".to_string(),
7317                family: test_type_family("money"),
7318                defining_spec: TypeDefiningSpec::Import,
7319            },
7320        );
7321        assert_ne!(local, imported);
7322    }
7323
7324    #[test]
7325    fn test_lemma_type_equality_import_unit_variant() {
7326        let measure_spec = TypeSpecification::measure();
7327        let left = LemmaType::new(
7328            "t".to_string(),
7329            measure_spec.clone(),
7330            TypeExtends::Custom {
7331                parent: "money".to_string(),
7332                family: test_type_family("money"),
7333                defining_spec: TypeDefiningSpec::Import,
7334            },
7335        );
7336        let right = LemmaType::new(
7337            "t".to_string(),
7338            measure_spec,
7339            TypeExtends::Custom {
7340                parent: "money".to_string(),
7341                family: test_type_family("money"),
7342                defining_spec: TypeDefiningSpec::Import,
7343            },
7344        );
7345        assert_eq!(left, right);
7346    }
7347
7348    fn month_suggestion_arg() -> CommandArg {
7349        CommandArg::Literal(crate::literals::Value::NumberWithUnit(
7350            Decimal::ONE,
7351            "month".to_string(),
7352        ))
7353    }
7354
7355    fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
7356        [
7357            CommandArg::Label(name.to_string()),
7358            CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
7359        ]
7360    }
7361
7362    #[test]
7363    fn default_calendar_on_text_reports_hint() {
7364        let mut specs = TypeSpecification::text();
7365        let mut default = None;
7366        let err = specs
7367            .apply_constraint(
7368                "notes",
7369                TypeConstraintCommand::Suggest,
7370                &[month_suggestion_arg()],
7371                &mut default,
7372                &mut None,
7373                None,
7374            )
7375            .unwrap_err();
7376        assert!(err.contains("Unit 'month' is for calendar data"));
7377        assert!(err.contains("double quotes"));
7378    }
7379
7380    #[test]
7381    fn default_calendar_on_duration_reports_valid_units() {
7382        let mut specs = TypeSpecification::measure();
7383        specs
7384            .apply_constraint(
7385                "duration",
7386                TypeConstraintCommand::Unit,
7387                &unit_factor_arg("second", 1),
7388                &mut None,
7389                &mut None,
7390                None,
7391            )
7392            .unwrap();
7393        specs
7394            .apply_constraint(
7395                "duration",
7396                TypeConstraintCommand::Unit,
7397                &unit_factor_arg("week", 604_800),
7398                &mut None,
7399                &mut None,
7400                None,
7401            )
7402            .unwrap();
7403        specs
7404            .apply_constraint(
7405                "duration",
7406                TypeConstraintCommand::Trait,
7407                &[CommandArg::Label("duration".to_string())],
7408                &mut None,
7409                &mut None,
7410                None,
7411            )
7412            .unwrap();
7413        let mut default = None;
7414        let err = specs
7415            .apply_constraint(
7416                "duration",
7417                TypeConstraintCommand::Suggest,
7418                &[month_suggestion_arg()],
7419                &mut default,
7420                &mut None,
7421                None,
7422            )
7423            .unwrap_err();
7424        assert!(err.contains("Unit 'month' is for calendar data"));
7425        assert!(err.contains("Valid 'duration' units are"));
7426        assert!(err.contains("week"));
7427    }
7428
7429    #[test]
7430    fn default_valid_duration_weeks_accepted() {
7431        let mut specs = TypeSpecification::measure();
7432        specs
7433            .apply_constraint(
7434                "duration",
7435                TypeConstraintCommand::Unit,
7436                &unit_factor_arg("second", 1),
7437                &mut None,
7438                &mut None,
7439                None,
7440            )
7441            .unwrap();
7442        specs
7443            .apply_constraint(
7444                "duration",
7445                TypeConstraintCommand::Unit,
7446                &unit_factor_arg("week", 604_800),
7447                &mut None,
7448                &mut None,
7449                None,
7450            )
7451            .unwrap();
7452        specs
7453            .apply_constraint(
7454                "duration",
7455                TypeConstraintCommand::Trait,
7456                &[CommandArg::Label("duration".to_string())],
7457                &mut None,
7458                &mut None,
7459                None,
7460            )
7461            .unwrap();
7462        let mut default = None;
7463        specs
7464            .apply_constraint(
7465                "duration",
7466                TypeConstraintCommand::Suggest,
7467                &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
7468                    Decimal::from(4),
7469                    "week".to_string(),
7470                ))],
7471                &mut default,
7472                &mut None,
7473                None,
7474            )
7475            .unwrap();
7476        assert!(matches!(
7477            default,
7478            Some(RawSuggestion::Measure {
7479                unit_name,
7480                ..
7481            }) if unit_name == "week"
7482        ));
7483    }
7484
7485    #[test]
7486    fn default_unknown_unit_on_duration_lists_valid_units() {
7487        let mut specs = TypeSpecification::measure();
7488        specs
7489            .apply_constraint(
7490                "duration",
7491                TypeConstraintCommand::Unit,
7492                &unit_factor_arg("second", 1),
7493                &mut None,
7494                &mut None,
7495                None,
7496            )
7497            .unwrap();
7498        specs
7499            .apply_constraint(
7500                "duration",
7501                TypeConstraintCommand::Trait,
7502                &[CommandArg::Label("duration".to_string())],
7503                &mut None,
7504                &mut None,
7505                None,
7506            )
7507            .unwrap();
7508        let mut default = None;
7509        let err = specs
7510            .apply_constraint(
7511                "duration",
7512                TypeConstraintCommand::Suggest,
7513                &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
7514                    Decimal::ONE,
7515                    "fortnight".to_string(),
7516                ))],
7517                &mut default,
7518                &mut None,
7519                None,
7520            )
7521            .unwrap_err();
7522        assert!(err.contains("fortnight"));
7523        assert!(err.contains("not defined on 'duration'"));
7524        assert!(err.contains("Valid units are"));
7525    }
7526
7527    fn money_measure_type() -> LemmaType {
7528        LemmaType::new(
7529            "Money".to_string(),
7530            TypeSpecification::Measure {
7531                minimum: None,
7532                maximum: None,
7533                decimals: None,
7534                units: MeasureUnits::from(vec![
7535                    MeasureUnit {
7536                        name: "eur".to_string(),
7537                        factor: crate::computation::rational::rational_one(),
7538                        derived_measure_factors: Vec::new(),
7539                        decomposition: BaseMeasureVector::new(),
7540                        minimum: None,
7541                        maximum: None,
7542                        suggestion_magnitude: None,
7543                    },
7544                    MeasureUnit {
7545                        name: "usd".to_string(),
7546                        factor: crate::computation::rational::decimal_to_rational(Decimal::new(
7547                            91, 2,
7548                        ))
7549                        .expect("factor"),
7550                        derived_measure_factors: Vec::new(),
7551                        decomposition: BaseMeasureVector::new(),
7552                        minimum: None,
7553                        maximum: None,
7554                        suggestion_magnitude: None,
7555                    },
7556                ]),
7557                traits: Vec::new(),
7558                decomposition: None,
7559                unit: None,
7560                help: String::new(),
7561            },
7562            TypeExtends::Primitive,
7563        )
7564    }
7565
7566    #[test]
7567    fn measure_unit_names_for_named_measure() {
7568        let money = money_measure_type();
7569        assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
7570    }
7571
7572    // ---------------------------------------------------------------------------
7573    // Phase 0 — pin combine_signatures and canonicalize_signature behavior
7574    // ---------------------------------------------------------------------------
7575
7576    fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
7577        pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
7578    }
7579
7580    #[test]
7581    fn combine_signatures_multiply_adds_exponents() {
7582        let left = sig(&[("eur", 1)]);
7583        let right = sig(&[("hour", -1)]);
7584        let result = combine_signatures(&left, &right, true);
7585        assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
7586    }
7587
7588    #[test]
7589    fn combine_signatures_divide_subtracts_exponents() {
7590        let left = sig(&[("eur", 1)]);
7591        let right = sig(&[("hour", 1)]);
7592        let result = combine_signatures(&left, &right, false);
7593        assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
7594    }
7595
7596    #[test]
7597    fn combine_signatures_cancels_to_empty() {
7598        let left = sig(&[("ce", 1), ("minute", -1)]);
7599        let right = sig(&[("minute", 1)]);
7600        let result = combine_signatures(&left, &right, true);
7601        // ce * (ce/min * min) = ce; minute cancels
7602        assert_eq!(result, sig(&[("ce", 1)]));
7603    }
7604
7605    #[test]
7606    fn combine_signatures_output_is_canonical_form() {
7607        let left = sig(&[("eur", 1), ("hour", 1)]);
7608        let right = sig(&[("minute", 1)]);
7609        let result = combine_signatures(&left, &right, false); // divide
7610                                                               // [("eur",1),("hour",1)] / [("minute",1)] = [("eur",1),("hour",1),("minute",-1)]
7611        let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
7612        assert_eq!(result, expected);
7613    }
7614
7615    #[test]
7616    fn canonicalize_signature_drops_zero_exponents() {
7617        let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
7618        let result = canonicalize_signature(&sig_with_zero);
7619        assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
7620    }
7621
7622    #[test]
7623    fn canonicalize_signature_sorts_by_name() {
7624        let unsorted = sig(&[("minute", -1), ("eur", 1)]);
7625        let result = canonicalize_signature(&unsorted);
7626        assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
7627    }
7628
7629    // ---------------------------------------------------------------------------
7630    // Phase 0 — format_signature_operator_style (to be implemented in
7631    // signature_factor_and_display todo)
7632    // ---------------------------------------------------------------------------
7633
7634    #[test]
7635    fn format_signature_operator_style_numerator_only() {
7636        let signature = sig(&[("eur", 1)]);
7637        let result = format_signature_operator_style(&signature);
7638        assert_eq!(result, "eur");
7639    }
7640
7641    #[test]
7642    fn format_signature_operator_style_with_denominator() {
7643        let signature = sig(&[("eur", 1), ("hour", -1)]);
7644        let result = format_signature_operator_style(&signature);
7645        assert_eq!(result, "eur/hour");
7646    }
7647
7648    #[test]
7649    fn format_signature_operator_style_denominator_only() {
7650        let signature = sig(&[("meter", -1)]);
7651        let result = format_signature_operator_style(&signature);
7652        assert_eq!(result, "1/meter");
7653    }
7654
7655    #[test]
7656    fn format_signature_operator_style_with_exponents() {
7657        let signature = sig(&[("meter", 2), ("second", -2)]);
7658        let result = format_signature_operator_style(&signature);
7659        assert_eq!(result, "meter^2/second^2");
7660    }
7661
7662    // ---------------------------------------------------------------------------
7663    // Phase 0 — calendar_unit_factor (to be implemented in builtin_calendar_factor_table)
7664    // ---------------------------------------------------------------------------
7665
7666    #[test]
7667    fn calendar_unit_factor_table_completeness() {
7668        // Every SemanticCalendarUnit Display string must resolve to a factor.
7669        // Today SemanticCalendarUnit only has Month and Year; more may be added.
7670        for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
7671            let name = unit.to_string();
7672            assert!(
7673                calendar_unit_factor(&name).is_some(),
7674                "calendar_unit_factor('{}') must return Some",
7675                name
7676            );
7677        }
7678    }
7679
7680    #[test]
7681    fn semantic_calendar_unit_display_returns_singular() {
7682        // Today Month => "month", Year => "year" (plural).
7683        // After singular_calendar_names_everywhere, must be "month" and "year".
7684        assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
7685        assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
7686    }
7687
7688    // ---------------------------------------------------------------------------
7689    // Phase 0 — signature_factor (to be implemented in signature_factor_and_display)
7690    // ---------------------------------------------------------------------------
7691
7692    #[test]
7693    fn signature_factor_with_calendar_units() {
7694        let calendar = test_calendar_type_for_signature_factor();
7695        let unit_index = crate::planning::unit_index::UnitIndex::new();
7696        // month factor = 1, year factor = 12.
7697        // [(month,1),(year,-1)] = 1/12
7698        let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
7699        let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
7700            .expect("must not overflow");
7701        let expected = rational_new(1, 12);
7702        assert_eq!(factor, expected, "month/year factor must be 1/12");
7703    }
7704
7705    fn test_calendar_type_for_signature_factor() -> LemmaType {
7706        use crate::computation::rational::{decimal_to_rational, rational_one};
7707        use crate::literals::{MeasureUnit, MeasureUnits};
7708        use rust_decimal::Decimal;
7709        LemmaType::new(
7710            "calendar".to_string(),
7711            TypeSpecification::Measure {
7712                minimum: None,
7713                maximum: None,
7714                decimals: None,
7715                units: MeasureUnits::from(vec![
7716                    MeasureUnit {
7717                        name: "month".to_string(),
7718                        factor: rational_one(),
7719                        minimum: None,
7720                        maximum: None,
7721                        suggestion_magnitude: None,
7722                        decomposition: calendar_decomposition(),
7723                        derived_measure_factors: Vec::new(),
7724                    },
7725                    MeasureUnit {
7726                        name: "year".to_string(),
7727                        factor: decimal_to_rational(Decimal::from(12)).expect("year factor"),
7728                        minimum: None,
7729                        maximum: None,
7730                        suggestion_magnitude: None,
7731                        decomposition: calendar_decomposition(),
7732                        derived_measure_factors: Vec::new(),
7733                    },
7734                ]),
7735                traits: vec![MeasureTrait::Calendar],
7736                decomposition: Some(calendar_decomposition()),
7737                unit: None,
7738                help: String::new(),
7739            },
7740            TypeExtends::Primitive,
7741        )
7742    }
7743
7744    #[test]
7745    #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
7746    fn signature_factor_panics_on_unresolved_name() {
7747        let unit_index = crate::planning::unit_index::UnitIndex::new();
7748        let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
7749        signature_factor(&bad_sig, &unit_index, None)
7750            .expect("BUG: signature_factor must panic before returning a failure");
7751    }
7752
7753    #[test]
7754    fn signature_factor_uses_owner_when_expression_index_empty() {
7755        let money = test_money_type_for_signature_factor();
7756        let expression_units = crate::planning::unit_index::UnitIndex::new();
7757        let sig_usd = sig(&[("usd", 1)]);
7758        let factor =
7759            signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
7760        assert_eq!(factor, rational_new(91, 100));
7761    }
7762
7763    fn test_money_type_for_signature_factor() -> LemmaType {
7764        use crate::computation::rational::decimal_to_rational;
7765        use crate::literals::{MeasureUnit, MeasureUnits};
7766        use rust_decimal::Decimal;
7767        LemmaType::new(
7768            "money".to_string(),
7769            TypeSpecification::Measure {
7770                minimum: None,
7771                maximum: None,
7772                decimals: Some(2),
7773                units: MeasureUnits::from(vec![
7774                    MeasureUnit {
7775                        name: "eur".to_string(),
7776                        factor: crate::computation::rational::rational_one(),
7777                        minimum: None,
7778                        maximum: None,
7779                        suggestion_magnitude: None,
7780                        decomposition: BaseMeasureVector::new(),
7781                        derived_measure_factors: Vec::new(),
7782                    },
7783                    MeasureUnit {
7784                        name: "usd".to_string(),
7785                        factor: decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
7786                        minimum: None,
7787                        maximum: None,
7788                        suggestion_magnitude: None,
7789                        decomposition: BaseMeasureVector::new(),
7790                        derived_measure_factors: Vec::new(),
7791                    },
7792                ]),
7793                traits: Vec::new(),
7794                decomposition: None,
7795                unit: None,
7796                help: String::new(),
7797            },
7798            TypeExtends::Primitive,
7799        )
7800    }
7801
7802    fn measure_type_with_kilogram() -> TypeSpecification {
7803        use crate::computation::rational::rational_one;
7804        use crate::literals::{MeasureUnit, MeasureUnits};
7805        let mut units = MeasureUnits::new();
7806        units.push(MeasureUnit {
7807            name: "kilogram".to_string(),
7808            factor: rational_one(),
7809            minimum: None,
7810            maximum: None,
7811            suggestion_magnitude: None,
7812            decomposition: BaseMeasureVector::new(),
7813            derived_measure_factors: Vec::new(),
7814        });
7815        TypeSpecification::Measure {
7816            minimum: None,
7817            maximum: None,
7818            decimals: None,
7819            units,
7820            traits: Vec::new(),
7821            decomposition: None,
7822            unit: None,
7823            help: String::new(),
7824        }
7825    }
7826
7827    #[test]
7828    fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
7829        let ten = Value::Number(Decimal::from(10));
7830        let err = parser_value_to_value_kind(&ten, &measure_type_with_kilogram(), None)
7831            .expect_err("bare number must not bind to measure");
7832        assert!(
7833            err.contains("kilogram"),
7834            "error must hint expected unit, got: {err}"
7835        );
7836    }
7837
7838    #[test]
7839    fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
7840        let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
7841        let kind = parser_value_to_value_kind(&ten_kg, &measure_type_with_kilogram(), None)
7842            .expect("10 kilogram must bind to measure");
7843        assert!(matches!(kind, ValueKind::Measure(_)));
7844    }
7845
7846    #[test]
7847    fn parser_value_to_value_kind_accepts_bare_number_for_ratio() {
7848        let ten = Value::Number(Decimal::from(10));
7849        let kind = parser_value_to_value_kind(&ten, &TypeSpecification::ratio(), None)
7850            .expect("number -> ratio");
7851        assert!(matches!(kind, ValueKind::Ratio(_)));
7852    }
7853
7854    #[test]
7855    fn value_kind_matches_spec_rejects_number_for_measure() {
7856        let n = ValueKind::Number(rational_new(10, 1));
7857        assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
7858    }
7859
7860    #[test]
7861    fn apply_constraint_rejects_inherited_unit_factor_change() {
7862        let mut specs = TypeSpecification::measure();
7863        specs
7864            .apply_constraint(
7865                "money",
7866                TypeConstraintCommand::Unit,
7867                &unit_factor_arg("eur", 1),
7868                &mut None,
7869                &mut None,
7870                None,
7871            )
7872            .expect("seed eur");
7873        let err = specs
7874            .apply_constraint(
7875                "money",
7876                TypeConstraintCommand::Unit,
7877                &[
7878                    CommandArg::Label("eur".to_string()),
7879                    CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
7880                ],
7881                &mut None,
7882                &mut None,
7883                None,
7884            )
7885            .expect_err("must not change inherited unit factor");
7886        assert!(err.contains("eur"), "error must name unit, got: {err}");
7887        assert!(
7888            err.contains("inherited") || err.contains("cannot change"),
7889            "error must reject factor change, got: {err}"
7890        );
7891    }
7892
7893    #[test]
7894    fn apply_constraint_allows_additive_unit_on_inherited_spec() {
7895        let mut specs = TypeSpecification::measure();
7896        specs
7897            .apply_constraint(
7898                "money",
7899                TypeConstraintCommand::Unit,
7900                &unit_factor_arg("eur", 1),
7901                &mut None,
7902                &mut None,
7903                None,
7904            )
7905            .expect("seed eur");
7906        specs
7907            .apply_constraint(
7908                "money",
7909                TypeConstraintCommand::Unit,
7910                &unit_factor_arg("usd", 1),
7911                &mut None,
7912                &mut None,
7913                None,
7914            )
7915            .expect("add usd");
7916        match &specs {
7917            TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
7918            other => panic!("expected Measure, got {other:?}"),
7919        }
7920    }
7921
7922    #[test]
7923    fn apply_constraint_idempotent_inherited_unit_redeclare() {
7924        let mut specs = TypeSpecification::measure();
7925        specs
7926            .apply_constraint(
7927                "money",
7928                TypeConstraintCommand::Unit,
7929                &unit_factor_arg("eur", 1),
7930                &mut None,
7931                &mut None,
7932                None,
7933            )
7934            .expect("seed eur");
7935        specs
7936            .apply_constraint(
7937                "money",
7938                TypeConstraintCommand::Unit,
7939                &unit_factor_arg("eur", 1),
7940                &mut None,
7941                &mut None,
7942                None,
7943            )
7944            .expect("idempotent eur");
7945        match &specs {
7946            TypeSpecification::Measure { units, .. } => {
7947                assert_eq!(units.len(), 1);
7948                assert_eq!(
7949                    units.iter().find(|u| u.name == "eur").expect("eur").factor,
7950                    crate::computation::rational::rational_one()
7951                );
7952            }
7953            other => panic!("expected Measure, got {other:?}"),
7954        }
7955    }
7956
7957    #[test]
7958    fn element_from_range_returns_element_for_every_range_primitive() {
7959        type RangeElementMatcher = fn(&TypeSpecification) -> bool;
7960        let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
7961            (PrimitiveKind::NumberRange, |element| {
7962                matches!(element, TypeSpecification::Number { .. })
7963            }),
7964            (PrimitiveKind::MeasureRange, |element| {
7965                matches!(element, TypeSpecification::Measure { .. })
7966            }),
7967            (PrimitiveKind::RatioRange, |element| {
7968                matches!(element, TypeSpecification::Ratio { .. })
7969            }),
7970            (PrimitiveKind::DateRange, |element| {
7971                matches!(element, TypeSpecification::Date { .. })
7972            }),
7973            (PrimitiveKind::TimeRange, |element| {
7974                matches!(element, TypeSpecification::Time { .. })
7975            }),
7976        ];
7977        for (kind, matches_element) in cases {
7978            let range_spec = type_spec_for_primitive(kind);
7979            let element = range_spec
7980                .element_from_range()
7981                .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
7982            assert!(
7983                matches_element(&element),
7984                "{kind:?} element must match documented mapping, got {element:?}"
7985            );
7986        }
7987    }
7988
7989    #[test]
7990    fn element_from_range_returns_none_for_non_range_primitives() {
7991        let non_range = [
7992            type_spec_for_primitive(PrimitiveKind::Boolean),
7993            type_spec_for_primitive(PrimitiveKind::Measure),
7994            TypeSpecification::Undetermined,
7995            TypeSpecification::veto(),
7996        ];
7997        for spec in non_range {
7998            assert!(
7999                spec.element_from_range().is_none(),
8000                "{spec:?} must not define element_from_range"
8001            );
8002        }
8003    }
8004
8005    fn spec_type(name: &str, effective: crate::parsing::ast::EffectiveDate) -> LemmaType {
8006        LemmaType::primitive(TypeSpecification::Spec {
8007            spec: SpecIdentity {
8008                repository: None,
8009                spec: name.to_string(),
8010                effective,
8011            },
8012        })
8013    }
8014
8015    #[test]
8016    fn same_value_type_spec_identity() {
8017        use crate::parsing::ast::EffectiveDate;
8018        let origin = EffectiveDate::Origin;
8019        let dated = EffectiveDate::DateTimeValue(crate::DateTimeValue {
8020            year: 2026,
8021            month: 1,
8022            day: 1,
8023            hour: 0,
8024            minute: 0,
8025            second: 0,
8026            microsecond: 0,
8027            timezone: None,
8028            granularity: crate::literals::DateGranularity::Full,
8029        });
8030        let tax = spec_type("tax_bracket", origin.clone());
8031        let tax_again = spec_type("tax_bracket", origin.clone());
8032        let other = spec_type("menu_item", origin);
8033        let tax_dated = spec_type("tax_bracket", dated);
8034        assert!(tax.same_value_type(&tax_again));
8035        assert!(!tax.same_value_type(&other));
8036        assert!(!tax.same_value_type(&tax_dated));
8037        let mut other_repo = tax.clone();
8038        match &mut other_repo.specifications {
8039            TypeSpecification::Spec { spec } => spec.repository = Some("@jack/finance".to_string()),
8040            other => panic!("expected Spec, got {other:?}"),
8041        }
8042        assert!(!tax.same_value_type(&other_repo));
8043    }
8044
8045    #[test]
8046    fn spec_identity_displays_repository_space_spec_space_effective() {
8047        use crate::parsing::ast::EffectiveDate;
8048        let workspace = SpecIdentity {
8049            repository: None,
8050            spec: "tax_bracket".to_string(),
8051            effective: EffectiveDate::Origin,
8052        };
8053        assert_eq!(workspace.to_string(), "tax_bracket");
8054        let dated = SpecIdentity {
8055            repository: Some("@jack/finance".to_string()),
8056            spec: "tax_bracket".to_string(),
8057            effective: EffectiveDate::DateTimeValue(
8058                DateTimeValue::from_str("2026-01-01").expect("date"),
8059            ),
8060        };
8061        assert_eq!(
8062            dated.to_string(),
8063            format!(
8064                "@jack/finance tax_bracket {}",
8065                DateTimeValue::from_str("2026-01-01").expect("date")
8066            )
8067        );
8068    }
8069
8070    #[test]
8071    fn type_family_displays_declaring_spec_then_type() {
8072        let workspace = TypeFamily {
8073            repository: None,
8074            spec: "pricing".to_string(),
8075            effective: crate::parsing::ast::EffectiveDate::Origin,
8076            name: "money".to_string(),
8077        };
8078        assert_eq!(workspace.to_string(), "pricing.money");
8079        let stdlib = TypeFamily {
8080            repository: Some("lemma".to_string()),
8081            spec: "units".to_string(),
8082            effective: crate::parsing::ast::EffectiveDate::Origin,
8083            name: "mass".to_string(),
8084        };
8085        assert_eq!(stdlib.to_string(), "lemma units.mass");
8086        let dated = TypeFamily {
8087            repository: None,
8088            spec: "pricing".to_string(),
8089            effective: crate::parsing::ast::EffectiveDate::DateTimeValue(
8090                DateTimeValue::from_str("2024-01-01").expect("date"),
8091            ),
8092            name: "money".to_string(),
8093        };
8094        assert_eq!(dated.to_string(), "pricing 2024-01-01.money");
8095    }
8096
8097    #[test]
8098    fn piecewise_type_keeps_spec_identity() {
8099        use crate::parsing::ast::EffectiveDate;
8100        let tax = Arc::new(spec_type("tax_bracket", EffectiveDate::Origin));
8101        let picked = crate::planning::typing::piecewise_type([
8102            Arc::new(LemmaType::veto_type()),
8103            Arc::clone(&tax),
8104        ]);
8105        assert!(picked.same_value_type(&tax));
8106    }
8107}