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 { factors, .. } = &mut unit.scale {
6338                    for (target, _) in factors.iter_mut() {
6339                        match target {
6340                            ReferenceTarget::Data(path) => path.segments.insert(0, segment.clone()),
6341                            ReferenceTarget::Rule(rule) => rule.segments.insert(0, segment.clone()),
6342                        }
6343                    }
6344                }
6345            }
6346        }
6347        fn prefix_specifications(specifications: &mut TypeSpecification, segment: &PathSegment) {
6348            match specifications {
6349                TypeSpecification::Measure { units, .. }
6350                | TypeSpecification::MeasureRange { units, .. } => {
6351                    prefix_units(units, segment);
6352                }
6353                TypeSpecification::List { element, .. } => {
6354                    let prefixed = element.with_scale_import_hop(segment);
6355                    *element = Arc::new(prefixed);
6356                }
6357                _ => {}
6358            }
6359        }
6360        let mut out = self.clone();
6361        prefix_specifications(&mut out.specifications, segment);
6362        out
6363    }
6364
6365    /// Scale factors of `unit_name` when that unit is reference-scaled on this type.
6366    /// Targets are in the coordinates of the spec that holds this type Arc
6367    /// (`uses` hops are already prefixed). Empty when the unit is not dynamic.
6368    #[must_use]
6369    pub(crate) fn unit_scale_factors(&self, unit_name: &str) -> &[(ReferenceTarget, i32)] {
6370        self.measure_units_table()
6371            .and_then(|units| units.get(unit_name).ok())
6372            .map(MeasureUnit::resolved_scale_factors)
6373            .unwrap_or(&[])
6374    }
6375
6376    /// Every scale target of every reference-scaled unit on this measure type,
6377    /// in declaration order (factors flattened per unit).
6378    #[must_use]
6379    pub(crate) fn dynamic_scale_targets(&self) -> Vec<ReferenceTarget> {
6380        let Some(units) = self.measure_units_table() else {
6381            return Vec::new();
6382        };
6383        units
6384            .iter()
6385            .flat_map(|unit| {
6386                unit.resolved_scale_factors()
6387                    .iter()
6388                    .map(|(target, _)| target.clone())
6389            })
6390            .collect()
6391    }
6392
6393    /// Written unit on a measure or ratio specification, if any.
6394    #[must_use]
6395    pub fn unit(&self) -> Option<&str> {
6396        match &self.specifications {
6397            TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
6398                unit.as_deref()
6399            }
6400            _ => None,
6401        }
6402    }
6403
6404    /// Get the type name, or a default based on the type specification
6405    pub fn name(&self) -> String {
6406        self.name
6407            .clone()
6408            .unwrap_or_else(|| self.specifications.to_string())
6409    }
6410
6411    /// Check if this type is boolean
6412    pub fn is_boolean(&self) -> bool {
6413        matches!(&self.specifications, TypeSpecification::Boolean { .. })
6414    }
6415
6416    pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
6417        matches!(
6418            (kind, &self.specifications),
6419            (PrimitiveKind::Number, TypeSpecification::Number { .. })
6420                | (PrimitiveKind::Text, TypeSpecification::Text { .. })
6421                | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
6422                | (PrimitiveKind::Date, TypeSpecification::Date { .. })
6423                | (PrimitiveKind::Time, TypeSpecification::Time { .. })
6424                | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
6425                | (PrimitiveKind::Measure, TypeSpecification::Measure { .. })
6426        )
6427    }
6428
6429    /// Check if this type is measure
6430    pub fn is_measure(&self) -> bool {
6431        matches!(&self.specifications, TypeSpecification::Measure { .. })
6432    }
6433
6434    pub fn is_measure_range(&self) -> bool {
6435        matches!(&self.specifications, TypeSpecification::MeasureRange { .. })
6436    }
6437
6438    /// Check if this type is number (dimensionless)
6439    pub fn is_number(&self) -> bool {
6440        matches!(&self.specifications, TypeSpecification::Number { .. })
6441    }
6442
6443    pub fn is_number_range(&self) -> bool {
6444        matches!(&self.specifications, TypeSpecification::NumberRange { .. })
6445    }
6446
6447    /// Check if this type is numeric (either measure or number)
6448    pub fn is_numeric(&self) -> bool {
6449        matches!(
6450            &self.specifications,
6451            TypeSpecification::Measure { .. } | TypeSpecification::Number { .. }
6452        )
6453    }
6454
6455    /// Check if this type is text
6456    pub fn is_text(&self) -> bool {
6457        matches!(&self.specifications, TypeSpecification::Text { .. })
6458    }
6459
6460    /// Check if this type is date
6461    pub fn is_date(&self) -> bool {
6462        matches!(&self.specifications, TypeSpecification::Date { .. })
6463    }
6464
6465    pub fn is_date_range(&self) -> bool {
6466        matches!(&self.specifications, TypeSpecification::DateRange { .. })
6467    }
6468
6469    pub fn is_time_range(&self) -> bool {
6470        matches!(&self.specifications, TypeSpecification::TimeRange { .. })
6471    }
6472
6473    /// Check if this type is time
6474    pub fn is_time(&self) -> bool {
6475        matches!(&self.specifications, TypeSpecification::Time { .. })
6476    }
6477
6478    pub fn has_trait_duration(&self) -> bool {
6479        match &self.specifications {
6480            TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Duration),
6481            _ => false,
6482        }
6483    }
6484
6485    pub fn is_duration_like_measure(&self) -> bool {
6486        if !self.is_measure() {
6487            return false;
6488        }
6489        if self.has_trait_duration() {
6490            return true;
6491        }
6492        self.is_anonymous_measure()
6493            && self
6494                .measure_type_decomposition()
6495                .is_some_and(|d| *d == duration_decomposition())
6496    }
6497
6498    pub fn is_duration_like(&self) -> bool {
6499        self.is_duration_like_measure()
6500    }
6501
6502    pub fn has_trait_calendar(&self) -> bool {
6503        match &self.specifications {
6504            TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Calendar),
6505            _ => false,
6506        }
6507    }
6508
6509    pub fn is_calendar_like_measure(&self) -> bool {
6510        if !self.is_measure() {
6511            return false;
6512        }
6513        if self.has_trait_calendar() {
6514            return true;
6515        }
6516        self.is_anonymous_measure()
6517            && self
6518                .measure_type_decomposition()
6519                .is_some_and(|d| *d == calendar_decomposition())
6520    }
6521
6522    pub fn is_calendar_like(&self) -> bool {
6523        self.is_calendar_like_measure()
6524    }
6525
6526    /// Check if this type is ratio
6527    pub fn is_ratio(&self) -> bool {
6528        matches!(&self.specifications, TypeSpecification::Ratio { .. })
6529    }
6530
6531    pub fn is_ratio_range(&self) -> bool {
6532        matches!(&self.specifications, TypeSpecification::RatioRange { .. })
6533    }
6534
6535    pub fn is_calendar_measure_range(&self) -> bool {
6536        matches!(
6537            &self.specifications,
6538            TypeSpecification::MeasureRange { decomposition: Some(decomposition), .. }
6539                if *decomposition == calendar_decomposition()
6540        )
6541    }
6542
6543    pub fn is_calendar_like_range(&self) -> bool {
6544        self.is_calendar_measure_range()
6545    }
6546
6547    pub fn is_range(&self) -> bool {
6548        matches!(
6549            &self.specifications,
6550            TypeSpecification::DateRange { .. }
6551                | TypeSpecification::TimeRange { .. }
6552                | TypeSpecification::NumberRange { .. }
6553                | TypeSpecification::MeasureRange { .. }
6554                | TypeSpecification::RatioRange { .. }
6555        )
6556    }
6557
6558    /// Check if this type is a spec instance.
6559    pub fn is_spec(&self) -> bool {
6560        matches!(&self.specifications, TypeSpecification::Spec { .. })
6561    }
6562
6563    pub fn is_list(&self) -> bool {
6564        matches!(&self.specifications, TypeSpecification::List { .. })
6565    }
6566
6567    pub fn list_element_spec(&self) -> Option<&TypeSpecification> {
6568        match &self.specifications {
6569            TypeSpecification::List { element, .. } => Some(&element.specifications),
6570            _ => None,
6571        }
6572    }
6573
6574    pub fn list_of(element: Arc<LemmaType>) -> Self {
6575        Self {
6576            name: Some(format!("{} list", element.name())),
6577            specifications: TypeSpecification::List {
6578                element,
6579                key: ListKey::Numbered,
6580                help: String::new(),
6581            },
6582            extends: TypeExtends::Primitive,
6583            options: TypeOptions::default(),
6584        }
6585    }
6586
6587    /// Element type of a list type (`money` for `money list`).
6588    pub fn list_element_lemma_type(&self) -> &Arc<LemmaType> {
6589        let TypeSpecification::List { element, .. } = &self.specifications else {
6590            panic!(
6591                "BUG: list_element_lemma_type on non-list type {}",
6592                self.name()
6593            );
6594        };
6595        element
6596    }
6597
6598    /// Spec identity when this type is a spec instance.
6599    #[must_use]
6600    pub fn spec_identity(&self) -> Option<&SpecIdentity> {
6601        match &self.specifications {
6602            TypeSpecification::Spec { spec } => Some(spec),
6603            _ => None,
6604        }
6605    }
6606
6607    /// Check if this type is veto
6608    pub fn vetoed(&self) -> bool {
6609        matches!(&self.specifications, TypeSpecification::Veto { .. })
6610    }
6611
6612    /// True if this type is the undetermined sentinel (type could not be inferred).
6613    pub fn is_undetermined(&self) -> bool {
6614        matches!(&self.specifications, TypeSpecification::Undetermined)
6615    }
6616
6617    /// Check if two types have the same base type specification (ignoring constraints)
6618    pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
6619        use TypeSpecification::*;
6620        matches!(
6621            (&self.specifications, &other.specifications),
6622            (Boolean { .. }, Boolean { .. })
6623                | (Number { .. }, Number { .. })
6624                | (NumberRange { .. }, NumberRange { .. })
6625                | (Measure { .. }, Measure { .. })
6626                | (MeasureRange { .. }, MeasureRange { .. })
6627                | (Text { .. }, Text { .. })
6628                | (Date { .. }, Date { .. })
6629                | (DateRange { .. }, DateRange { .. })
6630                | (Time { .. }, Time { .. })
6631                | (TimeRange { .. }, TimeRange { .. })
6632                | (Ratio { .. }, Ratio { .. })
6633                | (RatioRange { .. }, RatioRange { .. })
6634                | (Veto { .. }, Veto { .. })
6635                | (Undetermined, Undetermined)
6636                | (Spec { .. }, Spec { .. })
6637                | (List { .. }, List { .. })
6638        )
6639    }
6640
6641    /// Root of the custom extension chain; `None` for primitive and anonymous types.
6642    #[must_use]
6643    pub fn family(&self) -> Option<&TypeFamily> {
6644        match &self.extends {
6645            TypeExtends::Custom { family, .. } => Some(family),
6646            TypeExtends::Primitive => None,
6647        }
6648    }
6649
6650    /// Element type of a measure or ratio range. Keeps the range's [`TypeExtends`]
6651    /// (and thus its [`TypeFamily`]). Name is the family's root name when the
6652    /// range extends a named type; otherwise the range's own name.
6653    #[must_use]
6654    pub fn range_element_type(&self) -> Arc<LemmaType> {
6655        let element_spec = self
6656            .specifications
6657            .element_from_range()
6658            .expect("BUG: range_element_type requires a range specification");
6659        let name = match &self.extends {
6660            TypeExtends::Custom { family, .. } => Some(family.name.clone()),
6661            TypeExtends::Primitive => self.name.clone(),
6662        };
6663        Arc::new(LemmaType {
6664            name,
6665            specifications: element_spec,
6666            extends: self.extends.clone(),
6667            options: TypeOptions::default(),
6668        })
6669    }
6670
6671    /// Unit family of a measure or measure-range type.
6672    #[must_use]
6673    pub fn measure_family(&self) -> Option<&TypeFamily> {
6674        (self.is_measure() || self.is_measure_range())
6675            .then(|| self.family())
6676            .flatten()
6677    }
6678
6679    /// Type of the values this type holds: itself, or its (nested) list element.
6680    #[must_use]
6681    pub(crate) fn held_value_type(&self) -> &LemmaType {
6682        let mut current = self;
6683        while current.is_list() {
6684            current = current.list_element_lemma_type().as_ref();
6685        }
6686        current
6687    }
6688
6689    /// Measure family of the values this type holds: its own, or its list element's.
6690    #[must_use]
6691    pub(crate) fn held_measure_family(&self) -> Option<&TypeFamily> {
6692        self.held_value_type().measure_family()
6693    }
6694
6695    /// Unit family of a ratio or ratio-range type.
6696    #[must_use]
6697    pub fn ratio_family(&self) -> Option<&TypeFamily> {
6698        (self.is_ratio() || self.is_ratio_range())
6699            .then(|| self.family())
6700            .flatten()
6701    }
6702
6703    /// Unit family of a measure or ratio type.
6704    #[must_use]
6705    pub(crate) fn unit_family(&self) -> Option<&TypeFamily> {
6706        self.measure_family().or_else(|| self.ratio_family())
6707    }
6708
6709    /// Root type name of [`Self::measure_family`], for display.
6710    #[must_use]
6711    pub fn measure_family_name(&self) -> Option<&str> {
6712        self.measure_family().map(|family| family.name.as_str())
6713    }
6714
6715    /// Root type name of [`Self::ratio_family`], for display.
6716    #[must_use]
6717    pub fn ratio_family_name(&self) -> Option<&str> {
6718        self.ratio_family().map(|family| family.name.as_str())
6719    }
6720
6721    /// Same base kind and, for measures / measure ranges, the same named family:
6722    /// the relation under which two types describe the same values. Names and
6723    /// constraints may differ. Ratio and RatioRange stay base-kind only.
6724    #[must_use]
6725    pub(crate) fn same_value_type(&self, other: &LemmaType) -> bool {
6726        if !self.has_same_base_type(other) {
6727            return false;
6728        }
6729        if self.is_measure() {
6730            return match (self.measure_family(), other.measure_family()) {
6731                (Some(_), Some(_)) => self.same_measure_family(other),
6732                (None, None) => {
6733                    self.measure_type_decomposition() == other.measure_type_decomposition()
6734                }
6735                _ => false,
6736            };
6737        }
6738        if let (
6739            TypeSpecification::Spec { spec: self_spec },
6740            TypeSpecification::Spec { spec: other_spec },
6741        ) = (&self.specifications, &other.specifications)
6742        {
6743            return self_spec == other_spec;
6744        }
6745        if let (
6746            TypeSpecification::List {
6747                element: self_element,
6748                key: self_key,
6749                ..
6750            },
6751            TypeSpecification::List {
6752                element: other_element,
6753                key: other_key,
6754                ..
6755            },
6756        ) = (&self.specifications, &other.specifications)
6757        {
6758            return self_key == other_key && self_element.same_value_type(other_element);
6759        }
6760        if self.is_measure_range() {
6761            match (self.family(), other.family()) {
6762                (Some(self_family), Some(other_family)) => self_family == other_family,
6763                (None, None) => {
6764                    let self_element = self
6765                        .specifications
6766                        .element_from_range()
6767                        .expect("BUG: MeasureRange always defines element_from_range");
6768                    let other_element = other
6769                        .specifications
6770                        .element_from_range()
6771                        .expect("BUG: MeasureRange always defines element_from_range");
6772                    match (&self_element, &other_element) {
6773                        (
6774                            TypeSpecification::Measure {
6775                                decomposition: self_decomp,
6776                                ..
6777                            },
6778                            TypeSpecification::Measure {
6779                                decomposition: other_decomp,
6780                                ..
6781                            },
6782                        ) => self_decomp == other_decomp,
6783                        _ => unreachable!("BUG: element_from_range of MeasureRange yields Measure"),
6784                    }
6785                }
6786                _ => false,
6787            }
6788        } else {
6789            true
6790        }
6791    }
6792
6793    /// Drop a written unit so a path/reference expression type does not claim
6794    /// the stored value named that unit. Literals and `as <unit>` results keep
6795    /// units via [`Self::with_unit`].
6796    #[must_use]
6797    pub(crate) fn without_written_unit(&self) -> Self {
6798        let mut cleared = self.clone();
6799        match &mut cleared.specifications {
6800            TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
6801                *unit = None;
6802            }
6803            _ => {}
6804        }
6805        cleared
6806    }
6807
6808    /// Clear slot bounds, help, and written unit so a computed/generalized type
6809    /// does not carry either operand's constraints.
6810    #[must_use]
6811    pub(crate) fn without_slot_bounds(&self) -> Self {
6812        let mut cleared = self.without_written_unit();
6813        match &mut cleared.specifications {
6814            TypeSpecification::Measure {
6815                minimum,
6816                maximum,
6817                help,
6818                ..
6819            } => {
6820                *minimum = None;
6821                *maximum = None;
6822                *help = String::new();
6823            }
6824            TypeSpecification::Ratio {
6825                minimum,
6826                maximum,
6827                help,
6828                ..
6829            } => {
6830                *minimum = None;
6831                *maximum = None;
6832                *help = String::new();
6833            }
6834            TypeSpecification::Number {
6835                minimum,
6836                maximum,
6837                help,
6838                ..
6839            } => {
6840                *minimum = None;
6841                *maximum = None;
6842                *help = String::new();
6843            }
6844            TypeSpecification::Date {
6845                minimum,
6846                maximum,
6847                help,
6848                ..
6849            } => {
6850                *minimum = None;
6851                *maximum = None;
6852                *help = String::new();
6853            }
6854            TypeSpecification::Time {
6855                minimum,
6856                maximum,
6857                help,
6858                ..
6859            } => {
6860                *minimum = None;
6861                *maximum = None;
6862                *help = String::new();
6863            }
6864            TypeSpecification::MeasureRange {
6865                minimum,
6866                maximum,
6867                help,
6868                ..
6869            } => {
6870                *minimum = None;
6871                *maximum = None;
6872                *help = String::new();
6873            }
6874            TypeSpecification::DateRange {
6875                minimum,
6876                maximum,
6877                help,
6878                ..
6879            } => {
6880                *minimum = None;
6881                *maximum = None;
6882                *help = String::new();
6883            }
6884            TypeSpecification::TimeRange {
6885                minimum,
6886                maximum,
6887                help,
6888                ..
6889            } => {
6890                *minimum = None;
6891                *maximum = None;
6892                *help = String::new();
6893            }
6894            TypeSpecification::NumberRange {
6895                minimum,
6896                maximum,
6897                help,
6898                ..
6899            } => {
6900                *minimum = None;
6901                *maximum = None;
6902                *help = String::new();
6903            }
6904            TypeSpecification::RatioRange {
6905                minimum,
6906                maximum,
6907                help,
6908                ..
6909            } => {
6910                *minimum = None;
6911                *maximum = None;
6912                *help = String::new();
6913            }
6914            TypeSpecification::Boolean { help, .. } | TypeSpecification::Text { help, .. } => {
6915                *help = String::new();
6916            }
6917            TypeSpecification::Veto { .. }
6918            | TypeSpecification::Undetermined
6919            | TypeSpecification::Spec { .. }
6920            | TypeSpecification::List { .. } => {}
6921        }
6922        cleared
6923    }
6924
6925    /// Returns true if both types are measure and belong to the same named measure family.
6926    #[must_use]
6927    pub fn same_measure_family(&self, other: &LemmaType) -> bool {
6928        if !self.is_measure() || !other.is_measure() {
6929            return false;
6930        }
6931        match (self.measure_family(), other.measure_family()) {
6932            (Some(self_family), Some(other_family)) => self_family == other_family,
6933            _ => false,
6934        }
6935    }
6936
6937    /// Structural equality for interface drift: same as [`PartialEq`] but family
6938    /// versions (`TypeFamily::effective`) are ignored. Unpinned imports spanning
6939    /// unchanged versions of a dependency compare equal; real changes still differ.
6940    #[must_use]
6941    pub fn same_interface(&self, other: &Self) -> bool {
6942        self.with_families_without_effective() == other.with_families_without_effective()
6943    }
6944
6945    /// Like [`Self::same_interface`], but [`TypeOptions`] may differ. Used for
6946    /// `uses … -> effective` where option sets change across temporal versions.
6947    #[must_use]
6948    pub fn same_interface_ignoring_options(&self, other: &Self) -> bool {
6949        let mut left = self.with_families_without_effective();
6950        let mut right = other.with_families_without_effective();
6951        left.options = TypeOptions::default();
6952        right.options = TypeOptions::default();
6953        left == right
6954    }
6955
6956    /// Clone with every [`TypeFamily::effective`] set to [`EffectiveDate::Origin`].
6957    #[must_use]
6958    pub fn with_families_without_effective(&self) -> Self {
6959        Self {
6960            name: self.name.clone(),
6961            specifications: self.specifications.with_families_without_effective(),
6962            extends: self.extends.with_families_without_effective(),
6963            options: self.options.clone(),
6964        }
6965    }
6966
6967    #[must_use]
6968    pub fn compatible_with_anonymous_measure(&self, other: &LemmaType) -> bool {
6969        if !self.is_measure() || !other.is_measure() {
6970            return false;
6971        }
6972        if !self.is_anonymous_measure() && !other.is_anonymous_measure() {
6973            return false;
6974        }
6975        match (
6976            self.measure_type_decomposition(),
6977            other.measure_type_decomposition(),
6978        ) {
6979            (Some(a), Some(b)) => a == b,
6980            _ => false,
6981        }
6982    }
6983
6984    /// Create a Veto LemmaType
6985    pub fn veto_type() -> Self {
6986        Self::primitive(TypeSpecification::veto())
6987    }
6988
6989    /// LemmaType sentinel for undetermined type (used during inference when a type cannot be determined).
6990    /// Propagates through expressions and is never present in a validated graph.
6991    pub fn undetermined_type() -> Self {
6992        Self::primitive(TypeSpecification::Undetermined)
6993    }
6994
6995    /// Input scale on a number declaration. Rejects a supplied value with more
6996    /// fractional digits. Not applied when a result is published.
6997    pub fn decimal_places(&self) -> Option<u8> {
6998        match &self.specifications {
6999            TypeSpecification::Number { decimals, .. } => *decimals,
7000            _ => None,
7001        }
7002    }
7003
7004    /// Convert a rational magnitude to an API [`Decimal`] (28-scale boundary).
7005    ///
7006    /// Returns [`NumericFailure::Overflow`] when |magnitude| > Decimal::MAX
7007    /// (callers map this to a decimal-limit Veto). Does not apply `decimals`.
7008    pub fn try_rational_as_decimal(
7009        &self,
7010        magnitude: &crate::computation::rational::RationalInteger,
7011    ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
7012        let decimal = magnitude.try_to_decimal()?;
7013        Ok(format_decimal_for_api(decimal))
7014    }
7015
7016    /// Convert a canonical measure magnitude in the named declared unit to an API [`Decimal`].
7017    pub fn try_measure_canonical_as_decimal_in_unit(
7018        &self,
7019        canonical_magnitude: &crate::computation::rational::RationalInteger,
7020        unit_name: &str,
7021    ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
7022        use crate::computation::rational::checked_div;
7023        let unit_factor = self.measure_unit_factor(unit_name);
7024        let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
7025        self.try_rational_as_decimal(&magnitude_in_unit)
7026    }
7027
7028    /// Convert a canonical ratio magnitude in the named declared unit to an API [`Decimal`].
7029    pub fn try_ratio_canonical_as_decimal_in_unit(
7030        &self,
7031        canonical_magnitude: &crate::computation::rational::RationalInteger,
7032        unit_name: &str,
7033    ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
7034        use crate::computation::rational::checked_mul;
7035        let units = match &self.specifications {
7036            TypeSpecification::Ratio { units, .. } => units,
7037            _ => unreachable!(
7038                "BUG: try_ratio_canonical_as_decimal_in_unit called on non-ratio type {}",
7039                self.name()
7040            ),
7041        };
7042        let ratio_unit = units
7043            .iter()
7044            .find(|unit| unit.name == unit_name)
7045            .unwrap_or_else(|| {
7046                let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
7047                unreachable!(
7048                    "BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
7049                    unit_name,
7050                    self.name(),
7051                    valid.join(", ")
7052                )
7053            });
7054        let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
7055        self.try_rational_as_decimal(&magnitude_in_unit)
7056    }
7057
7058    /// Get an example value string for this type, suitable for UI help text
7059    pub fn example_value(&self) -> &'static str {
7060        match &self.specifications {
7061            TypeSpecification::Text { .. } => "\"hello world\"",
7062            TypeSpecification::Measure { .. } => "12.50 eur",
7063            TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
7064            TypeSpecification::Number { .. } => "3.14",
7065            TypeSpecification::NumberRange { .. } => "0...100",
7066            TypeSpecification::Boolean { .. } => "true",
7067            TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
7068            TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
7069            TypeSpecification::TimeRange { .. } => "09:00...17:00",
7070            TypeSpecification::Veto { .. } => "veto",
7071            TypeSpecification::Time { .. } => "14:30:00",
7072            TypeSpecification::Ratio { .. } => "50%",
7073            TypeSpecification::RatioRange { .. } => "10%...50%",
7074            TypeSpecification::Undetermined => unreachable!(
7075                "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
7076            ),
7077            TypeSpecification::Spec { .. } => unreachable!(
7078                "BUG: example_value called on a spec type; spec instances are not prompted data"
7079            ),
7080            TypeSpecification::List { element, .. } => element.example_value(),
7081        }
7082    }
7083
7084    /// Factor for a unit of this measure type (for unit conversion during evaluation only).
7085    /// Planning must validate conversions first and return Error for invalid units.
7086    /// If called with a non-measure type or unknown unit name, panics (invariant violation).
7087    #[must_use]
7088    /// Returns the resolved `BaseMeasureVector` for Measure types, or `None` if
7089    /// the decomposition pass has not yet resolved this type.
7090    /// Panics if called on non-Measure types.
7091    pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
7092        match &self.specifications {
7093            TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
7094            _ => unreachable!(
7095                "BUG: measure_type_decomposition called on non-measure type {}",
7096                self.name()
7097            ),
7098        }
7099    }
7100
7101    /// One family dimension with exponent 1 — the shape that may cross-family
7102    /// relabel when the expression also names a unit (`5 eur as kg`).
7103    #[must_use]
7104    pub(crate) fn is_simple_base_measure(&self) -> bool {
7105        if !self.is_measure() {
7106            return false;
7107        }
7108        match self.measure_type_decomposition() {
7109            Some(decomposition) if decomposition.len() == 1 => {
7110                decomposition.values().next() == Some(&1)
7111            }
7112            _ => false,
7113        }
7114    }
7115
7116    /// Declared measure unit table, when this type is a measure or measure range.
7117    #[must_use]
7118    pub fn measure_units_table(&self) -> Option<&MeasureUnits> {
7119        match &self.specifications {
7120            TypeSpecification::Measure { units, .. }
7121            | TypeSpecification::MeasureRange { units, .. } => Some(units),
7122            _ => None,
7123        }
7124    }
7125
7126    /// Whether any declared unit of this measure family has a reference scale.
7127    /// List types follow their element.
7128    #[must_use]
7129    pub fn has_dynamic_units(&self) -> bool {
7130        if self
7131            .measure_units_table()
7132            .is_some_and(|units| units.iter().any(MeasureUnit::is_dynamic))
7133        {
7134            return true;
7135        }
7136        match &self.specifications {
7137            TypeSpecification::List { element, .. } => element.has_dynamic_units(),
7138            _ => false,
7139        }
7140    }
7141
7142    /// Whether `unit_name` is a reference-scaled unit of this measure type.
7143    #[must_use]
7144    pub fn unit_is_dynamic(&self, unit_name: &str) -> bool {
7145        self.measure_units_table()
7146            .and_then(|units| units.get(unit_name).ok())
7147            .is_some_and(MeasureUnit::is_dynamic)
7148    }
7149
7150    /// Unit name chosen for measure display / wire `unit` under `runtime_scales`.
7151    #[must_use]
7152    pub(crate) fn measure_display_unit_name(
7153        &self,
7154        runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
7155    ) -> Option<&str> {
7156        match &self.specifications {
7157            TypeSpecification::Measure { units, .. } if !units.is_empty() => Some(
7158                measure_display_unit(units, self, runtime_scales)
7159                    .name
7160                    .as_str(),
7161            ),
7162            _ => None,
7163        }
7164    }
7165
7166    /// Fallback unit for a measure with no binding: factor-1, then the first
7167    /// literal-factor unit, then the first declared unit.
7168    #[must_use]
7169    pub fn measure_fallback_unit(&self) -> Option<&MeasureUnit> {
7170        let units = self.measure_units_table()?;
7171        units
7172            .iter()
7173            .find(|unit| unit.is_canonical_factor())
7174            .or_else(|| units.iter().find(|unit| unit.literal_factor().is_some()))
7175            .or_else(|| units.iter().next())
7176    }
7177
7178    /// Runtime unit signature for measure arithmetic and display.
7179    ///
7180    /// - Named measure with units: `[(binding_or_fallback_unit_name, 1)]`. Arithmetic
7181    ///   expands this via `expand_signature_to_base_units` using the unit table.
7182    /// - Anonymous / no units: decomposition converted to signature form.
7183    ///
7184    /// Fallback order when there is no binding: the factor-1 unit, the first unit
7185    /// with a literal factor, the first declared unit.
7186    #[must_use]
7187    pub fn measure_runtime_signature(&self) -> Vec<(String, i32)> {
7188        if let Some(binding) = self.unit() {
7189            return vec![(binding.to_string(), 1)];
7190        }
7191        match &self.specifications {
7192            TypeSpecification::Measure {
7193                units,
7194                decomposition,
7195                ..
7196            } => {
7197                if let Some(unit) = self.measure_fallback_unit() {
7198                    return vec![(unit.name.clone(), 1)];
7199                }
7200                if units.is_empty() {
7201                    return base_measure_vector_as_signature(
7202                        decomposition
7203                            .as_ref()
7204                            .expect("BUG: measure type without units must carry a decomposition"),
7205                    );
7206                }
7207                unreachable!("BUG: non-empty measure unit table has no fallback unit");
7208            }
7209            TypeSpecification::MeasureRange {
7210                units,
7211                decomposition,
7212                ..
7213            } => {
7214                if let Some(unit) = self.measure_fallback_unit() {
7215                    return vec![(unit.name.clone(), 1)];
7216                }
7217                if units.is_empty() {
7218                    return base_measure_vector_as_signature(
7219                        decomposition
7220                            .as_ref()
7221                            .expect("BUG: measure type without units must carry a decomposition"),
7222                    );
7223                }
7224                unreachable!("BUG: non-empty measure-range unit table has no fallback unit");
7225            }
7226            _ => unreachable!(
7227                "BUG: measure_runtime_signature called on non-measure type {}",
7228                self.name()
7229            ),
7230        }
7231    }
7232
7233    /// Primary declared ratio unit name when the type carries a non-empty unit table.
7234    #[must_use]
7235    pub fn ratio_primary_unit(&self) -> Option<&str> {
7236        match &self.specifications {
7237            TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
7238                units.iter().next().map(|unit| unit.name.as_str())
7239            }
7240            TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
7241                units.iter().next().map(|unit| unit.name.as_str())
7242            }
7243            _ => None,
7244        }
7245    }
7246
7247    /// Returns true if this is an anonymous (no-name) Measure — i.e. an anonymous
7248    /// intermediate produced by cross-axis arithmetic.
7249    pub fn is_anonymous_measure(&self) -> bool {
7250        self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
7251    }
7252
7253    /// Build an anonymous `LemmaType` for a given dimensional decomposition.
7254    /// Used at plan time to represent the inferred type of cross-axis intermediates.
7255    /// Runtime unit signature is derived from this decomposition via
7256    /// [`LemmaType::measure_runtime_signature`].
7257    pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
7258        Self {
7259            name: None,
7260            specifications: TypeSpecification::Measure {
7261                minimum: None,
7262                maximum: None,
7263                units: crate::literals::MeasureUnits::new(),
7264                traits: Vec::new(),
7265                decomposition: Some(decomposition),
7266                unit: None,
7267                help: String::new(),
7268            },
7269            extends: TypeExtends::Primitive,
7270            options: TypeOptions::default(),
7271        }
7272    }
7273
7274    /// Declared ratio unit names when the type carries a non-empty unit table (`None` otherwise).
7275    #[must_use]
7276    pub fn ratio_unit_names(&self) -> Option<Vec<&str>> {
7277        match &self.specifications {
7278            TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
7279                Some(units.iter().map(|unit| unit.name.as_str()).collect())
7280            }
7281            TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
7282                Some(units.iter().map(|unit| unit.name.as_str()).collect())
7283            }
7284            _ => None,
7285        }
7286    }
7287
7288    /// Declared unit names when the type carries a non-empty unit table (`None` otherwise).
7289    #[must_use]
7290    pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
7291        match &self.specifications {
7292            TypeSpecification::Measure { units, .. } if !units.is_empty() => {
7293                Some(units.iter().map(|unit| unit.name.as_str()).collect())
7294            }
7295            TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
7296                Some(units.iter().map(|unit| unit.name.as_str()).collect())
7297            }
7298            _ => None,
7299        }
7300    }
7301
7302    /// `age [number]` or `gender [gender_code]` — brackets omitted when type adds nothing.
7303    #[must_use]
7304    pub fn label_for_data_input(&self, input_key: &str) -> String {
7305        let type_label = if let Some(parent) = self.extends.parent_name() {
7306            parent.to_string()
7307        } else {
7308            let type_name = self.name();
7309            if type_name == input_key {
7310                self.specifications.to_string()
7311            } else {
7312                type_name
7313            }
7314        };
7315        if type_label == input_key {
7316            input_key.to_string()
7317        } else {
7318            format!("{input_key} [{type_label}]")
7319        }
7320    }
7321
7322    /// `Data age [number]: {detail}` for runtime data override vetoes (parse/validation).
7323    #[must_use]
7324    pub fn data_veto_message(&self, input_key: &str, detail: &str) -> String {
7325        format!("Data {}: {}", self.label_for_data_input(input_key), detail)
7326    }
7327
7328    /// Whether an empty [`EvaluateDataValue`] should veto before parse (text may accept `""`).
7329    #[must_use]
7330    pub fn empty_runtime_input_vetoes(&self) -> bool {
7331        !matches!(self.specifications, TypeSpecification::Text { .. })
7332    }
7333
7334    /// Return the conversion factor for a declared unit name on this measure type.
7335    pub fn measure_unit_factor(
7336        &self,
7337        unit_name: &str,
7338    ) -> &crate::computation::rational::RationalInteger {
7339        let units = match &self.specifications {
7340            TypeSpecification::Measure { units, .. } => units,
7341            TypeSpecification::MeasureRange { units, .. } => units,
7342            _ => unreachable!(
7343                "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
7344                self.name()
7345            ),
7346        };
7347        match units.get(unit_name) {
7348            Ok(unit) => unit.require_literal_factor(),
7349            Err(_) => {
7350                let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
7351                unreachable!(
7352                    "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
7353                    unit_name,
7354                    self.name(),
7355                    valid.join(", ")
7356                );
7357            }
7358        }
7359    }
7360
7361    pub fn ratio_unit_factor(
7362        &self,
7363        unit_name: &str,
7364    ) -> &crate::computation::rational::RationalInteger {
7365        let units = match &self.specifications {
7366            TypeSpecification::Ratio { units, .. } => units,
7367            _ => unreachable!(
7368                "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
7369                self.name()
7370            ),
7371        };
7372        match units.get(unit_name) {
7373            Ok(RatioUnit { value, .. }) => value,
7374            Err(_) => {
7375                let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
7376                unreachable!(
7377                    "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
7378                    unit_name,
7379                    self.name(),
7380                    valid.join(", ")
7381                );
7382            }
7383        }
7384    }
7385
7386    /// Convert a measure literal to API [`Decimal`]s in the given unit names.
7387    pub(crate) fn measure_literal_unit_map(
7388        &self,
7389        literal: &LiteralValue,
7390        unit_names: &[&str],
7391        factor_source: UnitFactorSource<'_>,
7392    ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
7393        use crate::computation::rational::checked_div;
7394
7395        let ValueKind::Measure(magnitude) = &literal.value else {
7396            panic!("BUG: measure_literal_unit_map called with non-measure value");
7397        };
7398        let mut map = BTreeMap::new();
7399        for &unit_name in unit_names {
7400            let Some(unit_factor) = factor_source.try_measure_unit_factor(unit_name) else {
7401                continue;
7402            };
7403            let magnitude_in_unit = checked_div(magnitude, &unit_factor)
7404                .map_err(LiteralUnitMapFailure::UnitConversion)?;
7405            let decimal = self
7406                .try_rational_as_decimal(&magnitude_in_unit)
7407                .map_err(LiteralUnitMapFailure::Commit)?;
7408            map.insert(unit_name.to_string(), decimal);
7409        }
7410        Ok(map)
7411    }
7412
7413    /// Convert a ratio literal to API [`Decimal`]s in the given unit names.
7414    pub(crate) fn ratio_literal_unit_map(
7415        &self,
7416        literal: &LiteralValue,
7417        unit_names: &[&str],
7418        factor_source: UnitFactorSource<'_>,
7419    ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
7420        use crate::computation::rational::checked_mul;
7421
7422        let ratio_api_type = match &self.specifications {
7423            TypeSpecification::Ratio { .. } => self,
7424            TypeSpecification::RatioRange { .. } => {
7425                return ratio_element_type_for_api(self).ratio_literal_unit_map(
7426                    literal,
7427                    unit_names,
7428                    factor_source,
7429                );
7430            }
7431            _ => {
7432                panic!(
7433                    "BUG: ratio_literal_unit_map called with non-ratio type {}",
7434                    self.name()
7435                );
7436            }
7437        };
7438        let ValueKind::Ratio(canonical) = &literal.value else {
7439            panic!("BUG: ratio_literal_unit_map called with non-ratio value");
7440        };
7441        if unit_names.is_empty() {
7442            panic!(
7443                "BUG: ratio literal type '{}' must have at least one unit name",
7444                self.name()
7445            );
7446        }
7447        let mut map = BTreeMap::new();
7448        for &unit_name in unit_names {
7449            let unit_factor = factor_source.ratio_unit_factor(unit_name);
7450            let magnitude_in_unit = checked_mul(canonical, unit_factor)
7451                .map_err(LiteralUnitMapFailure::UnitConversion)?;
7452            let decimal = ratio_api_type
7453                .try_rational_as_decimal(&magnitude_in_unit)
7454                .map_err(LiteralUnitMapFailure::Commit)?;
7455            map.insert(unit_name.to_string(), decimal);
7456        }
7457        Ok(map)
7458    }
7459}
7460
7461/// Where to read measure/ratio unit factors when building a per-unit decimal map.
7462pub(crate) enum UnitFactorSource<'a> {
7463    DeclaredOn(&'a LemmaType),
7464    Merged {
7465        measure: Option<&'a MeasureUnits>,
7466        ratio: Option<&'a RatioUnits>,
7467    },
7468    /// Evaluated reference scales over a declared or merged table. A dynamic
7469    /// column whose scale is missing from `scales` is omitted.
7470    Runtime {
7471        scales:
7472            &'a std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
7473        fallback: &'a UnitFactorSource<'a>,
7474    },
7475}
7476
7477impl UnitFactorSource<'_> {
7478    /// Literal factor of `unit_name`, or `None` when that unit is reference-scaled
7479    /// and this source has no runtime factor for it (the column is omitted).
7480    fn try_measure_unit_factor(
7481        &self,
7482        unit_name: &str,
7483    ) -> Option<crate::computation::rational::RationalInteger> {
7484        match self {
7485            UnitFactorSource::DeclaredOn(lemma_type) => {
7486                let units = lemma_type.measure_units_table().unwrap_or_else(|| {
7487                    panic!(
7488                        "BUG: measure_unit_factor on non-measure type {}",
7489                        lemma_type.name()
7490                    )
7491                });
7492                let unit = units.get(unit_name).unwrap_or_else(|_| {
7493                    panic!(
7494                        "BUG: unknown unit '{unit_name}' for measure type {}",
7495                        lemma_type.name()
7496                    )
7497                });
7498                unit.literal_factor().cloned()
7499            }
7500            UnitFactorSource::Merged { measure, .. } => {
7501                // A unit omitted from the merged table (owners disagree on its scale)
7502                // has no single factor here; the column is skipped.
7503                let units = (*measure)?;
7504                let unit = units.get(unit_name).ok()?;
7505                unit.literal_factor().cloned()
7506            }
7507            UnitFactorSource::Runtime { scales, fallback } => scales
7508                .get(unit_name)
7509                .cloned()
7510                .or_else(|| fallback.try_measure_unit_factor(unit_name)),
7511        }
7512    }
7513
7514    fn ratio_unit_factor(&self, unit_name: &str) -> &crate::computation::rational::RationalInteger {
7515        match self {
7516            UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.ratio_unit_factor(unit_name),
7517            UnitFactorSource::Merged { ratio, .. } => {
7518                let units = ratio.unwrap_or_else(|| {
7519                    panic!(
7520                        "BUG: family ratio expansion missing merged table for unit '{unit_name}'"
7521                    )
7522                });
7523                &units
7524                    .get(unit_name)
7525                    .unwrap_or_else(|_| {
7526                        panic!("BUG: family unit '{unit_name}' missing from merged ratio table")
7527                    })
7528                    .value
7529            }
7530            UnitFactorSource::Runtime { fallback, .. } => fallback.ratio_unit_factor(unit_name),
7531        }
7532    }
7533}
7534
7535/// Primitive ratio type for API unit-map conversion on ratio range wrappers.
7536pub(crate) fn ratio_element_type_for_api(lemma_type: &LemmaType) -> LemmaType {
7537    match &lemma_type.specifications {
7538        TypeSpecification::Ratio { .. } => lemma_type.clone(),
7539        TypeSpecification::RatioRange { .. } => {
7540            let element = range_element_type_specification(&lemma_type.specifications)
7541                .expect("BUG: ratio range type must have ratio element specification");
7542            let TypeSpecification::Ratio { units, .. } = element else {
7543                panic!("BUG: ratio range element spec must be Ratio");
7544            };
7545            LemmaType::primitive(TypeSpecification::Ratio {
7546                minimum: None,
7547                maximum: None,
7548                units,
7549                unit: None,
7550                help: String::new(),
7551            })
7552        }
7553        _ => panic!(
7554            "BUG: ratio_element_type_for_api called with non-ratio type {}",
7555            lemma_type.name()
7556        ),
7557    }
7558}
7559
7560/// Failure while converting a literal to decimal strings across declared units.
7561#[derive(Debug, Clone, PartialEq, Eq)]
7562pub(crate) enum LiteralUnitMapFailure {
7563    Commit(crate::computation::rational::NumericFailure),
7564    UnitConversion(crate::computation::rational::NumericFailure),
7565}
7566
7567/// Runtime literal payload. Type lives on `NormalForm.result_type` / `DataDefinition` /
7568/// explicit computation parameters — not on the value.
7569#[derive(Clone, Debug, PartialEq, Eq, Hash)]
7570pub struct LiteralValue {
7571    pub value: ValueKind,
7572}
7573
7574impl LiteralValue {
7575    #[inline]
7576    pub fn new(value: ValueKind) -> Self {
7577        Self { value }
7578    }
7579
7580    fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
7581        match signature {
7582            [(unit_name, 1)] => Some(unit_name.as_str()),
7583            _ => None,
7584        }
7585    }
7586}
7587
7588/// Planning-time literal: value plus resolved type.
7589///
7590/// Used by `ExpressionKind::Literal` and coerce/typing paths. Runtime
7591/// `OperationResult` / value-table slots store bare [`LiteralValue`]; the type
7592/// is on the node (`NormalForm.result_type`) or data definition.
7593#[derive(Clone, Debug, PartialEq, Eq, Hash)]
7594pub struct TypedLiteral {
7595    pub value: ValueKind,
7596    pub lemma_type: Arc<LemmaType>,
7597}
7598
7599impl TypedLiteral {
7600    #[inline]
7601    pub fn to_literal(&self) -> LiteralValue {
7602        LiteralValue {
7603            value: self.value.clone(),
7604        }
7605    }
7606
7607    #[inline]
7608    pub fn into_literal(self) -> LiteralValue {
7609        LiteralValue { value: self.value }
7610    }
7611}
7612
7613impl From<TypedLiteral> for LiteralValue {
7614    fn from(typed: TypedLiteral) -> Self {
7615        typed.into_literal()
7616    }
7617}
7618
7619impl From<&TypedLiteral> for LiteralValue {
7620    fn from(typed: &TypedLiteral) -> Self {
7621        typed.to_literal()
7622    }
7623}
7624
7625impl LiteralValue {
7626    pub fn text(s: impl Into<Arc<str>>) -> Self {
7627        Self {
7628            value: ValueKind::Text(s.into()),
7629        }
7630    }
7631
7632    pub fn number(n: RationalInteger) -> Self {
7633        Self {
7634            value: ValueKind::Number(n),
7635        }
7636    }
7637
7638    pub fn number_from_decimal(decimal: Decimal) -> Self {
7639        Self::number(
7640            crate::literals::rational_from_parsed_decimal(decimal)
7641                .expect("BUG: literal number from decimal must lift at boundary"),
7642        )
7643    }
7644
7645    pub fn measure(n: RationalInteger) -> Self {
7646        Self {
7647            value: ValueKind::Measure(n),
7648        }
7649    }
7650
7651    pub fn from_bool(b: bool) -> Self {
7652        Self {
7653            value: ValueKind::Boolean(b),
7654        }
7655    }
7656
7657    pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
7658        Self::date(date_time_to_semantic(dt))
7659    }
7660
7661    pub fn date(dt: SemanticDateTime) -> Self {
7662        Self {
7663            value: ValueKind::Date(dt),
7664        }
7665    }
7666
7667    pub fn time(t: SemanticTime) -> Self {
7668        Self {
7669            value: ValueKind::Time(t),
7670        }
7671    }
7672
7673    pub fn ratio(r: RationalInteger) -> Self {
7674        Self {
7675            value: ValueKind::Ratio(r),
7676        }
7677    }
7678
7679    pub fn ratio_from_decimal(r: Decimal) -> Self {
7680        Self::ratio(
7681            crate::literals::rational_from_parsed_decimal(r)
7682                .expect("BUG: ratio literal from decimal must lift at boundary"),
7683        )
7684    }
7685
7686    pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
7687        Self {
7688            value: ValueKind::Range(Box::new(left), Box::new(right)),
7689        }
7690    }
7691
7692    /// Display without an explicit type. Measure/ratio fall back to magnitude-only.
7693    pub fn display_value(&self) -> String {
7694        match &self.value {
7695            ValueKind::Measure(_) | ValueKind::Ratio(_) | ValueKind::UnscaledMeasure { .. } => {
7696                format!("{}", self.value)
7697            }
7698            ValueKind::Range(left, right) => {
7699                format!("{}...{}", left.display_value(), right.display_value())
7700            }
7701            ValueKind::List(rows) if rows.is_empty() => String::new(),
7702            ValueKind::List(rows) => rows
7703                .iter()
7704                .map(|row| row.display_value())
7705                .collect::<Vec<_>>()
7706                .join(", "),
7707            _ => format!("{}", self.value),
7708        }
7709    }
7710
7711    /// Display string given an explicit type (measure/ratio need unit identity).
7712    pub fn display_value_with_type(&self, lemma_type: &LemmaType) -> String {
7713        self.display_value_with_scales(lemma_type, None)
7714    }
7715
7716    /// Display string given an explicit type and settled reference-scale factors.
7717    pub fn display_value_with_scales(
7718        &self,
7719        lemma_type: &LemmaType,
7720        runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
7721    ) -> String {
7722        match &self.value {
7723            ValueKind::Measure(n) => {
7724                let signature = lemma_type.measure_runtime_signature();
7725                format_measure_canonical_for_display(n, lemma_type, &signature, runtime_scales)
7726            }
7727            ValueKind::UnscaledMeasure { magnitude, unit } => {
7728                format!(
7729                    "{} {unit}",
7730                    format_rational_magnitude_for_display(magnitude)
7731                )
7732            }
7733            ValueKind::Ratio(n) => format_ratio_canonical_for_display(n, lemma_type),
7734            ValueKind::Range(left, right) => {
7735                format!(
7736                    "{}...{}",
7737                    left.to_literal()
7738                        .display_value_with_scales(&left.lemma_type, runtime_scales),
7739                    right
7740                        .to_literal()
7741                        .display_value_with_scales(&right.lemma_type, runtime_scales)
7742                )
7743            }
7744            ValueKind::List(rows) if rows.is_empty() => lemma_type.specifications.to_string(),
7745            ValueKind::List(rows) => rows
7746                .iter()
7747                .map(|row| {
7748                    row.to_literal()
7749                        .display_value_with_scales(&row.lemma_type, runtime_scales)
7750                })
7751                .collect::<Vec<_>>()
7752                .join(", "),
7753            _ => format!("{}", self.value),
7754        }
7755    }
7756
7757    /// Structural byte-size estimate for resource limit checks.
7758    pub fn byte_size(&self) -> usize {
7759        self.value.structural_byte_size()
7760    }
7761
7762    /// Magnitude for decimal input prompts (API [`Decimal`]; format at call site).
7763    #[must_use]
7764    pub fn magnitude_suggestion_for_decimal_prompt(
7765        &self,
7766        lemma_type: &LemmaType,
7767    ) -> Option<rust_decimal::Decimal> {
7768        match &self.value {
7769            ValueKind::Number(n) => Some(
7770                lemma_type
7771                    .try_rational_as_decimal(n)
7772                    .expect("BUG: stored number literal must convert to decimal for prompt"),
7773            ),
7774            ValueKind::Measure(n) => {
7775                let signature = lemma_type.measure_runtime_signature();
7776                let unit_name = Self::single_measure_signature_unit_name(&signature).expect(
7777                    "BUG: measure prompt requires exactly one signature unit with exponent 1",
7778                );
7779                Some(
7780                    lemma_type
7781                        .try_measure_canonical_as_decimal_in_unit(n, unit_name)
7782                        .expect("BUG: stored measure literal must convert to decimal for prompt"),
7783                )
7784            }
7785            ValueKind::Ratio(n) => {
7786                if let Some(unit_name) = lemma_type.unit() {
7787                    Some(
7788                        lemma_type
7789                            .try_ratio_canonical_as_decimal_in_unit(n, unit_name)
7790                            .expect("BUG: stored ratio literal must convert to decimal for prompt"),
7791                    )
7792                } else {
7793                    Some(lemma_type.try_rational_as_decimal(n).expect(
7794                        "BUG: stored bare ratio literal must convert to decimal for prompt",
7795                    ))
7796                }
7797            }
7798            _ => None,
7799        }
7800    }
7801
7802    /// Per-unit magnitudes when this literal is a measure with declared units.
7803    #[must_use]
7804    pub fn measure_units(
7805        &self,
7806        lemma_type: &LemmaType,
7807    ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
7808        if !matches!(self.value, ValueKind::Measure(_)) {
7809            return None;
7810        }
7811        lemma_type.measure_unit_names()?;
7812        let declared: Vec<&str> = lemma_type
7813            .measure_unit_names()
7814            .expect("BUG: measure_unit_names checked above");
7815        lemma_type
7816            .measure_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
7817            .ok()
7818    }
7819
7820    /// Per-unit magnitudes when this literal is a ratio with declared units.
7821    #[must_use]
7822    pub fn ratio_units(
7823        &self,
7824        lemma_type: &LemmaType,
7825    ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
7826        if !matches!(self.value, ValueKind::Ratio(_)) {
7827            return None;
7828        }
7829        let has_declared_units = match &lemma_type.specifications {
7830            TypeSpecification::Ratio { units, .. } => !units.is_empty(),
7831            TypeSpecification::RatioRange { .. } => true,
7832            _ => return None,
7833        };
7834        if !has_declared_units {
7835            return None;
7836        }
7837        let declared: Vec<&str> = lemma_type
7838            .ratio_unit_names()
7839            .expect("BUG: ratio units checked above");
7840        lemma_type
7841            .ratio_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
7842            .ok()
7843    }
7844
7845    /// Magnitude in a declared unit when this literal is measure or ratio.
7846    #[must_use]
7847    pub fn magnitude_in_unit(
7848        &self,
7849        lemma_type: &LemmaType,
7850        unit: &str,
7851    ) -> Option<rust_decimal::Decimal> {
7852        self.measure_units(lemma_type)
7853            .and_then(|map| map.get(unit).copied())
7854            .or_else(|| {
7855                self.ratio_units(lemma_type)
7856                    .and_then(|map| map.get(unit).copied())
7857            })
7858    }
7859
7860    /// Derive second from a duration measure's canonical magnitude.
7861    pub fn duration_canonical_seconds(&self, lemma_type: &LemmaType) -> RationalInteger {
7862        let ValueKind::Measure(magnitude) = &self.value else {
7863            unreachable!(
7864                "BUG: duration_canonical_seconds called with {:?}",
7865                self.value
7866            );
7867        };
7868        if !lemma_type.is_duration_like_measure() {
7869            unreachable!(
7870                "BUG: duration_canonical_seconds called with type {}",
7871                lemma_type.name()
7872            );
7873        }
7874        let factor = lemma_type.measure_unit_factor("second");
7875        checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
7876    }
7877
7878    /// Derive month from a calendar measure's canonical magnitude.
7879    pub fn calendar_canonical_months(&self, lemma_type: &LemmaType) -> RationalInteger {
7880        let ValueKind::Measure(magnitude) = &self.value else {
7881            unreachable!(
7882                "BUG: calendar_canonical_months called with {:?}",
7883                self.value
7884            );
7885        };
7886        if !lemma_type.is_calendar_like() {
7887            unreachable!(
7888                "BUG: calendar_canonical_months called with type {}",
7889                lemma_type.name()
7890            );
7891        }
7892        let factor = lemma_type.measure_unit_factor("month");
7893        checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
7894    }
7895}
7896
7897impl TypedLiteral {
7898    pub fn text(s: impl Into<Arc<str>>) -> Self {
7899        Self {
7900            value: ValueKind::Text(s.into()),
7901            lemma_type: primitive_text_arc().clone(),
7902        }
7903    }
7904
7905    pub fn text_with_type(s: impl Into<Arc<str>>, lemma_type: Arc<LemmaType>) -> Self {
7906        Self {
7907            value: ValueKind::Text(s.into()),
7908            lemma_type,
7909        }
7910    }
7911
7912    pub fn number(n: RationalInteger) -> Self {
7913        Self {
7914            value: ValueKind::Number(n),
7915            lemma_type: primitive_number_arc().clone(),
7916        }
7917    }
7918
7919    pub fn number_from_decimal(decimal: Decimal) -> Self {
7920        Self::number(
7921            crate::literals::rational_from_parsed_decimal(decimal)
7922                .expect("BUG: literal number from decimal must lift at boundary"),
7923        )
7924    }
7925
7926    pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
7927        Self {
7928            value: ValueKind::Number(n),
7929            lemma_type,
7930        }
7931    }
7932
7933    pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
7934        Self::number_with_type(
7935            crate::literals::rational_from_parsed_decimal(decimal)
7936                .expect("BUG: literal number from decimal must lift at boundary"),
7937            lemma_type,
7938        )
7939    }
7940
7941    /// Build a Measure literal bound to a specific declared unit.
7942    pub fn measure_with_bound_unit(
7943        n: RationalInteger,
7944        unit_name: impl Into<String>,
7945        lemma_type: Arc<LemmaType>,
7946    ) -> Self {
7947        Self::measure_with_type(
7948            n,
7949            Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
7950        )
7951    }
7952
7953    pub fn measure_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
7954        Self {
7955            value: ValueKind::Measure(n),
7956            lemma_type,
7957        }
7958    }
7959
7960    pub fn measure_with_signature(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
7961        Self {
7962            value: ValueKind::Measure(n),
7963            lemma_type,
7964        }
7965    }
7966
7967    /// Unit literal outside a measure type context: no family, so no dimensions.
7968    pub fn number_interpreted_as_measure(value: RationalInteger) -> Self {
7969        Self {
7970            value: ValueKind::Measure(value),
7971            lemma_type: Arc::new(anonymous_measure_type()),
7972        }
7973    }
7974
7975    pub fn from_bool(b: bool) -> Self {
7976        Self {
7977            value: ValueKind::Boolean(b),
7978            lemma_type: primitive_boolean_arc().clone(),
7979        }
7980    }
7981
7982    pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
7983        Self::date(date_time_to_semantic(dt))
7984    }
7985
7986    pub fn date(dt: SemanticDateTime) -> Self {
7987        Self {
7988            value: ValueKind::Date(dt),
7989            lemma_type: primitive_date_arc().clone(),
7990        }
7991    }
7992
7993    pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
7994        Self {
7995            value: ValueKind::Date(dt),
7996            lemma_type,
7997        }
7998    }
7999
8000    pub fn time(t: SemanticTime) -> Self {
8001        Self {
8002            value: ValueKind::Time(t),
8003            lemma_type: primitive_time_arc().clone(),
8004        }
8005    }
8006
8007    pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
8008        Self {
8009            value: ValueKind::Time(t),
8010            lemma_type,
8011        }
8012    }
8013
8014    pub fn calendar(
8015        value: RationalInteger,
8016        unit: SemanticCalendarUnit,
8017        lemma_type: Arc<LemmaType>,
8018    ) -> Self {
8019        let unit_name = unit.to_string();
8020        debug_assert_eq!(
8021            semantic_calendar_unit_from_unit_name(&unit_name),
8022            unit,
8023            "BUG: calendar unit name must round-trip"
8024        );
8025        Self::measure_with_bound_unit(value, unit_name, lemma_type)
8026    }
8027
8028    pub fn calendar_from_decimal(
8029        value: Decimal,
8030        unit: SemanticCalendarUnit,
8031        lemma_type: Arc<LemmaType>,
8032    ) -> Self {
8033        Self::calendar(
8034            crate::literals::rational_from_parsed_decimal(value)
8035                .expect("BUG: calendar literal from decimal must lift at boundary"),
8036            unit,
8037            lemma_type,
8038        )
8039    }
8040
8041    pub fn calendar_with_type(
8042        value: RationalInteger,
8043        unit: SemanticCalendarUnit,
8044        lemma_type: Arc<LemmaType>,
8045    ) -> Self {
8046        Self::calendar(value, unit, lemma_type)
8047    }
8048
8049    pub fn ratio(r: RationalInteger) -> Self {
8050        Self {
8051            value: ValueKind::Ratio(r),
8052            lemma_type: primitive_ratio_arc().clone(),
8053        }
8054    }
8055
8056    pub fn ratio_from_decimal(r: Decimal) -> Self {
8057        Self::ratio(
8058            crate::literals::rational_from_parsed_decimal(r)
8059                .expect("BUG: ratio literal from decimal must lift at boundary"),
8060        )
8061    }
8062
8063    pub fn ratio_with_type(r: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
8064        Self {
8065            value: ValueKind::Ratio(r),
8066            lemma_type,
8067        }
8068    }
8069
8070    pub fn ratio_with_bound_unit(
8071        r: RationalInteger,
8072        unit_name: impl Into<String>,
8073        lemma_type: Arc<LemmaType>,
8074    ) -> Self {
8075        Self::ratio_with_type(
8076            r,
8077            Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
8078        )
8079    }
8080
8081    pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
8082        let specifications =
8083            range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
8084                .unwrap_or_else(|| {
8085                    unreachable!(
8086                "BUG: attempted to construct a range literal from incompatible endpoint types"
8087            )
8088                });
8089
8090        Self {
8091            value: ValueKind::Range(Box::new(left), Box::new(right)),
8092            lemma_type: Arc::new(LemmaType::primitive(specifications)),
8093        }
8094    }
8095
8096    pub fn display_value(&self) -> String {
8097        self.to_literal().display_value_with_type(&self.lemma_type)
8098    }
8099
8100    pub fn get_type(&self) -> &LemmaType {
8101        &self.lemma_type
8102    }
8103
8104    pub fn byte_size(&self) -> usize {
8105        self.value.structural_byte_size()
8106    }
8107}
8108
8109impl fmt::Display for TypedLiteral {
8110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8111        write!(f, "{}", self.display_value())
8112    }
8113}
8114
8115/// What a [`DataDefinition::Reference`] copies its value from: either another data path
8116/// or a rule whose result becomes this data's value.
8117#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8118pub enum ReferenceTarget {
8119    Data(DataPath),
8120    Rule(RulePath),
8121}
8122
8123impl fmt::Display for ReferenceTarget {
8124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8125        match self {
8126            ReferenceTarget::Data(path) => write!(f, "{path}"),
8127            ReferenceTarget::Rule(path) => write!(f, "{path}"),
8128        }
8129    }
8130}
8131
8132/// Where a [`DataDefinition::Reference`] chain ends. Computed once per slice in
8133/// [`crate::planning::graph::Graph::validate`] after `compute_data_reference_order`
8134/// rejected cycles.
8135#[derive(Clone, Debug, PartialEq, Eq)]
8136pub(crate) enum ReferenceEnd {
8137    Promptable(DataPath),
8138    Rule(RulePath),
8139}
8140
8141/// Who wrote a data slot's definition.
8142///
8143/// A slot declared by its own spec is a default: evaluation data may replace it. A
8144/// slot bound by a consumer (`uses x -> with field: value`, a path fill, or a row
8145/// field written in a data list) is final: supplying it at evaluate is a request
8146/// [`crate::Error`], and it is not a promptable input.
8147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8148pub enum DataOrigin {
8149    Declared,
8150    Bound,
8151}
8152
8153/// Date-typed local that a dynamic `uses … -> effective` import reads at evaluation.
8154#[derive(Clone, Debug, PartialEq, Eq, Hash)]
8155pub enum ImportEffectivePin {
8156    Data(DataPath),
8157    Rule(RulePath),
8158}
8159
8160/// Resolved data value for the execution plan: aligned with [`DataValue`] but with source per variant.
8161#[derive(Clone, Debug)]
8162pub enum DataDefinition {
8163    /// Value-holding data: current literal (spec default or `with` binding).
8164    Value {
8165        value: LiteralValue,
8166        resolved_type: Arc<LemmaType>,
8167        origin: DataOrigin,
8168        source: Source,
8169    },
8170    /// Type-only data: type known, value to be supplied (e.g. via run data).
8171    /// `declared_suggestion` carries the `-> suggest ...` payload for this binding or
8172    /// the suggestion inherited from the parent type chain, if any; value-promoting code
8173    /// uses it instead of re-deriving suggestions from [`TypeSpecification`].
8174    /// The evaluator never commits a suggestion — unbound stays MissingData.
8175    TypeDeclaration {
8176        resolved_type: Arc<LemmaType>,
8177        declared_suggestion: Option<BoundValueKind>,
8178        declared_value: Option<BoundValueKind>,
8179        source: Source,
8180    },
8181    /// Import (`uses`): a spec instance named `alias` in this plan, a row over the
8182    /// imported spec's template plan. Its bindings are the `-> with` / path-fill fields
8183    /// the declaring spec wrote; the imported data and rules are read through the
8184    /// template in the instance's context.
8185    ///
8186    /// When `pin` is `None`, `row` is the sole body. When `pin` is `Some` (`-> effective`),
8187    /// `candidates` holds every possible body and evaluation selects among them; `row`
8188    /// then equals an arbitrary candidate and must not be read without selection.
8189    Import {
8190        row: Arc<RowInstance>,
8191        /// Planned path of the `-> effective` date when present.
8192        pin: Option<ImportEffectivePin>,
8193        /// Bound rows keyed by identity when `pin` is set; empty for static imports.
8194        candidates: std::collections::BTreeMap<SpecIdentity, Arc<RowInstance>>,
8195        source: Source,
8196    },
8197    /// Instance list (`data x: spec list`). `resolved_type` is the list type.
8198    /// Rows written in the spec are instances of this plan under the list path
8199    /// ([`crate::planning::execution_plan::ExecutionPlan::instance_lists`]);
8200    /// evaluation lists them unless run data replaces the list.
8201    InstanceList {
8202        resolved_type: Arc<LemmaType>,
8203        origin: DataOrigin,
8204        source: Source,
8205    },
8206    /// Value-copy reference to another data or a rule result.
8207    ///
8208    /// `resolved_type` is the merged type that the copied value must satisfy at
8209    /// evaluation time. Merging folds together: (1) the LHS's own declared type,
8210    /// if any; (2) the target's type (data declared type or rule return type);
8211    /// (3) any `local_constraints` written after the `->` on the reference itself.
8212    /// Merging happens in a dedicated pass once all data and rule types are
8213    /// known; before that pass, `resolved_type` holds a provisional value and
8214    /// must not be consumed for type checking.
8215    ///
8216    /// `local_constraints` preserves the raw constraint list from the reference's
8217    /// `-> ...` tail (e.g. `minimum 5` in `data license2: law.other -> minimum 5`)
8218    /// for that merging pass. It is `None` when the reference has no trailing
8219    /// constraints.
8220    ///
8221    /// `local_suggestion` carries any `suggest <value>` constraint from the
8222    /// reference's `-> ...` tail. The reference-merge pass extracts it from the
8223    /// constraint list during type resolution. It is a UI hint only
8224    /// ([`Self::suggestion`] / show); the evaluator never commits it — unbound
8225    /// stays MissingData.
8226    ///
8227    /// The reference itself is evaluated by copying the target's value (data path)
8228    /// or the target rule's result in topological order. Evaluate data for this
8229    /// path binds via `resolved_type` like a type declaration.
8230    Reference {
8231        target: ReferenceTarget,
8232        resolved_type: Arc<LemmaType>,
8233        local_constraints: Option<Vec<Constraint>>,
8234        local_suggestion: Option<BoundValueKind>,
8235        local_value: Option<BoundValueKind>,
8236        origin: DataOrigin,
8237        source: Source,
8238    },
8239}
8240
8241impl DataDefinition {
8242    /// Whether a consumer bound this slot (`uses -> with`, path fill, or a row field
8243    /// written in a data list). Bound slots take no evaluation data.
8244    pub fn is_bound(&self) -> bool {
8245        match self {
8246            DataDefinition::Value { origin, .. }
8247            | DataDefinition::InstanceList { origin, .. }
8248            | DataDefinition::Reference { origin, .. } => *origin == DataOrigin::Bound,
8249            DataDefinition::TypeDeclaration { .. } | DataDefinition::Import { .. } => false,
8250        }
8251    }
8252
8253    /// Declared lemma type for value, type-declaration, and reference data; `None` for imports.
8254    pub fn lemma_type(&self) -> Option<&LemmaType> {
8255        self.resolved_type_arc().map(|arc| arc.as_ref())
8256    }
8257
8258    /// Shared [`Arc`] for the declared type; `None` for imports.
8259    pub fn resolved_type_arc(&self) -> Option<&Arc<LemmaType>> {
8260        match self {
8261            DataDefinition::Value { resolved_type, .. } => Some(resolved_type),
8262            DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
8263            DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
8264            DataDefinition::InstanceList { resolved_type, .. } => Some(resolved_type),
8265            DataDefinition::Import { .. } => None,
8266        }
8267    }
8268
8269    /// Alias for [`Self::lemma_type`] (historical name).
8270    #[inline]
8271    pub fn schema_type(&self) -> Option<&LemmaType> {
8272        self.lemma_type()
8273    }
8274
8275    /// Returns the literal value when the data already holds one, including `-> with`
8276    /// on a type declaration or reference. A data-target `Reference`'s copied value
8277    /// is produced by the evaluator at runtime, so at plan-time it has no value yet.
8278    pub fn value(&self) -> Option<LiteralValue> {
8279        match self {
8280            DataDefinition::Value { value, .. } => Some(value.clone()),
8281            DataDefinition::TypeDeclaration {
8282                declared_value: Some(dv),
8283                ..
8284            } => Some(dv.to_literal()),
8285            DataDefinition::Reference {
8286                local_value: Some(dv),
8287                resolved_type: _,
8288                ..
8289            } => Some(dv.to_literal()),
8290            DataDefinition::TypeDeclaration { .. }
8291            | DataDefinition::Import { .. }
8292            | DataDefinition::InstanceList { .. }
8293            | DataDefinition::Reference { .. } => None,
8294        }
8295    }
8296
8297    /// Fill value with optional written measure unit binding (Show display).
8298    pub(crate) fn bound_value(&self) -> Option<&BoundValueKind> {
8299        match self {
8300            DataDefinition::TypeDeclaration {
8301                declared_value: Some(dv),
8302                ..
8303            }
8304            | DataDefinition::Reference {
8305                local_value: Some(dv),
8306                ..
8307            } => Some(dv),
8308            DataDefinition::Value { .. }
8309            | DataDefinition::TypeDeclaration { .. }
8310            | DataDefinition::Reference { .. }
8311            | DataDefinition::InstanceList { .. }
8312            | DataDefinition::Import { .. } => None,
8313        }
8314    }
8315
8316    /// Suggestion with optional written measure unit binding (Show display).
8317    /// Surfaces in [`crate::planning::execution_plan::ShowData::suggestion`] for
8318    /// show/response/UI; the evaluator never commits it — unbound stays MissingData.
8319    pub(crate) fn bound_suggestion(&self) -> Option<&BoundValueKind> {
8320        match self {
8321            DataDefinition::TypeDeclaration {
8322                declared_suggestion: Some(dv),
8323                ..
8324            }
8325            | DataDefinition::Reference {
8326                local_suggestion: Some(dv),
8327                ..
8328            } => Some(dv),
8329            DataDefinition::Value { .. }
8330            | DataDefinition::TypeDeclaration { .. }
8331            | DataDefinition::Reference { .. }
8332            | DataDefinition::InstanceList { .. }
8333            | DataDefinition::Import { .. } => None,
8334        }
8335    }
8336
8337    /// Returns the source location for this data.
8338    pub fn source(&self) -> &Source {
8339        match self {
8340            DataDefinition::Value { source, .. } => source,
8341            DataDefinition::TypeDeclaration { source, .. } => source,
8342            DataDefinition::Import { source, .. } => source,
8343            DataDefinition::InstanceList { source, .. } => source,
8344            DataDefinition::Reference { source, .. } => source,
8345        }
8346    }
8347
8348    /// The sole row of a static (`pin` is `None`) import.
8349    pub fn import_sole_row(&self) -> &Arc<RowInstance> {
8350        let DataDefinition::Import { pin, row, .. } = self else {
8351            panic!("BUG: import_sole_row on non-import data");
8352        };
8353        assert!(
8354            pin.is_none(),
8355            "BUG: dynamic import needs pin selection before reading a row"
8356        );
8357        row
8358    }
8359
8360    /// Candidate rows of a dynamic import (`pin` is set).
8361    pub fn import_candidates(&self) -> &std::collections::BTreeMap<SpecIdentity, Arc<RowInstance>> {
8362        let DataDefinition::Import {
8363            pin, candidates, ..
8364        } = self
8365        else {
8366            panic!("BUG: import_candidates on non-import data");
8367        };
8368        assert!(pin.is_some(), "BUG: static import has no candidate map");
8369        candidates
8370    }
8371
8372    /// Whether this import selects its body from a `-> effective` pin at evaluation.
8373    pub fn import_pin(&self) -> Option<&ImportEffectivePin> {
8374        match self {
8375            DataDefinition::Import { pin, .. } => pin.as_ref(),
8376            _ => None,
8377        }
8378    }
8379
8380    /// Row of a dynamic import for `identity`, or the sole static row when `pin` is `None`.
8381    pub fn import_row_for(&self, identity: &SpecIdentity) -> &Arc<RowInstance> {
8382        let DataDefinition::Import {
8383            pin,
8384            row,
8385            candidates,
8386            ..
8387        } = self
8388        else {
8389            panic!("BUG: import_row_for on non-import data");
8390        };
8391        if pin.is_none() {
8392            return row;
8393        }
8394        candidates
8395            .get(identity)
8396            .unwrap_or_else(|| panic!("BUG: dynamic import has no row for identity '{identity}'"))
8397    }
8398}
8399
8400/// Bind a type-agnostic [`Value::NumberWithUnit`] using the unit index entry for `unit_name`.
8401pub fn number_with_unit_to_value_kind(
8402    magnitude: rust_decimal::Decimal,
8403    unit_name: &str,
8404    lemma_type: &LemmaType,
8405) -> Result<ValueKind, String> {
8406    match &lemma_type.specifications {
8407        TypeSpecification::Ratio { units, .. } => {
8408            use crate::computation::rational::{checked_div, decimal_to_rational};
8409            let unit = units.get(unit_name)?;
8410            let magnitude_rational = decimal_to_rational(magnitude)
8411                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
8412            let canonical_rational = checked_div(&magnitude_rational, &unit.value)
8413                .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
8414            Ok(ValueKind::Ratio(canonical_rational))
8415        }
8416        TypeSpecification::Measure { units, .. } => {
8417            use crate::computation::rational::checked_mul;
8418            let rational = lift_parser_decimal(magnitude)?;
8419            let unit = units.get(unit_name)?;
8420            match unit.literal_factor() {
8421                Some(factor) => {
8422                    let canonical = checked_mul(&rational, factor).map_err(|failure| {
8423                        format!("measure canonicalization overflow: {failure}")
8424                    })?;
8425                    Ok(ValueKind::Measure(canonical))
8426                }
8427                None => Ok(ValueKind::UnscaledMeasure {
8428                    magnitude: rational,
8429                    unit: std::sync::Arc::from(unit_name),
8430                }),
8431            }
8432        }
8433        _ => Err(format!(
8434            "Unit '{}' is defined on type '{}' which is not measure or ratio",
8435            unit_name,
8436            lemma_type.name()
8437        )),
8438    }
8439}
8440
8441/// Whether a [`ValueKind`] is structurally compatible with a [`TypeSpecification`].
8442/// Bound validation (min/max/decimals) is separate; this only checks shape.
8443pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
8444    matches!(
8445        (type_spec, value),
8446        (TypeSpecification::Number { .. }, ValueKind::Number(_))
8447            | (TypeSpecification::Text { .. }, ValueKind::Text(_))
8448            | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
8449            | (TypeSpecification::Date { .. }, ValueKind::Date(_))
8450            | (TypeSpecification::Time { .. }, ValueKind::Time(_))
8451            | (TypeSpecification::Measure { .. }, ValueKind::Measure(_))
8452            | (
8453                TypeSpecification::Measure { .. },
8454                ValueKind::UnscaledMeasure { .. }
8455            )
8456            | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_))
8457            | (
8458                TypeSpecification::NumberRange { .. },
8459                ValueKind::Range(_, _)
8460            )
8461            | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
8462            | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
8463            | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
8464            | (
8465                TypeSpecification::MeasureRange { .. },
8466                ValueKind::Range(_, _)
8467            )
8468            | (TypeSpecification::Veto { .. }, _)
8469            | (TypeSpecification::Undetermined, _)
8470            | (TypeSpecification::Spec { .. }, ValueKind::Spec(_))
8471            | (TypeSpecification::List { .. }, ValueKind::List(_))
8472    )
8473}
8474
8475fn parser_value_type_mismatch(
8476    value: &crate::literals::Value,
8477    type_spec: &TypeSpecification,
8478) -> String {
8479    use crate::parsing::ast::AsLemmaSource;
8480    let value_str = format!("{}", AsLemmaSource(value));
8481    match type_spec {
8482        TypeSpecification::Measure { units, .. } => {
8483            let unit_hint = units
8484                .iter()
8485                .find(|u| u.is_canonical_factor())
8486                .map(|u| u.name.as_str())
8487                .or_else(|| units.iter().next().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        TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
8492            let unit_hint = units
8493                .iter()
8494                .next()
8495                .map(|u| u.name.as_str())
8496                .unwrap_or("unit");
8497            format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
8498        }
8499        _ => format!("cannot use {value_str} as {type_spec}"),
8500    }
8501}
8502
8503/// Re-canonicalize a measure literal after compound unit factors were resolved.
8504///
8505/// Literals parsed before derived unit resolution were canonicalized with prefix-only
8506/// factors; multiply by `resolved_factor / stored_factor` to align with final factors.
8507pub fn refresh_measure_literal_canonical_magnitude(
8508    lit: &mut LiteralValue,
8509    previous_type: &LemmaType,
8510    resolved_type: &LemmaType,
8511) -> Result<(), crate::computation::rational::NumericFailure> {
8512    use crate::computation::rational::{checked_div, checked_mul};
8513    let ValueKind::Measure(magnitude) = &mut lit.value else {
8514        if matches!(lit.value, ValueKind::UnscaledMeasure { .. }) {
8515            return Ok(());
8516        }
8517        return Ok(());
8518    };
8519    // Binding unit is no longer on ValueKind; magnitude was canonicalized at bind time
8520    // against the then-current unit table. When a single-term runtime signature unit
8521    // exists on both tables, rescale if that unit's factor changed.
8522    let signature = previous_type.measure_runtime_signature();
8523    let Some((unit_name, 1)) = signature.first().map(|(n, e)| (n.as_str(), *e)) else {
8524        return Ok(());
8525    };
8526    if signature.len() != 1 {
8527        return Ok(());
8528    }
8529    let stored_factor = previous_type.measure_unit_factor(unit_name);
8530    let resolved_factor = resolved_type.measure_unit_factor(unit_name);
8531    if stored_factor == resolved_factor {
8532        return Ok(());
8533    }
8534    let scaled = checked_mul(magnitude, resolved_factor)?;
8535    *magnitude = checked_div(&scaled, stored_factor)?;
8536    Ok(())
8537}
8538
8539fn number_with_unit_value_kind(
8540    magnitude: rust_decimal::Decimal,
8541    unit_name: &str,
8542    type_spec: &TypeSpecification,
8543) -> Result<ValueKind, String> {
8544    use crate::computation::rational::{checked_div, checked_mul, decimal_to_rational};
8545    match type_spec {
8546        TypeSpecification::Ratio { units, .. } => {
8547            let unit = units.get(unit_name)?;
8548            let magnitude_rational = decimal_to_rational(magnitude)
8549                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
8550            let canonical_rational = checked_div(&magnitude_rational, &unit.value)
8551                .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
8552            Ok(ValueKind::Ratio(canonical_rational))
8553        }
8554        TypeSpecification::Measure { units, .. } => {
8555            let rational = lift_parser_decimal(magnitude)?;
8556            let unit = units.get(unit_name)?;
8557            match unit.literal_factor() {
8558                Some(factor) => {
8559                    let canonical = checked_mul(&rational, factor).map_err(|failure| {
8560                        format!("measure canonicalization overflow: {failure}")
8561                    })?;
8562                    Ok(ValueKind::Measure(canonical))
8563                }
8564                None => Ok(ValueKind::UnscaledMeasure {
8565                    magnitude: rational,
8566                    unit: Arc::from(unit_name),
8567                }),
8568            }
8569        }
8570        _ => Err("number_with_unit literal requires a measure or ratio type".to_string()),
8571    }
8572}
8573
8574/// Convert a non-range parser [`Value`] to [`ValueKind`] using the target type.
8575///
8576/// Range values must go through [`parser_value_to_value_kind`] so endpoints use
8577/// [`lift_range_endpoint`]. Kept separate so that helper cannot re-enter the Range arm.
8578fn parser_scalar_to_value_kind(
8579    value: &crate::literals::Value,
8580    type_spec: &TypeSpecification,
8581) -> Result<ValueKind, String> {
8582    use crate::literals::Value;
8583    match (value, type_spec) {
8584        (Value::NumberWithUnit(magnitude, unit_name), _) => {
8585            number_with_unit_value_kind(*magnitude, unit_name, type_spec)
8586        }
8587        (Value::Number(n), TypeSpecification::Number { .. }) => {
8588            Ok(ValueKind::Number(lift_parser_decimal(*n)?))
8589        }
8590        (Value::Text(s), TypeSpecification::Text { .. }) => {
8591            Ok(ValueKind::Text(Arc::from(s.as_str())))
8592        }
8593        (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
8594        (Value::Date(dt), TypeSpecification::Date { .. }) => {
8595            Ok(ValueKind::Date(date_time_to_semantic(dt)))
8596        }
8597        (Value::Time(t), TypeSpecification::Time { .. }) => {
8598            Ok(ValueKind::Time(time_to_semantic(t)))
8599        }
8600        (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
8601    }
8602}
8603
8604/// Convert parser [`Value`] to [`ValueKind`] using the target type (canonicalizes ratio at bind).
8605///
8606/// When `resolve_unit` is set, qualified range-endpoint unit tokens are resolved to bare
8607/// declared names before bind. Measure and ratio range endpoints keep `lemma_type`'s family
8608/// via [`LemmaType::range_element_type`].
8609pub fn parser_value_to_value_kind(
8610    value: &crate::literals::Value,
8611    lemma_type: &LemmaType,
8612    resolve_unit: Option<&UnitTokenResolver<'_>>,
8613) -> Result<ValueKind, String> {
8614    use crate::literals::Value;
8615    match (value, &lemma_type.specifications) {
8616        (
8617            Value::Range(left, right),
8618            TypeSpecification::NumberRange { .. }
8619            | TypeSpecification::DateRange { .. }
8620            | TypeSpecification::TimeRange { .. }
8621            | TypeSpecification::RatioRange { .. }
8622            | TypeSpecification::MeasureRange { .. },
8623        ) => {
8624            let endpoint = match &lemma_type.specifications {
8625                TypeSpecification::MeasureRange { .. } | TypeSpecification::RatioRange { .. } => {
8626                    lemma_type.range_element_type()
8627                }
8628                other => Arc::new(LemmaType::primitive(
8629                    range_element_type_specification(other).expect(
8630                        "BUG: range_element_type_specification missing arm for known range type",
8631                    ),
8632                )),
8633            };
8634            let left_lit = lift_range_endpoint(left, endpoint.as_ref(), resolve_unit)?;
8635            let right_lit = lift_range_endpoint(right, endpoint.as_ref(), resolve_unit)?;
8636            Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
8637        }
8638        (value, type_spec) => parser_scalar_to_value_kind(value, type_spec),
8639    }
8640}
8641
8642/// Read a parsed Lemma literal as a value of `lemma_type`, keeping the written unit.
8643pub(crate) fn typed_literal_from_parsed(
8644    parsed: &crate::literals::Value,
8645    lemma_type: &Arc<LemmaType>,
8646    unit_index: &UnitIndex,
8647    named_types: &IndexMap<String, Arc<LemmaType>>,
8648) -> Result<TypedLiteral, String> {
8649    let resolve = UnitTokenResolver::Typed {
8650        unit_index,
8651        named_types,
8652        lemma_type: lemma_type.as_ref(),
8653    };
8654    let kind = parser_value_to_value_kind(parsed, lemma_type.as_ref(), Some(&resolve))?;
8655    let binding = match binding_unit_from_parser_value(parsed) {
8656        Some(written) => Some(resolve.resolve(&written)?),
8657        None => None,
8658    };
8659    Ok(typed_literal_with_binding_unit(kind, binding, lemma_type))
8660}
8661
8662pub(crate) fn typed_literal_with_binding_unit(
8663    kind: ValueKind,
8664    binding_unit: Option<String>,
8665    lemma_type: &Arc<LemmaType>,
8666) -> TypedLiteral {
8667    let typed_type = match binding_unit {
8668        Some(unit)
8669            if matches!(
8670                &lemma_type.specifications,
8671                TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. }
8672            ) =>
8673        {
8674            Arc::new(lemma_type.as_ref().clone().with_unit(unit))
8675        }
8676        Some(_) | None => Arc::clone(lemma_type),
8677    };
8678    TypedLiteral {
8679        value: kind,
8680        lemma_type: typed_type,
8681    }
8682}
8683
8684fn binding_unit_from_parser_value(value: &crate::literals::Value) -> Option<String> {
8685    use crate::literals::Value;
8686    match value {
8687        Value::NumberWithUnit(_, unit) => Some(unit.clone()),
8688        Value::Range(left, right) => match (left.as_ref(), right.as_ref()) {
8689            (Value::NumberWithUnit(_, left_unit), Value::NumberWithUnit(_, right_unit))
8690                if left_unit == right_unit =>
8691            {
8692                Some(left_unit.clone())
8693            }
8694            _ => None,
8695        },
8696        _ => None,
8697    }
8698}
8699
8700/// Convert parser Value to ValueKind for primitives and ranges only.
8701///
8702/// [`Value::NumberWithUnit`] requires [`parser_value_to_value_kind`] with a measure or ratio type.
8703pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
8704    use crate::parsing::ast::Value;
8705    Ok(match value {
8706        Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
8707        Value::Text(s) => ValueKind::Text(Arc::from(s.as_str())),
8708        Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
8709        Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
8710        Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
8711        Value::NumberWithUnit(_, _) => {
8712            return Err(
8713                "number_with_unit literal requires type context (measure or ratio)".to_string(),
8714            );
8715        }
8716        Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
8717    })
8718}
8719
8720/// Convert AST date-time to semantic (for tests and planning).
8721pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
8722    SemanticDateTime {
8723        year: dt.year,
8724        month: dt.month,
8725        day: dt.day,
8726        hour: dt.hour,
8727        minute: dt.minute,
8728        second: dt.second,
8729        microsecond: dt.microsecond,
8730        timezone: dt.timezone.as_ref().map(SemanticTimezone::from),
8731    }
8732}
8733
8734/// Project a date's clock fields to a time-of-day (date-only values yield midnight).
8735pub(crate) fn semantic_date_as_time(date: &SemanticDateTime) -> SemanticTime {
8736    SemanticTime {
8737        hour: date.hour,
8738        minute: date.minute,
8739        second: date.second,
8740        microsecond: date.microsecond,
8741        timezone: date.timezone.clone(),
8742    }
8743}
8744
8745/// Convert AST time to semantic (for tests and planning).
8746pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
8747    SemanticTime {
8748        hour: t.hour.into(),
8749        minute: t.minute.into(),
8750        second: t.second.into(),
8751        microsecond: t.microsecond,
8752        timezone: t.timezone.as_ref().map(SemanticTimezone::from),
8753    }
8754}
8755
8756/// Compare two semantic date-time values by year, month, day, hour, minute,
8757/// second, then microsecond. Timezone normalisation is a separate concern
8758/// handled at evaluation time.
8759pub(crate) fn compare_semantic_dates(
8760    left: &SemanticDateTime,
8761    right: &SemanticDateTime,
8762) -> std::cmp::Ordering {
8763    left.year
8764        .cmp(&right.year)
8765        .then_with(|| left.month.cmp(&right.month))
8766        .then_with(|| left.day.cmp(&right.day))
8767        .then_with(|| left.hour.cmp(&right.hour))
8768        .then_with(|| left.minute.cmp(&right.minute))
8769        .then_with(|| left.second.cmp(&right.second))
8770        .then_with(|| left.microsecond.cmp(&right.microsecond))
8771}
8772
8773/// Compare two semantic time values by hour, minute, second, then microsecond.
8774/// Timezone is excluded for the same reason as [`compare_semantic_dates`].
8775pub(crate) fn compare_semantic_times(
8776    left: &SemanticTime,
8777    right: &SemanticTime,
8778) -> std::cmp::Ordering {
8779    left.hour
8780        .cmp(&right.hour)
8781        .then_with(|| left.minute.cmp(&right.minute))
8782        .then_with(|| left.second.cmp(&right.second))
8783        .then_with(|| left.microsecond.cmp(&right.microsecond))
8784}
8785
8786/// Convert AST conversion target to semantic (planning boundary; evaluation/computation use only semantic).
8787pub fn conversion_target_to_semantic(
8788    ct: &ConversionTarget,
8789    unit_index: Option<&crate::planning::unit_index::UnitIndex>,
8790    resolved_types: Option<&IndexMap<String, Arc<LemmaType>>>,
8791) -> Result<SemanticConversionTarget, String> {
8792    match ct {
8793        ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
8794        ConversionTarget::Unit { unit_name } => {
8795            let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
8796            let (bare, owning_type) = match resolved_types {
8797                Some(resolved) => index.resolve_with_named_types(unit_name, resolved)?,
8798                None => index.resolve(unit_name)?,
8799            };
8800            Ok(SemanticConversionTarget::Unit {
8801                unit_name: bare,
8802                owning_type,
8803            })
8804        }
8805    }
8806}
8807
8808// -----------------------------------------------------------------------------
8809// Primitive type constructors (moved from parsing::ast)
8810// -----------------------------------------------------------------------------
8811
8812// Statics for lazy initialization of production-used primitive types.
8813static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
8814static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
8815static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
8816static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8817static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8818static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
8819static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
8820static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8821static PRIMITIVE_MEASURE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8822static PRIMITIVE_NUMBER_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8823static PRIMITIVE_RATIO_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8824static PRIMITIVE_TIME_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
8825
8826#[must_use]
8827pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
8828    lemma_type_for_primitive(PrimitiveKind::Boolean)
8829}
8830
8831#[must_use]
8832pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
8833    lemma_type_for_primitive(PrimitiveKind::Number)
8834}
8835
8836#[must_use]
8837pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
8838    lemma_type_for_primitive(PrimitiveKind::Text)
8839}
8840
8841#[must_use]
8842pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
8843    lemma_type_for_primitive(PrimitiveKind::Date)
8844}
8845
8846#[must_use]
8847pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
8848    lemma_type_for_primitive(PrimitiveKind::DateRange)
8849}
8850
8851#[must_use]
8852pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
8853    lemma_type_for_primitive(PrimitiveKind::Time)
8854}
8855
8856#[must_use]
8857pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
8858    lemma_type_for_primitive(PrimitiveKind::Ratio)
8859}
8860
8861/// Singleton primitive `LemmaType` for `kind`. Same `Arc` for the same primitive for the process.
8862#[must_use]
8863pub fn lemma_type_for_primitive(kind: PrimitiveKind) -> &'static Arc<LemmaType> {
8864    let cell = match kind {
8865        PrimitiveKind::Boolean => &PRIMITIVE_BOOLEAN,
8866        PrimitiveKind::Number => &PRIMITIVE_NUMBER,
8867        PrimitiveKind::Text => &PRIMITIVE_TEXT,
8868        PrimitiveKind::Date => &PRIMITIVE_DATE,
8869        PrimitiveKind::DateRange => &PRIMITIVE_DATE_RANGE,
8870        PrimitiveKind::Time => &PRIMITIVE_TIME,
8871        PrimitiveKind::Ratio => &PRIMITIVE_RATIO,
8872        PrimitiveKind::Measure => &PRIMITIVE_MEASURE,
8873        PrimitiveKind::MeasureRange => &PRIMITIVE_MEASURE_RANGE,
8874        PrimitiveKind::NumberRange => &PRIMITIVE_NUMBER_RANGE,
8875        PrimitiveKind::RatioRange => &PRIMITIVE_RATIO_RANGE,
8876        PrimitiveKind::TimeRange => &PRIMITIVE_TIME_RANGE,
8877    };
8878    cell.get_or_init(|| Arc::new(LemmaType::primitive(type_spec_for_primitive(kind))))
8879}
8880
8881/// Map PrimitiveKind to TypeSpecification. Single source of truth for primitive type resolution.
8882#[must_use]
8883pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
8884    match kind {
8885        PrimitiveKind::Boolean => TypeSpecification::boolean(),
8886        PrimitiveKind::Measure => TypeSpecification::measure(),
8887        PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
8888        PrimitiveKind::Number => TypeSpecification::number(),
8889        PrimitiveKind::NumberRange => TypeSpecification::number_range(),
8890        PrimitiveKind::Ratio => TypeSpecification::ratio(),
8891        PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
8892        PrimitiveKind::Text => TypeSpecification::text(),
8893        PrimitiveKind::Date => TypeSpecification::date(),
8894        PrimitiveKind::DateRange => TypeSpecification::date_range(),
8895        PrimitiveKind::Time => TypeSpecification::time(),
8896        PrimitiveKind::TimeRange => TypeSpecification::time_range(),
8897    }
8898}
8899
8900// -----------------------------------------------------------------------------
8901// Display implementations
8902// -----------------------------------------------------------------------------
8903
8904impl fmt::Display for PathSegment {
8905    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8906        let hop = self.input_key();
8907        match &self.repository {
8908            Some(repository) => write!(f, "{hop} → {} {}", repository, self.spec),
8909            None => write!(f, "{hop} → {}", self.spec),
8910        }
8911    }
8912}
8913
8914impl fmt::Display for DataPath {
8915    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8916        write!(f, "{}", self.input_key())
8917    }
8918}
8919
8920impl fmt::Display for RulePath {
8921    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8922        write!(f, "{}", self.input_key())
8923    }
8924}
8925
8926impl fmt::Display for LemmaType {
8927    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8928        write!(f, "{}", self.name())
8929    }
8930}
8931
8932pub(crate) fn format_decimal_for_api(decimal: rust_decimal::Decimal) -> rust_decimal::Decimal {
8933    let normalized = decimal.normalize();
8934    if normalized.fract().is_zero() {
8935        normalized.trunc()
8936    } else {
8937        normalized
8938    }
8939}
8940
8941fn format_decimal_for_human_display(decimal: rust_decimal::Decimal) -> String {
8942    decimal.normalize().to_string()
8943}
8944
8945fn format_rational_for_human_display(
8946    magnitude: &crate::computation::rational::RationalInteger,
8947) -> String {
8948    match magnitude.try_to_decimal() {
8949        Ok(decimal) => format_decimal_for_human_display(decimal),
8950        Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
8951        Err(_) => magnitude.display_str(),
8952    }
8953}
8954
8955/// Whether any declared unit of this measure family has a known factor (literal or
8956/// settled / carried in `runtime_scales`).
8957pub(crate) fn measure_family_has_expressible_unit(
8958    lemma_type: &LemmaType,
8959    runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
8960) -> bool {
8961    let Some(units) = lemma_type.measure_units_table() else {
8962        return true;
8963    };
8964    if units.is_empty() {
8965        return true;
8966    }
8967    units.iter().any(|unit| {
8968        unit.literal_factor().is_some()
8969            || runtime_scales.is_some_and(|scales| scales.contains_key(&unit.name))
8970    })
8971}
8972
8973fn measure_display_unit<'a>(
8974    units: &'a crate::literals::MeasureUnits,
8975    lemma_type: &LemmaType,
8976    runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
8977) -> &'a crate::literals::MeasureUnit {
8978    let factor_known = |unit: &crate::literals::MeasureUnit| {
8979        unit.literal_factor().is_some()
8980            || runtime_scales.is_some_and(|scales| scales.contains_key(&unit.name))
8981    };
8982    if let Some(binding) = lemma_type.unit() {
8983        let unit = units
8984            .iter()
8985            .find(|unit| unit.name == binding)
8986            .unwrap_or_else(|| {
8987                panic!(
8988                    "BUG: measure display binding '{binding}' is not a declared unit on type {}",
8989                    lemma_type.name()
8990                )
8991            });
8992        if factor_known(unit) {
8993            return unit;
8994        }
8995    }
8996    units
8997        .iter()
8998        .find(|unit| unit.is_canonical_factor() && factor_known(unit))
8999        .or_else(|| {
9000            units
9001                .iter()
9002                .find(|unit| unit.literal_factor().is_some() && factor_known(unit))
9003        })
9004        .or_else(|| units.iter().find(|unit| factor_known(unit)))
9005        .unwrap_or_else(|| {
9006            panic!(
9007                "BUG: measure display of '{}' reached with no factor-known unit; callers must veto first",
9008                lemma_type.name()
9009            )
9010        })
9011}
9012
9013fn format_measure_canonical_for_display(
9014    canonical: &crate::computation::rational::RationalInteger,
9015    lemma_type: &LemmaType,
9016    signature: &[(String, i32)],
9017    runtime_scales: Option<&HashMap<String, crate::computation::rational::RationalInteger>>,
9018) -> String {
9019    use crate::computation::rational::checked_div;
9020
9021    if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
9022        if !units.is_empty() {
9023            let unit = measure_display_unit(units, lemma_type, runtime_scales);
9024            let factor = unit
9025                .literal_factor()
9026                .cloned()
9027                .or_else(|| {
9028                    runtime_scales.and_then(|scales| scales.get(&unit.name).cloned())
9029                })
9030                .unwrap_or_else(|| {
9031                    panic!(
9032                        "BUG: display of unit '{}' on {} requires a literal factor or a settled runtime scale",
9033                        unit.name,
9034                        lemma_type.name()
9035                    )
9036                });
9037            let in_unit = checked_div(canonical, &factor)
9038                .expect("BUG: de-canonicalization for measure display must not fail");
9039            return format_number_with_unit_for_display(&in_unit, &unit.name);
9040        }
9041    }
9042
9043    let unit_label = match signature {
9044        [] => String::new(),
9045        [(name, 1)] => name.clone(),
9046        _ => format_signature_operator_style(signature),
9047    };
9048    let formatted = format_rational_for_human_display(canonical);
9049    if unit_label.is_empty() {
9050        formatted
9051    } else {
9052        format!("{formatted} {unit_label}")
9053    }
9054}
9055
9056fn format_ratio_canonical_for_display(
9057    canonical: &crate::computation::rational::RationalInteger,
9058    lemma_type: &LemmaType,
9059) -> String {
9060    use crate::computation::rational::checked_mul;
9061
9062    let units = match &lemma_type.specifications {
9063        TypeSpecification::Ratio { units, .. } | TypeSpecification::RatioRange { units, .. } => {
9064            units
9065        }
9066        _ => unreachable!(
9067            "BUG: ratio display with non-ratio type {}",
9068            lemma_type.name()
9069        ),
9070    };
9071    let display_unit = lemma_type
9072        .unit()
9073        .or_else(|| lemma_type.ratio_primary_unit());
9074    let Some(unit_name) = display_unit else {
9075        return format_rational_magnitude_for_display(canonical);
9076    };
9077    let unit = units.get(unit_name).unwrap_or_else(|_| {
9078        panic!(
9079            "BUG: ratio display unit '{unit_name}' is not declared on type {}",
9080            lemma_type.name()
9081        )
9082    });
9083    let in_unit = checked_mul(canonical, &unit.value)
9084        .expect("BUG: de-canonicalization for ratio display must not fail");
9085    format_number_with_unit_for_display(&in_unit, unit_name)
9086}
9087
9088impl fmt::Display for LiteralValue {
9089    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9090        // Measure/ratio display needs an explicit type; callers must use
9091        // `display_value_with_type`. Bare Display only covers type-free kinds and
9092        // ranges of those kinds.
9093        match &self.value {
9094            ValueKind::Measure(_) | ValueKind::Ratio(_) => {
9095                write!(f, "{}", self.value)
9096            }
9097            ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
9098            _ => write!(f, "{}", self.value),
9099        }
9100    }
9101}
9102
9103// -----------------------------------------------------------------------------
9104// Tests
9105// -----------------------------------------------------------------------------
9106
9107#[cfg(test)]
9108pub(crate) mod tests {
9109    use super::*;
9110    use crate::computation::rational::decimal_to_rational;
9111    use crate::literals::DateGranularity;
9112    use crate::literals::Value;
9113    use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
9114    use rust_decimal::Decimal;
9115    use std::str::FromStr;
9116
9117    fn meter_axis(exponent: i32) -> BaseMeasureVector {
9118        [(Dimension::Family(test_type_family("meter")), exponent)]
9119            .into_iter()
9120            .collect()
9121    }
9122
9123    #[test]
9124    fn scale_decomposition_exponent_one_is_identity() {
9125        let base = meter_axis(1);
9126        assert_eq!(scale_decomposition(&base, 1), Some(base));
9127    }
9128
9129    #[test]
9130    fn scale_decomposition_squares_meter() {
9131        assert_eq!(scale_decomposition(&meter_axis(1), 2), Some(meter_axis(2)));
9132    }
9133
9134    #[test]
9135    fn scale_decomposition_zero_is_empty() {
9136        assert_eq!(
9137            scale_decomposition(&meter_axis(1), 0),
9138            Some(BaseMeasureVector::new())
9139        );
9140    }
9141
9142    #[test]
9143    fn scale_decomposition_negative_inverts_meter() {
9144        assert_eq!(
9145            scale_decomposition(&meter_axis(1), -1),
9146            Some(meter_axis(-1))
9147        );
9148    }
9149
9150    #[test]
9151    fn scale_decomposition_at_exponent_cap() {
9152        assert_eq!(
9153            scale_decomposition(&meter_axis(32), 2),
9154            Some(meter_axis(64))
9155        );
9156    }
9157
9158    #[test]
9159    fn scale_decomposition_past_exponent_cap_is_none() {
9160        assert_eq!(scale_decomposition(&meter_axis(1), 65), None);
9161        assert_eq!(scale_decomposition(&meter_axis(32), 3), None);
9162    }
9163
9164    #[must_use]
9165    pub(crate) fn test_type_family(name: &str) -> TypeFamily {
9166        TypeFamily {
9167            repository: None,
9168            spec: "test".to_string(),
9169            effective: crate::parsing::ast::EffectiveDate::Origin,
9170            name: name.to_string(),
9171        }
9172    }
9173
9174    #[must_use]
9175    pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
9176        crate::planning::semantics::lemma_type_for_primitive(PrimitiveKind::Measure)
9177    }
9178
9179    #[must_use]
9180    pub(crate) fn primitive_measure() -> &'static LemmaType {
9181        primitive_measure_arc().as_ref()
9182    }
9183
9184    #[test]
9185    fn default_primitive_help_is_goal_oriented() {
9186        let kinds = [
9187            PrimitiveKind::Boolean,
9188            PrimitiveKind::Measure,
9189            PrimitiveKind::MeasureRange,
9190            PrimitiveKind::Number,
9191            PrimitiveKind::NumberRange,
9192            PrimitiveKind::Ratio,
9193            PrimitiveKind::RatioRange,
9194            PrimitiveKind::Text,
9195            PrimitiveKind::Date,
9196            PrimitiveKind::DateRange,
9197            PrimitiveKind::Time,
9198            PrimitiveKind::TimeRange,
9199        ];
9200        for kind in kinds {
9201            let spec = type_spec_for_primitive(kind);
9202            let help = match &spec {
9203                TypeSpecification::Boolean { help, .. }
9204                | TypeSpecification::Number { help, .. }
9205                | TypeSpecification::NumberRange { help, .. }
9206                | TypeSpecification::Text { help, .. }
9207                | TypeSpecification::Measure { help, .. }
9208                | TypeSpecification::MeasureRange { help, .. }
9209                | TypeSpecification::Ratio { help, .. }
9210                | TypeSpecification::RatioRange { help, .. }
9211                | TypeSpecification::Date { help, .. }
9212                | TypeSpecification::DateRange { help, .. }
9213                | TypeSpecification::TimeRange { help, .. }
9214                | TypeSpecification::Time { help, .. } => help,
9215                TypeSpecification::Veto { .. }
9216                | TypeSpecification::Undetermined
9217                | TypeSpecification::Spec { .. }
9218                | TypeSpecification::List { .. } => {
9219                    unreachable!(
9220                        "BUG: primitive kind {:?} mapped to non-primitive spec",
9221                        kind
9222                    )
9223                }
9224            };
9225            assert!(!help.is_empty(), "help for {:?}", kind);
9226            assert!(
9227                !help.to_ascii_lowercase().contains("format:"),
9228                "help for {:?} must not describe syntax: {:?}",
9229                kind,
9230                help
9231            );
9232            assert_eq!(help, default_help_for_primitive(kind));
9233        }
9234    }
9235
9236    #[test]
9237    fn data_path_and_rule_path_display_are_input_key() {
9238        let implicit = PathSegment {
9239            uses: Arc::from("bag"),
9240            row: None,
9241            repository: None,
9242            spec: Arc::from("bag"),
9243        };
9244        let explicit = PathSegment {
9245            uses: Arc::from("bag"),
9246            row: None,
9247            repository: None,
9248            spec: Arc::from("item_bag"),
9249        };
9250        let named_repo = PathSegment {
9251            uses: Arc::from("calc"),
9252            row: None,
9253            repository: Some(Arc::from("alpha")),
9254            spec: Arc::from("pricing"),
9255        };
9256        let row = PathSegment {
9257            uses: Arc::from("staff"),
9258            row: Some(2),
9259            repository: None,
9260            spec: Arc::from("employee"),
9261        };
9262
9263        assert_eq!(
9264            DataPath::new(vec![row.clone()], "salary".to_string()).to_string(),
9265            "staff.2.salary"
9266        );
9267        assert_eq!(
9268            RulePath::new(vec![row.clone()], "double".to_string()).to_string(),
9269            "staff.2.double"
9270        );
9271        assert_eq!(row.to_string(), "staff.2 → employee");
9272        assert_eq!(
9273            SpecInstance {
9274                prefix: vec![implicit.clone(), row]
9275            }
9276            .to_string(),
9277            "bag.staff.2"
9278        );
9279        assert_eq!(
9280            DataPath::new(vec![implicit.clone()], "weight".to_string()).to_string(),
9281            "bag.weight"
9282        );
9283        assert_eq!(
9284            DataPath::new(vec![explicit.clone()], "weight".to_string()).to_string(),
9285            "bag.weight"
9286        );
9287        assert_eq!(
9288            DataPath::new(vec![named_repo.clone()], "price".to_string()).to_string(),
9289            "calc.price"
9290        );
9291        assert_eq!(
9292            RulePath::new(vec![implicit], "cost".to_string()).to_string(),
9293            "bag.cost"
9294        );
9295        assert_eq!(
9296            RulePath::new(vec![explicit], "cost".to_string()).to_string(),
9297            "bag.cost"
9298        );
9299        assert_eq!(
9300            RulePath::new(vec![named_repo], "price".to_string()).to_string(),
9301            "calc.price"
9302        );
9303
9304        assert_eq!(
9305            PathSegment {
9306                uses: Arc::from("bag"),
9307                row: None,
9308                repository: None,
9309                spec: Arc::from("item_bag"),
9310            }
9311            .to_string(),
9312            "bag → item_bag"
9313        );
9314        assert_eq!(
9315            PathSegment {
9316                uses: Arc::from("calc"),
9317                row: None,
9318                repository: Some(Arc::from("alpha")),
9319                spec: Arc::from("pricing"),
9320            }
9321            .to_string(),
9322            "calc → alpha pricing"
9323        );
9324    }
9325
9326    #[test]
9327    fn test_negated_comparison() {
9328        assert_eq!(
9329            negated_comparison(ComparisonComputation::LessThan),
9330            ComparisonComputation::GreaterThanOrEqual
9331        );
9332        assert_eq!(
9333            negated_comparison(ComparisonComputation::GreaterThanOrEqual),
9334            ComparisonComputation::LessThan
9335        );
9336        assert_eq!(
9337            negated_comparison(ComparisonComputation::Is),
9338            ComparisonComputation::IsNot
9339        );
9340        assert_eq!(
9341            negated_comparison(ComparisonComputation::IsNot),
9342            ComparisonComputation::Is
9343        );
9344    }
9345
9346    #[test]
9347    fn value_to_semantic_number_is_decimal() {
9348        let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
9349        assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
9350    }
9351
9352    #[test]
9353    fn value_kind_measure_serializes_magnitude_only() {
9354        let kind =
9355            ValueKind::Measure(decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap());
9356        let json = serde_json::to_value(crate::api::ValueKind::from(&kind)).unwrap();
9357        assert_eq!(json["measure"]["value"], "99.5");
9358        assert!(json["measure"].get("signature").is_none());
9359    }
9360
9361    #[test]
9362    fn value_kind_measure_roundtrips() {
9363        let original =
9364            ValueKind::Measure(decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap());
9365        let api = crate::api::ValueKind::from(&original);
9366        let json = serde_json::to_string(&api).unwrap();
9367        let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
9368        assert_eq!(api, parsed);
9369    }
9370
9371    #[test]
9372    fn value_kind_measure_empty_roundtrips() {
9373        let original =
9374            ValueKind::Measure(decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap());
9375        let api = crate::api::ValueKind::from(&original);
9376        let json = serde_json::to_string(&api).unwrap();
9377        let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
9378        assert_eq!(api, parsed);
9379    }
9380
9381    #[test]
9382    fn literal_value_number_serde_not_rational_array() {
9383        let lit = LiteralValue::number_from_decimal(Decimal::from(20));
9384        let json = serde_json::to_value(crate::api::LiteralValue::from(&lit)).unwrap();
9385        let number = json
9386            .get("value")
9387            .and_then(|v| v.get("number"))
9388            .expect("number field");
9389        assert!(number.is_string());
9390        assert_eq!(number.as_str(), Some("20"));
9391        assert!(
9392            !number.is_array(),
9393            "stored number must not serialize as [n,d]"
9394        );
9395    }
9396
9397    #[test]
9398    fn test_literal_value_to_primitive_type() {
9399        let one = rational_new(1, 1);
9400
9401        assert_eq!(TypedLiteral::text("".to_string()).lemma_type.name(), "text");
9402        assert_eq!(
9403            TypedLiteral::number(one.clone()).lemma_type.name(),
9404            "number"
9405        );
9406        assert_eq!(
9407            TypedLiteral::from_bool(bool::from(BooleanValue::True))
9408                .lemma_type
9409                .name(),
9410            "boolean"
9411        );
9412
9413        let dt = DateTimeValue {
9414            year: 2024,
9415            month: 1,
9416            day: 1,
9417            hour: 0,
9418            minute: 0,
9419            second: 0,
9420            microsecond: 0,
9421            timezone: None,
9422
9423            granularity: DateGranularity::Full,
9424        };
9425        assert_eq!(
9426            TypedLiteral::date(date_time_to_semantic(&dt))
9427                .lemma_type
9428                .name(),
9429            "date"
9430        );
9431        assert_eq!(
9432            TypedLiteral::ratio_from_decimal(Decimal::new(1, 2))
9433                .lemma_type
9434                .name(),
9435            "ratio"
9436        );
9437        let dur_type = LemmaType::new(
9438            "duration".to_string(),
9439            TypeSpecification::Measure {
9440                minimum: None,
9441                maximum: None,
9442                units: MeasureUnits::from(vec![MeasureUnit {
9443                    name: "second".to_string(),
9444                    scale: UnitScale::Literal(crate::computation::rational::rational_one()),
9445                    derived_measure_factors: Vec::new(),
9446                    decomposition: BaseMeasureVector::new(),
9447                    minimum: None,
9448                    maximum: None,
9449                    suggestion_magnitude: None,
9450                }]),
9451                traits: vec![MeasureTrait::Duration],
9452                decomposition: None,
9453                unit: None,
9454                help: String::new(),
9455            },
9456            TypeExtends::Primitive,
9457        );
9458        assert_eq!(
9459            TypedLiteral::measure_with_type(one.clone(), Arc::new(dur_type))
9460                .lemma_type
9461                .name(),
9462            "duration"
9463        );
9464    }
9465
9466    #[test]
9467    fn test_type_display() {
9468        let specs = TypeSpecification::text();
9469        let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
9470        assert_eq!(format!("{}", lemma_type), "name");
9471    }
9472
9473    #[test]
9474    fn test_type_serialization() {
9475        let specs = TypeSpecification::number();
9476        let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
9477        let api = crate::api::LemmaType::from(&lemma_type);
9478        let serialized = serde_json::to_string(&api).unwrap();
9479        let deserialized: crate::api::LemmaType = serde_json::from_str(&serialized).unwrap();
9480        assert_eq!(api, deserialized);
9481    }
9482
9483    #[test]
9484    fn empty_list_display_value_with_type_is_type_spec() {
9485        let element = Arc::new(LemmaType::new(
9486            "item".to_string(),
9487            TypeSpecification::Spec {
9488                spec: SpecIdentity {
9489                    repository: None,
9490                    spec: "item".to_string(),
9491                    effective: crate::parsing::ast::EffectiveDate::Origin,
9492                    slice: crate::parsing::ast::EffectiveDate::Origin,
9493                },
9494            },
9495            TypeExtends::Primitive,
9496        ));
9497        let list_ty = LemmaType::list_of(element);
9498        let empty = LiteralValue::new(ValueKind::List(Vec::new().into()));
9499        assert_eq!(empty.display_value_with_type(&list_ty), "item list");
9500        assert_eq!(empty.display_value(), "");
9501    }
9502
9503    #[test]
9504    fn test_literal_value_display_value() {
9505        let ten = rational_new(10, 1);
9506
9507        assert_eq!(
9508            LiteralValue::text("hello".to_string()).display_value(),
9509            "hello"
9510        );
9511        assert_eq!(LiteralValue::number(ten).display_value(), "10");
9512        assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
9513        assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
9514
9515        // 0.10 ratio with "percent" binding displays as 10% (unit conversion applied)
9516        let ratio_ty = Arc::new(primitive_ratio_arc().as_ref().clone().with_unit("percent"));
9517        let ten_percent_ratio = LiteralValue::ratio(
9518            crate::literals::rational_from_parsed_decimal(Decimal::new(1, 1))
9519                .expect("ratio decimal"),
9520        );
9521        assert_eq!(
9522            ten_percent_ratio.display_value_with_type(ratio_ty.as_ref()),
9523            "10%"
9524        );
9525
9526        let time = TimeValue {
9527            hour: 14,
9528            minute: 30,
9529            second: 0,
9530            microsecond: 0,
9531            timezone: None,
9532        };
9533        let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
9534        assert!(time_display.contains("14"));
9535        assert!(time_display.contains("30"));
9536    }
9537
9538    #[test]
9539    fn test_measure_display_trims_trailing_zeros() {
9540        let money_type = LemmaType {
9541            name: Some("money".to_string()),
9542            specifications: TypeSpecification::Measure {
9543                minimum: None,
9544                maximum: None,
9545                units: MeasureUnits::from(vec![MeasureUnit {
9546                    name: "eur".to_string(),
9547                    scale: UnitScale::Literal(crate::computation::rational::rational_one()),
9548                    derived_measure_factors: Vec::new(),
9549                    decomposition: BaseMeasureVector::new(),
9550                    minimum: None,
9551                    maximum: None,
9552                    suggestion_magnitude: None,
9553                }]),
9554                traits: Vec::new(),
9555                decomposition: None,
9556                unit: None,
9557                help: String::new(),
9558            },
9559            extends: TypeExtends::Primitive,
9560            options: TypeOptions::default(),
9561        };
9562        let money_type = Arc::new(money_type);
9563        let val =
9564            LiteralValue::measure(decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap());
9565        assert_eq!(val.display_value_with_type(money_type.as_ref()), "1.8 eur");
9566        let more_precision = LiteralValue::measure(
9567            decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
9568        );
9569        assert_eq!(
9570            more_precision.display_value_with_type(money_type.as_ref()),
9571            "1.8 eur"
9572        );
9573        let count_type = Arc::new(LemmaType {
9574            name: Some("count".to_string()),
9575            specifications: TypeSpecification::Measure {
9576                minimum: None,
9577                maximum: None,
9578                units: MeasureUnits::from(vec![MeasureUnit {
9579                    name: "items".to_string(),
9580                    scale: UnitScale::Literal(crate::computation::rational::rational_one()),
9581                    derived_measure_factors: Vec::new(),
9582                    decomposition: BaseMeasureVector::new(),
9583                    minimum: None,
9584                    maximum: None,
9585                    suggestion_magnitude: None,
9586                }]),
9587                traits: Vec::new(),
9588                decomposition: None,
9589                unit: None,
9590                help: String::new(),
9591            },
9592            extends: TypeExtends::Primitive,
9593            options: TypeOptions::default(),
9594        });
9595        let val_any = LiteralValue::measure(
9596            decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
9597        );
9598        assert_eq!(
9599            val_any.display_value_with_type(count_type.as_ref()),
9600            "42.5 items"
9601        );
9602    }
9603
9604    #[test]
9605    fn test_literal_value_time_type() {
9606        let time = TimeValue {
9607            hour: 14,
9608            minute: 30,
9609            second: 0,
9610            microsecond: 0,
9611            timezone: None,
9612        };
9613        let lit = TypedLiteral::time(time_to_semantic(&time));
9614        assert_eq!(lit.lemma_type.name(), "time");
9615    }
9616
9617    #[test]
9618    fn test_measure_family_name_none_without_declaring_spec() {
9619        let money_primitive = LemmaType::new(
9620            "money".to_string(),
9621            TypeSpecification::measure(),
9622            TypeExtends::Primitive,
9623        );
9624        assert_eq!(money_primitive.measure_family_name(), None);
9625    }
9626
9627    #[test]
9628    fn test_measure_family_name_custom() {
9629        let measure_spec = TypeSpecification::measure();
9630        let money_custom = LemmaType::new(
9631            "money".to_string(),
9632            measure_spec,
9633            TypeExtends::custom_local("money".to_string(), test_type_family("money")),
9634        );
9635        assert_eq!(money_custom.measure_family_name(), Some("money"));
9636    }
9637
9638    #[test]
9639    fn test_same_named_roots_in_different_specs_are_different_families() {
9640        let declared_in = |spec: &str| TypeFamily {
9641            repository: None,
9642            spec: spec.to_string(),
9643            effective: crate::parsing::ast::EffectiveDate::Origin,
9644            name: "money".to_string(),
9645        };
9646        let pricing_money = LemmaType::new(
9647            "money".to_string(),
9648            TypeSpecification::measure(),
9649            TypeExtends::custom_local("measure".to_string(), declared_in("pricing")),
9650        );
9651        let shop_money = LemmaType::new(
9652            "money".to_string(),
9653            TypeSpecification::measure(),
9654            TypeExtends::custom_local("measure".to_string(), declared_in("shop")),
9655        );
9656        assert!(!pricing_money.same_measure_family(&shop_money));
9657        assert!(!shop_money.same_measure_family(&pricing_money));
9658    }
9659
9660    #[test]
9661    fn test_same_measure_family_parent_and_child() {
9662        let measure_spec = TypeSpecification::measure();
9663        let type_x = LemmaType::new(
9664            "x".to_string(),
9665            measure_spec.clone(),
9666            TypeExtends::custom_local("measure".to_string(), test_type_family("x")),
9667        );
9668        let type_x2 = LemmaType::new(
9669            "x2".to_string(),
9670            measure_spec,
9671            TypeExtends::custom_local("x".to_string(), test_type_family("x")),
9672        );
9673        assert_eq!(type_x.measure_family_name(), Some("x"));
9674        assert_eq!(type_x2.measure_family_name(), Some("x"));
9675        assert!(type_x.same_measure_family(&type_x2));
9676        assert!(type_x2.same_measure_family(&type_x));
9677    }
9678
9679    #[test]
9680    fn test_same_measure_family_siblings() {
9681        let measure_spec = TypeSpecification::measure();
9682        let type_x2_a = LemmaType::new(
9683            "x2a".to_string(),
9684            measure_spec.clone(),
9685            TypeExtends::custom_local("x".to_string(), test_type_family("x")),
9686        );
9687        let type_x2_b = LemmaType::new(
9688            "x2b".to_string(),
9689            measure_spec,
9690            TypeExtends::custom_local("x".to_string(), test_type_family("x")),
9691        );
9692        assert!(type_x2_a.same_measure_family(&type_x2_b));
9693    }
9694
9695    #[test]
9696    fn test_same_measure_family_different_families() {
9697        let measure_spec = TypeSpecification::measure();
9698        let money = LemmaType::new(
9699            "money".to_string(),
9700            measure_spec.clone(),
9701            TypeExtends::Primitive,
9702        );
9703        let temperature = LemmaType::new(
9704            "temperature".to_string(),
9705            measure_spec,
9706            TypeExtends::Primitive,
9707        );
9708        assert!(!money.same_measure_family(&temperature));
9709        assert!(!temperature.same_measure_family(&money));
9710    }
9711
9712    #[test]
9713    fn test_same_measure_family_measure_vs_non_measure() {
9714        let measure_spec = TypeSpecification::measure();
9715        let number_spec = TypeSpecification::number();
9716        let measure_type =
9717            LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
9718        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
9719        assert!(!measure_type.same_measure_family(&number_type));
9720        assert!(!number_type.same_measure_family(&measure_type));
9721    }
9722
9723    #[test]
9724    fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
9725        let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
9726        let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
9727
9728        assert!(!left.same_measure_family(&right));
9729        assert!(left.compatible_with_anonymous_measure(&right));
9730    }
9731
9732    #[test]
9733    fn same_value_type_distinct_families_same_decomposition_are_not_same() {
9734        let decomp: BaseMeasureVector = [
9735            (Dimension::Family(test_type_family("length")), 1i32),
9736            (Dimension::Family(test_type_family("elapsed")), -1i32),
9737        ]
9738        .into_iter()
9739        .collect();
9740        let speed_spec = |name: &str, family: &str| {
9741            let mut spec = TypeSpecification::measure();
9742            if let TypeSpecification::Measure {
9743                decomposition,
9744                help,
9745                ..
9746            } = &mut spec
9747            {
9748                *decomposition = Some(decomp.clone());
9749                *help = String::new();
9750            }
9751            LemmaType::new(
9752                name.to_string(),
9753                spec,
9754                TypeExtends::custom_local("measure".to_string(), test_type_family(family)),
9755            )
9756        };
9757        let speed_a = speed_spec("speed_a", "speed_a");
9758        let speed_b = speed_spec("speed_b", "speed_b");
9759        assert_eq!(
9760            speed_a.measure_type_decomposition(),
9761            speed_b.measure_type_decomposition()
9762        );
9763        assert!(!speed_a.same_measure_family(&speed_b));
9764        assert!(
9765            !speed_a.same_value_type(&speed_b),
9766            "same decomposition alone must not make distinct measure families same_value_type"
9767        );
9768    }
9769
9770    #[test]
9771    fn test_measure_family_name_non_measure_returns_none() {
9772        let number_spec = TypeSpecification::number();
9773        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
9774        assert_eq!(number_type.measure_family_name(), None);
9775    }
9776
9777    #[test]
9778    fn test_lemma_type_inequality_local_vs_import_same_shape() {
9779        let measure_spec = TypeSpecification::measure();
9780        let local = LemmaType::new(
9781            "t".to_string(),
9782            measure_spec.clone(),
9783            TypeExtends::custom_local("money".to_string(), test_type_family("money")),
9784        );
9785        let imported = LemmaType::new(
9786            "t".to_string(),
9787            measure_spec,
9788            TypeExtends::Custom {
9789                parent: "money".to_string(),
9790                family: test_type_family("money"),
9791                defining_spec: TypeDefiningSpec::Import,
9792            },
9793        );
9794        assert_ne!(local, imported);
9795    }
9796
9797    #[test]
9798    fn test_lemma_type_equality_import_unit_variant() {
9799        let measure_spec = TypeSpecification::measure();
9800        let left = LemmaType::new(
9801            "t".to_string(),
9802            measure_spec.clone(),
9803            TypeExtends::Custom {
9804                parent: "money".to_string(),
9805                family: test_type_family("money"),
9806                defining_spec: TypeDefiningSpec::Import,
9807            },
9808        );
9809        let right = LemmaType::new(
9810            "t".to_string(),
9811            measure_spec,
9812            TypeExtends::Custom {
9813                parent: "money".to_string(),
9814                family: test_type_family("money"),
9815                defining_spec: TypeDefiningSpec::Import,
9816            },
9817        );
9818        assert_eq!(left, right);
9819    }
9820
9821    fn month_suggestion_arg() -> CommandArg {
9822        CommandArg::Literal(crate::literals::Value::NumberWithUnit(
9823            Decimal::ONE,
9824            "month".to_string(),
9825        ))
9826    }
9827
9828    fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
9829        [
9830            CommandArg::Label(name.to_string()),
9831            CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
9832        ]
9833    }
9834
9835    #[test]
9836    fn default_calendar_on_text_reports_hint() {
9837        let mut specs = TypeSpecification::text();
9838        let mut default = None;
9839        let err = specs
9840            .apply_constraint(
9841                ConstraintNames {
9842                    type_name: "notes",
9843                    data_name: "notes",
9844                },
9845                TypeConstraintCommand::Suggest,
9846                &[month_suggestion_arg()],
9847                &mut default,
9848                &mut None,
9849                None,
9850            )
9851            .unwrap_err();
9852        assert!(err.contains("Unit 'month' is for calendar data"));
9853        assert!(err.contains("double quotes"));
9854    }
9855
9856    #[test]
9857    fn default_calendar_on_duration_reports_valid_units() {
9858        let mut specs = TypeSpecification::measure();
9859        specs
9860            .apply_constraint(
9861                ConstraintNames {
9862                    type_name: "duration",
9863                    data_name: "duration",
9864                },
9865                TypeConstraintCommand::Unit,
9866                &unit_factor_arg("second", 1),
9867                &mut None,
9868                &mut None,
9869                None,
9870            )
9871            .unwrap();
9872        specs
9873            .apply_constraint(
9874                ConstraintNames {
9875                    type_name: "duration",
9876                    data_name: "duration",
9877                },
9878                TypeConstraintCommand::Unit,
9879                &unit_factor_arg("week", 604_800),
9880                &mut None,
9881                &mut None,
9882                None,
9883            )
9884            .unwrap();
9885        specs
9886            .apply_constraint(
9887                ConstraintNames {
9888                    type_name: "duration",
9889                    data_name: "duration",
9890                },
9891                TypeConstraintCommand::Trait,
9892                &[CommandArg::Label("duration".to_string())],
9893                &mut None,
9894                &mut None,
9895                None,
9896            )
9897            .unwrap();
9898        let mut default = None;
9899        let err = specs
9900            .apply_constraint(
9901                ConstraintNames {
9902                    type_name: "duration",
9903                    data_name: "duration",
9904                },
9905                TypeConstraintCommand::Suggest,
9906                &[month_suggestion_arg()],
9907                &mut default,
9908                &mut None,
9909                None,
9910            )
9911            .unwrap_err();
9912        assert!(err.contains("Unit 'month' is for calendar data"));
9913        assert!(err.contains("Valid 'duration' units are"));
9914        assert!(err.contains("week"));
9915    }
9916
9917    #[test]
9918    fn default_valid_duration_weeks_accepted() {
9919        let mut specs = TypeSpecification::measure();
9920        specs
9921            .apply_constraint(
9922                ConstraintNames {
9923                    type_name: "duration",
9924                    data_name: "duration",
9925                },
9926                TypeConstraintCommand::Unit,
9927                &unit_factor_arg("second", 1),
9928                &mut None,
9929                &mut None,
9930                None,
9931            )
9932            .unwrap();
9933        specs
9934            .apply_constraint(
9935                ConstraintNames {
9936                    type_name: "duration",
9937                    data_name: "duration",
9938                },
9939                TypeConstraintCommand::Unit,
9940                &unit_factor_arg("week", 604_800),
9941                &mut None,
9942                &mut None,
9943                None,
9944            )
9945            .unwrap();
9946        specs
9947            .apply_constraint(
9948                ConstraintNames {
9949                    type_name: "duration",
9950                    data_name: "duration",
9951                },
9952                TypeConstraintCommand::Trait,
9953                &[CommandArg::Label("duration".to_string())],
9954                &mut None,
9955                &mut None,
9956                None,
9957            )
9958            .unwrap();
9959        let mut default = None;
9960        specs
9961            .apply_constraint(
9962                ConstraintNames {
9963                    type_name: "duration",
9964                    data_name: "duration",
9965                },
9966                TypeConstraintCommand::Suggest,
9967                &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
9968                    Decimal::from(4),
9969                    "week".to_string(),
9970                ))],
9971                &mut default,
9972                &mut None,
9973                None,
9974            )
9975            .unwrap();
9976        assert!(matches!(
9977            default,
9978            Some(RawSuggestion::Measure {
9979                unit_name,
9980                ..
9981            }) if unit_name == "week"
9982        ));
9983    }
9984
9985    #[test]
9986    fn default_unknown_unit_on_duration_lists_valid_units() {
9987        let mut specs = TypeSpecification::measure();
9988        specs
9989            .apply_constraint(
9990                ConstraintNames {
9991                    type_name: "duration",
9992                    data_name: "duration",
9993                },
9994                TypeConstraintCommand::Unit,
9995                &unit_factor_arg("second", 1),
9996                &mut None,
9997                &mut None,
9998                None,
9999            )
10000            .unwrap();
10001        specs
10002            .apply_constraint(
10003                ConstraintNames {
10004                    type_name: "duration",
10005                    data_name: "duration",
10006                },
10007                TypeConstraintCommand::Trait,
10008                &[CommandArg::Label("duration".to_string())],
10009                &mut None,
10010                &mut None,
10011                None,
10012            )
10013            .unwrap();
10014        let mut default = None;
10015        let err = specs
10016            .apply_constraint(
10017                ConstraintNames {
10018                    type_name: "duration",
10019                    data_name: "duration",
10020                },
10021                TypeConstraintCommand::Suggest,
10022                &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
10023                    Decimal::ONE,
10024                    "fortnight".to_string(),
10025                ))],
10026                &mut default,
10027                &mut None,
10028                None,
10029            )
10030            .unwrap_err();
10031        assert!(err.contains("fortnight"));
10032        assert!(err.contains("not defined on 'duration'"));
10033        assert!(err.contains("Valid units are"));
10034    }
10035
10036    fn money_measure_type() -> LemmaType {
10037        LemmaType::new(
10038            "Money".to_string(),
10039            TypeSpecification::Measure {
10040                minimum: None,
10041                maximum: None,
10042                units: MeasureUnits::from(vec![
10043                    MeasureUnit {
10044                        name: "eur".to_string(),
10045                        scale: UnitScale::Literal(crate::computation::rational::rational_one()),
10046                        derived_measure_factors: Vec::new(),
10047                        decomposition: BaseMeasureVector::new(),
10048                        minimum: None,
10049                        maximum: None,
10050                        suggestion_magnitude: None,
10051                    },
10052                    MeasureUnit {
10053                        name: "usd".to_string(),
10054                        scale: UnitScale::Literal(
10055                            crate::computation::rational::decimal_to_rational(Decimal::new(91, 2))
10056                                .expect("factor"),
10057                        ),
10058                        derived_measure_factors: Vec::new(),
10059                        decomposition: BaseMeasureVector::new(),
10060                        minimum: None,
10061                        maximum: None,
10062                        suggestion_magnitude: None,
10063                    },
10064                ]),
10065                traits: Vec::new(),
10066                decomposition: None,
10067                unit: None,
10068                help: String::new(),
10069            },
10070            TypeExtends::Primitive,
10071        )
10072    }
10073
10074    #[test]
10075    fn measure_unit_names_for_named_measure() {
10076        let money = money_measure_type();
10077        assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
10078    }
10079
10080    // ---------------------------------------------------------------------------
10081    // Phase 0 — pin combine_signatures and canonicalize_signature behavior
10082    // ---------------------------------------------------------------------------
10083
10084    fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
10085        pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
10086    }
10087
10088    #[test]
10089    fn combine_signatures_multiply_adds_exponents() {
10090        let left = sig(&[("eur", 1)]);
10091        let right = sig(&[("hour", -1)]);
10092        let result = combine_signatures(&left, &right, true);
10093        assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
10094    }
10095
10096    #[test]
10097    fn combine_signatures_divide_subtracts_exponents() {
10098        let left = sig(&[("eur", 1)]);
10099        let right = sig(&[("hour", 1)]);
10100        let result = combine_signatures(&left, &right, false);
10101        assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
10102    }
10103
10104    #[test]
10105    fn combine_signatures_cancels_to_empty() {
10106        let left = sig(&[("ce", 1), ("minute", -1)]);
10107        let right = sig(&[("minute", 1)]);
10108        let result = combine_signatures(&left, &right, true);
10109        // ce * (ce/min * min) = ce; minute cancels
10110        assert_eq!(result, sig(&[("ce", 1)]));
10111    }
10112
10113    #[test]
10114    fn combine_signatures_output_is_canonical_form() {
10115        let left = sig(&[("eur", 1), ("hour", 1)]);
10116        let right = sig(&[("minute", 1)]);
10117        let result = combine_signatures(&left, &right, false); // divide
10118                                                               // [("eur",1),("hour",1)] / [("minute",1)] = [("eur",1),("hour",1),("minute",-1)]
10119        let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
10120        assert_eq!(result, expected);
10121    }
10122
10123    #[test]
10124    fn canonicalize_signature_drops_zero_exponents() {
10125        let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
10126        let result = canonicalize_signature(&sig_with_zero);
10127        assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
10128    }
10129
10130    #[test]
10131    fn canonicalize_signature_sorts_by_name() {
10132        let unsorted = sig(&[("minute", -1), ("eur", 1)]);
10133        let result = canonicalize_signature(&unsorted);
10134        assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
10135    }
10136
10137    // ---------------------------------------------------------------------------
10138    // Phase 0 — format_signature_operator_style (to be implemented in
10139    // signature_factor_and_display todo)
10140    // ---------------------------------------------------------------------------
10141
10142    #[test]
10143    fn format_signature_operator_style_numerator_only() {
10144        let signature = sig(&[("eur", 1)]);
10145        let result = format_signature_operator_style(&signature);
10146        assert_eq!(result, "eur");
10147    }
10148
10149    #[test]
10150    fn format_signature_operator_style_with_denominator() {
10151        let signature = sig(&[("eur", 1), ("hour", -1)]);
10152        let result = format_signature_operator_style(&signature);
10153        assert_eq!(result, "eur/hour");
10154    }
10155
10156    #[test]
10157    fn format_signature_operator_style_denominator_only() {
10158        let signature = sig(&[("meter", -1)]);
10159        let result = format_signature_operator_style(&signature);
10160        assert_eq!(result, "1/meter");
10161    }
10162
10163    #[test]
10164    fn format_signature_operator_style_with_exponents() {
10165        let signature = sig(&[("meter", 2), ("second", -2)]);
10166        let result = format_signature_operator_style(&signature);
10167        assert_eq!(result, "meter^2/second^2");
10168    }
10169
10170    // ---------------------------------------------------------------------------
10171    // Phase 0 — calendar_unit_factor (to be implemented in builtin_calendar_factor_table)
10172    // ---------------------------------------------------------------------------
10173
10174    #[test]
10175    fn calendar_unit_factor_table_completeness() {
10176        // Every SemanticCalendarUnit Display string must resolve to a factor.
10177        // Today SemanticCalendarUnit only has Month and Year; more may be added.
10178        for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
10179            let name = unit.to_string();
10180            assert!(
10181                calendar_unit_factor(&name).is_some(),
10182                "calendar_unit_factor('{}') must return Some",
10183                name
10184            );
10185        }
10186    }
10187
10188    #[test]
10189    fn semantic_calendar_unit_display_returns_singular() {
10190        // Today Month => "month", Year => "year" (plural).
10191        // After singular_calendar_names_everywhere, must be "month" and "year".
10192        assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
10193        assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
10194    }
10195
10196    // ---------------------------------------------------------------------------
10197    // Phase 0 — signature_factor (to be implemented in signature_factor_and_display)
10198    // ---------------------------------------------------------------------------
10199
10200    #[test]
10201    fn signature_factor_with_calendar_units() {
10202        let calendar = test_calendar_type_for_signature_factor();
10203        let unit_index = crate::planning::unit_index::UnitIndex::new();
10204        // month factor = 1, year factor = 12.
10205        // [(month,1),(year,-1)] = 1/12
10206        let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
10207        let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
10208            .expect("must not overflow");
10209        let expected = rational_new(1, 12);
10210        assert_eq!(factor, expected, "month/year factor must be 1/12");
10211    }
10212
10213    fn test_calendar_type_for_signature_factor() -> LemmaType {
10214        use crate::computation::rational::{decimal_to_rational, rational_one};
10215        use crate::literals::{MeasureUnit, MeasureUnits};
10216        use rust_decimal::Decimal;
10217        LemmaType::new(
10218            "calendar".to_string(),
10219            TypeSpecification::Measure {
10220                minimum: None,
10221                maximum: None,
10222                units: MeasureUnits::from(vec![
10223                    MeasureUnit {
10224                        name: "month".to_string(),
10225                        scale: UnitScale::Literal(rational_one()),
10226                        minimum: None,
10227                        maximum: None,
10228                        suggestion_magnitude: None,
10229                        decomposition: calendar_decomposition(),
10230                        derived_measure_factors: Vec::new(),
10231                    },
10232                    MeasureUnit {
10233                        name: "year".to_string(),
10234                        scale: UnitScale::Literal(
10235                            decimal_to_rational(Decimal::from(12)).expect("year factor"),
10236                        ),
10237                        minimum: None,
10238                        maximum: None,
10239                        suggestion_magnitude: None,
10240                        decomposition: calendar_decomposition(),
10241                        derived_measure_factors: Vec::new(),
10242                    },
10243                ]),
10244                traits: vec![MeasureTrait::Calendar],
10245                decomposition: Some(calendar_decomposition()),
10246                unit: None,
10247                help: String::new(),
10248            },
10249            TypeExtends::Primitive,
10250        )
10251    }
10252
10253    #[test]
10254    #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
10255    fn signature_factor_panics_on_unresolved_name() {
10256        let unit_index = crate::planning::unit_index::UnitIndex::new();
10257        let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
10258        signature_factor(&bad_sig, &unit_index, None)
10259            .expect("BUG: signature_factor must panic before returning a failure");
10260    }
10261
10262    #[test]
10263    fn signature_factor_uses_owner_when_expression_index_empty() {
10264        let money = test_money_type_for_signature_factor();
10265        let expression_units = crate::planning::unit_index::UnitIndex::new();
10266        let sig_usd = sig(&[("usd", 1)]);
10267        let factor =
10268            signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
10269        assert_eq!(factor, rational_new(91, 100));
10270    }
10271
10272    fn test_money_type_for_signature_factor() -> LemmaType {
10273        use crate::computation::rational::decimal_to_rational;
10274        use crate::literals::{MeasureUnit, MeasureUnits};
10275        use rust_decimal::Decimal;
10276        LemmaType::new(
10277            "money".to_string(),
10278            TypeSpecification::Measure {
10279                minimum: None,
10280                maximum: None,
10281                units: MeasureUnits::from(vec![
10282                    MeasureUnit {
10283                        name: "eur".to_string(),
10284                        scale: UnitScale::Literal(crate::computation::rational::rational_one()),
10285                        minimum: None,
10286                        maximum: None,
10287                        suggestion_magnitude: None,
10288                        decomposition: BaseMeasureVector::new(),
10289                        derived_measure_factors: Vec::new(),
10290                    },
10291                    MeasureUnit {
10292                        name: "usd".to_string(),
10293                        scale: UnitScale::Literal(
10294                            decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
10295                        ),
10296                        minimum: None,
10297                        maximum: None,
10298                        suggestion_magnitude: None,
10299                        decomposition: BaseMeasureVector::new(),
10300                        derived_measure_factors: Vec::new(),
10301                    },
10302                ]),
10303                traits: Vec::new(),
10304                decomposition: None,
10305                unit: None,
10306                help: String::new(),
10307            },
10308            TypeExtends::Primitive,
10309        )
10310    }
10311
10312    fn measure_type_with_kilogram() -> TypeSpecification {
10313        use crate::computation::rational::rational_one;
10314        use crate::literals::{MeasureUnit, MeasureUnits};
10315        let mut units = MeasureUnits::new();
10316        units.push(MeasureUnit {
10317            name: "kilogram".to_string(),
10318            scale: UnitScale::Literal(rational_one()),
10319            minimum: None,
10320            maximum: None,
10321            suggestion_magnitude: None,
10322            decomposition: BaseMeasureVector::new(),
10323            derived_measure_factors: Vec::new(),
10324        });
10325        TypeSpecification::Measure {
10326            minimum: None,
10327            maximum: None,
10328            units,
10329            traits: Vec::new(),
10330            decomposition: None,
10331            unit: None,
10332            help: String::new(),
10333        }
10334    }
10335
10336    #[test]
10337    fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
10338        let ten = Value::Number(Decimal::from(10));
10339        let measure = LemmaType::primitive(measure_type_with_kilogram());
10340        let err = parser_value_to_value_kind(&ten, &measure, None)
10341            .expect_err("bare number must not bind to measure");
10342        assert!(
10343            err.contains("kilogram"),
10344            "error must hint expected unit, got: {err}"
10345        );
10346    }
10347
10348    #[test]
10349    fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
10350        let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
10351        let measure = LemmaType::primitive(measure_type_with_kilogram());
10352        let kind = parser_value_to_value_kind(&ten_kg, &measure, None)
10353            .expect("10 kilogram must bind to measure");
10354        assert!(matches!(kind, ValueKind::Measure(_)));
10355    }
10356
10357    #[test]
10358    fn parser_value_to_value_kind_rejects_bare_number_for_ratio() {
10359        let ten = Value::Number(Decimal::from(10));
10360        let ratio = LemmaType::primitive(TypeSpecification::ratio());
10361        let err = parser_value_to_value_kind(&ten, &ratio, None)
10362            .expect_err("bare number must not bind to ratio");
10363        assert!(
10364            err.contains("percent"),
10365            "error must hint expected unit, got: {err}"
10366        );
10367    }
10368
10369    #[test]
10370    fn value_kind_matches_spec_rejects_number_for_measure() {
10371        let n = ValueKind::Number(rational_new(10, 1));
10372        assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
10373    }
10374
10375    #[test]
10376    fn value_kind_matches_spec_rejects_number_for_ratio() {
10377        let n = ValueKind::Number(rational_new(1, 2));
10378        assert!(!value_kind_matches_spec(&n, &TypeSpecification::ratio()));
10379    }
10380
10381    #[test]
10382    fn apply_constraint_rejects_inherited_unit_factor_change() {
10383        let mut specs = TypeSpecification::measure();
10384        specs
10385            .apply_constraint(
10386                ConstraintNames {
10387                    type_name: "money",
10388                    data_name: "money",
10389                },
10390                TypeConstraintCommand::Unit,
10391                &unit_factor_arg("eur", 1),
10392                &mut None,
10393                &mut None,
10394                None,
10395            )
10396            .expect("seed eur");
10397        let err = specs
10398            .apply_constraint(
10399                ConstraintNames {
10400                    type_name: "money",
10401                    data_name: "money",
10402                },
10403                TypeConstraintCommand::Unit,
10404                &[
10405                    CommandArg::Label("eur".to_string()),
10406                    CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
10407                ],
10408                &mut None,
10409                &mut None,
10410                None,
10411            )
10412            .expect_err("must not change inherited unit factor");
10413        assert!(err.contains("eur"), "error must name unit, got: {err}");
10414        assert!(
10415            err.contains("inherited") || err.contains("cannot change"),
10416            "error must reject factor change, got: {err}"
10417        );
10418    }
10419
10420    #[test]
10421    fn apply_constraint_allows_additive_unit_on_inherited_spec() {
10422        let mut specs = TypeSpecification::measure();
10423        specs
10424            .apply_constraint(
10425                ConstraintNames {
10426                    type_name: "money",
10427                    data_name: "money",
10428                },
10429                TypeConstraintCommand::Unit,
10430                &unit_factor_arg("eur", 1),
10431                &mut None,
10432                &mut None,
10433                None,
10434            )
10435            .expect("seed eur");
10436        specs
10437            .apply_constraint(
10438                ConstraintNames {
10439                    type_name: "money",
10440                    data_name: "money",
10441                },
10442                TypeConstraintCommand::Unit,
10443                &unit_factor_arg("usd", 1),
10444                &mut None,
10445                &mut None,
10446                None,
10447            )
10448            .expect("add usd");
10449        match &specs {
10450            TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
10451            other => panic!("expected Measure, got {other:?}"),
10452        }
10453    }
10454
10455    #[test]
10456    fn apply_constraint_idempotent_inherited_unit_redeclare() {
10457        let mut specs = TypeSpecification::measure();
10458        specs
10459            .apply_constraint(
10460                ConstraintNames {
10461                    type_name: "money",
10462                    data_name: "money",
10463                },
10464                TypeConstraintCommand::Unit,
10465                &unit_factor_arg("eur", 1),
10466                &mut None,
10467                &mut None,
10468                None,
10469            )
10470            .expect("seed eur");
10471        specs
10472            .apply_constraint(
10473                ConstraintNames {
10474                    type_name: "money",
10475                    data_name: "money",
10476                },
10477                TypeConstraintCommand::Unit,
10478                &unit_factor_arg("eur", 1),
10479                &mut None,
10480                &mut None,
10481                None,
10482            )
10483            .expect("idempotent eur");
10484        match &specs {
10485            TypeSpecification::Measure { units, .. } => {
10486                assert_eq!(units.len(), 1);
10487                assert_eq!(
10488                    *units
10489                        .iter()
10490                        .find(|u| u.name == "eur")
10491                        .expect("eur")
10492                        .require_literal_factor(),
10493                    crate::computation::rational::rational_one()
10494                );
10495            }
10496            other => panic!("expected Measure, got {other:?}"),
10497        }
10498    }
10499
10500    #[test]
10501    fn element_from_range_returns_element_for_every_range_primitive() {
10502        type RangeElementMatcher = fn(&TypeSpecification) -> bool;
10503        let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
10504            (PrimitiveKind::NumberRange, |element| {
10505                matches!(element, TypeSpecification::Number { .. })
10506            }),
10507            (PrimitiveKind::MeasureRange, |element| {
10508                matches!(element, TypeSpecification::Measure { .. })
10509            }),
10510            (PrimitiveKind::RatioRange, |element| {
10511                matches!(element, TypeSpecification::Ratio { .. })
10512            }),
10513            (PrimitiveKind::DateRange, |element| {
10514                matches!(element, TypeSpecification::Date { .. })
10515            }),
10516            (PrimitiveKind::TimeRange, |element| {
10517                matches!(element, TypeSpecification::Time { .. })
10518            }),
10519        ];
10520        for (kind, matches_element) in cases {
10521            let range_spec = type_spec_for_primitive(kind);
10522            let element = range_spec
10523                .element_from_range()
10524                .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
10525            assert!(
10526                matches_element(&element),
10527                "{kind:?} element must match documented mapping, got {element:?}"
10528            );
10529        }
10530    }
10531
10532    #[test]
10533    fn element_from_range_returns_none_for_non_range_primitives() {
10534        let non_range = [
10535            type_spec_for_primitive(PrimitiveKind::Boolean),
10536            type_spec_for_primitive(PrimitiveKind::Measure),
10537            TypeSpecification::Undetermined,
10538            TypeSpecification::veto(),
10539        ];
10540        for spec in non_range {
10541            assert!(
10542                spec.element_from_range().is_none(),
10543                "{spec:?} must not define element_from_range"
10544            );
10545        }
10546    }
10547
10548    fn spec_type(name: &str, effective: crate::parsing::ast::EffectiveDate) -> LemmaType {
10549        LemmaType::primitive(TypeSpecification::Spec {
10550            spec: SpecIdentity {
10551                repository: None,
10552                spec: name.to_string(),
10553                effective: effective.clone(),
10554                slice: effective,
10555            },
10556        })
10557    }
10558
10559    #[test]
10560    fn same_value_type_spec_identity() {
10561        use crate::parsing::ast::EffectiveDate;
10562        let origin = EffectiveDate::Origin;
10563        let dated = EffectiveDate::DateTimeValue(crate::DateTimeValue {
10564            year: 2026,
10565            month: 1,
10566            day: 1,
10567            hour: 0,
10568            minute: 0,
10569            second: 0,
10570            microsecond: 0,
10571            timezone: None,
10572            granularity: crate::literals::DateGranularity::Full,
10573        });
10574        let tax = spec_type("tax_bracket", origin.clone());
10575        let tax_again = spec_type("tax_bracket", origin.clone());
10576        let other = spec_type("menu_item", origin);
10577        let tax_dated = spec_type("tax_bracket", dated.clone());
10578        assert!(tax.same_value_type(&tax_again));
10579        assert!(!tax.same_value_type(&other));
10580        assert!(!tax.same_value_type(&tax_dated));
10581        let mut tax_later_slice = tax.clone();
10582        match &mut tax_later_slice.specifications {
10583            TypeSpecification::Spec { spec } => spec.slice = dated,
10584            other => panic!("expected Spec, got {other:?}"),
10585        }
10586        assert!(!tax.same_value_type(&tax_later_slice));
10587        let mut other_repo = tax.clone();
10588        match &mut other_repo.specifications {
10589            TypeSpecification::Spec { spec } => spec.repository = Some("@jack/finance".to_string()),
10590            other => panic!("expected Spec, got {other:?}"),
10591        }
10592        assert!(!tax.same_value_type(&other_repo));
10593    }
10594
10595    #[test]
10596    fn spec_identity_displays_repository_space_spec_space_effective() {
10597        use crate::parsing::ast::EffectiveDate;
10598        let workspace = SpecIdentity {
10599            repository: None,
10600            spec: "tax_bracket".to_string(),
10601            effective: EffectiveDate::Origin,
10602            slice: EffectiveDate::Origin,
10603        };
10604        assert_eq!(workspace.to_string(), "tax_bracket");
10605        let start =
10606            EffectiveDate::DateTimeValue(DateTimeValue::from_str("2026-01-01").expect("date"));
10607        let dated = SpecIdentity {
10608            repository: Some("@jack/finance".to_string()),
10609            spec: "tax_bracket".to_string(),
10610            effective: start.clone(),
10611            slice: start,
10612        };
10613        assert_eq!(
10614            dated.to_string(),
10615            format!(
10616                "@jack/finance tax_bracket {}",
10617                DateTimeValue::from_str("2026-01-01").expect("date")
10618            )
10619        );
10620        let later_slice = SpecIdentity {
10621            slice: EffectiveDate::DateTimeValue(
10622                DateTimeValue::from_str("2026-07-01").expect("date"),
10623            ),
10624            ..workspace
10625        };
10626        assert_eq!(
10627            later_slice.to_string(),
10628            format!(
10629                "tax_bracket slice {}",
10630                DateTimeValue::from_str("2026-07-01").expect("date")
10631            )
10632        );
10633    }
10634
10635    #[test]
10636    fn type_family_displays_declaring_spec_then_type() {
10637        let workspace = TypeFamily {
10638            repository: None,
10639            spec: "pricing".to_string(),
10640            effective: crate::parsing::ast::EffectiveDate::Origin,
10641            name: "money".to_string(),
10642        };
10643        assert_eq!(workspace.to_string(), "pricing.money");
10644        let stdlib = TypeFamily {
10645            repository: Some("lemma".to_string()),
10646            spec: "units".to_string(),
10647            effective: crate::parsing::ast::EffectiveDate::Origin,
10648            name: "mass".to_string(),
10649        };
10650        assert_eq!(stdlib.to_string(), "lemma units.mass");
10651        let dated = TypeFamily {
10652            repository: None,
10653            spec: "pricing".to_string(),
10654            effective: crate::parsing::ast::EffectiveDate::DateTimeValue(
10655                DateTimeValue::from_str("2024-01-01").expect("date"),
10656            ),
10657            name: "money".to_string(),
10658        };
10659        assert_eq!(dated.to_string(), "pricing 2024-01-01.money");
10660    }
10661
10662    #[test]
10663    fn piecewise_type_keeps_spec_identity() {
10664        use crate::parsing::ast::EffectiveDate;
10665        use crate::planning::typing::{piecewise_type, MeasureScope};
10666        use crate::planning::unit_index::UnitIndex;
10667        let tax = Arc::new(spec_type("tax_bracket", EffectiveDate::Origin));
10668        let unit_index = UnitIndex::new();
10669        let signature_index = crate::computation::arithmetic::SignatureIndex::new();
10670        let named_types = IndexMap::new();
10671        let scope = MeasureScope {
10672            unit_index: &unit_index,
10673            signature_index: &signature_index,
10674            named_types: &named_types,
10675        };
10676        let picked = piecewise_type([Arc::new(LemmaType::veto_type()), Arc::clone(&tax)], &scope);
10677        assert!(picked.same_value_type(&tax));
10678    }
10679
10680    #[test]
10681    #[should_panic(expected = "BUG: literal factor required")]
10682    fn measure_unit_factor_panics_on_reference_scale() {
10683        let units = crate::literals::MeasureUnits::from(vec![
10684            crate::literals::MeasureUnit::from_reference(
10685                "gbp".to_string(),
10686                "rate".to_string(),
10687                Vec::new(),
10688                "rate".to_string(),
10689            ),
10690        ]);
10691        let lemma_type = LemmaType::new(
10692            "money".to_string(),
10693            TypeSpecification::Measure {
10694                units,
10695                traits: vec![],
10696                decomposition: None,
10697                minimum: None,
10698                maximum: None,
10699                unit: None,
10700                help: String::new(),
10701            },
10702            TypeExtends::Primitive,
10703        );
10704        let _ = lemma_type.measure_unit_factor("gbp");
10705    }
10706}