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, RoundingMode, 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::computation::OperationResult;
46use crate::parsing::ast::Constraint;
47use crate::parsing::ast::{
48    BooleanValue, CalendarPeriodUnit, ChainStep, CommandArg, ConversionTarget, ConversionTypeKind,
49    DateCalendarKind, DateRelativeKind, DateTimeValue, LogicalConnective, PrimitiveKind, TimeValue,
50    TimezoneValue, TypeConstraintCommand,
51};
52use crate::planning::normalize::NormalFormId;
53use crate::planning::unit_index::UnitIndex;
54use crate::Error;
55use indexmap::IndexMap;
56use rust_decimal::Decimal;
57use serde::{Deserialize, Deserializer, Serialize, Serializer};
58use std::collections::{BTreeMap, HashMap};
59use std::fmt;
60use std::hash::Hash;
61use std::str::FromStr;
62use std::sync::{Arc, OnceLock};
63
64// -----------------------------------------------------------------------------
65// Type specification and units (resolved type shape; apply constraints is planning)
66// -----------------------------------------------------------------------------
67
68// Unit tables live in `crate::literals` (no dependency on parsing/ast). Re-exported
69// here so downstream modules importing from `planning::semantics` keep working.
70pub use crate::literals::{
71    BaseMeasureVector, MeasureUnit, MeasureUnits, RatioUnit, RatioUnits, UnitScale,
72};
73
74/// Combine two `BaseMeasureVector`s by adding (for multiply) or subtracting (for divide) exponents.
75/// Entries that reach zero exponent are removed (they cancel out). `None` when a resulting
76/// exponent exceeds [`crate::limits::MAX_UNIT_EXPONENT`] in magnitude.
77pub fn combine_decompositions(
78    left: &BaseMeasureVector,
79    right: &BaseMeasureVector,
80    is_multiply: bool,
81) -> Option<BaseMeasureVector> {
82    let mut result = left.clone();
83    for (dim, &exp) in right {
84        let delta = if is_multiply { exp } else { exp.checked_neg()? };
85        let entry = result.entry(dim.clone()).or_insert(0);
86        *entry = entry.checked_add(delta)?;
87        if entry.unsigned_abs() > crate::limits::MAX_UNIT_EXPONENT.unsigned_abs() {
88            return None;
89        }
90        if *entry == 0 {
91            result.remove(dim);
92        }
93    }
94    Some(result)
95}
96
97/// Scale every exponent in `base` by `exponent`. Zero cancels to an empty vector.
98/// `None` when a product overflows `i32` or exceeds [`crate::limits::MAX_UNIT_EXPONENT`].
99pub fn scale_decomposition(base: &BaseMeasureVector, exponent: i32) -> Option<BaseMeasureVector> {
100    if exponent == 0 {
101        return Some(BaseMeasureVector::new());
102    }
103    let mut result = BaseMeasureVector::new();
104    for (dimension, &power) in base {
105        let scaled = power.checked_mul(exponent)?;
106        if scaled.unsigned_abs() > crate::limits::MAX_UNIT_EXPONENT.unsigned_abs() {
107            return None;
108        }
109        if scaled != 0 {
110            result.insert(dimension.clone(), scaled);
111        }
112    }
113    Some(result)
114}
115
116/// Combine two symbolic unit signatures (sorted-by-unit-name, no-zero-exponent vectors)
117/// under multiplication or division. The result is in canonical form: sorted by unit name
118/// ascending, no zero exponents.
119///
120/// Planning resolves measure products via decompositions; this remains the pinned algebra
121/// for signature-index tests.
122#[cfg(test)]
123pub fn combine_signatures(
124    left: &[(String, i32)],
125    right: &[(String, i32)],
126    is_multiply: bool,
127) -> Vec<(String, i32)> {
128    use std::collections::BTreeMap;
129    let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
130    for (name, exponent) in left {
131        *accumulator.entry(name.clone()).or_insert(0) += exponent;
132    }
133    for (name, exponent) in right {
134        let delta = if is_multiply { *exponent } else { -*exponent };
135        *accumulator.entry(name.clone()).or_insert(0) += delta;
136    }
137    accumulator
138        .into_iter()
139        .filter(|(_, exponent)| *exponent != 0)
140        .collect()
141}
142
143/// Canonicalize a unit signature: sum duplicate entries by name, drop zero exponents,
144/// sort ascending by unit name. Idempotent.
145/// Format a canonical unit signature into human-readable operator style.
146///
147/// Rules:
148/// - Numerator units (positive exponents) sorted alphabetically, joined by `*`.
149/// - Denominator units (negative exponents) sorted alphabetically, joined by `*`, exponents shown
150///   as positive values.
151/// - Separated by `/`. Empty numerator: `1/<denominator>`. No denominator: numerator only.
152/// - Exponents > 1 suffixed as `^n`.
153///
154/// Examples: `eur/hour`, `kilogram*meter^2/second^2`, `1/meter`.
155pub fn format_signature_operator_style(signature: &[(String, i32)]) -> String {
156    let canonical = canonicalize_signature(signature);
157    let mut numerator: Vec<(String, i32)> = Vec::new();
158    let mut denominator: Vec<(String, i32)> = Vec::new();
159    for (name, exponent) in canonical {
160        if exponent > 0 {
161            numerator.push((name, exponent));
162        } else if exponent < 0 {
163            denominator.push((name, -exponent));
164        }
165    }
166    let render = |terms: &[(String, i32)]| -> String {
167        terms
168            .iter()
169            .map(|(name, exp)| {
170                if *exp == 1 {
171                    name.clone()
172                } else {
173                    format!("{name}^{exp}")
174                }
175            })
176            .collect::<Vec<_>>()
177            .join("*")
178    };
179    match (numerator.is_empty(), denominator.is_empty()) {
180        (true, true) => String::new(),
181        (false, true) => render(&numerator),
182        (true, false) => format!("1/{}", render(&denominator)),
183        (false, false) => format!("{}/{}", render(&numerator), render(&denominator)),
184    }
185}
186
187/// Returns the intra-calendar-dimension factor for `name`, if it is a known calendar unit.
188///
189/// Month is canonical (factor 1). Year = 12.
190/// Keys are **singular** (`"month"`, `"year"`).
191///
192/// Returns `None` for names that are not calendar units.
193pub fn calendar_unit_factor(name: &str) -> Option<crate::computation::rational::RationalInteger> {
194    use crate::computation::rational::rational_one;
195    match name {
196        "month" => Some(rational_one()),
197        "year" => Some(rational_new(12, 1)),
198        _ => None,
199    }
200}
201
202fn reject_negative_width_magnitude(magnitude: &RationalInteger, cmd: &str) -> Result<(), String> {
203    use crate::computation::rational::rational_zero;
204    use std::cmp::Ordering;
205    match magnitude.try_cmp(&rational_zero()) {
206        Ok(Ordering::Less) => Err(format!("{cmd} width must not be negative")),
207        Ok(Ordering::Equal | Ordering::Greater) => Ok(()),
208        Err(failure) => Err(format!("{cmd} width compare failed: {failure}")),
209    }
210}
211
212/// Store a width bound as declared `(magnitude, unit)`. Family and factors are resolved
213/// later against `unit_index` during planning validation.
214fn parse_unresolved_width_bound(
215    args: &[CommandArg],
216    cmd: &str,
217) -> Result<(RationalInteger, String), String> {
218    use crate::computation::rational::decimal_to_rational;
219    let lit = require_literal(args, cmd)?;
220    let (magnitude, unit_name) = match lit {
221        crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
222        other => {
223            return Err(format!(
224                "{cmd} requires a measure literal with a unit, got {}",
225                value_kind_name(other)
226            ));
227        }
228    };
229    let magnitude_rational = decimal_to_rational(magnitude)
230        .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
231    reject_negative_width_magnitude(&magnitude_rational, cmd)?;
232    Ok((magnitude_rational, unit_name))
233}
234
235/// Planning consistency for range endpoint and width bound declarations.
236///
237/// `unit_index` resolves date/time width units (duration vs calendar). Pass empty for
238/// callers that only check number/ratio/measure ranges.
239pub(crate) fn check_range_bound_consistency(
240    spec: &TypeSpecification,
241    unit_index: &crate::planning::unit_index::UnitIndex,
242) -> Result<(), String> {
243    use std::cmp::Ordering;
244
245    fn endpoint_order_ok_dates(lo: &DateTimeValue, hi: &DateTimeValue) -> bool {
246        compare_semantic_dates(&date_time_to_semantic(lo), &date_time_to_semantic(hi))
247            != Ordering::Greater
248    }
249    fn endpoint_order_ok_times(lo: &TimeValue, hi: &TimeValue) -> bool {
250        compare_semantic_times(&time_to_semantic(lo), &time_to_semantic(hi)) != Ordering::Greater
251    }
252
253    match spec {
254        TypeSpecification::NumberRange {
255            lower,
256            upper,
257            minimum,
258            maximum,
259            ..
260        }
261        | TypeSpecification::RatioRange {
262            lower,
263            upper,
264            minimum,
265            maximum,
266            ..
267        } => {
268            if let (Some(lo), Some(hi)) = (lower, upper) {
269                if lo
270                    .try_cmp(hi)
271                    .map_err(|failure| format!("range endpoint compare failed: {failure}"))?
272                    == Ordering::Greater
273                {
274                    return Err(format!(
275                        "invalid range: lower {} is greater than upper {}",
276                        lo.display_str(),
277                        hi.display_str()
278                    ));
279                }
280            }
281            if let (Some(min_w), Some(max_w)) = (minimum, maximum) {
282                if min_w
283                    .try_cmp(max_w)
284                    .map_err(|failure| format!("range width compare failed: {failure}"))?
285                    == Ordering::Greater
286                {
287                    return Err(format!(
288                        "invalid range: minimum width {} is greater than maximum width {}",
289                        min_w.display_str(),
290                        max_w.display_str()
291                    ));
292                }
293            }
294            Ok(())
295        }
296        TypeSpecification::MeasureRange {
297            lower,
298            upper,
299            minimum,
300            maximum,
301            units,
302            ..
303        } => {
304            if let (Some(lo), Some(hi)) = (lower, upper) {
305                let lo_c =
306                    measure_declared_bound_to_canonical(&lo.0, &lo.1, units, "range", "lower")?;
307                let hi_c =
308                    measure_declared_bound_to_canonical(&hi.0, &hi.1, units, "range", "upper")?;
309                if lo_c
310                    .try_cmp(&hi_c)
311                    .map_err(|failure| format!("range endpoint compare failed: {failure}"))?
312                    == Ordering::Greater
313                {
314                    return Err(format!(
315                        "invalid range: lower {} {} is greater than upper {} {}",
316                        lo.0.display_str(),
317                        lo.1,
318                        hi.0.display_str(),
319                        hi.1
320                    ));
321                }
322            }
323            if let (Some(min_w), Some(max_w)) = (minimum, maximum) {
324                let min_c = measure_declared_bound_to_canonical(
325                    &min_w.0, &min_w.1, units, "range", "minimum",
326                )?;
327                let max_c = measure_declared_bound_to_canonical(
328                    &max_w.0, &max_w.1, units, "range", "maximum",
329                )?;
330                if min_c
331                    .try_cmp(&max_c)
332                    .map_err(|failure| format!("range width compare failed: {failure}"))?
333                    == Ordering::Greater
334                {
335                    return Err(format!(
336                        "invalid range: minimum width {} {} is greater than maximum width {} {}",
337                        min_w.0.display_str(),
338                        min_w.1,
339                        max_w.0.display_str(),
340                        max_w.1
341                    ));
342                }
343            }
344            Ok(())
345        }
346        TypeSpecification::DateRange {
347            lower,
348            upper,
349            minimum,
350            maximum,
351            ..
352        } => {
353            if let (Some(lo), Some(hi)) = (lower, upper) {
354                if !endpoint_order_ok_dates(lo, hi) {
355                    return Err(format!(
356                        "invalid range: lower {lo} is greater than upper {hi}"
357                    ));
358                }
359            }
360            check_temporal_width_pair_consistency(minimum, maximum, unit_index, true)
361        }
362        TypeSpecification::TimeRange {
363            lower,
364            upper,
365            minimum,
366            maximum,
367            ..
368        } => {
369            if let (Some(lo), Some(hi)) = (lower, upper) {
370                if !endpoint_order_ok_times(lo, hi) {
371                    return Err(format!(
372                        "invalid range: lower {lo} is greater than upper {hi}"
373                    ));
374                }
375            }
376            check_temporal_width_pair_consistency(minimum, maximum, unit_index, false)
377        }
378        _ => Ok(()),
379    }
380}
381
382fn check_temporal_width_pair_consistency(
383    minimum: &Option<(RationalInteger, String)>,
384    maximum: &Option<(RationalInteger, String)>,
385    unit_index: &crate::planning::unit_index::UnitIndex,
386    allow_calendar: bool,
387) -> Result<(), String> {
388    use std::cmp::Ordering;
389    let resolve = |bound: &(RationalInteger, String),
390                   command: &str|
391     -> Result<(RationalInteger, Arc<LemmaType>), String> {
392        let (bare, owner) = unit_index.resolve(bound.1.as_str()).map_err(|err| {
393            format!(
394                "{command} width unit '{}': {err} (add `uses lemma units` or declare the unit)",
395                bound.1
396            )
397        })?;
398        if allow_calendar {
399            if !owner.is_duration_like() && !owner.is_calendar_like() {
400                return Err(format!(
401                    "{command} width unit '{bare}' must be a duration or calendar unit",
402                ));
403            }
404        } else if !owner.is_duration_like() {
405            return Err(format!(
406                "{command} width unit '{bare}' must be a duration unit",
407            ));
408        }
409        let TypeSpecification::Measure { units, .. } = &owner.specifications else {
410            return Err(format!(
411                "{command} width unit '{bare}' must resolve to a measure type",
412            ));
413        };
414        let canonical = measure_declared_bound_to_canonical(
415            &bound.0,
416            &bare,
417            units,
418            owner.name().as_str(),
419            command,
420        )?;
421        Ok((canonical, Arc::clone(&owner)))
422    };
423
424    match (minimum, maximum) {
425        (None, None) => Ok(()),
426        (Some(min_w), None) => {
427            resolve(min_w, "minimum")?;
428            Ok(())
429        }
430        (None, Some(max_w)) => {
431            resolve(max_w, "maximum")?;
432            Ok(())
433        }
434        (Some(min_w), Some(max_w)) => {
435            let (min_c, min_owner) = resolve(min_w, "minimum")?;
436            let (max_c, max_owner) = resolve(max_w, "maximum")?;
437            if min_owner.is_calendar_like() != max_owner.is_calendar_like() {
438                return Err(
439                    "invalid range: minimum and maximum width must not mix calendar and duration units"
440                        .to_string(),
441                );
442            }
443            if min_c
444                .try_cmp(&max_c)
445                .map_err(|failure| format!("range width compare failed: {failure}"))?
446                == Ordering::Greater
447            {
448                return Err(format!(
449                    "invalid range: minimum width {} {} is greater than maximum width {} {}",
450                    min_w.0.display_str(),
451                    min_w.1,
452                    max_w.0.display_str(),
453                    max_w.1
454                ));
455            }
456            Ok(())
457        }
458    }
459}
460
461fn owner_declares_measure_unit(owner: &LemmaType, unit_name: &str) -> bool {
462    owner
463        .measure_unit_names()
464        .is_some_and(|names| names.contains(&unit_name))
465}
466
467/// Compute the numeric factor of a symbolic unit signature relative to canonical bases.
468///
469/// For each `(unit_name, exponent)` in `signature`:
470/// 1. When `owner` declares the unit, use `owner.measure_unit_factor(unit_name)`.
471/// 2. Fall back to `expression_units[unit_name].measure_unit_factor(unit_name)`.
472/// 3. Unknown names panic with `"BUG: signature_factor called with unresolved unit name"`.
473///    Ambiguous multi-owner names panic asking the caller to pass a declaring owner.
474///
475/// Returns the product of `factor^exponent` over all pairs, or `NumericFailure` on overflow.
476pub fn signature_factor(
477    signature: &[(String, i32)],
478    expression_units: &crate::planning::unit_index::UnitIndex,
479    owner: Option<&LemmaType>,
480) -> Result<
481    crate::computation::rational::RationalInteger,
482    crate::computation::rational::NumericFailure,
483> {
484    use crate::computation::rational::{checked_div, checked_mul, rational_one};
485    let mut acc = rational_one();
486    for (name, exponent) in signature {
487        let factor =
488            if let Some(owner) = owner.filter(|owner| owner_declares_measure_unit(owner, name)) {
489                owner.measure_unit_factor(name).clone()
490            } else if let Some(lemma_type) = expression_units.unique_owner(name) {
491                lemma_type.measure_unit_factor(name).clone()
492            } else if !expression_units.owners_for(name).is_empty() {
493                panic!(
494                "BUG: signature_factor called with ambiguous unit name '{}' (pass declaring owner)",
495                name
496            );
497            } else {
498                panic!(
499                    "BUG: signature_factor called with unresolved unit name '{}'",
500                    name
501                );
502            };
503        let mut term = rational_one();
504        let abs_exp = exponent.unsigned_abs();
505        for _ in 0..abs_exp {
506            term = checked_mul(&term, &factor)?;
507        }
508        if *exponent >= 0 {
509            acc = checked_mul(&acc, &term)?;
510        } else {
511            acc = checked_div(&acc, &term)?;
512        }
513    }
514    Ok(acc)
515}
516
517pub fn canonicalize_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
518    use std::collections::BTreeMap;
519    let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
520    for (name, exponent) in signature {
521        let entry = accumulator.entry(name.clone()).or_insert(0);
522        *entry = entry
523            .checked_add(*exponent)
524            .expect("BUG: planning bounds signature unit exponents");
525    }
526    accumulator
527        .into_iter()
528        .filter(|(_, exponent)| *exponent != 0)
529        .collect()
530}
531
532/// Convert a `BaseMeasureVector` decomposition into canonical signature form.
533pub fn base_measure_vector_as_signature(decomposition: &BaseMeasureVector) -> Vec<(String, i32)> {
534    decomposition
535        .iter()
536        .filter(|(_, exponent)| **exponent != 0)
537        .map(|(dimension, exponent)| (dimension.name().to_string(), *exponent))
538        .collect()
539}
540
541/// One base axis of a measure decomposition.
542#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
543pub enum Dimension {
544    Duration,
545    Calendar,
546    Family(TypeFamily),
547}
548
549impl Dimension {
550    /// Bare name: builtin axis or family root type name.
551    #[must_use]
552    pub fn name(&self) -> &str {
553        match self {
554            Dimension::Duration => "duration",
555            Dimension::Calendar => "calendar",
556            Dimension::Family(family) => &family.name,
557        }
558    }
559
560    #[must_use]
561    fn with_families_without_effective(&self) -> Self {
562        match self {
563            Dimension::Duration => Dimension::Duration,
564            Dimension::Calendar => Dimension::Calendar,
565            Dimension::Family(family) => Dimension::Family(family.without_effective()),
566        }
567    }
568}
569
570fn base_measure_vector_without_effective(domain: &BaseMeasureVector) -> BaseMeasureVector {
571    domain
572        .iter()
573        .map(|(dimension, exponent)| (dimension.with_families_without_effective(), *exponent))
574        .collect()
575}
576
577fn measure_units_without_effective(units: &MeasureUnits) -> MeasureUnits {
578    MeasureUnits(
579        units
580            .iter()
581            .map(|unit| MeasureUnit {
582                name: unit.name.clone(),
583                scale: unit.scale.clone(),
584                derived_measure_factors: unit.derived_measure_factors.clone(),
585                decomposition: base_measure_vector_without_effective(&unit.decomposition),
586                minimum: unit.minimum.clone(),
587                maximum: unit.maximum.clone(),
588                suggestion_magnitude: unit.suggestion_magnitude.clone(),
589            })
590            .collect(),
591    )
592}
593
594#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
595pub enum MeasureTrait {
596    Duration,
597    Calendar,
598}
599
600pub fn duration_decomposition() -> BaseMeasureVector {
601    [(Dimension::Duration, 1i32)].into_iter().collect()
602}
603
604pub fn calendar_decomposition() -> BaseMeasureVector {
605    [(Dimension::Calendar, 1i32)].into_iter().collect()
606}
607
608/// Marker `LemmaType` for a Measure value whose signature has not been resolved to
609/// a named measure type. Carries an empty decomposition; runtime signature is
610/// derived via [`LemmaType::measure_runtime_signature`].
611pub fn anonymous_measure_type() -> LemmaType {
612    LemmaType::anonymous_for_decomposition(BaseMeasureVector::new())
613}
614
615/// Return a copy of `signature` with every exponent negated. Used by Number/Measure
616/// reciprocal construction (`1 / Q`).
617pub fn negate_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
618    signature.iter().map(|(n, e)| (n.clone(), -*e)).collect()
619}
620
621/// Allowed values of a type, plus at most one rule whose list is added to that set.
622/// Empty values and no rule means the type is not constrained by a set. A spec list
623/// has no values: its set is the rule's list.
624#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
625pub(crate) struct TypeOptions {
626    pub values: Vec<OptionValue>,
627    pub rule: Option<String>,
628}
629
630/// One inline option: canonical value and the written unit, when the literal named one.
631#[derive(Clone, Debug, PartialEq, Eq, Hash)]
632pub(crate) struct OptionValue {
633    pub value: BoundValueKind,
634}
635
636pub(crate) const SPEC_LIST_HAS_NO_OPTIONS: &str =
637    "`-> option` and `-> options` do not apply to a list of spec instances";
638
639/// Dot-separated field key for a spec member read (`path` hops, then `name`).
640pub(crate) fn spec_member_input_key(path: &[String], name: &str) -> String {
641    let mut key = String::new();
642    for segment in path {
643        key.push_str(segment);
644        key.push('.');
645    }
646    key.push_str(name);
647    key
648}
649
650impl TypeOptions {
651    #[must_use]
652    pub fn is_empty(&self) -> bool {
653        self.values.is_empty() && self.rule.is_none()
654    }
655}
656
657#[derive(Clone, Debug, PartialEq, Eq, Hash)]
658pub enum TypeSpecification {
659    Boolean {
660        help: String,
661    },
662    Measure {
663        minimum: Option<(RationalInteger, String)>,
664        maximum: Option<(RationalInteger, String)>,
665        units: MeasureUnits,
666        traits: Vec<MeasureTrait>,
667        /// Common dimensional decomposition vector shared by all units in this measure.
668        /// `None` until the decomposition pass runs. Base measures (no compound unit expression)
669        /// are assigned `Some({measure_name: 1})` by the pass. `Some(empty_map)` means resolved
670        /// to dimensionless (e.g. `kg/kg`).
671        decomposition: Option<BaseMeasureVector>,
672        /// Written unit for this measure value (`100 eur`, `as usd`). Schema types use `None`.
673        unit: Option<String>,
674        help: String,
675    },
676    Number {
677        minimum: Option<RationalInteger>,
678        maximum: Option<RationalInteger>,
679        decimals: Option<u8>,
680        help: String,
681    },
682    NumberRange {
683        lower: Option<RationalInteger>,
684        upper: Option<RationalInteger>,
685        minimum: Option<RationalInteger>,
686        maximum: Option<RationalInteger>,
687        help: String,
688    },
689    Ratio {
690        minimum: Option<RationalInteger>,
691        maximum: Option<RationalInteger>,
692        units: RatioUnits,
693        /// Written unit for this ratio value (`25%`, `as percent`). Schema types use `None`.
694        unit: Option<String>,
695        help: String,
696    },
697    RatioRange {
698        lower: Option<RationalInteger>,
699        upper: Option<RationalInteger>,
700        minimum: Option<RationalInteger>,
701        maximum: Option<RationalInteger>,
702        units: RatioUnits,
703        help: String,
704    },
705    Text {
706        length: Option<usize>,
707        help: String,
708    },
709    Date {
710        minimum: Option<DateTimeValue>,
711        maximum: Option<DateTimeValue>,
712        help: String,
713    },
714    DateRange {
715        lower: Option<DateTimeValue>,
716        upper: Option<DateTimeValue>,
717        minimum: Option<(RationalInteger, String)>,
718        maximum: Option<(RationalInteger, String)>,
719        help: String,
720    },
721    Time {
722        minimum: Option<TimeValue>,
723        maximum: Option<TimeValue>,
724        help: String,
725    },
726    TimeRange {
727        lower: Option<TimeValue>,
728        upper: Option<TimeValue>,
729        minimum: Option<(RationalInteger, String)>,
730        maximum: Option<(RationalInteger, String)>,
731        help: String,
732    },
733    MeasureRange {
734        lower: Option<(RationalInteger, String)>,
735        upper: Option<(RationalInteger, String)>,
736        minimum: Option<(RationalInteger, String)>,
737        maximum: Option<(RationalInteger, String)>,
738        units: MeasureUnits,
739        decomposition: Option<BaseMeasureVector>,
740        help: String,
741    },
742    Veto {
743        message: Option<String>,
744    },
745    /// Sentinel used during type inference when the type could not be determined.
746    /// Propagates through expressions without generating cascading errors.
747    /// Must never appear in a successfully validated graph or execution plan.
748    Undetermined,
749    /// A spec instance. Identity is repository, spec name, and the resolved
750    /// temporal row (`effective`). Appended so discriminants of earlier
751    /// variants stay stable.
752    Spec {
753        spec: SpecIdentity,
754    },
755    /// A list of rows of `element`, keyed by row number or named fields.
756    List {
757        element: Arc<LemmaType>,
758        key: ListKey,
759        help: String,
760    },
761}
762
763/// One field in a list's row key: display name and resolved template data path.
764#[derive(Clone, Debug, PartialEq, Eq, Hash)]
765pub struct KeyField {
766    pub name: String,
767    pub path: DataPath,
768}
769
770/// How a list addresses its rows.
771#[derive(Clone, Debug, PartialEq, Eq, Hash)]
772pub enum ListKey {
773    Numbered,
774    Fields(Vec<KeyField>),
775}
776
777/// One loaded spec version: its repository, name and `effective_from`. Types are declared
778/// per version, whatever slice of it a plan covers.
779#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
780pub(crate) struct SpecVersionKey {
781    pub repository: Option<String>,
782    pub spec: String,
783    pub effective: crate::parsing::ast::EffectiveDate,
784}
785
786impl SpecVersionKey {
787    #[must_use]
788    pub(crate) fn of(
789        repository: &crate::parsing::ast::LemmaRepository,
790        spec: &crate::parsing::ast::LemmaSpec,
791    ) -> Self {
792        Self {
793            repository: repository.name.clone(),
794            spec: spec.name.clone(),
795            effective: spec.effective_from.clone(),
796        }
797    }
798}
799
800/// Resolved spec a value or rule result refers to: one temporal slice of one version.
801///
802/// `effective` is the resolved row's `effective_from`, not the run instant. `slice` is
803/// the start of the slice of that version the plan covers: the greatest of the version's
804/// breakpoints ([`crate::planning::discovery::plan_breakpoints`]) at or before the
805/// instant the spec was resolved at. Inside one slice every unpinned import of the
806/// version resolves to the same version, so one slice is one plan.
807#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
808pub struct SpecIdentity {
809    pub repository: Option<String>,
810    pub spec: String,
811    pub effective: crate::parsing::ast::EffectiveDate,
812    pub slice: crate::parsing::ast::EffectiveDate,
813}
814
815impl SpecIdentity {
816    #[must_use]
817    pub(crate) fn of(
818        repository: &crate::parsing::ast::LemmaRepository,
819        spec: &crate::parsing::ast::LemmaSpec,
820        slice: crate::parsing::ast::EffectiveDate,
821    ) -> Self {
822        assert!(
823            slice >= spec.effective_from,
824            "BUG: slice {slice:?} of spec '{}' starts before its version",
825            spec.name
826        );
827        Self {
828            repository: repository.name.clone(),
829            spec: spec.name.clone(),
830            effective: spec.effective_from.clone(),
831            slice,
832        }
833    }
834}
835
836/// `[<repo> ]<spec>[ <date>][ slice <date>]`: the slice is written when it starts after
837/// the version.
838impl fmt::Display for SpecIdentity {
839    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
840        if let Some(repository) = &self.repository {
841            write!(f, "{repository} ")?;
842        }
843        write!(f, "{}", self.spec)?;
844        if let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.effective {
845            write!(f, " {at}")?;
846        }
847        if self.slice != self.effective {
848            let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.slice else {
849                unreachable!("BUG: a slice after its version's start is a date");
850            };
851            write!(f, " slice {at}")?;
852        }
853        Ok(())
854    }
855}
856
857impl std::fmt::Display for TypeSpecification {
858    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
859        match self {
860            Self::Boolean { .. } => f.write_str("boolean"),
861            Self::Measure { .. } => f.write_str("measure"),
862            Self::MeasureRange { .. } => f.write_str("measure range"),
863            Self::Number { .. } => f.write_str("number"),
864            Self::NumberRange { .. } => f.write_str("number range"),
865            Self::Text { .. } => f.write_str("text"),
866            Self::Date { .. } => f.write_str("date"),
867            Self::DateRange { .. } => f.write_str("date range"),
868            Self::Time { .. } => f.write_str("time"),
869            Self::TimeRange { .. } => f.write_str("time range"),
870            Self::Ratio { .. } => f.write_str("ratio"),
871            Self::RatioRange { .. } => f.write_str("ratio range"),
872            Self::Veto { .. } => f.write_str("veto"),
873            Self::Undetermined => f.write_str("undetermined"),
874            Self::Spec { spec } => write!(f, "{spec}"),
875            Self::List {
876                element,
877                key: ListKey::Numbered,
878                ..
879            } => write!(f, "{} list", element.specifications),
880            Self::List {
881                element,
882                key: ListKey::Fields(fields),
883                ..
884            } => write!(
885                f,
886                "{} list key {}",
887                element.specifications,
888                fields
889                    .iter()
890                    .map(|field| field.name.as_str())
891                    .collect::<Vec<_>>()
892                    .join(", ")
893            ),
894        }
895    }
896}
897
898impl TypeSpecification {
899    /// Returns the help text associated with this type, or an empty string if none.
900    pub fn help(&self) -> &str {
901        match self {
902            Self::Boolean { help, .. }
903            | Self::Measure { help, .. }
904            | Self::Number { help, .. }
905            | Self::NumberRange { help, .. }
906            | Self::Text { help, .. }
907            | Self::Date { help, .. }
908            | Self::DateRange { help, .. }
909            | Self::Time { help, .. }
910            | Self::TimeRange { help, .. }
911            | Self::Ratio { help, .. }
912            | Self::RatioRange { help, .. }
913            | Self::MeasureRange { help, .. } => help.as_str(),
914            Self::Veto { .. } | Self::Undetermined | Self::Spec { .. } => "",
915            Self::List { element, help, .. } => {
916                if help.is_empty() {
917                    element.specifications.help()
918                } else {
919                    help.as_str()
920                }
921            }
922        }
923    }
924
925    /// Primitive `measure range` before units/decomposition are declared on a named type.
926    #[must_use]
927    pub fn is_unresolved_measure_range(&self) -> bool {
928        matches!(
929            self,
930            Self::MeasureRange {
931                units,
932                decomposition: None,
933                ..
934            } if units.0.is_empty()
935        )
936    }
937
938    #[must_use]
939    fn with_families_without_effective(&self) -> Self {
940        match self {
941            Self::Measure {
942                minimum,
943                maximum,
944                units,
945                traits,
946                decomposition,
947                unit,
948                help,
949            } => Self::Measure {
950                minimum: minimum.clone(),
951                maximum: maximum.clone(),
952                units: measure_units_without_effective(units),
953                traits: traits.clone(),
954                decomposition: decomposition
955                    .as_ref()
956                    .map(base_measure_vector_without_effective),
957                unit: unit.clone(),
958                help: help.clone(),
959            },
960            Self::MeasureRange {
961                lower,
962                upper,
963                minimum,
964                maximum,
965                units,
966                decomposition,
967                help,
968            } => Self::MeasureRange {
969                lower: lower.clone(),
970                upper: upper.clone(),
971                minimum: minimum.clone(),
972                maximum: maximum.clone(),
973                units: measure_units_without_effective(units),
974                decomposition: decomposition
975                    .as_ref()
976                    .map(base_measure_vector_without_effective),
977                help: help.clone(),
978            },
979            Self::List { element, key, help } => Self::List {
980                element: Arc::new(element.with_families_without_effective()),
981                key: key.clone(),
982                help: help.clone(),
983            },
984            other => other.clone(),
985        }
986    }
987
988    /// Schema equality that ignores written [`unit`](Self::Measure::unit) /
989    /// [`unit`](Self::Ratio::unit). Used when comparing declared types that may
990    /// carry a value-specific written unit.
991    #[must_use]
992    pub fn equal_ignoring_unit(&self, other: &Self) -> bool {
993        match (self, other) {
994            (
995                Self::Measure {
996                    minimum: a_min,
997                    maximum: a_max,
998                    units: a_units,
999                    traits: a_traits,
1000                    decomposition: a_decomp,
1001                    help: a_help,
1002                    unit: _,
1003                },
1004                Self::Measure {
1005                    minimum: b_min,
1006                    maximum: b_max,
1007                    units: b_units,
1008                    traits: b_traits,
1009                    decomposition: b_decomp,
1010                    help: b_help,
1011                    unit: _,
1012                },
1013            ) => {
1014                a_min == b_min
1015                    && a_max == b_max
1016                    && a_units == b_units
1017                    && a_traits == b_traits
1018                    && a_decomp == b_decomp
1019                    && a_help == b_help
1020            }
1021            (
1022                Self::Ratio {
1023                    minimum: a_min,
1024                    maximum: a_max,
1025                    units: a_units,
1026                    help: a_help,
1027                    unit: _,
1028                },
1029                Self::Ratio {
1030                    minimum: b_min,
1031                    maximum: b_max,
1032                    units: b_units,
1033                    help: b_help,
1034                    unit: _,
1035                },
1036            ) => a_min == b_min && a_max == b_max && a_units == b_units && a_help == b_help,
1037            _ => self == other,
1038        }
1039    }
1040}
1041
1042/// Extract a typed [`Value`] from the first `CommandArg`, requiring `Literal` shape.
1043///
1044/// `Label` args carry identifiers (unit names, option keywords) and never satisfy a
1045/// command position that wants a literal value. Returning a typed `Value` keeps the
1046/// caller's match exhaustive over [`Value`] variants — no string coercion path.
1047fn require_literal<'a>(
1048    args: &'a [CommandArg],
1049    cmd: &str,
1050) -> Result<&'a crate::literals::Value, String> {
1051    if args.len() > 1 {
1052        return Err(format!("{cmd} takes exactly one value"));
1053    }
1054    let arg = args
1055        .first()
1056        .ok_or_else(|| format!("{} requires an argument", cmd))?;
1057    match arg {
1058        CommandArg::Literal(v) => Ok(v),
1059        CommandArg::Label(name) => Err(format!(
1060            "{} requires a literal value, got identifier '{}'",
1061            cmd, name
1062        )),
1063        CommandArg::UnitExpr(_) => Err(format!(
1064            "{} requires a literal value, got a unit expression (only valid for 'unit' command)",
1065            cmd
1066        )),
1067    }
1068}
1069
1070fn apply_type_help_command(help: &mut String, args: &[CommandArg]) -> Result<(), String> {
1071    match require_literal(args, "help")? {
1072        crate::literals::Value::Text(s) => {
1073            *help = s.clone();
1074            Ok(())
1075        }
1076        other => Err(format!(
1077            "help requires a text literal (quoted string), got {}",
1078            value_kind_name(other)
1079        )),
1080    }
1081}
1082
1083fn format_measure_units_list(units: &MeasureUnits) -> String {
1084    units
1085        .iter()
1086        .map(|u| u.name.as_str())
1087        .collect::<Vec<_>>()
1088        .join(", ")
1089}
1090
1091/// What kind of value `-> suggest` expects when rejecting a calendar literal.
1092#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1093pub(crate) enum SuggestionExpectation {
1094    MeasureUnits,
1095    Text,
1096    Number,
1097    Boolean,
1098    Date,
1099    Time,
1100    Ratio,
1101    NumberRange,
1102    DateRange,
1103    TimeRange,
1104    MeasureRange,
1105    RatioRange,
1106}
1107
1108pub(crate) fn suggestion_value_mismatch_error(
1109    calendar_unit: &str,
1110    type_name: &str,
1111    expectation: SuggestionExpectation,
1112    measure_units: Option<&MeasureUnits>,
1113) -> String {
1114    let unit_label = calendar_unit;
1115    let first = format!("Unit '{unit_label}' is for calendar data.");
1116    match expectation {
1117        SuggestionExpectation::MeasureUnits => {
1118            let list = measure_units
1119                .map(format_measure_units_list)
1120                .unwrap_or_default();
1121            format!("{first} Valid '{type_name}' units are: {list}.")
1122        }
1123        SuggestionExpectation::Text => format!(
1124            "{first} Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
1125        ),
1126        SuggestionExpectation::Number => format!(
1127            "{first} Please provide a number, for example `-> suggest 42`."
1128        ),
1129        SuggestionExpectation::Boolean => format!(
1130            "{first} Please provide true or false, for example `-> suggest true`."
1131        ),
1132        SuggestionExpectation::Date => format!(
1133            "{first} Please provide a date, for example `-> suggest 2024-06-15`."
1134        ),
1135        SuggestionExpectation::Time => format!(
1136            "{first} Please provide a time, for example `-> suggest 09:00:00`."
1137        ),
1138        SuggestionExpectation::Ratio | SuggestionExpectation::RatioRange => format!(
1139            "{first} Please provide a ratio, for example `-> suggest 25%`."
1140        ),
1141        SuggestionExpectation::NumberRange => format!(
1142            "{first} Please provide a number range, for example `-> suggest 10...100`."
1143        ),
1144        SuggestionExpectation::DateRange => format!(
1145            "{first} Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
1146        ),
1147        SuggestionExpectation::TimeRange => format!(
1148            "{first} Please provide a time range, for example `-> suggest 09:00...17:00`."
1149        ),
1150        SuggestionExpectation::MeasureRange => format!(
1151            "{first} Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
1152        ),
1153    }
1154}
1155
1156fn measure_suggestion_wrong_shape_error(type_name: &str, traits: &[MeasureTrait]) -> String {
1157    let example = if traits.contains(&MeasureTrait::Duration) {
1158        "4 week"
1159    } else if traits.contains(&MeasureTrait::Calendar) {
1160        "3 month"
1161    } else {
1162        "30 kilogram"
1163    };
1164    format!(
1165        "Please provide a value with a unit valid for '{type_name}', for example `-> suggest {example}`."
1166    )
1167}
1168
1169/// Names a data command reports in its errors: the type the command applies to,
1170/// and the data being declared.
1171#[derive(Clone, Copy, Debug)]
1172pub struct ConstraintNames<'a> {
1173    pub type_name: &'a str,
1174    pub data_name: &'a str,
1175}
1176
1177fn decimals_depend_on_the_unit_error(
1178    kind: &str,
1179    names: ConstraintNames<'_>,
1180    args: &[CommandArg],
1181    first_unit: Option<&str>,
1182) -> Result<String, String> {
1183    let ConstraintNames {
1184        type_name,
1185        data_name,
1186    } = names;
1187    let places = decimal_to_u8(require_decimal_literal(args, "decimals")?, "decimals")?;
1188    let step = match places {
1189        0 => "1".to_string(),
1190        _ => format!("0.{}1", "0".repeat(usize::from(places) - 1)),
1191    };
1192    let advice = match first_unit {
1193        Some(unit) => {
1194            format!("Round in a rule instead, for example '{data_name} -> round {step} {unit}'.")
1195        }
1196        None => format!(
1197            "Round in a rule instead, for example '{data_name} -> round' followed by a step of {step} in one of the type's units."
1198        ),
1199    };
1200    Ok(format!("'decimals' is not valid on {kind} type '{type_name}': precision depends on the unit. {advice}"))
1201}
1202
1203fn reject_calendar_for_suggestion(
1204    value: &crate::literals::Value,
1205    type_name: &str,
1206    expectation: SuggestionExpectation,
1207    measure_units: Option<&MeasureUnits>,
1208) -> Result<(), String> {
1209    if let crate::literals::Value::NumberWithUnit(_, unit) = value {
1210        if calendar_unit_factor(unit).is_some() {
1211            return Err(suggestion_value_mismatch_error(
1212                unit,
1213                type_name,
1214                expectation,
1215                measure_units,
1216            ));
1217        }
1218    }
1219    Ok(())
1220}
1221
1222/// Human-readable name for a [`Value`] variant — used in mismatch error messages.
1223fn value_kind_name(v: &crate::literals::Value) -> &'static str {
1224    use crate::literals::Value;
1225    match v {
1226        Value::Number(_) => "number",
1227        Value::NumberWithUnit(_, _) => "number_with_unit",
1228        Value::Text(_) => "text",
1229        Value::Date(_) => "date",
1230        Value::Time(_) => "time",
1231        Value::Boolean(_) => "boolean",
1232        Value::Range(_, _) => "range",
1233    }
1234}
1235
1236fn require_suggestion_range_endpoints<'a>(
1237    args: &'a [CommandArg],
1238    type_name: &str,
1239    expectation: SuggestionExpectation,
1240    measure_units: Option<&MeasureUnits>,
1241) -> Result<(&'a crate::literals::Value, &'a crate::literals::Value), String> {
1242    match require_literal(args, "suggest")? {
1243        crate::literals::Value::NumberWithUnit(_, unit)
1244            if calendar_unit_factor(unit).is_some() =>
1245        {
1246            Err(suggestion_value_mismatch_error(
1247                unit,
1248                type_name,
1249                expectation,
1250                measure_units,
1251            ))
1252        }
1253        crate::literals::Value::Range(left, right) => Ok((left.as_ref(), right.as_ref())),
1254        _ => Err(match expectation {
1255            SuggestionExpectation::NumberRange => {
1256                "Please provide a number range, for example `-> suggest 10...100`.".to_string()
1257            }
1258            SuggestionExpectation::DateRange => {
1259                "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
1260                    .to_string()
1261            }
1262            SuggestionExpectation::RatioRange => {
1263                "Please provide a ratio range, for example `-> suggest 10%...50%`.".to_string()
1264            }
1265            SuggestionExpectation::MeasureRange => format!(
1266                "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
1267            ),
1268            _ => unreachable!("BUG: require_suggestion_range_endpoints called with non-range expectation"),
1269        }),
1270    }
1271}
1272
1273fn lift_parser_decimal(decimal: rust_decimal::Decimal) -> Result<RationalInteger, String> {
1274    crate::computation::rational::decimal_to_rational(decimal)
1275        .map_err(|failure| format!("literal failed rational lift: {failure}"))
1276}
1277
1278/// Element spec for a range type, used for parsing endpoints and lifting literal endpoints.
1279pub fn range_element_type_specification(
1280    range_spec: &TypeSpecification,
1281) -> Option<TypeSpecification> {
1282    range_spec.element_from_range()
1283}
1284
1285fn range_endpoints_compatible(left: &LemmaType, right: &LemmaType) -> bool {
1286    match (&left.specifications, &right.specifications) {
1287        (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
1288        (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
1289        (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
1290        (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
1291            left.same_measure_family(right)
1292        }
1293        (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
1294        _ => false,
1295    }
1296}
1297
1298/// Infer the range type specification from two compatible endpoint types.
1299pub fn range_type_specification_from_endpoints(
1300    left: &LemmaType,
1301    right: &LemmaType,
1302) -> Option<TypeSpecification> {
1303    if !range_endpoints_compatible(left, right) {
1304        return None;
1305    }
1306    left.specifications.range_from_element()
1307}
1308
1309/// Resolves a written unit token to a bare declared unit name against the named types
1310/// visible at one planning site.
1311pub enum UnitTokenResolver<'a> {
1312    /// The spec's types are resolved: tokens resolve through its unit index.
1313    Declared {
1314        unit_index: &'a UnitIndex,
1315        named_types: &'a IndexMap<String, Arc<LemmaType>>,
1316    },
1317    /// The spec has no resolved types: every token is unknown.
1318    Undeclared,
1319    /// Tokens resolve through the planning unit index of the spec being planned.
1320    Planning {
1321        unit_index: &'a crate::planning::unit_index::PlanningUnitIndex,
1322        named_types: &'a IndexMap<String, Arc<LemmaType>>,
1323    },
1324    /// Tokens resolve through the first scope that knows them.
1325    Search(Vec<(&'a UnitIndex, &'a IndexMap<String, Arc<LemmaType>>)>),
1326    /// Tokens resolve through the unit index and must be declared on `lemma_type`
1327    /// or on its range element.
1328    Typed {
1329        unit_index: &'a UnitIndex,
1330        named_types: &'a IndexMap<String, Arc<LemmaType>>,
1331        lemma_type: &'a LemmaType,
1332    },
1333}
1334
1335impl UnitTokenResolver<'_> {
1336    pub fn resolve(&self, unit: &str) -> Result<String, String> {
1337        match self {
1338            UnitTokenResolver::Declared {
1339                unit_index,
1340                named_types,
1341            } => unit_index
1342                .resolve_with_named_types(unit, named_types)
1343                .map(|(bare, _)| bare),
1344            UnitTokenResolver::Undeclared => Err(unknown_unit_message(unit)),
1345            UnitTokenResolver::Planning {
1346                unit_index,
1347                named_types,
1348            } => unit_index
1349                .resolve_with_named_types(unit, named_types)
1350                .map(|(bare, _)| bare),
1351            UnitTokenResolver::Search(scopes) => scopes
1352                .iter()
1353                .find_map(|(unit_index, named_types)| {
1354                    unit_index
1355                        .resolve_with_named_types(unit, named_types)
1356                        .ok()
1357                        .map(|(bare, _)| bare)
1358                })
1359                .ok_or_else(|| unknown_unit_message(unit)),
1360            UnitTokenResolver::Typed {
1361                unit_index,
1362                named_types,
1363                lemma_type,
1364            } => resolve_declared_on_type(unit, lemma_type, unit_index, named_types),
1365        }
1366    }
1367}
1368
1369fn unknown_unit_message(unit: &str) -> String {
1370    format!("Unknown unit '{unit}'. Declare it on a measure or ratio type, or import it with uses.")
1371}
1372
1373fn resolve_declared_on_type(
1374    written: &str,
1375    lemma_type: &LemmaType,
1376    unit_index: &UnitIndex,
1377    named_types: &IndexMap<String, Arc<LemmaType>>,
1378) -> Result<String, String> {
1379    match unit_index.resolve_with_named_types(written, named_types) {
1380        Ok((bare, _)) => {
1381            if unit_name_declared_on_type(lemma_type, &bare)
1382                || range_element_declares_unit(lemma_type, &bare)
1383            {
1384                Ok(bare)
1385            } else {
1386                Err(format!(
1387                    "Unit '{written}' resolves to '{bare}', which is not declared on type {}",
1388                    lemma_type.name()
1389                ))
1390            }
1391        }
1392        Err(index_err) => {
1393            if unit_name_declared_on_type(lemma_type, written)
1394                || range_element_declares_unit(lemma_type, written)
1395            {
1396                Ok(written.to_string())
1397            } else {
1398                Err(index_err)
1399            }
1400        }
1401    }
1402}
1403
1404fn range_element_declares_unit(lemma_type: &LemmaType, unit_name: &str) -> bool {
1405    let Some(element) = lemma_type.specifications.element_from_range() else {
1406        return false;
1407    };
1408    unit_name_declared_on_type(&LemmaType::primitive(element), unit_name)
1409}
1410
1411fn unit_name_declared_on_type(lemma_type: &LemmaType, unit_name: &str) -> bool {
1412    match &lemma_type.specifications {
1413        TypeSpecification::Measure { units, .. } => units.get(unit_name).is_ok(),
1414        TypeSpecification::Ratio { units, .. } => units.get(unit_name).is_ok(),
1415        _ => false,
1416    }
1417}
1418
1419/// Declared bare unit name on a measure or ratio element type for `token`.
1420fn bare_declared_unit_on_element(
1421    element_spec: &TypeSpecification,
1422    token: &str,
1423) -> Result<String, String> {
1424    match element_spec {
1425        TypeSpecification::Measure { units, .. } => Ok(units.get(token)?.name.clone()),
1426        TypeSpecification::Ratio { units, .. } => Ok(units.get(token)?.name.clone()),
1427        other => panic!("BUG: bare_declared_unit_on_element on non-measure/ratio element {other}"),
1428    }
1429}
1430
1431/// Resolve a written unit token to a bare declared name on `element_spec`.
1432///
1433/// Tries an exact declared name first. When that fails and `resolve_unit` is
1434/// provided, resolves through the unit index and checks the bare name is declared
1435/// on the element.
1436pub fn resolve_endpoint_unit_token(
1437    token: &str,
1438    element_spec: &TypeSpecification,
1439    resolve_unit: Option<&UnitTokenResolver<'_>>,
1440) -> Result<String, String> {
1441    match bare_declared_unit_on_element(element_spec, token) {
1442        Ok(bare) => Ok(bare),
1443        Err(declared_err) => {
1444            let Some(resolve) = resolve_unit else {
1445                return Err(declared_err);
1446            };
1447            let bare = resolve.resolve(token)?;
1448            bare_declared_unit_on_element(element_spec, &bare).map_err(|_| {
1449                format!("Unit '{token}' resolves to '{bare}', which is not declared on this type")
1450            })
1451        }
1452    }
1453}
1454
1455/// Lift a parser literal range endpoint to a [`TypedLiteral`] typed as `element_type`.
1456/// Routes [`Value::NumberWithUnit`] through [`number_with_unit_value_kind`] so ratio endpoints
1457/// (e.g. `10%` in a `ratio range`) canonicalize to ratios, not anonymous quantities.
1458///
1459/// `element_type` must already carry the slot's measure/ratio family when the range
1460/// is a named type (`money range` → money). Callers use [`LemmaType::range_element_type`].
1461///
1462/// When `resolve_unit` is set, qualified tokens are resolved to bare declared names before bind.
1463pub(crate) fn lift_range_endpoint(
1464    value: &crate::parsing::ast::Value,
1465    element_type: &LemmaType,
1466    resolve_unit: Option<&UnitTokenResolver<'_>>,
1467) -> Result<TypedLiteral, String> {
1468    use crate::parsing::ast::Value;
1469    let element_spec = &element_type.specifications;
1470    match value {
1471        Value::NumberWithUnit(magnitude, unit_token) => {
1472            let bare = resolve_endpoint_unit_token(unit_token, element_spec, resolve_unit)?;
1473            let kind = number_with_unit_value_kind(*magnitude, &bare, element_spec)?;
1474            let lemma_type = match &kind {
1475                ValueKind::Measure(_) | ValueKind::Ratio(_) | ValueKind::UnscaledMeasure { .. } => {
1476                    Arc::new(element_type.clone().with_unit(bare))
1477                }
1478                _ => Arc::new(element_type.clone()),
1479            };
1480            Ok(TypedLiteral {
1481                value: kind,
1482                lemma_type,
1483            })
1484        }
1485        _ => {
1486            let kind = parser_scalar_to_value_kind(value, element_spec)?;
1487            Ok(TypedLiteral {
1488                value: kind,
1489                lemma_type: Arc::new(element_type.clone()),
1490            })
1491        }
1492    }
1493}
1494
1495fn literal_value_from_parser_value(
1496    value: &crate::parsing::ast::Value,
1497) -> Result<TypedLiteral, String> {
1498    use crate::parsing::ast::Value;
1499
1500    enum Step<'a> {
1501        Visit(&'a Value),
1502        CombineRange,
1503    }
1504
1505    let mut steps = vec![Step::Visit(value)];
1506    let mut literals: Vec<TypedLiteral> = Vec::new();
1507    while let Some(step) = steps.pop() {
1508        match step {
1509            Step::Visit(Value::Range(left, right)) => {
1510                steps.push(Step::CombineRange);
1511                steps.push(Step::Visit(right));
1512                steps.push(Step::Visit(left));
1513            }
1514            Step::Visit(scalar) => literals.push(scalar_literal_from_parser_value(scalar)?),
1515            Step::CombineRange => {
1516                let right = literals
1517                    .pop()
1518                    .expect("BUG: range right endpoint missing from literal stack");
1519                let left = literals
1520                    .pop()
1521                    .expect("BUG: range left endpoint missing from literal stack");
1522                let compatible = match (
1523                    &left.lemma_type.specifications,
1524                    &right.lemma_type.specifications,
1525                ) {
1526                    (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
1527                    (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
1528                    (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
1529                    (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
1530                        left.lemma_type.same_measure_family(&right.lemma_type)
1531                            || left
1532                                .lemma_type
1533                                .compatible_with_anonymous_measure(&right.lemma_type)
1534                            || right
1535                                .lemma_type
1536                                .compatible_with_anonymous_measure(&left.lemma_type)
1537                    }
1538                    (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
1539                    _ => false,
1540                };
1541                if !compatible {
1542                    return Err(format!(
1543                        "range endpoints must have the same supported base type, got {} and {}",
1544                        left.lemma_type.name(),
1545                        right.lemma_type.name()
1546                    ));
1547                }
1548                literals.push(TypedLiteral::range(left, right));
1549            }
1550        }
1551    }
1552    Ok(literals.pop().expect("BUG: literal walk left no result"))
1553}
1554
1555fn scalar_literal_from_parser_value(
1556    value: &crate::parsing::ast::Value,
1557) -> Result<TypedLiteral, String> {
1558    use crate::parsing::ast::Value;
1559
1560    match value {
1561        Value::Number(n) => Ok(TypedLiteral::number(lift_parser_decimal(*n)?)),
1562        Value::Text(s) => Ok(TypedLiteral::text(s.clone())),
1563        Value::Date(dt) => Ok(TypedLiteral::date(date_time_to_semantic(dt))),
1564        Value::Time(t) => Ok(TypedLiteral::time(time_to_semantic(t))),
1565        Value::Boolean(b) => Ok(TypedLiteral::from_bool(bool::from(*b))),
1566        Value::NumberWithUnit(n, _) => Ok(TypedLiteral::number_interpreted_as_measure(
1567            lift_parser_decimal(*n)?,
1568        )),
1569        Value::Range(_, _) => {
1570            unreachable!("BUG: range values are walked by literal_value_from_parser_value")
1571        }
1572    }
1573}
1574
1575/// Cast a [`RationalInteger`] to `u8`, requiring it to be a non-negative whole number that fits.
1576fn decimal_to_u8(d: RationalInteger, ctx: &str) -> Result<u8, String> {
1577    if !d.is_integer() {
1578        return Err(format!(
1579            "{} requires a whole number, got fractional value",
1580            ctx
1581        ));
1582    }
1583    d.numer_to_u8()
1584        .ok_or_else(|| format!("{} value out of range for u8", ctx))
1585}
1586
1587/// Cast a [`RationalInteger`] to `usize`, requiring it to be a non-negative whole number that fits.
1588fn decimal_to_usize(d: RationalInteger, ctx: &str) -> Result<usize, String> {
1589    if !d.is_integer() {
1590        return Err(format!(
1591            "{} requires a whole number, got fractional value",
1592            ctx
1593        ));
1594    }
1595    d.numer_to_usize()
1596        .ok_or_else(|| format!("{} value out of range for usize", ctx))
1597}
1598
1599/// Extract a number literal from a [`Value::Number`] arg and lift it to [`RationalInteger`].
1600///
1601/// Numeric meta-constraints (`decimals`, `length`, `minimum`/`maximum`
1602/// on `Number` and `Measure`) take a bare number literal — not a ratio, not a measure. Reject
1603/// any other variant to honour the no-coercion contract.
1604fn ratio_bound_to_canonical_rational(
1605    args: &[CommandArg],
1606    cmd: &str,
1607    units: &RatioUnits,
1608) -> Result<RationalInteger, String> {
1609    use crate::computation::rational::{checked_div, decimal_to_rational};
1610    let lit = require_literal(args, cmd)?;
1611    match lit {
1612        crate::literals::Value::NumberWithUnit(magnitude, unit_name) => {
1613            let unit = units.get(unit_name.as_str())?;
1614            let magnitude_rational = decimal_to_rational(*magnitude)
1615                .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1616            checked_div(&magnitude_rational, &unit.value)
1617                .map_err(|failure| format!("{cmd}: unit conversion failed: {failure}"))
1618        }
1619        other => Err(format!(
1620            "{cmd} requires a ratio literal with a unit, got {}",
1621            value_kind_name(other)
1622        )),
1623    }
1624}
1625
1626fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<RationalInteger, String> {
1627    use crate::computation::rational::decimal_to_rational;
1628    match require_literal(args, cmd)? {
1629        crate::literals::Value::Number(d) => decimal_to_rational(*d)
1630            .map_err(|failure| format!("{} literal failed rational lift: {}", cmd, failure)),
1631        other => Err(format!(
1632            "{} requires a number literal, got {}",
1633            cmd,
1634            value_kind_name(other)
1635        )),
1636    }
1637}
1638
1639enum UnitConstraintField {
1640    Minimum,
1641    Maximum,
1642    SuggestionMagnitude,
1643}
1644
1645fn apply_measure_unit_factor(
1646    units: &mut MeasureUnits,
1647    unit_name: String,
1648    value: rust_decimal::Decimal,
1649    derived_measure_factors: Vec<(String, i32)>,
1650) -> Result<(), String> {
1651    if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1652        let new_factor = crate::computation::rational::decimal_to_rational(value)
1653            .map_err(|failure| failure.to_string())?;
1654        if existing.literal_factor() != Some(&new_factor)
1655            || existing.derived_measure_factors != derived_measure_factors
1656        {
1657            return Err(format!(
1658                "Unit '{unit_name}' is already defined in this type's inherited units; \
1659                 cannot change factor or decomposition. Add a new unit name instead."
1660            ));
1661        }
1662        return Ok(());
1663    }
1664    units.0.push(MeasureUnit::from_decimal_factor(
1665        unit_name,
1666        value,
1667        derived_measure_factors,
1668    )?);
1669    Ok(())
1670}
1671
1672pub(crate) fn measure_declared_bound_to_canonical(
1673    magnitude: &RationalInteger,
1674    unit_name: &str,
1675    units: &MeasureUnits,
1676    type_name: &str,
1677    command: &str,
1678) -> Result<RationalInteger, String> {
1679    use crate::computation::rational::checked_mul;
1680    let unit = units.get(unit_name).map_err(|_| {
1681        format!(
1682            "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1683            format_measure_units_list(units)
1684        )
1685    })?;
1686    let factor = unit.literal_factor().ok_or_else(|| {
1687        format!(
1688            "{command} cannot use unit '{unit_name}' on '{type_name}': its conversion factor is a reference and is not known at planning."
1689        )
1690    })?;
1691    checked_mul(magnitude, factor)
1692        .map_err(|failure| format!("{command}: unit conversion overflow: {failure}"))
1693}
1694
1695fn parse_measure_declared_bound(
1696    args: &[CommandArg],
1697    cmd: &str,
1698    units: &MeasureUnits,
1699    type_name: &str,
1700) -> Result<(RationalInteger, String), String> {
1701    use crate::computation::rational::decimal_to_rational;
1702    let lit = require_literal(args, cmd)?;
1703    let (magnitude, unit_name) = match lit {
1704        crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
1705        other => {
1706            return Err(format!(
1707                "{cmd} requires a measure literal with a unit, got {}",
1708                value_kind_name(other)
1709            ));
1710        }
1711    };
1712    units.get(unit_name.as_str()).map_err(|_| {
1713        format!(
1714            "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1715            format_measure_units_list(units)
1716        )
1717    })?;
1718    let magnitude_rational = decimal_to_rational(magnitude)
1719        .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1720    Ok((magnitude_rational, unit_name))
1721}
1722
1723fn sync_measure_units_from_canonical(
1724    units: &mut MeasureUnits,
1725    canonical: &RationalInteger,
1726    field: UnitConstraintField,
1727) -> Result<(), String> {
1728    use crate::computation::rational::checked_div;
1729    for unit in &mut units.0 {
1730        let Some(factor) = unit.literal_factor() else {
1731            continue;
1732        };
1733        let magnitude = checked_div(canonical, factor).map_err(|failure| {
1734            format!(
1735                "cannot derive per-unit constraint for unit '{}': {failure}",
1736                unit.name
1737            )
1738        })?;
1739        match field {
1740            UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1741            UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1742            UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1743        }
1744    }
1745    Ok(())
1746}
1747
1748fn sync_ratio_units_from_canonical(
1749    units: &mut RatioUnits,
1750    canonical: &RationalInteger,
1751    field: UnitConstraintField,
1752) -> Result<(), String> {
1753    use crate::computation::rational::checked_mul;
1754    for unit in &mut units.0 {
1755        let magnitude = checked_mul(canonical, &unit.value).map_err(|failure| {
1756            format!(
1757                "cannot derive per-unit constraint for ratio unit '{}': {failure}",
1758                unit.name
1759            )
1760        })?;
1761        match field {
1762            UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1763            UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1764            UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1765        }
1766    }
1767    Ok(())
1768}
1769
1770fn sync_measure_suggestion_units(
1771    units: &mut MeasureUnits,
1772    default: &ValueKind,
1773    type_name: &str,
1774) -> Result<(), String> {
1775    let ValueKind::Measure(magnitude) = default else {
1776        return Ok(());
1777    };
1778    let unit_name = units
1779        .iter()
1780        .find(|unit| unit.is_canonical_factor())
1781        .or_else(|| units.iter().next())
1782        .map(|unit| unit.name.as_str())
1783        .expect(
1784            "BUG: Measure suggestion value requires at least one declared unit on the measure type",
1785        );
1786    units.get(unit_name).map_err(|_| {
1787        format!("Suggestion unit '{unit_name}' is not defined on measure type '{type_name}'.")
1788    })?;
1789    sync_measure_units_from_canonical(units, magnitude, UnitConstraintField::SuggestionMagnitude)
1790}
1791
1792pub(crate) fn finalize_measure_unit_constraint_magnitudes(
1793    specification: &mut TypeSpecification,
1794    declared_suggestion: Option<&ValueKind>,
1795    type_name: &str,
1796) -> Result<(), String> {
1797    let TypeSpecification::Measure {
1798        minimum,
1799        maximum,
1800        units,
1801        ..
1802    } = specification
1803    else {
1804        return Ok(());
1805    };
1806
1807    let mut synced_minimum = false;
1808    if let Some(bound) = minimum.as_ref() {
1809        let bound_unit = units.get(bound.1.as_str()).map_err(|err| err.to_string())?;
1810        if bound_unit.literal_factor().is_some() {
1811            let canonical = measure_declared_bound_to_canonical(
1812                &bound.0, &bound.1, units, type_name, "minimum",
1813            )?;
1814            sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Minimum)?;
1815            synced_minimum = true;
1816        }
1817    }
1818    let mut synced_maximum = false;
1819    if let Some(bound) = maximum.as_ref() {
1820        let bound_unit = units.get(bound.1.as_str()).map_err(|err| err.to_string())?;
1821        if bound_unit.literal_factor().is_some() {
1822            let canonical = measure_declared_bound_to_canonical(
1823                &bound.0, &bound.1, units, type_name, "maximum",
1824            )?;
1825            sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Maximum)?;
1826            synced_maximum = true;
1827        }
1828    }
1829    let mut synced_suggestion = false;
1830    if let Some(default) = declared_suggestion {
1831        if matches!(default, ValueKind::Measure(_)) {
1832            sync_measure_suggestion_units(units, default, type_name)?;
1833            synced_suggestion = true;
1834        }
1835    }
1836
1837    if synced_minimum {
1838        for unit in units.iter() {
1839            if unit.literal_factor().is_none() {
1840                continue;
1841            }
1842            assert!(
1843                unit.minimum.is_some(),
1844                "BUG: type '{type_name}' has minimum but unit '{}' missing per-unit minimum after finalize",
1845                unit.name
1846            );
1847        }
1848    }
1849    if synced_maximum {
1850        for unit in units.iter() {
1851            if unit.literal_factor().is_none() {
1852                continue;
1853            }
1854            assert!(
1855                unit.maximum.is_some(),
1856                "BUG: type '{type_name}' has maximum but unit '{}' missing per-unit maximum after finalize",
1857                unit.name
1858            );
1859        }
1860    }
1861    if synced_suggestion {
1862        for unit in units.iter() {
1863            if unit.literal_factor().is_none() {
1864                continue;
1865            }
1866            assert!(
1867                unit.suggestion_magnitude.is_some(),
1868                "BUG: type '{type_name}' has default but unit '{}' missing per-unit default after finalize",
1869                unit.name
1870            );
1871        }
1872    }
1873
1874    Ok(())
1875}
1876
1877fn sync_ratio_suggestion_units(units: &mut RatioUnits, default: &ValueKind) -> Result<(), String> {
1878    let ValueKind::Ratio(canonical) = default else {
1879        return Ok(());
1880    };
1881    sync_ratio_units_from_canonical(units, canonical, UnitConstraintField::SuggestionMagnitude)
1882}
1883
1884/// Extract an option name from a single arg.
1885///
1886/// Both `option red` (bare identifier, parsed as `Label`) and `option "red"`
1887/// (quoted text literal) are valid lemma syntax for option enumeration; the
1888/// grammar accepts either form. All other variants are rejected.
1889pub(crate) fn apply_option_constraint(
1890    lemma_type: &mut LemmaType,
1891    command: TypeConstraintCommand,
1892    args: &[CommandArg],
1893    resolve_unit: Option<&UnitTokenResolver<'_>>,
1894    spec: &crate::parsing::ast::LemmaSpec,
1895) -> Result<(), String> {
1896    let mut hops = 0;
1897    {
1898        let mut probe = &*lemma_type;
1899        while let TypeSpecification::List { element, .. } = &probe.specifications {
1900            hops += 1;
1901            probe = element.as_ref();
1902        }
1903    }
1904    let mut current = lemma_type;
1905    for _ in 0..hops {
1906        let TypeSpecification::List { element, .. } = &mut current.specifications else {
1907            unreachable!("BUG: option forwarding counted a list");
1908        };
1909        current = Arc::make_mut(element);
1910    }
1911    apply_options_command(current, command, args, resolve_unit, spec)
1912}
1913
1914/// `-> with` / `-> suggest` naming one `uses` instance of a spec type.
1915pub(crate) fn apply_spec_member_command(
1916    lemma_type: &LemmaType,
1917    command: TypeConstraintCommand,
1918    args: &[CommandArg],
1919    spec: &crate::parsing::ast::LemmaSpec,
1920    target: &mut Option<RawSuggestion>,
1921) -> Result<(), String> {
1922    let label = match args {
1923        [CommandArg::Label(name)] => name,
1924        _ => return Err(format!("{command} on a spec takes one instance name")),
1925    };
1926    let option = spec_alias_option(
1927        lemma_type,
1928        uses_alias(spec, label)
1929            .ok_or_else(|| format!("'{label}' is not an instance of this spec"))?,
1930        label,
1931    )?;
1932    *target = Some(RawSuggestion::Value(option.value.value));
1933    Ok(())
1934}
1935
1936/// One `-> with` line appends rows on a list. A spec element names a `uses` instance.
1937pub(crate) fn apply_list_with_rows(
1938    lemma_type: &mut LemmaType,
1939    names: ConstraintNames<'_>,
1940    args: &[CommandArg],
1941    declared_value: &mut Option<RawSuggestion>,
1942    resolve_unit: Option<&UnitTokenResolver<'_>>,
1943    spec: &crate::parsing::ast::LemmaSpec,
1944) -> Result<(), String> {
1945    let mut rows = match declared_value.take() {
1946        Some(RawSuggestion::List(existing)) => existing,
1947        None => Vec::new(),
1948        Some(other) => {
1949            unreachable!("BUG: list declared value must be RawSuggestion::List, got {other:?}")
1950        }
1951    };
1952    let TypeSpecification::List { element, .. } = &mut lemma_type.specifications else {
1953        unreachable!("BUG: apply_list_with_rows on a non-list type");
1954    };
1955    for arg in args {
1956        rows.push(list_with_row(
1957            Arc::make_mut(element),
1958            names,
1959            arg,
1960            resolve_unit,
1961            spec,
1962        )?);
1963    }
1964    *declared_value = Some(RawSuggestion::List(rows));
1965    Ok(())
1966}
1967
1968fn list_with_row(
1969    element: &mut LemmaType,
1970    names: ConstraintNames<'_>,
1971    arg: &CommandArg,
1972    resolve_unit: Option<&UnitTokenResolver<'_>>,
1973    spec: &crate::parsing::ast::LemmaSpec,
1974) -> Result<RawSuggestion, String> {
1975    let mut hops = 0;
1976    {
1977        let mut probe = &*element;
1978        while let TypeSpecification::List { element, .. } = &probe.specifications {
1979            hops += 1;
1980            probe = element.as_ref();
1981        }
1982    }
1983    let mut current = element;
1984    for _ in 0..hops {
1985        let TypeSpecification::List { element, .. } = &mut current.specifications else {
1986            unreachable!("BUG: list -> with counted a list");
1987        };
1988        current = Arc::make_mut(element);
1989    }
1990    let mut row_value = None;
1991    if matches!(current.specifications, TypeSpecification::Spec { .. }) {
1992        apply_spec_member_command(
1993            current,
1994            TypeConstraintCommand::With,
1995            std::slice::from_ref(arg),
1996            spec,
1997            &mut row_value,
1998        )?;
1999    } else {
2000        let mut unused_suggestion = None;
2001        current.apply_constraint(
2002            names,
2003            TypeConstraintCommand::With,
2004            std::slice::from_ref(arg),
2005            &mut unused_suggestion,
2006            &mut row_value,
2007            resolve_unit,
2008        )?;
2009    }
2010    let mut value = row_value.expect("BUG: -> with on a list row must produce a value");
2011    for _ in 0..hops {
2012        value = RawSuggestion::List(vec![value]);
2013    }
2014    Ok(value)
2015}
2016
2017fn apply_options_command(
2018    lemma_type: &mut LemmaType,
2019    command: TypeConstraintCommand,
2020    args: &[CommandArg],
2021    resolve_unit: Option<&UnitTokenResolver<'_>>,
2022    spec: &crate::parsing::ast::LemmaSpec,
2023) -> Result<(), String> {
2024    if command == TypeConstraintCommand::Option && args.len() != 1 {
2025        return Err("option takes exactly one argument".to_string());
2026    }
2027    if args.is_empty() {
2028        return Err("options takes at least one value".to_string());
2029    }
2030    let mut literals: Vec<OptionValue> = Vec::new();
2031    let mut names: Vec<String> = Vec::new();
2032    for arg in args {
2033        match arg {
2034            CommandArg::Literal(value) => {
2035                literals.push(OptionValue {
2036                    value: option_bound_from_literal(value, lemma_type, resolve_unit)?,
2037                });
2038            }
2039            CommandArg::Label(name) => names.push(name.clone()),
2040            CommandArg::UnitExpr(_) => {
2041                return Err(format!(
2042                    "{command} requires a value of this type, got a unit expression"
2043                ));
2044            }
2045        }
2046    }
2047    if !literals.is_empty() && !names.is_empty() {
2048        return Err(format!(
2049            "{command} mixes values and a rule. Write the values, or name one rule."
2050        ));
2051    }
2052    if matches!(lemma_type.specifications, TypeSpecification::Spec { .. }) {
2053        return apply_spec_list_options(lemma_type, command, args, spec);
2054    }
2055    if !names.is_empty() {
2056        return apply_option_names(lemma_type, &names, spec);
2057    }
2058    append_option_literals(lemma_type, literals)?;
2059    Ok(())
2060}
2061
2062fn option_bound_from_literal(
2063    value: &crate::literals::Value,
2064    lemma_type: &LemmaType,
2065    resolve_unit: Option<&UnitTokenResolver<'_>>,
2066) -> Result<BoundValueKind, String> {
2067    let kind = parser_value_to_value_kind(value, lemma_type, resolve_unit)?;
2068    let token = match value {
2069        crate::literals::Value::NumberWithUnit(_, unit) => Some(unit.as_str()),
2070        crate::literals::Value::Range(left, right) => match (left.as_ref(), right.as_ref()) {
2071            (
2072                crate::literals::Value::NumberWithUnit(_, left_unit),
2073                crate::literals::Value::NumberWithUnit(_, right_unit),
2074            ) if left_unit == right_unit => Some(left_unit.as_str()),
2075            _ => None,
2076        },
2077        _ => None,
2078    };
2079    let unit = match token {
2080        Some(token) => Some(Arc::<str>::from(resolve_endpoint_unit_token(
2081            token,
2082            &lemma_type.specifications,
2083            resolve_unit,
2084        )?)),
2085        None => None,
2086    };
2087    Ok(BoundValueKind {
2088        value: kind,
2089        unit,
2090        instance_factors: None,
2091    })
2092}
2093
2094fn apply_spec_list_options(
2095    _lemma_type: &mut LemmaType,
2096    _command: TypeConstraintCommand,
2097    _args: &[CommandArg],
2098    _spec: &crate::parsing::ast::LemmaSpec,
2099) -> Result<(), String> {
2100    Err(SPEC_LIST_HAS_NO_OPTIONS.to_string())
2101}
2102
2103enum OptionName {
2104    Text(OptionValue),
2105    Instance(OptionValue),
2106    Rule,
2107}
2108
2109fn apply_option_names(
2110    lemma_type: &mut LemmaType,
2111    names: &[String],
2112    spec: &crate::parsing::ast::LemmaSpec,
2113) -> Result<(), String> {
2114    let mut meanings = Vec::with_capacity(names.len());
2115    for name in names {
2116        meanings.push(option_name_meaning(lemma_type, spec, name)?);
2117    }
2118    let rule_count = meanings
2119        .iter()
2120        .filter(|meaning| matches!(meaning, OptionName::Rule))
2121        .count();
2122    if rule_count > 0 {
2123        if names.len() != 1 || rule_count != 1 {
2124            return Err(
2125                "options names one rule, or lists values. A rule is not mixed with values."
2126                    .to_string(),
2127            );
2128        }
2129        if let Some(existing) = &lemma_type.options.rule {
2130            return Err(format!(
2131                "`-> options` already names rule '{existing}', not '{}'",
2132                names[0]
2133            ));
2134        }
2135        lemma_type.options.rule = Some(names[0].clone());
2136        return Ok(());
2137    }
2138    let mut literals = Vec::with_capacity(meanings.len());
2139    for meaning in meanings {
2140        match meaning {
2141            OptionName::Text(value) | OptionName::Instance(value) => literals.push(value),
2142            OptionName::Rule => unreachable!("BUG: rule meanings are returned above"),
2143        }
2144    }
2145    reject_duplicate_options(lemma_type, &literals)?;
2146    append_option_literals(lemma_type, literals)?;
2147    Ok(())
2148}
2149
2150/// One grammatical name has one meaning. Two meanings is an error; zero is an error.
2151fn option_name_meaning(
2152    lemma_type: &LemmaType,
2153    spec: &crate::parsing::ast::LemmaSpec,
2154    name: &str,
2155) -> Result<OptionName, String> {
2156    let mut found: Vec<OptionName> = Vec::new();
2157    if matches!(lemma_type.specifications, TypeSpecification::Text { .. }) {
2158        found.push(OptionName::Text(OptionValue {
2159            value: BoundValueKind::unbound(ValueKind::Text(Arc::from(name))),
2160        }));
2161    }
2162    if let Some(data) = uses_alias(spec, name) {
2163        if let Ok(instance) = spec_alias_option(lemma_type, data, name) {
2164            found.push(OptionName::Instance(instance));
2165        }
2166    }
2167    if spec.rules.iter().any(|rule| rule.name == name) {
2168        found.push(OptionName::Rule);
2169    }
2170    match found.len() {
2171        1 => Ok(found.pop().expect("BUG: one option meaning")),
2172        0 => Err(format!("'{name}' is not a value of this type or a rule")),
2173        _ => Err(format!(
2174            "'{name}' is more than one of a text option, an instance, and a rule"
2175        )),
2176    }
2177}
2178
2179fn uses_alias<'a>(
2180    spec: &'a crate::parsing::ast::LemmaSpec,
2181    name: &str,
2182) -> Option<&'a crate::parsing::ast::LemmaData> {
2183    spec.data.iter().find(|data| {
2184        data.reference.name == name
2185            && matches!(data.value, crate::parsing::ast::DataValue::Import { .. })
2186    })
2187}
2188
2189fn spec_alias_option(
2190    lemma_type: &LemmaType,
2191    data: &crate::parsing::ast::LemmaData,
2192    name: &str,
2193) -> Result<OptionValue, String> {
2194    let TypeSpecification::Spec { spec } = &lemma_type.specifications else {
2195        return Err(format!("'{name}' is not a value of this type or a rule"));
2196    };
2197    let crate::parsing::ast::DataValue::Import { spec_ref, .. } = &data.value else {
2198        unreachable!("BUG: uses_alias returns an import");
2199    };
2200    if spec_ref.name != spec.spec {
2201        return Err(format!(
2202            "'{name}' is an instance of '{}', not '{}'",
2203            spec_ref.name, spec.spec
2204        ));
2205    }
2206    unreachable!(
2207        "BUG: `uses` alias `{name}` named on a spec type: a spec type is only a list element, `-> options` on it names a rule, and the parser reads `-> with {name}` on a list as an instance-list row"
2208    )
2209}
2210
2211fn append_option_literals(
2212    lemma_type: &mut LemmaType,
2213    incoming: Vec<OptionValue>,
2214) -> Result<(), String> {
2215    for option in incoming {
2216        if lemma_type
2217            .options
2218            .values
2219            .iter()
2220            .any(|existing| existing.value == option.value)
2221        {
2222            return Err(format!(
2223                "duplicate option '{}'",
2224                lemma_type.display_bound(&option.value)
2225            ));
2226        }
2227        lemma_type.options.values.push(option);
2228    }
2229    Ok(())
2230}
2231
2232fn static_option_match(options: &TypeOptions, value: &ValueKind) -> bool {
2233    options
2234        .values
2235        .iter()
2236        .any(|option| option_values_match(&option.value.value, value))
2237}
2238
2239fn options_have_unscaled_measure(options: &TypeOptions) -> bool {
2240    options
2241        .values
2242        .iter()
2243        .any(|option| option.value.value.has_unscaled_measure())
2244}
2245
2246/// Rewrite every [`ValueKind::UnscaledMeasure`] using composed runtime unit factors.
2247/// Walks ranges and lists iteratively. Missing scale for a needed unit is an error.
2248pub(crate) fn settle_value_kind_with_scales(
2249    value: &ValueKind,
2250    scales: &std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
2251) -> Result<ValueKind, String> {
2252    use crate::computation::rational::checked_mul;
2253
2254    enum Task {
2255        Visit(ValueKind),
2256        AssembleRange {
2257            left_type: Arc<LemmaType>,
2258            right_type: Arc<LemmaType>,
2259        },
2260        AssembleList {
2261            row_types: Vec<Arc<LemmaType>>,
2262        },
2263    }
2264
2265    let mut tasks = vec![Task::Visit(value.clone())];
2266    let mut done: Vec<ValueKind> = Vec::new();
2267    while let Some(task) = tasks.pop() {
2268        match task {
2269            Task::Visit(ValueKind::UnscaledMeasure { magnitude, unit }) => {
2270                let factor = scales.get(unit.as_ref()).ok_or_else(|| {
2271                    format!("unit '{unit}' scale is not settled for option membership")
2272                })?;
2273                let canonical =
2274                    checked_mul(&magnitude, factor).map_err(|failure| failure.to_string())?;
2275                done.push(ValueKind::Measure(canonical));
2276            }
2277            Task::Visit(ValueKind::Range(left, right)) => {
2278                tasks.push(Task::AssembleRange {
2279                    left_type: Arc::clone(&left.lemma_type),
2280                    right_type: Arc::clone(&right.lemma_type),
2281                });
2282                let right = *right;
2283                let left = *left;
2284                tasks.push(Task::Visit(right.value));
2285                tasks.push(Task::Visit(left.value));
2286            }
2287            Task::Visit(ValueKind::List(rows)) => {
2288                let owned = rows.to_vec();
2289                let row_types: Vec<Arc<LemmaType>> = owned
2290                    .iter()
2291                    .map(|row| Arc::clone(&row.lemma_type))
2292                    .collect();
2293                tasks.push(Task::AssembleList { row_types });
2294                for row in owned.into_iter().rev() {
2295                    tasks.push(Task::Visit(row.value));
2296                }
2297            }
2298            Task::Visit(other) => done.push(other),
2299            Task::AssembleRange {
2300                left_type,
2301                right_type,
2302            } => {
2303                let right_value = done
2304                    .pop()
2305                    .expect("BUG: settled range missing right endpoint");
2306                let left_value = done
2307                    .pop()
2308                    .expect("BUG: settled range missing left endpoint");
2309                done.push(ValueKind::Range(
2310                    Box::new(TypedLiteral {
2311                        value: left_value,
2312                        lemma_type: left_type,
2313                    }),
2314                    Box::new(TypedLiteral {
2315                        value: right_value,
2316                        lemma_type: right_type,
2317                    }),
2318                ));
2319            }
2320            Task::AssembleList { row_types } => {
2321                let mut settled = Vec::with_capacity(row_types.len());
2322                for lemma_type in row_types.into_iter().rev() {
2323                    let value = done.pop().expect("BUG: settled list missing a row");
2324                    settled.push(TypedLiteral { value, lemma_type });
2325                }
2326                settled.reverse();
2327                done.push(ValueKind::List(settled.into()));
2328            }
2329        }
2330    }
2331    Ok(done
2332        .pop()
2333        .expect("BUG: settle_value_kind_with_scales leaves one result"))
2334}
2335
2336fn static_option_match_with_scales(
2337    options: &TypeOptions,
2338    value: &ValueKind,
2339    scales: &std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
2340) -> Result<bool, String> {
2341    let settled_value = settle_value_kind_with_scales(value, scales)?;
2342    for option in &options.values {
2343        let settled_option = settle_value_kind_with_scales(&option.value.value, scales)?;
2344        if option_values_match(&settled_option, &settled_value) {
2345            return Ok(true);
2346        }
2347    }
2348    Ok(false)
2349}
2350
2351pub(crate) fn disallowed_message(
2352    lemma_type: &LemmaType,
2353    options: &TypeOptions,
2354    value: &ValueKind,
2355    rule_rows: Option<&ListValue>,
2356) -> String {
2357    let mut shown = options
2358        .values
2359        .iter()
2360        .map(|option| lemma_type.display_bound(&option.value))
2361        .collect::<Vec<_>>()
2362        .join(", ");
2363    if let Some(rows) = rule_rows {
2364        let rule_shown = rows
2365            .iter()
2366            .map(|row| row.value.to_string())
2367            .collect::<Vec<_>>()
2368            .join(", ");
2369        if !shown.is_empty() && !rule_shown.is_empty() {
2370            shown.push_str(", ");
2371        }
2372        shown.push_str(&rule_shown);
2373    }
2374    format!("'{value}' is not one of the allowed options: {shown}.")
2375}
2376
2377/// Equality for option matching of scalar and list-of-scalar values. Spec values never
2378/// appear: a list of spec instances takes no options ([`SPEC_LIST_HAS_NO_OPTIONS`]).
2379fn option_values_match(option: &ValueKind, value: &ValueKind) -> bool {
2380    let mut values: Vec<(ValueKind, ValueKind)> = vec![(option.clone(), value.clone())];
2381    while let Some((left, right)) = values.pop() {
2382        match (left, right) {
2383            (ValueKind::Spec(_), _) | (_, ValueKind::Spec(_)) => {
2384                unreachable!(
2385                    "BUG: option matching saw a spec value; {}",
2386                    SPEC_LIST_HAS_NO_OPTIONS
2387                );
2388            }
2389            (ValueKind::List(left), ValueKind::List(right)) => {
2390                if left.len() != right.len() {
2391                    return false;
2392                }
2393                for (a, b) in left.iter().zip(right.iter()) {
2394                    if a.lemma_type != b.lemma_type {
2395                        return false;
2396                    }
2397                    values.push((a.into_owned().value, b.into_owned().value));
2398                }
2399            }
2400            (left, right) => {
2401                if left != right {
2402                    return false;
2403                }
2404            }
2405        }
2406    }
2407    true
2408}
2409
2410pub(crate) fn option_detail_line(lemma_type: &LemmaType) -> Option<String> {
2411    let options = &lemma_type.options;
2412    let mut parts = Vec::new();
2413    if !options.values.is_empty() {
2414        parts.push(
2415            options
2416                .values
2417                .iter()
2418                .map(|option| lemma_type.display_bound(&option.value))
2419                .collect::<Vec<_>>()
2420                .join(", "),
2421        );
2422    }
2423    if let Some(name) = &options.rule {
2424        parts.push(name.clone());
2425    }
2426    if parts.is_empty() {
2427        None
2428    } else {
2429        Some(format!("options: {}", parts.join(", ")))
2430    }
2431}
2432
2433fn reject_duplicate_options(lemma_type: &LemmaType, values: &[OptionValue]) -> Result<(), String> {
2434    for (index, option) in values.iter().enumerate() {
2435        if values[..index]
2436            .iter()
2437            .any(|earlier| earlier.value == option.value)
2438        {
2439            return Err(format!(
2440                "duplicate option '{}'",
2441                lemma_type.display_bound(&option.value)
2442            ));
2443        }
2444    }
2445    Ok(())
2446}
2447
2448fn label_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
2449    match arg {
2450        CommandArg::Label(name) => Ok(name.clone()),
2451        CommandArg::Literal(other) => Err(format!(
2452            "{} requires an identifier, got {}",
2453            cmd,
2454            value_kind_name(other)
2455        )),
2456        CommandArg::UnitExpr(_) => Err(format!(
2457            "{} requires an identifier, got a unit expression",
2458            cmd
2459        )),
2460    }
2461}
2462
2463fn measure_trait_name(measure_trait: MeasureTrait) -> &'static str {
2464    match measure_trait {
2465        MeasureTrait::Duration => "duration",
2466        MeasureTrait::Calendar => "calendar",
2467    }
2468}
2469
2470fn parse_measure_trait(args: &[CommandArg]) -> Result<MeasureTrait, String> {
2471    if args.len() != 1 {
2472        return Err("trait requires exactly one identifier argument".to_string());
2473    }
2474    match label_name(&args[0], "trait")?
2475        .trim()
2476        .to_lowercase()
2477        .as_str()
2478    {
2479        "duration" => Ok(MeasureTrait::Duration),
2480        "calendar" => Ok(MeasureTrait::Calendar),
2481        other => Err(format!("Unknown measure trait '{}'", other)),
2482    }
2483}
2484
2485fn validate_calendar_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
2486    let month_unit = units
2487        .iter()
2488        .find(|unit| unit.name == "month")
2489        .ok_or_else(|| {
2490            "trait calendar requires a canonical 'month' unit declared before 'trait calendar'"
2491                .to_string()
2492        })?;
2493    if !month_unit.is_canonical_factor() {
2494        return Err("trait calendar requires unit month 1".to_string());
2495    }
2496    Ok(())
2497}
2498
2499fn validate_duration_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
2500    let second_unit = units
2501        .iter()
2502        .find(|unit| unit.name == "second")
2503        .ok_or_else(|| {
2504            "trait duration requires a canonical 'second' unit declared before 'trait duration'"
2505                .to_string()
2506        })?;
2507    if !second_unit.is_canonical_factor() {
2508        return Err("trait duration requires unit second 1".to_string());
2509    }
2510    Ok(())
2511}
2512
2513/// Extract a [`DateTimeValue`] from a [`Value::Date`] literal arg.
2514fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
2515    match require_literal(args, cmd)? {
2516        crate::literals::Value::Date(dt) => Ok(dt.clone()),
2517        other => Err(format!(
2518            "{} requires a date literal (e.g. 2024-01-01), got {}",
2519            cmd,
2520            value_kind_name(other)
2521        )),
2522    }
2523}
2524
2525/// Extract a [`TimeValue`] from a [`Value::Time`] literal arg.
2526fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
2527    match require_literal(args, cmd)? {
2528        crate::literals::Value::Time(t) => Ok(t.clone()),
2529        other => Err(format!(
2530            "{} requires a time literal (e.g. 12:30:00), got {}",
2531            cmd,
2532            value_kind_name(other)
2533        )),
2534    }
2535}
2536
2537/// Default `help` for a built-in primitive (goal-oriented; syntax lives in [`LemmaType::example_value`]).
2538#[must_use]
2539pub fn default_help_for_primitive(kind: PrimitiveKind) -> &'static str {
2540    use PrimitiveKind::*;
2541    match kind {
2542        Boolean => "Whether this holds (true or false).",
2543        Number => "A dimensionless number.",
2544        NumberRange => "The lower and upper bound of the number range.",
2545        Text => "A text value.",
2546        Measure => "A numeric amount in one of this type's units.",
2547        MeasureRange => "The lower and upper bound of the measure range.",
2548        Ratio => "A ratio in one of this type's units (e.g. percent).",
2549        RatioRange => "The lower and upper bound of the ratio range.",
2550        Date => "A date, or a date and time with optional timezone.",
2551        DateRange => "The start date and end date of the date range.",
2552        Time => "A time of day, with optional timezone.",
2553        TimeRange => "The start time and end time of the time range.",
2554    }
2555}
2556
2557impl TypeSpecification {
2558    pub fn boolean() -> Self {
2559        TypeSpecification::Boolean {
2560            help: default_help_for_primitive(PrimitiveKind::Boolean).to_string(),
2561        }
2562    }
2563    pub fn measure() -> Self {
2564        TypeSpecification::Measure {
2565            minimum: None,
2566            maximum: None,
2567            units: MeasureUnits::new(),
2568            traits: Vec::new(),
2569            decomposition: None,
2570            unit: None,
2571            help: default_help_for_primitive(PrimitiveKind::Measure).to_string(),
2572        }
2573    }
2574    pub fn number() -> Self {
2575        TypeSpecification::Number {
2576            minimum: None,
2577            maximum: None,
2578            decimals: None,
2579            help: default_help_for_primitive(PrimitiveKind::Number).to_string(),
2580        }
2581    }
2582    pub fn number_range() -> Self {
2583        TypeSpecification::NumberRange {
2584            lower: None,
2585            upper: None,
2586            minimum: None,
2587            maximum: None,
2588            help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
2589        }
2590    }
2591    pub fn ratio() -> Self {
2592        TypeSpecification::Ratio {
2593            minimum: None,
2594            maximum: None,
2595            units: RatioUnits(vec![
2596                RatioUnit {
2597                    name: "percent".to_string(),
2598                    value: crate::computation::rational::rational_new(100, 1),
2599                    minimum: None,
2600                    maximum: None,
2601                    suggestion_magnitude: None,
2602                },
2603                RatioUnit {
2604                    name: "permille".to_string(),
2605                    value: crate::computation::rational::rational_new(1000, 1),
2606                    minimum: None,
2607                    maximum: None,
2608                    suggestion_magnitude: None,
2609                },
2610            ]),
2611            unit: None,
2612            help: default_help_for_primitive(PrimitiveKind::Ratio).to_string(),
2613        }
2614    }
2615    pub fn ratio_range() -> Self {
2616        TypeSpecification::RatioRange {
2617            lower: None,
2618            upper: None,
2619            minimum: None,
2620            maximum: None,
2621            units: match TypeSpecification::ratio() {
2622                TypeSpecification::Ratio { units, .. } => units,
2623                _ => unreachable!("BUG: ratio constructor must return a ratio type"),
2624            },
2625            help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
2626        }
2627    }
2628    pub fn text() -> Self {
2629        TypeSpecification::Text {
2630            length: None,
2631            help: default_help_for_primitive(PrimitiveKind::Text).to_string(),
2632        }
2633    }
2634    pub fn date() -> Self {
2635        TypeSpecification::Date {
2636            minimum: None,
2637            maximum: None,
2638            help: default_help_for_primitive(PrimitiveKind::Date).to_string(),
2639        }
2640    }
2641    pub fn date_range() -> Self {
2642        TypeSpecification::DateRange {
2643            lower: None,
2644            upper: None,
2645            minimum: None,
2646            maximum: None,
2647            help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
2648        }
2649    }
2650    pub fn time() -> Self {
2651        TypeSpecification::Time {
2652            minimum: None,
2653            maximum: None,
2654            help: default_help_for_primitive(PrimitiveKind::Time).to_string(),
2655        }
2656    }
2657    pub fn time_range() -> Self {
2658        TypeSpecification::TimeRange {
2659            lower: None,
2660            upper: None,
2661            minimum: None,
2662            maximum: None,
2663            help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
2664        }
2665    }
2666    pub fn measure_range() -> Self {
2667        TypeSpecification::MeasureRange {
2668            lower: None,
2669            upper: None,
2670            minimum: None,
2671            maximum: None,
2672            units: MeasureUnits::new(),
2673            decomposition: None,
2674            help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
2675        }
2676    }
2677
2678    /// Element spec for a range type (e.g. `MeasureRange` → `Measure`).
2679    #[must_use]
2680    pub fn element_from_range(&self) -> Option<Self> {
2681        match self {
2682            TypeSpecification::NumberRange { lower, upper, .. } => {
2683                Some(TypeSpecification::Number {
2684                    minimum: lower.clone(),
2685                    maximum: upper.clone(),
2686                    decimals: None,
2687                    help: String::new(),
2688                })
2689            }
2690            TypeSpecification::MeasureRange {
2691                lower,
2692                upper,
2693                units,
2694                decomposition,
2695                ..
2696            } => Some(TypeSpecification::Measure {
2697                minimum: lower.clone(),
2698                maximum: upper.clone(),
2699                units: units.clone(),
2700                traits: Vec::new(),
2701                decomposition: decomposition.clone(),
2702                unit: None,
2703                help: String::new(),
2704            }),
2705            TypeSpecification::DateRange { lower, upper, .. } => Some(TypeSpecification::Date {
2706                minimum: lower.clone(),
2707                maximum: upper.clone(),
2708                help: String::new(),
2709            }),
2710            TypeSpecification::TimeRange { lower, upper, .. } => Some(TypeSpecification::Time {
2711                minimum: lower.clone(),
2712                maximum: upper.clone(),
2713                help: String::new(),
2714            }),
2715            TypeSpecification::RatioRange {
2716                lower,
2717                upper,
2718                units,
2719                ..
2720            } => Some(TypeSpecification::Ratio {
2721                minimum: lower.clone(),
2722                maximum: upper.clone(),
2723                units: units.clone(),
2724                unit: None,
2725                help: String::new(),
2726            }),
2727            _ => None,
2728        }
2729    }
2730
2731    /// Range spec for an element type (e.g. `Measure` → `MeasureRange`).
2732    #[must_use]
2733    pub fn range_from_element(&self) -> Option<Self> {
2734        match self {
2735            TypeSpecification::Number {
2736                minimum, maximum, ..
2737            } => Some(TypeSpecification::NumberRange {
2738                lower: minimum.clone(),
2739                upper: maximum.clone(),
2740                minimum: None,
2741                maximum: None,
2742                help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
2743            }),
2744            TypeSpecification::Measure {
2745                minimum,
2746                maximum,
2747                units,
2748                decomposition,
2749                ..
2750            } => Some(TypeSpecification::MeasureRange {
2751                lower: minimum.clone(),
2752                upper: maximum.clone(),
2753                minimum: None,
2754                maximum: None,
2755                units: units.clone(),
2756                decomposition: decomposition.clone(),
2757                help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
2758            }),
2759            TypeSpecification::Date {
2760                minimum, maximum, ..
2761            } => Some(TypeSpecification::DateRange {
2762                lower: minimum.clone(),
2763                upper: maximum.clone(),
2764                minimum: None,
2765                maximum: None,
2766                help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
2767            }),
2768            TypeSpecification::Time {
2769                minimum, maximum, ..
2770            } => Some(TypeSpecification::TimeRange {
2771                lower: minimum.clone(),
2772                upper: maximum.clone(),
2773                minimum: None,
2774                maximum: None,
2775                help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
2776            }),
2777            TypeSpecification::Ratio {
2778                minimum,
2779                maximum,
2780                units,
2781                ..
2782            } => Some(TypeSpecification::RatioRange {
2783                lower: minimum.clone(),
2784                upper: maximum.clone(),
2785                minimum: None,
2786                maximum: None,
2787                units: units.clone(),
2788                help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
2789            }),
2790            _ => None,
2791        }
2792    }
2793
2794    /// Type-level minimum as decimal: bare number bound, ratio canonical fraction, or
2795    /// measure magnitude in the declaring unit (unit tag ignored). Interactive prompts that
2796    /// ask for a selected-unit magnitude must use [`crate::RatioUnit`] /
2797    /// [`crate::MeasureUnit`] decimals after unit selection instead.
2798    #[must_use]
2799    pub fn minimum_decimal(&self) -> Option<Decimal> {
2800        match self {
2801            TypeSpecification::Number { minimum, .. }
2802            | TypeSpecification::Ratio { minimum, .. } => minimum.as_ref().map(|bound| {
2803                bound
2804                    .try_to_decimal()
2805                    .expect("BUG: planned minimum must convert to decimal")
2806            }),
2807            TypeSpecification::Measure { minimum, .. } => minimum.as_ref().map(|(bound, _)| {
2808                bound
2809                    .try_to_decimal()
2810                    .expect("BUG: planned minimum must convert to decimal")
2811            }),
2812            _ => None,
2813        }
2814    }
2815
2816    /// Type-level maximum as decimal: bare number bound, ratio canonical fraction, or
2817    /// measure magnitude in the declaring unit (unit tag ignored). Interactive prompts that
2818    /// ask for a selected-unit magnitude must use [`crate::RatioUnit`] /
2819    /// [`crate::MeasureUnit`] decimals after unit selection instead.
2820    #[must_use]
2821    pub fn maximum_decimal(&self) -> Option<Decimal> {
2822        match self {
2823            TypeSpecification::Number { maximum, .. }
2824            | TypeSpecification::Ratio { maximum, .. } => maximum.as_ref().map(|bound| {
2825                bound
2826                    .try_to_decimal()
2827                    .expect("BUG: planned maximum must convert to decimal")
2828            }),
2829            TypeSpecification::Measure { maximum, .. } => maximum.as_ref().map(|(bound, _)| {
2830                bound
2831                    .try_to_decimal()
2832                    .expect("BUG: planned maximum must convert to decimal")
2833            }),
2834            _ => None,
2835        }
2836    }
2837
2838    pub fn veto() -> Self {
2839        TypeSpecification::Veto { message: None }
2840    }
2841
2842    /// Apply a single constraint command to this spec.
2843    ///
2844    /// The `declared_suggestion` and `declared_value` out-parameters receive default values
2845    /// (if the command is `Suggest` or `With`), encoded as [`RawSuggestion`]. Defaults are
2846    /// owned by the data binding or typedef entry, not by the type specification itself;
2847    /// callers thread `&mut Option<RawSuggestion>` for each across constraint applications
2848    /// for one declaration. Duplicate `-> suggest` / `-> with` / `minimum` / `maximum` /
2849    /// `decimals` on the same declaration are rejected by the caller seen-set before this
2850    /// runs. A child typedef may override an inherited suggest, with, or bound with one
2851    /// command of that kind. Measure scalars stay raw until unit factors are resolved;
2852    /// callers convert via [`bound_value_kind_from_raw_suggestion`].
2853    pub fn apply_constraint(
2854        &mut self,
2855        names: ConstraintNames<'_>,
2856        command: TypeConstraintCommand,
2857        args: &[CommandArg],
2858        declared_suggestion: &mut Option<RawSuggestion>,
2859        declared_value: &mut Option<RawSuggestion>,
2860        resolve_unit: Option<&UnitTokenResolver<'_>>,
2861    ) -> Result<(), String> {
2862        let type_name = names.type_name;
2863        if command == TypeConstraintCommand::With
2864            && args.len() > 1
2865            && !matches!(self, TypeSpecification::List { .. })
2866        {
2867            return Err(format!("`-> with` on {self} takes exactly one value"));
2868        }
2869        if command == TypeConstraintCommand::Trait
2870            && !matches!(&self, TypeSpecification::Measure { .. })
2871        {
2872            return Err("trait command is only valid on measure types".to_string());
2873        }
2874        match self {
2875            TypeSpecification::Boolean { help } => match command {
2876                TypeConstraintCommand::Help => {
2877                    apply_type_help_command(help, args)?;
2878                }
2879                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
2880                    let (target, cmd) = match command {
2881                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2882                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
2883                        _ => unreachable!("BUG: only Suggest or With in this arm"),
2884                    };
2885                    let lit = require_literal(args, cmd)?;
2886                    reject_calendar_for_suggestion(
2887                        lit,
2888                        type_name,
2889                        SuggestionExpectation::Boolean,
2890                        None,
2891                    )?;
2892                    match lit {
2893                        crate::literals::Value::Boolean(bv) => {
2894                            *target =
2895                                Some(RawSuggestion::Value(ValueKind::Boolean(bool::from(bv))));
2896                        }
2897                        _ => {
2898                            return Err(
2899                                "Please provide true or false, for example `-> suggest true`."
2900                                    .to_string(),
2901                            );
2902                        }
2903                    }
2904                }
2905                other => {
2906                    return Err(format!(
2907                        "Invalid command '{}' for boolean type. Valid commands: help, suggest, with",
2908                        other
2909                    ));
2910                }
2911            },
2912            TypeSpecification::Measure {
2913                minimum,
2914                maximum,
2915                units,
2916                traits,
2917                help,
2918                ..
2919            } => match command {
2920                TypeConstraintCommand::Decimals => {
2921                    return Err(decimals_depend_on_the_unit_error(
2922                        "measure",
2923                        names,
2924                        args,
2925                        units.0.first().map(|unit| unit.name.as_str()),
2926                    )?);
2927                }
2928                TypeConstraintCommand::Unit => match args {
2929                    [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Reference(reference))] =>
2930                    {
2931                        let unit_name = name.clone();
2932                        let written = reference.to_string();
2933                        if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2934                            let same = match &existing.scale {
2935                                UnitScale::Unresolved {
2936                                    segments,
2937                                    name: target_name,
2938                                    ..
2939                                } => {
2940                                    segments == &reference.segments
2941                                        && target_name == &reference.name
2942                                }
2943                                UnitScale::Literal(_) | UnitScale::Reference { .. } => false,
2944                            };
2945                            if !same {
2946                                return Err(format!(
2947                                        "Unit '{unit_name}' is already defined in this type's inherited units; \
2948                                         cannot change factor or decomposition. Add a new unit name instead."
2949                                    ));
2950                            }
2951                        } else {
2952                            units.0.push(MeasureUnit::from_reference(
2953                                unit_name,
2954                                written,
2955                                reference.segments.clone(),
2956                                reference.name.clone(),
2957                            ));
2958                        }
2959                    }
2960                    [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(value))] =>
2961                    {
2962                        apply_measure_unit_factor(units, name.clone(), *value, Vec::new())?;
2963                    }
2964                    [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(prefix, factors))] =>
2965                    {
2966                        let derived_measure_factors: Vec<(String, i32)> = factors
2967                            .iter()
2968                            .map(|f| (f.measure_ref.clone(), f.exp))
2969                            .collect();
2970                        apply_measure_unit_factor(
2971                            units,
2972                            name.clone(),
2973                            *prefix,
2974                            derived_measure_factors,
2975                        )?;
2976                    }
2977                    _ => {
2978                        return Err(
2979                                "unit requires a unit name followed by a conversion factor, compound unit expression, or number reference (e.g., 'unit eur 1.00', 'unit mps meter/second', 'unit gbp rate')"
2980                                    .to_string(),
2981                            );
2982                    }
2983                },
2984                TypeConstraintCommand::Trait => {
2985                    let measure_trait = parse_measure_trait(args)?;
2986                    if traits.contains(&measure_trait) {
2987                        return Err(format!(
2988                            "Duplicate trait '{}' for measure type.",
2989                            measure_trait_name(measure_trait)
2990                        ));
2991                    }
2992                    if measure_trait == MeasureTrait::Duration {
2993                        validate_duration_trait_requirements(units)?;
2994                    }
2995                    if measure_trait == MeasureTrait::Calendar {
2996                        validate_calendar_trait_requirements(units)?;
2997                    }
2998                    traits.push(measure_trait);
2999                }
3000                TypeConstraintCommand::Minimum => {
3001                    *minimum = Some(parse_measure_declared_bound(
3002                        args, "minimum", units, type_name,
3003                    )?);
3004                }
3005                TypeConstraintCommand::Maximum => {
3006                    *maximum = Some(parse_measure_declared_bound(
3007                        args, "maximum", units, type_name,
3008                    )?);
3009                }
3010                TypeConstraintCommand::Help => {
3011                    apply_type_help_command(help, args)?;
3012                }
3013                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3014                    let (target, cmd) = match command {
3015                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3016                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
3017                        _ => unreachable!("BUG: only Suggest or With in this arm"),
3018                    };
3019                    let lit = require_literal(args, cmd)?;
3020                    if !traits.contains(&MeasureTrait::Calendar) {
3021                        reject_calendar_for_suggestion(
3022                            lit,
3023                            type_name,
3024                            SuggestionExpectation::MeasureUnits,
3025                            Some(units),
3026                        )?;
3027                    }
3028                    match lit {
3029                        crate::literals::Value::NumberWithUnit(n, unit_name) => {
3030                            if units
3031                                .get(unit_name)
3032                                .is_ok_and(crate::literals::MeasureUnit::is_dynamic)
3033                            {
3034                                *target = Some(RawSuggestion::Measure {
3035                                    magnitude: lift_parser_decimal(*n)?,
3036                                    unit_name: unit_name.clone(),
3037                                });
3038                            } else {
3039                                let (magnitude, unit_name) =
3040                                    parse_measure_declared_bound(args, cmd, units, type_name)?;
3041                                *target = Some(RawSuggestion::Measure {
3042                                    magnitude,
3043                                    unit_name,
3044                                });
3045                            }
3046                        }
3047                        _ => {
3048                            return Err(measure_suggestion_wrong_shape_error(type_name, traits));
3049                        }
3050                    }
3051                }
3052                _ => {
3053                    return Err(format!(
3054                        "Invalid command '{}' for measure type. Valid commands: unit, trait, minimum, maximum, help, suggest, with",
3055                        command
3056                    ));
3057                }
3058            },
3059            TypeSpecification::Number {
3060                decimals,
3061                minimum,
3062                maximum,
3063                help,
3064            } => match command {
3065                TypeConstraintCommand::Decimals => {
3066                    let d = require_decimal_literal(args, "decimals")?;
3067                    *decimals = Some(decimal_to_u8(d, "decimals")?);
3068                }
3069                TypeConstraintCommand::Unit => {
3070                    return Err(
3071                        "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'measure' type instead.".to_string()
3072                    );
3073                }
3074                TypeConstraintCommand::Minimum => {
3075                    *minimum = Some(require_decimal_literal(args, "minimum")?);
3076                }
3077                TypeConstraintCommand::Maximum => {
3078                    *maximum = Some(require_decimal_literal(args, "maximum")?);
3079                }
3080                TypeConstraintCommand::Help => {
3081                    apply_type_help_command(help, args)?;
3082                }
3083                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3084                    let (target, cmd) = match command {
3085                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3086                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
3087                        _ => unreachable!("BUG: only Suggest or With in this arm"),
3088                    };
3089                    let lit = require_literal(args, cmd)?;
3090                    reject_calendar_for_suggestion(
3091                        lit,
3092                        type_name,
3093                        SuggestionExpectation::Number,
3094                        None,
3095                    )?;
3096                    match lit {
3097                        crate::literals::Value::Number(d) => {
3098                            *target = Some(RawSuggestion::Value(ValueKind::Number(
3099                                lift_parser_decimal(*d)?,
3100                            )));
3101                        }
3102                        _ => {
3103                            return Err(
3104                                "Please provide a number, for example `-> suggest 42`.".to_string()
3105                            );
3106                        }
3107                    }
3108                }
3109                _ => {
3110                    return Err(format!(
3111                        "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, suggest, with",
3112                        command
3113                    ));
3114                }
3115            },
3116            TypeSpecification::NumberRange {
3117                lower,
3118                upper,
3119                minimum,
3120                maximum,
3121                help,
3122            } => match command {
3123                TypeConstraintCommand::Lower => {
3124                    *lower = Some(require_decimal_literal(args, "lower")?);
3125                }
3126                TypeConstraintCommand::Upper => {
3127                    *upper = Some(require_decimal_literal(args, "upper")?);
3128                }
3129                TypeConstraintCommand::Minimum => {
3130                    let width = require_decimal_literal(args, "minimum")?;
3131                    reject_negative_width_magnitude(&width, "minimum")?;
3132                    *minimum = Some(width);
3133                }
3134                TypeConstraintCommand::Maximum => {
3135                    let width = require_decimal_literal(args, "maximum")?;
3136                    reject_negative_width_magnitude(&width, "maximum")?;
3137                    *maximum = Some(width);
3138                }
3139                TypeConstraintCommand::Help => {
3140                    apply_type_help_command(help, args)?;
3141                }
3142                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3143                    let target = match command {
3144                        TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3145                        TypeConstraintCommand::With => &mut *declared_value,
3146                        _ => unreachable!("BUG: only Suggest or With in this arm"),
3147                    };
3148                    let (left, right) = require_suggestion_range_endpoints(
3149                        args,
3150                        type_name,
3151                        SuggestionExpectation::NumberRange,
3152                        None,
3153                    )?;
3154                    let left = literal_value_from_parser_value(left)?;
3155                    let right = literal_value_from_parser_value(right)?;
3156                    if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
3157                        return Err(
3158                            "Please provide a number range, for example `-> suggest 10...100`."
3159                                .to_string(),
3160                        );
3161                    }
3162                    *target = Some(RawSuggestion::Value(ValueKind::Range(
3163                        Box::new(left),
3164                        Box::new(right),
3165                    )));
3166                }
3167                _ => {
3168                    return Err(format!(
3169                        "Invalid command '{}' for number range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3170                        command
3171                    ));
3172                }
3173            },
3174            TypeSpecification::Ratio {
3175                minimum,
3176                maximum,
3177                units,
3178                unit: _,
3179                help,
3180            } => match command {
3181                TypeConstraintCommand::Decimals => {
3182                    return Err(decimals_depend_on_the_unit_error(
3183                        "ratio",
3184                        names,
3185                        args,
3186                        units.0.first().map(|unit| unit.name.as_str()),
3187                    )?);
3188                }
3189                TypeConstraintCommand::Unit => {
3190                    let (unit_name, value_dec) = match args {
3191                        [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
3192                            (name.clone(), *v)
3193                        }
3194                        _ => {
3195                            return Err(
3196                                "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."
3197                                    .to_string(),
3198                            );
3199                        }
3200                    };
3201                    let value = crate::computation::rational::decimal_to_rational(value_dec)
3202                        .map_err(|failure| {
3203                            format!(
3204                                "ratio unit value is not exactly representable as a rational: {}",
3205                                failure
3206                            )
3207                        })?;
3208                    if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
3209                        if existing.value != value {
3210                            return Err(format!(
3211                                "Unit '{unit_name}' is already defined in this type's inherited units; \
3212                                 cannot change factor. Add a new unit name instead."
3213                            ));
3214                        }
3215                    } else {
3216                        units.0.push(RatioUnit {
3217                            name: unit_name,
3218                            value,
3219                            minimum: None,
3220                            maximum: None,
3221                            suggestion_magnitude: None,
3222                        });
3223                    }
3224                }
3225                TypeConstraintCommand::Minimum => {
3226                    let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
3227                    sync_ratio_units_from_canonical(
3228                        units,
3229                        &canonical,
3230                        UnitConstraintField::Minimum,
3231                    )?;
3232                    *minimum = Some(canonical);
3233                }
3234                TypeConstraintCommand::Maximum => {
3235                    let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
3236                    sync_ratio_units_from_canonical(
3237                        units,
3238                        &canonical,
3239                        UnitConstraintField::Maximum,
3240                    )?;
3241                    *maximum = Some(canonical);
3242                }
3243                TypeConstraintCommand::Help => {
3244                    apply_type_help_command(help, args)?;
3245                }
3246                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3247                    let (target, cmd) = match command {
3248                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3249                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
3250                        _ => unreachable!("BUG: only Suggest or With in this arm"),
3251                    };
3252                    let lit = require_literal(args, cmd)?;
3253                    reject_calendar_for_suggestion(
3254                        lit,
3255                        type_name,
3256                        SuggestionExpectation::Ratio,
3257                        None,
3258                    )?;
3259                    match lit {
3260                        crate::literals::Value::NumberWithUnit(_, unit_name) => {
3261                            let element_type = LemmaType::primitive(TypeSpecification::Ratio {
3262                                minimum: minimum.clone(),
3263                                maximum: maximum.clone(),
3264                                units: units.clone(),
3265                                unit: None,
3266                                help: help.clone(),
3267                            });
3268                            let value = parser_value_to_value_kind(lit, &element_type, None)?;
3269                            sync_ratio_suggestion_units(units, &value)?;
3270                            *target = Some(RawSuggestion::UnitBound {
3271                                value,
3272                                unit_name: unit_name.clone(),
3273                            });
3274                        }
3275                        other => {
3276                            return Err(format!(
3277                                "suggest requires a ratio literal with a unit, got {}. Please provide a ratio value with a unit, for example `-> suggest 25%`.",
3278                                value_kind_name(other)
3279                            ));
3280                        }
3281                    }
3282                }
3283                _ => {
3284                    return Err(format!(
3285                        "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, help, suggest, with",
3286                        command
3287                    ));
3288                }
3289            },
3290            TypeSpecification::RatioRange {
3291                lower,
3292                upper,
3293                minimum,
3294                maximum,
3295                units,
3296                help,
3297            } => match command {
3298                TypeConstraintCommand::Unit => {
3299                    return Err(format!(
3300                        "'unit' is not allowed on 'ratio range' type '{type_name}'. Declare units on a ratio type and write 'data x: <that type> range'."
3301                    ));
3302                }
3303                TypeConstraintCommand::Lower => {
3304                    *lower = Some(ratio_bound_to_canonical_rational(args, "lower", units)?);
3305                }
3306                TypeConstraintCommand::Upper => {
3307                    *upper = Some(ratio_bound_to_canonical_rational(args, "upper", units)?);
3308                }
3309                TypeConstraintCommand::Minimum => {
3310                    let width = ratio_bound_to_canonical_rational(args, "minimum", units)?;
3311                    reject_negative_width_magnitude(&width, "minimum")?;
3312                    *minimum = Some(width);
3313                }
3314                TypeConstraintCommand::Maximum => {
3315                    let width = ratio_bound_to_canonical_rational(args, "maximum", units)?;
3316                    reject_negative_width_magnitude(&width, "maximum")?;
3317                    *maximum = Some(width);
3318                }
3319                TypeConstraintCommand::Help => {
3320                    apply_type_help_command(help, args)?;
3321                }
3322                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3323                    let target = match command {
3324                        TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3325                        TypeConstraintCommand::With => &mut *declared_value,
3326                        _ => unreachable!("BUG: only Suggest or With in this arm"),
3327                    };
3328                    let (left, right) = require_suggestion_range_endpoints(
3329                        args,
3330                        type_name,
3331                        SuggestionExpectation::RatioRange,
3332                        None,
3333                    )?;
3334                    let element_type = LemmaType::primitive(
3335                        TypeSpecification::RatioRange {
3336                            lower: lower.clone(),
3337                            upper: upper.clone(),
3338                            minimum: minimum.clone(),
3339                            maximum: maximum.clone(),
3340                            units: units.clone(),
3341                            help: help.clone(),
3342                        }
3343                        .element_from_range()
3344                        .expect("BUG: RatioRange must define element_from_range"),
3345                    );
3346                    let left = lift_range_endpoint(left, &element_type, resolve_unit)?;
3347                    let right = lift_range_endpoint(right, &element_type, resolve_unit)?;
3348                    if !value_kind_matches_spec(&left.value, &element_type.specifications)
3349                        || !value_kind_matches_spec(&right.value, &element_type.specifications)
3350                        || !matches!(&left.value, ValueKind::Ratio(_))
3351                        || !matches!(&right.value, ValueKind::Ratio(_))
3352                    {
3353                        return Err(
3354                            "Please provide a ratio range, for example `-> suggest 10%...50%`."
3355                                .to_string(),
3356                        );
3357                    }
3358                    *target = Some(RawSuggestion::Value(ValueKind::Range(
3359                        Box::new(left),
3360                        Box::new(right),
3361                    )));
3362                }
3363                _ => {
3364                    return Err(format!(
3365                        "Invalid command '{}' for ratio range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3366                        command
3367                    ));
3368                }
3369            },
3370            TypeSpecification::Text { length, help } => match command {
3371                TypeConstraintCommand::Length => {
3372                    let d = require_decimal_literal(args, "length")?;
3373                    *length = Some(decimal_to_usize(d, "length")?);
3374                }
3375                TypeConstraintCommand::Help => {
3376                    apply_type_help_command(help, args)?;
3377                }
3378                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3379                    let (target, cmd) = match command {
3380                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3381                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
3382                        _ => unreachable!("BUG: only Suggest or With in this arm"),
3383                    };
3384                    let lit = require_literal(args, cmd)?;
3385                    reject_calendar_for_suggestion(
3386                        lit,
3387                        type_name,
3388                        SuggestionExpectation::Text,
3389                        None,
3390                    )?;
3391                    match lit {
3392                        crate::literals::Value::Text(s) => {
3393                            *target =
3394                                Some(RawSuggestion::Value(ValueKind::Text(Arc::from(s.as_str()))));
3395                        }
3396                        _ => {
3397                            return Err(
3398                                "Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
3399                                    .to_string(),
3400                            );
3401                        }
3402                    }
3403                }
3404                _ => {
3405                    return Err(format!(
3406                        "Invalid command '{}' for text type. Valid commands: length, help, suggest, with",
3407                        command
3408                    ));
3409                }
3410            },
3411            TypeSpecification::Date {
3412                minimum,
3413                maximum,
3414                help,
3415            } => match command {
3416                TypeConstraintCommand::Minimum => {
3417                    let dt = require_date_literal(args, "minimum")?;
3418                    *minimum = Some(dt);
3419                }
3420                TypeConstraintCommand::Maximum => {
3421                    let dt = require_date_literal(args, "maximum")?;
3422                    *maximum = Some(dt);
3423                }
3424                TypeConstraintCommand::Help => {
3425                    apply_type_help_command(help, args)?;
3426                }
3427                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3428                    let (target, cmd) = match command {
3429                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3430                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
3431                        _ => unreachable!("BUG: only Suggest or With in this arm"),
3432                    };
3433                    let lit = require_literal(args, cmd)?;
3434                    reject_calendar_for_suggestion(
3435                        lit,
3436                        type_name,
3437                        SuggestionExpectation::Date,
3438                        None,
3439                    )?;
3440                    match lit {
3441                        crate::literals::Value::Date(dt) => {
3442                            *target = Some(RawSuggestion::Value(ValueKind::Date(
3443                                date_time_to_semantic(dt),
3444                            )));
3445                        }
3446                        _ => {
3447                            return Err(
3448                                "Please provide a date, for example `-> suggest 2024-06-15`."
3449                                    .to_string(),
3450                            );
3451                        }
3452                    }
3453                }
3454                _ => {
3455                    return Err(format!(
3456                        "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, suggest, with",
3457                        command
3458                    ));
3459                }
3460            },
3461            TypeSpecification::DateRange {
3462                lower,
3463                upper,
3464                minimum,
3465                maximum,
3466                help,
3467            } => match command {
3468                TypeConstraintCommand::Lower => {
3469                    *lower = Some(require_date_literal(args, "lower")?);
3470                }
3471                TypeConstraintCommand::Upper => {
3472                    *upper = Some(require_date_literal(args, "upper")?);
3473                }
3474                TypeConstraintCommand::Minimum => {
3475                    *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
3476                }
3477                TypeConstraintCommand::Maximum => {
3478                    *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
3479                }
3480                TypeConstraintCommand::Help => {
3481                    apply_type_help_command(help, args)?;
3482                }
3483                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3484                    let target = match command {
3485                        TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3486                        TypeConstraintCommand::With => &mut *declared_value,
3487                        _ => unreachable!("BUG: only Suggest or With in this arm"),
3488                    };
3489                    let (left, right) = require_suggestion_range_endpoints(
3490                        args,
3491                        type_name,
3492                        SuggestionExpectation::DateRange,
3493                        None,
3494                    )?;
3495                    let left = literal_value_from_parser_value(left)?;
3496                    let right = literal_value_from_parser_value(right)?;
3497                    if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
3498                        return Err(
3499                            "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
3500                                .to_string(),
3501                        );
3502                    }
3503                    *target = Some(RawSuggestion::Value(ValueKind::Range(
3504                        Box::new(left),
3505                        Box::new(right),
3506                    )));
3507                }
3508                _ => {
3509                    return Err(format!(
3510                        "Invalid command '{}' for date range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3511                        command
3512                    ));
3513                }
3514            },
3515            TypeSpecification::Time {
3516                minimum,
3517                maximum,
3518                help,
3519            } => match command {
3520                TypeConstraintCommand::Minimum => {
3521                    let t = require_time_literal(args, "minimum")?;
3522                    *minimum = Some(t);
3523                }
3524                TypeConstraintCommand::Maximum => {
3525                    let t = require_time_literal(args, "maximum")?;
3526                    *maximum = Some(t);
3527                }
3528                TypeConstraintCommand::Help => {
3529                    apply_type_help_command(help, args)?;
3530                }
3531                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3532                    let (target, cmd) = match command {
3533                        TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
3534                        TypeConstraintCommand::With => (&mut *declared_value, "with"),
3535                        _ => unreachable!("BUG: only Suggest or With in this arm"),
3536                    };
3537                    let lit = require_literal(args, cmd)?;
3538                    reject_calendar_for_suggestion(
3539                        lit,
3540                        type_name,
3541                        SuggestionExpectation::Time,
3542                        None,
3543                    )?;
3544                    match lit {
3545                        crate::literals::Value::Time(t) => {
3546                            *target =
3547                                Some(RawSuggestion::Value(ValueKind::Time(time_to_semantic(t))));
3548                        }
3549                        _ => {
3550                            return Err(
3551                                "Please provide a time, for example `-> suggest 09:00:00`."
3552                                    .to_string(),
3553                            );
3554                        }
3555                    }
3556                }
3557                _ => {
3558                    return Err(format!(
3559                        "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, suggest, with",
3560                        command
3561                    ));
3562                }
3563            },
3564            TypeSpecification::TimeRange {
3565                lower,
3566                upper,
3567                minimum,
3568                maximum,
3569                help,
3570            } => match command {
3571                TypeConstraintCommand::Lower => {
3572                    *lower = Some(require_time_literal(args, "lower")?);
3573                }
3574                TypeConstraintCommand::Upper => {
3575                    *upper = Some(require_time_literal(args, "upper")?);
3576                }
3577                TypeConstraintCommand::Minimum => {
3578                    *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
3579                }
3580                TypeConstraintCommand::Maximum => {
3581                    *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
3582                }
3583                TypeConstraintCommand::Help => {
3584                    apply_type_help_command(help, args)?;
3585                }
3586                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3587                    let target = match command {
3588                        TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3589                        TypeConstraintCommand::With => &mut *declared_value,
3590                        _ => unreachable!("BUG: only Suggest or With in this arm"),
3591                    };
3592                    let (left, right) = require_suggestion_range_endpoints(
3593                        args,
3594                        type_name,
3595                        SuggestionExpectation::TimeRange,
3596                        None,
3597                    )?;
3598                    let left = literal_value_from_parser_value(left)?;
3599                    let right = literal_value_from_parser_value(right)?;
3600                    if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
3601                        return Err(
3602                            "Please provide a time range, for example `-> suggest 09:00...17:00`."
3603                                .to_string(),
3604                        );
3605                    }
3606                    *target = Some(RawSuggestion::Value(ValueKind::Range(
3607                        Box::new(left),
3608                        Box::new(right),
3609                    )));
3610                }
3611                _ => {
3612                    return Err(format!(
3613                        "Invalid command '{}' for time range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3614                        command
3615                    ));
3616                }
3617            },
3618            TypeSpecification::MeasureRange {
3619                lower,
3620                upper,
3621                minimum,
3622                maximum,
3623                units,
3624                decomposition,
3625                help,
3626            } => match command {
3627                TypeConstraintCommand::Unit => {
3628                    return Err(format!(
3629                        "'unit' is not allowed on 'measure range' type '{type_name}'. Declare units on a measure type and write 'data x: <that type> range'."
3630                    ));
3631                }
3632                TypeConstraintCommand::Lower => {
3633                    *lower = Some(parse_measure_declared_bound(
3634                        args, "lower", units, type_name,
3635                    )?);
3636                }
3637                TypeConstraintCommand::Upper => {
3638                    *upper = Some(parse_measure_declared_bound(
3639                        args, "upper", units, type_name,
3640                    )?);
3641                }
3642                TypeConstraintCommand::Minimum => {
3643                    let width = parse_measure_declared_bound(args, "minimum", units, type_name)?;
3644                    reject_negative_width_magnitude(&width.0, "minimum")?;
3645                    *minimum = Some(width);
3646                }
3647                TypeConstraintCommand::Maximum => {
3648                    let width = parse_measure_declared_bound(args, "maximum", units, type_name)?;
3649                    reject_negative_width_magnitude(&width.0, "maximum")?;
3650                    *maximum = Some(width);
3651                }
3652                TypeConstraintCommand::Help => {
3653                    apply_type_help_command(help, args)?;
3654                }
3655                TypeConstraintCommand::Suggest | TypeConstraintCommand::With => {
3656                    let target = match command {
3657                        TypeConstraintCommand::Suggest => &mut *declared_suggestion,
3658                        TypeConstraintCommand::With => &mut *declared_value,
3659                        _ => unreachable!("BUG: only Suggest or With in this arm"),
3660                    };
3661                    let (left, right) = require_suggestion_range_endpoints(
3662                        args,
3663                        type_name,
3664                        SuggestionExpectation::MeasureRange,
3665                        Some(units),
3666                    )?;
3667                    let element_type = LemmaType::primitive(
3668                        TypeSpecification::MeasureRange {
3669                            lower: lower.clone(),
3670                            upper: upper.clone(),
3671                            minimum: minimum.clone(),
3672                            maximum: maximum.clone(),
3673                            units: units.clone(),
3674                            decomposition: decomposition.clone(),
3675                            help: help.clone(),
3676                        }
3677                        .element_from_range()
3678                        .expect("BUG: MeasureRange must define element_from_range"),
3679                    );
3680                    let left = lift_range_endpoint(left, &element_type, resolve_unit)?;
3681                    let right = lift_range_endpoint(right, &element_type, resolve_unit)?;
3682                    if !value_kind_matches_spec(&left.value, &element_type.specifications)
3683                        || !value_kind_matches_spec(&right.value, &element_type.specifications)
3684                        || !matches!(&left.value, ValueKind::Measure(_))
3685                        || !matches!(&right.value, ValueKind::Measure(_))
3686                    {
3687                        return Err(format!(
3688                            "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
3689                        ));
3690                    }
3691                    *target = Some(RawSuggestion::Value(ValueKind::Range(
3692                        Box::new(left),
3693                        Box::new(right),
3694                    )));
3695                }
3696                _ => {
3697                    return Err(format!(
3698                        "Invalid command '{}' for measure range type. Valid commands: lower, upper, minimum, maximum, help, suggest, with",
3699                        command
3700                    ));
3701                }
3702            },
3703            TypeSpecification::Veto { .. } => {
3704                return Err(format!(
3705                    "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
3706                    command
3707                ));
3708            }
3709            TypeSpecification::Undetermined => {
3710                return Err(format!(
3711                    "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
3712                    command
3713                ));
3714            }
3715            TypeSpecification::Spec { .. } => {
3716                return Err(format!(
3717                    "Invalid command '{command}' for spec type. A spec instance has no data constraints"
3718                ));
3719            }
3720            TypeSpecification::List { element, help, .. } => match command {
3721                TypeConstraintCommand::Help => {
3722                    if matches!(element.specifications, TypeSpecification::Spec { .. }) {
3723                        apply_type_help_command(help, args)?;
3724                    } else {
3725                        let mut unused_suggestion = None;
3726                        let mut unused_value = None;
3727                        Arc::make_mut(element).specifications.apply_constraint(
3728                            names,
3729                            command,
3730                            args,
3731                            &mut unused_suggestion,
3732                            &mut unused_value,
3733                            resolve_unit,
3734                        )?;
3735                    }
3736                }
3737                TypeConstraintCommand::With => {
3738                    return Err(
3739                        "BUG: list -> with is applied before TypeSpecification::apply_constraint"
3740                            .to_string(),
3741                    );
3742                }
3743                other => {
3744                    return Err(format!(
3745                        "Invalid command '{other}' for list type. Valid commands: with, help"
3746                    ));
3747                }
3748            },
3749        }
3750        Ok(())
3751    }
3752}
3753
3754/// Parse a "number unit" string into a Measure or Ratio value according to the type.
3755/// Caller must have obtained the TypeSpecification via unit_index from the unit in the string.
3756pub fn parse_number_unit(
3757    value_str: &str,
3758    type_spec: &TypeSpecification,
3759) -> Result<crate::parsing::ast::Value, String> {
3760    use crate::literals::{NumberWithUnit, RatioLiteral};
3761    use crate::parsing::ast::Value;
3762
3763    let trimmed = value_str.trim();
3764    match type_spec {
3765        TypeSpecification::Measure { units, .. } => {
3766            if units.is_empty() {
3767                unreachable!(
3768                    "BUG: Measure type has no units; should have been validated during planning"
3769                );
3770            }
3771            match trimmed.parse::<NumberWithUnit>() {
3772                Ok(n) => {
3773                    let unit = units.get(&n.1).map_err(|e| e.to_string())?;
3774                    Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
3775                }
3776                Err(e) => {
3777                    if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
3778                        let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
3779                        let example_unit = units
3780                            .iter()
3781                            .next()
3782                            .expect("BUG: units non-empty after guard")
3783                            .name
3784                            .as_str();
3785                        Err(format!(
3786                            "Measure value must include a unit, for example: '{} {}'. Valid units: {}.",
3787                            trimmed,
3788                            example_unit,
3789                            valid.join(", ")
3790                        ))
3791                    } else {
3792                        Err(e)
3793                    }
3794                }
3795            }
3796        }
3797        TypeSpecification::Ratio { units, .. } => {
3798            if units.is_empty() {
3799                unreachable!(
3800                    "BUG: Ratio type has no units; should have been validated during planning"
3801                );
3802            }
3803            match trimmed.parse::<RatioLiteral>()? {
3804                RatioLiteral::Bare(_) => {
3805                    Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
3806                }
3807                RatioLiteral::Percent(n) => {
3808                    let unit = units.get("percent").map_err(|e| e.to_string())?;
3809                    Ok(Value::NumberWithUnit(n, unit.name.clone()))
3810                }
3811                RatioLiteral::Permille(n) => {
3812                    let unit = units.get("permille").map_err(|e| e.to_string())?;
3813                    Ok(Value::NumberWithUnit(n, unit.name.clone()))
3814                }
3815                RatioLiteral::Named { value, unit } => {
3816                    let resolved = units.get(&unit).map_err(|e| e.to_string())?;
3817                    Ok(Value::NumberWithUnit(value, resolved.name.clone()))
3818                }
3819            }
3820        }
3821        _ => Err("parse_number_unit only accepts Measure or Ratio type".to_string()),
3822    }
3823}
3824
3825/// Parse a string value according to a TypeSpecification.
3826/// Used to parse runtime user input into typed values.
3827pub fn parse_value_from_string(
3828    value_str: &str,
3829    type_spec: &TypeSpecification,
3830    source: &Source,
3831) -> Result<crate::parsing::ast::Value, Error> {
3832    match type_spec {
3833        TypeSpecification::NumberRange { .. }
3834        | TypeSpecification::MeasureRange { .. }
3835        | TypeSpecification::DateRange { .. }
3836        | TypeSpecification::TimeRange { .. }
3837        | TypeSpecification::RatioRange { .. } => {
3838            let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
3839                unreachable!(
3840                    "BUG: range_element_type_specification missing arm for known range type"
3841                )
3842            });
3843            parse_range_value_from_string(value_str, &element_spec, source)
3844        }
3845        _ => parse_scalar_value_from_string(value_str, type_spec, source),
3846    }
3847}
3848
3849fn parse_range_value_from_string(
3850    value_str: &str,
3851    element_spec: &TypeSpecification,
3852    source: &Source,
3853) -> Result<crate::parsing::ast::Value, Error> {
3854    use crate::parsing::ast::Value;
3855
3856    let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
3857
3858    let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
3859        to_err("Range value must use '...' between the two endpoints".to_string())
3860    })?;
3861    if left_str.trim().is_empty() || right_str.trim().is_empty() {
3862        return Err(to_err(
3863            "Range value must contain a non-empty left and right endpoint".to_string(),
3864        ));
3865    }
3866    let left = parse_scalar_value_from_string(left_str.trim(), element_spec, source)?;
3867    let right = parse_scalar_value_from_string(right_str.trim(), element_spec, source)?;
3868    Ok(Value::Range(Box::new(left), Box::new(right)))
3869}
3870
3871fn parse_scalar_value_from_string(
3872    value_str: &str,
3873    type_spec: &TypeSpecification,
3874    source: &Source,
3875) -> Result<crate::parsing::ast::Value, Error> {
3876    use crate::parsing::ast::Value;
3877
3878    let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
3879
3880    match type_spec {
3881        TypeSpecification::Text { .. } => value_str
3882            .parse::<crate::literals::TextLiteral>()
3883            .map(|t| Value::Text(t.0))
3884            .map_err(to_err),
3885        TypeSpecification::Number { .. } => value_str
3886            .parse::<crate::literals::NumberLiteral>()
3887            .map(|n| Value::Number(n.0))
3888            .map_err(to_err),
3889        TypeSpecification::Measure { .. } => {
3890            parse_number_unit(value_str, type_spec).map_err(to_err)
3891        }
3892        TypeSpecification::Boolean { .. } => value_str
3893            .parse::<BooleanValue>()
3894            .map(Value::Boolean)
3895            .map_err(to_err),
3896        TypeSpecification::Date { .. } => {
3897            let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
3898            Ok(Value::Date(date))
3899        }
3900        TypeSpecification::Time { .. } => {
3901            let time = value_str.parse::<TimeValue>().map_err(to_err)?;
3902            Ok(Value::Time(time))
3903        }
3904        TypeSpecification::Ratio { .. } => {
3905            parse_number_unit(value_str, type_spec).map_err(to_err)
3906        }
3907        TypeSpecification::NumberRange { .. }
3908        | TypeSpecification::MeasureRange { .. }
3909        | TypeSpecification::DateRange { .. }
3910        | TypeSpecification::TimeRange { .. }
3911        | TypeSpecification::RatioRange { .. } => {
3912            unreachable!("BUG: range types are parsed by parse_range_value_from_string")
3913        }
3914        TypeSpecification::Veto { .. } => Err(to_err(
3915            "Veto type cannot be parsed from string".to_string(),
3916        )),
3917        TypeSpecification::Undetermined => unreachable!(
3918            "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
3919        ),
3920        TypeSpecification::Spec { .. } => Err(to_err(
3921            "spec values are not evaluation-data scalars".to_string(),
3922        )),
3923        TypeSpecification::List { .. } => Err(to_err(
3924            "list values are not evaluation-data scalars".to_string(),
3925        )),
3926    }
3927}
3928
3929// -----------------------------------------------------------------------------
3930// Semantic value types (no parser dependency - used by evaluation, inversion, etc.)
3931// -----------------------------------------------------------------------------
3932
3933#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3934pub enum SemanticCalendarUnit {
3935    Month,
3936    Year,
3937}
3938
3939impl fmt::Display for SemanticCalendarUnit {
3940    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3941        let s = match self {
3942            SemanticCalendarUnit::Month => "month",
3943            SemanticCalendarUnit::Year => "year",
3944        };
3945        write!(f, "{}", s)
3946    }
3947}
3948
3949pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
3950    match unit_name {
3951        "month" => SemanticCalendarUnit::Month,
3952        "year" => SemanticCalendarUnit::Year,
3953        other => unreachable!(
3954            "BUG: calendar measure signature unit must be month or year, got '{other}'"
3955        ),
3956    }
3957}
3958
3959pub fn semantic_calendar_unit_from_measure_signature(
3960    signature: &[(String, i32)],
3961) -> SemanticCalendarUnit {
3962    let unit_name = signature
3963        .first()
3964        .map(|(name, _)| name.as_str())
3965        .expect("BUG: calendar measure must carry a unit signature");
3966    semantic_calendar_unit_from_unit_name(unit_name)
3967}
3968
3969pub fn semantic_calendar_unit_from_measure_type(lemma_type: &LemmaType) -> SemanticCalendarUnit {
3970    if !lemma_type.is_calendar_like() {
3971        unreachable!(
3972            "BUG: semantic_calendar_unit_from_measure_type called on non-calendar type {}",
3973            lemma_type.name()
3974        );
3975    }
3976    let signature = lemma_type.measure_runtime_signature();
3977    if signature.is_empty() {
3978        return SemanticCalendarUnit::Month;
3979    }
3980    semantic_calendar_unit_from_measure_signature(&signature)
3981}
3982
3983/// Target type for `as` casts (semantic; used by evaluation/computation).
3984#[derive(Debug, Clone, PartialEq, Eq)]
3985pub enum SemanticConversionTarget {
3986    Type(ConversionTypeKind),
3987    /// `number as eur` — construct, convert, relabel, or range-span into `unit_name`.
3988    Unit {
3989        unit_name: String,
3990        /// Measure/ratio type resolved in the spec where the conversion was written.
3991        owning_type: Arc<LemmaType>,
3992    },
3993}
3994
3995impl std::hash::Hash for SemanticConversionTarget {
3996    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3997        match self {
3998            Self::Type(kind) => {
3999                0u8.hash(state);
4000                kind.hash(state);
4001            }
4002            Self::Unit {
4003                unit_name,
4004                owning_type,
4005            } => {
4006                1u8.hash(state);
4007                unit_name.hash(state);
4008                owning_type.hash(state);
4009            }
4010        }
4011    }
4012}
4013
4014impl SemanticConversionTarget {}
4015
4016impl fmt::Display for SemanticConversionTarget {
4017    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4018        match self {
4019            SemanticConversionTarget::Type(kind) => write!(f, "{kind}"),
4020            SemanticConversionTarget::Unit { unit_name, .. } => write!(f, "{unit_name}"),
4021        }
4022    }
4023}
4024
4025/// Timezone for semantic date/time values: UTC offset in signed whole minutes, same
4026/// representation and bounds as [`TimezoneValue`].
4027#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4028pub struct SemanticTimezone {
4029    pub offset_minutes: i16,
4030}
4031
4032impl SemanticTimezone {
4033    /// `None` when `seconds` is not a whole number of minutes within ±23:59.
4034    pub fn from_offset_seconds(seconds: i32) -> Option<Self> {
4035        TimezoneValue::from_offset_seconds(seconds).map(|tz| Self::from(&tz))
4036    }
4037
4038    pub fn offset_seconds(&self) -> i32 {
4039        i32::from(self.offset_minutes) * 60
4040    }
4041}
4042
4043impl From<&TimezoneValue> for SemanticTimezone {
4044    fn from(tz: &TimezoneValue) -> Self {
4045        Self {
4046            offset_minutes: tz.offset_minutes,
4047        }
4048    }
4049}
4050
4051impl From<&SemanticTimezone> for TimezoneValue {
4052    fn from(tz: &SemanticTimezone) -> Self {
4053        Self {
4054            offset_minutes: tz.offset_minutes,
4055        }
4056    }
4057}
4058
4059impl fmt::Display for SemanticTimezone {
4060    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4061        TimezoneValue::from(self).fmt(f)
4062    }
4063}
4064
4065impl Serialize for SemanticTimezone {
4066    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4067        serializer.serialize_str(&self.to_string())
4068    }
4069}
4070
4071impl<'de> Deserialize<'de> for SemanticTimezone {
4072    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4073        let s = String::deserialize(deserializer)?;
4074        Self::from_str(&s).map_err(serde::de::Error::custom)
4075    }
4076}
4077
4078impl FromStr for SemanticTimezone {
4079    type Err = String;
4080
4081    fn from_str(s: &str) -> Result<Self, Self::Err> {
4082        Ok(Self::from(&TimezoneValue::from_str(s)?))
4083    }
4084}
4085
4086/// Time-of-day for semantic values
4087#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4088pub struct SemanticTime {
4089    pub hour: u32,
4090    pub minute: u32,
4091    pub second: u32,
4092    pub microsecond: u32,
4093    pub timezone: Option<SemanticTimezone>,
4094}
4095
4096impl fmt::Display for SemanticTime {
4097    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4098        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
4099        if self.microsecond != 0 {
4100            write!(f, ".{:06}", self.microsecond)?;
4101        }
4102        if let Some(timezone) = &self.timezone {
4103            write!(f, "{}", timezone)?;
4104        }
4105        Ok(())
4106    }
4107}
4108
4109impl Serialize for SemanticTime {
4110    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4111        serializer.serialize_str(&self.to_string())
4112    }
4113}
4114
4115impl<'de> Deserialize<'de> for SemanticTime {
4116    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4117        let s = String::deserialize(deserializer)?;
4118        Self::from_str(&s).map_err(serde::de::Error::custom)
4119    }
4120}
4121
4122impl FromStr for SemanticTime {
4123    type Err = String;
4124
4125    fn from_str(s: &str) -> Result<Self, Self::Err> {
4126        Ok(time_to_semantic(&TimeValue::from_str(s)?))
4127    }
4128}
4129
4130/// Date-time for semantic values
4131#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4132pub struct SemanticDateTime {
4133    pub year: i32,
4134    pub month: u32,
4135    pub day: u32,
4136    pub hour: u32,
4137    pub minute: u32,
4138    pub second: u32,
4139    pub microsecond: u32,
4140    pub timezone: Option<SemanticTimezone>,
4141}
4142
4143impl fmt::Display for SemanticDateTime {
4144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4145        let has_time = self.hour != 0
4146            || self.minute != 0
4147            || self.second != 0
4148            || self.microsecond != 0
4149            || self.timezone.is_some();
4150        if !has_time {
4151            write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
4152        } else {
4153            write!(
4154                f,
4155                "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
4156                self.year, self.month, self.day, self.hour, self.minute, self.second
4157            )?;
4158            if self.microsecond != 0 {
4159                write!(f, ".{:06}", self.microsecond)?;
4160            }
4161            if let Some(tz) = &self.timezone {
4162                write!(f, "{}", tz)?;
4163            }
4164            Ok(())
4165        }
4166    }
4167}
4168
4169impl Serialize for SemanticDateTime {
4170    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4171        serializer.serialize_str(&self.to_string())
4172    }
4173}
4174
4175impl<'de> Deserialize<'de> for SemanticDateTime {
4176    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4177        let s = String::deserialize(deserializer)?;
4178        Self::from_str(&s).map_err(serde::de::Error::custom)
4179    }
4180}
4181
4182impl FromStr for SemanticDateTime {
4183    type Err = String;
4184
4185    fn from_str(s: &str) -> Result<Self, Self::Err> {
4186        Ok(date_time_to_semantic(&DateTimeValue::from_str(s)?))
4187    }
4188}
4189
4190/// Default captured during type constraint application, before measure unit factors are final.
4191/// Converted into [`ValueKind`] after `resolve_measure_decompositions` (or immediately for
4192/// reference-local defaults, which run after that pass).
4193#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4194pub enum RawSuggestion {
4195    Value(ValueKind),
4196    /// Measure suggest/fill: magnitude in the written unit (canonicalized later).
4197    Measure {
4198        magnitude: RationalInteger,
4199        unit_name: String,
4200    },
4201    /// Ratio (or other) suggest/fill that already holds a canonical [`ValueKind`]
4202    /// plus the written unit name for display binding.
4203    UnitBound {
4204        value: ValueKind,
4205        unit_name: String,
4206    },
4207    List(Vec<RawSuggestion>),
4208}
4209
4210/// Canonical suggestion/fill value plus the written measure unit when one was declared.
4211///
4212/// `unit` is the unit from `-> suggest 100 inr` / `-> with 5 eur`; Show
4213/// display stamps it so the one-liner matches the written unit, not the type's first unit.
4214/// Shared via [`Arc`] so value-table clones bump a refcount instead of allocating.
4215/// For ranges this stays `None`; each endpoint carries its own unit on its type.
4216///
4217/// `instance_factors` are the settled dynamic unit factors of the instance this value
4218/// was read from (`uses` alias or list row). Same-family arithmetic keeps only factors
4219/// both sides agree on; a family change drops them.
4220#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4221pub struct BoundValueKind {
4222    pub value: ValueKind,
4223    pub unit: Option<Arc<str>>,
4224    pub instance_factors: Option<Arc<std::collections::BTreeMap<String, RationalInteger>>>,
4225}
4226
4227impl BoundValueKind {
4228    pub fn unbound(value: ValueKind) -> Self {
4229        Self {
4230            value,
4231            unit: None,
4232            instance_factors: None,
4233        }
4234    }
4235
4236    pub fn with_binding(value: ValueKind, unit: Option<Arc<str>>) -> Self {
4237        Self {
4238            value,
4239            unit,
4240            instance_factors: None,
4241        }
4242    }
4243
4244    pub fn with_instance_factors(
4245        value: ValueKind,
4246        unit: Option<Arc<str>>,
4247        instance_factors: Option<Arc<std::collections::BTreeMap<String, RationalInteger>>>,
4248    ) -> Self {
4249        Self {
4250            value,
4251            unit,
4252            instance_factors,
4253        }
4254    }
4255
4256    pub fn to_literal(&self) -> LiteralValue {
4257        LiteralValue {
4258            value: self.value.clone(),
4259        }
4260    }
4261
4262    /// Written-unit agreement for ± on same-family measure/ratio: equal keep; one Some keep that;
4263    /// conflict → left wins.
4264    #[must_use]
4265    pub fn agree_or_left_binding(
4266        left: Option<&Arc<str>>,
4267        right: Option<&Arc<str>>,
4268    ) -> Option<Arc<str>> {
4269        match (left, right) {
4270            (Some(a), Some(b)) if a.as_ref() == b.as_ref() => Some(Arc::clone(a)),
4271            (Some(a), Some(_)) => Some(Arc::clone(a)),
4272            (Some(a), None) => Some(Arc::clone(a)),
4273            (None, Some(b)) => Some(Arc::clone(b)),
4274            (None, None) => None,
4275        }
4276    }
4277
4278    /// Agreed instance factors for same-family arithmetic: units both sides carry with
4279    /// equal factors stay; either side missing a map means no carried instance agreement
4280    /// (local / plan-scoped values rely on runtime scales, not a one-sided carry).
4281    #[must_use]
4282    pub fn agree_instance_factors(
4283        left: Option<&Arc<std::collections::BTreeMap<String, RationalInteger>>>,
4284        right: Option<&Arc<std::collections::BTreeMap<String, RationalInteger>>>,
4285    ) -> Option<Arc<std::collections::BTreeMap<String, RationalInteger>>> {
4286        match (left, right) {
4287            (None, None) | (Some(_), None) | (None, Some(_)) => None,
4288            (Some(left), Some(right)) => {
4289                let mut agreed = std::collections::BTreeMap::new();
4290                for (unit, factor) in left.iter() {
4291                    if right.get(unit) == Some(factor) {
4292                        agreed.insert(unit.clone(), factor.clone());
4293                    }
4294                }
4295                if agreed.is_empty() {
4296                    None
4297                } else {
4298                    Some(Arc::new(agreed))
4299                }
4300            }
4301        }
4302    }
4303
4304    /// Keep a written unit only when agreed instance factors still know its factor.
4305    /// When there is no instance map, leave the binding for plan-scoped runtime scales.
4306    #[must_use]
4307    pub fn binding_with_agreed_factors(
4308        unit: Option<Arc<str>>,
4309        factors: &Option<Arc<std::collections::BTreeMap<String, RationalInteger>>>,
4310    ) -> Option<Arc<str>> {
4311        match (unit, factors) {
4312            (Some(unit), Some(factors)) if factors.contains_key(unit.as_ref()) => Some(unit),
4313            (Some(_), Some(_)) => None,
4314            (unit, None) => unit,
4315            (None, _) => None,
4316        }
4317    }
4318}
4319
4320pub fn bound_value_kind_from_raw_suggestion(
4321    raw: RawSuggestion,
4322    specifications: &TypeSpecification,
4323    type_name: &str,
4324) -> Result<BoundValueKind, String> {
4325    enum Step<'a> {
4326        Visit(RawSuggestion, &'a TypeSpecification),
4327        CollectList {
4328            element: &'a Arc<LemmaType>,
4329            rows: usize,
4330        },
4331    }
4332
4333    let mut steps = vec![Step::Visit(raw, specifications)];
4334    let mut bounds: Vec<BoundValueKind> = Vec::new();
4335    while let Some(step) = steps.pop() {
4336        match step {
4337            Step::Visit(raw, specifications) => match raw {
4338                RawSuggestion::Value(vk) => bounds.push(BoundValueKind::unbound(vk)),
4339                RawSuggestion::UnitBound { value, unit_name } => bounds.push(BoundValueKind {
4340                    value,
4341                    unit: Some(Arc::from(unit_name)),
4342                    instance_factors: None,
4343                }),
4344                RawSuggestion::Measure {
4345                    magnitude,
4346                    unit_name,
4347                } => {
4348                    let TypeSpecification::Measure { units, .. } = specifications else {
4349                        unreachable!(
4350                            "BUG: RawSuggestion::Measure for non-measure type '{type_name}'"
4351                        );
4352                    };
4353                    if units
4354                        .get(&unit_name)
4355                        .is_ok_and(crate::literals::MeasureUnit::is_dynamic)
4356                    {
4357                        bounds.push(BoundValueKind {
4358                            value: ValueKind::UnscaledMeasure {
4359                                magnitude,
4360                                unit: Arc::from(unit_name.as_str()),
4361                            },
4362                            unit: Some(Arc::from(unit_name)),
4363                            instance_factors: None,
4364                        });
4365                    } else {
4366                        let canonical = measure_declared_bound_to_canonical(
4367                            &magnitude, &unit_name, units, type_name, "suggest",
4368                        )?;
4369                        bounds.push(BoundValueKind {
4370                            value: ValueKind::Measure(canonical),
4371                            unit: Some(Arc::from(unit_name)),
4372                            instance_factors: None,
4373                        });
4374                    }
4375                }
4376                RawSuggestion::List(rows) => {
4377                    let TypeSpecification::List { element, .. } = specifications else {
4378                        unreachable!("BUG: RawSuggestion::List for non-list type '{type_name}'");
4379                    };
4380                    steps.push(Step::CollectList {
4381                        element,
4382                        rows: rows.len(),
4383                    });
4384                    for row in rows.into_iter().rev() {
4385                        steps.push(Step::Visit(row, &element.specifications));
4386                    }
4387                }
4388            },
4389            Step::CollectList { element, rows } => {
4390                let start = bounds
4391                    .len()
4392                    .checked_sub(rows)
4393                    .expect("BUG: list rows missing from bound stack");
4394                let converted: Arc<[TypedLiteral]> = bounds
4395                    .split_off(start)
4396                    .iter()
4397                    .map(|bound| typed_literal_from_bound(bound, element))
4398                    .collect();
4399                bounds.push(BoundValueKind::unbound(ValueKind::List(converted.into())));
4400            }
4401        }
4402    }
4403    Ok(bounds.pop().expect("BUG: bound walk left no result"))
4404}
4405
4406/// Display one-liner unit: `as` / schema > settled > fill > suggest > none (first declared).
4407///
4408/// Range schemas are skipped: endpoint units live on each endpoint type.
4409#[must_use]
4410pub fn display_binding_unit(
4411    schema: &LemmaType,
4412    settled: Option<&BoundValueKind>,
4413    fill: Option<&BoundValueKind>,
4414    suggestion: Option<&BoundValueKind>,
4415) -> Option<String> {
4416    if schema.is_measure_range() || schema.is_ratio_range() {
4417        return None;
4418    }
4419    if let Some(unit) = schema.unit() {
4420        return Some(unit.to_string());
4421    }
4422    if let Some(unit) = settled.and_then(|bound| bound.unit.as_ref()) {
4423        return Some(unit.to_string());
4424    }
4425    if let Some(unit) = fill.and_then(|bound| bound.unit.as_ref()) {
4426        return Some(unit.to_string());
4427    }
4428    suggestion
4429        .and_then(|bound| bound.unit.as_ref())
4430        .map(|unit| unit.to_string())
4431}
4432
4433/// Schema type stamped with [`display_binding_unit`] when a binding wins.
4434///
4435/// Clones only when the winning unit differs from what `schema` already carries.
4436/// Range types are returned unchanged.
4437#[must_use]
4438pub fn lemma_type_with_display_binding(
4439    schema: &LemmaType,
4440    settled: Option<&BoundValueKind>,
4441    fill: Option<&BoundValueKind>,
4442    suggestion: Option<&BoundValueKind>,
4443) -> LemmaType {
4444    if schema.is_measure_range() || schema.is_ratio_range() {
4445        return schema.clone();
4446    }
4447    match display_binding_unit(schema, settled, fill, suggestion) {
4448        Some(unit) if schema.unit() != Some(unit.as_str()) => schema.clone().with_unit(unit),
4449        Some(_) | None => schema.clone(),
4450    }
4451}
4452
4453/// Like [`lemma_type_with_display_binding`] but reuses `schema` when no override.
4454#[must_use]
4455pub fn lemma_type_arc_with_display_binding(
4456    schema: &Arc<LemmaType>,
4457    settled: Option<&BoundValueKind>,
4458    fill: Option<&BoundValueKind>,
4459    suggestion: Option<&BoundValueKind>,
4460) -> Arc<LemmaType> {
4461    if schema.is_measure_range() || schema.is_ratio_range() {
4462        return Arc::clone(schema);
4463    }
4464    match display_binding_unit(schema.as_ref(), settled, fill, suggestion) {
4465        Some(unit) if schema.unit() != Some(unit.as_str()) => {
4466            Arc::new(schema.as_ref().clone().with_unit(unit))
4467        }
4468        Some(_) | None => Arc::clone(schema),
4469    }
4470}
4471
4472/// Build a [`TypedLiteral`] from an eval bound and the plan type for that cell.
4473///
4474/// Stamps `bound.unit` onto measure/ratio types. Range values already carry
4475/// typed endpoints inside [`ValueKind::Range`].
4476#[must_use]
4477pub fn typed_literal_from_bound(
4478    bound: &BoundValueKind,
4479    lemma_type: &Arc<LemmaType>,
4480) -> TypedLiteral {
4481    let lemma_type = match (&bound.unit, &lemma_type.specifications) {
4482        (Some(unit), TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. })
4483            if lemma_type.unit() != Some(unit.as_ref()) =>
4484        {
4485            Arc::new(lemma_type.as_ref().clone().with_unit(unit.as_ref()))
4486        }
4487        _ => Arc::clone(lemma_type),
4488    };
4489    TypedLiteral {
4490        value: bound.value.clone(),
4491        lemma_type,
4492    }
4493}
4494
4495/// Value payload (shape of a literal).
4496///
4497/// Scalar variants are type-free: type lives on `NormalForm.result_type` /
4498/// `DataDefinition`. The [`ValueKind::Range`] arm is the exception — each
4499/// endpoint keeps a [`TypedLiteral`] so a mixed-unit range (`10 eur...20 usd`)
4500/// retains both written units.
4501#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4502pub enum ValueKind {
4503    Number(RationalInteger),
4504    /// Measure: magnitude in canonical (base-unit) space.
4505    ///
4506    /// At bind time the user-facing value is multiplied by the unit's scale to produce
4507    /// the stored magnitude; API unit maps divide back. Unit/signature identity
4508    /// comes from the node/`DataDefinition` type via [`LemmaType::measure_runtime_signature`].
4509    Measure(RationalInteger),
4510    /// Measure written in a reference-scaled unit. Lives only in stored values
4511    /// (literal leaves, data slots, definitions, bindings). A cell result never
4512    /// holds one: the owning leaf settles it to [`ValueKind::Measure`] first.
4513    UnscaledMeasure {
4514        magnitude: RationalInteger,
4515        unit: Arc<str>,
4516    },
4517    Text(Arc<str>),
4518    Date(SemanticDateTime),
4519    Time(SemanticTime),
4520    Boolean(bool),
4521    /// Ratio: canonical magnitude. Display unit comes from the node/`DataDefinition` type.
4522    Ratio(RationalInteger),
4523    /// Range endpoints carry their own type (and written unit for measure/ratio).
4524    Range(Box<TypedLiteral>, Box<TypedLiteral>),
4525    /// A spec instance: a `uses` path in the consumer plan, or one row of an instance list.
4526    Spec(SpecValue),
4527    /// Ordered rows of one type. Each row keeps its own type (written unit for measures).
4528    List(ListValue),
4529}
4530
4531/// Items of a list value, with the key index of a keyed list that carries one.
4532/// Equality, hashing and serialization are over the items only.
4533#[derive(Debug, Clone)]
4534pub struct ListValue {
4535    items: ListItems,
4536    index: Option<Arc<crate::planning::instance_list::KeyIndex>>,
4537}
4538
4539/// The rows of a store as one list reads them.
4540pub(crate) struct StoredRows<'l> {
4541    pub rows: &'l [Arc<RowInstance>],
4542    pub owner: Option<&'l InstanceKey>,
4543}
4544
4545#[derive(Debug, Clone)]
4546enum ListItems {
4547    Values(Arc<[TypedLiteral]>),
4548    /// The rows of a store as one list reads them: each item is the row owned by
4549    /// `owner`, of type `row_type`.
4550    Rows {
4551        store: Arc<crate::planning::instance_list::RowStore>,
4552        owner: Option<InstanceKey>,
4553        row_type: Arc<LemmaType>,
4554    },
4555}
4556
4557impl ListValue {
4558    /// A list holding `items`, without an index.
4559    fn held(items: Arc<[TypedLiteral]>) -> Self {
4560        Self {
4561            items: ListItems::Values(items),
4562            index: None,
4563        }
4564    }
4565
4566    pub(crate) fn indexed(
4567        items: Arc<[TypedLiteral]>,
4568        index: Arc<crate::planning::instance_list::KeyIndex>,
4569    ) -> Self {
4570        Self {
4571            items: ListItems::Values(items),
4572            index: Some(index),
4573        }
4574    }
4575
4576    /// The rows of `store` read by `owner`, indexed by the store's index.
4577    pub(crate) fn stored_rows(
4578        store: &Arc<crate::planning::instance_list::RowStore>,
4579        owner: Option<InstanceKey>,
4580        row_type: Arc<LemmaType>,
4581    ) -> Self {
4582        Self {
4583            index: store.index.clone(),
4584            items: ListItems::Rows {
4585                store: Arc::clone(store),
4586                owner,
4587                row_type,
4588            },
4589        }
4590    }
4591
4592    pub fn len(&self) -> usize {
4593        match &self.items {
4594            ListItems::Values(items) => items.len(),
4595            ListItems::Rows { store, .. } => store.rows.len(),
4596        }
4597    }
4598
4599    pub fn is_empty(&self) -> bool {
4600        self.len() == 0
4601    }
4602
4603    pub fn get(&self, position: usize) -> Option<std::borrow::Cow<'_, TypedLiteral>> {
4604        match &self.items {
4605            ListItems::Values(items) => items.get(position).map(std::borrow::Cow::Borrowed),
4606            ListItems::Rows {
4607                store,
4608                owner,
4609                row_type,
4610            } => store.rows.get(position).map(|row| {
4611                std::borrow::Cow::Owned(TypedLiteral {
4612                    value: ValueKind::Spec(SpecValue::Row(InstanceKey::new(
4613                        Arc::clone(row),
4614                        owner.clone(),
4615                    ))),
4616                    lemma_type: Arc::clone(row_type),
4617                })
4618            }),
4619        }
4620    }
4621
4622    pub fn first(&self) -> Option<std::borrow::Cow<'_, TypedLiteral>> {
4623        self.get(0)
4624    }
4625
4626    pub fn last(&self) -> Option<std::borrow::Cow<'_, TypedLiteral>> {
4627        self.len().checked_sub(1).and_then(|last| self.get(last))
4628    }
4629
4630    pub fn iter(
4631        &self,
4632    ) -> impl DoubleEndedIterator<Item = std::borrow::Cow<'_, TypedLiteral>> + ExactSizeIterator
4633    {
4634        (0..self.len()).map(|position| {
4635            self.get(position)
4636                .expect("BUG: list position below the list length")
4637        })
4638    }
4639
4640    /// The items of a list that holds them: every list but one reading a row store.
4641    pub(crate) fn values(&self) -> &[TypedLiteral] {
4642        match &self.items {
4643            ListItems::Values(items) => items,
4644            ListItems::Rows { .. } => panic!("BUG: held items read from a list of stored rows"),
4645        }
4646    }
4647
4648    /// The rows of the store this list reads, when it reads one.
4649    pub(crate) fn stored(&self) -> Option<StoredRows<'_>> {
4650        match &self.items {
4651            ListItems::Values(_) => None,
4652            ListItems::Rows { store, owner, .. } => Some(StoredRows {
4653                rows: &store.rows,
4654                owner: owner.as_ref(),
4655            }),
4656        }
4657    }
4658
4659    /// The items as owned values.
4660    pub fn to_vec(&self) -> Vec<TypedLiteral> {
4661        self.iter().map(std::borrow::Cow::into_owned).collect()
4662    }
4663
4664    pub(crate) fn index(&self) -> Option<&Arc<crate::planning::instance_list::KeyIndex>> {
4665        self.index.as_ref()
4666    }
4667
4668    /// The same items without an index.
4669    pub(crate) fn unindexed(&self) -> Self {
4670        Self {
4671            items: self.items.clone(),
4672            index: None,
4673        }
4674    }
4675
4676    /// The same items with `index`.
4677    pub(crate) fn with_index(&self, index: Arc<crate::planning::instance_list::KeyIndex>) -> Self {
4678        Self {
4679            items: self.items.clone(),
4680            index: Some(index),
4681        }
4682    }
4683}
4684
4685impl From<Arc<[TypedLiteral]>> for ListValue {
4686    fn from(items: Arc<[TypedLiteral]>) -> Self {
4687        Self::held(items)
4688    }
4689}
4690
4691impl From<Vec<TypedLiteral>> for ListValue {
4692    fn from(items: Vec<TypedLiteral>) -> Self {
4693        Self::held(Arc::from(items))
4694    }
4695}
4696
4697impl FromIterator<TypedLiteral> for ListValue {
4698    fn from_iter<I: IntoIterator<Item = TypedLiteral>>(iter: I) -> Self {
4699        Self::held(iter.into_iter().collect())
4700    }
4701}
4702
4703impl PartialEq for ListValue {
4704    fn eq(&self, other: &Self) -> bool {
4705        self.len() == other.len() && self.iter().eq(other.iter())
4706    }
4707}
4708
4709impl Eq for ListValue {}
4710
4711impl Hash for ListValue {
4712    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
4713        self.len().hash(state);
4714        for item in self.iter() {
4715            item.hash(state);
4716        }
4717    }
4718}
4719
4720impl ValueKind {
4721    /// Decimal magnitude for numeric variants (number, measure, ratio).
4722    pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
4723        match self {
4724            ValueKind::Number(n) | ValueKind::Measure(n) | ValueKind::Ratio(n) => {
4725                n.try_to_decimal().map_err(|failure| failure.to_string())
4726            }
4727            other => Err(format!("expected numeric value kind, got {other}")),
4728        }
4729    }
4730
4731    /// Cheap structural byte-size estimate (no formatting, no rational→decimal).
4732    ///
4733    /// Used by resource-limit checks where the exact string length is not
4734    /// required — only an upper-bound within a constant factor.
4735    pub fn structural_byte_size(&self) -> usize {
4736        fn rational_byte_estimate(r: &RationalInteger) -> usize {
4737            let numer_bytes = (r.numer_magnitude_bits() as usize).div_ceil(8);
4738            let denom_bytes = (r.denom_magnitude_bits() as usize).div_ceil(8);
4739            numer_bytes.max(1) + denom_bytes.max(1)
4740        }
4741        match self {
4742            ValueKind::Number(r) => rational_byte_estimate(r),
4743            ValueKind::Measure(r) => rational_byte_estimate(r),
4744            ValueKind::UnscaledMeasure { magnitude, unit } => {
4745                rational_byte_estimate(magnitude) + unit.len()
4746            }
4747            ValueKind::Ratio(r) => rational_byte_estimate(r),
4748            ValueKind::Text(s) => s.len(),
4749            ValueKind::Date(_) => 30, // "2026-01-01T00:00:00+02:00" upper bound
4750            ValueKind::Time(_) => 12, // "23:59:59" upper bound
4751            ValueKind::Boolean(_) => 5, // "false"
4752            ValueKind::Range(left, right) => {
4753                left.value.structural_byte_size() + 3 + right.value.structural_byte_size()
4754            }
4755            ValueKind::Spec(SpecValue::Fixed(instance)) => instance.structural_byte_size(),
4756            ValueKind::Spec(SpecValue::Row(InstanceKey { row, .. })) => row.structural_byte_size(),
4757            ValueKind::List(rows) => rows
4758                .iter()
4759                .map(|row| row.value.structural_byte_size())
4760                .sum(),
4761        }
4762    }
4763
4764    /// Whether this stored value still contains a reference-scaled measure.
4765    #[must_use]
4766    pub fn has_unscaled_measure(&self) -> bool {
4767        match self {
4768            ValueKind::UnscaledMeasure { .. } => true,
4769            ValueKind::Range(left, right) => match (&left.value, &right.value) {
4770                (ValueKind::UnscaledMeasure { .. }, _) | (_, ValueKind::UnscaledMeasure { .. }) => {
4771                    true
4772                }
4773                (ValueKind::Range(_, _) | ValueKind::List(_), _)
4774                | (_, ValueKind::Range(_, _) | ValueKind::List(_)) => unscaled_in_value_tree(self),
4775                _ => false,
4776            },
4777            ValueKind::List(_) => unscaled_in_value_tree(self),
4778            _ => false,
4779        }
4780    }
4781}
4782
4783fn unscaled_in_value_tree(root: &ValueKind) -> bool {
4784    let mut pending = vec![root];
4785    while let Some(value) = pending.pop() {
4786        match value {
4787            ValueKind::UnscaledMeasure { .. } => return true,
4788            ValueKind::Range(left, right) => {
4789                pending.push(&right.value);
4790                pending.push(&left.value);
4791            }
4792            ValueKind::List(rows) => {
4793                if rows.stored().is_none() {
4794                    for item in rows.values().iter() {
4795                        pending.push(&item.value);
4796                    }
4797                }
4798            }
4799            _ => {}
4800        }
4801    }
4802    false
4803}
4804
4805fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
4806    rational.display_str()
4807}
4808
4809fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
4810    use crate::parsing::ast::Value;
4811    match rational.try_to_decimal() {
4812        Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
4813        Err(_) => format!("{} {}", rational.display_str(), unit),
4814    }
4815}
4816
4817impl fmt::Display for ValueKind {
4818    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4819        match self {
4820            ValueKind::Number(rational) => {
4821                write!(f, "{}", format_rational_magnitude_for_display(rational))
4822            }
4823            ValueKind::Measure(rational) => {
4824                write!(f, "{}", format_rational_magnitude_for_display(rational))
4825            }
4826            ValueKind::UnscaledMeasure { magnitude, unit } => {
4827                write!(
4828                    f,
4829                    "{} {unit}",
4830                    format_rational_magnitude_for_display(magnitude)
4831                )
4832            }
4833            ValueKind::Text(s) => {
4834                write!(f, "{}", crate::parsing::ast::Value::Text(s.to_string()))
4835            }
4836            ValueKind::Ratio(rational) => {
4837                write!(f, "{}", format_rational_magnitude_for_display(rational))
4838            }
4839            ValueKind::Date(dt) => write!(f, "{}", dt),
4840            ValueKind::Time(t) => write!(
4841                f,
4842                "{}",
4843                crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
4844                    hour: t.hour as u8,
4845                    minute: t.minute as u8,
4846                    second: t.second as u8,
4847                    microsecond: t.microsecond,
4848                    timezone: t
4849                        .timezone
4850                        .as_ref()
4851                        .map(crate::parsing::ast::TimezoneValue::from),
4852                })
4853            ),
4854            ValueKind::Boolean(b) => write!(f, "{}", b),
4855            ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
4856            ValueKind::Spec(value) => write!(f, "{value}"),
4857            ValueKind::List(rows) => {
4858                write!(f, "list[")?;
4859                for (i, row) in rows.iter().enumerate() {
4860                    if i > 0 {
4861                        write!(f, ", ")?;
4862                    }
4863                    write!(f, "{row}")?;
4864                }
4865                write!(f, "]")
4866            }
4867        }
4868    }
4869}
4870
4871// -----------------------------------------------------------------------------
4872// Resolved path types (moved from parsing::ast)
4873// -----------------------------------------------------------------------------
4874
4875/// A single segment in a resolved path traversal
4876///
4877/// Used in both DataPath and RulePath for cross-spec traversal. Each segment is
4878/// one spec instance of the consumer plan: a `uses` import (`uses`, `row: None`)
4879/// or row `row` of the instance list `uses` written in the spec (`row: Some(n)`,
4880/// 1-based). `repository` is the resolved target repository name (`None` = unnamed
4881/// workspace) and `spec` the resolved target spec name.
4882#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4883pub struct PathSegment {
4884    /// The `uses` alias, or the instance list name when `row` is set.
4885    pub uses: Arc<str>,
4886    /// Row number (1-based) of list `uses` when this hop is a row written in the spec.
4887    pub row: Option<usize>,
4888    /// Target repository name from `Arc<LemmaRepository>.name` at resolve.
4889    /// `None` is the unnamed workspace.
4890    pub repository: Option<Arc<str>>,
4891    /// The spec this hop resolves to (resolved during planning)
4892    pub spec: Arc<str>,
4893}
4894
4895impl PathSegment {
4896    /// `uses` import hop of `identity` named `alias`.
4897    pub(crate) fn import(alias: &str, identity: &SpecIdentity) -> Self {
4898        Self {
4899            uses: Arc::from(alias),
4900            row: None,
4901            repository: identity.repository.as_deref().map(Arc::from),
4902            spec: Arc::from(identity.spec.as_str()),
4903        }
4904    }
4905
4906    /// Hop of row `number` (1-based) of instance list whose name and identity strings
4907    /// are already shared as `Arc<str>`: every row of one list reuses the same three
4908    /// allocations, so building a row's hop is refcount bumps, not fresh allocations.
4909    pub(crate) fn list_row_shared(
4910        list: &Arc<str>,
4911        number: usize,
4912        spec: &Arc<str>,
4913        repository: Option<&Arc<str>>,
4914    ) -> Self {
4915        Self {
4916            uses: Arc::clone(list),
4917            row: Some(number),
4918            repository: repository.map(Arc::clone),
4919            spec: Arc::clone(spec),
4920        }
4921    }
4922
4923    /// Dotted input-key form of this hop: `ada`, or `staff.2` for a row.
4924    pub fn input_key(&self) -> String {
4925        match self.row {
4926            Some(number) => format!("{}.{number}", self.uses),
4927            None => self.uses.to_string(),
4928        }
4929    }
4930}
4931
4932/// The hops that name a spec instance, relative to the plan holding it: `uses` hops
4933/// (`rack.low`), or a list hop with its row number (`staff.2`).
4934#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4935pub struct SpecInstance {
4936    pub prefix: Vec<PathSegment>,
4937}
4938
4939impl SpecInstance {
4940    pub fn structural_byte_size(&self) -> usize {
4941        self.prefix
4942            .iter()
4943            .map(|segment| {
4944                segment.uses.len()
4945                    + segment.spec.len()
4946                    + segment.repository.as_deref().map_or(0, str::len)
4947                    + segment.row.map_or(0, |_| std::mem::size_of::<usize>())
4948            })
4949            .sum()
4950    }
4951}
4952
4953impl fmt::Display for SpecInstance {
4954    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4955        for (index, segment) in self.prefix.iter().enumerate() {
4956            if index > 0 {
4957                write!(f, ".")?;
4958            }
4959            write!(f, "{}", segment.input_key())?;
4960        }
4961        Ok(())
4962    }
4963}
4964
4965/// Value of a spec-typed expression.
4966///
4967/// `Row` is a row instance as read (see [`InstanceKey`]); a `uses` import is the row of
4968/// its alias, read by the instance that declares it. Evaluation produces only `Row`.
4969/// `Fixed` is a spec instance named by its hops, rebuilt from a [`SpecResult`] by
4970/// [`RuleResultValue::to_literal`].
4971///
4972/// [`SpecResult`]: crate::result_value::SpecResult
4973/// [`RuleResultValue::to_literal`]: crate::result_value::RuleResultValue::to_literal
4974#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4975pub enum SpecValue {
4976    Fixed(SpecInstance),
4977    Row(InstanceKey),
4978}
4979
4980impl fmt::Display for SpecValue {
4981    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4982        match self {
4983            SpecValue::Fixed(instance) => write!(f, "{instance}"),
4984            SpecValue::Row(key) => write!(f, "{key}"),
4985        }
4986    }
4987}
4988
4989/// One row as read in an evaluation request, and the identity of its context: the row
4990/// `Arc` and the owning instance whose context read it (`None` for the root scope).
4991/// The bindings of the row resolve in the owner's context.
4992#[derive(Debug, Clone)]
4993pub struct InstanceKey {
4994    pub row: Arc<RowInstance>,
4995    pub owner: Option<Box<InstanceKey>>,
4996}
4997
4998impl InstanceKey {
4999    pub fn new(row: Arc<RowInstance>, owner: Option<InstanceKey>) -> Self {
5000        Self {
5001            row,
5002            owner: owner.map(Box::new),
5003        }
5004    }
5005
5006    pub fn owner(&self) -> Option<&InstanceKey> {
5007        self.owner.as_deref()
5008    }
5009
5010    /// The context a binding written `hops` instances above this row resolves in:
5011    /// the owner for `0`, the owner's owner for `1`, and so on.
5012    pub(crate) fn scope(&self, hops: u32) -> Option<&InstanceKey> {
5013        let mut scope = self.owner();
5014        for _ in 0..hops {
5015            scope = scope
5016                .expect("BUG: a binding hops above the root scope")
5017                .owner();
5018        }
5019        scope
5020    }
5021
5022    /// This key read inside `owner`: its outermost row, owned by the root scope here,
5023    /// is owned by `owner` instead.
5024    pub(crate) fn under(&self, owner: &InstanceKey) -> InstanceKey {
5025        let mut rows: Vec<&Arc<RowInstance>> = Vec::new();
5026        let mut key = Some(self);
5027        while let Some(current) = key {
5028            rows.push(&current.row);
5029            key = current.owner();
5030        }
5031        let mut rebased = owner.clone();
5032        for row in rows.into_iter().rev() {
5033            rebased = InstanceKey::new(Arc::clone(row), Some(rebased));
5034        }
5035        rebased
5036    }
5037
5038    /// Origins naming this instance, innermost first, up to the first that is absolute.
5039    fn naming_origins(&self) -> Vec<&RowOrigin> {
5040        let mut origins = Vec::new();
5041        let mut key = Some(self);
5042        while let Some(current) = key {
5043            origins.push(&current.row.origin);
5044            if matches!(current.row.origin, RowOrigin::Numbered { .. }) {
5045                break;
5046            }
5047            key = current.owner();
5048        }
5049        origins
5050    }
5051
5052    /// Data path of this instance in the run data that wrote its nearest run-data row
5053    /// (`i`, `i.staff.2`, `items.2.j`; `lines.1.i` for a row of a run-data row's list);
5054    /// `None` when it is or sits under an inline row, which has no path.
5055    pub fn path(&self) -> Option<String> {
5056        let origins = self.naming_origins();
5057        if origins.iter().any(|origin| origin.is_inline()) {
5058            return None;
5059        }
5060        Some(
5061            origins
5062                .iter()
5063                .rev()
5064                .map(ToString::to_string)
5065                .collect::<Vec<_>>()
5066                .join("."),
5067        )
5068    }
5069
5070    /// This instance named from `row`, one of its owners (`holder.items.1`); `None` when
5071    /// `row` does not hold it.
5072    pub(crate) fn label_under(&self, row: &InstanceKey) -> Option<String> {
5073        let mut below: Vec<&RowOrigin> = Vec::new();
5074        let mut key = self;
5075        while key != row {
5076            below.push(&key.row.origin);
5077            key = key.owner()?;
5078        }
5079        Some(
5080            below
5081                .iter()
5082                .rev()
5083                .map(ToString::to_string)
5084                .collect::<Vec<_>>()
5085                .join("."),
5086        )
5087    }
5088
5089    /// The rows-table row this instance publishes under: itself or the nearest owner that
5090    /// is a run-data or appended row. With the label of this instance: its path, or under
5091    /// an appended row its path from that row (`item`).
5092    pub(crate) fn table_row(&self) -> (&InstanceKey, Option<String>) {
5093        let mut key = self;
5094        loop {
5095            match &key.row.origin {
5096                RowOrigin::Numbered { .. } => return (key, self.path()),
5097                RowOrigin::Inline { .. } if key == self => return (key, None),
5098                RowOrigin::Inline { .. } => return (key, self.label_under(key)),
5099                RowOrigin::Import { .. } | RowOrigin::Written { .. } => {
5100                    key = key
5101                        .owner()
5102                        .expect("BUG: an instance off the rows table published as a table row");
5103                }
5104            }
5105        }
5106    }
5107
5108    /// Evaluation-data key of field `key` of this instance from the request's plan;
5109    /// `None` when it has no path.
5110    pub(crate) fn field_key(&self, key: &str) -> Option<String> {
5111        self.request_path().map(|path| format!("{path}.{key}"))
5112    }
5113
5114    /// [`Self::path`] from the request's plan: a run-data row of a run-data row's list
5115    /// is named under that row (`shipments.1.lines.1.i`).
5116    fn request_path(&self) -> Option<String> {
5117        let origins = self.naming_origins();
5118        if origins.iter().any(|origin| origin.is_inline()) {
5119            return None;
5120        }
5121        Some(
5122            origins
5123                .iter()
5124                .rev()
5125                .map(|origin| match origin {
5126                    RowOrigin::Numbered {
5127                        under: Some(under), ..
5128                    } => format!("{under}.{origin}"),
5129                    origin => origin.to_string(),
5130                })
5131                .collect::<Vec<_>>()
5132                .join("."),
5133        )
5134    }
5135
5136    /// Hops naming this instance from the root plan, when every hop is a `uses` import or a
5137    /// row written in a spec.
5138    pub(crate) fn segments(&self) -> Option<Vec<PathSegment>> {
5139        let mut parts: Vec<Vec<PathSegment>> = Vec::new();
5140        let mut key = Some(self);
5141        while let Some(current) = key {
5142            match &current.row.origin {
5143                RowOrigin::Import { alias } => {
5144                    parts.push(vec![PathSegment::import(alias, &current.row.identity)])
5145                }
5146                RowOrigin::Written { instance, .. } => parts.push(instance.prefix.clone()),
5147                RowOrigin::Numbered { .. } | RowOrigin::Inline { .. } => return None,
5148            }
5149            key = current.owner();
5150        }
5151        Some(parts.into_iter().rev().flatten().collect())
5152    }
5153
5154    /// Hops of the `uses` imports this instance is reached through from the nearest row
5155    /// (or the root) above it, outermost first: empty for a row itself.
5156    pub(crate) fn import_hops(&self) -> Vec<PathSegment> {
5157        let mut hops = Vec::new();
5158        let mut key = Some(self);
5159        while let Some(current) = key {
5160            let RowOrigin::Import { alias } = &current.row.origin else {
5161                break;
5162            };
5163            hops.push(PathSegment::import(alias, &current.row.identity));
5164            key = current.owner();
5165        }
5166        hops.reverse();
5167        hops
5168    }
5169
5170    /// Whether this instance and every owner above it is a `uses` import or a row written
5171    /// in a spec.
5172    pub(crate) fn is_planned(&self) -> bool {
5173        let mut key = Some(self);
5174        while let Some(current) = key {
5175            if !matches!(
5176                current.row.origin,
5177                RowOrigin::Import { .. } | RowOrigin::Written { .. }
5178            ) {
5179                return false;
5180            }
5181            key = current.owner();
5182        }
5183        true
5184    }
5185}
5186
5187impl fmt::Display for InstanceKey {
5188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5189        for (index, origin) in self.naming_origins().iter().rev().enumerate() {
5190            if index > 0 {
5191                write!(f, ".")?;
5192            }
5193            write!(f, "{origin}")?;
5194        }
5195        Ok(())
5196    }
5197}
5198
5199impl PartialEq for InstanceKey {
5200    fn eq(&self, other: &Self) -> bool {
5201        Arc::ptr_eq(&self.row, &other.row) && self.owner == other.owner
5202    }
5203}
5204
5205impl Eq for InstanceKey {}
5206
5207impl std::hash::Hash for InstanceKey {
5208    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
5209        (Arc::as_ptr(&self.row) as usize).hash(state);
5210        self.owner.hash(state);
5211    }
5212}
5213
5214/// A field of a [`RowInstance`].
5215///
5216/// `hops` on a reference or rows binding names the context it was written in: `0` is the
5217/// instance that holds the row (the row key's owner), `1` the one above it, and so on.
5218/// A consumer writing `-> with j.x: v` on `uses i` writes `x` of `i`'s nested `j` row
5219/// one hop above the instance (`i`) that holds `j`.
5220#[derive(Debug, Clone)]
5221pub enum RowBinding {
5222    Bound(OperationResult),
5223    /// Sealed spec-written reference. Not yet forced: resolution happens when a read of
5224    /// the row touches the field, so lists holding rule-bound rows can be read
5225    /// (`-> count`) without evaluating rules the read never reaches.
5226    Deferred {
5227        target: ReferenceTarget,
5228        hops: u32,
5229    },
5230    /// Rows of a list field written with the row (`-> with ada` copying a `uses`
5231    /// instance whose list field is bound). Read as a list whose rows are owned by
5232    /// the context `hops` names.
5233    Rows {
5234        rows: Vec<Arc<RowInstance>>,
5235        row_type: Arc<LemmaType>,
5236        hops: u32,
5237    },
5238    /// A nested `uses` slot whose fields a consumer wrote (`-> with j.x: v`): the nested
5239    /// import row with those fields bound. Read as that row owned by the holding instance.
5240    Instance(Arc<RowInstance>),
5241}
5242
5243impl RowBinding {
5244    /// Whether this binding is `other` copied: the same value, target, or rows.
5245    pub(crate) fn is_copy_of(&self, other: &RowBinding) -> bool {
5246        match (self, other) {
5247            (RowBinding::Bound(a), RowBinding::Bound(b)) => a == b,
5248            (
5249                RowBinding::Deferred { target, hops },
5250                RowBinding::Deferred {
5251                    target: other_target,
5252                    hops: other_hops,
5253                },
5254            ) => target == other_target && hops == other_hops,
5255            (
5256                RowBinding::Rows { rows, hops, .. },
5257                RowBinding::Rows {
5258                    rows: other_rows,
5259                    hops: other_hops,
5260                    ..
5261                },
5262            ) => {
5263                hops == other_hops
5264                    && rows.len() == other_rows.len()
5265                    && rows
5266                        .iter()
5267                        .zip(other_rows)
5268                        .all(|(row, other)| Arc::ptr_eq(row, other))
5269            }
5270            (RowBinding::Instance(row), RowBinding::Instance(other)) => Arc::ptr_eq(row, other),
5271            _ => false,
5272        }
5273    }
5274
5275    /// Value of a [`Self::Rows`] binding whose rows are owned by `owner`.
5276    pub(crate) fn rows_value(
5277        rows: &[Arc<RowInstance>],
5278        row_type: &Arc<LemmaType>,
5279        owner: Option<&InstanceKey>,
5280    ) -> OperationResult {
5281        OperationResult::from_literal(LiteralValue {
5282            value: ValueKind::List(
5283                rows.iter()
5284                    .map(|row| TypedLiteral {
5285                        value: ValueKind::Spec(SpecValue::Row(InstanceKey::new(
5286                            Arc::clone(row),
5287                            owner.cloned(),
5288                        ))),
5289                        lemma_type: Arc::clone(row_type),
5290                    })
5291                    .collect(),
5292            ),
5293        })
5294    }
5295}
5296
5297/// A row instance: its spec, where it came from, and its bound fields.
5298///
5299/// `bindings` are positional over the row spec's template plan `data` slots (`index = template.data.get_index_of`).
5300/// A slot that is `None` reads the template's own default or is missing.
5301#[derive(Debug, Clone)]
5302pub struct RowInstance {
5303    pub identity: Arc<SpecIdentity>,
5304    pub origin: RowOrigin,
5305    pub bindings: Box<[Option<RowBinding>]>,
5306}
5307
5308impl RowInstance {
5309    pub fn get_binding(
5310        &self,
5311        template: &crate::planning::execution_plan::ExecutionPlan,
5312        path: &DataPath,
5313    ) -> Option<&RowBinding> {
5314        let index = template.data.get_index_of(path)?;
5315        self.bindings.get(index)?.as_ref()
5316    }
5317
5318    /// Upper-bound byte size of the row's bound fields; see [`ValueKind::structural_byte_size`].
5319    pub fn structural_byte_size(&self) -> usize {
5320        let mut size = 0;
5321        let mut pending: Vec<&RowInstance> = vec![self];
5322        while let Some(row) = pending.pop() {
5323            for binding in row.bindings.iter().flatten() {
5324                match binding {
5325                    RowBinding::Bound(OperationResult::Value(bound)) => {
5326                        size += bound.value.structural_byte_size();
5327                    }
5328                    RowBinding::Rows { rows, .. } => pending.extend(rows.iter().map(Arc::as_ref)),
5329                    RowBinding::Instance(row) => pending.push(row),
5330                    RowBinding::Bound(OperationResult::Veto(_)) | RowBinding::Deferred { .. } => {}
5331                }
5332            }
5333        }
5334        size
5335    }
5336}
5337
5338/// Where a row instance came from. Names missing fields and explanations.
5339#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5340pub enum RowOrigin {
5341    /// Row `number` (1-based) of list `list`: a run-data row. `under` is the
5342    /// evaluation-data key of the run-data row whose list it is (`items.1`); `None` for
5343    /// a list of the request's plan.
5344    Numbered {
5345        list: String,
5346        number: usize,
5347        under: Option<Arc<str>>,
5348    },
5349    /// A row written in the spec at `source`, read through the row spec's template
5350    /// plan. The instance names it relative to the plan holding its list (`staff.2`).
5351    Written {
5352        instance: SpecInstance,
5353        source: Source,
5354    },
5355    /// The row of `uses alias`, named relative to the plan declaring it.
5356    Import { alias: Arc<str> },
5357    /// Row written inline by the `-> with field: value` command at cell `node`, as row
5358    /// `position` (1-based) of its result. Its written fields are values of the plan
5359    /// that holds the command and are read as that plan planned them; the row spec's
5360    /// rules run over them in the row's context. Every field a read names is set:
5361    /// planning rejects reads of unset fields.
5362    Inline { node: NormalFormId, position: usize },
5363}
5364
5365/// 1-based numbered origin of a list row (`items.2`).
5366#[must_use]
5367pub fn numbered_row_origin(list_key: &str, number: usize) -> String {
5368    format!("{list_key}.{number}")
5369}
5370
5371/// Input key of a field of a numbered list row (`items.2.weight`).
5372#[must_use]
5373pub fn row_field_input_key(list_key: &str, number: usize, field_key: &str) -> String {
5374    format!("{}.{field_key}", numbered_row_origin(list_key, number))
5375}
5376
5377impl RowOrigin {
5378    /// Row number (1-based) of a list row, and whether it was appended inline.
5379    pub(crate) fn list_position(&self) -> (usize, bool) {
5380        match self {
5381            RowOrigin::Numbered { number, .. } => (*number, false),
5382            RowOrigin::Written { instance, .. } => (
5383                instance
5384                    .prefix
5385                    .last()
5386                    .and_then(|last| last.row)
5387                    .expect("BUG: written row origin without a row hop"),
5388                false,
5389            ),
5390            RowOrigin::Inline { position, .. } => (*position, true),
5391            RowOrigin::Import { alias } => {
5392                unreachable!("BUG: the row of `uses {alias}` is not a list row")
5393            }
5394        }
5395    }
5396
5397    pub(crate) fn is_inline(&self) -> bool {
5398        matches!(self, RowOrigin::Inline { .. })
5399    }
5400}
5401
5402impl fmt::Display for RowOrigin {
5403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5404        match self {
5405            RowOrigin::Numbered { list, number, .. } => {
5406                write!(f, "{}", numbered_row_origin(list, *number))
5407            }
5408            RowOrigin::Written { instance, .. } => write!(f, "{instance}"),
5409            RowOrigin::Import { alias } => write!(f, "{alias}"),
5410            RowOrigin::Inline { position, .. } => write!(f, "appended row {position}"),
5411        }
5412    }
5413}
5414
5415/// One evaluated instance-list row as a typed literal.
5416#[must_use]
5417pub fn row_literal(
5418    identity: Arc<SpecIdentity>,
5419    origin: RowOrigin,
5420    bindings: Vec<Option<RowBinding>>,
5421    lemma_type: Arc<LemmaType>,
5422) -> TypedLiteral {
5423    TypedLiteral {
5424        value: ValueKind::Spec(SpecValue::Row(InstanceKey::new(
5425            Arc::new(RowInstance {
5426                identity,
5427                origin,
5428                bindings: bindings.into(),
5429            }),
5430            None,
5431        ))),
5432        lemma_type,
5433    }
5434}
5435
5436/// Field of a row read through the row spec's template plan.
5437#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5438pub enum RowEnd {
5439    Data(DataPath),
5440    Rule(RulePath),
5441}
5442
5443impl fmt::Display for RowEnd {
5444    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5445        match self {
5446            RowEnd::Data(path) => write!(f, "{}", path.input_key()),
5447            RowEnd::Rule(path) => write!(f, "{}", path.input_key()),
5448        }
5449    }
5450}
5451
5452/// Resolved path to a data (created during planning from AST DataReference)
5453///
5454/// Represents a fully resolved path through specs to reach a datum.
5455#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
5456pub struct DataPath {
5457    /// Path segments (each is a cross-spec step)
5458    pub segments: Vec<PathSegment>,
5459    /// Final data name
5460    pub data: String,
5461}
5462
5463impl DataPath {
5464    /// Create a data path from segments and data name (matches AST DataReference shape)
5465    pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
5466        Self { segments, data }
5467    }
5468
5469    /// Create a local data path (no cross-spec steps)
5470    pub fn local(data: String) -> Self {
5471        Self {
5472            segments: vec![],
5473            data,
5474        }
5475    }
5476
5477    /// Dot-separated key used for matching user-provided data values (e.g. `"order.payment_method"`,
5478    /// `"staff.2.salary"`). Same string as [`Display`]: hops only, no repository or spec names.
5479    pub fn input_key(&self) -> String {
5480        let mut s = String::new();
5481        for segment in &self.segments {
5482            s.push_str(&segment.input_key());
5483            s.push('.');
5484        }
5485        s.push_str(&self.data);
5486        s
5487    }
5488}
5489
5490/// Resolved path to a rule (created during planning from a rule reference).
5491///
5492/// Represents a fully resolved path through specs to reach a rule.
5493#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
5494pub struct RulePath {
5495    /// Path segments (each is a cross-spec step)
5496    pub segments: Vec<PathSegment>,
5497    /// Final rule name
5498    pub rule: String,
5499}
5500
5501impl RulePath {
5502    /// Create a rule path from segments and rule name.
5503    pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
5504        Self { segments, rule }
5505    }
5506
5507    /// Dot-separated key for Show/run (`alias.rule`), twin of [`DataPath::input_key`].
5508    /// Same string as [`Display`]: alias hops only, no repository or spec names.
5509    pub fn input_key(&self) -> String {
5510        if self.segments.is_empty() {
5511            return self.rule.clone();
5512        }
5513        let mut s = String::new();
5514        for segment in &self.segments {
5515            s.push_str(&segment.input_key());
5516            s.push('.');
5517        }
5518        s.push_str(&self.rule);
5519        s
5520    }
5521}
5522
5523// -----------------------------------------------------------------------------
5524// Resolved expression types (created during planning)
5525// -----------------------------------------------------------------------------
5526
5527/// Resolved expression (all references resolved to paths, all literals typed)
5528///
5529/// Created during planning from AST Expression. All unresolved references
5530/// are converted to DataPath/RulePath, and all literals are typed.
5531/// Resolved expression tree (all references resolved to paths, all literals typed).
5532///
5533/// Nodes live in a flat table and reference children by [`ExprId`]. Created during
5534/// planning from the AST; all unresolved references are converted to DataPath/RulePath.
5535pub type Expression = ExprTree<ExpressionKind>;
5536
5537/// Resolved expression kind (only resolved variants, no unresolved references)
5538#[derive(Debug, Clone, PartialEq)]
5539pub enum ExpressionKind {
5540    /// Resolved literal with type (boxed to keep enum small)
5541    Literal(Box<TypedLiteral>),
5542    /// Resolved data path
5543    DataPath(DataPath),
5544    /// Resolved rule path
5545    RulePath(RulePath),
5546    Logical {
5547        connective: LogicalConnective,
5548        first: ExprId,
5549        steps: Vec<ChainStep<()>>,
5550    },
5551    /// Left fold of `first` over `steps`; every step's operator has one precedence level.
5552    Arithmetic {
5553        first: ExprId,
5554        steps: Vec<ChainStep<ArithmeticComputation>>,
5555    },
5556    Comparison(ExprId, ComparisonComputation, ExprId),
5557    UnitConversion(ExprId, SemanticConversionTarget),
5558    LogicalNegation(ExprId, NegationType),
5559    MathematicalComputation(MathematicalComputation, ExprId),
5560    /// `operand -> round` or `operand -> round step`.
5561    StepRounding {
5562        operand: ExprId,
5563        mode: RoundingMode,
5564        step: Option<ExprId>,
5565    },
5566    Veto(VetoExpression),
5567    /// The `now` keyword — resolved at evaluation to the effective datetime.
5568    Now,
5569    /// Date-relative sugar: `<date_expr> in past` / `in future`
5570    DateRelative(DateRelativeKind, ExprId),
5571    /// Calendar-period sugar: `<date_expr> in [past|future] calendar year|month|week`
5572    DateCalendar(DateCalendarKind, CalendarPeriodUnit, ExprId),
5573    RangeLiteral(ExprId, ExprId),
5574    PastFutureRange(DateRelativeKind, ExprId),
5575    RangeContainment(ExprId, ExprId),
5576    /// Ordered endpoint of a range (`lower` included, `upper` excluded).
5577    RangeBound(RangeBound, ExprId),
5578    /// Whether evaluating the operand produced a veto (no value). Parses as `is veto` syntax.
5579    ResultIsVeto(ExprId),
5580    /// Field of a spec value: `top_bracket.tax`, `top_bracket.finance.fee`, or
5581    /// `(staff -> first).bonus`.
5582    ///
5583    /// Every instance the base yields is a row of its spec, so the field is read through
5584    /// that spec's template plan. A `uses` alias base is its import slot.
5585    SpecMember {
5586        base: ExprId,
5587        path: Vec<String>,
5588        name: String,
5589    },
5590    /// Field of the current row of the list command at `level` (0 = outermost).
5591    ///
5592    /// `end` is the field on the row spec's own plan: every row reads it through its
5593    /// row template.
5594    RowField {
5595        level: u32,
5596        identity: SpecIdentity,
5597        end: RowEnd,
5598        /// Written list qualifier (`children` in `children.is_leaf`), when the
5599        /// source used a qualified row-field reference. `None` for bare unambiguous refs.
5600        qualifier: Option<Vec<String>>,
5601    },
5602    /// Current value row. `list` is a type-only reference to the earlier list expression.
5603    RowValue {
5604        level: u32,
5605        list: ExprId,
5606    },
5607    /// Unless structure: (condition, result) pairs in source order; last true condition wins.
5608    /// First arm is the default (condition is always-true literal).
5609    Piecewise(Vec<(ExprId, ExprId)>),
5610    /// `row_level` is set when arguments read the current list row at that
5611    /// level, through [`Self::RowValue`] or [`Self::RowField`].
5612    ListCommand {
5613        list: ExprId,
5614        op: ListOp,
5615        row_level: Option<u32>,
5616    },
5617}
5618
5619/// Planned postfix list command.
5620#[derive(Debug, Clone, PartialEq)]
5621pub enum ListOp {
5622    Find {
5623        keys: Vec<ExprId>,
5624    },
5625    First,
5626    Last,
5627    Count,
5628    Reduce {
5629        kind: ListReduce,
5630        argument: Option<ExprId>,
5631    },
5632    Predicate {
5633        kind: ListPredKind,
5634        form: crate::parsing::ast::ListPredicateForm,
5635        expr: ExprId,
5636    },
5637    Sort {
5638        descending: bool,
5639        argument: Option<ExprId>,
5640    },
5641    Top {
5642        n: ExprId,
5643    },
5644    With {
5645        rows: Vec<ExprId>,
5646    },
5647    /// Append one row of spec `identity`; each field is a row-spec data path and its value.
5648    WithRow {
5649        identity: SpecIdentity,
5650        fields: Vec<(DataPath, ExprId)>,
5651    },
5652    /// Declare field key(s) on a Spec-element list; lives on the list type, eval no-op.
5653    Key {
5654        fields: Vec<DataPath>,
5655    },
5656}
5657
5658#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5659pub enum ListReduce {
5660    Sum,
5661    Avg,
5662    Product,
5663    Min,
5664    Max,
5665}
5666
5667impl ListReduce {
5668    pub fn keyword(self) -> &'static str {
5669        match self {
5670            ListReduce::Sum => "sum",
5671            ListReduce::Avg => "avg",
5672            ListReduce::Product => "product",
5673            ListReduce::Min => "min",
5674            ListReduce::Max => "max",
5675        }
5676    }
5677}
5678
5679#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5680pub enum ListPredKind {
5681    Any,
5682    All,
5683    Where,
5684}
5685
5686impl ListPredKind {
5687    pub fn keyword(self) -> &'static str {
5688        match self {
5689            ListPredKind::Any => "any",
5690            ListPredKind::All => "all",
5691            ListPredKind::Where => "where",
5692        }
5693    }
5694}
5695
5696impl ExprKind for ExpressionKind {
5697    fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
5698        match self {
5699            ExpressionKind::Literal(_)
5700            | ExpressionKind::DataPath(_)
5701            | ExpressionKind::RulePath(_)
5702            | ExpressionKind::Veto(_)
5703            | ExpressionKind::Now
5704            | ExpressionKind::RowField { .. }
5705            | ExpressionKind::RowValue { .. } => {}
5706            ExpressionKind::Logical { first, steps, .. } => {
5707                f(*first);
5708                steps.iter().for_each(|step| f(step.operand));
5709            }
5710            ExpressionKind::Arithmetic { first, steps } => {
5711                f(*first);
5712                steps.iter().for_each(|step| f(step.operand));
5713            }
5714            ExpressionKind::Comparison(left, _, right)
5715            | ExpressionKind::RangeLiteral(left, right)
5716            | ExpressionKind::RangeContainment(left, right) => {
5717                f(*left);
5718                f(*right);
5719            }
5720            ExpressionKind::StepRounding { operand, step, .. } => {
5721                f(*operand);
5722                if let Some(step) = step {
5723                    f(*step);
5724                }
5725            }
5726            ExpressionKind::UnitConversion(inner, _)
5727            | ExpressionKind::LogicalNegation(inner, _)
5728            | ExpressionKind::MathematicalComputation(_, inner)
5729            | ExpressionKind::DateRelative(_, inner)
5730            | ExpressionKind::DateCalendar(_, _, inner)
5731            | ExpressionKind::PastFutureRange(_, inner)
5732            | ExpressionKind::RangeBound(_, inner)
5733            | ExpressionKind::ResultIsVeto(inner)
5734            | ExpressionKind::SpecMember { base: inner, .. } => f(*inner),
5735            ExpressionKind::Piecewise(arms) => arms.iter().for_each(|(condition, result)| {
5736                f(*condition);
5737                f(*result);
5738            }),
5739            ExpressionKind::ListCommand { list, op, .. } => {
5740                f(*list);
5741                op.for_each_child(f);
5742            }
5743        }
5744    }
5745
5746    fn children_mut(&mut self) -> Vec<&mut ExprId> {
5747        match self {
5748            ExpressionKind::Literal(_)
5749            | ExpressionKind::DataPath(_)
5750            | ExpressionKind::RulePath(_)
5751            | ExpressionKind::Veto(_)
5752            | ExpressionKind::Now
5753            | ExpressionKind::RowField { .. }
5754            | ExpressionKind::RowValue { .. } => Vec::new(),
5755            ExpressionKind::Logical { first, steps, .. } => std::iter::once(first)
5756                .chain(steps.iter_mut().map(|step| &mut step.operand))
5757                .collect(),
5758            ExpressionKind::Arithmetic { first, steps } => std::iter::once(first)
5759                .chain(steps.iter_mut().map(|step| &mut step.operand))
5760                .collect(),
5761            ExpressionKind::Comparison(left, _, right)
5762            | ExpressionKind::RangeLiteral(left, right)
5763            | ExpressionKind::RangeContainment(left, right) => vec![left, right],
5764            ExpressionKind::StepRounding { operand, step, .. } => {
5765                let mut children = vec![operand];
5766                if let Some(step) = step {
5767                    children.push(step);
5768                }
5769                children
5770            }
5771            ExpressionKind::UnitConversion(inner, _)
5772            | ExpressionKind::LogicalNegation(inner, _)
5773            | ExpressionKind::MathematicalComputation(_, inner)
5774            | ExpressionKind::DateRelative(_, inner)
5775            | ExpressionKind::DateCalendar(_, _, inner)
5776            | ExpressionKind::PastFutureRange(_, inner)
5777            | ExpressionKind::RangeBound(_, inner)
5778            | ExpressionKind::ResultIsVeto(inner) => vec![inner],
5779            ExpressionKind::SpecMember { base, .. } => vec![base],
5780            ExpressionKind::Piecewise(arms) => arms
5781                .iter_mut()
5782                .flat_map(|(condition, result)| [condition, result])
5783                .collect(),
5784            ExpressionKind::ListCommand { list, op, .. } => {
5785                let mut children = vec![list];
5786                children.extend(op.children_mut());
5787                children
5788            }
5789        }
5790    }
5791}
5792
5793impl ListOp {
5794    fn for_each_child(&self, mut f: impl FnMut(ExprId)) {
5795        match self {
5796            ListOp::First | ListOp::Last | ListOp::Count | ListOp::Key { .. } => {}
5797            ListOp::Find { keys } => keys.iter().copied().for_each(f),
5798            ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
5799                argument.iter().copied().for_each(f)
5800            }
5801            ListOp::Predicate { expr, .. } => f(*expr),
5802            ListOp::Top { n } => f(*n),
5803            ListOp::With { rows } => rows.iter().copied().for_each(f),
5804            ListOp::WithRow { fields, .. } => fields.iter().for_each(|(_, value)| f(*value)),
5805        }
5806    }
5807
5808    fn children_mut(&mut self) -> Vec<&mut ExprId> {
5809        match self {
5810            ListOp::First | ListOp::Last | ListOp::Count | ListOp::Key { .. } => Vec::new(),
5811            ListOp::Find { keys } => keys.iter_mut().collect(),
5812            ListOp::Reduce { argument, .. } | ListOp::Sort { argument, .. } => {
5813                argument.iter_mut().collect()
5814            }
5815            ListOp::Predicate { expr, .. } => vec![expr],
5816            ListOp::Top { n } => vec![n],
5817            ListOp::With { rows } => rows.iter_mut().collect(),
5818            ListOp::WithRow { fields, .. } => fields.iter_mut().map(|(_, value)| value).collect(),
5819        }
5820    }
5821}
5822
5823// -----------------------------------------------------------------------------
5824// Resolved types and values
5825// -----------------------------------------------------------------------------
5826
5827/// Where the custom extension chain is rooted: same spec as this type, or imported from another resolved spec.
5828#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5829pub enum TypeDefiningSpec {
5830    /// Parent type is defined in the same spec as this type.
5831    Local,
5832    /// Parent type was resolved from types loaded from another spec.
5833    Import,
5834}
5835
5836/// Root of a custom type's extension chain: the spec version that declares the root type,
5837/// and its name. Types share a unit family only when they share this root declaration;
5838/// a local `money` and an imported `pricing.money` are different families. Two versions
5839/// of `pricing` declare different `pricing <date>.money` families; values from different
5840/// versions do not mix. Interface drift across unpinned versions compares structure with
5841/// [`LemmaType::same_interface`], which ignores the family's effective date.
5842#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
5843pub struct TypeFamily {
5844    pub repository: Option<String>,
5845    pub spec: String,
5846    pub effective: crate::parsing::ast::EffectiveDate,
5847    pub name: String,
5848}
5849
5850impl TypeFamily {
5851    #[must_use]
5852    pub(crate) fn declared_in(spec: &SpecVersionKey, name: &str) -> Self {
5853        Self {
5854            repository: spec.repository.clone(),
5855            spec: spec.spec.clone(),
5856            effective: spec.effective.clone(),
5857            name: name.to_string(),
5858        }
5859    }
5860
5861    /// Whether `spec` (this exact version) declares this family's root.
5862    #[must_use]
5863    pub(crate) fn is_declared_in(&self, spec: &SpecVersionKey) -> bool {
5864        self.repository == spec.repository
5865            && self.spec == spec.spec
5866            && self.effective == spec.effective
5867    }
5868
5869    /// Family identity with the version erased (interface comparisons only).
5870    #[must_use]
5871    pub fn without_effective(&self) -> Self {
5872        Self {
5873            repository: self.repository.clone(),
5874            spec: self.spec.clone(),
5875            effective: crate::parsing::ast::EffectiveDate::Origin,
5876            name: self.name.clone(),
5877        }
5878    }
5879}
5880
5881/// `[<repo> ]<spec>[ <date>].<type>`: the root type named with its declaring spec version.
5882impl fmt::Display for TypeFamily {
5883    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5884        if let Some(repository) = &self.repository {
5885            write!(f, "{repository} ")?;
5886        }
5887        write!(f, "{}", self.spec)?;
5888        if let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.effective {
5889            write!(f, " {at}")?;
5890        }
5891        write!(f, ".{}", self.name)
5892    }
5893}
5894
5895/// What this type extends (primitive built-in or custom type by name).
5896#[derive(Clone, Debug)]
5897pub enum TypeExtends {
5898    /// Extends a primitive built-in type (number, boolean, text, etc.)
5899    Primitive,
5900    /// Extends a custom type: parent is the immediate parent type name; family is the root of the extension chain.
5901    /// `defining_spec` records whether the parent chain is local or imported from another spec.
5902    Custom {
5903        parent: String,
5904        family: TypeFamily,
5905        defining_spec: TypeDefiningSpec,
5906    },
5907}
5908
5909impl PartialEq for TypeExtends {
5910    fn eq(&self, other: &Self) -> bool {
5911        match (self, other) {
5912            (TypeExtends::Primitive, TypeExtends::Primitive) => true,
5913            (
5914                TypeExtends::Custom {
5915                    parent: lp,
5916                    family: lf,
5917                    defining_spec: ld,
5918                },
5919                TypeExtends::Custom {
5920                    parent: rp,
5921                    family: rf,
5922                    defining_spec: rd,
5923                },
5924            ) => lp == rp && lf == rf && ld == rd,
5925            _ => false,
5926        }
5927    }
5928}
5929
5930impl Eq for TypeExtends {}
5931
5932impl std::hash::Hash for TypeExtends {
5933    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
5934        match self {
5935            TypeExtends::Primitive => {
5936                0u8.hash(state);
5937            }
5938            TypeExtends::Custom {
5939                parent,
5940                family,
5941                defining_spec,
5942            } => {
5943                1u8.hash(state);
5944                parent.hash(state);
5945                family.hash(state);
5946                defining_spec.hash(state);
5947            }
5948        }
5949    }
5950}
5951
5952impl TypeExtends {
5953    /// Custom extension in the same spec as the defining type (no cross-spec import for the parent chain).
5954    #[must_use]
5955    pub fn custom_local(parent: String, family: TypeFamily) -> Self {
5956        TypeExtends::Custom {
5957            parent,
5958            family,
5959            defining_spec: TypeDefiningSpec::Local,
5960        }
5961    }
5962
5963    /// Returns the parent type name if this type extends a custom type.
5964    #[must_use]
5965    pub fn parent_name(&self) -> Option<&str> {
5966        match self {
5967            TypeExtends::Primitive => None,
5968            TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
5969        }
5970    }
5971
5972    #[must_use]
5973    fn with_families_without_effective(&self) -> Self {
5974        match self {
5975            TypeExtends::Primitive => TypeExtends::Primitive,
5976            TypeExtends::Custom {
5977                parent,
5978                family,
5979                defining_spec,
5980            } => TypeExtends::Custom {
5981                parent: parent.clone(),
5982                family: family.without_effective(),
5983                defining_spec: defining_spec.clone(),
5984            },
5985        }
5986    }
5987}
5988
5989/// Resolved type after planning
5990///
5991/// Contains a type specification and optional name. Created during planning
5992/// from TypeSpecification in the AST.
5993#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5994pub struct LemmaType {
5995    /// Optional type name (e.g., "age", "temperature")
5996    pub name: Option<String>,
5997    /// The type specification (Boolean, Number, Measure, etc.).
5998    pub specifications: TypeSpecification,
5999    /// What this type extends (primitive or custom from a spec)
6000    pub extends: TypeExtends,
6001    pub(crate) options: TypeOptions,
6002}
6003
6004impl LemmaType {
6005    /// Functional update of the `Measure` payload (units + decomposition).
6006    /// Non-Measure variants pass through unchanged. The transform receives the owned
6007    /// units and decomposition and returns the replacements.
6008    pub fn map_measure<F>(self, f: F) -> Self
6009    where
6010        F: FnOnce(
6011            MeasureUnits,
6012            Option<BaseMeasureVector>,
6013        ) -> (MeasureUnits, Option<BaseMeasureVector>),
6014    {
6015        let LemmaType {
6016            name,
6017            specifications,
6018            extends,
6019            options,
6020        } = self;
6021        let specifications = match specifications {
6022            TypeSpecification::Measure {
6023                minimum,
6024                maximum,
6025                units,
6026                traits,
6027                decomposition,
6028                unit,
6029                help,
6030            } => {
6031                let (units, decomposition) = f(units, decomposition);
6032                TypeSpecification::Measure {
6033                    minimum,
6034                    maximum,
6035                    units,
6036                    traits,
6037                    decomposition,
6038                    unit,
6039                    help,
6040                }
6041            }
6042            other => other,
6043        };
6044        LemmaType {
6045            name,
6046            specifications,
6047            extends,
6048            options,
6049        }
6050    }
6051
6052    /// Create a new type with a name
6053    pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
6054        Self {
6055            name: Some(name),
6056            specifications,
6057            extends,
6058            options: TypeOptions::default(),
6059        }
6060    }
6061
6062    /// Create a type without a name (anonymous/inline type)
6063    pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
6064        Self {
6065            name: None,
6066            specifications,
6067            extends,
6068            options: TypeOptions::default(),
6069        }
6070    }
6071
6072    /// Create a primitive type (no name, extends Primitive)
6073    pub fn primitive(specifications: TypeSpecification) -> Self {
6074        Self {
6075            name: None,
6076            specifications,
6077            extends: TypeExtends::Primitive,
6078            options: TypeOptions::default(),
6079        }
6080    }
6081
6082    /// Apply one data command that does not need the surrounding spec.
6083    /// `-> option`, `-> options`, and a spec `-> with` / `-> suggest` are applied
6084    /// by the caller, which already holds the spec.
6085    pub fn apply_constraint(
6086        &mut self,
6087        names: ConstraintNames<'_>,
6088        command: TypeConstraintCommand,
6089        args: &[CommandArg],
6090        declared_suggestion: &mut Option<RawSuggestion>,
6091        declared_value: &mut Option<RawSuggestion>,
6092        resolve_unit: Option<&UnitTokenResolver<'_>>,
6093    ) -> Result<(), String> {
6094        self.specifications.apply_constraint(
6095            names,
6096            command,
6097            args,
6098            declared_suggestion,
6099            declared_value,
6100            resolve_unit,
6101        )
6102    }
6103
6104    /// Value is outside an inline option set. A rule is checked at evaluation, with the static rows.
6105    ///
6106    /// When the option set contains unscaled (dynamic-unit) values and `runtime_scales` is
6107    /// absent, membership is deferred to the evaluation walk (`Ok`).
6108    pub(crate) fn reject_disallowed(
6109        &self,
6110        value: &ValueKind,
6111        runtime_scales: Option<
6112            &std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
6113        >,
6114    ) -> Result<(), String> {
6115        let options = self.options_for_check();
6116        if options.rule.is_some() || options.values.is_empty() {
6117            return Ok(());
6118        }
6119        if options_have_unscaled_measure(&options) {
6120            let Some(scales) = runtime_scales else {
6121                return Ok(());
6122            };
6123            return if static_option_match_with_scales(&options, value, scales)? {
6124                Ok(())
6125            } else {
6126                Err(disallowed_message(self, &options, value, None))
6127            };
6128        }
6129        if static_option_match(&options, value) {
6130            return Ok(());
6131        }
6132        Err(disallowed_message(self, &options, value, None))
6133    }
6134
6135    /// Rule whose list is added to the allowed set, on this type or on a list's element.
6136    #[must_use]
6137    pub fn options_rule(&self) -> Option<&str> {
6138        if let Some(name) = &self.options.rule {
6139            return Some(name.as_str());
6140        }
6141        if let TypeSpecification::List { element, .. } = &self.specifications {
6142            if let Some(name) = &element.options.rule {
6143                return Some(name.as_str());
6144            }
6145        }
6146        None
6147    }
6148
6149    /// Inline option literals that still need a runtime scale before membership.
6150    #[must_use]
6151    pub(crate) fn has_unscaled_options(&self) -> bool {
6152        options_have_unscaled_measure(&self.options_for_check())
6153    }
6154
6155    /// Evaluation re-checks a named options rule, or a static set that includes
6156    /// dynamic-unit (unscaled) option literals.
6157    #[must_use]
6158    pub(crate) fn needs_runtime_option_check(&self) -> bool {
6159        self.options_rule().is_some() || self.has_unscaled_options()
6160    }
6161
6162    /// Static option displays. A rule that adds rows on a run is not included.
6163    #[must_use]
6164    pub fn static_options(&self) -> Vec<String> {
6165        let options = self.options_for_check();
6166        options
6167            .values
6168            .iter()
6169            .map(|option| self.display_bound(&option.value))
6170            .collect()
6171    }
6172
6173    /// Inline `-> option` values on this type (and a list's element, when both carry options).
6174    /// A named `-> options` rule is not enumerated.
6175    #[must_use]
6176    pub fn inline_option_values(&self) -> Vec<BoundValueKind> {
6177        self.options_for_check()
6178            .values
6179            .iter()
6180            .map(|option| option.value.clone())
6181            .collect()
6182    }
6183
6184    pub(crate) fn display_bound(&self, bound: &BoundValueKind) -> String {
6185        typed_literal_from_bound(bound, &Arc::new(self.clone())).display_value()
6186    }
6187
6188    /// Borrow the set that applies. Clone only when a list and its element both carry options.
6189    pub(crate) fn options_for_check(&self) -> std::borrow::Cow<'_, TypeOptions> {
6190        match &self.specifications {
6191            TypeSpecification::List { element, .. }
6192                if !self.options.is_empty() && !element.options.is_empty() =>
6193            {
6194                let mut options = self.options.clone();
6195                options
6196                    .values
6197                    .extend(element.options.values.iter().cloned());
6198                if options.rule.is_none() {
6199                    options.rule.clone_from(&element.options.rule);
6200                }
6201                std::borrow::Cow::Owned(options)
6202            }
6203            TypeSpecification::List { element, .. } if !element.options.is_empty() => {
6204                std::borrow::Cow::Borrowed(&element.options)
6205            }
6206            _ => std::borrow::Cow::Borrowed(&self.options),
6207        }
6208    }
6209
6210    /// `None` when `value` is in the static set or in `rule_rows`.
6211    ///
6212    /// When the static set has unscaled options and `runtime_scales` is absent, returns
6213    /// `None` so the caller can defer. When scales are present, settles before matching.
6214    /// Settle failure returns `Some(message)`.
6215    pub(crate) fn explain_disallowed(
6216        &self,
6217        value: &ValueKind,
6218        rule_rows: Option<&ListValue>,
6219        runtime_scales: Option<
6220            &std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
6221        >,
6222    ) -> Option<String> {
6223        let options = self.options_for_check();
6224        let needs_scales = options_have_unscaled_measure(&options)
6225            || rule_rows
6226                .is_some_and(|rows| rows.iter().any(|row| row.value.has_unscaled_measure()))
6227            || value.has_unscaled_measure();
6228        if needs_scales && runtime_scales.is_none() {
6229            return None;
6230        }
6231        let allowed = |candidate: &ValueKind| -> Result<bool, String> {
6232            if let Some(scales) = runtime_scales {
6233                if options_have_unscaled_measure(&options) {
6234                    if static_option_match_with_scales(&options, candidate, scales)? {
6235                        return Ok(true);
6236                    }
6237                } else if static_option_match(&options, candidate) {
6238                    return Ok(true);
6239                }
6240                if let Some(rows) = rule_rows {
6241                    let settled_candidate = settle_value_kind_with_scales(candidate, scales)?;
6242                    for option in rows.iter() {
6243                        let settled_option = settle_value_kind_with_scales(&option.value, scales)?;
6244                        if option_values_match(&settled_option, &settled_candidate) {
6245                            return Ok(true);
6246                        }
6247                    }
6248                    return Ok(false);
6249                }
6250                return Ok(options.is_empty());
6251            }
6252            if static_option_match(&options, candidate) {
6253                return Ok(true);
6254            }
6255            if let Some(rows) = rule_rows {
6256                return Ok(rows
6257                    .iter()
6258                    .any(|option| option_values_match(&option.value, candidate)));
6259            }
6260            Ok(options.is_empty())
6261        };
6262        match (&self.specifications, value) {
6263            (TypeSpecification::List { .. }, ValueKind::List(rows)) => {
6264                for row in rows.iter() {
6265                    match allowed(&row.value) {
6266                        Ok(true) => {}
6267                        Ok(false) => {
6268                            return Some(disallowed_message(self, &options, &row.value, rule_rows));
6269                        }
6270                        Err(message) => return Some(message),
6271                    }
6272                }
6273                None
6274            }
6275            _ => match allowed(value) {
6276                Ok(true) => None,
6277                Ok(false) => Some(disallowed_message(self, &options, value, rule_rows)),
6278                Err(message) => Some(message),
6279            },
6280        }
6281    }
6282
6283    /// Prefer `unit_name` for [`measure_runtime_signature`] (bind / signature-index hit).
6284    ///
6285    /// Writes `unit` onto `Measure` or `Ratio` specifications. Any other variant panics.
6286    #[must_use]
6287    pub fn with_unit(mut self, unit_name: impl Into<String>) -> Self {
6288        let unit_name = unit_name.into();
6289        match &mut self.specifications {
6290            TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
6291                *unit = Some(unit_name);
6292            }
6293            other => panic!(
6294                "BUG: with_unit called on non-measure/non-ratio type {}",
6295                other
6296            ),
6297        }
6298        self
6299    }
6300
6301    /// Prefix dynamic scale references on `lemma_type` with the `uses` hops from
6302    /// import `base_alias` through nested import names along `member_path`.
6303    /// `nested_import` returns the identity of the import slot named `segment` on
6304    /// the template of the current identity, or `None` when that hop is not an import.
6305    #[must_use]
6306    pub(crate) fn reanchor_through_import_path(
6307        lemma_type: Arc<LemmaType>,
6308        base_alias: &str,
6309        base_identity: &SpecIdentity,
6310        member_path: &[String],
6311        mut nested_import: impl FnMut(&SpecIdentity, &str) -> Option<SpecIdentity>,
6312    ) -> Arc<LemmaType> {
6313        let mut hops = vec![PathSegment::import(base_alias, base_identity)];
6314        let mut identity = base_identity.clone();
6315        for segment in member_path {
6316            let Some(next) = nested_import(&identity, segment) else {
6317                break;
6318            };
6319            hops.push(PathSegment::import(segment, &next));
6320            identity = next;
6321        }
6322        let mut out = lemma_type.as_ref().clone();
6323        for hop in hops.into_iter().rev() {
6324            out = out.with_scale_import_hop(&hop);
6325        }
6326        Arc::new(out)
6327    }
6328
6329    /// Clone with `segment` prefixed onto every dynamic unit scale reference: the
6330    /// `uses` alias hop a type crossed when it entered another spec. The declaring
6331    /// spec's coordinates stay on the type of the declaring spec itself; this clone
6332    /// is the importing spec's instance view of the same type.
6333    #[must_use]
6334    pub(crate) fn with_scale_import_hop(&self, segment: &PathSegment) -> Self {
6335        fn prefix_units(units: &mut MeasureUnits, segment: &PathSegment) {
6336            for unit in units.0.iter_mut() {
6337                if let crate::literals::UnitScale::Reference { target, .. } = &mut unit.scale {
6338                    match target {
6339                        ReferenceTarget::Data(path) => path.segments.insert(0, segment.clone()),
6340                        ReferenceTarget::Rule(rule) => rule.segments.insert(0, segment.clone()),
6341                    }
6342                }
6343            }
6344        }
6345        fn prefix_specifications(specifications: &mut TypeSpecification, segment: &PathSegment) {
6346            match specifications {
6347                TypeSpecification::Measure { units, .. }
6348                | TypeSpecification::MeasureRange { units, .. } => {
6349                    prefix_units(units, segment);
6350                }
6351                TypeSpecification::List { element, .. } => {
6352                    let prefixed = element.with_scale_import_hop(segment);
6353                    *element = Arc::new(prefixed);
6354                }
6355                _ => {}
6356            }
6357        }
6358        let mut out = self.clone();
6359        prefix_specifications(&mut out.specifications, segment);
6360        out
6361    }
6362
6363    /// Scale target of `unit_name` when that unit is reference-scaled on this type.
6364    /// The target is in the coordinates of the spec that holds this type Arc
6365    /// (`uses` hops are already prefixed).
6366    #[must_use]
6367    pub(crate) fn unit_scale_target(&self, unit_name: &str) -> Option<&ReferenceTarget> {
6368        self.measure_units_table()
6369            .and_then(|units| units.get(unit_name).ok())
6370            .and_then(MeasureUnit::resolved_scale_target)
6371    }
6372
6373    /// Every reference-scaled unit on this measure type, in declaration order.
6374    #[must_use]
6375    pub(crate) fn dynamic_scale_targets(&self) -> Vec<ReferenceTarget> {
6376        let Some(units) = self.measure_units_table() else {
6377            return Vec::new();
6378        };
6379        units
6380            .iter()
6381            .filter_map(|unit| unit.resolved_scale_target().cloned())
6382            .collect()
6383    }
6384
6385    /// Written unit on a measure or ratio specification, if any.
6386    #[must_use]
6387    pub fn unit(&self) -> Option<&str> {
6388        match &self.specifications {
6389            TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
6390                unit.as_deref()
6391            }
6392            _ => None,
6393        }
6394    }
6395
6396    /// Get the type name, or a default based on the type specification
6397    pub fn name(&self) -> String {
6398        self.name
6399            .clone()
6400            .unwrap_or_else(|| self.specifications.to_string())
6401    }
6402
6403    /// Check if this type is boolean
6404    pub fn is_boolean(&self) -> bool {
6405        matches!(&self.specifications, TypeSpecification::Boolean { .. })
6406    }
6407
6408    pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
6409        matches!(
6410            (kind, &self.specifications),
6411            (PrimitiveKind::Number, TypeSpecification::Number { .. })
6412                | (PrimitiveKind::Text, TypeSpecification::Text { .. })
6413                | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
6414                | (PrimitiveKind::Date, TypeSpecification::Date { .. })
6415                | (PrimitiveKind::Time, TypeSpecification::Time { .. })
6416                | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
6417                | (PrimitiveKind::Measure, TypeSpecification::Measure { .. })
6418        )
6419    }
6420
6421    /// Check if this type is measure
6422    pub fn is_measure(&self) -> bool {
6423        matches!(&self.specifications, TypeSpecification::Measure { .. })
6424    }
6425
6426    pub fn is_measure_range(&self) -> bool {
6427        matches!(&self.specifications, TypeSpecification::MeasureRange { .. })
6428    }
6429
6430    /// Check if this type is number (dimensionless)
6431    pub fn is_number(&self) -> bool {
6432        matches!(&self.specifications, TypeSpecification::Number { .. })
6433    }
6434
6435    pub fn is_number_range(&self) -> bool {
6436        matches!(&self.specifications, TypeSpecification::NumberRange { .. })
6437    }
6438
6439    /// Check if this type is numeric (either measure or number)
6440    pub fn is_numeric(&self) -> bool {
6441        matches!(
6442            &self.specifications,
6443            TypeSpecification::Measure { .. } | TypeSpecification::Number { .. }
6444        )
6445    }
6446
6447    /// Check if this type is text
6448    pub fn is_text(&self) -> bool {
6449        matches!(&self.specifications, TypeSpecification::Text { .. })
6450    }
6451
6452    /// Check if this type is date
6453    pub fn is_date(&self) -> bool {
6454        matches!(&self.specifications, TypeSpecification::Date { .. })
6455    }
6456
6457    pub fn is_date_range(&self) -> bool {
6458        matches!(&self.specifications, TypeSpecification::DateRange { .. })
6459    }
6460
6461    pub fn is_time_range(&self) -> bool {
6462        matches!(&self.specifications, TypeSpecification::TimeRange { .. })
6463    }
6464
6465    /// Check if this type is time
6466    pub fn is_time(&self) -> bool {
6467        matches!(&self.specifications, TypeSpecification::Time { .. })
6468    }
6469
6470    pub fn has_trait_duration(&self) -> bool {
6471        match &self.specifications {
6472            TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Duration),
6473            _ => false,
6474        }
6475    }
6476
6477    pub fn is_duration_like_measure(&self) -> bool {
6478        if !self.is_measure() {
6479            return false;
6480        }
6481        if self.has_trait_duration() {
6482            return true;
6483        }
6484        self.is_anonymous_measure()
6485            && self
6486                .measure_type_decomposition()
6487                .is_some_and(|d| *d == duration_decomposition())
6488    }
6489
6490    pub fn is_duration_like(&self) -> bool {
6491        self.is_duration_like_measure()
6492    }
6493
6494    pub fn has_trait_calendar(&self) -> bool {
6495        match &self.specifications {
6496            TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Calendar),
6497            _ => false,
6498        }
6499    }
6500
6501    pub fn is_calendar_like_measure(&self) -> bool {
6502        if !self.is_measure() {
6503            return false;
6504        }
6505        if self.has_trait_calendar() {
6506            return true;
6507        }
6508        self.is_anonymous_measure()
6509            && self
6510                .measure_type_decomposition()
6511                .is_some_and(|d| *d == calendar_decomposition())
6512    }
6513
6514    pub fn is_calendar_like(&self) -> bool {
6515        self.is_calendar_like_measure()
6516    }
6517
6518    /// Check if this type is ratio
6519    pub fn is_ratio(&self) -> bool {
6520        matches!(&self.specifications, TypeSpecification::Ratio { .. })
6521    }
6522
6523    pub fn is_ratio_range(&self) -> bool {
6524        matches!(&self.specifications, TypeSpecification::RatioRange { .. })
6525    }
6526
6527    pub fn is_calendar_measure_range(&self) -> bool {
6528        matches!(
6529            &self.specifications,
6530            TypeSpecification::MeasureRange { decomposition: Some(decomposition), .. }
6531                if *decomposition == calendar_decomposition()
6532        )
6533    }
6534
6535    pub fn is_calendar_like_range(&self) -> bool {
6536        self.is_calendar_measure_range()
6537    }
6538
6539    pub fn is_range(&self) -> bool {
6540        matches!(
6541            &self.specifications,
6542            TypeSpecification::DateRange { .. }
6543                | TypeSpecification::TimeRange { .. }
6544                | TypeSpecification::NumberRange { .. }
6545                | TypeSpecification::MeasureRange { .. }
6546                | TypeSpecification::RatioRange { .. }
6547        )
6548    }
6549
6550    /// Check if this type is a spec instance.
6551    pub fn is_spec(&self) -> bool {
6552        matches!(&self.specifications, TypeSpecification::Spec { .. })
6553    }
6554
6555    pub fn is_list(&self) -> bool {
6556        matches!(&self.specifications, TypeSpecification::List { .. })
6557    }
6558
6559    pub fn list_element_spec(&self) -> Option<&TypeSpecification> {
6560        match &self.specifications {
6561            TypeSpecification::List { element, .. } => Some(&element.specifications),
6562            _ => None,
6563        }
6564    }
6565
6566    pub fn list_of(element: Arc<LemmaType>) -> Self {
6567        Self {
6568            name: Some(format!("{} list", element.name())),
6569            specifications: TypeSpecification::List {
6570                element,
6571                key: ListKey::Numbered,
6572                help: String::new(),
6573            },
6574            extends: TypeExtends::Primitive,
6575            options: TypeOptions::default(),
6576        }
6577    }
6578
6579    /// Element type of a list type (`money` for `money list`).
6580    pub fn list_element_lemma_type(&self) -> &Arc<LemmaType> {
6581        let TypeSpecification::List { element, .. } = &self.specifications else {
6582            panic!(
6583                "BUG: list_element_lemma_type on non-list type {}",
6584                self.name()
6585            );
6586        };
6587        element
6588    }
6589
6590    /// Spec identity when this type is a spec instance.
6591    #[must_use]
6592    pub fn spec_identity(&self) -> Option<&SpecIdentity> {
6593        match &self.specifications {
6594            TypeSpecification::Spec { spec } => Some(spec),
6595            _ => None,
6596        }
6597    }
6598
6599    /// Check if this type is veto
6600    pub fn vetoed(&self) -> bool {
6601        matches!(&self.specifications, TypeSpecification::Veto { .. })
6602    }
6603
6604    /// True if this type is the undetermined sentinel (type could not be inferred).
6605    pub fn is_undetermined(&self) -> bool {
6606        matches!(&self.specifications, TypeSpecification::Undetermined)
6607    }
6608
6609    /// Check if two types have the same base type specification (ignoring constraints)
6610    pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
6611        use TypeSpecification::*;
6612        matches!(
6613            (&self.specifications, &other.specifications),
6614            (Boolean { .. }, Boolean { .. })
6615                | (Number { .. }, Number { .. })
6616                | (NumberRange { .. }, NumberRange { .. })
6617                | (Measure { .. }, Measure { .. })
6618                | (MeasureRange { .. }, MeasureRange { .. })
6619                | (Text { .. }, Text { .. })
6620                | (Date { .. }, Date { .. })
6621                | (DateRange { .. }, DateRange { .. })
6622                | (Time { .. }, Time { .. })
6623                | (TimeRange { .. }, TimeRange { .. })
6624                | (Ratio { .. }, Ratio { .. })
6625                | (RatioRange { .. }, RatioRange { .. })
6626                | (Veto { .. }, Veto { .. })
6627                | (Undetermined, Undetermined)
6628                | (Spec { .. }, Spec { .. })
6629                | (List { .. }, List { .. })
6630        )
6631    }
6632
6633    /// Root of the custom extension chain; `None` for primitive and anonymous types.
6634    #[must_use]
6635    pub fn family(&self) -> Option<&TypeFamily> {
6636        match &self.extends {
6637            TypeExtends::Custom { family, .. } => Some(family),
6638            TypeExtends::Primitive => None,
6639        }
6640    }
6641
6642    /// Element type of a measure or ratio range. Keeps the range's [`TypeExtends`]
6643    /// (and thus its [`TypeFamily`]). Name is the family's root name when the
6644    /// range extends a named type; otherwise the range's own name.
6645    #[must_use]
6646    pub fn range_element_type(&self) -> Arc<LemmaType> {
6647        let element_spec = self
6648            .specifications
6649            .element_from_range()
6650            .expect("BUG: range_element_type requires a range specification");
6651        let name = match &self.extends {
6652            TypeExtends::Custom { family, .. } => Some(family.name.clone()),
6653            TypeExtends::Primitive => self.name.clone(),
6654        };
6655        Arc::new(LemmaType {
6656            name,
6657            specifications: element_spec,
6658            extends: self.extends.clone(),
6659            options: TypeOptions::default(),
6660        })
6661    }
6662
6663    /// Unit family of a measure or measure-range type.
6664    #[must_use]
6665    pub fn measure_family(&self) -> Option<&TypeFamily> {
6666        (self.is_measure() || self.is_measure_range())
6667            .then(|| self.family())
6668            .flatten()
6669    }
6670
6671    /// Type of the values this type holds: itself, or its (nested) list element.
6672    #[must_use]
6673    pub(crate) fn held_value_type(&self) -> &LemmaType {
6674        let mut current = self;
6675        while current.is_list() {
6676            current = current.list_element_lemma_type().as_ref();
6677        }
6678        current
6679    }
6680
6681    /// Measure family of the values this type holds: its own, or its list element's.
6682    #[must_use]
6683    pub(crate) fn held_measure_family(&self) -> Option<&TypeFamily> {
6684        self.held_value_type().measure_family()
6685    }
6686
6687    /// Unit family of a ratio or ratio-range type.
6688    #[must_use]
6689    pub fn ratio_family(&self) -> Option<&TypeFamily> {
6690        (self.is_ratio() || self.is_ratio_range())
6691            .then(|| self.family())
6692            .flatten()
6693    }
6694
6695    /// Unit family of a measure or ratio type.
6696    #[must_use]
6697    pub(crate) fn unit_family(&self) -> Option<&TypeFamily> {
6698        self.measure_family().or_else(|| self.ratio_family())
6699    }
6700
6701    /// Root type name of [`Self::measure_family`], for display.
6702    #[must_use]
6703    pub fn measure_family_name(&self) -> Option<&str> {
6704        self.measure_family().map(|family| family.name.as_str())
6705    }
6706
6707    /// Root type name of [`Self::ratio_family`], for display.
6708    #[must_use]
6709    pub fn ratio_family_name(&self) -> Option<&str> {
6710        self.ratio_family().map(|family| family.name.as_str())
6711    }
6712
6713    /// Same base kind and, for measures / measure ranges, the same named family:
6714    /// the relation under which two types describe the same values. Names and
6715    /// constraints may differ. Ratio and RatioRange stay base-kind only.
6716    #[must_use]
6717    pub(crate) fn same_value_type(&self, other: &LemmaType) -> bool {
6718        if !self.has_same_base_type(other) {
6719            return false;
6720        }
6721        if self.is_measure() {
6722            return match (self.measure_family(), other.measure_family()) {
6723                (Some(_), Some(_)) => self.same_measure_family(other),
6724                (None, None) => {
6725                    self.measure_type_decomposition() == other.measure_type_decomposition()
6726                }
6727                _ => false,
6728            };
6729        }
6730        if let (
6731            TypeSpecification::Spec { spec: self_spec },
6732            TypeSpecification::Spec { spec: other_spec },
6733        ) = (&self.specifications, &other.specifications)
6734        {
6735            return self_spec == other_spec;
6736        }
6737        if let (
6738            TypeSpecification::List {
6739                element: self_element,
6740                key: self_key,
6741                ..
6742            },
6743            TypeSpecification::List {
6744                element: other_element,
6745                key: other_key,
6746                ..
6747            },
6748        ) = (&self.specifications, &other.specifications)
6749        {
6750            return self_key == other_key && self_element.same_value_type(other_element);
6751        }
6752        if self.is_measure_range() {
6753            match (self.family(), other.family()) {
6754                (Some(self_family), Some(other_family)) => self_family == other_family,
6755                (None, None) => {
6756                    let self_element = self
6757                        .specifications
6758                        .element_from_range()
6759                        .expect("BUG: MeasureRange always defines element_from_range");
6760                    let other_element = other
6761                        .specifications
6762                        .element_from_range()
6763                        .expect("BUG: MeasureRange always defines element_from_range");
6764                    match (&self_element, &other_element) {
6765                        (
6766                            TypeSpecification::Measure {
6767                                decomposition: self_decomp,
6768                                ..
6769                            },
6770                            TypeSpecification::Measure {
6771                                decomposition: other_decomp,
6772                                ..
6773                            },
6774                        ) => self_decomp == other_decomp,
6775                        _ => unreachable!("BUG: element_from_range of MeasureRange yields Measure"),
6776                    }
6777                }
6778                _ => false,
6779            }
6780        } else {
6781            true
6782        }
6783    }
6784
6785    /// Drop a written unit so a path/reference expression type does not claim
6786    /// the stored value named that unit. Literals and `as <unit>` results keep
6787    /// units via [`Self::with_unit`].
6788    #[must_use]
6789    pub(crate) fn without_written_unit(&self) -> Self {
6790        let mut cleared = self.clone();
6791        match &mut cleared.specifications {
6792            TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
6793                *unit = None;
6794            }
6795            _ => {}
6796        }
6797        cleared
6798    }
6799
6800    /// Clear slot bounds, help, and written unit so a computed/generalized type
6801    /// does not carry either operand's constraints.
6802    #[must_use]
6803    pub(crate) fn without_slot_bounds(&self) -> Self {
6804        let mut cleared = self.without_written_unit();
6805        match &mut cleared.specifications {
6806            TypeSpecification::Measure {
6807                minimum,
6808                maximum,
6809                help,
6810                ..
6811            } => {
6812                *minimum = None;
6813                *maximum = None;
6814                *help = String::new();
6815            }
6816            TypeSpecification::Ratio {
6817                minimum,
6818                maximum,
6819                help,
6820                ..
6821            } => {
6822                *minimum = None;
6823                *maximum = None;
6824                *help = String::new();
6825            }
6826            TypeSpecification::Number {
6827                minimum,
6828                maximum,
6829                help,
6830                ..
6831            } => {
6832                *minimum = None;
6833                *maximum = None;
6834                *help = String::new();
6835            }
6836            TypeSpecification::Date {
6837                minimum,
6838                maximum,
6839                help,
6840                ..
6841            } => {
6842                *minimum = None;
6843                *maximum = None;
6844                *help = String::new();
6845            }
6846            TypeSpecification::Time {
6847                minimum,
6848                maximum,
6849                help,
6850                ..
6851            } => {
6852                *minimum = None;
6853                *maximum = None;
6854                *help = String::new();
6855            }
6856            TypeSpecification::MeasureRange {
6857                minimum,
6858                maximum,
6859                help,
6860                ..
6861            } => {
6862                *minimum = None;
6863                *maximum = None;
6864                *help = String::new();
6865            }
6866            TypeSpecification::DateRange {
6867                minimum,
6868                maximum,
6869                help,
6870                ..
6871            } => {
6872                *minimum = None;
6873                *maximum = None;
6874                *help = String::new();
6875            }
6876            TypeSpecification::TimeRange {
6877                minimum,
6878                maximum,
6879                help,
6880                ..
6881            } => {
6882                *minimum = None;
6883                *maximum = None;
6884                *help = String::new();
6885            }
6886            TypeSpecification::NumberRange {
6887                minimum,
6888                maximum,
6889                help,
6890                ..
6891            } => {
6892                *minimum = None;
6893                *maximum = None;
6894                *help = String::new();
6895            }
6896            TypeSpecification::RatioRange {
6897                minimum,
6898                maximum,
6899                help,
6900                ..
6901            } => {
6902                *minimum = None;
6903                *maximum = None;
6904                *help = String::new();
6905            }
6906            TypeSpecification::Boolean { help, .. } | TypeSpecification::Text { help, .. } => {
6907                *help = String::new();
6908            }
6909            TypeSpecification::Veto { .. }
6910            | TypeSpecification::Undetermined
6911            | TypeSpecification::Spec { .. }
6912            | TypeSpecification::List { .. } => {}
6913        }
6914        cleared
6915    }
6916
6917    /// Returns true if both types are measure and belong to the same named measure family.
6918    #[must_use]
6919    pub fn same_measure_family(&self, other: &LemmaType) -> bool {
6920        if !self.is_measure() || !other.is_measure() {
6921            return false;
6922        }
6923        match (self.measure_family(), other.measure_family()) {
6924            (Some(self_family), Some(other_family)) => self_family == other_family,
6925            _ => false,
6926        }
6927    }
6928
6929    /// Structural equality for interface drift: same as [`PartialEq`] but family
6930    /// versions (`TypeFamily::effective`) are ignored. Unpinned imports spanning
6931    /// unchanged versions of a dependency compare equal; real changes still differ.
6932    #[must_use]
6933    pub fn same_interface(&self, other: &Self) -> bool {
6934        self.with_families_without_effective() == other.with_families_without_effective()
6935    }
6936
6937    /// Like [`Self::same_interface`], but [`TypeOptions`] may differ. Used for
6938    /// `uses … -> effective` where option sets change across temporal versions.
6939    #[must_use]
6940    pub fn same_interface_ignoring_options(&self, other: &Self) -> bool {
6941        let mut left = self.with_families_without_effective();
6942        let mut right = other.with_families_without_effective();
6943        left.options = TypeOptions::default();
6944        right.options = TypeOptions::default();
6945        left == right
6946    }
6947
6948    /// Clone with every [`TypeFamily::effective`] set to [`EffectiveDate::Origin`].
6949    #[must_use]
6950    pub fn with_families_without_effective(&self) -> Self {
6951        Self {
6952            name: self.name.clone(),
6953            specifications: self.specifications.with_families_without_effective(),
6954            extends: self.extends.with_families_without_effective(),
6955            options: self.options.clone(),
6956        }
6957    }
6958
6959    #[must_use]
6960    pub fn compatible_with_anonymous_measure(&self, other: &LemmaType) -> bool {
6961        if !self.is_measure() || !other.is_measure() {
6962            return false;
6963        }
6964        if !self.is_anonymous_measure() && !other.is_anonymous_measure() {
6965            return false;
6966        }
6967        match (
6968            self.measure_type_decomposition(),
6969            other.measure_type_decomposition(),
6970        ) {
6971            (Some(a), Some(b)) => a == b,
6972            _ => false,
6973        }
6974    }
6975
6976    /// Create a Veto LemmaType
6977    pub fn veto_type() -> Self {
6978        Self::primitive(TypeSpecification::veto())
6979    }
6980
6981    /// LemmaType sentinel for undetermined type (used during inference when a type cannot be determined).
6982    /// Propagates through expressions and is never present in a validated graph.
6983    pub fn undetermined_type() -> Self {
6984        Self::primitive(TypeSpecification::Undetermined)
6985    }
6986
6987    /// Input scale on a number declaration. Rejects a supplied value with more
6988    /// fractional digits. Not applied when a result is published.
6989    pub fn decimal_places(&self) -> Option<u8> {
6990        match &self.specifications {
6991            TypeSpecification::Number { decimals, .. } => *decimals,
6992            _ => None,
6993        }
6994    }
6995
6996    /// Convert a rational magnitude to an API [`Decimal`] (28-scale boundary).
6997    ///
6998    /// Returns [`NumericFailure::Overflow`] when |magnitude| > Decimal::MAX
6999    /// (callers map this to a decimal-limit Veto). Does not apply `decimals`.
7000    pub fn try_rational_as_decimal(
7001        &self,
7002        magnitude: &crate::computation::rational::RationalInteger,
7003    ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
7004        let decimal = magnitude.try_to_decimal()?;
7005        Ok(format_decimal_for_api(decimal))
7006    }
7007
7008    /// Convert a canonical measure magnitude in the named declared unit to an API [`Decimal`].
7009    pub fn try_measure_canonical_as_decimal_in_unit(
7010        &self,
7011        canonical_magnitude: &crate::computation::rational::RationalInteger,
7012        unit_name: &str,
7013    ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
7014        use crate::computation::rational::checked_div;
7015        let unit_factor = self.measure_unit_factor(unit_name);
7016        let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
7017        self.try_rational_as_decimal(&magnitude_in_unit)
7018    }
7019
7020    /// Convert a canonical ratio magnitude in the named declared unit to an API [`Decimal`].
7021    pub fn try_ratio_canonical_as_decimal_in_unit(
7022        &self,
7023        canonical_magnitude: &crate::computation::rational::RationalInteger,
7024        unit_name: &str,
7025    ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
7026        use crate::computation::rational::checked_mul;
7027        let units = match &self.specifications {
7028            TypeSpecification::Ratio { units, .. } => units,
7029            _ => unreachable!(
7030                "BUG: try_ratio_canonical_as_decimal_in_unit called on non-ratio type {}",
7031                self.name()
7032            ),
7033        };
7034        let ratio_unit = units
7035            .iter()
7036            .find(|unit| unit.name == unit_name)
7037            .unwrap_or_else(|| {
7038                let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
7039                unreachable!(
7040                    "BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
7041                    unit_name,
7042                    self.name(),
7043                    valid.join(", ")
7044                )
7045            });
7046        let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
7047        self.try_rational_as_decimal(&magnitude_in_unit)
7048    }
7049
7050    /// Get an example value string for this type, suitable for UI help text
7051    pub fn example_value(&self) -> &'static str {
7052        match &self.specifications {
7053            TypeSpecification::Text { .. } => "\"hello world\"",
7054            TypeSpecification::Measure { .. } => "12.50 eur",
7055            TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
7056            TypeSpecification::Number { .. } => "3.14",
7057            TypeSpecification::NumberRange { .. } => "0...100",
7058            TypeSpecification::Boolean { .. } => "true",
7059            TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
7060            TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
7061            TypeSpecification::TimeRange { .. } => "09:00...17:00",
7062            TypeSpecification::Veto { .. } => "veto",
7063            TypeSpecification::Time { .. } => "14:30:00",
7064            TypeSpecification::Ratio { .. } => "50%",
7065            TypeSpecification::RatioRange { .. } => "10%...50%",
7066            TypeSpecification::Undetermined => unreachable!(
7067                "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
7068            ),
7069            TypeSpecification::Spec { .. } => unreachable!(
7070                "BUG: example_value called on a spec type; spec instances are not prompted data"
7071            ),
7072            TypeSpecification::List { element, .. } => element.example_value(),
7073        }
7074    }
7075
7076    /// Factor for a unit of this measure type (for unit conversion during evaluation only).
7077    /// Planning must validate conversions first and return Error for invalid units.
7078    /// If called with a non-measure type or unknown unit name, panics (invariant violation).
7079    #[must_use]
7080    /// Returns the resolved `BaseMeasureVector` for Measure types, or `None` if
7081    /// the decomposition pass has not yet resolved this type.
7082    /// Panics if called on non-Measure types.
7083    pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
7084        match &self.specifications {
7085            TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
7086            _ => unreachable!(
7087                "BUG: measure_type_decomposition called on non-measure type {}",
7088                self.name()
7089            ),
7090        }
7091    }
7092
7093    /// One family dimension with exponent 1 — the shape that may cross-family
7094    /// relabel when the expression also names a unit (`5 eur as kg`).
7095    #[must_use]
7096    pub(crate) fn is_simple_base_measure(&self) -> bool {
7097        if !self.is_measure() {
7098            return false;
7099        }
7100        match self.measure_type_decomposition() {
7101            Some(decomposition) if decomposition.len() == 1 => {
7102                decomposition.values().next() == Some(&1)
7103            }
7104            _ => false,
7105        }
7106    }
7107
7108    /// Declared measure unit table, when this type is a measure or measure range.
7109    #[must_use]
7110    pub fn measure_units_table(&self) -> Option<&MeasureUnits> {
7111        match &self.specifications {
7112            TypeSpecification::Measure { units, .. }
7113            | TypeSpecification::MeasureRange { units, .. } => Some(units),
7114            _ => None,
7115        }
7116    }
7117
7118    /// Whether any declared unit of this measure family has a reference scale.
7119    /// List types follow their element.
7120    #[must_use]
7121    pub fn has_dynamic_units(&self) -> bool {
7122        if self
7123            .measure_units_table()
7124            .is_some_and(|units| units.iter().any(MeasureUnit::is_dynamic))
7125        {
7126            return true;
7127        }
7128        match &self.specifications {
7129            TypeSpecification::List { element, .. } => element.has_dynamic_units(),
7130            _ => false,
7131        }
7132    }
7133
7134    /// Whether `unit_name` is a reference-scaled unit of this measure type.
7135    #[must_use]
7136    pub fn unit_is_dynamic(&self, unit_name: &str) -> bool {
7137        self.measure_units_table()
7138            .and_then(|units| units.get(unit_name).ok())
7139            .is_some_and(MeasureUnit::is_dynamic)
7140    }
7141
7142    /// Unit name chosen for measure display / wire `unit` under `runtime_scales`.
7143    #[must_use]
7144    pub(crate) fn measure_display_unit_name(
7145        &self,
7146        runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
7147    ) -> Option<&str> {
7148        match &self.specifications {
7149            TypeSpecification::Measure { units, .. } if !units.is_empty() => Some(
7150                measure_display_unit(units, self, runtime_scales)
7151                    .name
7152                    .as_str(),
7153            ),
7154            _ => None,
7155        }
7156    }
7157
7158    /// Fallback unit for a measure with no binding: factor-1, then the first
7159    /// literal-factor unit, then the first declared unit.
7160    #[must_use]
7161    pub fn measure_fallback_unit(&self) -> Option<&MeasureUnit> {
7162        let units = self.measure_units_table()?;
7163        units
7164            .iter()
7165            .find(|unit| unit.is_canonical_factor())
7166            .or_else(|| units.iter().find(|unit| unit.literal_factor().is_some()))
7167            .or_else(|| units.iter().next())
7168    }
7169
7170    /// Runtime unit signature for measure arithmetic and display.
7171    ///
7172    /// - Named measure with units: `[(binding_or_fallback_unit_name, 1)]`. Arithmetic
7173    ///   expands this via `expand_signature_to_base_units` using the unit table.
7174    /// - Anonymous / no units: decomposition converted to signature form.
7175    ///
7176    /// Fallback order when there is no binding: the factor-1 unit, the first unit
7177    /// with a literal factor, the first declared unit.
7178    #[must_use]
7179    pub fn measure_runtime_signature(&self) -> Vec<(String, i32)> {
7180        if let Some(binding) = self.unit() {
7181            return vec![(binding.to_string(), 1)];
7182        }
7183        match &self.specifications {
7184            TypeSpecification::Measure {
7185                units,
7186                decomposition,
7187                ..
7188            } => {
7189                if let Some(unit) = self.measure_fallback_unit() {
7190                    return vec![(unit.name.clone(), 1)];
7191                }
7192                if units.is_empty() {
7193                    return base_measure_vector_as_signature(
7194                        decomposition
7195                            .as_ref()
7196                            .expect("BUG: measure type without units must carry a decomposition"),
7197                    );
7198                }
7199                unreachable!("BUG: non-empty measure unit table has no fallback unit");
7200            }
7201            TypeSpecification::MeasureRange {
7202                units,
7203                decomposition,
7204                ..
7205            } => {
7206                if let Some(unit) = self.measure_fallback_unit() {
7207                    return vec![(unit.name.clone(), 1)];
7208                }
7209                if units.is_empty() {
7210                    return base_measure_vector_as_signature(
7211                        decomposition
7212                            .as_ref()
7213                            .expect("BUG: measure type without units must carry a decomposition"),
7214                    );
7215                }
7216                unreachable!("BUG: non-empty measure-range unit table has no fallback unit");
7217            }
7218            _ => unreachable!(
7219                "BUG: measure_runtime_signature called on non-measure type {}",
7220                self.name()
7221            ),
7222        }
7223    }
7224
7225    /// Primary declared ratio unit name when the type carries a non-empty unit table.
7226    #[must_use]
7227    pub fn ratio_primary_unit(&self) -> Option<&str> {
7228        match &self.specifications {
7229            TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
7230                units.iter().next().map(|unit| unit.name.as_str())
7231            }
7232            TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
7233                units.iter().next().map(|unit| unit.name.as_str())
7234            }
7235            _ => None,
7236        }
7237    }
7238
7239    /// Returns true if this is an anonymous (no-name) Measure — i.e. an anonymous
7240    /// intermediate produced by cross-axis arithmetic.
7241    pub fn is_anonymous_measure(&self) -> bool {
7242        self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
7243    }
7244
7245    /// Build an anonymous `LemmaType` for a given dimensional decomposition.
7246    /// Used at plan time to represent the inferred type of cross-axis intermediates.
7247    /// Runtime unit signature is derived from this decomposition via
7248    /// [`LemmaType::measure_runtime_signature`].
7249    pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
7250        Self {
7251            name: None,
7252            specifications: TypeSpecification::Measure {
7253                minimum: None,
7254                maximum: None,
7255                units: crate::literals::MeasureUnits::new(),
7256                traits: Vec::new(),
7257                decomposition: Some(decomposition),
7258                unit: None,
7259                help: String::new(),
7260            },
7261            extends: TypeExtends::Primitive,
7262            options: TypeOptions::default(),
7263        }
7264    }
7265
7266    /// Declared ratio unit names when the type carries a non-empty unit table (`None` otherwise).
7267    #[must_use]
7268    pub fn ratio_unit_names(&self) -> Option<Vec<&str>> {
7269        match &self.specifications {
7270            TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
7271                Some(units.iter().map(|unit| unit.name.as_str()).collect())
7272            }
7273            TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
7274                Some(units.iter().map(|unit| unit.name.as_str()).collect())
7275            }
7276            _ => None,
7277        }
7278    }
7279
7280    /// Declared unit names when the type carries a non-empty unit table (`None` otherwise).
7281    #[must_use]
7282    pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
7283        match &self.specifications {
7284            TypeSpecification::Measure { units, .. } if !units.is_empty() => {
7285                Some(units.iter().map(|unit| unit.name.as_str()).collect())
7286            }
7287            TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
7288                Some(units.iter().map(|unit| unit.name.as_str()).collect())
7289            }
7290            _ => None,
7291        }
7292    }
7293
7294    /// `age [number]` or `gender [gender_code]` — brackets omitted when type adds nothing.
7295    #[must_use]
7296    pub fn label_for_data_input(&self, input_key: &str) -> String {
7297        let type_label = if let Some(parent) = self.extends.parent_name() {
7298            parent.to_string()
7299        } else {
7300            let type_name = self.name();
7301            if type_name == input_key {
7302                self.specifications.to_string()
7303            } else {
7304                type_name
7305            }
7306        };
7307        if type_label == input_key {
7308            input_key.to_string()
7309        } else {
7310            format!("{input_key} [{type_label}]")
7311        }
7312    }
7313
7314    /// `Data age [number]: {detail}` for runtime data override vetoes (parse/validation).
7315    #[must_use]
7316    pub fn data_veto_message(&self, input_key: &str, detail: &str) -> String {
7317        format!("Data {}: {}", self.label_for_data_input(input_key), detail)
7318    }
7319
7320    /// Whether an empty [`EvaluateDataValue`] should veto before parse (text may accept `""`).
7321    #[must_use]
7322    pub fn empty_runtime_input_vetoes(&self) -> bool {
7323        !matches!(self.specifications, TypeSpecification::Text { .. })
7324    }
7325
7326    /// Return the conversion factor for a declared unit name on this measure type.
7327    pub fn measure_unit_factor(
7328        &self,
7329        unit_name: &str,
7330    ) -> &crate::computation::rational::RationalInteger {
7331        let units = match &self.specifications {
7332            TypeSpecification::Measure { units, .. } => units,
7333            TypeSpecification::MeasureRange { units, .. } => units,
7334            _ => unreachable!(
7335                "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
7336                self.name()
7337            ),
7338        };
7339        match units.get(unit_name) {
7340            Ok(unit) => unit.require_literal_factor(),
7341            Err(_) => {
7342                let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
7343                unreachable!(
7344                    "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
7345                    unit_name,
7346                    self.name(),
7347                    valid.join(", ")
7348                );
7349            }
7350        }
7351    }
7352
7353    pub fn ratio_unit_factor(
7354        &self,
7355        unit_name: &str,
7356    ) -> &crate::computation::rational::RationalInteger {
7357        let units = match &self.specifications {
7358            TypeSpecification::Ratio { units, .. } => units,
7359            _ => unreachable!(
7360                "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
7361                self.name()
7362            ),
7363        };
7364        match units.get(unit_name) {
7365            Ok(RatioUnit { value, .. }) => value,
7366            Err(_) => {
7367                let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
7368                unreachable!(
7369                    "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
7370                    unit_name,
7371                    self.name(),
7372                    valid.join(", ")
7373                );
7374            }
7375        }
7376    }
7377
7378    /// Convert a measure literal to API [`Decimal`]s in the given unit names.
7379    pub(crate) fn measure_literal_unit_map(
7380        &self,
7381        literal: &LiteralValue,
7382        unit_names: &[&str],
7383        factor_source: UnitFactorSource<'_>,
7384    ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
7385        use crate::computation::rational::checked_div;
7386
7387        let ValueKind::Measure(magnitude) = &literal.value else {
7388            panic!("BUG: measure_literal_unit_map called with non-measure value");
7389        };
7390        let mut map = BTreeMap::new();
7391        for &unit_name in unit_names {
7392            let Some(unit_factor) = factor_source.try_measure_unit_factor(unit_name) else {
7393                continue;
7394            };
7395            let magnitude_in_unit = checked_div(magnitude, &unit_factor)
7396                .map_err(LiteralUnitMapFailure::UnitConversion)?;
7397            let decimal = self
7398                .try_rational_as_decimal(&magnitude_in_unit)
7399                .map_err(LiteralUnitMapFailure::Commit)?;
7400            map.insert(unit_name.to_string(), decimal);
7401        }
7402        Ok(map)
7403    }
7404
7405    /// Convert a ratio literal to API [`Decimal`]s in the given unit names.
7406    pub(crate) fn ratio_literal_unit_map(
7407        &self,
7408        literal: &LiteralValue,
7409        unit_names: &[&str],
7410        factor_source: UnitFactorSource<'_>,
7411    ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
7412        use crate::computation::rational::checked_mul;
7413
7414        let ratio_api_type = match &self.specifications {
7415            TypeSpecification::Ratio { .. } => self,
7416            TypeSpecification::RatioRange { .. } => {
7417                return ratio_element_type_for_api(self).ratio_literal_unit_map(
7418                    literal,
7419                    unit_names,
7420                    factor_source,
7421                );
7422            }
7423            _ => {
7424                panic!(
7425                    "BUG: ratio_literal_unit_map called with non-ratio type {}",
7426                    self.name()
7427                );
7428            }
7429        };
7430        let ValueKind::Ratio(canonical) = &literal.value else {
7431            panic!("BUG: ratio_literal_unit_map called with non-ratio value");
7432        };
7433        if unit_names.is_empty() {
7434            panic!(
7435                "BUG: ratio literal type '{}' must have at least one unit name",
7436                self.name()
7437            );
7438        }
7439        let mut map = BTreeMap::new();
7440        for &unit_name in unit_names {
7441            let unit_factor = factor_source.ratio_unit_factor(unit_name);
7442            let magnitude_in_unit = checked_mul(canonical, unit_factor)
7443                .map_err(LiteralUnitMapFailure::UnitConversion)?;
7444            let decimal = ratio_api_type
7445                .try_rational_as_decimal(&magnitude_in_unit)
7446                .map_err(LiteralUnitMapFailure::Commit)?;
7447            map.insert(unit_name.to_string(), decimal);
7448        }
7449        Ok(map)
7450    }
7451}
7452
7453/// Where to read measure/ratio unit factors when building a per-unit decimal map.
7454pub(crate) enum UnitFactorSource<'a> {
7455    DeclaredOn(&'a LemmaType),
7456    Merged {
7457        measure: Option<&'a MeasureUnits>,
7458        ratio: Option<&'a RatioUnits>,
7459    },
7460    /// Evaluated reference scales over a declared or merged table. A dynamic
7461    /// column whose scale is missing from `scales` is omitted.
7462    Runtime {
7463        scales:
7464            &'a std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
7465        fallback: &'a UnitFactorSource<'a>,
7466    },
7467}
7468
7469impl UnitFactorSource<'_> {
7470    /// Literal factor of `unit_name`, or `None` when that unit is reference-scaled
7471    /// and this source has no runtime factor for it (the column is omitted).
7472    fn try_measure_unit_factor(
7473        &self,
7474        unit_name: &str,
7475    ) -> Option<crate::computation::rational::RationalInteger> {
7476        match self {
7477            UnitFactorSource::DeclaredOn(lemma_type) => {
7478                let units = lemma_type.measure_units_table().unwrap_or_else(|| {
7479                    panic!(
7480                        "BUG: measure_unit_factor on non-measure type {}",
7481                        lemma_type.name()
7482                    )
7483                });
7484                let unit = units.get(unit_name).unwrap_or_else(|_| {
7485                    panic!(
7486                        "BUG: unknown unit '{unit_name}' for measure type {}",
7487                        lemma_type.name()
7488                    )
7489                });
7490                unit.literal_factor().cloned()
7491            }
7492            UnitFactorSource::Merged { measure, .. } => {
7493                // A unit omitted from the merged table (owners disagree on its scale)
7494                // has no single factor here; the column is skipped.
7495                let units = (*measure)?;
7496                let unit = units.get(unit_name).ok()?;
7497                unit.literal_factor().cloned()
7498            }
7499            UnitFactorSource::Runtime { scales, fallback } => scales
7500                .get(unit_name)
7501                .cloned()
7502                .or_else(|| fallback.try_measure_unit_factor(unit_name)),
7503        }
7504    }
7505
7506    fn ratio_unit_factor(&self, unit_name: &str) -> &crate::computation::rational::RationalInteger {
7507        match self {
7508            UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.ratio_unit_factor(unit_name),
7509            UnitFactorSource::Merged { ratio, .. } => {
7510                let units = ratio.unwrap_or_else(|| {
7511                    panic!(
7512                        "BUG: family ratio expansion missing merged table for unit '{unit_name}'"
7513                    )
7514                });
7515                &units
7516                    .get(unit_name)
7517                    .unwrap_or_else(|_| {
7518                        panic!("BUG: family unit '{unit_name}' missing from merged ratio table")
7519                    })
7520                    .value
7521            }
7522            UnitFactorSource::Runtime { fallback, .. } => fallback.ratio_unit_factor(unit_name),
7523        }
7524    }
7525}
7526
7527/// Primitive ratio type for API unit-map conversion on ratio range wrappers.
7528pub(crate) fn ratio_element_type_for_api(lemma_type: &LemmaType) -> LemmaType {
7529    match &lemma_type.specifications {
7530        TypeSpecification::Ratio { .. } => lemma_type.clone(),
7531        TypeSpecification::RatioRange { .. } => {
7532            let element = range_element_type_specification(&lemma_type.specifications)
7533                .expect("BUG: ratio range type must have ratio element specification");
7534            let TypeSpecification::Ratio { units, .. } = element else {
7535                panic!("BUG: ratio range element spec must be Ratio");
7536            };
7537            LemmaType::primitive(TypeSpecification::Ratio {
7538                minimum: None,
7539                maximum: None,
7540                units,
7541                unit: None,
7542                help: String::new(),
7543            })
7544        }
7545        _ => panic!(
7546            "BUG: ratio_element_type_for_api called with non-ratio type {}",
7547            lemma_type.name()
7548        ),
7549    }
7550}
7551
7552/// Failure while converting a literal to decimal strings across declared units.
7553#[derive(Debug, Clone, PartialEq, Eq)]
7554pub(crate) enum LiteralUnitMapFailure {
7555    Commit(crate::computation::rational::NumericFailure),
7556    UnitConversion(crate::computation::rational::NumericFailure),
7557}
7558
7559/// Runtime literal payload. Type lives on `NormalForm.result_type` / `DataDefinition` /
7560/// explicit computation parameters — not on the value.
7561#[derive(Clone, Debug, PartialEq, Eq, Hash)]
7562pub struct LiteralValue {
7563    pub value: ValueKind,
7564}
7565
7566impl LiteralValue {
7567    #[inline]
7568    pub fn new(value: ValueKind) -> Self {
7569        Self { value }
7570    }
7571
7572    fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
7573        match signature {
7574            [(unit_name, 1)] => Some(unit_name.as_str()),
7575            _ => None,
7576        }
7577    }
7578}
7579
7580/// Planning-time literal: value plus resolved type.
7581///
7582/// Used by `ExpressionKind::Literal` and coerce/typing paths. Runtime
7583/// `OperationResult` / value-table slots store bare [`LiteralValue`]; the type
7584/// is on the node (`NormalForm.result_type`) or data definition.
7585#[derive(Clone, Debug, PartialEq, Eq, Hash)]
7586pub struct TypedLiteral {
7587    pub value: ValueKind,
7588    pub lemma_type: Arc<LemmaType>,
7589}
7590
7591impl TypedLiteral {
7592    #[inline]
7593    pub fn to_literal(&self) -> LiteralValue {
7594        LiteralValue {
7595            value: self.value.clone(),
7596        }
7597    }
7598
7599    #[inline]
7600    pub fn into_literal(self) -> LiteralValue {
7601        LiteralValue { value: self.value }
7602    }
7603}
7604
7605impl From<TypedLiteral> for LiteralValue {
7606    fn from(typed: TypedLiteral) -> Self {
7607        typed.into_literal()
7608    }
7609}
7610
7611impl From<&TypedLiteral> for LiteralValue {
7612    fn from(typed: &TypedLiteral) -> Self {
7613        typed.to_literal()
7614    }
7615}
7616
7617impl LiteralValue {
7618    pub fn text(s: impl Into<Arc<str>>) -> Self {
7619        Self {
7620            value: ValueKind::Text(s.into()),
7621        }
7622    }
7623
7624    pub fn number(n: RationalInteger) -> Self {
7625        Self {
7626            value: ValueKind::Number(n),
7627        }
7628    }
7629
7630    pub fn number_from_decimal(decimal: Decimal) -> Self {
7631        Self::number(
7632            crate::literals::rational_from_parsed_decimal(decimal)
7633                .expect("BUG: literal number from decimal must lift at boundary"),
7634        )
7635    }
7636
7637    pub fn measure(n: RationalInteger) -> Self {
7638        Self {
7639            value: ValueKind::Measure(n),
7640        }
7641    }
7642
7643    pub fn from_bool(b: bool) -> Self {
7644        Self {
7645            value: ValueKind::Boolean(b),
7646        }
7647    }
7648
7649    pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
7650        Self::date(date_time_to_semantic(dt))
7651    }
7652
7653    pub fn date(dt: SemanticDateTime) -> Self {
7654        Self {
7655            value: ValueKind::Date(dt),
7656        }
7657    }
7658
7659    pub fn time(t: SemanticTime) -> Self {
7660        Self {
7661            value: ValueKind::Time(t),
7662        }
7663    }
7664
7665    pub fn ratio(r: RationalInteger) -> Self {
7666        Self {
7667            value: ValueKind::Ratio(r),
7668        }
7669    }
7670
7671    pub fn ratio_from_decimal(r: Decimal) -> Self {
7672        Self::ratio(
7673            crate::literals::rational_from_parsed_decimal(r)
7674                .expect("BUG: ratio literal from decimal must lift at boundary"),
7675        )
7676    }
7677
7678    pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
7679        Self {
7680            value: ValueKind::Range(Box::new(left), Box::new(right)),
7681        }
7682    }
7683
7684    /// Display without an explicit type. Measure/ratio fall back to magnitude-only.
7685    pub fn display_value(&self) -> String {
7686        match &self.value {
7687            ValueKind::Measure(_) | ValueKind::Ratio(_) | ValueKind::UnscaledMeasure { .. } => {
7688                format!("{}", self.value)
7689            }
7690            ValueKind::Range(left, right) => {
7691                format!("{}...{}", left.display_value(), right.display_value())
7692            }
7693            ValueKind::List(rows) if rows.is_empty() => String::new(),
7694            ValueKind::List(rows) => rows
7695                .iter()
7696                .map(|row| row.display_value())
7697                .collect::<Vec<_>>()
7698                .join(", "),
7699            _ => format!("{}", self.value),
7700        }
7701    }
7702
7703    /// Display string given an explicit type (measure/ratio need unit identity).
7704    pub fn display_value_with_type(&self, lemma_type: &LemmaType) -> String {
7705        self.display_value_with_scales(lemma_type, None)
7706    }
7707
7708    /// Display string given an explicit type and settled reference-scale factors.
7709    pub fn display_value_with_scales(
7710        &self,
7711        lemma_type: &LemmaType,
7712        runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
7713    ) -> String {
7714        match &self.value {
7715            ValueKind::Measure(n) => {
7716                let signature = lemma_type.measure_runtime_signature();
7717                format_measure_canonical_for_display(n, lemma_type, &signature, runtime_scales)
7718            }
7719            ValueKind::UnscaledMeasure { magnitude, unit } => {
7720                format!(
7721                    "{} {unit}",
7722                    format_rational_magnitude_for_display(magnitude)
7723                )
7724            }
7725            ValueKind::Ratio(n) => format_ratio_canonical_for_display(n, lemma_type),
7726            ValueKind::Range(left, right) => {
7727                format!(
7728                    "{}...{}",
7729                    left.to_literal()
7730                        .display_value_with_scales(&left.lemma_type, runtime_scales),
7731                    right
7732                        .to_literal()
7733                        .display_value_with_scales(&right.lemma_type, runtime_scales)
7734                )
7735            }
7736            ValueKind::List(rows) if rows.is_empty() => lemma_type.specifications.to_string(),
7737            ValueKind::List(rows) => rows
7738                .iter()
7739                .map(|row| {
7740                    row.to_literal()
7741                        .display_value_with_scales(&row.lemma_type, runtime_scales)
7742                })
7743                .collect::<Vec<_>>()
7744                .join(", "),
7745            _ => format!("{}", self.value),
7746        }
7747    }
7748
7749    /// Structural byte-size estimate for resource limit checks.
7750    pub fn byte_size(&self) -> usize {
7751        self.value.structural_byte_size()
7752    }
7753
7754    /// Magnitude for decimal input prompts (API [`Decimal`]; format at call site).
7755    #[must_use]
7756    pub fn magnitude_suggestion_for_decimal_prompt(
7757        &self,
7758        lemma_type: &LemmaType,
7759    ) -> Option<rust_decimal::Decimal> {
7760        match &self.value {
7761            ValueKind::Number(n) => Some(
7762                lemma_type
7763                    .try_rational_as_decimal(n)
7764                    .expect("BUG: stored number literal must convert to decimal for prompt"),
7765            ),
7766            ValueKind::Measure(n) => {
7767                let signature = lemma_type.measure_runtime_signature();
7768                let unit_name = Self::single_measure_signature_unit_name(&signature).expect(
7769                    "BUG: measure prompt requires exactly one signature unit with exponent 1",
7770                );
7771                Some(
7772                    lemma_type
7773                        .try_measure_canonical_as_decimal_in_unit(n, unit_name)
7774                        .expect("BUG: stored measure literal must convert to decimal for prompt"),
7775                )
7776            }
7777            ValueKind::Ratio(n) => {
7778                if let Some(unit_name) = lemma_type.unit() {
7779                    Some(
7780                        lemma_type
7781                            .try_ratio_canonical_as_decimal_in_unit(n, unit_name)
7782                            .expect("BUG: stored ratio literal must convert to decimal for prompt"),
7783                    )
7784                } else {
7785                    Some(lemma_type.try_rational_as_decimal(n).expect(
7786                        "BUG: stored bare ratio literal must convert to decimal for prompt",
7787                    ))
7788                }
7789            }
7790            _ => None,
7791        }
7792    }
7793
7794    /// Per-unit magnitudes when this literal is a measure with declared units.
7795    #[must_use]
7796    pub fn measure_units(
7797        &self,
7798        lemma_type: &LemmaType,
7799    ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
7800        if !matches!(self.value, ValueKind::Measure(_)) {
7801            return None;
7802        }
7803        lemma_type.measure_unit_names()?;
7804        let declared: Vec<&str> = lemma_type
7805            .measure_unit_names()
7806            .expect("BUG: measure_unit_names checked above");
7807        lemma_type
7808            .measure_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
7809            .ok()
7810    }
7811
7812    /// Per-unit magnitudes when this literal is a ratio with declared units.
7813    #[must_use]
7814    pub fn ratio_units(
7815        &self,
7816        lemma_type: &LemmaType,
7817    ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
7818        if !matches!(self.value, ValueKind::Ratio(_)) {
7819            return None;
7820        }
7821        let has_declared_units = match &lemma_type.specifications {
7822            TypeSpecification::Ratio { units, .. } => !units.is_empty(),
7823            TypeSpecification::RatioRange { .. } => true,
7824            _ => return None,
7825        };
7826        if !has_declared_units {
7827            return None;
7828        }
7829        let declared: Vec<&str> = lemma_type
7830            .ratio_unit_names()
7831            .expect("BUG: ratio units checked above");
7832        lemma_type
7833            .ratio_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
7834            .ok()
7835    }
7836
7837    /// Magnitude in a declared unit when this literal is measure or ratio.
7838    #[must_use]
7839    pub fn magnitude_in_unit(
7840        &self,
7841        lemma_type: &LemmaType,
7842        unit: &str,
7843    ) -> Option<rust_decimal::Decimal> {
7844        self.measure_units(lemma_type)
7845            .and_then(|map| map.get(unit).copied())
7846            .or_else(|| {
7847                self.ratio_units(lemma_type)
7848                    .and_then(|map| map.get(unit).copied())
7849            })
7850    }
7851
7852    /// Derive second from a duration measure's canonical magnitude.
7853    pub fn duration_canonical_seconds(&self, lemma_type: &LemmaType) -> RationalInteger {
7854        let ValueKind::Measure(magnitude) = &self.value else {
7855            unreachable!(
7856                "BUG: duration_canonical_seconds called with {:?}",
7857                self.value
7858            );
7859        };
7860        if !lemma_type.is_duration_like_measure() {
7861            unreachable!(
7862                "BUG: duration_canonical_seconds called with type {}",
7863                lemma_type.name()
7864            );
7865        }
7866        let factor = lemma_type.measure_unit_factor("second");
7867        checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
7868    }
7869
7870    /// Derive month from a calendar measure's canonical magnitude.
7871    pub fn calendar_canonical_months(&self, lemma_type: &LemmaType) -> RationalInteger {
7872        let ValueKind::Measure(magnitude) = &self.value else {
7873            unreachable!(
7874                "BUG: calendar_canonical_months called with {:?}",
7875                self.value
7876            );
7877        };
7878        if !lemma_type.is_calendar_like() {
7879            unreachable!(
7880                "BUG: calendar_canonical_months called with type {}",
7881                lemma_type.name()
7882            );
7883        }
7884        let factor = lemma_type.measure_unit_factor("month");
7885        checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
7886    }
7887}
7888
7889impl TypedLiteral {
7890    pub fn text(s: impl Into<Arc<str>>) -> Self {
7891        Self {
7892            value: ValueKind::Text(s.into()),
7893            lemma_type: primitive_text_arc().clone(),
7894        }
7895    }
7896
7897    pub fn text_with_type(s: impl Into<Arc<str>>, lemma_type: Arc<LemmaType>) -> Self {
7898        Self {
7899            value: ValueKind::Text(s.into()),
7900            lemma_type,
7901        }
7902    }
7903
7904    pub fn number(n: RationalInteger) -> Self {
7905        Self {
7906            value: ValueKind::Number(n),
7907            lemma_type: primitive_number_arc().clone(),
7908        }
7909    }
7910
7911    pub fn number_from_decimal(decimal: Decimal) -> Self {
7912        Self::number(
7913            crate::literals::rational_from_parsed_decimal(decimal)
7914                .expect("BUG: literal number from decimal must lift at boundary"),
7915        )
7916    }
7917
7918    pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
7919        Self {
7920            value: ValueKind::Number(n),
7921            lemma_type,
7922        }
7923    }
7924
7925    pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
7926        Self::number_with_type(
7927            crate::literals::rational_from_parsed_decimal(decimal)
7928                .expect("BUG: literal number from decimal must lift at boundary"),
7929            lemma_type,
7930        )
7931    }
7932
7933    /// Build a Measure literal bound to a specific declared unit.
7934    pub fn measure_with_bound_unit(
7935        n: RationalInteger,
7936        unit_name: impl Into<String>,
7937        lemma_type: Arc<LemmaType>,
7938    ) -> Self {
7939        Self::measure_with_type(
7940            n,
7941            Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
7942        )
7943    }
7944
7945    pub fn measure_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
7946        Self {
7947            value: ValueKind::Measure(n),
7948            lemma_type,
7949        }
7950    }
7951
7952    pub fn measure_with_signature(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
7953        Self {
7954            value: ValueKind::Measure(n),
7955            lemma_type,
7956        }
7957    }
7958
7959    /// Unit literal outside a measure type context: no family, so no dimensions.
7960    pub fn number_interpreted_as_measure(value: RationalInteger) -> Self {
7961        Self {
7962            value: ValueKind::Measure(value),
7963            lemma_type: Arc::new(anonymous_measure_type()),
7964        }
7965    }
7966
7967    pub fn from_bool(b: bool) -> Self {
7968        Self {
7969            value: ValueKind::Boolean(b),
7970            lemma_type: primitive_boolean_arc().clone(),
7971        }
7972    }
7973
7974    pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
7975        Self::date(date_time_to_semantic(dt))
7976    }
7977
7978    pub fn date(dt: SemanticDateTime) -> Self {
7979        Self {
7980            value: ValueKind::Date(dt),
7981            lemma_type: primitive_date_arc().clone(),
7982        }
7983    }
7984
7985    pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
7986        Self {
7987            value: ValueKind::Date(dt),
7988            lemma_type,
7989        }
7990    }
7991
7992    pub fn time(t: SemanticTime) -> Self {
7993        Self {
7994            value: ValueKind::Time(t),
7995            lemma_type: primitive_time_arc().clone(),
7996        }
7997    }
7998
7999    pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
8000        Self {
8001            value: ValueKind::Time(t),
8002            lemma_type,
8003        }
8004    }
8005
8006    pub fn calendar(
8007        value: RationalInteger,
8008        unit: SemanticCalendarUnit,
8009        lemma_type: Arc<LemmaType>,
8010    ) -> Self {
8011        let unit_name = unit.to_string();
8012        debug_assert_eq!(
8013            semantic_calendar_unit_from_unit_name(&unit_name),
8014            unit,
8015            "BUG: calendar unit name must round-trip"
8016        );
8017        Self::measure_with_bound_unit(value, unit_name, lemma_type)
8018    }
8019
8020    pub fn calendar_from_decimal(
8021        value: Decimal,
8022        unit: SemanticCalendarUnit,
8023        lemma_type: Arc<LemmaType>,
8024    ) -> Self {
8025        Self::calendar(
8026            crate::literals::rational_from_parsed_decimal(value)
8027                .expect("BUG: calendar literal from decimal must lift at boundary"),
8028            unit,
8029            lemma_type,
8030        )
8031    }
8032
8033    pub fn calendar_with_type(
8034        value: RationalInteger,
8035        unit: SemanticCalendarUnit,
8036        lemma_type: Arc<LemmaType>,
8037    ) -> Self {
8038        Self::calendar(value, unit, lemma_type)
8039    }
8040
8041    pub fn ratio(r: RationalInteger) -> Self {
8042        Self {
8043            value: ValueKind::Ratio(r),
8044            lemma_type: primitive_ratio_arc().clone(),
8045        }
8046    }
8047
8048    pub fn ratio_from_decimal(r: Decimal) -> Self {
8049        Self::ratio(
8050            crate::literals::rational_from_parsed_decimal(r)
8051                .expect("BUG: ratio literal from decimal must lift at boundary"),
8052        )
8053    }
8054
8055    pub fn ratio_with_type(r: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
8056        Self {
8057            value: ValueKind::Ratio(r),
8058            lemma_type,
8059        }
8060    }
8061
8062    pub fn ratio_with_bound_unit(
8063        r: RationalInteger,
8064        unit_name: impl Into<String>,
8065        lemma_type: Arc<LemmaType>,
8066    ) -> Self {
8067        Self::ratio_with_type(
8068            r,
8069            Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
8070        )
8071    }
8072
8073    pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
8074        let specifications =
8075            range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
8076                .unwrap_or_else(|| {
8077                    unreachable!(
8078                "BUG: attempted to construct a range literal from incompatible endpoint types"
8079            )
8080                });
8081
8082        Self {
8083            value: ValueKind::Range(Box::new(left), Box::new(right)),
8084            lemma_type: Arc::new(LemmaType::primitive(specifications)),
8085        }
8086    }
8087
8088    pub fn display_value(&self) -> String {
8089        self.to_literal().display_value_with_type(&self.lemma_type)
8090    }
8091
8092    pub fn get_type(&self) -> &LemmaType {
8093        &self.lemma_type
8094    }
8095
8096    pub fn byte_size(&self) -> usize {
8097        self.value.structural_byte_size()
8098    }
8099}
8100
8101impl fmt::Display for TypedLiteral {
8102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8103        write!(f, "{}", self.display_value())
8104    }
8105}
8106
8107/// What a [`DataDefinition::Reference`] copies its value from: either another data path
8108/// or a rule whose result becomes this data's value.
8109#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8110pub enum ReferenceTarget {
8111    Data(DataPath),
8112    Rule(RulePath),
8113}
8114
8115impl fmt::Display for ReferenceTarget {
8116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8117        match self {
8118            ReferenceTarget::Data(path) => write!(f, "{path}"),
8119            ReferenceTarget::Rule(path) => write!(f, "{path}"),
8120        }
8121    }
8122}
8123
8124/// Where a [`DataDefinition::Reference`] chain ends. Computed once per slice in
8125/// [`crate::planning::graph::Graph::validate`] after `compute_data_reference_order`
8126/// rejected cycles.
8127#[derive(Clone, Debug, PartialEq, Eq)]
8128pub(crate) enum ReferenceEnd {
8129    Promptable(DataPath),
8130    Rule(RulePath),
8131}
8132
8133/// Who wrote a data slot's definition.
8134///
8135/// A slot declared by its own spec is a default: evaluation data may replace it. A
8136/// slot bound by a consumer (`uses x -> with field: value`, a path fill, or a row
8137/// field written in a data list) is final: supplying it at evaluate is a request
8138/// [`crate::Error`], and it is not a promptable input.
8139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8140pub enum DataOrigin {
8141    Declared,
8142    Bound,
8143}
8144
8145/// Date-typed local that a dynamic `uses … -> effective` import reads at evaluation.
8146#[derive(Clone, Debug, PartialEq, Eq, Hash)]
8147pub enum ImportEffectivePin {
8148    Data(DataPath),
8149    Rule(RulePath),
8150}
8151
8152/// Resolved data value for the execution plan: aligned with [`DataValue`] but with source per variant.
8153#[derive(Clone, Debug)]
8154pub enum DataDefinition {
8155    /// Value-holding data: current literal (spec default or `with` binding).
8156    Value {
8157        value: LiteralValue,
8158        resolved_type: Arc<LemmaType>,
8159        origin: DataOrigin,
8160        source: Source,
8161    },
8162    /// Type-only data: type known, value to be supplied (e.g. via run data).
8163    /// `declared_suggestion` carries the `-> suggest ...` payload for this binding or
8164    /// the suggestion inherited from the parent type chain, if any; value-promoting code
8165    /// uses it instead of re-deriving suggestions from [`TypeSpecification`].
8166    /// The evaluator never commits a suggestion — unbound stays MissingData.
8167    TypeDeclaration {
8168        resolved_type: Arc<LemmaType>,
8169        declared_suggestion: Option<BoundValueKind>,
8170        declared_value: Option<BoundValueKind>,
8171        source: Source,
8172    },
8173    /// Import (`uses`): a spec instance named `alias` in this plan, a row over the
8174    /// imported spec's template plan. Its bindings are the `-> with` / path-fill fields
8175    /// the declaring spec wrote; the imported data and rules are read through the
8176    /// template in the instance's context.
8177    ///
8178    /// When `pin` is `None`, `row` is the sole body. When `pin` is `Some` (`-> effective`),
8179    /// `candidates` holds every possible body and evaluation selects among them; `row`
8180    /// then equals an arbitrary candidate and must not be read without selection.
8181    Import {
8182        row: Arc<RowInstance>,
8183        /// Planned path of the `-> effective` date when present.
8184        pin: Option<ImportEffectivePin>,
8185        /// Bound rows keyed by identity when `pin` is set; empty for static imports.
8186        candidates: std::collections::BTreeMap<SpecIdentity, Arc<RowInstance>>,
8187        source: Source,
8188    },
8189    /// Instance list (`data x: spec list`). `resolved_type` is the list type.
8190    /// Rows written in the spec are instances of this plan under the list path
8191    /// ([`crate::planning::execution_plan::ExecutionPlan::instance_lists`]);
8192    /// evaluation lists them unless run data replaces the list.
8193    InstanceList {
8194        resolved_type: Arc<LemmaType>,
8195        origin: DataOrigin,
8196        source: Source,
8197    },
8198    /// Value-copy reference to another data or a rule result.
8199    ///
8200    /// `resolved_type` is the merged type that the copied value must satisfy at
8201    /// evaluation time. Merging folds together: (1) the LHS's own declared type,
8202    /// if any; (2) the target's type (data declared type or rule return type);
8203    /// (3) any `local_constraints` written after the `->` on the reference itself.
8204    /// Merging happens in a dedicated pass once all data and rule types are
8205    /// known; before that pass, `resolved_type` holds a provisional value and
8206    /// must not be consumed for type checking.
8207    ///
8208    /// `local_constraints` preserves the raw constraint list from the reference's
8209    /// `-> ...` tail (e.g. `minimum 5` in `data license2: law.other -> minimum 5`)
8210    /// for that merging pass. It is `None` when the reference has no trailing
8211    /// constraints.
8212    ///
8213    /// `local_suggestion` carries any `suggest <value>` constraint from the
8214    /// reference's `-> ...` tail. The reference-merge pass extracts it from the
8215    /// constraint list during type resolution. It is a UI hint only
8216    /// ([`Self::suggestion`] / show); the evaluator never commits it — unbound
8217    /// stays MissingData.
8218    ///
8219    /// The reference itself is evaluated by copying the target's value (data path)
8220    /// or the target rule's result in topological order. Evaluate data for this
8221    /// path binds via `resolved_type` like a type declaration.
8222    Reference {
8223        target: ReferenceTarget,
8224        resolved_type: Arc<LemmaType>,
8225        local_constraints: Option<Vec<Constraint>>,
8226        local_suggestion: Option<BoundValueKind>,
8227        local_value: Option<BoundValueKind>,
8228        origin: DataOrigin,
8229        source: Source,
8230    },
8231}
8232
8233impl DataDefinition {
8234    /// Whether a consumer bound this slot (`uses -> with`, path fill, or a row field
8235    /// written in a data list). Bound slots take no evaluation data.
8236    pub fn is_bound(&self) -> bool {
8237        match self {
8238            DataDefinition::Value { origin, .. }
8239            | DataDefinition::InstanceList { origin, .. }
8240            | DataDefinition::Reference { origin, .. } => *origin == DataOrigin::Bound,
8241            DataDefinition::TypeDeclaration { .. } | DataDefinition::Import { .. } => false,
8242        }
8243    }
8244
8245    /// Declared lemma type for value, type-declaration, and reference data; `None` for imports.
8246    pub fn lemma_type(&self) -> Option<&LemmaType> {
8247        self.resolved_type_arc().map(|arc| arc.as_ref())
8248    }
8249
8250    /// Shared [`Arc`] for the declared type; `None` for imports.
8251    pub fn resolved_type_arc(&self) -> Option<&Arc<LemmaType>> {
8252        match self {
8253            DataDefinition::Value { resolved_type, .. } => Some(resolved_type),
8254            DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
8255            DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
8256            DataDefinition::InstanceList { resolved_type, .. } => Some(resolved_type),
8257            DataDefinition::Import { .. } => None,
8258        }
8259    }
8260
8261    /// Alias for [`Self::lemma_type`] (historical name).
8262    #[inline]
8263    pub fn schema_type(&self) -> Option<&LemmaType> {
8264        self.lemma_type()
8265    }
8266
8267    /// Returns the literal value when the data already holds one, including `-> with`
8268    /// on a type declaration or reference. A data-target `Reference`'s copied value
8269    /// is produced by the evaluator at runtime, so at plan-time it has no value yet.
8270    pub fn value(&self) -> Option<LiteralValue> {
8271        match self {
8272            DataDefinition::Value { value, .. } => Some(value.clone()),
8273            DataDefinition::TypeDeclaration {
8274                declared_value: Some(dv),
8275                ..
8276            } => Some(dv.to_literal()),
8277            DataDefinition::Reference {
8278                local_value: Some(dv),
8279                resolved_type: _,
8280                ..
8281            } => Some(dv.to_literal()),
8282            DataDefinition::TypeDeclaration { .. }
8283            | DataDefinition::Import { .. }
8284            | DataDefinition::InstanceList { .. }
8285            | DataDefinition::Reference { .. } => None,
8286        }
8287    }
8288
8289    /// Fill value with optional written measure unit binding (Show display).
8290    pub(crate) fn bound_value(&self) -> Option<&BoundValueKind> {
8291        match self {
8292            DataDefinition::TypeDeclaration {
8293                declared_value: Some(dv),
8294                ..
8295            }
8296            | DataDefinition::Reference {
8297                local_value: Some(dv),
8298                ..
8299            } => Some(dv),
8300            DataDefinition::Value { .. }
8301            | DataDefinition::TypeDeclaration { .. }
8302            | DataDefinition::Reference { .. }
8303            | DataDefinition::InstanceList { .. }
8304            | DataDefinition::Import { .. } => None,
8305        }
8306    }
8307
8308    /// Suggestion with optional written measure unit binding (Show display).
8309    /// Surfaces in [`crate::planning::execution_plan::ShowData::suggestion`] for
8310    /// show/response/UI; the evaluator never commits it — unbound stays MissingData.
8311    pub(crate) fn bound_suggestion(&self) -> Option<&BoundValueKind> {
8312        match self {
8313            DataDefinition::TypeDeclaration {
8314                declared_suggestion: Some(dv),
8315                ..
8316            }
8317            | DataDefinition::Reference {
8318                local_suggestion: Some(dv),
8319                ..
8320            } => Some(dv),
8321            DataDefinition::Value { .. }
8322            | DataDefinition::TypeDeclaration { .. }
8323            | DataDefinition::Reference { .. }
8324            | DataDefinition::InstanceList { .. }
8325            | DataDefinition::Import { .. } => None,
8326        }
8327    }
8328
8329    /// Returns the source location for this data.
8330    pub fn source(&self) -> &Source {
8331        match self {
8332            DataDefinition::Value { source, .. } => source,
8333            DataDefinition::TypeDeclaration { source, .. } => source,
8334            DataDefinition::Import { source, .. } => source,
8335            DataDefinition::InstanceList { source, .. } => source,
8336            DataDefinition::Reference { source, .. } => source,
8337        }
8338    }
8339
8340    /// The sole row of a static (`pin` is `None`) import.
8341    pub fn import_sole_row(&self) -> &Arc<RowInstance> {
8342        let DataDefinition::Import { pin, row, .. } = self else {
8343            panic!("BUG: import_sole_row on non-import data");
8344        };
8345        assert!(
8346            pin.is_none(),
8347            "BUG: dynamic import needs pin selection before reading a row"
8348        );
8349        row
8350    }
8351
8352    /// Candidate rows of a dynamic import (`pin` is set).
8353    pub fn import_candidates(&self) -> &std::collections::BTreeMap<SpecIdentity, Arc<RowInstance>> {
8354        let DataDefinition::Import {
8355            pin, candidates, ..
8356        } = self
8357        else {
8358            panic!("BUG: import_candidates on non-import data");
8359        };
8360        assert!(pin.is_some(), "BUG: static import has no candidate map");
8361        candidates
8362    }
8363
8364    /// Whether this import selects its body from a `-> effective` pin at evaluation.
8365    pub fn import_pin(&self) -> Option<&ImportEffectivePin> {
8366        match self {
8367            DataDefinition::Import { pin, .. } => pin.as_ref(),
8368            _ => None,
8369        }
8370    }
8371
8372    /// Row of a dynamic import for `identity`, or the sole static row when `pin` is `None`.
8373    pub fn import_row_for(&self, identity: &SpecIdentity) -> &Arc<RowInstance> {
8374        let DataDefinition::Import {
8375            pin,
8376            row,
8377            candidates,
8378            ..
8379        } = self
8380        else {
8381            panic!("BUG: import_row_for on non-import data");
8382        };
8383        if pin.is_none() {
8384            return row;
8385        }
8386        candidates
8387            .get(identity)
8388            .unwrap_or_else(|| panic!("BUG: dynamic import has no row for identity '{identity}'"))
8389    }
8390}
8391
8392/// Bind a type-agnostic [`Value::NumberWithUnit`] using the unit index entry for `unit_name`.
8393pub fn number_with_unit_to_value_kind(
8394    magnitude: rust_decimal::Decimal,
8395    unit_name: &str,
8396    lemma_type: &LemmaType,
8397) -> Result<ValueKind, String> {
8398    match &lemma_type.specifications {
8399        TypeSpecification::Ratio { units, .. } => {
8400            use crate::computation::rational::{checked_div, decimal_to_rational};
8401            let unit = units.get(unit_name)?;
8402            let magnitude_rational = decimal_to_rational(magnitude)
8403                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
8404            let canonical_rational = checked_div(&magnitude_rational, &unit.value)
8405                .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
8406            Ok(ValueKind::Ratio(canonical_rational))
8407        }
8408        TypeSpecification::Measure { units, .. } => {
8409            use crate::computation::rational::checked_mul;
8410            let rational = lift_parser_decimal(magnitude)?;
8411            let unit = units.get(unit_name)?;
8412            match unit.literal_factor() {
8413                Some(factor) => {
8414                    let canonical = checked_mul(&rational, factor).map_err(|failure| {
8415                        format!("measure canonicalization overflow: {failure}")
8416                    })?;
8417                    Ok(ValueKind::Measure(canonical))
8418                }
8419                None => Ok(ValueKind::UnscaledMeasure {
8420                    magnitude: rational,
8421                    unit: std::sync::Arc::from(unit_name),
8422                }),
8423            }
8424        }
8425        _ => Err(format!(
8426            "Unit '{}' is defined on type '{}' which is not measure or ratio",
8427            unit_name,
8428            lemma_type.name()
8429        )),
8430    }
8431}
8432
8433/// Whether a [`ValueKind`] is structurally compatible with a [`TypeSpecification`].
8434/// Bound validation (min/max/decimals) is separate; this only checks shape.
8435pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
8436    matches!(
8437        (type_spec, value),
8438        (TypeSpecification::Number { .. }, ValueKind::Number(_))
8439            | (TypeSpecification::Text { .. }, ValueKind::Text(_))
8440            | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
8441            | (TypeSpecification::Date { .. }, ValueKind::Date(_))
8442            | (TypeSpecification::Time { .. }, ValueKind::Time(_))
8443            | (TypeSpecification::Measure { .. }, ValueKind::Measure(_))
8444            | (
8445                TypeSpecification::Measure { .. },
8446                ValueKind::UnscaledMeasure { .. }
8447            )
8448            | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_))
8449            | (
8450                TypeSpecification::NumberRange { .. },
8451                ValueKind::Range(_, _)
8452            )
8453            | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
8454            | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
8455            | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
8456            | (
8457                TypeSpecification::MeasureRange { .. },
8458                ValueKind::Range(_, _)
8459            )
8460            | (TypeSpecification::Veto { .. }, _)
8461            | (TypeSpecification::Undetermined, _)
8462            | (TypeSpecification::Spec { .. }, ValueKind::Spec(_))
8463            | (TypeSpecification::List { .. }, ValueKind::List(_))
8464    )
8465}
8466
8467fn parser_value_type_mismatch(
8468    value: &crate::literals::Value,
8469    type_spec: &TypeSpecification,
8470) -> String {
8471    use crate::parsing::ast::AsLemmaSource;
8472    let value_str = format!("{}", AsLemmaSource(value));
8473    match type_spec {
8474        TypeSpecification::Measure { units, .. } => {
8475            let unit_hint = units
8476                .iter()
8477                .find(|u| u.is_canonical_factor())
8478                .map(|u| u.name.as_str())
8479                .or_else(|| units.iter().next().map(|u| u.name.as_str()))
8480                .unwrap_or("unit");
8481            format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
8482        }
8483        TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
8484            let unit_hint = units
8485                .iter()
8486                .next()
8487                .map(|u| u.name.as_str())
8488                .unwrap_or("unit");
8489            format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
8490        }
8491        _ => format!("cannot use {value_str} as {type_spec}"),
8492    }
8493}
8494
8495/// Re-canonicalize a measure literal after compound unit factors were resolved.
8496///
8497/// Literals parsed before derived unit resolution were canonicalized with prefix-only
8498/// factors; multiply by `resolved_factor / stored_factor` to align with final factors.
8499pub fn refresh_measure_literal_canonical_magnitude(
8500    lit: &mut LiteralValue,
8501    previous_type: &LemmaType,
8502    resolved_type: &LemmaType,
8503) -> Result<(), crate::computation::rational::NumericFailure> {
8504    use crate::computation::rational::{checked_div, checked_mul};
8505    let ValueKind::Measure(magnitude) = &mut lit.value else {
8506        if matches!(lit.value, ValueKind::UnscaledMeasure { .. }) {
8507            return Ok(());
8508        }
8509        return Ok(());
8510    };
8511    // Binding unit is no longer on ValueKind; magnitude was canonicalized at bind time
8512    // against the then-current unit table. When a single-term runtime signature unit
8513    // exists on both tables, rescale if that unit's factor changed.
8514    let signature = previous_type.measure_runtime_signature();
8515    let Some((unit_name, 1)) = signature.first().map(|(n, e)| (n.as_str(), *e)) else {
8516        return Ok(());
8517    };
8518    if signature.len() != 1 {
8519        return Ok(());
8520    }
8521    let stored_factor = previous_type.measure_unit_factor(unit_name);
8522    let resolved_factor = resolved_type.measure_unit_factor(unit_name);
8523    if stored_factor == resolved_factor {
8524        return Ok(());
8525    }
8526    let scaled = checked_mul(magnitude, resolved_factor)?;
8527    *magnitude = checked_div(&scaled, stored_factor)?;
8528    Ok(())
8529}
8530
8531fn number_with_unit_value_kind(
8532    magnitude: rust_decimal::Decimal,
8533    unit_name: &str,
8534    type_spec: &TypeSpecification,
8535) -> Result<ValueKind, String> {
8536    use crate::computation::rational::{checked_div, checked_mul, decimal_to_rational};
8537    match type_spec {
8538        TypeSpecification::Ratio { units, .. } => {
8539            let unit = units.get(unit_name)?;
8540            let magnitude_rational = decimal_to_rational(magnitude)
8541                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
8542            let canonical_rational = checked_div(&magnitude_rational, &unit.value)
8543                .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
8544            Ok(ValueKind::Ratio(canonical_rational))
8545        }
8546        TypeSpecification::Measure { units, .. } => {
8547            let rational = lift_parser_decimal(magnitude)?;
8548            let unit = units.get(unit_name)?;
8549            match unit.literal_factor() {
8550                Some(factor) => {
8551                    let canonical = checked_mul(&rational, factor).map_err(|failure| {
8552                        format!("measure canonicalization overflow: {failure}")
8553                    })?;
8554                    Ok(ValueKind::Measure(canonical))
8555                }
8556                None => Ok(ValueKind::UnscaledMeasure {
8557                    magnitude: rational,
8558                    unit: Arc::from(unit_name),
8559                }),
8560            }
8561        }
8562        _ => Err("number_with_unit literal requires a measure or ratio type".to_string()),
8563    }
8564}
8565
8566/// Convert a non-range parser [`Value`] to [`ValueKind`] using the target type.
8567///
8568/// Range values must go through [`parser_value_to_value_kind`] so endpoints use
8569/// [`lift_range_endpoint`]. Kept separate so that helper cannot re-enter the Range arm.
8570fn parser_scalar_to_value_kind(
8571    value: &crate::literals::Value,
8572    type_spec: &TypeSpecification,
8573) -> Result<ValueKind, String> {
8574    use crate::literals::Value;
8575    match (value, type_spec) {
8576        (Value::NumberWithUnit(magnitude, unit_name), _) => {
8577            number_with_unit_value_kind(*magnitude, unit_name, type_spec)
8578        }
8579        (Value::Number(n), TypeSpecification::Number { .. }) => {
8580            Ok(ValueKind::Number(lift_parser_decimal(*n)?))
8581        }
8582        (Value::Text(s), TypeSpecification::Text { .. }) => {
8583            Ok(ValueKind::Text(Arc::from(s.as_str())))
8584        }
8585        (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
8586        (Value::Date(dt), TypeSpecification::Date { .. }) => {
8587            Ok(ValueKind::Date(date_time_to_semantic(dt)))
8588        }
8589        (Value::Time(t), TypeSpecification::Time { .. }) => {
8590            Ok(ValueKind::Time(time_to_semantic(t)))
8591        }
8592        (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
8593    }
8594}
8595
8596/// Convert parser [`Value`] to [`ValueKind`] using the target type (canonicalizes ratio at bind).
8597///
8598/// When `resolve_unit` is set, qualified range-endpoint unit tokens are resolved to bare
8599/// declared names before bind. Measure and ratio range endpoints keep `lemma_type`'s family
8600/// via [`LemmaType::range_element_type`].
8601pub fn parser_value_to_value_kind(
8602    value: &crate::literals::Value,
8603    lemma_type: &LemmaType,
8604    resolve_unit: Option<&UnitTokenResolver<'_>>,
8605) -> Result<ValueKind, String> {
8606    use crate::literals::Value;
8607    match (value, &lemma_type.specifications) {
8608        (
8609            Value::Range(left, right),
8610            TypeSpecification::NumberRange { .. }
8611            | TypeSpecification::DateRange { .. }
8612            | TypeSpecification::TimeRange { .. }
8613            | TypeSpecification::RatioRange { .. }
8614            | TypeSpecification::MeasureRange { .. },
8615        ) => {
8616            let endpoint = match &lemma_type.specifications {
8617                TypeSpecification::MeasureRange { .. } | TypeSpecification::RatioRange { .. } => {
8618                    lemma_type.range_element_type()
8619                }
8620                other => Arc::new(LemmaType::primitive(
8621                    range_element_type_specification(other).expect(
8622                        "BUG: range_element_type_specification missing arm for known range type",
8623                    ),
8624                )),
8625            };
8626            let left_lit = lift_range_endpoint(left, endpoint.as_ref(), resolve_unit)?;
8627            let right_lit = lift_range_endpoint(right, endpoint.as_ref(), resolve_unit)?;
8628            Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
8629        }
8630        (value, type_spec) => parser_scalar_to_value_kind(value, type_spec),
8631    }
8632}
8633
8634/// Read a parsed Lemma literal as a value of `lemma_type`, keeping the written unit.
8635pub(crate) fn typed_literal_from_parsed(
8636    parsed: &crate::literals::Value,
8637    lemma_type: &Arc<LemmaType>,
8638    unit_index: &UnitIndex,
8639    named_types: &IndexMap<String, Arc<LemmaType>>,
8640) -> Result<TypedLiteral, String> {
8641    let resolve = UnitTokenResolver::Typed {
8642        unit_index,
8643        named_types,
8644        lemma_type: lemma_type.as_ref(),
8645    };
8646    let kind = parser_value_to_value_kind(parsed, lemma_type.as_ref(), Some(&resolve))?;
8647    let binding = match binding_unit_from_parser_value(parsed) {
8648        Some(written) => Some(resolve.resolve(&written)?),
8649        None => None,
8650    };
8651    Ok(typed_literal_with_binding_unit(kind, binding, lemma_type))
8652}
8653
8654pub(crate) fn typed_literal_with_binding_unit(
8655    kind: ValueKind,
8656    binding_unit: Option<String>,
8657    lemma_type: &Arc<LemmaType>,
8658) -> TypedLiteral {
8659    let typed_type = match binding_unit {
8660        Some(unit)
8661            if matches!(
8662                &lemma_type.specifications,
8663                TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. }
8664            ) =>
8665        {
8666            Arc::new(lemma_type.as_ref().clone().with_unit(unit))
8667        }
8668        Some(_) | None => Arc::clone(lemma_type),
8669    };
8670    TypedLiteral {
8671        value: kind,
8672        lemma_type: typed_type,
8673    }
8674}
8675
8676fn binding_unit_from_parser_value(value: &crate::literals::Value) -> Option<String> {
8677    use crate::literals::Value;
8678    match value {
8679        Value::NumberWithUnit(_, unit) => Some(unit.clone()),
8680        Value::Range(left, right) => match (left.as_ref(), right.as_ref()) {
8681            (Value::NumberWithUnit(_, left_unit), Value::NumberWithUnit(_, right_unit))
8682                if left_unit == right_unit =>
8683            {
8684                Some(left_unit.clone())
8685            }
8686            _ => None,
8687        },
8688        _ => None,
8689    }
8690}
8691
8692/// Convert parser Value to ValueKind for primitives and ranges only.
8693///
8694/// [`Value::NumberWithUnit`] requires [`parser_value_to_value_kind`] with a measure or ratio type.
8695pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
8696    use crate::parsing::ast::Value;
8697    Ok(match value {
8698        Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
8699        Value::Text(s) => ValueKind::Text(Arc::from(s.as_str())),
8700        Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
8701        Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
8702        Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
8703        Value::NumberWithUnit(_, _) => {
8704            return Err(
8705                "number_with_unit literal requires type context (measure or ratio)".to_string(),
8706            );
8707        }
8708        Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
8709    })
8710}
8711
8712/// Convert AST date-time to semantic (for tests and planning).
8713pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
8714    SemanticDateTime {
8715        year: dt.year,
8716        month: dt.month,
8717        day: dt.day,
8718        hour: dt.hour,
8719        minute: dt.minute,
8720        second: dt.second,
8721        microsecond: dt.microsecond,
8722        timezone: dt.timezone.as_ref().map(SemanticTimezone::from),
8723    }
8724}
8725
8726/// Project a date's clock fields to a time-of-day (date-only values yield midnight).
8727pub(crate) fn semantic_date_as_time(date: &SemanticDateTime) -> SemanticTime {
8728    SemanticTime {
8729        hour: date.hour,
8730        minute: date.minute,
8731        second: date.second,
8732        microsecond: date.microsecond,
8733        timezone: date.timezone.clone(),
8734    }
8735}
8736
8737/// Convert AST time to semantic (for tests and planning).
8738pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
8739    SemanticTime {
8740        hour: t.hour.into(),
8741        minute: t.minute.into(),
8742        second: t.second.into(),
8743        microsecond: t.microsecond,
8744        timezone: t.timezone.as_ref().map(SemanticTimezone::from),
8745    }
8746}
8747
8748/// Compare two semantic date-time values by year, month, day, hour, minute,
8749/// second, then microsecond. Timezone normalisation is a separate concern
8750/// handled at evaluation time.
8751pub(crate) fn compare_semantic_dates(
8752    left: &SemanticDateTime,
8753    right: &SemanticDateTime,
8754) -> std::cmp::Ordering {
8755    left.year
8756        .cmp(&right.year)
8757        .then_with(|| left.month.cmp(&right.month))
8758        .then_with(|| left.day.cmp(&right.day))
8759        .then_with(|| left.hour.cmp(&right.hour))
8760        .then_with(|| left.minute.cmp(&right.minute))
8761        .then_with(|| left.second.cmp(&right.second))
8762        .then_with(|| left.microsecond.cmp(&right.microsecond))
8763}
8764
8765/// Compare two semantic time values by hour, minute, second, then microsecond.
8766/// Timezone is excluded for the same reason as [`compare_semantic_dates`].
8767pub(crate) fn compare_semantic_times(
8768    left: &SemanticTime,
8769    right: &SemanticTime,
8770) -> std::cmp::Ordering {
8771    left.hour
8772        .cmp(&right.hour)
8773        .then_with(|| left.minute.cmp(&right.minute))
8774        .then_with(|| left.second.cmp(&right.second))
8775        .then_with(|| left.microsecond.cmp(&right.microsecond))
8776}
8777
8778/// Convert AST conversion target to semantic (planning boundary; evaluation/computation use only semantic).
8779pub fn conversion_target_to_semantic(
8780    ct: &ConversionTarget,
8781    unit_index: Option<&crate::planning::unit_index::UnitIndex>,
8782    resolved_types: Option<&IndexMap<String, Arc<LemmaType>>>,
8783) -> Result<SemanticConversionTarget, String> {
8784    match ct {
8785        ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
8786        ConversionTarget::Unit { unit_name } => {
8787            let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
8788            let (bare, owning_type) = match resolved_types {
8789                Some(resolved) => index.resolve_with_named_types(unit_name, resolved)?,
8790                None => index.resolve(unit_name)?,
8791            };
8792            Ok(SemanticConversionTarget::Unit {
8793                unit_name: bare,
8794                owning_type,
8795            })
8796        }
8797    }
8798}
8799
8800// -----------------------------------------------------------------------------
8801// Primitive type constructors (moved from parsing::ast)
8802// -----------------------------------------------------------------------------
8803
8804// Statics for lazy initialization of production-used primitive types.
8805static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
8806static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
8807static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
8808static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8809static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8810static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
8811static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
8812static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8813static PRIMITIVE_MEASURE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8814static PRIMITIVE_NUMBER_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8815static PRIMITIVE_RATIO_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8816static PRIMITIVE_TIME_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8817
8818#[must_use]
8819pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
8820    lemma_type_for_primitive(PrimitiveKind::Boolean)
8821}
8822
8823#[must_use]
8824pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
8825    lemma_type_for_primitive(PrimitiveKind::Number)
8826}
8827
8828#[must_use]
8829pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
8830    lemma_type_for_primitive(PrimitiveKind::Text)
8831}
8832
8833#[must_use]
8834pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
8835    lemma_type_for_primitive(PrimitiveKind::Date)
8836}
8837
8838#[must_use]
8839pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
8840    lemma_type_for_primitive(PrimitiveKind::DateRange)
8841}
8842
8843#[must_use]
8844pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
8845    lemma_type_for_primitive(PrimitiveKind::Time)
8846}
8847
8848#[must_use]
8849pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
8850    lemma_type_for_primitive(PrimitiveKind::Ratio)
8851}
8852
8853/// Singleton primitive `LemmaType` for `kind`. Same `Arc` for the same primitive for the process.
8854#[must_use]
8855pub fn lemma_type_for_primitive(kind: PrimitiveKind) -> &'static Arc<LemmaType> {
8856    let cell = match kind {
8857        PrimitiveKind::Boolean => &PRIMITIVE_BOOLEAN,
8858        PrimitiveKind::Number => &PRIMITIVE_NUMBER,
8859        PrimitiveKind::Text => &PRIMITIVE_TEXT,
8860        PrimitiveKind::Date => &PRIMITIVE_DATE,
8861        PrimitiveKind::DateRange => &PRIMITIVE_DATE_RANGE,
8862        PrimitiveKind::Time => &PRIMITIVE_TIME,
8863        PrimitiveKind::Ratio => &PRIMITIVE_RATIO,
8864        PrimitiveKind::Measure => &PRIMITIVE_MEASURE,
8865        PrimitiveKind::MeasureRange => &PRIMITIVE_MEASURE_RANGE,
8866        PrimitiveKind::NumberRange => &PRIMITIVE_NUMBER_RANGE,
8867        PrimitiveKind::RatioRange => &PRIMITIVE_RATIO_RANGE,
8868        PrimitiveKind::TimeRange => &PRIMITIVE_TIME_RANGE,
8869    };
8870    cell.get_or_init(|| Arc::new(LemmaType::primitive(type_spec_for_primitive(kind))))
8871}
8872
8873/// Map PrimitiveKind to TypeSpecification. Single source of truth for primitive type resolution.
8874#[must_use]
8875pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
8876    match kind {
8877        PrimitiveKind::Boolean => TypeSpecification::boolean(),
8878        PrimitiveKind::Measure => TypeSpecification::measure(),
8879        PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
8880        PrimitiveKind::Number => TypeSpecification::number(),
8881        PrimitiveKind::NumberRange => TypeSpecification::number_range(),
8882        PrimitiveKind::Ratio => TypeSpecification::ratio(),
8883        PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
8884        PrimitiveKind::Text => TypeSpecification::text(),
8885        PrimitiveKind::Date => TypeSpecification::date(),
8886        PrimitiveKind::DateRange => TypeSpecification::date_range(),
8887        PrimitiveKind::Time => TypeSpecification::time(),
8888        PrimitiveKind::TimeRange => TypeSpecification::time_range(),
8889    }
8890}
8891
8892// -----------------------------------------------------------------------------
8893// Display implementations
8894// -----------------------------------------------------------------------------
8895
8896impl fmt::Display for PathSegment {
8897    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8898        let hop = self.input_key();
8899        match &self.repository {
8900            Some(repository) => write!(f, "{hop} → {} {}", repository, self.spec),
8901            None => write!(f, "{hop} → {}", self.spec),
8902        }
8903    }
8904}
8905
8906impl fmt::Display for DataPath {
8907    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8908        write!(f, "{}", self.input_key())
8909    }
8910}
8911
8912impl fmt::Display for RulePath {
8913    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8914        write!(f, "{}", self.input_key())
8915    }
8916}
8917
8918impl fmt::Display for LemmaType {
8919    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8920        write!(f, "{}", self.name())
8921    }
8922}
8923
8924pub(crate) fn format_decimal_for_api(decimal: rust_decimal::Decimal) -> rust_decimal::Decimal {
8925    let normalized = decimal.normalize();
8926    if normalized.fract().is_zero() {
8927        normalized.trunc()
8928    } else {
8929        normalized
8930    }
8931}
8932
8933fn format_decimal_for_human_display(decimal: rust_decimal::Decimal) -> String {
8934    decimal.normalize().to_string()
8935}
8936
8937fn format_rational_for_human_display(
8938    magnitude: &crate::computation::rational::RationalInteger,
8939) -> String {
8940    match magnitude.try_to_decimal() {
8941        Ok(decimal) => format_decimal_for_human_display(decimal),
8942        Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
8943        Err(_) => magnitude.display_str(),
8944    }
8945}
8946
8947/// Whether any declared unit of this measure family has a known factor (literal or
8948/// settled / carried in `runtime_scales`).
8949pub(crate) fn measure_family_has_expressible_unit(
8950    lemma_type: &LemmaType,
8951    runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
8952) -> bool {
8953    let Some(units) = lemma_type.measure_units_table() else {
8954        return true;
8955    };
8956    if units.is_empty() {
8957        return true;
8958    }
8959    units.iter().any(|unit| {
8960        unit.literal_factor().is_some()
8961            || runtime_scales.is_some_and(|scales| scales.contains_key(&unit.name))
8962    })
8963}
8964
8965fn measure_display_unit<'a>(
8966    units: &'a crate::literals::MeasureUnits,
8967    lemma_type: &LemmaType,
8968    runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
8969) -> &'a crate::literals::MeasureUnit {
8970    let factor_known = |unit: &crate::literals::MeasureUnit| {
8971        unit.literal_factor().is_some()
8972            || runtime_scales.is_some_and(|scales| scales.contains_key(&unit.name))
8973    };
8974    if let Some(binding) = lemma_type.unit() {
8975        let unit = units
8976            .iter()
8977            .find(|unit| unit.name == binding)
8978            .unwrap_or_else(|| {
8979                panic!(
8980                    "BUG: measure display binding '{binding}' is not a declared unit on type {}",
8981                    lemma_type.name()
8982                )
8983            });
8984        if factor_known(unit) {
8985            return unit;
8986        }
8987    }
8988    units
8989        .iter()
8990        .find(|unit| unit.is_canonical_factor() && factor_known(unit))
8991        .or_else(|| {
8992            units
8993                .iter()
8994                .find(|unit| unit.literal_factor().is_some() && factor_known(unit))
8995        })
8996        .or_else(|| units.iter().find(|unit| factor_known(unit)))
8997        .unwrap_or_else(|| {
8998            panic!(
8999                "BUG: measure display of '{}' reached with no factor-known unit; callers must veto first",
9000                lemma_type.name()
9001            )
9002        })
9003}
9004
9005fn format_measure_canonical_for_display(
9006    canonical: &crate::computation::rational::RationalInteger,
9007    lemma_type: &LemmaType,
9008    signature: &[(String, i32)],
9009    runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
9010) -> String {
9011    use crate::computation::rational::checked_div;
9012
9013    if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
9014        if !units.is_empty() {
9015            let unit = measure_display_unit(units, lemma_type, runtime_scales);
9016            let factor = unit
9017                .literal_factor()
9018                .cloned()
9019                .or_else(|| {
9020                    runtime_scales.and_then(|scales| scales.get(&unit.name).cloned())
9021                })
9022                .unwrap_or_else(|| {
9023                    panic!(
9024                        "BUG: display of unit '{}' on {} requires a literal factor or a settled runtime scale",
9025                        unit.name,
9026                        lemma_type.name()
9027                    )
9028                });
9029            let in_unit = checked_div(canonical, &factor)
9030                .expect("BUG: de-canonicalization for measure display must not fail");
9031            return format_number_with_unit_for_display(&in_unit, &unit.name);
9032        }
9033    }
9034
9035    let unit_label = match signature {
9036        [] => String::new(),
9037        [(name, 1)] => name.clone(),
9038        _ => format_signature_operator_style(signature),
9039    };
9040    let formatted = format_rational_for_human_display(canonical);
9041    if unit_label.is_empty() {
9042        formatted
9043    } else {
9044        format!("{formatted} {unit_label}")
9045    }
9046}
9047
9048fn format_ratio_canonical_for_display(
9049    canonical: &crate::computation::rational::RationalInteger,
9050    lemma_type: &LemmaType,
9051) -> String {
9052    use crate::computation::rational::checked_mul;
9053
9054    let units = match &lemma_type.specifications {
9055        TypeSpecification::Ratio { units, .. } | TypeSpecification::RatioRange { units, .. } => {
9056            units
9057        }
9058        _ => unreachable!(
9059            "BUG: ratio display with non-ratio type {}",
9060            lemma_type.name()
9061        ),
9062    };
9063    let display_unit = lemma_type
9064        .unit()
9065        .or_else(|| lemma_type.ratio_primary_unit());
9066    let Some(unit_name) = display_unit else {
9067        return format_rational_magnitude_for_display(canonical);
9068    };
9069    let unit = units.get(unit_name).unwrap_or_else(|_| {
9070        panic!(
9071            "BUG: ratio display unit '{unit_name}' is not declared on type {}",
9072            lemma_type.name()
9073        )
9074    });
9075    let in_unit = checked_mul(canonical, &unit.value)
9076        .expect("BUG: de-canonicalization for ratio display must not fail");
9077    format_number_with_unit_for_display(&in_unit, unit_name)
9078}
9079
9080impl fmt::Display for LiteralValue {
9081    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9082        // Measure/ratio display needs an explicit type; callers must use
9083        // `display_value_with_type`. Bare Display only covers type-free kinds and
9084        // ranges of those kinds.
9085        match &self.value {
9086            ValueKind::Measure(_) | ValueKind::Ratio(_) => {
9087                write!(f, "{}", self.value)
9088            }
9089            ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
9090            _ => write!(f, "{}", self.value),
9091        }
9092    }
9093}
9094
9095// -----------------------------------------------------------------------------
9096// Tests
9097// -----------------------------------------------------------------------------
9098
9099#[cfg(test)]
9100pub(crate) mod tests {
9101    use super::*;
9102    use crate::computation::rational::decimal_to_rational;
9103    use crate::literals::DateGranularity;
9104    use crate::literals::Value;
9105    use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
9106    use rust_decimal::Decimal;
9107    use std::str::FromStr;
9108
9109    fn meter_axis(exponent: i32) -> BaseMeasureVector {
9110        [(Dimension::Family(test_type_family("meter")), exponent)]
9111            .into_iter()
9112            .collect()
9113    }
9114
9115    #[test]
9116    fn scale_decomposition_exponent_one_is_identity() {
9117        let base = meter_axis(1);
9118        assert_eq!(scale_decomposition(&base, 1), Some(base));
9119    }
9120
9121    #[test]
9122    fn scale_decomposition_squares_meter() {
9123        assert_eq!(scale_decomposition(&meter_axis(1), 2), Some(meter_axis(2)));
9124    }
9125
9126    #[test]
9127    fn scale_decomposition_zero_is_empty() {
9128        assert_eq!(
9129            scale_decomposition(&meter_axis(1), 0),
9130            Some(BaseMeasureVector::new())
9131        );
9132    }
9133
9134    #[test]
9135    fn scale_decomposition_negative_inverts_meter() {
9136        assert_eq!(
9137            scale_decomposition(&meter_axis(1), -1),
9138            Some(meter_axis(-1))
9139        );
9140    }
9141
9142    #[test]
9143    fn scale_decomposition_at_exponent_cap() {
9144        assert_eq!(
9145            scale_decomposition(&meter_axis(32), 2),
9146            Some(meter_axis(64))
9147        );
9148    }
9149
9150    #[test]
9151    fn scale_decomposition_past_exponent_cap_is_none() {
9152        assert_eq!(scale_decomposition(&meter_axis(1), 65), None);
9153        assert_eq!(scale_decomposition(&meter_axis(32), 3), None);
9154    }
9155
9156    #[must_use]
9157    pub(crate) fn test_type_family(name: &str) -> TypeFamily {
9158        TypeFamily {
9159            repository: None,
9160            spec: "test".to_string(),
9161            effective: crate::parsing::ast::EffectiveDate::Origin,
9162            name: name.to_string(),
9163        }
9164    }
9165
9166    #[must_use]
9167    pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
9168        crate::planning::semantics::lemma_type_for_primitive(PrimitiveKind::Measure)
9169    }
9170
9171    #[must_use]
9172    pub(crate) fn primitive_measure() -> &'static LemmaType {
9173        primitive_measure_arc().as_ref()
9174    }
9175
9176    #[test]
9177    fn default_primitive_help_is_goal_oriented() {
9178        let kinds = [
9179            PrimitiveKind::Boolean,
9180            PrimitiveKind::Measure,
9181            PrimitiveKind::MeasureRange,
9182            PrimitiveKind::Number,
9183            PrimitiveKind::NumberRange,
9184            PrimitiveKind::Ratio,
9185            PrimitiveKind::RatioRange,
9186            PrimitiveKind::Text,
9187            PrimitiveKind::Date,
9188            PrimitiveKind::DateRange,
9189            PrimitiveKind::Time,
9190            PrimitiveKind::TimeRange,
9191        ];
9192        for kind in kinds {
9193            let spec = type_spec_for_primitive(kind);
9194            let help = match &spec {
9195                TypeSpecification::Boolean { help, .. }
9196                | TypeSpecification::Number { help, .. }
9197                | TypeSpecification::NumberRange { help, .. }
9198                | TypeSpecification::Text { help, .. }
9199                | TypeSpecification::Measure { help, .. }
9200                | TypeSpecification::MeasureRange { help, .. }
9201                | TypeSpecification::Ratio { help, .. }
9202                | TypeSpecification::RatioRange { help, .. }
9203                | TypeSpecification::Date { help, .. }
9204                | TypeSpecification::DateRange { help, .. }
9205                | TypeSpecification::TimeRange { help, .. }
9206                | TypeSpecification::Time { help, .. } => help,
9207                TypeSpecification::Veto { .. }
9208                | TypeSpecification::Undetermined
9209                | TypeSpecification::Spec { .. }
9210                | TypeSpecification::List { .. } => {
9211                    unreachable!(
9212                        "BUG: primitive kind {:?} mapped to non-primitive spec",
9213                        kind
9214                    )
9215                }
9216            };
9217            assert!(!help.is_empty(), "help for {:?}", kind);
9218            assert!(
9219                !help.to_ascii_lowercase().contains("format:"),
9220                "help for {:?} must not describe syntax: {:?}",
9221                kind,
9222                help
9223            );
9224            assert_eq!(help, default_help_for_primitive(kind));
9225        }
9226    }
9227
9228    #[test]
9229    fn data_path_and_rule_path_display_are_input_key() {
9230        let implicit = PathSegment {
9231            uses: Arc::from("bag"),
9232            row: None,
9233            repository: None,
9234            spec: Arc::from("bag"),
9235        };
9236        let explicit = PathSegment {
9237            uses: Arc::from("bag"),
9238            row: None,
9239            repository: None,
9240            spec: Arc::from("item_bag"),
9241        };
9242        let named_repo = PathSegment {
9243            uses: Arc::from("calc"),
9244            row: None,
9245            repository: Some(Arc::from("alpha")),
9246            spec: Arc::from("pricing"),
9247        };
9248        let row = PathSegment {
9249            uses: Arc::from("staff"),
9250            row: Some(2),
9251            repository: None,
9252            spec: Arc::from("employee"),
9253        };
9254
9255        assert_eq!(
9256            DataPath::new(vec![row.clone()], "salary".to_string()).to_string(),
9257            "staff.2.salary"
9258        );
9259        assert_eq!(
9260            RulePath::new(vec![row.clone()], "double".to_string()).to_string(),
9261            "staff.2.double"
9262        );
9263        assert_eq!(row.to_string(), "staff.2 → employee");
9264        assert_eq!(
9265            SpecInstance {
9266                prefix: vec![implicit.clone(), row]
9267            }
9268            .to_string(),
9269            "bag.staff.2"
9270        );
9271        assert_eq!(
9272            DataPath::new(vec![implicit.clone()], "weight".to_string()).to_string(),
9273            "bag.weight"
9274        );
9275        assert_eq!(
9276            DataPath::new(vec![explicit.clone()], "weight".to_string()).to_string(),
9277            "bag.weight"
9278        );
9279        assert_eq!(
9280            DataPath::new(vec![named_repo.clone()], "price".to_string()).to_string(),
9281            "calc.price"
9282        );
9283        assert_eq!(
9284            RulePath::new(vec![implicit], "cost".to_string()).to_string(),
9285            "bag.cost"
9286        );
9287        assert_eq!(
9288            RulePath::new(vec![explicit], "cost".to_string()).to_string(),
9289            "bag.cost"
9290        );
9291        assert_eq!(
9292            RulePath::new(vec![named_repo], "price".to_string()).to_string(),
9293            "calc.price"
9294        );
9295
9296        assert_eq!(
9297            PathSegment {
9298                uses: Arc::from("bag"),
9299                row: None,
9300                repository: None,
9301                spec: Arc::from("item_bag"),
9302            }
9303            .to_string(),
9304            "bag → item_bag"
9305        );
9306        assert_eq!(
9307            PathSegment {
9308                uses: Arc::from("calc"),
9309                row: None,
9310                repository: Some(Arc::from("alpha")),
9311                spec: Arc::from("pricing"),
9312            }
9313            .to_string(),
9314            "calc → alpha pricing"
9315        );
9316    }
9317
9318    #[test]
9319    fn test_negated_comparison() {
9320        assert_eq!(
9321            negated_comparison(ComparisonComputation::LessThan),
9322            ComparisonComputation::GreaterThanOrEqual
9323        );
9324        assert_eq!(
9325            negated_comparison(ComparisonComputation::GreaterThanOrEqual),
9326            ComparisonComputation::LessThan
9327        );
9328        assert_eq!(
9329            negated_comparison(ComparisonComputation::Is),
9330            ComparisonComputation::IsNot
9331        );
9332        assert_eq!(
9333            negated_comparison(ComparisonComputation::IsNot),
9334            ComparisonComputation::Is
9335        );
9336    }
9337
9338    #[test]
9339    fn value_to_semantic_number_is_decimal() {
9340        let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
9341        assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
9342    }
9343
9344    #[test]
9345    fn value_kind_measure_serializes_magnitude_only() {
9346        let kind =
9347            ValueKind::Measure(decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap());
9348        let json = serde_json::to_value(crate::api::ValueKind::from(&kind)).unwrap();
9349        assert_eq!(json["measure"]["value"], "99.5");
9350        assert!(json["measure"].get("signature").is_none());
9351    }
9352
9353    #[test]
9354    fn value_kind_measure_roundtrips() {
9355        let original =
9356            ValueKind::Measure(decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap());
9357        let api = crate::api::ValueKind::from(&original);
9358        let json = serde_json::to_string(&api).unwrap();
9359        let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
9360        assert_eq!(api, parsed);
9361    }
9362
9363    #[test]
9364    fn value_kind_measure_empty_roundtrips() {
9365        let original =
9366            ValueKind::Measure(decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap());
9367        let api = crate::api::ValueKind::from(&original);
9368        let json = serde_json::to_string(&api).unwrap();
9369        let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
9370        assert_eq!(api, parsed);
9371    }
9372
9373    #[test]
9374    fn literal_value_number_serde_not_rational_array() {
9375        let lit = LiteralValue::number_from_decimal(Decimal::from(20));
9376        let json = serde_json::to_value(crate::api::LiteralValue::from(&lit)).unwrap();
9377        let number = json
9378            .get("value")
9379            .and_then(|v| v.get("number"))
9380            .expect("number field");
9381        assert!(number.is_string());
9382        assert_eq!(number.as_str(), Some("20"));
9383        assert!(
9384            !number.is_array(),
9385            "stored number must not serialize as [n,d]"
9386        );
9387    }
9388
9389    #[test]
9390    fn test_literal_value_to_primitive_type() {
9391        let one = rational_new(1, 1);
9392
9393        assert_eq!(TypedLiteral::text("".to_string()).lemma_type.name(), "text");
9394        assert_eq!(
9395            TypedLiteral::number(one.clone()).lemma_type.name(),
9396            "number"
9397        );
9398        assert_eq!(
9399            TypedLiteral::from_bool(bool::from(BooleanValue::True))
9400                .lemma_type
9401                .name(),
9402            "boolean"
9403        );
9404
9405        let dt = DateTimeValue {
9406            year: 2024,
9407            month: 1,
9408            day: 1,
9409            hour: 0,
9410            minute: 0,
9411            second: 0,
9412            microsecond: 0,
9413            timezone: None,
9414
9415            granularity: DateGranularity::Full,
9416        };
9417        assert_eq!(
9418            TypedLiteral::date(date_time_to_semantic(&dt))
9419                .lemma_type
9420                .name(),
9421            "date"
9422        );
9423        assert_eq!(
9424            TypedLiteral::ratio_from_decimal(Decimal::new(1, 2))
9425                .lemma_type
9426                .name(),
9427            "ratio"
9428        );
9429        let dur_type = LemmaType::new(
9430            "duration".to_string(),
9431            TypeSpecification::Measure {
9432                minimum: None,
9433                maximum: None,
9434                units: MeasureUnits::from(vec![MeasureUnit {
9435                    name: "second".to_string(),
9436                    scale: UnitScale::Literal(crate::computation::rational::rational_one()),
9437                    derived_measure_factors: Vec::new(),
9438                    decomposition: BaseMeasureVector::new(),
9439                    minimum: None,
9440                    maximum: None,
9441                    suggestion_magnitude: None,
9442                }]),
9443                traits: vec![MeasureTrait::Duration],
9444                decomposition: None,
9445                unit: None,
9446                help: String::new(),
9447            },
9448            TypeExtends::Primitive,
9449        );
9450        assert_eq!(
9451            TypedLiteral::measure_with_type(one.clone(), Arc::new(dur_type))
9452                .lemma_type
9453                .name(),
9454            "duration"
9455        );
9456    }
9457
9458    #[test]
9459    fn test_type_display() {
9460        let specs = TypeSpecification::text();
9461        let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
9462        assert_eq!(format!("{}", lemma_type), "name");
9463    }
9464
9465    #[test]
9466    fn test_type_serialization() {
9467        let specs = TypeSpecification::number();
9468        let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
9469        let api = crate::api::LemmaType::from(&lemma_type);
9470        let serialized = serde_json::to_string(&api).unwrap();
9471        let deserialized: crate::api::LemmaType = serde_json::from_str(&serialized).unwrap();
9472        assert_eq!(api, deserialized);
9473    }
9474
9475    #[test]
9476    fn empty_list_display_value_with_type_is_type_spec() {
9477        let element = Arc::new(LemmaType::new(
9478            "item".to_string(),
9479            TypeSpecification::Spec {
9480                spec: SpecIdentity {
9481                    repository: None,
9482                    spec: "item".to_string(),
9483                    effective: crate::parsing::ast::EffectiveDate::Origin,
9484                    slice: crate::parsing::ast::EffectiveDate::Origin,
9485                },
9486            },
9487            TypeExtends::Primitive,
9488        ));
9489        let list_ty = LemmaType::list_of(element);
9490        let empty = LiteralValue::new(ValueKind::List(Vec::new().into()));
9491        assert_eq!(empty.display_value_with_type(&list_ty), "item list");
9492        assert_eq!(empty.display_value(), "");
9493    }
9494
9495    #[test]
9496    fn test_literal_value_display_value() {
9497        let ten = rational_new(10, 1);
9498
9499        assert_eq!(
9500            LiteralValue::text("hello".to_string()).display_value(),
9501            "hello"
9502        );
9503        assert_eq!(LiteralValue::number(ten).display_value(), "10");
9504        assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
9505        assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
9506
9507        // 0.10 ratio with "percent" binding displays as 10% (unit conversion applied)
9508        let ratio_ty = Arc::new(primitive_ratio_arc().as_ref().clone().with_unit("percent"));
9509        let ten_percent_ratio = LiteralValue::ratio(
9510            crate::literals::rational_from_parsed_decimal(Decimal::new(1, 1))
9511                .expect("ratio decimal"),
9512        );
9513        assert_eq!(
9514            ten_percent_ratio.display_value_with_type(ratio_ty.as_ref()),
9515            "10%"
9516        );
9517
9518        let time = TimeValue {
9519            hour: 14,
9520            minute: 30,
9521            second: 0,
9522            microsecond: 0,
9523            timezone: None,
9524        };
9525        let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
9526        assert!(time_display.contains("14"));
9527        assert!(time_display.contains("30"));
9528    }
9529
9530    #[test]
9531    fn test_measure_display_trims_trailing_zeros() {
9532        let money_type = LemmaType {
9533            name: Some("money".to_string()),
9534            specifications: TypeSpecification::Measure {
9535                minimum: None,
9536                maximum: None,
9537                units: MeasureUnits::from(vec![MeasureUnit {
9538                    name: "eur".to_string(),
9539                    scale: UnitScale::Literal(crate::computation::rational::rational_one()),
9540                    derived_measure_factors: Vec::new(),
9541                    decomposition: BaseMeasureVector::new(),
9542                    minimum: None,
9543                    maximum: None,
9544                    suggestion_magnitude: None,
9545                }]),
9546                traits: Vec::new(),
9547                decomposition: None,
9548                unit: None,
9549                help: String::new(),
9550            },
9551            extends: TypeExtends::Primitive,
9552            options: TypeOptions::default(),
9553        };
9554        let money_type = Arc::new(money_type);
9555        let val =
9556            LiteralValue::measure(decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap());
9557        assert_eq!(val.display_value_with_type(money_type.as_ref()), "1.8 eur");
9558        let more_precision = LiteralValue::measure(
9559            decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
9560        );
9561        assert_eq!(
9562            more_precision.display_value_with_type(money_type.as_ref()),
9563            "1.8 eur"
9564        );
9565        let count_type = Arc::new(LemmaType {
9566            name: Some("count".to_string()),
9567            specifications: TypeSpecification::Measure {
9568                minimum: None,
9569                maximum: None,
9570                units: MeasureUnits::from(vec![MeasureUnit {
9571                    name: "items".to_string(),
9572                    scale: UnitScale::Literal(crate::computation::rational::rational_one()),
9573                    derived_measure_factors: Vec::new(),
9574                    decomposition: BaseMeasureVector::new(),
9575                    minimum: None,
9576                    maximum: None,
9577                    suggestion_magnitude: None,
9578                }]),
9579                traits: Vec::new(),
9580                decomposition: None,
9581                unit: None,
9582                help: String::new(),
9583            },
9584            extends: TypeExtends::Primitive,
9585            options: TypeOptions::default(),
9586        });
9587        let val_any = LiteralValue::measure(
9588            decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
9589        );
9590        assert_eq!(
9591            val_any.display_value_with_type(count_type.as_ref()),
9592            "42.5 items"
9593        );
9594    }
9595
9596    #[test]
9597    fn test_literal_value_time_type() {
9598        let time = TimeValue {
9599            hour: 14,
9600            minute: 30,
9601            second: 0,
9602            microsecond: 0,
9603            timezone: None,
9604        };
9605        let lit = TypedLiteral::time(time_to_semantic(&time));
9606        assert_eq!(lit.lemma_type.name(), "time");
9607    }
9608
9609    #[test]
9610    fn test_measure_family_name_none_without_declaring_spec() {
9611        let money_primitive = LemmaType::new(
9612            "money".to_string(),
9613            TypeSpecification::measure(),
9614            TypeExtends::Primitive,
9615        );
9616        assert_eq!(money_primitive.measure_family_name(), None);
9617    }
9618
9619    #[test]
9620    fn test_measure_family_name_custom() {
9621        let measure_spec = TypeSpecification::measure();
9622        let money_custom = LemmaType::new(
9623            "money".to_string(),
9624            measure_spec,
9625            TypeExtends::custom_local("money".to_string(), test_type_family("money")),
9626        );
9627        assert_eq!(money_custom.measure_family_name(), Some("money"));
9628    }
9629
9630    #[test]
9631    fn test_same_named_roots_in_different_specs_are_different_families() {
9632        let declared_in = |spec: &str| TypeFamily {
9633            repository: None,
9634            spec: spec.to_string(),
9635            effective: crate::parsing::ast::EffectiveDate::Origin,
9636            name: "money".to_string(),
9637        };
9638        let pricing_money = LemmaType::new(
9639            "money".to_string(),
9640            TypeSpecification::measure(),
9641            TypeExtends::custom_local("measure".to_string(), declared_in("pricing")),
9642        );
9643        let shop_money = LemmaType::new(
9644            "money".to_string(),
9645            TypeSpecification::measure(),
9646            TypeExtends::custom_local("measure".to_string(), declared_in("shop")),
9647        );
9648        assert!(!pricing_money.same_measure_family(&shop_money));
9649        assert!(!shop_money.same_measure_family(&pricing_money));
9650    }
9651
9652    #[test]
9653    fn test_same_measure_family_parent_and_child() {
9654        let measure_spec = TypeSpecification::measure();
9655        let type_x = LemmaType::new(
9656            "x".to_string(),
9657            measure_spec.clone(),
9658            TypeExtends::custom_local("measure".to_string(), test_type_family("x")),
9659        );
9660        let type_x2 = LemmaType::new(
9661            "x2".to_string(),
9662            measure_spec,
9663            TypeExtends::custom_local("x".to_string(), test_type_family("x")),
9664        );
9665        assert_eq!(type_x.measure_family_name(), Some("x"));
9666        assert_eq!(type_x2.measure_family_name(), Some("x"));
9667        assert!(type_x.same_measure_family(&type_x2));
9668        assert!(type_x2.same_measure_family(&type_x));
9669    }
9670
9671    #[test]
9672    fn test_same_measure_family_siblings() {
9673        let measure_spec = TypeSpecification::measure();
9674        let type_x2_a = LemmaType::new(
9675            "x2a".to_string(),
9676            measure_spec.clone(),
9677            TypeExtends::custom_local("x".to_string(), test_type_family("x")),
9678        );
9679        let type_x2_b = LemmaType::new(
9680            "x2b".to_string(),
9681            measure_spec,
9682            TypeExtends::custom_local("x".to_string(), test_type_family("x")),
9683        );
9684        assert!(type_x2_a.same_measure_family(&type_x2_b));
9685    }
9686
9687    #[test]
9688    fn test_same_measure_family_different_families() {
9689        let measure_spec = TypeSpecification::measure();
9690        let money = LemmaType::new(
9691            "money".to_string(),
9692            measure_spec.clone(),
9693            TypeExtends::Primitive,
9694        );
9695        let temperature = LemmaType::new(
9696            "temperature".to_string(),
9697            measure_spec,
9698            TypeExtends::Primitive,
9699        );
9700        assert!(!money.same_measure_family(&temperature));
9701        assert!(!temperature.same_measure_family(&money));
9702    }
9703
9704    #[test]
9705    fn test_same_measure_family_measure_vs_non_measure() {
9706        let measure_spec = TypeSpecification::measure();
9707        let number_spec = TypeSpecification::number();
9708        let measure_type =
9709            LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
9710        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
9711        assert!(!measure_type.same_measure_family(&number_type));
9712        assert!(!number_type.same_measure_family(&measure_type));
9713    }
9714
9715    #[test]
9716    fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
9717        let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
9718        let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
9719
9720        assert!(!left.same_measure_family(&right));
9721        assert!(left.compatible_with_anonymous_measure(&right));
9722    }
9723
9724    #[test]
9725    fn same_value_type_distinct_families_same_decomposition_are_not_same() {
9726        let decomp: BaseMeasureVector = [
9727            (Dimension::Family(test_type_family("length")), 1i32),
9728            (Dimension::Family(test_type_family("elapsed")), -1i32),
9729        ]
9730        .into_iter()
9731        .collect();
9732        let speed_spec = |name: &str, family: &str| {
9733            let mut spec = TypeSpecification::measure();
9734            if let TypeSpecification::Measure {
9735                decomposition,
9736                help,
9737                ..
9738            } = &mut spec
9739            {
9740                *decomposition = Some(decomp.clone());
9741                *help = String::new();
9742            }
9743            LemmaType::new(
9744                name.to_string(),
9745                spec,
9746                TypeExtends::custom_local("measure".to_string(), test_type_family(family)),
9747            )
9748        };
9749        let speed_a = speed_spec("speed_a", "speed_a");
9750        let speed_b = speed_spec("speed_b", "speed_b");
9751        assert_eq!(
9752            speed_a.measure_type_decomposition(),
9753            speed_b.measure_type_decomposition()
9754        );
9755        assert!(!speed_a.same_measure_family(&speed_b));
9756        assert!(
9757            !speed_a.same_value_type(&speed_b),
9758            "same decomposition alone must not make distinct measure families same_value_type"
9759        );
9760    }
9761
9762    #[test]
9763    fn test_measure_family_name_non_measure_returns_none() {
9764        let number_spec = TypeSpecification::number();
9765        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
9766        assert_eq!(number_type.measure_family_name(), None);
9767    }
9768
9769    #[test]
9770    fn test_lemma_type_inequality_local_vs_import_same_shape() {
9771        let measure_spec = TypeSpecification::measure();
9772        let local = LemmaType::new(
9773            "t".to_string(),
9774            measure_spec.clone(),
9775            TypeExtends::custom_local("money".to_string(), test_type_family("money")),
9776        );
9777        let imported = LemmaType::new(
9778            "t".to_string(),
9779            measure_spec,
9780            TypeExtends::Custom {
9781                parent: "money".to_string(),
9782                family: test_type_family("money"),
9783                defining_spec: TypeDefiningSpec::Import,
9784            },
9785        );
9786        assert_ne!(local, imported);
9787    }
9788
9789    #[test]
9790    fn test_lemma_type_equality_import_unit_variant() {
9791        let measure_spec = TypeSpecification::measure();
9792        let left = LemmaType::new(
9793            "t".to_string(),
9794            measure_spec.clone(),
9795            TypeExtends::Custom {
9796                parent: "money".to_string(),
9797                family: test_type_family("money"),
9798                defining_spec: TypeDefiningSpec::Import,
9799            },
9800        );
9801        let right = LemmaType::new(
9802            "t".to_string(),
9803            measure_spec,
9804            TypeExtends::Custom {
9805                parent: "money".to_string(),
9806                family: test_type_family("money"),
9807                defining_spec: TypeDefiningSpec::Import,
9808            },
9809        );
9810        assert_eq!(left, right);
9811    }
9812
9813    fn month_suggestion_arg() -> CommandArg {
9814        CommandArg::Literal(crate::literals::Value::NumberWithUnit(
9815            Decimal::ONE,
9816            "month".to_string(),
9817        ))
9818    }
9819
9820    fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
9821        [
9822            CommandArg::Label(name.to_string()),
9823            CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
9824        ]
9825    }
9826
9827    #[test]
9828    fn default_calendar_on_text_reports_hint() {
9829        let mut specs = TypeSpecification::text();
9830        let mut default = None;
9831        let err = specs
9832            .apply_constraint(
9833                ConstraintNames {
9834                    type_name: "notes",
9835                    data_name: "notes",
9836                },
9837                TypeConstraintCommand::Suggest,
9838                &[month_suggestion_arg()],
9839                &mut default,
9840                &mut None,
9841                None,
9842            )
9843            .unwrap_err();
9844        assert!(err.contains("Unit 'month' is for calendar data"));
9845        assert!(err.contains("double quotes"));
9846    }
9847
9848    #[test]
9849    fn default_calendar_on_duration_reports_valid_units() {
9850        let mut specs = TypeSpecification::measure();
9851        specs
9852            .apply_constraint(
9853                ConstraintNames {
9854                    type_name: "duration",
9855                    data_name: "duration",
9856                },
9857                TypeConstraintCommand::Unit,
9858                &unit_factor_arg("second", 1),
9859                &mut None,
9860                &mut None,
9861                None,
9862            )
9863            .unwrap();
9864        specs
9865            .apply_constraint(
9866                ConstraintNames {
9867                    type_name: "duration",
9868                    data_name: "duration",
9869                },
9870                TypeConstraintCommand::Unit,
9871                &unit_factor_arg("week", 604_800),
9872                &mut None,
9873                &mut None,
9874                None,
9875            )
9876            .unwrap();
9877        specs
9878            .apply_constraint(
9879                ConstraintNames {
9880                    type_name: "duration",
9881                    data_name: "duration",
9882                },
9883                TypeConstraintCommand::Trait,
9884                &[CommandArg::Label("duration".to_string())],
9885                &mut None,
9886                &mut None,
9887                None,
9888            )
9889            .unwrap();
9890        let mut default = None;
9891        let err = specs
9892            .apply_constraint(
9893                ConstraintNames {
9894                    type_name: "duration",
9895                    data_name: "duration",
9896                },
9897                TypeConstraintCommand::Suggest,
9898                &[month_suggestion_arg()],
9899                &mut default,
9900                &mut None,
9901                None,
9902            )
9903            .unwrap_err();
9904        assert!(err.contains("Unit 'month' is for calendar data"));
9905        assert!(err.contains("Valid 'duration' units are"));
9906        assert!(err.contains("week"));
9907    }
9908
9909    #[test]
9910    fn default_valid_duration_weeks_accepted() {
9911        let mut specs = TypeSpecification::measure();
9912        specs
9913            .apply_constraint(
9914                ConstraintNames {
9915                    type_name: "duration",
9916                    data_name: "duration",
9917                },
9918                TypeConstraintCommand::Unit,
9919                &unit_factor_arg("second", 1),
9920                &mut None,
9921                &mut None,
9922                None,
9923            )
9924            .unwrap();
9925        specs
9926            .apply_constraint(
9927                ConstraintNames {
9928                    type_name: "duration",
9929                    data_name: "duration",
9930                },
9931                TypeConstraintCommand::Unit,
9932                &unit_factor_arg("week", 604_800),
9933                &mut None,
9934                &mut None,
9935                None,
9936            )
9937            .unwrap();
9938        specs
9939            .apply_constraint(
9940                ConstraintNames {
9941                    type_name: "duration",
9942                    data_name: "duration",
9943                },
9944                TypeConstraintCommand::Trait,
9945                &[CommandArg::Label("duration".to_string())],
9946                &mut None,
9947                &mut None,
9948                None,
9949            )
9950            .unwrap();
9951        let mut default = None;
9952        specs
9953            .apply_constraint(
9954                ConstraintNames {
9955                    type_name: "duration",
9956                    data_name: "duration",
9957                },
9958                TypeConstraintCommand::Suggest,
9959                &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
9960                    Decimal::from(4),
9961                    "week".to_string(),
9962                ))],
9963                &mut default,
9964                &mut None,
9965                None,
9966            )
9967            .unwrap();
9968        assert!(matches!(
9969            default,
9970            Some(RawSuggestion::Measure {
9971                unit_name,
9972                ..
9973            }) if unit_name == "week"
9974        ));
9975    }
9976
9977    #[test]
9978    fn default_unknown_unit_on_duration_lists_valid_units() {
9979        let mut specs = TypeSpecification::measure();
9980        specs
9981            .apply_constraint(
9982                ConstraintNames {
9983                    type_name: "duration",
9984                    data_name: "duration",
9985                },
9986                TypeConstraintCommand::Unit,
9987                &unit_factor_arg("second", 1),
9988                &mut None,
9989                &mut None,
9990                None,
9991            )
9992            .unwrap();
9993        specs
9994            .apply_constraint(
9995                ConstraintNames {
9996                    type_name: "duration",
9997                    data_name: "duration",
9998                },
9999                TypeConstraintCommand::Trait,
10000                &[CommandArg::Label("duration".to_string())],
10001                &mut None,
10002                &mut None,
10003                None,
10004            )
10005            .unwrap();
10006        let mut default = None;
10007        let err = specs
10008            .apply_constraint(
10009                ConstraintNames {
10010                    type_name: "duration",
10011                    data_name: "duration",
10012                },
10013                TypeConstraintCommand::Suggest,
10014                &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
10015                    Decimal::ONE,
10016                    "fortnight".to_string(),
10017                ))],
10018                &mut default,
10019                &mut None,
10020                None,
10021            )
10022            .unwrap_err();
10023        assert!(err.contains("fortnight"));
10024        assert!(err.contains("not defined on 'duration'"));
10025        assert!(err.contains("Valid units are"));
10026    }
10027
10028    fn money_measure_type() -> LemmaType {
10029        LemmaType::new(
10030            "Money".to_string(),
10031            TypeSpecification::Measure {
10032                minimum: None,
10033                maximum: None,
10034                units: MeasureUnits::from(vec![
10035                    MeasureUnit {
10036                        name: "eur".to_string(),
10037                        scale: UnitScale::Literal(crate::computation::rational::rational_one()),
10038                        derived_measure_factors: Vec::new(),
10039                        decomposition: BaseMeasureVector::new(),
10040                        minimum: None,
10041                        maximum: None,
10042                        suggestion_magnitude: None,
10043                    },
10044                    MeasureUnit {
10045                        name: "usd".to_string(),
10046                        scale: UnitScale::Literal(
10047                            crate::computation::rational::decimal_to_rational(Decimal::new(91, 2))
10048                                .expect("factor"),
10049                        ),
10050                        derived_measure_factors: Vec::new(),
10051                        decomposition: BaseMeasureVector::new(),
10052                        minimum: None,
10053                        maximum: None,
10054                        suggestion_magnitude: None,
10055                    },
10056                ]),
10057                traits: Vec::new(),
10058                decomposition: None,
10059                unit: None,
10060                help: String::new(),
10061            },
10062            TypeExtends::Primitive,
10063        )
10064    }
10065
10066    #[test]
10067    fn measure_unit_names_for_named_measure() {
10068        let money = money_measure_type();
10069        assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
10070    }
10071
10072    // ---------------------------------------------------------------------------
10073    // Phase 0 — pin combine_signatures and canonicalize_signature behavior
10074    // ---------------------------------------------------------------------------
10075
10076    fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
10077        pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
10078    }
10079
10080    #[test]
10081    fn combine_signatures_multiply_adds_exponents() {
10082        let left = sig(&[("eur", 1)]);
10083        let right = sig(&[("hour", -1)]);
10084        let result = combine_signatures(&left, &right, true);
10085        assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
10086    }
10087
10088    #[test]
10089    fn combine_signatures_divide_subtracts_exponents() {
10090        let left = sig(&[("eur", 1)]);
10091        let right = sig(&[("hour", 1)]);
10092        let result = combine_signatures(&left, &right, false);
10093        assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
10094    }
10095
10096    #[test]
10097    fn combine_signatures_cancels_to_empty() {
10098        let left = sig(&[("ce", 1), ("minute", -1)]);
10099        let right = sig(&[("minute", 1)]);
10100        let result = combine_signatures(&left, &right, true);
10101        // ce * (ce/min * min) = ce; minute cancels
10102        assert_eq!(result, sig(&[("ce", 1)]));
10103    }
10104
10105    #[test]
10106    fn combine_signatures_output_is_canonical_form() {
10107        let left = sig(&[("eur", 1), ("hour", 1)]);
10108        let right = sig(&[("minute", 1)]);
10109        let result = combine_signatures(&left, &right, false); // divide
10110                                                               // [("eur",1),("hour",1)] / [("minute",1)] = [("eur",1),("hour",1),("minute",-1)]
10111        let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
10112        assert_eq!(result, expected);
10113    }
10114
10115    #[test]
10116    fn canonicalize_signature_drops_zero_exponents() {
10117        let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
10118        let result = canonicalize_signature(&sig_with_zero);
10119        assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
10120    }
10121
10122    #[test]
10123    fn canonicalize_signature_sorts_by_name() {
10124        let unsorted = sig(&[("minute", -1), ("eur", 1)]);
10125        let result = canonicalize_signature(&unsorted);
10126        assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
10127    }
10128
10129    // ---------------------------------------------------------------------------
10130    // Phase 0 — format_signature_operator_style (to be implemented in
10131    // signature_factor_and_display todo)
10132    // ---------------------------------------------------------------------------
10133
10134    #[test]
10135    fn format_signature_operator_style_numerator_only() {
10136        let signature = sig(&[("eur", 1)]);
10137        let result = format_signature_operator_style(&signature);
10138        assert_eq!(result, "eur");
10139    }
10140
10141    #[test]
10142    fn format_signature_operator_style_with_denominator() {
10143        let signature = sig(&[("eur", 1), ("hour", -1)]);
10144        let result = format_signature_operator_style(&signature);
10145        assert_eq!(result, "eur/hour");
10146    }
10147
10148    #[test]
10149    fn format_signature_operator_style_denominator_only() {
10150        let signature = sig(&[("meter", -1)]);
10151        let result = format_signature_operator_style(&signature);
10152        assert_eq!(result, "1/meter");
10153    }
10154
10155    #[test]
10156    fn format_signature_operator_style_with_exponents() {
10157        let signature = sig(&[("meter", 2), ("second", -2)]);
10158        let result = format_signature_operator_style(&signature);
10159        assert_eq!(result, "meter^2/second^2");
10160    }
10161
10162    // ---------------------------------------------------------------------------
10163    // Phase 0 — calendar_unit_factor (to be implemented in builtin_calendar_factor_table)
10164    // ---------------------------------------------------------------------------
10165
10166    #[test]
10167    fn calendar_unit_factor_table_completeness() {
10168        // Every SemanticCalendarUnit Display string must resolve to a factor.
10169        // Today SemanticCalendarUnit only has Month and Year; more may be added.
10170        for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
10171            let name = unit.to_string();
10172            assert!(
10173                calendar_unit_factor(&name).is_some(),
10174                "calendar_unit_factor('{}') must return Some",
10175                name
10176            );
10177        }
10178    }
10179
10180    #[test]
10181    fn semantic_calendar_unit_display_returns_singular() {
10182        // Today Month => "month", Year => "year" (plural).
10183        // After singular_calendar_names_everywhere, must be "month" and "year".
10184        assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
10185        assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
10186    }
10187
10188    // ---------------------------------------------------------------------------
10189    // Phase 0 — signature_factor (to be implemented in signature_factor_and_display)
10190    // ---------------------------------------------------------------------------
10191
10192    #[test]
10193    fn signature_factor_with_calendar_units() {
10194        let calendar = test_calendar_type_for_signature_factor();
10195        let unit_index = crate::planning::unit_index::UnitIndex::new();
10196        // month factor = 1, year factor = 12.
10197        // [(month,1),(year,-1)] = 1/12
10198        let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
10199        let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
10200            .expect("must not overflow");
10201        let expected = rational_new(1, 12);
10202        assert_eq!(factor, expected, "month/year factor must be 1/12");
10203    }
10204
10205    fn test_calendar_type_for_signature_factor() -> LemmaType {
10206        use crate::computation::rational::{decimal_to_rational, rational_one};
10207        use crate::literals::{MeasureUnit, MeasureUnits};
10208        use rust_decimal::Decimal;
10209        LemmaType::new(
10210            "calendar".to_string(),
10211            TypeSpecification::Measure {
10212                minimum: None,
10213                maximum: None,
10214                units: MeasureUnits::from(vec![
10215                    MeasureUnit {
10216                        name: "month".to_string(),
10217                        scale: UnitScale::Literal(rational_one()),
10218                        minimum: None,
10219                        maximum: None,
10220                        suggestion_magnitude: None,
10221                        decomposition: calendar_decomposition(),
10222                        derived_measure_factors: Vec::new(),
10223                    },
10224                    MeasureUnit {
10225                        name: "year".to_string(),
10226                        scale: UnitScale::Literal(
10227                            decimal_to_rational(Decimal::from(12)).expect("year factor"),
10228                        ),
10229                        minimum: None,
10230                        maximum: None,
10231                        suggestion_magnitude: None,
10232                        decomposition: calendar_decomposition(),
10233                        derived_measure_factors: Vec::new(),
10234                    },
10235                ]),
10236                traits: vec![MeasureTrait::Calendar],
10237                decomposition: Some(calendar_decomposition()),
10238                unit: None,
10239                help: String::new(),
10240            },
10241            TypeExtends::Primitive,
10242        )
10243    }
10244
10245    #[test]
10246    #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
10247    fn signature_factor_panics_on_unresolved_name() {
10248        let unit_index = crate::planning::unit_index::UnitIndex::new();
10249        let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
10250        signature_factor(&bad_sig, &unit_index, None)
10251            .expect("BUG: signature_factor must panic before returning a failure");
10252    }
10253
10254    #[test]
10255    fn signature_factor_uses_owner_when_expression_index_empty() {
10256        let money = test_money_type_for_signature_factor();
10257        let expression_units = crate::planning::unit_index::UnitIndex::new();
10258        let sig_usd = sig(&[("usd", 1)]);
10259        let factor =
10260            signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
10261        assert_eq!(factor, rational_new(91, 100));
10262    }
10263
10264    fn test_money_type_for_signature_factor() -> LemmaType {
10265        use crate::computation::rational::decimal_to_rational;
10266        use crate::literals::{MeasureUnit, MeasureUnits};
10267        use rust_decimal::Decimal;
10268        LemmaType::new(
10269            "money".to_string(),
10270            TypeSpecification::Measure {
10271                minimum: None,
10272                maximum: None,
10273                units: MeasureUnits::from(vec![
10274                    MeasureUnit {
10275                        name: "eur".to_string(),
10276                        scale: UnitScale::Literal(crate::computation::rational::rational_one()),
10277                        minimum: None,
10278                        maximum: None,
10279                        suggestion_magnitude: None,
10280                        decomposition: BaseMeasureVector::new(),
10281                        derived_measure_factors: Vec::new(),
10282                    },
10283                    MeasureUnit {
10284                        name: "usd".to_string(),
10285                        scale: UnitScale::Literal(
10286                            decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
10287                        ),
10288                        minimum: None,
10289                        maximum: None,
10290                        suggestion_magnitude: None,
10291                        decomposition: BaseMeasureVector::new(),
10292                        derived_measure_factors: Vec::new(),
10293                    },
10294                ]),
10295                traits: Vec::new(),
10296                decomposition: None,
10297                unit: None,
10298                help: String::new(),
10299            },
10300            TypeExtends::Primitive,
10301        )
10302    }
10303
10304    fn measure_type_with_kilogram() -> TypeSpecification {
10305        use crate::computation::rational::rational_one;
10306        use crate::literals::{MeasureUnit, MeasureUnits};
10307        let mut units = MeasureUnits::new();
10308        units.push(MeasureUnit {
10309            name: "kilogram".to_string(),
10310            scale: UnitScale::Literal(rational_one()),
10311            minimum: None,
10312            maximum: None,
10313            suggestion_magnitude: None,
10314            decomposition: BaseMeasureVector::new(),
10315            derived_measure_factors: Vec::new(),
10316        });
10317        TypeSpecification::Measure {
10318            minimum: None,
10319            maximum: None,
10320            units,
10321            traits: Vec::new(),
10322            decomposition: None,
10323            unit: None,
10324            help: String::new(),
10325        }
10326    }
10327
10328    #[test]
10329    fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
10330        let ten = Value::Number(Decimal::from(10));
10331        let measure = LemmaType::primitive(measure_type_with_kilogram());
10332        let err = parser_value_to_value_kind(&ten, &measure, None)
10333            .expect_err("bare number must not bind to measure");
10334        assert!(
10335            err.contains("kilogram"),
10336            "error must hint expected unit, got: {err}"
10337        );
10338    }
10339
10340    #[test]
10341    fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
10342        let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
10343        let measure = LemmaType::primitive(measure_type_with_kilogram());
10344        let kind = parser_value_to_value_kind(&ten_kg, &measure, None)
10345            .expect("10 kilogram must bind to measure");
10346        assert!(matches!(kind, ValueKind::Measure(_)));
10347    }
10348
10349    #[test]
10350    fn parser_value_to_value_kind_rejects_bare_number_for_ratio() {
10351        let ten = Value::Number(Decimal::from(10));
10352        let ratio = LemmaType::primitive(TypeSpecification::ratio());
10353        let err = parser_value_to_value_kind(&ten, &ratio, None)
10354            .expect_err("bare number must not bind to ratio");
10355        assert!(
10356            err.contains("percent"),
10357            "error must hint expected unit, got: {err}"
10358        );
10359    }
10360
10361    #[test]
10362    fn value_kind_matches_spec_rejects_number_for_measure() {
10363        let n = ValueKind::Number(rational_new(10, 1));
10364        assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
10365    }
10366
10367    #[test]
10368    fn value_kind_matches_spec_rejects_number_for_ratio() {
10369        let n = ValueKind::Number(rational_new(1, 2));
10370        assert!(!value_kind_matches_spec(&n, &TypeSpecification::ratio()));
10371    }
10372
10373    #[test]
10374    fn apply_constraint_rejects_inherited_unit_factor_change() {
10375        let mut specs = TypeSpecification::measure();
10376        specs
10377            .apply_constraint(
10378                ConstraintNames {
10379                    type_name: "money",
10380                    data_name: "money",
10381                },
10382                TypeConstraintCommand::Unit,
10383                &unit_factor_arg("eur", 1),
10384                &mut None,
10385                &mut None,
10386                None,
10387            )
10388            .expect("seed eur");
10389        let err = specs
10390            .apply_constraint(
10391                ConstraintNames {
10392                    type_name: "money",
10393                    data_name: "money",
10394                },
10395                TypeConstraintCommand::Unit,
10396                &[
10397                    CommandArg::Label("eur".to_string()),
10398                    CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
10399                ],
10400                &mut None,
10401                &mut None,
10402                None,
10403            )
10404            .expect_err("must not change inherited unit factor");
10405        assert!(err.contains("eur"), "error must name unit, got: {err}");
10406        assert!(
10407            err.contains("inherited") || err.contains("cannot change"),
10408            "error must reject factor change, got: {err}"
10409        );
10410    }
10411
10412    #[test]
10413    fn apply_constraint_allows_additive_unit_on_inherited_spec() {
10414        let mut specs = TypeSpecification::measure();
10415        specs
10416            .apply_constraint(
10417                ConstraintNames {
10418                    type_name: "money",
10419                    data_name: "money",
10420                },
10421                TypeConstraintCommand::Unit,
10422                &unit_factor_arg("eur", 1),
10423                &mut None,
10424                &mut None,
10425                None,
10426            )
10427            .expect("seed eur");
10428        specs
10429            .apply_constraint(
10430                ConstraintNames {
10431                    type_name: "money",
10432                    data_name: "money",
10433                },
10434                TypeConstraintCommand::Unit,
10435                &unit_factor_arg("usd", 1),
10436                &mut None,
10437                &mut None,
10438                None,
10439            )
10440            .expect("add usd");
10441        match &specs {
10442            TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
10443            other => panic!("expected Measure, got {other:?}"),
10444        }
10445    }
10446
10447    #[test]
10448    fn apply_constraint_idempotent_inherited_unit_redeclare() {
10449        let mut specs = TypeSpecification::measure();
10450        specs
10451            .apply_constraint(
10452                ConstraintNames {
10453                    type_name: "money",
10454                    data_name: "money",
10455                },
10456                TypeConstraintCommand::Unit,
10457                &unit_factor_arg("eur", 1),
10458                &mut None,
10459                &mut None,
10460                None,
10461            )
10462            .expect("seed eur");
10463        specs
10464            .apply_constraint(
10465                ConstraintNames {
10466                    type_name: "money",
10467                    data_name: "money",
10468                },
10469                TypeConstraintCommand::Unit,
10470                &unit_factor_arg("eur", 1),
10471                &mut None,
10472                &mut None,
10473                None,
10474            )
10475            .expect("idempotent eur");
10476        match &specs {
10477            TypeSpecification::Measure { units, .. } => {
10478                assert_eq!(units.len(), 1);
10479                assert_eq!(
10480                    *units
10481                        .iter()
10482                        .find(|u| u.name == "eur")
10483                        .expect("eur")
10484                        .require_literal_factor(),
10485                    crate::computation::rational::rational_one()
10486                );
10487            }
10488            other => panic!("expected Measure, got {other:?}"),
10489        }
10490    }
10491
10492    #[test]
10493    fn element_from_range_returns_element_for_every_range_primitive() {
10494        type RangeElementMatcher = fn(&TypeSpecification) -> bool;
10495        let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
10496            (PrimitiveKind::NumberRange, |element| {
10497                matches!(element, TypeSpecification::Number { .. })
10498            }),
10499            (PrimitiveKind::MeasureRange, |element| {
10500                matches!(element, TypeSpecification::Measure { .. })
10501            }),
10502            (PrimitiveKind::RatioRange, |element| {
10503                matches!(element, TypeSpecification::Ratio { .. })
10504            }),
10505            (PrimitiveKind::DateRange, |element| {
10506                matches!(element, TypeSpecification::Date { .. })
10507            }),
10508            (PrimitiveKind::TimeRange, |element| {
10509                matches!(element, TypeSpecification::Time { .. })
10510            }),
10511        ];
10512        for (kind, matches_element) in cases {
10513            let range_spec = type_spec_for_primitive(kind);
10514            let element = range_spec
10515                .element_from_range()
10516                .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
10517            assert!(
10518                matches_element(&element),
10519                "{kind:?} element must match documented mapping, got {element:?}"
10520            );
10521        }
10522    }
10523
10524    #[test]
10525    fn element_from_range_returns_none_for_non_range_primitives() {
10526        let non_range = [
10527            type_spec_for_primitive(PrimitiveKind::Boolean),
10528            type_spec_for_primitive(PrimitiveKind::Measure),
10529            TypeSpecification::Undetermined,
10530            TypeSpecification::veto(),
10531        ];
10532        for spec in non_range {
10533            assert!(
10534                spec.element_from_range().is_none(),
10535                "{spec:?} must not define element_from_range"
10536            );
10537        }
10538    }
10539
10540    fn spec_type(name: &str, effective: crate::parsing::ast::EffectiveDate) -> LemmaType {
10541        LemmaType::primitive(TypeSpecification::Spec {
10542            spec: SpecIdentity {
10543                repository: None,
10544                spec: name.to_string(),
10545                effective: effective.clone(),
10546                slice: effective,
10547            },
10548        })
10549    }
10550
10551    #[test]
10552    fn same_value_type_spec_identity() {
10553        use crate::parsing::ast::EffectiveDate;
10554        let origin = EffectiveDate::Origin;
10555        let dated = EffectiveDate::DateTimeValue(crate::DateTimeValue {
10556            year: 2026,
10557            month: 1,
10558            day: 1,
10559            hour: 0,
10560            minute: 0,
10561            second: 0,
10562            microsecond: 0,
10563            timezone: None,
10564            granularity: crate::literals::DateGranularity::Full,
10565        });
10566        let tax = spec_type("tax_bracket", origin.clone());
10567        let tax_again = spec_type("tax_bracket", origin.clone());
10568        let other = spec_type("menu_item", origin);
10569        let tax_dated = spec_type("tax_bracket", dated.clone());
10570        assert!(tax.same_value_type(&tax_again));
10571        assert!(!tax.same_value_type(&other));
10572        assert!(!tax.same_value_type(&tax_dated));
10573        let mut tax_later_slice = tax.clone();
10574        match &mut tax_later_slice.specifications {
10575            TypeSpecification::Spec { spec } => spec.slice = dated,
10576            other => panic!("expected Spec, got {other:?}"),
10577        }
10578        assert!(!tax.same_value_type(&tax_later_slice));
10579        let mut other_repo = tax.clone();
10580        match &mut other_repo.specifications {
10581            TypeSpecification::Spec { spec } => spec.repository = Some("@jack/finance".to_string()),
10582            other => panic!("expected Spec, got {other:?}"),
10583        }
10584        assert!(!tax.same_value_type(&other_repo));
10585    }
10586
10587    #[test]
10588    fn spec_identity_displays_repository_space_spec_space_effective() {
10589        use crate::parsing::ast::EffectiveDate;
10590        let workspace = SpecIdentity {
10591            repository: None,
10592            spec: "tax_bracket".to_string(),
10593            effective: EffectiveDate::Origin,
10594            slice: EffectiveDate::Origin,
10595        };
10596        assert_eq!(workspace.to_string(), "tax_bracket");
10597        let start =
10598            EffectiveDate::DateTimeValue(DateTimeValue::from_str("2026-01-01").expect("date"));
10599        let dated = SpecIdentity {
10600            repository: Some("@jack/finance".to_string()),
10601            spec: "tax_bracket".to_string(),
10602            effective: start.clone(),
10603            slice: start,
10604        };
10605        assert_eq!(
10606            dated.to_string(),
10607            format!(
10608                "@jack/finance tax_bracket {}",
10609                DateTimeValue::from_str("2026-01-01").expect("date")
10610            )
10611        );
10612        let later_slice = SpecIdentity {
10613            slice: EffectiveDate::DateTimeValue(
10614                DateTimeValue::from_str("2026-07-01").expect("date"),
10615            ),
10616            ..workspace
10617        };
10618        assert_eq!(
10619            later_slice.to_string(),
10620            format!(
10621                "tax_bracket slice {}",
10622                DateTimeValue::from_str("2026-07-01").expect("date")
10623            )
10624        );
10625    }
10626
10627    #[test]
10628    fn type_family_displays_declaring_spec_then_type() {
10629        let workspace = TypeFamily {
10630            repository: None,
10631            spec: "pricing".to_string(),
10632            effective: crate::parsing::ast::EffectiveDate::Origin,
10633            name: "money".to_string(),
10634        };
10635        assert_eq!(workspace.to_string(), "pricing.money");
10636        let stdlib = TypeFamily {
10637            repository: Some("lemma".to_string()),
10638            spec: "units".to_string(),
10639            effective: crate::parsing::ast::EffectiveDate::Origin,
10640            name: "mass".to_string(),
10641        };
10642        assert_eq!(stdlib.to_string(), "lemma units.mass");
10643        let dated = TypeFamily {
10644            repository: None,
10645            spec: "pricing".to_string(),
10646            effective: crate::parsing::ast::EffectiveDate::DateTimeValue(
10647                DateTimeValue::from_str("2024-01-01").expect("date"),
10648            ),
10649            name: "money".to_string(),
10650        };
10651        assert_eq!(dated.to_string(), "pricing 2024-01-01.money");
10652    }
10653
10654    #[test]
10655    fn piecewise_type_keeps_spec_identity() {
10656        use crate::parsing::ast::EffectiveDate;
10657        use crate::planning::typing::{piecewise_type, MeasureScope};
10658        use crate::planning::unit_index::UnitIndex;
10659        let tax = Arc::new(spec_type("tax_bracket", EffectiveDate::Origin));
10660        let unit_index = UnitIndex::new();
10661        let signature_index = crate::computation::arithmetic::SignatureIndex::new();
10662        let named_types = IndexMap::new();
10663        let scope = MeasureScope {
10664            unit_index: &unit_index,
10665            signature_index: &signature_index,
10666            named_types: &named_types,
10667        };
10668        let picked = piecewise_type([Arc::new(LemmaType::veto_type()), Arc::clone(&tax)], &scope);
10669        assert!(picked.same_value_type(&tax));
10670    }
10671
10672    #[test]
10673    #[should_panic(expected = "BUG: literal factor required")]
10674    fn measure_unit_factor_panics_on_reference_scale() {
10675        let units = crate::literals::MeasureUnits::from(vec![
10676            crate::literals::MeasureUnit::from_reference(
10677                "gbp".to_string(),
10678                "rate".to_string(),
10679                Vec::new(),
10680                "rate".to_string(),
10681            ),
10682        ]);
10683        let lemma_type = LemmaType::new(
10684            "money".to_string(),
10685            TypeSpecification::Measure {
10686                units,
10687                traits: vec![],
10688                decomposition: None,
10689                minimum: None,
10690                maximum: None,
10691                unit: None,
10692                help: String::new(),
10693            },
10694            TypeExtends::Primitive,
10695        );
10696        let _ = lemma_type.measure_unit_factor("gbp");
10697    }
10698}