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