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