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