Skip to main content

lemma/planning/
semantics.rs

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