Skip to main content

lemma/planning/
semantics.rs

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