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;
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    /// Return the conversion factor for a declared unit name on this measure type.
3996    pub fn measure_unit_factor(
3997        &self,
3998        unit_name: &str,
3999    ) -> &crate::computation::rational::RationalInteger {
4000        let units = match &self.specifications {
4001            TypeSpecification::Measure { units, .. } => units,
4002            TypeSpecification::MeasureRange { units, .. } => units,
4003            _ => unreachable!(
4004                "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
4005                self.name()
4006            ),
4007        };
4008        match units.get(unit_name) {
4009            Ok(MeasureUnit { factor, .. }) => factor,
4010            Err(_) => {
4011                let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
4012                unreachable!(
4013                    "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
4014                    unit_name,
4015                    self.name(),
4016                    valid.join(", ")
4017                );
4018            }
4019        }
4020    }
4021
4022    pub fn ratio_unit_factor(
4023        &self,
4024        unit_name: &str,
4025    ) -> &crate::computation::rational::RationalInteger {
4026        let units = match &self.specifications {
4027            TypeSpecification::Ratio { units, .. } => units,
4028            _ => unreachable!(
4029                "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
4030                self.name()
4031            ),
4032        };
4033        match units.get(unit_name) {
4034            Ok(RatioUnit { value, .. }) => value,
4035            Err(_) => {
4036                let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
4037                unreachable!(
4038                    "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
4039                    unit_name,
4040                    self.name(),
4041                    valid.join(", ")
4042                );
4043            }
4044        }
4045    }
4046
4047    /// Convert a measure literal to decimal strings in every declared unit (same path as rule-result `measure` maps).
4048    pub(crate) fn measure_literal_in_all_units(
4049        &self,
4050        literal: &LiteralValue,
4051    ) -> Result<BTreeMap<String, String>, LiteralUnitMapFailure> {
4052        use crate::computation::rational::checked_div;
4053
4054        let unit_names = self
4055            .measure_unit_names()
4056            .expect("BUG: measure literal in all units requires declared units");
4057        let ValueKind::Measure(magnitude, _signature) = &literal.value else {
4058            panic!("BUG: measure_literal_in_all_units called with non-measure value");
4059        };
4060        let mut map = BTreeMap::new();
4061        for unit_name in unit_names {
4062            let unit_factor = self.measure_unit_factor(unit_name);
4063            let magnitude_in_unit = checked_div(magnitude, unit_factor)
4064                .map_err(LiteralUnitMapFailure::UnitConversion)?;
4065            let decimal_string = self
4066                .try_rational_as_decimal_string(&magnitude_in_unit)
4067                .map_err(LiteralUnitMapFailure::Commit)?;
4068            map.insert(unit_name.to_string(), decimal_string);
4069        }
4070        Ok(map)
4071    }
4072
4073    /// Convert a ratio literal to decimal strings in every declared unit (same path as rule-result `ratio` maps).
4074    pub(crate) fn ratio_literal_in_all_units(
4075        &self,
4076        literal: &LiteralValue,
4077    ) -> Result<BTreeMap<String, String>, LiteralUnitMapFailure> {
4078        use crate::computation::rational::checked_mul;
4079
4080        let ratio_api_type = match &self.specifications {
4081            TypeSpecification::Ratio { .. } => self,
4082            TypeSpecification::RatioRange { .. } => {
4083                let element = range_element_type_specification(&self.specifications)
4084                    .expect("BUG: ratio range type must have ratio element specification");
4085                let TypeSpecification::Ratio {
4086                    units, decimals, ..
4087                } = element
4088                else {
4089                    panic!("BUG: ratio range element spec must be Ratio");
4090                };
4091                return LemmaType::primitive(TypeSpecification::Ratio {
4092                    minimum: None,
4093                    maximum: None,
4094                    decimals,
4095                    units,
4096                    help: String::new(),
4097                })
4098                .ratio_literal_in_all_units(literal);
4099            }
4100            _ => {
4101                panic!(
4102                    "BUG: ratio_literal_in_all_units called with non-ratio type {}",
4103                    self.name()
4104                );
4105            }
4106        };
4107        let units = match &ratio_api_type.specifications {
4108            TypeSpecification::Ratio { units, .. } => units,
4109            _ => unreachable!("BUG: ratio API type must be Ratio"),
4110        };
4111        let ValueKind::Ratio(canonical, _) = &literal.value else {
4112            panic!("BUG: ratio_literal_in_all_units called with non-ratio value");
4113        };
4114        if units.is_empty() {
4115            panic!(
4116                "BUG: ratio literal type '{}' must have declared units",
4117                self.name()
4118            );
4119        }
4120        let mut map = BTreeMap::new();
4121        for unit in units.iter() {
4122            let magnitude_in_unit = checked_mul(canonical, &unit.value)
4123                .map_err(LiteralUnitMapFailure::UnitConversion)?;
4124            let decimal_string = ratio_api_type
4125                .try_rational_as_decimal_string(&magnitude_in_unit)
4126                .map_err(LiteralUnitMapFailure::Commit)?;
4127            map.insert(unit.name.clone(), decimal_string);
4128        }
4129        Ok(map)
4130    }
4131}
4132
4133/// Failure while converting a literal to decimal strings across declared units.
4134#[derive(Debug, Clone, PartialEq, Eq)]
4135pub(crate) enum LiteralUnitMapFailure {
4136    Commit(crate::computation::rational::NumericFailure),
4137    UnitConversion(crate::computation::rational::NumericFailure),
4138}
4139
4140/// Literal value with type. The single value type in semantics.
4141#[derive(Clone, Debug, PartialEq, Eq, Hash)]
4142pub struct LiteralValue {
4143    pub value: ValueKind,
4144    pub lemma_type: Arc<LemmaType>,
4145}
4146
4147impl LiteralValue {
4148    fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
4149        match signature {
4150            [(unit_name, 1)] => Some(unit_name.as_str()),
4151            _ => None,
4152        }
4153    }
4154}
4155
4156impl Serialize for LiteralValue {
4157    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
4158    where
4159        S: serde::Serializer,
4160    {
4161        use serde::ser::SerializeStruct;
4162        let mut state = serializer.serialize_struct("LiteralValue", 3)?;
4163        state.serialize_field("value", &self.value)?;
4164        state.serialize_field("lemma_type", self.lemma_type.as_ref())?;
4165        state.serialize_field("display_value", &self.display_value())?;
4166        state.end()
4167    }
4168}
4169
4170impl<'de> Deserialize<'de> for LiteralValue {
4171    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4172    where
4173        D: serde::Deserializer<'de>,
4174    {
4175        #[derive(Deserialize)]
4176        struct Raw {
4177            value: ValueKind,
4178            lemma_type: LemmaType,
4179        }
4180        let raw = Raw::deserialize(deserializer)?;
4181        Ok(Self {
4182            value: raw.value,
4183            lemma_type: Arc::new(raw.lemma_type),
4184        })
4185    }
4186}
4187
4188impl LiteralValue {
4189    pub fn text(s: String) -> Self {
4190        Self {
4191            value: ValueKind::Text(s),
4192            lemma_type: primitive_text_arc().clone(),
4193        }
4194    }
4195
4196    pub fn text_with_type(s: String, lemma_type: Arc<LemmaType>) -> Self {
4197        Self {
4198            value: ValueKind::Text(s),
4199            lemma_type,
4200        }
4201    }
4202
4203    pub fn number(n: RationalInteger) -> Self {
4204        Self {
4205            value: ValueKind::Number(n),
4206            lemma_type: primitive_number_arc().clone(),
4207        }
4208    }
4209
4210    pub fn number_from_decimal(decimal: Decimal) -> Self {
4211        Self::number(
4212            crate::literals::rational_from_parsed_decimal(decimal)
4213                .expect("BUG: literal number from decimal must lift at boundary"),
4214        )
4215    }
4216
4217    pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
4218        Self {
4219            value: ValueKind::Number(n),
4220            lemma_type,
4221        }
4222    }
4223
4224    pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
4225        Self::number_with_type(
4226            crate::literals::rational_from_parsed_decimal(decimal)
4227                .expect("BUG: literal number from decimal must lift at boundary"),
4228            lemma_type,
4229        )
4230    }
4231
4232    /// Build a Measure literal carrying a single user-typed unit name.
4233    /// The signature is `[(unit_name, 1)]`; the normalize pass expands compound names
4234    /// against `unit_index` so all stored signatures end up in canonical (base-unit) form.
4235    pub fn measure_with_type(n: RationalInteger, unit: String, lemma_type: Arc<LemmaType>) -> Self {
4236        Self {
4237            value: ValueKind::Measure(n, vec![(unit, 1)]),
4238            lemma_type,
4239        }
4240    }
4241
4242    /// Build a Measure literal with an explicit signature (already in canonical form).
4243    /// Used by arithmetic when combining operand signatures yields a multi-term result.
4244    pub fn measure_with_signature(
4245        n: RationalInteger,
4246        signature: Vec<(String, i32)>,
4247        lemma_type: Arc<LemmaType>,
4248    ) -> Self {
4249        Self {
4250            value: ValueKind::Measure(n, signature),
4251            lemma_type,
4252        }
4253    }
4254
4255    /// Number interpreted as a measure value in the given unit (e.g. "3 as usd" where 3 is a number).
4256    /// Creates an anonymous one-unit measure type so computation does not depend on parsing types.
4257    pub fn number_interpreted_as_measure(value: RationalInteger, unit_name: String) -> Self {
4258        Self {
4259            value: ValueKind::Measure(value, vec![(unit_name, 1)]),
4260            lemma_type: Arc::new(anonymous_measure_type()),
4261        }
4262    }
4263
4264    pub fn from_bool(b: bool) -> Self {
4265        Self {
4266            value: ValueKind::Boolean(b),
4267            lemma_type: primitive_boolean_arc().clone(),
4268        }
4269    }
4270
4271    pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
4272        Self::date(date_time_to_semantic(dt))
4273    }
4274
4275    /// Magnitude string for decimal input prompts (number, single-unit measure, ratio with percent/permille scaling).
4276    #[must_use]
4277    pub fn magnitude_suggestion_for_decimal_prompt(&self) -> Option<String> {
4278        match &self.value {
4279            ValueKind::Number(n) => Some(
4280                self.lemma_type
4281                    .try_rational_as_decimal_string(n)
4282                    .expect("BUG: stored number literal must convert to decimal for prompt"),
4283            ),
4284            ValueKind::Measure(n, signature) => {
4285                let unit_name = Self::single_measure_signature_unit_name(signature).expect(
4286                    "BUG: measure prompt requires exactly one signature unit with exponent 1",
4287                );
4288                Some(
4289                    self.lemma_type
4290                        .try_measure_canonical_as_decimal_in_unit(n, unit_name)
4291                        .expect("BUG: stored measure literal must convert to decimal for prompt"),
4292                )
4293            }
4294            ValueKind::Ratio(n, Some(unit_name)) => Some(
4295                self.lemma_type
4296                    .try_ratio_canonical_as_decimal_in_unit(n, unit_name)
4297                    .expect("BUG: stored ratio literal must convert to decimal for prompt"),
4298            ),
4299            ValueKind::Ratio(n, None) => Some(
4300                self.lemma_type
4301                    .try_rational_as_decimal_string(n)
4302                    .expect("BUG: stored bare ratio literal must convert to decimal for prompt"),
4303            ),
4304            _ => None,
4305        }
4306    }
4307
4308    /// Per-unit magnitudes when this literal is a measure with declared units.
4309    #[must_use]
4310    pub fn measure_units(&self) -> Option<BTreeMap<String, String>> {
4311        if !matches!(self.value, ValueKind::Measure(_, _)) {
4312            return None;
4313        }
4314        self.lemma_type.measure_unit_names()?;
4315        self.lemma_type.measure_literal_in_all_units(self).ok()
4316    }
4317
4318    /// Per-unit magnitudes when this literal is a ratio with declared units.
4319    #[must_use]
4320    pub fn ratio_units(&self) -> Option<BTreeMap<String, String>> {
4321        if !matches!(self.value, ValueKind::Ratio(_, _)) {
4322            return None;
4323        }
4324        let has_declared_units = match &self.lemma_type.specifications {
4325            TypeSpecification::Ratio { units, .. } => !units.is_empty(),
4326            TypeSpecification::RatioRange { .. } => true,
4327            _ => return None,
4328        };
4329        if !has_declared_units {
4330            return None;
4331        }
4332        self.lemma_type.ratio_literal_in_all_units(self).ok()
4333    }
4334
4335    /// Magnitude in a declared unit when this literal is measure or ratio.
4336    #[must_use]
4337    pub fn magnitude_in_unit(&self, unit: &str) -> Option<String> {
4338        self.measure_units()
4339            .and_then(|map| map.get(unit).cloned())
4340            .or_else(|| self.ratio_units().and_then(|map| map.get(unit).cloned()))
4341    }
4342
4343    pub fn date(dt: SemanticDateTime) -> Self {
4344        Self {
4345            value: ValueKind::Date(dt),
4346            lemma_type: primitive_date_arc().clone(),
4347        }
4348    }
4349
4350    pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
4351        Self {
4352            value: ValueKind::Date(dt),
4353            lemma_type,
4354        }
4355    }
4356
4357    pub fn time(t: SemanticTime) -> Self {
4358        Self {
4359            value: ValueKind::Time(t),
4360            lemma_type: primitive_time_arc().clone(),
4361        }
4362    }
4363
4364    pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
4365        Self {
4366            value: ValueKind::Time(t),
4367            lemma_type,
4368        }
4369    }
4370
4371    pub fn calendar(
4372        value: RationalInteger,
4373        unit: SemanticCalendarUnit,
4374        lemma_type: Arc<LemmaType>,
4375    ) -> Self {
4376        Self::measure_with_type(value, unit.to_string(), lemma_type)
4377    }
4378
4379    pub fn calendar_from_decimal(
4380        value: Decimal,
4381        unit: SemanticCalendarUnit,
4382        lemma_type: Arc<LemmaType>,
4383    ) -> Self {
4384        Self::calendar(
4385            crate::literals::rational_from_parsed_decimal(value)
4386                .expect("BUG: calendar literal from decimal must lift at boundary"),
4387            unit,
4388            lemma_type,
4389        )
4390    }
4391
4392    pub fn calendar_with_type(
4393        value: RationalInteger,
4394        unit: SemanticCalendarUnit,
4395        lemma_type: Arc<LemmaType>,
4396    ) -> Self {
4397        Self::calendar(value, unit, lemma_type)
4398    }
4399
4400    /// Derive second from a duration measure's canonical magnitude.
4401    pub fn duration_canonical_seconds(&self) -> RationalInteger {
4402        let ValueKind::Measure(magnitude, _) = &self.value else {
4403            unreachable!(
4404                "BUG: duration_canonical_seconds called with {:?}",
4405                self.value
4406            );
4407        };
4408        if !self.lemma_type.is_duration_like_measure() {
4409            unreachable!(
4410                "BUG: duration_canonical_seconds called with type {}",
4411                self.lemma_type.name()
4412            );
4413        }
4414        let factor = self.lemma_type.measure_unit_factor("second");
4415        checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
4416    }
4417
4418    /// Derive month from a calendar measure's canonical magnitude.
4419    pub fn calendar_canonical_months(&self) -> RationalInteger {
4420        let ValueKind::Measure(magnitude, _) = &self.value else {
4421            unreachable!(
4422                "BUG: calendar_canonical_months called with {:?}",
4423                self.value
4424            );
4425        };
4426        if !self.lemma_type.is_calendar_like() {
4427            unreachable!(
4428                "BUG: calendar_canonical_months called with type {}",
4429                self.lemma_type.name()
4430            );
4431        }
4432        let factor = self.lemma_type.measure_unit_factor("month");
4433        checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
4434    }
4435
4436    pub fn ratio(r: RationalInteger, unit: Option<String>) -> Self {
4437        Self {
4438            value: ValueKind::Ratio(r, unit),
4439            lemma_type: primitive_ratio_arc().clone(),
4440        }
4441    }
4442
4443    pub fn ratio_from_decimal(r: Decimal, unit: Option<String>) -> Self {
4444        Self::ratio(
4445            crate::literals::rational_from_parsed_decimal(r)
4446                .expect("BUG: ratio literal from decimal must lift at boundary"),
4447            unit,
4448        )
4449    }
4450
4451    pub fn ratio_with_type(
4452        r: RationalInteger,
4453        unit: Option<String>,
4454        lemma_type: Arc<LemmaType>,
4455    ) -> Self {
4456        Self {
4457            value: ValueKind::Ratio(r, unit),
4458            lemma_type,
4459        }
4460    }
4461
4462    pub fn range(left: LiteralValue, right: LiteralValue) -> Self {
4463        let specifications =
4464            range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
4465                .unwrap_or_else(|| {
4466                    unreachable!(
4467                "BUG: attempted to construct a range literal from incompatible endpoint types"
4468            )
4469                });
4470
4471        Self {
4472            value: ValueKind::Range(Box::new(left), Box::new(right)),
4473            lemma_type: Arc::new(LemmaType::primitive(specifications)),
4474        }
4475    }
4476
4477    /// Get a display string for this value (for UI/output)
4478    pub fn display_value(&self) -> String {
4479        format!("{}", self)
4480    }
4481
4482    /// Approximate byte size for resource limit checks (string representation length)
4483    pub fn byte_size(&self) -> usize {
4484        format!("{}", self).len()
4485    }
4486
4487    /// Get the resolved type of this literal
4488    pub fn get_type(&self) -> &LemmaType {
4489        &self.lemma_type
4490    }
4491}
4492
4493/// What a [`DataDefinition::Reference`] copies its value from: either another data path
4494/// or a rule whose result becomes this data's value.
4495#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
4496#[serde(rename_all = "snake_case", tag = "kind")]
4497pub enum ReferenceTarget {
4498    Data(DataPath),
4499    Rule(RulePath),
4500}
4501
4502/// Resolved data value for the execution plan: aligned with [`DataValue`] but with source per variant.
4503#[derive(Clone, Debug, Serialize, Deserialize)]
4504#[serde(rename_all = "snake_case")]
4505pub enum DataDefinition {
4506    /// Value-holding data: current literal (spec or `with` binding); type is on the value.
4507    Value { value: LiteralValue, source: Source },
4508    /// Type-only data: type known, value to be supplied (e.g. via run data).
4509    /// `declared_suggestion` carries the `-> suggest ...` payload for this binding or
4510    /// the suggestion inherited from the parent type chain, if any; value-promoting code
4511    /// uses it instead of re-deriving suggestions from [`TypeSpecification`].
4512    /// The evaluator never commits a suggestion — unbound stays MissingData.
4513    TypeDeclaration {
4514        resolved_type: Arc<LemmaType>,
4515        declared_suggestion: Option<ValueKind>,
4516        source: Source,
4517    },
4518    /// Import (`uses`): alias for another spec; nested members are flattened onto the plan.
4519    Import { target_name: String, source: Source },
4520    /// Value-copy reference to another data or a rule result.
4521    ///
4522    /// `resolved_type` is the merged type that the copied value must satisfy at
4523    /// evaluation time. Merging folds together: (1) the LHS's own declared type,
4524    /// if any; (2) the target's type (data declared type or rule return type);
4525    /// (3) any `local_constraints` written after the `->` on the reference itself.
4526    /// Merging happens in a dedicated pass once all data and rule types are
4527    /// known; before that pass, `resolved_type` holds a provisional value and
4528    /// must not be consumed for type checking.
4529    ///
4530    /// `local_constraints` preserves the raw constraint list from the reference's
4531    /// `-> ...` tail (e.g. `minimum 5` in `data license2: law.other -> minimum 5`)
4532    /// for that merging pass. It is `None` when the reference has no trailing
4533    /// constraints.
4534    ///
4535    /// `local_suggestion` carries any `suggest <value>` constraint from the
4536    /// reference's `-> ...` tail. The reference-merge pass extracts it from the
4537    /// constraint list during type resolution. It is a UI hint only
4538    /// ([`Self::suggestion`] / show); the evaluator never commits it — unbound
4539    /// stays MissingData.
4540    ///
4541    /// The reference itself is evaluated by copying the target's value (data path)
4542    /// or the target rule's result in topological order; caller values in
4543    /// [`crate::evaluation::run_data::RunData`] override the reference.
4544    Reference {
4545        target: ReferenceTarget,
4546        resolved_type: Arc<LemmaType>,
4547        local_constraints: Option<Vec<Constraint>>,
4548        local_suggestion: Option<ValueKind>,
4549        source: Source,
4550    },
4551}
4552
4553impl DataDefinition {
4554    /// Declared lemma type for value, type-declaration, and reference data; `None` for imports.
4555    pub fn lemma_type(&self) -> Option<&LemmaType> {
4556        match self {
4557            DataDefinition::Value { value, .. } => Some(value.lemma_type.as_ref()),
4558            DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type.as_ref()),
4559            DataDefinition::Reference { resolved_type, .. } => Some(resolved_type.as_ref()),
4560            DataDefinition::Import { .. } => None,
4561        }
4562    }
4563
4564    /// Alias for [`Self::lemma_type`] (historical name).
4565    #[inline]
4566    pub fn schema_type(&self) -> Option<&LemmaType> {
4567        self.lemma_type()
4568    }
4569
4570    /// Returns the literal value when the data already holds one. A `Reference`'s
4571    /// value is produced by the evaluator at runtime, so at plan-time it has no
4572    /// value yet.
4573    pub fn value(&self) -> Option<&LiteralValue> {
4574        match self {
4575            DataDefinition::Value { value, .. } => Some(value),
4576            DataDefinition::TypeDeclaration { .. }
4577            | DataDefinition::Import { .. }
4578            | DataDefinition::Reference { .. } => None,
4579        }
4580    }
4581
4582    /// Spec literal (`data x: literal`) or literal `with` binding at plan time.
4583    /// Not a suggestion; see [`Self::suggestion`].
4584    #[inline]
4585    pub fn prefilled_value(&self) -> Option<&LiteralValue> {
4586        self.value()
4587    }
4588
4589    /// Suggestion from `-> suggest ...` on a type declaration or reference.
4590    /// Surfaces in [`crate::planning::execution_plan::ShowData::suggestion`] for
4591    /// show/response/UI; the evaluator never commits it — unbound stays MissingData.
4592    pub fn suggestion(&self) -> Option<LiteralValue> {
4593        match self {
4594            DataDefinition::TypeDeclaration {
4595                resolved_type,
4596                declared_suggestion: Some(dv),
4597                ..
4598            } => Some(LiteralValue {
4599                value: dv.clone(),
4600                lemma_type: Arc::clone(resolved_type),
4601            }),
4602            DataDefinition::Reference {
4603                resolved_type,
4604                local_suggestion: Some(dv),
4605                ..
4606            } => Some(LiteralValue {
4607                value: dv.clone(),
4608                lemma_type: Arc::clone(resolved_type),
4609            }),
4610            DataDefinition::Value { .. }
4611            | DataDefinition::TypeDeclaration {
4612                declared_suggestion: None,
4613                ..
4614            }
4615            | DataDefinition::Reference {
4616                local_suggestion: None,
4617                ..
4618            }
4619            | DataDefinition::Import { .. } => None,
4620        }
4621    }
4622
4623    /// Returns the source location for this data.
4624    pub fn source(&self) -> &Source {
4625        match self {
4626            DataDefinition::Value { source, .. } => source,
4627            DataDefinition::TypeDeclaration { source, .. } => source,
4628            DataDefinition::Import { source, .. } => source,
4629            DataDefinition::Reference { source, .. } => source,
4630        }
4631    }
4632}
4633
4634/// Bind a type-agnostic [`Value::NumberWithUnit`] using the unit index entry for `unit_name`.
4635pub fn number_with_unit_to_value_kind(
4636    magnitude: rust_decimal::Decimal,
4637    unit_name: &str,
4638    lemma_type: &LemmaType,
4639) -> Result<ValueKind, String> {
4640    match &lemma_type.specifications {
4641        TypeSpecification::Ratio { units, .. } => {
4642            use crate::computation::rational::{checked_div, decimal_to_rational};
4643            let unit = units.get(unit_name)?;
4644            let magnitude_rational = decimal_to_rational(magnitude)
4645                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4646            let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4647                .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4648            Ok(ValueKind::Ratio(
4649                canonical_rational,
4650                Some(unit.name.clone()),
4651            ))
4652        }
4653        TypeSpecification::Measure { units, .. } => {
4654            use crate::computation::rational::checked_mul;
4655            let rational = lift_parser_decimal(magnitude)?;
4656            let unit = units.get(unit_name)?;
4657            let canonical = checked_mul(&rational, &unit.factor)
4658                .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
4659            Ok(ValueKind::Measure(
4660                canonical,
4661                vec![(unit_name.to_string(), 1)],
4662            ))
4663        }
4664        _ => Err(format!(
4665            "Unit '{}' is defined on type '{}' which is not measure or ratio",
4666            unit_name,
4667            lemma_type.name()
4668        )),
4669    }
4670}
4671
4672/// Whether a [`ValueKind`] is structurally compatible with a [`TypeSpecification`].
4673/// Bound validation (min/max/decimals) is separate; this only checks shape.
4674pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
4675    matches!(
4676        (type_spec, value),
4677        (TypeSpecification::Number { .. }, ValueKind::Number(_))
4678            | (TypeSpecification::Text { .. }, ValueKind::Text(_))
4679            | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
4680            | (TypeSpecification::Date { .. }, ValueKind::Date(_))
4681            | (TypeSpecification::Time { .. }, ValueKind::Time(_))
4682            | (TypeSpecification::Measure { .. }, ValueKind::Measure(_, _))
4683            | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_, _))
4684            | (TypeSpecification::Ratio { .. }, ValueKind::Number(_))
4685            | (
4686                TypeSpecification::NumberRange { .. },
4687                ValueKind::Range(_, _)
4688            )
4689            | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
4690            | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
4691            | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
4692            | (
4693                TypeSpecification::MeasureRange { .. },
4694                ValueKind::Range(_, _)
4695            )
4696            | (TypeSpecification::Veto { .. }, _)
4697            | (TypeSpecification::Undetermined, _)
4698    )
4699}
4700
4701fn parser_value_type_mismatch(
4702    value: &crate::literals::Value,
4703    type_spec: &TypeSpecification,
4704) -> String {
4705    use crate::parsing::ast::AsLemmaSource;
4706    let value_str = format!("{}", AsLemmaSource(value));
4707    match type_spec {
4708        TypeSpecification::Measure { units, .. } => {
4709            let unit_hint = units
4710                .iter()
4711                .find(|u| u.factor == crate::computation::rational::rational_one())
4712                .map(|u| u.name.as_str())
4713                .or_else(|| units.iter().next().map(|u| u.name.as_str()))
4714                .unwrap_or("unit");
4715            format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
4716        }
4717        TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
4718            let unit_hint = units
4719                .iter()
4720                .next()
4721                .map(|u| u.name.as_str())
4722                .unwrap_or("unit");
4723            format!(
4724                "cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}` or bare ratio"
4725            )
4726        }
4727        _ => format!("cannot use {value_str} as {type_spec}"),
4728    }
4729}
4730
4731/// Re-canonicalize a measure literal after compound unit factors were resolved.
4732///
4733/// Literals parsed before derived unit resolution were canonicalized with prefix-only
4734/// factors; multiply by `resolved_factor / stored_factor` to align with final factors.
4735pub fn refresh_measure_literal_canonical_magnitude(
4736    lit: &mut LiteralValue,
4737    resolved_type: &LemmaType,
4738) {
4739    let ValueKind::Measure(magnitude, signature) = &mut lit.value else {
4740        return;
4741    };
4742    let (unit_name, exponent) = signature
4743        .first()
4744        .expect("BUG: measure literal has empty signature during canonical magnitude refresh");
4745    if *exponent != 1 || signature.len() != 1 {
4746        return;
4747    }
4748    let stored_factor = lit.lemma_type.measure_unit_factor(unit_name);
4749    let resolved_factor = resolved_type.measure_unit_factor(unit_name);
4750    if stored_factor == resolved_factor {
4751        lit.lemma_type = Arc::new(resolved_type.clone());
4752        return;
4753    }
4754    let scaled = checked_mul(magnitude, resolved_factor)
4755        .expect("BUG: measure recanonicalization multiply overflow");
4756    *magnitude =
4757        checked_div(&scaled, stored_factor).expect("BUG: measure recanonicalization divide failed");
4758    lit.lemma_type = Arc::new(resolved_type.clone());
4759}
4760
4761/// Convert parser [`Value`] to [`ValueKind`] using the target type (canonicalizes ratio at bind).
4762pub fn parser_value_to_value_kind(
4763    value: &crate::literals::Value,
4764    type_spec: &TypeSpecification,
4765) -> Result<ValueKind, String> {
4766    use crate::computation::rational::decimal_to_rational;
4767    use crate::literals::Value;
4768    match (value, type_spec) {
4769        (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Ratio { units, .. }) => {
4770            use crate::computation::rational::checked_div;
4771            let unit = units.get(unit_name.as_str())?;
4772            let magnitude_rational = decimal_to_rational(*magnitude)
4773                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4774            let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4775                .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4776            Ok(ValueKind::Ratio(
4777                canonical_rational,
4778                Some(unit.name.clone()),
4779            ))
4780        }
4781        (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Measure { units, .. }) => {
4782            use crate::computation::rational::checked_mul;
4783            let rational = lift_parser_decimal(*magnitude)?;
4784            let unit = units.get(unit_name.as_str())?;
4785            let canonical = checked_mul(&rational, &unit.factor)
4786                .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
4787            Ok(ValueKind::Measure(canonical, vec![(unit_name.clone(), 1)]))
4788        }
4789        (Value::NumberWithUnit(_, _), _) => {
4790            Err("number_with_unit literal requires a measure or ratio type".to_string())
4791        }
4792        (Value::Number(n), TypeSpecification::Number { .. }) => {
4793            Ok(ValueKind::Number(lift_parser_decimal(*n)?))
4794        }
4795        (Value::Number(n), TypeSpecification::Ratio { .. }) => {
4796            let r = decimal_to_rational(*n)
4797                .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4798            Ok(ValueKind::Ratio(r, None))
4799        }
4800        (Value::Text(s), TypeSpecification::Text { .. }) => Ok(ValueKind::Text(s.clone())),
4801        (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
4802        (Value::Date(dt), TypeSpecification::Date { .. }) => {
4803            Ok(ValueKind::Date(date_time_to_semantic(dt)))
4804        }
4805        (Value::Time(t), TypeSpecification::Time { .. }) => {
4806            Ok(ValueKind::Time(time_to_semantic(t)))
4807        }
4808        (
4809            Value::Range(left, right),
4810            range_spec @ (TypeSpecification::NumberRange { .. }
4811            | TypeSpecification::DateRange { .. }
4812            | TypeSpecification::TimeRange { .. }
4813            | TypeSpecification::RatioRange { .. }
4814            | TypeSpecification::MeasureRange { .. }),
4815        ) => {
4816            let endpoint = range_element_type_specification(range_spec).ok_or_else(|| {
4817                "BUG: range_element_type_specification missing arm for range type".to_string()
4818            })?;
4819            let left_lit = lift_range_endpoint(left, &endpoint)?;
4820            let right_lit = lift_range_endpoint(right, &endpoint)?;
4821            Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
4822        }
4823        (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
4824    }
4825}
4826
4827/// Convert parser Value to ValueKind for primitives and ranges only.
4828///
4829/// [`Value::NumberWithUnit`] requires [`parser_value_to_value_kind`] with a measure or ratio type.
4830pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
4831    use crate::parsing::ast::Value;
4832    Ok(match value {
4833        Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
4834        Value::Text(s) => ValueKind::Text(s.clone()),
4835        Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
4836        Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
4837        Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
4838        Value::NumberWithUnit(_, _) => {
4839            return Err(
4840                "number_with_unit literal requires type context (measure or ratio)".to_string(),
4841            );
4842        }
4843        Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
4844    })
4845}
4846
4847/// Convert AST date-time to semantic (for tests and planning).
4848pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
4849    SemanticDateTime {
4850        year: dt.year,
4851        month: dt.month,
4852        day: dt.day,
4853        hour: dt.hour,
4854        minute: dt.minute,
4855        second: dt.second,
4856        microsecond: dt.microsecond,
4857        timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
4858            offset_hours: tz.offset_hours,
4859            offset_minutes: tz.offset_minutes,
4860        }),
4861    }
4862}
4863
4864/// Convert AST time to semantic (for tests and planning).
4865pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
4866    SemanticTime {
4867        hour: t.hour.into(),
4868        minute: t.minute.into(),
4869        second: t.second.into(),
4870        microsecond: t.microsecond,
4871        timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
4872            offset_hours: tz.offset_hours,
4873            offset_minutes: tz.offset_minutes,
4874        }),
4875    }
4876}
4877
4878/// Compare two semantic date-time values by year, month, day, hour, minute,
4879/// second, then microsecond. Timezone normalisation is a separate concern
4880/// handled at evaluation time.
4881pub(crate) fn compare_semantic_dates(
4882    left: &SemanticDateTime,
4883    right: &SemanticDateTime,
4884) -> std::cmp::Ordering {
4885    left.year
4886        .cmp(&right.year)
4887        .then_with(|| left.month.cmp(&right.month))
4888        .then_with(|| left.day.cmp(&right.day))
4889        .then_with(|| left.hour.cmp(&right.hour))
4890        .then_with(|| left.minute.cmp(&right.minute))
4891        .then_with(|| left.second.cmp(&right.second))
4892        .then_with(|| left.microsecond.cmp(&right.microsecond))
4893}
4894
4895/// Compare two semantic time values by hour, minute, second, then microsecond.
4896/// Timezone is excluded for the same reason as [`compare_semantic_dates`].
4897pub(crate) fn compare_semantic_times(
4898    left: &SemanticTime,
4899    right: &SemanticTime,
4900) -> std::cmp::Ordering {
4901    left.hour
4902        .cmp(&right.hour)
4903        .then_with(|| left.minute.cmp(&right.minute))
4904        .then_with(|| left.second.cmp(&right.second))
4905        .then_with(|| left.microsecond.cmp(&right.microsecond))
4906}
4907
4908/// Convert AST conversion target to semantic (planning boundary; evaluation/computation use only semantic).
4909pub fn conversion_target_to_semantic(
4910    ct: &ConversionTarget,
4911    unit_index: Option<&crate::planning::unit_index::UnitIndex>,
4912) -> Result<SemanticConversionTarget, String> {
4913    match ct {
4914        ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
4915        ConversionTarget::Unit { unit_name } => {
4916            let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
4917            let (bare, owning_type) = index.resolve(unit_name)?;
4918            Ok(SemanticConversionTarget::Unit {
4919                unit_name: bare,
4920                owning_type,
4921            })
4922        }
4923    }
4924}
4925
4926// -----------------------------------------------------------------------------
4927// Primitive type constructors (moved from parsing::ast)
4928// -----------------------------------------------------------------------------
4929
4930// Statics for lazy initialization of production-used primitive types.
4931static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
4932static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
4933static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
4934static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4935static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4936static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
4937static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
4938
4939#[must_use]
4940pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
4941    PRIMITIVE_BOOLEAN.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::boolean())))
4942}
4943
4944#[must_use]
4945pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
4946    PRIMITIVE_NUMBER.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::number())))
4947}
4948
4949#[must_use]
4950pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
4951    PRIMITIVE_TEXT.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::text())))
4952}
4953
4954#[must_use]
4955pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
4956    PRIMITIVE_DATE.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date())))
4957}
4958
4959#[must_use]
4960pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
4961    PRIMITIVE_DATE_RANGE
4962        .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date_range())))
4963}
4964
4965#[must_use]
4966pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
4967    PRIMITIVE_TIME.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::time())))
4968}
4969
4970#[must_use]
4971pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
4972    PRIMITIVE_RATIO.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::ratio())))
4973}
4974
4975/// Map PrimitiveKind to TypeSpecification. Single source of truth for primitive type resolution.
4976#[must_use]
4977pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
4978    match kind {
4979        PrimitiveKind::Boolean => TypeSpecification::boolean(),
4980        PrimitiveKind::Measure => TypeSpecification::measure(),
4981        PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
4982        PrimitiveKind::Number => TypeSpecification::number(),
4983        PrimitiveKind::NumberRange => TypeSpecification::number_range(),
4984        PrimitiveKind::Ratio => TypeSpecification::ratio(),
4985        PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
4986        PrimitiveKind::Text => TypeSpecification::text(),
4987        PrimitiveKind::Date => TypeSpecification::date(),
4988        PrimitiveKind::DateRange => TypeSpecification::date_range(),
4989        PrimitiveKind::Time => TypeSpecification::time(),
4990        PrimitiveKind::TimeRange => TypeSpecification::time_range(),
4991    }
4992}
4993
4994// -----------------------------------------------------------------------------
4995// Display implementations
4996// -----------------------------------------------------------------------------
4997
4998impl fmt::Display for PathSegment {
4999    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5000        write!(f, "{} → {}", self.data, self.spec)
5001    }
5002}
5003
5004impl fmt::Display for DataPath {
5005    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5006        for segment in &self.segments {
5007            write!(f, "{}.", segment)?;
5008        }
5009        write!(f, "{}", self.data)
5010    }
5011}
5012
5013impl fmt::Display for RulePath {
5014    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5015        for segment in &self.segments {
5016            write!(f, "{}.", segment)?;
5017        }
5018        write!(f, "{}", self.rule)
5019    }
5020}
5021
5022impl fmt::Display for LemmaType {
5023    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5024        write!(f, "{}", self.name())
5025    }
5026}
5027
5028fn decimal_places_in_display_value(decimal: &rust_decimal::Decimal) -> u32 {
5029    if decimal.is_integer() {
5030        return 0;
5031    }
5032    decimal.fract().normalize().scale()
5033}
5034
5035fn format_decimal_for_api(decimal: rust_decimal::Decimal, decimal_places: Option<u8>) -> String {
5036    match decimal_places {
5037        Some(decimal_places) => {
5038            let rounded = decimal.round_dp(u32::from(decimal_places));
5039            format!("{:.prec$}", rounded, prec = decimal_places as usize)
5040        }
5041        None => {
5042            let normalized = decimal.normalize();
5043            if normalized.fract().is_zero() {
5044                normalized.trunc().to_string()
5045            } else {
5046                normalized.to_string()
5047            }
5048        }
5049    }
5050}
5051
5052fn format_decimal_for_human_display(
5053    decimal: rust_decimal::Decimal,
5054    decimal_places: Option<u8>,
5055) -> String {
5056    match decimal_places {
5057        Some(decimal_places) => {
5058            let rounded = decimal.round_dp(u32::from(decimal_places));
5059            format!("{:.prec$}", rounded, prec = decimal_places as usize)
5060        }
5061        None => decimal.normalize().to_string(),
5062    }
5063}
5064
5065fn format_rational_for_human_display(
5066    magnitude: &crate::computation::rational::RationalInteger,
5067    decimal_places: Option<u8>,
5068) -> String {
5069    match magnitude.try_to_decimal() {
5070        Ok(decimal) => format_decimal_for_human_display(decimal, decimal_places),
5071        Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
5072        Err(_) => magnitude.display_str(),
5073    }
5074}
5075
5076fn format_measure_canonical_for_display(
5077    canonical: &crate::computation::rational::RationalInteger,
5078    lemma_type: &LemmaType,
5079    signature: &[(String, i32)],
5080) -> String {
5081    use crate::computation::rational::{checked_div, rational_new};
5082    use rust_decimal::Decimal;
5083
5084    let decimals = lemma_type.decimal_places();
5085
5086    if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
5087        if !units.is_empty() {
5088            if let [(sig_unit, 1)] = signature {
5089                if let Some(unit) = units.iter().find(|u| u.name == *sig_unit) {
5090                    let in_unit = checked_div(canonical, &unit.factor)
5091                        .expect("BUG: de-canonicalization for measure display must not fail");
5092                    let formatted = format_rational_for_human_display(&in_unit, decimals);
5093                    return format!("{} {}", formatted, unit.name);
5094                }
5095            }
5096
5097            struct UnitDisplayCandidate {
5098                unit_name: String,
5099                decimal_places: u32,
5100                under_1000: bool,
5101                decimal_abs: Option<Decimal>,
5102                formatted: String,
5103            }
5104
5105            let thousand = rational_new(1000, 1);
5106            let mut candidates: Vec<UnitDisplayCandidate> = Vec::with_capacity(units.len());
5107            for unit in units.iter() {
5108                let in_unit = checked_div(canonical, &unit.factor)
5109                    .expect("BUG: de-canonicalization for measure display must not fail");
5110                let formatted = format_rational_for_human_display(&in_unit, decimals);
5111                let decimal_abs = in_unit.try_to_decimal().ok().map(|decimal| decimal.abs());
5112                let decimal_places = decimal_abs
5113                    .as_ref()
5114                    .map(decimal_places_in_display_value)
5115                    .unwrap_or(u32::MAX);
5116                let under_1000 = in_unit
5117                    .try_cmp(&thousand)
5118                    .ok()
5119                    .is_some_and(|ordering| ordering == std::cmp::Ordering::Less);
5120                candidates.push(UnitDisplayCandidate {
5121                    unit_name: unit.name.clone(),
5122                    decimal_places,
5123                    under_1000,
5124                    decimal_abs,
5125                    formatted,
5126                });
5127            }
5128
5129            let pool: Vec<&UnitDisplayCandidate> = {
5130                let under: Vec<_> = candidates.iter().filter(|c| c.under_1000).collect();
5131                if under.is_empty() {
5132                    candidates.iter().collect()
5133                } else {
5134                    under
5135                }
5136            };
5137            let best = pool
5138                .iter()
5139                .min_by(|left, right| {
5140                    left.decimal_places
5141                        .cmp(&right.decimal_places)
5142                        .then_with(|| match (left.decimal_abs, right.decimal_abs) {
5143                            (Some(left_abs), Some(right_abs)) => left_abs.cmp(&right_abs),
5144                            (Some(_), None) => std::cmp::Ordering::Less,
5145                            (None, Some(_)) => std::cmp::Ordering::Greater,
5146                            (None, None) => std::cmp::Ordering::Equal,
5147                        })
5148                })
5149                .expect("BUG: measure type must have at least one declared unit");
5150            return format!("{} {}", best.formatted, best.unit_name);
5151        }
5152    }
5153
5154    let unit_label = match signature {
5155        [] => String::new(),
5156        [(name, 1)] => name.clone(),
5157        _ => format_signature_operator_style(signature),
5158    };
5159    let formatted = format_rational_for_human_display(canonical, decimals);
5160    if unit_label.is_empty() {
5161        formatted
5162    } else {
5163        format!("{formatted} {unit_label}")
5164    }
5165}
5166
5167impl fmt::Display for LiteralValue {
5168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5169        match &self.value {
5170            ValueKind::Measure(n, signature) => {
5171                write!(
5172                    f,
5173                    "{}",
5174                    format_measure_canonical_for_display(n, &self.lemma_type, signature)
5175                )
5176            }
5177            ValueKind::Ratio(_, Some(_unit_name)) => write!(f, "{}", self.value),
5178            ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
5179            _ => write!(f, "{}", self.value),
5180        }
5181    }
5182}
5183
5184// -----------------------------------------------------------------------------
5185// Tests
5186// -----------------------------------------------------------------------------
5187
5188#[cfg(test)]
5189pub(crate) mod tests {
5190    use super::*;
5191    use crate::computation::rational::decimal_to_rational;
5192    use crate::literals::DateGranularity;
5193    use crate::literals::Value;
5194    use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
5195    use rust_decimal::Decimal;
5196    use std::str::FromStr;
5197    use std::sync::{Arc, OnceLock};
5198
5199    static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
5200
5201    #[must_use]
5202    pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
5203        PRIMITIVE_MEASURE
5204            .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::measure())))
5205    }
5206
5207    #[must_use]
5208    pub(crate) fn primitive_measure() -> &'static LemmaType {
5209        primitive_measure_arc().as_ref()
5210    }
5211
5212    #[test]
5213    fn default_primitive_help_is_goal_oriented() {
5214        let kinds = [
5215            PrimitiveKind::Boolean,
5216            PrimitiveKind::Measure,
5217            PrimitiveKind::MeasureRange,
5218            PrimitiveKind::Number,
5219            PrimitiveKind::NumberRange,
5220            PrimitiveKind::Ratio,
5221            PrimitiveKind::RatioRange,
5222            PrimitiveKind::Text,
5223            PrimitiveKind::Date,
5224            PrimitiveKind::DateRange,
5225            PrimitiveKind::Time,
5226            PrimitiveKind::TimeRange,
5227        ];
5228        for kind in kinds {
5229            let spec = type_spec_for_primitive(kind);
5230            let help = match &spec {
5231                TypeSpecification::Boolean { help, .. }
5232                | TypeSpecification::Number { help, .. }
5233                | TypeSpecification::NumberRange { help, .. }
5234                | TypeSpecification::Text { help, .. }
5235                | TypeSpecification::Measure { help, .. }
5236                | TypeSpecification::MeasureRange { help, .. }
5237                | TypeSpecification::Ratio { help, .. }
5238                | TypeSpecification::RatioRange { help, .. }
5239                | TypeSpecification::Date { help, .. }
5240                | TypeSpecification::DateRange { help, .. }
5241                | TypeSpecification::TimeRange { help, .. }
5242                | TypeSpecification::Time { help, .. } => help,
5243                TypeSpecification::Veto { .. } | TypeSpecification::Undetermined => {
5244                    unreachable!(
5245                        "BUG: primitive kind {:?} mapped to non-primitive spec",
5246                        kind
5247                    )
5248                }
5249            };
5250            assert!(!help.is_empty(), "help for {:?}", kind);
5251            assert!(
5252                !help.to_ascii_lowercase().contains("format:"),
5253                "help for {:?} must not describe syntax: {:?}",
5254                kind,
5255                help
5256            );
5257            assert_eq!(help, default_help_for_primitive(kind));
5258        }
5259    }
5260
5261    #[test]
5262    fn test_negated_comparison() {
5263        assert_eq!(
5264            negated_comparison(ComparisonComputation::LessThan),
5265            ComparisonComputation::GreaterThanOrEqual
5266        );
5267        assert_eq!(
5268            negated_comparison(ComparisonComputation::GreaterThanOrEqual),
5269            ComparisonComputation::LessThan
5270        );
5271        assert_eq!(
5272            negated_comparison(ComparisonComputation::Is),
5273            ComparisonComputation::IsNot
5274        );
5275        assert_eq!(
5276            negated_comparison(ComparisonComputation::IsNot),
5277            ComparisonComputation::Is
5278        );
5279    }
5280
5281    #[test]
5282    fn value_to_semantic_number_is_decimal() {
5283        let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
5284        assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
5285    }
5286
5287    #[test]
5288    fn value_kind_measure_serializes_with_signature() {
5289        let kind = ValueKind::Measure(
5290            decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap(),
5291            vec![("eur".to_string(), 1)],
5292        );
5293        let json = serde_json::to_value(&kind).unwrap();
5294        assert_eq!(json["measure"]["value"], "99.5");
5295        assert_eq!(json["measure"]["signature"][0][0], "eur");
5296        assert_eq!(json["measure"]["signature"][0][1], 1);
5297    }
5298
5299    #[test]
5300    fn value_kind_measure_compound_signature_roundtrips() {
5301        let original = ValueKind::Measure(
5302            decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap(),
5303            vec![
5304                ("eur".to_string(), 1),
5305                ("hour".to_string(), 1),
5306                ("minute".to_string(), -1),
5307            ],
5308        );
5309        let json = serde_json::to_string(&original).unwrap();
5310        let parsed: ValueKind = serde_json::from_str(&json).unwrap();
5311        assert_eq!(original, parsed);
5312    }
5313
5314    #[test]
5315    fn value_kind_measure_empty_signature_roundtrips() {
5316        let original = ValueKind::Measure(
5317            decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap(),
5318            Vec::new(),
5319        );
5320        let json = serde_json::to_string(&original).unwrap();
5321        let parsed: ValueKind = serde_json::from_str(&json).unwrap();
5322        assert_eq!(original, parsed);
5323    }
5324
5325    #[test]
5326    fn literal_value_number_serde_not_rational_array() {
5327        let lit = LiteralValue::number_from_decimal(Decimal::from(20));
5328        let json = serde_json::to_value(&lit).unwrap();
5329        let number = json
5330            .get("value")
5331            .and_then(|v| v.get("number"))
5332            .expect("number field");
5333        assert!(number.is_string());
5334        assert_eq!(number.as_str(), Some("20"));
5335        assert!(
5336            !number.is_array(),
5337            "stored number must not serialize as [n,d]"
5338        );
5339    }
5340
5341    #[test]
5342    fn test_literal_value_to_primitive_type() {
5343        let one = rational_new(1, 1);
5344
5345        assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
5346        assert_eq!(
5347            LiteralValue::number(one.clone()).lemma_type.name(),
5348            "number"
5349        );
5350        assert_eq!(
5351            LiteralValue::from_bool(bool::from(BooleanValue::True))
5352                .lemma_type
5353                .name(),
5354            "boolean"
5355        );
5356
5357        let dt = DateTimeValue {
5358            year: 2024,
5359            month: 1,
5360            day: 1,
5361            hour: 0,
5362            minute: 0,
5363            second: 0,
5364            microsecond: 0,
5365            timezone: None,
5366
5367            granularity: DateGranularity::Full,
5368        };
5369        assert_eq!(
5370            LiteralValue::date(date_time_to_semantic(&dt))
5371                .lemma_type
5372                .name(),
5373            "date"
5374        );
5375        assert_eq!(
5376            LiteralValue::ratio_from_decimal(Decimal::new(1, 2), Some("percent".to_string()))
5377                .lemma_type
5378                .name(),
5379            "ratio"
5380        );
5381        let dur_type = LemmaType::new(
5382            "duration".to_string(),
5383            TypeSpecification::Measure {
5384                minimum: None,
5385                maximum: None,
5386                decimals: None,
5387                units: MeasureUnits::from(vec![MeasureUnit {
5388                    name: "second".to_string(),
5389                    factor: crate::computation::rational::rational_one(),
5390                    derived_measure_factors: Vec::new(),
5391                    decomposition: BaseMeasureVector::new(),
5392                    minimum: None,
5393                    maximum: None,
5394                    suggestion_magnitude: None,
5395                }]),
5396                traits: vec![MeasureTrait::Duration],
5397                decomposition: None,
5398                help: String::new(),
5399            },
5400            TypeExtends::Primitive,
5401        );
5402        assert_eq!(
5403            LiteralValue::measure_with_type(one.clone(), "second".to_string(), Arc::new(dur_type))
5404                .lemma_type
5405                .name(),
5406            "duration"
5407        );
5408    }
5409
5410    #[test]
5411    fn test_type_display() {
5412        let specs = TypeSpecification::text();
5413        let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
5414        assert_eq!(format!("{}", lemma_type), "name");
5415    }
5416
5417    #[test]
5418    fn test_type_serialization() {
5419        let specs = TypeSpecification::number();
5420        let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
5421        let serialized = serde_json::to_string(&lemma_type).unwrap();
5422        let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
5423        assert_eq!(lemma_type, deserialized);
5424    }
5425
5426    #[test]
5427    fn test_literal_value_display_value() {
5428        let ten = rational_new(10, 1);
5429
5430        assert_eq!(
5431            LiteralValue::text("hello".to_string()).display_value(),
5432            "hello"
5433        );
5434        assert_eq!(LiteralValue::number(ten).display_value(), "10");
5435        assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
5436        assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
5437
5438        // 0.10 ratio with "percent" unit displays as 10% (unit conversion applied)
5439        let ten_percent_ratio =
5440            LiteralValue::ratio_from_decimal(Decimal::new(1, 1), Some("percent".to_string()));
5441        assert_eq!(ten_percent_ratio.display_value(), "10%");
5442
5443        let time = TimeValue {
5444            hour: 14,
5445            minute: 30,
5446            second: 0,
5447            microsecond: 0,
5448            timezone: None,
5449        };
5450        let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
5451        assert!(time_display.contains("14"));
5452        assert!(time_display.contains("30"));
5453    }
5454
5455    #[test]
5456    fn test_measure_display_respects_type_decimals() {
5457        let money_type = LemmaType {
5458            name: Some("money".to_string()),
5459            specifications: TypeSpecification::Measure {
5460                minimum: None,
5461                maximum: None,
5462                decimals: Some(2),
5463                units: MeasureUnits::from(vec![MeasureUnit {
5464                    name: "eur".to_string(),
5465                    factor: crate::computation::rational::rational_one(),
5466                    derived_measure_factors: Vec::new(),
5467                    decomposition: BaseMeasureVector::new(),
5468                    minimum: None,
5469                    maximum: None,
5470                    suggestion_magnitude: None,
5471                }]),
5472                traits: Vec::new(),
5473                decomposition: None,
5474                help: String::new(),
5475            },
5476            extends: TypeExtends::Primitive,
5477        };
5478        let money_type = Arc::new(money_type);
5479        let val = LiteralValue::measure_with_type(
5480            decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap(),
5481            "eur".to_string(),
5482            money_type.clone(),
5483        );
5484        assert_eq!(val.display_value(), "1.80 eur");
5485        let more_precision = LiteralValue::measure_with_type(
5486            decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
5487            "eur".to_string(),
5488            money_type,
5489        );
5490        assert_eq!(more_precision.display_value(), "1.80 eur");
5491        let measure_no_decimals = LemmaType {
5492            name: Some("count".to_string()),
5493            specifications: TypeSpecification::Measure {
5494                minimum: None,
5495                maximum: None,
5496                decimals: None,
5497                units: MeasureUnits::from(vec![MeasureUnit {
5498                    name: "items".to_string(),
5499                    factor: crate::computation::rational::rational_one(),
5500                    derived_measure_factors: Vec::new(),
5501                    decomposition: BaseMeasureVector::new(),
5502                    minimum: None,
5503                    maximum: None,
5504                    suggestion_magnitude: None,
5505                }]),
5506                traits: Vec::new(),
5507                decomposition: None,
5508                help: String::new(),
5509            },
5510            extends: TypeExtends::Primitive,
5511        };
5512        let val_any = LiteralValue::measure_with_type(
5513            decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
5514            "items".to_string(),
5515            Arc::new(measure_no_decimals),
5516        );
5517        assert_eq!(val_any.display_value(), "42.5 items");
5518    }
5519
5520    #[test]
5521    fn test_literal_value_time_type() {
5522        let time = TimeValue {
5523            hour: 14,
5524            minute: 30,
5525            second: 0,
5526            microsecond: 0,
5527            timezone: None,
5528        };
5529        let lit = LiteralValue::time(time_to_semantic(&time));
5530        assert_eq!(lit.lemma_type.name(), "time");
5531    }
5532
5533    #[test]
5534    fn test_measure_family_name_primitive_root() {
5535        let measure_spec = TypeSpecification::measure();
5536        let money_primitive = LemmaType::new(
5537            "money".to_string(),
5538            measure_spec.clone(),
5539            TypeExtends::Primitive,
5540        );
5541        assert_eq!(money_primitive.measure_family_name(), Some("money"));
5542    }
5543
5544    #[test]
5545    fn test_measure_family_name_custom() {
5546        let measure_spec = TypeSpecification::measure();
5547        let money_custom = LemmaType::new(
5548            "money".to_string(),
5549            measure_spec,
5550            TypeExtends::custom_local("money".to_string(), "money".to_string()),
5551        );
5552        assert_eq!(money_custom.measure_family_name(), Some("money"));
5553    }
5554
5555    #[test]
5556    fn test_same_measure_family_same_name_different_extends() {
5557        let measure_spec = TypeSpecification::measure();
5558        let money_primitive = LemmaType::new(
5559            "money".to_string(),
5560            measure_spec.clone(),
5561            TypeExtends::Primitive,
5562        );
5563        let money_custom = LemmaType::new(
5564            "money".to_string(),
5565            measure_spec,
5566            TypeExtends::custom_local("money".to_string(), "money".to_string()),
5567        );
5568        assert!(money_primitive.same_measure_family(&money_custom));
5569        assert!(money_custom.same_measure_family(&money_primitive));
5570    }
5571
5572    #[test]
5573    fn test_same_measure_family_parent_and_child() {
5574        let measure_spec = TypeSpecification::measure();
5575        let type_x = LemmaType::new(
5576            "x".to_string(),
5577            measure_spec.clone(),
5578            TypeExtends::Primitive,
5579        );
5580        let type_x2 = LemmaType::new(
5581            "x2".to_string(),
5582            measure_spec,
5583            TypeExtends::custom_local("x".to_string(), "x".to_string()),
5584        );
5585        assert_eq!(type_x.measure_family_name(), Some("x"));
5586        assert_eq!(type_x2.measure_family_name(), Some("x"));
5587        assert!(type_x.same_measure_family(&type_x2));
5588        assert!(type_x2.same_measure_family(&type_x));
5589    }
5590
5591    #[test]
5592    fn test_same_measure_family_siblings() {
5593        let measure_spec = TypeSpecification::measure();
5594        let type_x2_a = LemmaType::new(
5595            "x2a".to_string(),
5596            measure_spec.clone(),
5597            TypeExtends::custom_local("x".to_string(), "x".to_string()),
5598        );
5599        let type_x2_b = LemmaType::new(
5600            "x2b".to_string(),
5601            measure_spec,
5602            TypeExtends::custom_local("x".to_string(), "x".to_string()),
5603        );
5604        assert!(type_x2_a.same_measure_family(&type_x2_b));
5605    }
5606
5607    #[test]
5608    fn test_same_measure_family_different_families() {
5609        let measure_spec = TypeSpecification::measure();
5610        let money = LemmaType::new(
5611            "money".to_string(),
5612            measure_spec.clone(),
5613            TypeExtends::Primitive,
5614        );
5615        let temperature = LemmaType::new(
5616            "temperature".to_string(),
5617            measure_spec,
5618            TypeExtends::Primitive,
5619        );
5620        assert!(!money.same_measure_family(&temperature));
5621        assert!(!temperature.same_measure_family(&money));
5622    }
5623
5624    #[test]
5625    fn test_same_measure_family_measure_vs_non_measure() {
5626        let measure_spec = TypeSpecification::measure();
5627        let number_spec = TypeSpecification::number();
5628        let measure_type =
5629            LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
5630        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5631        assert!(!measure_type.same_measure_family(&number_type));
5632        assert!(!number_type.same_measure_family(&measure_type));
5633    }
5634
5635    #[test]
5636    fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
5637        let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
5638        let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
5639
5640        assert!(!left.same_measure_family(&right));
5641        assert!(left.compatible_with_anonymous_measure(&right));
5642    }
5643
5644    #[test]
5645    fn test_measure_family_name_non_measure_returns_none() {
5646        let number_spec = TypeSpecification::number();
5647        let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5648        assert_eq!(number_type.measure_family_name(), None);
5649    }
5650
5651    #[test]
5652    fn test_lemma_type_inequality_local_vs_import_same_shape() {
5653        let measure_spec = TypeSpecification::measure();
5654        let local = LemmaType::new(
5655            "t".to_string(),
5656            measure_spec.clone(),
5657            TypeExtends::custom_local("money".to_string(), "money".to_string()),
5658        );
5659        let imported = LemmaType::new(
5660            "t".to_string(),
5661            measure_spec,
5662            TypeExtends::Custom {
5663                parent: "money".to_string(),
5664                family: "money".to_string(),
5665                defining_spec: TypeDefiningSpec::Import,
5666            },
5667        );
5668        assert_ne!(local, imported);
5669    }
5670
5671    #[test]
5672    fn test_lemma_type_equality_import_unit_variant() {
5673        let measure_spec = TypeSpecification::measure();
5674        let left = LemmaType::new(
5675            "t".to_string(),
5676            measure_spec.clone(),
5677            TypeExtends::Custom {
5678                parent: "money".to_string(),
5679                family: "money".to_string(),
5680                defining_spec: TypeDefiningSpec::Import,
5681            },
5682        );
5683        let right = LemmaType::new(
5684            "t".to_string(),
5685            measure_spec,
5686            TypeExtends::Custom {
5687                parent: "money".to_string(),
5688                family: "money".to_string(),
5689                defining_spec: TypeDefiningSpec::Import,
5690            },
5691        );
5692        assert_eq!(left, right);
5693    }
5694
5695    fn month_suggestion_arg() -> CommandArg {
5696        CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5697            Decimal::ONE,
5698            "month".to_string(),
5699        ))
5700    }
5701
5702    fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
5703        [
5704            CommandArg::Label(name.to_string()),
5705            CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
5706        ]
5707    }
5708
5709    #[test]
5710    fn default_calendar_on_text_reports_hint() {
5711        let mut specs = TypeSpecification::text();
5712        let mut default = None;
5713        let err = specs
5714            .apply_constraint(
5715                "notes",
5716                TypeConstraintCommand::Suggest,
5717                &[month_suggestion_arg()],
5718                &mut default,
5719            )
5720            .unwrap_err();
5721        assert!(err.contains("Unit 'month' is for calendar data"));
5722        assert!(err.contains("double quotes"));
5723    }
5724
5725    #[test]
5726    fn default_calendar_on_duration_reports_valid_units() {
5727        let mut specs = TypeSpecification::measure();
5728        specs
5729            .apply_constraint(
5730                "duration",
5731                TypeConstraintCommand::Unit,
5732                &unit_factor_arg("second", 1),
5733                &mut None,
5734            )
5735            .unwrap();
5736        specs
5737            .apply_constraint(
5738                "duration",
5739                TypeConstraintCommand::Unit,
5740                &unit_factor_arg("week", 604_800),
5741                &mut None,
5742            )
5743            .unwrap();
5744        specs
5745            .apply_constraint(
5746                "duration",
5747                TypeConstraintCommand::Trait,
5748                &[CommandArg::Label("duration".to_string())],
5749                &mut None,
5750            )
5751            .unwrap();
5752        let mut default = None;
5753        let err = specs
5754            .apply_constraint(
5755                "duration",
5756                TypeConstraintCommand::Suggest,
5757                &[month_suggestion_arg()],
5758                &mut default,
5759            )
5760            .unwrap_err();
5761        assert!(err.contains("Unit 'month' is for calendar data"));
5762        assert!(err.contains("Valid 'duration' units are"));
5763        assert!(err.contains("week"));
5764    }
5765
5766    #[test]
5767    fn default_valid_duration_weeks_accepted() {
5768        let mut specs = TypeSpecification::measure();
5769        specs
5770            .apply_constraint(
5771                "duration",
5772                TypeConstraintCommand::Unit,
5773                &unit_factor_arg("second", 1),
5774                &mut None,
5775            )
5776            .unwrap();
5777        specs
5778            .apply_constraint(
5779                "duration",
5780                TypeConstraintCommand::Unit,
5781                &unit_factor_arg("week", 604_800),
5782                &mut None,
5783            )
5784            .unwrap();
5785        specs
5786            .apply_constraint(
5787                "duration",
5788                TypeConstraintCommand::Trait,
5789                &[CommandArg::Label("duration".to_string())],
5790                &mut None,
5791            )
5792            .unwrap();
5793        let mut default = None;
5794        specs
5795            .apply_constraint(
5796                "duration",
5797                TypeConstraintCommand::Suggest,
5798                &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5799                    Decimal::from(4),
5800                    "week".to_string(),
5801                ))],
5802                &mut default,
5803            )
5804            .unwrap();
5805        assert!(matches!(
5806            default,
5807            Some(RawSuggestion::Measure {
5808                unit_name,
5809                ..
5810            }) if unit_name == "week"
5811        ));
5812    }
5813
5814    #[test]
5815    fn default_unknown_unit_on_duration_lists_valid_units() {
5816        let mut specs = TypeSpecification::measure();
5817        specs
5818            .apply_constraint(
5819                "duration",
5820                TypeConstraintCommand::Unit,
5821                &unit_factor_arg("second", 1),
5822                &mut None,
5823            )
5824            .unwrap();
5825        specs
5826            .apply_constraint(
5827                "duration",
5828                TypeConstraintCommand::Trait,
5829                &[CommandArg::Label("duration".to_string())],
5830                &mut None,
5831            )
5832            .unwrap();
5833        let mut default = None;
5834        let err = specs
5835            .apply_constraint(
5836                "duration",
5837                TypeConstraintCommand::Suggest,
5838                &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5839                    Decimal::ONE,
5840                    "fortnight".to_string(),
5841                ))],
5842                &mut default,
5843            )
5844            .unwrap_err();
5845        assert!(err.contains("fortnight"));
5846        assert!(err.contains("not defined on 'duration'"));
5847        assert!(err.contains("Valid units are"));
5848    }
5849
5850    fn money_measure_type() -> LemmaType {
5851        LemmaType::new(
5852            "Money".to_string(),
5853            TypeSpecification::Measure {
5854                minimum: None,
5855                maximum: None,
5856                decimals: None,
5857                units: MeasureUnits::from(vec![
5858                    MeasureUnit {
5859                        name: "eur".to_string(),
5860                        factor: crate::computation::rational::rational_one(),
5861                        derived_measure_factors: Vec::new(),
5862                        decomposition: BaseMeasureVector::new(),
5863                        minimum: None,
5864                        maximum: None,
5865                        suggestion_magnitude: None,
5866                    },
5867                    MeasureUnit {
5868                        name: "usd".to_string(),
5869                        factor: crate::computation::rational::decimal_to_rational(Decimal::new(
5870                            91, 2,
5871                        ))
5872                        .expect("factor"),
5873                        derived_measure_factors: Vec::new(),
5874                        decomposition: BaseMeasureVector::new(),
5875                        minimum: None,
5876                        maximum: None,
5877                        suggestion_magnitude: None,
5878                    },
5879                ]),
5880                traits: Vec::new(),
5881                decomposition: None,
5882                help: String::new(),
5883            },
5884            TypeExtends::Primitive,
5885        )
5886    }
5887
5888    #[test]
5889    fn measure_unit_names_for_named_measure() {
5890        let money = money_measure_type();
5891        assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
5892    }
5893
5894    // ---------------------------------------------------------------------------
5895    // Phase 0 — pin combine_signatures and canonicalize_signature behavior
5896    // ---------------------------------------------------------------------------
5897
5898    fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
5899        pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
5900    }
5901
5902    #[test]
5903    fn combine_signatures_multiply_adds_exponents() {
5904        let left = sig(&[("eur", 1)]);
5905        let right = sig(&[("hour", -1)]);
5906        let result = combine_signatures(&left, &right, true);
5907        assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5908    }
5909
5910    #[test]
5911    fn combine_signatures_divide_subtracts_exponents() {
5912        let left = sig(&[("eur", 1)]);
5913        let right = sig(&[("hour", 1)]);
5914        let result = combine_signatures(&left, &right, false);
5915        assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5916    }
5917
5918    #[test]
5919    fn combine_signatures_cancels_to_empty() {
5920        let left = sig(&[("ce", 1), ("minute", -1)]);
5921        let right = sig(&[("minute", 1)]);
5922        let result = combine_signatures(&left, &right, true);
5923        // ce * (ce/min * min) = ce; minute cancels
5924        assert_eq!(result, sig(&[("ce", 1)]));
5925    }
5926
5927    #[test]
5928    fn combine_signatures_output_is_canonical_form() {
5929        let left = sig(&[("eur", 1), ("hour", 1)]);
5930        let right = sig(&[("minute", 1)]);
5931        let result = combine_signatures(&left, &right, false); // divide
5932                                                               // [("eur",1),("hour",1)] / [("minute",1)] = [("eur",1),("hour",1),("minute",-1)]
5933        let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
5934        assert_eq!(result, expected);
5935    }
5936
5937    #[test]
5938    fn canonicalize_signature_drops_zero_exponents() {
5939        let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
5940        let result = canonicalize_signature(&sig_with_zero);
5941        assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5942    }
5943
5944    #[test]
5945    fn canonicalize_signature_sorts_by_name() {
5946        let unsorted = sig(&[("minute", -1), ("eur", 1)]);
5947        let result = canonicalize_signature(&unsorted);
5948        assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5949    }
5950
5951    // ---------------------------------------------------------------------------
5952    // Phase 0 — format_signature_operator_style (to be implemented in
5953    // signature_factor_and_display todo)
5954    // ---------------------------------------------------------------------------
5955
5956    #[test]
5957    fn format_signature_operator_style_numerator_only() {
5958        let signature = sig(&[("eur", 1)]);
5959        let result = format_signature_operator_style(&signature);
5960        assert_eq!(result, "eur");
5961    }
5962
5963    #[test]
5964    fn format_signature_operator_style_with_denominator() {
5965        let signature = sig(&[("eur", 1), ("hour", -1)]);
5966        let result = format_signature_operator_style(&signature);
5967        assert_eq!(result, "eur/hour");
5968    }
5969
5970    #[test]
5971    fn format_signature_operator_style_denominator_only() {
5972        let signature = sig(&[("meter", -1)]);
5973        let result = format_signature_operator_style(&signature);
5974        assert_eq!(result, "1/meter");
5975    }
5976
5977    #[test]
5978    fn format_signature_operator_style_with_exponents() {
5979        let signature = sig(&[("meter", 2), ("second", -2)]);
5980        let result = format_signature_operator_style(&signature);
5981        assert_eq!(result, "meter^2/second^2");
5982    }
5983
5984    // ---------------------------------------------------------------------------
5985    // Phase 0 — calendar_unit_factor (to be implemented in builtin_calendar_factor_table)
5986    // ---------------------------------------------------------------------------
5987
5988    #[test]
5989    fn calendar_unit_factor_table_completeness() {
5990        // Every SemanticCalendarUnit Display string must resolve to a factor.
5991        // Today SemanticCalendarUnit only has Month and Year; more may be added.
5992        for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
5993            let name = unit.to_string();
5994            assert!(
5995                calendar_unit_factor(&name).is_some(),
5996                "calendar_unit_factor('{}') must return Some",
5997                name
5998            );
5999        }
6000    }
6001
6002    #[test]
6003    fn semantic_calendar_unit_display_returns_singular() {
6004        // Today Month => "month", Year => "year" (plural).
6005        // After singular_calendar_names_everywhere, must be "month" and "year".
6006        assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
6007        assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
6008    }
6009
6010    // ---------------------------------------------------------------------------
6011    // Phase 0 — signature_factor (to be implemented in signature_factor_and_display)
6012    // ---------------------------------------------------------------------------
6013
6014    #[test]
6015    fn signature_factor_with_calendar_units() {
6016        let calendar = test_calendar_type_for_signature_factor();
6017        let unit_index = crate::planning::unit_index::UnitIndex::new();
6018        // month factor = 1, year factor = 12.
6019        // [(month,1),(year,-1)] = 1/12
6020        let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
6021        let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
6022            .expect("must not overflow");
6023        let expected = rational_new(1, 12);
6024        assert_eq!(factor, expected, "month/year factor must be 1/12");
6025    }
6026
6027    fn test_calendar_type_for_signature_factor() -> LemmaType {
6028        use crate::computation::rational::{decimal_to_rational, rational_one};
6029        use crate::literals::{MeasureUnit, MeasureUnits};
6030        use rust_decimal::Decimal;
6031        LemmaType::new(
6032            "calendar".to_string(),
6033            TypeSpecification::Measure {
6034                minimum: None,
6035                maximum: None,
6036                decimals: None,
6037                units: MeasureUnits::from(vec![
6038                    MeasureUnit {
6039                        name: "month".to_string(),
6040                        factor: rational_one(),
6041                        minimum: None,
6042                        maximum: None,
6043                        suggestion_magnitude: None,
6044                        decomposition: calendar_decomposition(),
6045                        derived_measure_factors: Vec::new(),
6046                    },
6047                    MeasureUnit {
6048                        name: "year".to_string(),
6049                        factor: decimal_to_rational(Decimal::from(12)).expect("year factor"),
6050                        minimum: None,
6051                        maximum: None,
6052                        suggestion_magnitude: None,
6053                        decomposition: calendar_decomposition(),
6054                        derived_measure_factors: Vec::new(),
6055                    },
6056                ]),
6057                traits: vec![MeasureTrait::Calendar],
6058                decomposition: Some(calendar_decomposition()),
6059                help: String::new(),
6060            },
6061            TypeExtends::Primitive,
6062        )
6063    }
6064
6065    #[test]
6066    #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
6067    fn signature_factor_panics_on_unresolved_name() {
6068        let unit_index = crate::planning::unit_index::UnitIndex::new();
6069        let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
6070        let _ = signature_factor(&bad_sig, &unit_index, None);
6071    }
6072
6073    #[test]
6074    fn signature_factor_uses_owner_when_expression_index_empty() {
6075        let money = test_money_type_for_signature_factor();
6076        let expression_units = crate::planning::unit_index::UnitIndex::new();
6077        let sig_usd = sig(&[("usd", 1)]);
6078        let factor =
6079            signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
6080        assert_eq!(factor, rational_new(91, 100));
6081    }
6082
6083    fn test_money_type_for_signature_factor() -> LemmaType {
6084        use crate::computation::rational::decimal_to_rational;
6085        use crate::literals::{MeasureUnit, MeasureUnits};
6086        use rust_decimal::Decimal;
6087        LemmaType::new(
6088            "money".to_string(),
6089            TypeSpecification::Measure {
6090                minimum: None,
6091                maximum: None,
6092                decimals: Some(2),
6093                units: MeasureUnits::from(vec![
6094                    MeasureUnit {
6095                        name: "eur".to_string(),
6096                        factor: crate::computation::rational::rational_one(),
6097                        minimum: None,
6098                        maximum: None,
6099                        suggestion_magnitude: None,
6100                        decomposition: BaseMeasureVector::new(),
6101                        derived_measure_factors: Vec::new(),
6102                    },
6103                    MeasureUnit {
6104                        name: "usd".to_string(),
6105                        factor: decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
6106                        minimum: None,
6107                        maximum: None,
6108                        suggestion_magnitude: None,
6109                        decomposition: BaseMeasureVector::new(),
6110                        derived_measure_factors: Vec::new(),
6111                    },
6112                ]),
6113                traits: Vec::new(),
6114                decomposition: None,
6115                help: String::new(),
6116            },
6117            TypeExtends::Primitive,
6118        )
6119    }
6120
6121    fn measure_type_with_kilogram() -> TypeSpecification {
6122        use crate::computation::rational::rational_one;
6123        use crate::literals::{MeasureUnit, MeasureUnits};
6124        let mut units = MeasureUnits::new();
6125        units.push(MeasureUnit {
6126            name: "kilogram".to_string(),
6127            factor: rational_one(),
6128            minimum: None,
6129            maximum: None,
6130            suggestion_magnitude: None,
6131            decomposition: BaseMeasureVector::new(),
6132            derived_measure_factors: Vec::new(),
6133        });
6134        TypeSpecification::Measure {
6135            minimum: None,
6136            maximum: None,
6137            decimals: None,
6138            units,
6139            traits: Vec::new(),
6140            decomposition: None,
6141            help: String::new(),
6142        }
6143    }
6144
6145    #[test]
6146    fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
6147        let ten = Value::Number(Decimal::from(10));
6148        let err = parser_value_to_value_kind(&ten, &measure_type_with_kilogram())
6149            .expect_err("bare number must not bind to measure");
6150        assert!(
6151            err.contains("kilogram"),
6152            "error must hint expected unit, got: {err}"
6153        );
6154    }
6155
6156    #[test]
6157    fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
6158        let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
6159        let kind = parser_value_to_value_kind(&ten_kg, &measure_type_with_kilogram())
6160            .expect("10 kilogram must bind to measure");
6161        assert!(matches!(kind, ValueKind::Measure(_, _)));
6162    }
6163
6164    #[test]
6165    fn parser_value_to_value_kind_accepts_bare_number_for_ratio() {
6166        let ten = Value::Number(Decimal::from(10));
6167        let kind =
6168            parser_value_to_value_kind(&ten, &TypeSpecification::ratio()).expect("number -> ratio");
6169        assert!(matches!(kind, ValueKind::Ratio(_, None)));
6170    }
6171
6172    #[test]
6173    fn value_kind_matches_spec_rejects_number_for_measure() {
6174        let n = ValueKind::Number(rational_new(10, 1));
6175        assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
6176    }
6177
6178    #[test]
6179    fn apply_constraint_rejects_inherited_unit_factor_change() {
6180        let mut specs = TypeSpecification::measure();
6181        specs
6182            .apply_constraint(
6183                "money",
6184                TypeConstraintCommand::Unit,
6185                &unit_factor_arg("eur", 1),
6186                &mut None,
6187            )
6188            .expect("seed eur");
6189        let err = specs
6190            .apply_constraint(
6191                "money",
6192                TypeConstraintCommand::Unit,
6193                &[
6194                    CommandArg::Label("eur".to_string()),
6195                    CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
6196                ],
6197                &mut None,
6198            )
6199            .expect_err("must not change inherited unit factor");
6200        assert!(err.contains("eur"), "error must name unit, got: {err}");
6201        assert!(
6202            err.contains("inherited") || err.contains("cannot change"),
6203            "error must reject factor change, got: {err}"
6204        );
6205    }
6206
6207    #[test]
6208    fn apply_constraint_allows_additive_unit_on_inherited_spec() {
6209        let mut specs = TypeSpecification::measure();
6210        specs
6211            .apply_constraint(
6212                "money",
6213                TypeConstraintCommand::Unit,
6214                &unit_factor_arg("eur", 1),
6215                &mut None,
6216            )
6217            .expect("seed eur");
6218        specs
6219            .apply_constraint(
6220                "money",
6221                TypeConstraintCommand::Unit,
6222                &unit_factor_arg("usd", 1),
6223                &mut None,
6224            )
6225            .expect("add usd");
6226        match &specs {
6227            TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
6228            other => panic!("expected Measure, got {other:?}"),
6229        }
6230    }
6231
6232    #[test]
6233    fn apply_constraint_idempotent_inherited_unit_redeclare() {
6234        let mut specs = TypeSpecification::measure();
6235        specs
6236            .apply_constraint(
6237                "money",
6238                TypeConstraintCommand::Unit,
6239                &unit_factor_arg("eur", 1),
6240                &mut None,
6241            )
6242            .expect("seed eur");
6243        specs
6244            .apply_constraint(
6245                "money",
6246                TypeConstraintCommand::Unit,
6247                &unit_factor_arg("eur", 1),
6248                &mut None,
6249            )
6250            .expect("idempotent eur");
6251        match &specs {
6252            TypeSpecification::Measure { units, .. } => {
6253                assert_eq!(units.len(), 1);
6254                assert_eq!(
6255                    units.iter().find(|u| u.name == "eur").expect("eur").factor,
6256                    crate::computation::rational::rational_one()
6257                );
6258            }
6259            other => panic!("expected Measure, got {other:?}"),
6260        }
6261    }
6262
6263    #[test]
6264    fn element_from_range_returns_element_for_every_range_primitive() {
6265        type RangeElementMatcher = fn(&TypeSpecification) -> bool;
6266        let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
6267            (PrimitiveKind::NumberRange, |element| {
6268                matches!(element, TypeSpecification::Number { .. })
6269            }),
6270            (PrimitiveKind::MeasureRange, |element| {
6271                matches!(element, TypeSpecification::Measure { .. })
6272            }),
6273            (PrimitiveKind::RatioRange, |element| {
6274                matches!(element, TypeSpecification::Ratio { .. })
6275            }),
6276            (PrimitiveKind::DateRange, |element| {
6277                matches!(element, TypeSpecification::Date { .. })
6278            }),
6279            (PrimitiveKind::TimeRange, |element| {
6280                matches!(element, TypeSpecification::Time { .. })
6281            }),
6282        ];
6283        for (kind, matches_element) in cases {
6284            let range_spec = type_spec_for_primitive(kind);
6285            let element = range_spec
6286                .element_from_range()
6287                .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
6288            assert!(
6289                matches_element(&element),
6290                "{kind:?} element must match documented mapping, got {element:?}"
6291            );
6292        }
6293    }
6294
6295    #[test]
6296    fn element_from_range_returns_none_for_non_range_primitives() {
6297        let non_range = [
6298            type_spec_for_primitive(PrimitiveKind::Boolean),
6299            type_spec_for_primitive(PrimitiveKind::Measure),
6300            TypeSpecification::Undetermined,
6301            TypeSpecification::veto(),
6302        ];
6303        for spec in non_range {
6304            assert!(
6305                spec.element_from_range().is_none(),
6306                "{spec:?} must not define element_from_range"
6307            );
6308        }
6309    }
6310}